forked from MapComplete/MapComplete
		
	Feature: allow to erase freeform values if there are no mappings
This commit is contained in:
		
							parent
							
								
									b7f044e976
								
							
						
					
					
						commit
						3483ac81b1
					
				
					 5 changed files with 192 additions and 158 deletions
				
			
		|  | @ -28,8 +28,12 @@ | |||
|    * This is only copied to 'value' when appropriate so that no invalid values leak outside; | ||||
|    * Additionally, the unit is added when copying | ||||
|    */ | ||||
|   let _value = new UIEventSource(value.data ?? "") | ||||
|   export let unvalidatedText = new UIEventSource(value.data ?? "") | ||||
| 
 | ||||
|    | ||||
|   if(unvalidatedText == /*Compare by reference!*/ value){ | ||||
|     throw "Value and unvalidatedText may not be the same store!" | ||||
|   } | ||||
|   let validator: Validator = Validators.get(type ?? "string") | ||||
|   if (validator === undefined) { | ||||
|     console.warn("Didn't find a validator for type", type) | ||||
|  | @ -41,13 +45,13 @@ | |||
|     if (unit && value.data) { | ||||
|       const [v, denom] = unit?.findDenomination(value.data, getCountry) | ||||
|       if (denom) { | ||||
|         _value.setData(v) | ||||
|         unvalidatedText.setData(v) | ||||
|         selectedUnit.setData(denom.canonical) | ||||
|       } else { | ||||
|         _value.setData(value.data ?? "") | ||||
|         unvalidatedText.setData(value.data ?? "") | ||||
|       } | ||||
|     } else { | ||||
|       _value.setData(value.data ?? "") | ||||
|       unvalidatedText.setData(value.data ?? "") | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -67,8 +71,8 @@ | |||
|     validator = Validators.get(type ?? "string") | ||||
| 
 | ||||
|     _placeholder = placeholder ?? validator?.getPlaceholder() ?? type | ||||
|     if (_value.data?.length > 0) { | ||||
|       feedback?.setData(validator?.getFeedback(_value.data, getCountry)) | ||||
|     if (unvalidatedText.data?.length > 0) { | ||||
|       feedback?.setData(validator?.getFeedback(unvalidatedText.data, getCountry)) | ||||
|     } else { | ||||
|       feedback?.setData(undefined) | ||||
|     } | ||||
|  | @ -78,7 +82,7 @@ | |||
| 
 | ||||
|   function setValues() { | ||||
|     // Update the value stores | ||||
|     const v = _value.data | ||||
|     const v = unvalidatedText.data | ||||
|     if (v === "") { | ||||
|       value.setData(undefined) | ||||
|       feedback?.setData(undefined) | ||||
|  | @ -103,12 +107,12 @@ | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onDestroy(_value.addCallbackAndRun((_) => setValues())) | ||||
|   onDestroy(unvalidatedText.addCallbackAndRun((_) => setValues())) | ||||
|   if (unit === undefined) { | ||||
|     onDestroy( | ||||
|       value.addCallbackAndRunD((fromUpstream) => { | ||||
|         if (_value.data !== fromUpstream && fromUpstream !== "") { | ||||
|           _value.setData(fromUpstream) | ||||
|         if (unvalidatedText.data !== fromUpstream && fromUpstream !== "") { | ||||
|           unvalidatedText.setData(fromUpstream) | ||||
|         } | ||||
|       }) | ||||
|     ) | ||||
|  | @ -131,7 +135,7 @@ | |||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true) | ||||
|   const isValid = unvalidatedText.map((v) => validator?.isValid(v, getCountry) ?? true) | ||||
| 
 | ||||
|   let htmlElem: HTMLInputElement | HTMLTextAreaElement | ||||
| 
 | ||||
|  | @ -149,7 +153,7 @@ | |||
| {#if validator?.textArea} | ||||
|   <textarea | ||||
|     class="w-full" | ||||
|     bind:value={$_value} | ||||
|     bind:value={$unvalidatedText} | ||||
|     inputmode={validator?.inputmode ?? "text"} | ||||
|     placeholder={_placeholder} | ||||
|     bind:this={htmlElem} | ||||
|  | @ -159,7 +163,7 @@ | |||
|   <div class={twMerge("inline-flex", cls)}> | ||||
|     <input | ||||
|       bind:this={htmlElem} | ||||
|       bind:value={$_value} | ||||
|       bind:value={$unvalidatedText} | ||||
|       class="w-full" | ||||
|       inputmode={validator?.inputmode ?? "text"} | ||||
|       placeholder={_placeholder} | ||||
|  | @ -170,7 +174,7 @@ | |||
|     {/if} | ||||
| 
 | ||||
|     {#if unit !== undefined} | ||||
|       <UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} {getCountry} /> | ||||
|       <UnitInput {unit} {selectedUnit} textValue={unvalidatedText} upstreamValue={value} {getCountry} /> | ||||
|     {/if} | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ | |||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization" | ||||
| 
 | ||||
|   export let value: UIEventSource<string> | ||||
|   export let unvalidatedText: UIEventSource<string> = new UIEventSource<string>(value.data) | ||||
|   export let config: TagRenderingConfig | ||||
|   export let tags: UIEventSource<Record<string, string>> | ||||
| 
 | ||||
|  | @ -64,6 +65,7 @@ | |||
|       type={config.freeform.type} | ||||
|       {placeholder} | ||||
|       {value} | ||||
|       {unvalidatedText} | ||||
|     /> | ||||
|   {/if} | ||||
| 
 | ||||
|  | @ -74,5 +76,6 @@ | |||
|     {value} | ||||
|     {state} | ||||
|     on:submit | ||||
|     {unvalidatedText} | ||||
|   /> | ||||
| </div> | ||||
|  |  | |||
|  | @ -87,6 +87,7 @@ | |||
|         {state} | ||||
|         {layer} | ||||
|         on:saved={() => (editMode = false)} | ||||
|         allowDeleteOfFreeform={true} | ||||
|       > | ||||
|         <button | ||||
|           slot="cancel" | ||||
|  |  | |||
|  | @ -22,17 +22,23 @@ | |||
|   import { Unit } from "../../../Models/Unit" | ||||
|   import UserRelatedState from "../../../Logic/State/UserRelatedState" | ||||
|   import { twJoin } from "tailwind-merge" | ||||
|   import type { UploadableTag } from "../../../Logic/Tags/TagUtils" | ||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils" | ||||
| 
 | ||||
|   import Search from "../../../assets/svg/Search.svelte" | ||||
|   import Login from "../../../assets/svg/Login.svelte" | ||||
|   import { placeholder } from "../../../Utils/placeholder" | ||||
|   import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { Tag } from "../../../Logic/Tags/Tag" | ||||
| 
 | ||||
|   export let config: TagRenderingConfig | ||||
|   export let tags: UIEventSource<Record<string, string>> | ||||
|   export let selectedElement: Feature | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let layer: LayerConfig | undefined | ||||
|   export let selectedTags: TagsFilter = undefined | ||||
|   export let selectedTags: UploadableTag = undefined | ||||
| 
 | ||||
|   export let allowDeleteOfFreeform: boolean = false | ||||
| 
 | ||||
|   let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) | ||||
| 
 | ||||
|  | @ -40,6 +46,8 @@ | |||
| 
 | ||||
|   // Will be bound if a freeform is available | ||||
|   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]) | ||||
|   let freeformInputUnvalidated = new UIEventSource<string>(freeformInput.data) | ||||
|    | ||||
|   let selectedMapping: number = undefined | ||||
|   /** | ||||
|    * A list of booleans, used if multiAnswer is set | ||||
|  | @ -145,20 +153,27 @@ | |||
|     } | ||||
|   }) | ||||
|   $: { | ||||
|     try { | ||||
|       selectedTags = config?.constructChangeSpecification( | ||||
|         $freeformInput, | ||||
|         selectedMapping, | ||||
|         checkedMappings, | ||||
|         tags.data, | ||||
|       ) | ||||
|     } catch (e) { | ||||
|       console.error("Could not calculate changeSpecification:", e) | ||||
|       selectedTags = undefined | ||||
|     if (allowDeleteOfFreeform && $freeformInput === undefined && $freeformInputUnvalidated === "" && mappings.length === 0) { | ||||
|       selectedTags = new Tag(config.freeform.key, "") | ||||
|     } else { | ||||
| 
 | ||||
|       try { | ||||
|         selectedTags = config?.constructChangeSpecification( | ||||
|           $freeformInput, | ||||
|           selectedMapping, | ||||
|           checkedMappings, | ||||
|           tags.data, | ||||
|         ) | ||||
|       } catch (e) { | ||||
|         console.error("Could not calculate changeSpecification:", e) | ||||
|         selectedTags = undefined | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function onSave(e) { | ||||
|   function onSave(e = undefined, deleteFreeform = false) { | ||||
|     console.log("On click", deleteFreeform, ">>>", selectedTags) | ||||
| 
 | ||||
|     if (selectedTags === undefined) { | ||||
|       return | ||||
|     } | ||||
|  | @ -197,9 +212,9 @@ | |||
| 
 | ||||
|   function onInputKeypress(e: KeyboardEvent) { | ||||
|     if (e.key === "Enter") { | ||||
|         e.preventDefault() | ||||
|         e.stopPropagation() | ||||
|         onSave(e) | ||||
|       e.preventDefault() | ||||
|       e.stopPropagation() | ||||
|       onSave() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -230,136 +245,139 @@ | |||
|       <fieldset> | ||||
| 
 | ||||
|         <legend> | ||||
|             <div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11"> | ||||
|                 <SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} /> | ||||
|             </div> | ||||
|           <div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11"> | ||||
|             <SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} /> | ||||
|           </div> | ||||
| 
 | ||||
|             {#if config.questionhint} | ||||
|               <div class="max-h-60 overflow-y-auto"> | ||||
|                 <SpecialTranslation | ||||
|                   t={config.questionhint} | ||||
|                   {tags} | ||||
|                   {state} | ||||
|                   {layer} | ||||
|                   feature={selectedElement} | ||||
|                 /> | ||||
|               </div> | ||||
|             {/if} | ||||
|           {#if config.questionhint} | ||||
|             <div class="max-h-60 overflow-y-auto"> | ||||
|               <SpecialTranslation | ||||
|                 t={config.questionhint} | ||||
|                 {tags} | ||||
|                 {state} | ||||
|                 {layer} | ||||
|                 feature={selectedElement} | ||||
|               /> | ||||
|             </div> | ||||
|           {/if} | ||||
|         </legend> | ||||
| 
 | ||||
|             {#if config.mappings?.length >= 8} | ||||
|               <div class="sticky flex w-full" aria-hidden="true"> | ||||
|                 <Search class="h-6 w-6" /> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   bind:value={$searchTerm} | ||||
|                   class="w-full" | ||||
|                   use:placeholder={Translations.t.general.searchAnswer} | ||||
|                 /> | ||||
|               </div> | ||||
|             {/if} | ||||
|         {#if config.mappings?.length >= 8} | ||||
|           <div class="sticky flex w-full" aria-hidden="true"> | ||||
|             <Search class="h-6 w-6" /> | ||||
|             <input | ||||
|               type="text" | ||||
|               bind:value={$searchTerm} | ||||
|               class="w-full" | ||||
|               use:placeholder={Translations.t.general.searchAnswer} | ||||
|             /> | ||||
|           </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} | ||||
|         {#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} | ||||
|             {feedback} | ||||
|             {unit} | ||||
|             {state} | ||||
|             feature={selectedElement} | ||||
|             value={freeformInput} | ||||
|             unvalidatedText={freeformInputUnvalidated} | ||||
|             on:submit={onSave} | ||||
|           /> | ||||
|         {: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--> | ||||
|               <TagRenderingMappingInput | ||||
|                 {mapping} | ||||
|                 {tags} | ||||
|                 {feedback} | ||||
|                 {unit} | ||||
|                 {state} | ||||
|                 feature={selectedElement} | ||||
|                 value={freeformInput} | ||||
|                 on:submit={onSave} | ||||
|               /> | ||||
|             {: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--> | ||||
|                   <TagRenderingMappingInput | ||||
|                     {mapping} | ||||
|                     {tags} | ||||
|                     {state} | ||||
|                     {selectedElement} | ||||
|                     {layer} | ||||
|                     {searchTerm} | ||||
|                     mappingIsSelected={selectedMapping === i} | ||||
|                   > | ||||
|                     <input | ||||
|                       type="radio" | ||||
|                       bind:group={selectedMapping} | ||||
|                       name={"mappings-radio-" + config.id} | ||||
|                       value={i} | ||||
|                       on:keypress={(e) => onInputKeypress(e)} | ||||
|                     /> | ||||
|                   </TagRenderingMappingInput> | ||||
|                 {/each} | ||||
|                 {#if config.freeform?.key} | ||||
|                   <label class="flex gap-x-1"> | ||||
|                     <input | ||||
|                       type="radio" | ||||
|                       bind:group={selectedMapping} | ||||
|                       name={"mappings-radio-" + config.id} | ||||
|                       value={config.mappings?.length} | ||||
|                       on:keypress={(e) => onInputKeypress(e)} | ||||
|                     /> | ||||
|                     <FreeformInput | ||||
|                       {config} | ||||
|                       {tags} | ||||
|                       {feedback} | ||||
|                       {unit} | ||||
|                       {state} | ||||
|                       feature={selectedElement} | ||||
|                       value={freeformInput} | ||||
|                       on:selected={() => (selectedMapping = config.mappings?.length)} | ||||
|                       on:submit={onSave} | ||||
|                     /> | ||||
|                   </label> | ||||
|                 {/if} | ||||
|               </div> | ||||
|             {:else if mappings !== undefined && config.multiAnswer} | ||||
|               <!-- Multiple answers can be chosen: checkboxes --> | ||||
|               <div class="flex flex-col"> | ||||
|                 {#each config.mappings as mapping, i (mapping.then)} | ||||
|                   <TagRenderingMappingInput | ||||
|                     {mapping} | ||||
|                     {tags} | ||||
|                     {state} | ||||
|                     {selectedElement} | ||||
|                     {layer} | ||||
|                     {searchTerm} | ||||
|                     mappingIsSelected={checkedMappings[i]} | ||||
|                   > | ||||
|                     <input | ||||
|                       type="checkbox" | ||||
|                       name={"mappings-checkbox-" + config.id + "-" + i} | ||||
|                       bind:checked={checkedMappings[i]} | ||||
|                       on:keypress={(e) => onInputKeypress(e)} | ||||
|                     /> | ||||
|                   </TagRenderingMappingInput> | ||||
|                 {/each} | ||||
|                 {#if config.freeform?.key} | ||||
|                   <label class="flex gap-x-1"> | ||||
|                     <input | ||||
|                       type="checkbox" | ||||
|                       name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length} | ||||
|                       bind:checked={checkedMappings[config.mappings.length]} | ||||
|                       on:keypress={(e) => onInputKeypress(e)} | ||||
|                     /> | ||||
|                     <FreeformInput | ||||
|                       {config} | ||||
|                       {tags} | ||||
|                       {feedback} | ||||
|                       {unit} | ||||
|                       {state} | ||||
|                       feature={selectedElement} | ||||
|                       value={freeformInput} | ||||
|                       on:submit={onSave} | ||||
|                     /> | ||||
|                   </label> | ||||
|                 {/if} | ||||
|               </div> | ||||
|                 {selectedElement} | ||||
|                 {layer} | ||||
|                 {searchTerm} | ||||
|                 mappingIsSelected={selectedMapping === i} | ||||
|               > | ||||
|                 <input | ||||
|                   type="radio" | ||||
|                   bind:group={selectedMapping} | ||||
|                   name={"mappings-radio-" + config.id} | ||||
|                   value={i} | ||||
|                   on:keypress={(e) => onInputKeypress(e)} | ||||
|                 /> | ||||
|               </TagRenderingMappingInput> | ||||
|             {/each} | ||||
|             {#if config.freeform?.key} | ||||
|               <label class="flex gap-x-1"> | ||||
|                 <input | ||||
|                   type="radio" | ||||
|                   bind:group={selectedMapping} | ||||
|                   name={"mappings-radio-" + config.id} | ||||
|                   value={config.mappings?.length} | ||||
|                   on:keypress={(e) => onInputKeypress(e)} | ||||
|                 /> | ||||
|                 <FreeformInput | ||||
|                   {config} | ||||
|                   {tags} | ||||
|                   {feedback} | ||||
|                   {unit} | ||||
|                   {state} | ||||
|                   feature={selectedElement} | ||||
|                   value={freeformInput} | ||||
|                   unvalidatedText={freeformInputUnvalidated} | ||||
|                   on:selected={() => (selectedMapping = config.mappings?.length)} | ||||
|                   on:submit={onSave} | ||||
|                 /> | ||||
|               </label> | ||||
|             {/if} | ||||
|           </div> | ||||
|         {:else if mappings !== undefined && config.multiAnswer} | ||||
|           <!-- Multiple answers can be chosen: checkboxes --> | ||||
|           <div class="flex flex-col"> | ||||
|             {#each config.mappings as mapping, i (mapping.then)} | ||||
|               <TagRenderingMappingInput | ||||
|                 {mapping} | ||||
|                 {tags} | ||||
|                 {state} | ||||
|                 {selectedElement} | ||||
|                 {layer} | ||||
|                 {searchTerm} | ||||
|                 mappingIsSelected={checkedMappings[i]} | ||||
|               > | ||||
|                 <input | ||||
|                   type="checkbox" | ||||
|                   name={"mappings-checkbox-" + config.id + "-" + i} | ||||
|                   bind:checked={checkedMappings[i]} | ||||
|                   on:keypress={(e) => onInputKeypress(e)} | ||||
|                 /> | ||||
|               </TagRenderingMappingInput> | ||||
|             {/each} | ||||
|             {#if config.freeform?.key} | ||||
|               <label class="flex gap-x-1"> | ||||
|                 <input | ||||
|                   type="checkbox" | ||||
|                   name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length} | ||||
|                   bind:checked={checkedMappings[config.mappings.length]} | ||||
|                   on:keypress={(e) => onInputKeypress(e)} | ||||
|                 /> | ||||
|                 <FreeformInput | ||||
|                   {config} | ||||
|                   {tags} | ||||
|                   {feedback} | ||||
|                   {unit} | ||||
|                   {state} | ||||
|                   feature={selectedElement} | ||||
|                   value={freeformInput} | ||||
|                   unvalidatedText={freeformInputUnvalidated} | ||||
|                   on:submit={onSave} | ||||
|                 /> | ||||
|               </label> | ||||
|             {/if} | ||||
|           </div> | ||||
|         {/if} | ||||
|         <LoginToggle {state}> | ||||
|           <Loading slot="loading" /> | ||||
|           <SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}> | ||||
|  | @ -378,12 +396,19 @@ | |||
|             <!-- TagRenderingQuestion-buttons --> | ||||
|             <slot name="cancel" /> | ||||
|             <slot name="save-button" {selectedTags}> | ||||
|               <button | ||||
|                 on:click={onSave} | ||||
|                 class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")} | ||||
|               > | ||||
|                 <Tr t={Translations.t.general.save} /> | ||||
|               </button> | ||||
|               {#if allowDeleteOfFreeform && mappings.length === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""} | ||||
|                 <button class="primary flex" on:click|stopPropagation|preventDefault={_ => onSave(_, true)}> | ||||
|                   <TrashIcon class="w-6 h-6 text-red-500" /> | ||||
|                   <Tr t={Translations.t.general.eraseValue}/> | ||||
|                 </button> | ||||
|               {:else} | ||||
|                 <button | ||||
|                   on:click={onSave} | ||||
|                   class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")} | ||||
|                 > | ||||
|                   <Tr t={Translations.t.general.save} /> | ||||
|                 </button> | ||||
|               {/if} | ||||
|             </slot> | ||||
|           </div> | ||||
|           {#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging} | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue