Feature: allow to erase freeform values if there are no mappings

This commit is contained in:
Pieter Vander Vennet 2024-01-22 02:11:42 +01:00
parent ef34304efa
commit f7b555b9b6
5 changed files with 192 additions and 158 deletions

View file

@ -217,6 +217,7 @@
}, },
"enableGeolocationForSafari": "Did you not get the popup to ask for geopermission?", "enableGeolocationForSafari": "Did you not get the popup to ask for geopermission?",
"enableGeolocationForSafariLink": "Learn how to enable the geolocation permission in settings", "enableGeolocationForSafariLink": "Learn how to enable the geolocation permission in settings",
"eraseValue": "Erase this value",
"error": "Something went wrong", "error": "Something went wrong",
"example": "Example", "example": "Example",
"examples": "Examples", "examples": "Examples",

View file

@ -28,8 +28,12 @@
* This is only copied to 'value' when appropriate so that no invalid values leak outside; * This is only copied to 'value' when appropriate so that no invalid values leak outside;
* Additionally, the unit is added when copying * 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") let validator: Validator = Validators.get(type ?? "string")
if (validator === undefined) { if (validator === undefined) {
console.warn("Didn't find a validator for type", type) console.warn("Didn't find a validator for type", type)
@ -41,13 +45,13 @@
if (unit && value.data) { if (unit && value.data) {
const [v, denom] = unit?.findDenomination(value.data, getCountry) const [v, denom] = unit?.findDenomination(value.data, getCountry)
if (denom) { if (denom) {
_value.setData(v) unvalidatedText.setData(v)
selectedUnit.setData(denom.canonical) selectedUnit.setData(denom.canonical)
} else { } else {
_value.setData(value.data ?? "") unvalidatedText.setData(value.data ?? "")
} }
} else { } else {
_value.setData(value.data ?? "") unvalidatedText.setData(value.data ?? "")
} }
} }
@ -67,8 +71,8 @@
validator = Validators.get(type ?? "string") validator = Validators.get(type ?? "string")
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
if (_value.data?.length > 0) { if (unvalidatedText.data?.length > 0) {
feedback?.setData(validator?.getFeedback(_value.data, getCountry)) feedback?.setData(validator?.getFeedback(unvalidatedText.data, getCountry))
} else { } else {
feedback?.setData(undefined) feedback?.setData(undefined)
} }
@ -78,7 +82,7 @@
function setValues() { function setValues() {
// Update the value stores // Update the value stores
const v = _value.data const v = unvalidatedText.data
if (v === "") { if (v === "") {
value.setData(undefined) value.setData(undefined)
feedback?.setData(undefined) feedback?.setData(undefined)
@ -103,12 +107,12 @@
} }
} }
onDestroy(_value.addCallbackAndRun((_) => setValues())) onDestroy(unvalidatedText.addCallbackAndRun((_) => setValues()))
if (unit === undefined) { if (unit === undefined) {
onDestroy( onDestroy(
value.addCallbackAndRunD((fromUpstream) => { value.addCallbackAndRunD((fromUpstream) => {
if (_value.data !== fromUpstream && fromUpstream !== "") { if (unvalidatedText.data !== fromUpstream && fromUpstream !== "") {
_value.setData(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 let htmlElem: HTMLInputElement | HTMLTextAreaElement
@ -149,7 +153,7 @@
{#if validator?.textArea} {#if validator?.textArea}
<textarea <textarea
class="w-full" class="w-full"
bind:value={$_value} bind:value={$unvalidatedText}
inputmode={validator?.inputmode ?? "text"} inputmode={validator?.inputmode ?? "text"}
placeholder={_placeholder} placeholder={_placeholder}
bind:this={htmlElem} bind:this={htmlElem}
@ -159,7 +163,7 @@
<div class={twMerge("inline-flex", cls)}> <div class={twMerge("inline-flex", cls)}>
<input <input
bind:this={htmlElem} bind:this={htmlElem}
bind:value={$_value} bind:value={$unvalidatedText}
class="w-full" class="w-full"
inputmode={validator?.inputmode ?? "text"} inputmode={validator?.inputmode ?? "text"}
placeholder={_placeholder} placeholder={_placeholder}
@ -170,7 +174,7 @@
{/if} {/if}
{#if unit !== undefined} {#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} {getCountry} /> <UnitInput {unit} {selectedUnit} textValue={unvalidatedText} upstreamValue={value} {getCountry} />
{/if} {/if}
</div> </div>
{/if} {/if}

View file

@ -12,6 +12,7 @@
import type { SpecialVisualizationState } from "../../SpecialVisualization" import type { SpecialVisualizationState } from "../../SpecialVisualization"
export let value: UIEventSource<string> export let value: UIEventSource<string>
export let unvalidatedText: UIEventSource<string> = new UIEventSource<string>(value.data)
export let config: TagRenderingConfig export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -64,6 +65,7 @@
type={config.freeform.type} type={config.freeform.type}
{placeholder} {placeholder}
{value} {value}
{unvalidatedText}
/> />
{/if} {/if}
@ -74,5 +76,6 @@
{value} {value}
{state} {state}
on:submit on:submit
{unvalidatedText}
/> />
</div> </div>

View file

@ -87,6 +87,7 @@
{state} {state}
{layer} {layer}
on:saved={() => (editMode = false)} on:saved={() => (editMode = false)}
allowDeleteOfFreeform={true}
> >
<button <button
slot="cancel" slot="cancel"

View file

@ -22,17 +22,23 @@
import { Unit } from "../../../Models/Unit" import { Unit } from "../../../Models/Unit"
import UserRelatedState from "../../../Logic/State/UserRelatedState" import UserRelatedState from "../../../Logic/State/UserRelatedState"
import { twJoin } from "tailwind-merge" import { twJoin } from "tailwind-merge"
import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
import { TagUtils } from "../../../Logic/Tags/TagUtils" import { TagUtils } from "../../../Logic/Tags/TagUtils"
import Search from "../../../assets/svg/Search.svelte" import Search from "../../../assets/svg/Search.svelte"
import Login from "../../../assets/svg/Login.svelte" import Login from "../../../assets/svg/Login.svelte"
import { placeholder } from "../../../Utils/placeholder" import { placeholder } from "../../../Utils/placeholder"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Tag } from "../../../Logic/Tags/Tag"
export let config: TagRenderingConfig export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature export let selectedElement: Feature
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let layer: LayerConfig | undefined 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) let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
@ -40,6 +46,8 @@
// Will be bound if a freeform is available // Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]) let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
let freeformInputUnvalidated = new UIEventSource<string>(freeformInput.data)
let selectedMapping: number = undefined let selectedMapping: number = undefined
/** /**
* A list of booleans, used if multiAnswer is set * A list of booleans, used if multiAnswer is set
@ -145,20 +153,27 @@
} }
}) })
$: { $: {
try { if (allowDeleteOfFreeform && $freeformInput === undefined && $freeformInputUnvalidated === "" && mappings.length === 0) {
selectedTags = config?.constructChangeSpecification( selectedTags = new Tag(config.freeform.key, "")
$freeformInput, } else {
selectedMapping,
checkedMappings, try {
tags.data, selectedTags = config?.constructChangeSpecification(
) $freeformInput,
} catch (e) { selectedMapping,
console.error("Could not calculate changeSpecification:", e) checkedMappings,
selectedTags = undefined 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) { if (selectedTags === undefined) {
return return
} }
@ -197,9 +212,9 @@
function onInputKeypress(e: KeyboardEvent) { function onInputKeypress(e: KeyboardEvent) {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
onSave(e) onSave()
} }
} }
@ -230,136 +245,139 @@
<fieldset> <fieldset>
<legend> <legend>
<div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11"> <div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} /> <SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</div> </div>
{#if config.questionhint} {#if config.questionhint}
<div class="max-h-60 overflow-y-auto"> <div class="max-h-60 overflow-y-auto">
<SpecialTranslation <SpecialTranslation
t={config.questionhint} t={config.questionhint}
{tags} {tags}
{state} {state}
{layer} {layer}
feature={selectedElement} feature={selectedElement}
/> />
</div> </div>
{/if} {/if}
</legend> </legend>
{#if config.mappings?.length >= 8} {#if config.mappings?.length >= 8}
<div class="sticky flex w-full" aria-hidden="true"> <div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" /> <Search class="h-6 w-6" />
<input <input
type="text" type="text"
bind:value={$searchTerm} bind:value={$searchTerm}
class="w-full" class="w-full"
use:placeholder={Translations.t.general.searchAnswer} use:placeholder={Translations.t.general.searchAnswer}
/> />
</div> </div>
{/if} {/if}
{#if config.freeform?.key && !(mappings?.length > 0)} {#if config.freeform?.key && !(mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field --> <!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput <FreeformInput
{config} {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} {tags}
{feedback}
{unit}
{state} {state}
feature={selectedElement} {selectedElement}
value={freeformInput} {layer}
on:submit={onSave} {searchTerm}
/> mappingIsSelected={selectedMapping === i}
{:else if mappings !== undefined && !config.multiAnswer} >
<!-- Simple radiobuttons as mapping --> <input
<div class="flex flex-col"> type="radio"
{#each config.mappings as mapping, i (mapping.then)} bind:group={selectedMapping}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices--> name={"mappings-radio-" + config.id}
<TagRenderingMappingInput value={i}
{mapping} on:keypress={(e) => onInputKeypress(e)}
{tags} />
{state} </TagRenderingMappingInput>
{selectedElement} {/each}
{layer} {#if config.freeform?.key}
{searchTerm} <label class="flex gap-x-1">
mappingIsSelected={selectedMapping === i} <input
> type="radio"
<input bind:group={selectedMapping}
type="radio" name={"mappings-radio-" + config.id}
bind:group={selectedMapping} value={config.mappings?.length}
name={"mappings-radio-" + config.id} on:keypress={(e) => onInputKeypress(e)}
value={i} />
on:keypress={(e) => onInputKeypress(e)} <FreeformInput
/> {config}
</TagRenderingMappingInput> {tags}
{/each} {feedback}
{#if config.freeform?.key} {unit}
<label class="flex gap-x-1"> {state}
<input feature={selectedElement}
type="radio" value={freeformInput}
bind:group={selectedMapping} unvalidatedText={freeformInputUnvalidated}
name={"mappings-radio-" + config.id} on:selected={() => (selectedMapping = config.mappings?.length)}
value={config.mappings?.length} on:submit={onSave}
on:keypress={(e) => onInputKeypress(e)} />
/> </label>
<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>
{/if} {/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}> <LoginToggle {state}>
<Loading slot="loading" /> <Loading slot="loading" />
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}> <SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
@ -378,12 +396,19 @@
<!-- TagRenderingQuestion-buttons --> <!-- TagRenderingQuestion-buttons -->
<slot name="cancel" /> <slot name="cancel" />
<slot name="save-button" {selectedTags}> <slot name="save-button" {selectedTags}>
<button {#if allowDeleteOfFreeform && mappings.length === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
on:click={onSave} <button class="primary flex" on:click|stopPropagation|preventDefault={_ => onSave(_, true)}>
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")} <TrashIcon class="w-6 h-6 text-red-500" />
> <Tr t={Translations.t.general.eraseValue}/>
<Tr t={Translations.t.general.save} /> </button>
</button> {:else}
<button
on:click={onSave}
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
>
<Tr t={Translations.t.general.save} />
</button>
{/if}
</slot> </slot>
</div> </div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging} {#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}