Refactoring: port unit picker

This commit is contained in:
Pieter Vander Vennet 2023-06-11 01:32:30 +02:00
parent 622df2df19
commit 2b05d79dbb
8 changed files with 188 additions and 67 deletions

View file

@ -23,7 +23,7 @@ export class Denomination {
}
this._canonicalSingular = json.canonicalDenominationSingular?.trim()
json.alternativeDenomination.forEach((v, i) => {
json.alternativeDenomination?.forEach((v, i) => {
if ((v?.trim() ?? "") === "") {
throw `${context}.alternativeDenomination.${i}: invalid alternative denomination: undefined, null or only whitespace`
}
@ -51,12 +51,6 @@ export class Denomination {
return (this._humanSingular ?? this._human).Clone()
}
getToggledHuman(isSingular: Store<boolean>): BaseUIElement {
if (this._humanSingular === undefined) {
return this.human
}
return new Toggle(this.humanSingular, this.human, isSingular)
}
/**
* Create a representation of the given value
@ -165,7 +159,7 @@ export class Denomination {
return null
}
isDefaultUnit(country: () => string) {
isDefaultDenomination(country: () => string) {
if (this.useIfNoUnitGiven === true) {
return true
}

View file

@ -199,7 +199,7 @@ export class Unit {
) {
continue
}
let countries: string | string[] = country()
let countries: string | string[] = country() ?? []
if (typeof countries === "string") {
countries = countries.split(",")
}

View file

@ -1,64 +1,109 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import type { ValidatorType } from "./Validators";
import Validators from "./Validators";
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Translation } from "../i18n/Translation";
import { createEventDispatcher, onDestroy } from "svelte";
import {Validator} from "./Validator";
import {UIEventSource} from "../../Logic/UIEventSource";
import type {ValidatorType} from "./Validators";
import Validators from "./Validators";
import {ExclamationIcon} from "@rgossiaux/svelte-heroicons/solid";
import {Translation} from "../i18n/Translation";
import {createEventDispatcher, onDestroy} from "svelte";
import {Validator} from "./Validator";
import {Unit} from "../../Models/Unit";
import UnitInput from "../Popup/UnitInput.svelte";
export let value: UIEventSource<string>;
// Internal state, only copied to 'value' so that no invalid values leak outside
let _value = new UIEventSource(value.data ?? "");
export let type: ValidatorType;
export let feedback: UIEventSource<Translation> | undefined = undefined;
export let getCountry: () => string | undefined
export let placeholder: string | Translation | undefined
let validator : Validator = Validators.get(type)
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
$: {
// The type changed -> reset some values
validator = Validators.get(type)
_value.setData(value.data ?? "")
feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry));
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
}
onDestroy(_value.addCallbackAndRun(v => {
if (validator.isValid(v, getCountry)) {
feedback?.setData(undefined);
value.setData(v);
return;
export let type: ValidatorType;
export let feedback: UIEventSource<Translation> | undefined = undefined;
export let getCountry: () => string | undefined
export let placeholder: string | Translation | undefined
export let unit: Unit = undefined
export let value: UIEventSource<string>;
/**
* Internal state bound to the input element.
*
* 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 ?? "");
let validator: Validator = Validators.get(type ?? "string")
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
function initValueAndDenom() {
if (unit && value.data) {
const [v, denom] = unit?.findDenomination(value.data, getCountry)
if (denom) {
_value.setData(v)
selectedUnit.setData(denom.canonical)
} else {
_value.setData(value.data ?? "")
}
} else {
_value.setData(value.data ?? "")
}
}
value.setData(undefined);
feedback?.setData(validator.getFeedback(v, getCountry));
}))
if (validator === undefined) {
throw "Not a valid type for a validator:" + type;
}
initValueAndDenom()
const isValid = _value.map(v => validator.isValid(v, getCountry));
$: {
// The type changed -> reset some values
validator = Validators.get(type ?? "string")
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry));
let htmlElem: HTMLInputElement;
let dispatch = createEventDispatcher<{ selected }>();
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected");
initValueAndDenom()
}
function setValues() {
// Update the value stores
const v = _value.data
if (!validator.isValid(v, getCountry) || v === "") {
value.setData(undefined);
feedback?.setData(validator.getFeedback(v, getCountry));
return
}
if (unit && isNaN(Number(v))) {
value.setData(undefined);
return
}
feedback?.setData(undefined);
value.setData(v + (selectedUnit.data ?? ""));
}
onDestroy(_value.addCallbackAndRun(_ => setValues()))
onDestroy(selectedUnit.addCallback(_ => setValues()))
if (validator === undefined) {
throw "Not a valid type for a validator:" + type;
}
const isValid = _value.map(v => validator.isValid(v, getCountry));
let htmlElem: HTMLInputElement;
let dispatch = createEventDispatcher<{ selected }>();
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected");
}
}
}
</script>
{#if validator.textArea}
<textarea class="w-full" bind:value={$_value} inputmode={validator.inputmode ?? "text"} placeholder={_placeholder}></textarea>
<textarea class="w-full" bind:value={$_value} inputmode={validator.inputmode ?? "text"}
placeholder={_placeholder}></textarea>
{:else }
<span class="inline-flex">
<input bind:this={htmlElem} bind:value={$_value} class="w-full" inputmode={validator.inputmode ?? "text"} placeholder={_placeholder}>
{#if !$isValid}
<input bind:this={htmlElem} bind:value={$_value} class="w-full" inputmode={validator.inputmode ?? "text"}
placeholder={_placeholder}>
{#if !$isValid}
<ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon>
{/if}
{#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value}/>
{/if}
</span>
{/if}

View file

@ -7,17 +7,20 @@
import {createEventDispatcher, onDestroy} from "svelte";
import InputHelper from "../../InputElement/InputHelper.svelte";
import type {Feature} from "geojson";
import {Unit} from "../../../Models/Unit";
export let value: UIEventSource<string>;
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let feature: Feature = undefined;
export let unit: Unit | undefined
let placeholder = config.freeform?.placeholder
$: {
placeholder = config.freeform?.placeholder
}
let inline = config.freeform.inline
export let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
@ -35,13 +38,14 @@
{#if config.freeform.inline}
<Inline key={config.freeform.key} {tags} template={config.render}>
<ValidatedInput {feedback} {getCountry} on:selected={() => dispatch("selected")}
<ValidatedInput {feedback} {getCountry} {unit} on:selected={() => dispatch("selected")}
type={config.freeform.type} {placeholder} {value}></ValidatedInput>
</Inline>
{:else}
<ValidatedInput {feedback} {getCountry} on:selected={() => dispatch("selected")}
<ValidatedInput {feedback} {getCountry} {unit} on:selected={() => dispatch("selected")}
type={config.freeform.type} {placeholder} {value}></ValidatedInput>
{/if}
<InputHelper args={config.freeform.helperArgs} {feature} type={config.freeform.type} {value}/>
</div>

View file

@ -31,7 +31,7 @@
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(_tags))}
<div class={"link-underline flex flex-col w-full "+classes+" "+extraClasses}>
<div class={"link-underline inline-block w-full "+classes+" "+extraClasses}>
{#if trs.length === 1}
<TagRenderingMapping mapping={trs[0]} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
{/if}

View file

@ -19,7 +19,7 @@
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte";
import {Translation} from "../../i18n/Translation";
import Constants from "../../../Models/Constants";
import {TagUtils} from "../../../Logic/Tags/TagUtils";
import {Unit} from "../../../Models/Unit";
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
@ -29,6 +29,8 @@
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
let unit: Unit = layer.units?.find(unit => unit.appliesToKeys.has(config.freeform?.key))
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]);
let selectedMapping: number = undefined;
@ -41,11 +43,13 @@
return m.hideInAnswer.matchesProperties(tags.data)
})
// We received a new config -> reinit
unit = layer.units.find(unit => unit.appliesToKeys.has(config.freeform?.key))
if (config.mappings?.length > 0 && (checkedMappings === undefined || checkedMappings?.length < config.mappings.length)) {
checkedMappings = [...config.mappings.map(_ => false), false /*One element extra in case a freeform value is added*/];
}
if (config.freeform?.key) {
if(!config.multiAnswer){ // Somehow, setting multianswer freeform values is broken if this is not set
if (!config.multiAnswer) { // Somehow, setting multianswer freeform values is broken if this is not set
freeformInput.setData(tags.data[config.freeform.key]);
}
} else {
@ -77,7 +81,7 @@
}>();
function onSave() {
if(selectedTags === undefined){
if (selectedTags === undefined) {
return
}
if (layer.source === null) {
@ -152,7 +156,7 @@
{#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} feature={selectedElement} value={freeformInput}/>
<FreeformInput {config} {tags} {feedback} {unit} feature={selectedElement} value={freeformInput}/>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
@ -169,7 +173,7 @@
<label class="flex">
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
value={config.mappings?.length}>
<FreeformInput {config} {tags} {feedback} feature={selectedElement} value={freeformInput}
<FreeformInput {config} {tags} {feedback} {unit} feature={selectedElement} value={freeformInput}
on:selected={() => selectedMapping = config.mappings?.length }/>
</label>
{/if}
@ -188,7 +192,7 @@
<label class="flex">
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}>
<FreeformInput {config} {tags} {feedback} feature={selectedElement} value={freeformInput}
<FreeformInput {config} {tags} {feedback} {unit} feature={selectedElement} value={freeformInput}
on:selected={() => checkedMappings[config.mappings.length] = true}/>
</label>
{/if}
@ -210,7 +214,8 @@
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel"></slot>
<slot name="save-button" {selectedTags}>
<button on:click={onSave} class={(selectedTags === undefined ? "disabled" : "button-shadow")+" primary"}>
<button on:click={onSave}
class={(selectedTags === undefined ? "disabled" : "button-shadow")+" primary"}>
<Tr t={Translations.t.general.save}/>
</button>
</slot>

67
UI/Popup/UnitInput.svelte Normal file
View file

@ -0,0 +1,67 @@
<script lang="ts">
import {Unit} from "../../Models/Unit";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import Tr from "../Base/Tr.svelte";
import {onDestroy} from "svelte";
export let unit: Unit
/**
* The current value of the input field
* Not necessarily a correct number
*/
export let textValue: UIEventSource<string>
/**
* The actual _valid_ value that is upstreamed
*/
export let upstreamValue: Store<string>
let isSingle: Store<boolean> = textValue.map(v => Number(v) === 1)
export let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
export let getCountry = () => "be"
console.log("Unit", unit)
onDestroy(upstreamValue.addCallbackAndRun(v => {
if (v === undefined) {
if (!selectedUnit.data) {
selectedUnit.setData(unit.getDefaultDenomination(getCountry).canonical)
}
return
}
const selected = unit.findDenomination(v, getCountry)
if (selected === undefined) {
selectedUnit.setData(unit.getDefaultDenomination(getCountry).canonical)
return
}
const [value, denomination] = selected
selectedUnit.setData(denomination.canonical)
return
}))
onDestroy(textValue.addCallbackAndRunD(v => {
// Fallback in case that the user manually types a denomination
const [value, denomination] = unit.findDenomination(v, getCountry)
if (value === undefined || denomination === undefined) {
return
}
textValue.setData(value)
selectedUnit.setData(denomination.canonical)
}))
</script>
<select bind:value={$selectedUnit}>
{#each unit.denominations as denom (denom.canonical)}
<option value={denom.canonical}>
{#if $isSingle}
<Tr t={denom.humanSingular}/>
{:else }
<Tr t={denom.human}/>
{/if}
</option>
{/each}
</select>

View file

@ -3,6 +3,13 @@ import * as theme from "./assets/generated/themes/bookcases.json"
import ThemeViewState from "./Models/ThemeViewState"
import Combine from "./UI/Base/Combine"
import SpecialVisualizations from "./UI/SpecialVisualizations"
import ValidatedInput from "./UI/InputElement/ValidatedInput.svelte";
import SvelteUIElement from "./UI/Base/SvelteUIElement";
import {UIEventSource} from "./Logic/UIEventSource";
import {Unit} from "./Models/Unit";
import {Denomination} from "./Models/Denomination";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {FixedUiElement} from "./UI/Base/FixedUiElement";
function testspecial() {
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
@ -14,7 +21,6 @@ function testspecial() {
new Combine(all).AttachTo("maindiv")
}
/*/
testspecial()
//*/