forked from MapComplete/MapComplete
		
	Add a search bar to filter many mappings
This commit is contained in:
		
							parent
							
								
									9384267b74
								
							
						
					
					
						commit
						e34e39d20f
					
				
					 3 changed files with 130 additions and 61 deletions
				
			
		| 
						 | 
				
			
			@ -1,34 +1,38 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import { Translation } from "../../i18n/Translation";
 | 
			
		||||
  import SpecialTranslation from "./SpecialTranslation.svelte";
 | 
			
		||||
  import type { SpecialVisualizationState } from "../../SpecialVisualization";
 | 
			
		||||
  import type { Feature } from "geojson";
 | 
			
		||||
  import { UIEventSource } from "../../../Logic/UIEventSource";
 | 
			
		||||
  import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
 | 
			
		||||
    import {Translation} from "../../i18n/Translation";
 | 
			
		||||
    import SpecialTranslation from "./SpecialTranslation.svelte";
 | 
			
		||||
    import type {SpecialVisualizationState} from "../../SpecialVisualization";
 | 
			
		||||
    import type {Feature} from "geojson";
 | 
			
		||||
    import {Store, UIEventSource} from "../../../Logic/UIEventSource";
 | 
			
		||||
    import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
 | 
			
		||||
    import Locale from "../../i18n/Locale";
 | 
			
		||||
    import {onDestroy} from "svelte";
 | 
			
		||||
 | 
			
		||||
  export let selectedElement: Feature
 | 
			
		||||
  export let tags: UIEventSource<Record<string, string>>;
 | 
			
		||||
  export let state: SpecialVisualizationState
 | 
			
		||||
  export let layer: LayerConfig
 | 
			
		||||
  export let mapping: {
 | 
			
		||||
    then: Translation; icon?: string; iconClass?: | "small"
 | 
			
		||||
      | "medium"
 | 
			
		||||
      | "large"
 | 
			
		||||
      | "small-height"
 | 
			
		||||
      | "medium-height"
 | 
			
		||||
      | "large-height"
 | 
			
		||||
  };
 | 
			
		||||
  let iconclass = "mapping-icon-" + mapping.iconClass;
 | 
			
		||||
    export let selectedElement: Feature
 | 
			
		||||
    export let tags: UIEventSource<Record<string, string>>;
 | 
			
		||||
    export let state: SpecialVisualizationState
 | 
			
		||||
    export let layer: LayerConfig
 | 
			
		||||
 | 
			
		||||
    export let mapping: {
 | 
			
		||||
        readonly then: Translation;
 | 
			
		||||
        readonly searchTerms?: Record<string, string[]>
 | 
			
		||||
        readonly icon?: string;
 | 
			
		||||
        readonly iconClass?: | "small"
 | 
			
		||||
            | "medium"
 | 
			
		||||
            | "large"
 | 
			
		||||
            | "small-height"
 | 
			
		||||
            | "medium-height"
 | 
			
		||||
            | "large-height"
 | 
			
		||||
    };
 | 
			
		||||
    let iconclass = "mapping-icon-" + mapping.iconClass;
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if mapping.icon !== undefined}
 | 
			
		||||
  <div class="inline-flex">
 | 
			
		||||
    <img class={iconclass+" mr-1"} src={mapping.icon}>
 | 
			
		||||
    <SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
 | 
			
		||||
  </div>
 | 
			
		||||
    <div class="inline-flex">
 | 
			
		||||
        <img class={iconclass+" mr-1"} src={mapping.icon}>
 | 
			
		||||
        <SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
 | 
			
		||||
    </div>
 | 
			
		||||
{:else if mapping.then !== undefined}
 | 
			
		||||
  <SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
 | 
			
		||||
    <SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										76
									
								
								UI/Popup/TagRendering/TagRenderingMappingInput.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								UI/Popup/TagRendering/TagRenderingMappingInput.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
<script lang="ts">/**
 | 
			
		||||
 * A thin wrapper around 'TagRenderingMapping'.
 | 
			
		||||
 * As extra, it contains:
 | 
			
		||||
 * - a slot to place an input element (such as a radio or checkbox)
 | 
			
		||||
 * - It'll hide the mapping if the searchterm does not match
 | 
			
		||||
 */
 | 
			
		||||
import type {Feature} from "geojson";
 | 
			
		||||
import {ImmutableStore, Store, UIEventSource} from "../../../Logic/UIEventSource";
 | 
			
		||||
import type {SpecialVisualizationState} from "../../SpecialVisualization";
 | 
			
		||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
 | 
			
		||||
import Locale from "../../i18n/Locale";
 | 
			
		||||
import type {Mapping} from "../../../Models/ThemeConfig/TagRenderingConfig";
 | 
			
		||||
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
 | 
			
		||||
import {onDestroy} from "svelte";
 | 
			
		||||
import TagRenderingMapping from "./TagRenderingMapping.svelte";
 | 
			
		||||
 | 
			
		||||
export let selectedElement: Feature
 | 
			
		||||
export let tags: UIEventSource<Record<string, string>>;
 | 
			
		||||
export let state: SpecialVisualizationState
 | 
			
		||||
export let layer: LayerConfig
 | 
			
		||||
 | 
			
		||||
export let mapping: Mapping
 | 
			
		||||
/**
 | 
			
		||||
 * If the mapping is selected, it should always be shown
 | 
			
		||||
 */
 | 
			
		||||
export let mappingIsSelected: boolean
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * If there are many mappings, we might hide it.
 | 
			
		||||
 * This is the searchterm where it might hide
 | 
			
		||||
 */
 | 
			
		||||
export let searchTerm: undefined | UIEventSource<string>
 | 
			
		||||
let matchesTerm: Store<boolean> | undefined = searchTerm?.map(search => {
 | 
			
		||||
    if (!search) {
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
    if(mappingIsSelected){
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
    search = search.toLowerCase()
 | 
			
		||||
    // There is a searchterm - this might hide the mapping
 | 
			
		||||
    if (mapping.priorityIf?.matchesProperties(tags.data)) {
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
    if (mapping.then.txt.toLowerCase().indexOf(search) >= 0) {
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
    const searchTerms = mapping?.searchTerms[Locale.language.data]
 | 
			
		||||
    if (searchTerms?.some(t => t.toLowerCase().indexOf(search) >= 0)) {
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
    return false
 | 
			
		||||
}, [], onDestroy) ?? new ImmutableStore(true)
 | 
			
		||||
 | 
			
		||||
let mappingIsHidden: Store<boolean> = tags.map(tags => {
 | 
			
		||||
    if (mapping.hideInAnswer === undefined || mapping.hideInAnswer === false) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    if (mapping.hideInAnswer === true) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (<TagsFilter>mapping.hideInAnswer).matchesProperties(tags);
 | 
			
		||||
}, [], onDestroy)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $matchesTerm && !$mappingIsHidden }
 | 
			
		||||
 | 
			
		||||
    <label class="flex">
 | 
			
		||||
        <slot/>
 | 
			
		||||
        <TagRenderingMapping {mapping} {tags} {state} {selectedElement}
 | 
			
		||||
                             {layer}></TagRenderingMapping>
 | 
			
		||||
    </label>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,8 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
    import {UIEventSource} from "../../../Logic/UIEventSource";
 | 
			
		||||
    import {Store, UIEventSource} from "../../../Logic/UIEventSource";
 | 
			
		||||
    import type {SpecialVisualizationState} from "../../SpecialVisualization";
 | 
			
		||||
    import Tr from "../../Base/Tr.svelte";
 | 
			
		||||
    import If from "../../Base/If.svelte";
 | 
			
		||||
    import TagRenderingMapping from "./TagRenderingMapping.svelte";
 | 
			
		||||
    import type {Feature} from "geojson";
 | 
			
		||||
    import type {Mapping} from "../../../Models/ThemeConfig/TagRenderingConfig";
 | 
			
		||||
    import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +18,7 @@
 | 
			
		|||
    import LoginToggle from "../../Base/LoginToggle.svelte";
 | 
			
		||||
    import SubtleButton from "../../Base/SubtleButton.svelte";
 | 
			
		||||
    import Loading from "../../Base/Loading.svelte";
 | 
			
		||||
    import type {Writable} from "svelte/store";
 | 
			
		||||
    import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte";
 | 
			
		||||
 | 
			
		||||
    export let config: TagRenderingConfig;
 | 
			
		||||
    export let tags: UIEventSource<Record<string, string>>;
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +37,7 @@
 | 
			
		|||
    let selectedMapping: number = undefined;
 | 
			
		||||
    let checkedMappings: boolean[];
 | 
			
		||||
    $: {
 | 
			
		||||
        mappings = config.mappings
 | 
			
		||||
        // We received a new config -> reinit
 | 
			
		||||
        console.log("Initing checkedMappings for", config)
 | 
			
		||||
        if (config.mappings?.length > 0 && (checkedMappings === undefined || checkedMappings?.length < config.mappings.length)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -47,25 +47,16 @@
 | 
			
		|||
    }
 | 
			
		||||
    let selectedTags: TagsFilter = undefined;
 | 
			
		||||
 | 
			
		||||
    function mappingIsHidden(mapping: Mapping): boolean {
 | 
			
		||||
        if (mapping.hideInAnswer === undefined || mapping.hideInAnswer === false) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (mapping.hideInAnswer === true) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return (<TagsFilter>mapping.hideInAnswer).matchesProperties(tags.data);
 | 
			
		||||
 | 
			
		||||
    let mappings: Mapping[] = config?.mappings;
 | 
			
		||||
    let searchTerm: Store<string> = new UIEventSource("")
 | 
			
		||||
    $:{
 | 
			
		||||
        console.log("Seachterm:", $searchTerm)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mappings: Mapping[];
 | 
			
		||||
    let searchTerm: Writable<string> = new UIEventSource("")
 | 
			
		||||
    $:{console.log("Seachterm:", $searchTerm)}
 | 
			
		||||
    
 | 
			
		||||
    $: {
 | 
			
		||||
        mappings = config.mappings?.filter(m => !mappingIsHidden(m));
 | 
			
		||||
        try {
 | 
			
		||||
            let freeformInputValue = $freeformInput
 | 
			
		||||
            selectedTags = config?.constructChangeSpecification(freeformInputValue, selectedMapping, checkedMappings, tags.data);
 | 
			
		||||
            selectedTags = config?.constructChangeSpecification($freeformInput, selectedMapping, checkedMappings, tags.data);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error("Could not calculate changeSpecification:", e);
 | 
			
		||||
            selectedTags = undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -143,27 +134,27 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
        {#if config.mappings?.length >= 8}
 | 
			
		||||
            <input type="text" bind:value={$searchTerm}>
 | 
			
		||||
            <div class="flex w-full">
 | 
			
		||||
                <img src="./assets/svg/search.svg" class="w-6 h-6"/>
 | 
			
		||||
                <input type="text" bind:value={$searchTerm} class="w-full">
 | 
			
		||||
            </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        {#if config.freeform?.key && !(mappings?.length > 0)}
 | 
			
		||||
            <!-- There are no options to choose from, simply show the input element: fill out the text field -->
 | 
			
		||||
            <FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}/>
 | 
			
		||||
            <img src="./assets/svg/search.svg" class="w-4 h-4"/>
 | 
			
		||||
        {:else if mappings !== undefined && !config.multiAnswer}
 | 
			
		||||
            <!-- Simple radiobuttons as mapping -->
 | 
			
		||||
            <div class="flex flex-col">
 | 
			
		||||
                {#each config.mappings as mapping, i (mapping.then)}
 | 
			
		||||
                    <!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
 | 
			
		||||
                    {#if !mappingIsHidden(mapping)  }
 | 
			
		||||
                        <label class="flex">
 | 
			
		||||
                            <input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
 | 
			
		||||
                                   value={i}>
 | 
			
		||||
                            <TagRenderingMapping {mapping} {tags} {state} {selectedElement}
 | 
			
		||||
                                                 {layer}></TagRenderingMapping>
 | 
			
		||||
                        </label>
 | 
			
		||||
                    {/if}
 | 
			
		||||
                    <TagRenderingMappingInput {mapping} {tags} {state} {selectedElement}
 | 
			
		||||
                                              {layer} {searchTerm} mappingIsSelected={selectedMapping === i}>
 | 
			
		||||
                        <input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
 | 
			
		||||
                               value={i}>
 | 
			
		||||
 | 
			
		||||
                    </TagRenderingMappingInput>
 | 
			
		||||
                {/each}
 | 
			
		||||
                {#if config.freeform?.key}
 | 
			
		||||
                    <label class="flex">
 | 
			
		||||
| 
						 | 
				
			
			@ -178,13 +169,11 @@
 | 
			
		|||
            <!-- Multiple answers can be chosen: checkboxes -->
 | 
			
		||||
            <div class="flex flex-col">
 | 
			
		||||
                {#each config.mappings as mapping, i (mapping.then)}
 | 
			
		||||
                    {#if !mappingIsHidden(mapping)}
 | 
			
		||||
                        <label class="flex">
 | 
			
		||||
                            <input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+i}
 | 
			
		||||
                                   bind:checked={checkedMappings[i]}>
 | 
			
		||||
                            <TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping>
 | 
			
		||||
                        </label>
 | 
			
		||||
                    {/if}
 | 
			
		||||
                    <TagRenderingMappingInput {mapping} {tags} {state} {selectedElement}
 | 
			
		||||
                                              {layer} {searchTerm} mappingIsSelected={checkedMappings[i]}>
 | 
			
		||||
                        <input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+i}
 | 
			
		||||
                               bind:checked={checkedMappings[i]}>
 | 
			
		||||
                    </TagRenderingMappingInput>
 | 
			
		||||
                {/each}
 | 
			
		||||
                {#if config.freeform?.key}
 | 
			
		||||
                    <label class="flex">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue