From 9877abec17aedbfbf0b31c40255bb0a076de04e4 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 6 Jun 2022 19:37:22 +0200 Subject: [PATCH] More refactoring and fixes --- .../Sources/RememberingSource.ts | 10 +- Logic/Osm/OsmConnection.ts | 8 +- Logic/State/UserRelatedState.ts | 2 +- Logic/UIEventSource.ts | 405 +++++++++++++----- UI/Input/CombinedInputElement.ts | 10 +- UI/Input/InputElement.ts | 1 + UI/Input/RadioButton.ts | 1 + UI/Input/Toggle.ts | 2 +- UI/Input/ValidatedTextField.ts | 3 +- UI/Input/VariableInputElement.ts | 6 +- UI/Popup/SaveButton.ts | 4 +- UI/Popup/TagRenderingQuestion.ts | 36 +- .../public_bookcase/public_bookcase.json | 3 +- test.ts | 35 +- 14 files changed, 375 insertions(+), 151 deletions(-) diff --git a/Logic/FeatureSource/Sources/RememberingSource.ts b/Logic/FeatureSource/Sources/RememberingSource.ts index 7f32a1324..344107f07 100644 --- a/Logic/FeatureSource/Sources/RememberingSource.ts +++ b/Logic/FeatureSource/Sources/RememberingSource.ts @@ -20,17 +20,15 @@ export default class RememberingSource implements FeatureSource, Tiled { this.bbox = source.bbox; const empty = []; - this.features = source.features.map(features => { + const featureSource = new UIEventSource<{feature: any, freshness: Date}[]>(empty) + this.features = featureSource + source.features.addCallbackAndRunD(features => { const oldFeatures = self.features?.data ?? empty; - if (features === undefined) { - return oldFeatures; - } - // Then new ids const ids = new Set(features.map(f => f.feature.properties.id + f.feature.geometry.type)); // the old data const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type)) - return [...features, ...oldData]; + featureSource.setData([...features, ...oldData]) }) } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 5eca8976d..945580788 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -1,5 +1,5 @@ import osmAuth from "osm-auth"; -import {Stores, UIEventSource} from "../UIEventSource"; +import {Store, Stores, UIEventSource} from "../UIEventSource"; import {OsmPreferences} from "./OsmPreferences"; import {ChangesetHandler} from "./ChangesetHandler"; import {ElementStorage} from "../ElementStorage"; @@ -44,7 +44,7 @@ export class OsmConnection { } public auth; public userDetails: UIEventSource; - public isLoggedIn: UIEventSource + public isLoggedIn: Store public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">("not-attempted") public preferencesHandler: OsmPreferences; public readonly _oauth_config: { @@ -86,13 +86,15 @@ export class OsmConnection { ud.totalMessages = 42; } const self = this; - this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { + this.isLoggedIn = this.userDetails.map(user => user.loggedIn); + this.isLoggedIn.addCallback(isLoggedIn => { if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do // This means someone attempted to toggle this; so we attempt to login! self.AttemptLogin() } }); + this._dryRun = options.dryRun ?? new UIEventSource(false); this.updateAuthObject(); diff --git a/Logic/State/UserRelatedState.ts b/Logic/State/UserRelatedState.ts index 6ec0b876f..b89750edf 100644 --- a/Logic/State/UserRelatedState.ts +++ b/Logic/State/UserRelatedState.ts @@ -142,7 +142,7 @@ export default class UserRelatedState extends ElementsState { Locale.language.setData(layoutToUse.language[0]); } }) - .ping(); + Locale.language.ping(); } } \ No newline at end of file diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index c631f5746..0cec032fc 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -18,7 +18,7 @@ export class Stores { return source; } - public static FromPromiseWithErr(promise: Promise): Store<{ success: T } | { error: any }>{ + public static FromPromiseWithErr(promise: Promise): Store<{ success: T } | { error: any }> { return UIEventSource.FromPromiseWithErr(promise); } @@ -54,9 +54,9 @@ export class Stores { * @constructor */ public static ListStabilized(src: Store): Store { - - const stable = new UIEventSource(src.data) - src.addCallback(list => { + const stable = new UIEventSource(undefined) + src.addCallbackAndRun(list => { + console.trace("Running list stabilization", list) if (list === undefined) { stable.setData(undefined) return; @@ -65,6 +65,9 @@ export class Stores { if (oldList === list) { return; } + if(oldList == list){ + return; + } if (oldList === undefined || oldList.length !== list.length) { stable.setData(list); return; @@ -91,16 +94,16 @@ export abstract class Store { * OPtional value giving a title to the UIEventSource, mainly used for debugging */ public readonly tag: string | undefined; - + constructor(tag: string = undefined) { this.tag = tag; if ((tag === undefined || tag === "")) { let createStack = Utils.runningFromConsole; - if(!Utils.runningFromConsole) { + if (!Utils.runningFromConsole) { createStack = window.location.hostname === "127.0.0.1" } - if(createStack) { + if (createStack) { const callstack = new Error().stack.split("\n") this.tag = callstack[1] } @@ -113,25 +116,25 @@ export abstract class Store { /** * Add a callback function which will run on future data changes */ - abstract addCallback(callback: (data: T) => void); + abstract addCallback(callback: (data: T) => void): (() => void); /** * Adds a callback function, which will be run immediately. * Only triggers if the current data is defined */ - abstract addCallbackAndRunD(callback: (data: T) => void); + abstract addCallbackAndRunD(callback: (data: T) => void): (() => void); /** * Add a callback function which will run on future data changes * Only triggers if the data is defined */ - abstract addCallbackD(callback: (data: T) => void); + abstract addCallbackD(callback: (data: T) => void): (() => void); /** * Adds a callback function, which will be run immediately. * Only triggers if the current data is defined */ - abstract addCallbackAndRun(callback: (data: T) => void); + abstract addCallbackAndRun(callback: (data: T) => void): (() => void); public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): Store { let oldValue = undefined; @@ -149,6 +152,49 @@ export abstract class Store { /** * Monadic bind function + * + * // simple test with bound and immutablestores + * const src = new UIEventSource(3) + * const bound = src.bind(i => new ImmutableStore(i * 2)) + * let lastValue = undefined; + * bound.addCallbackAndRun(v => lastValue = v); + * lastValue // => 6 + * src.setData(21) + * lastValue // => 42 + * + * // simple test with bind over a mapped value + * const src = new UIEventSource(0) + * const srcs : UIEventSource[] = [new UIEventSource("a"), new UIEventSource("b")] + * const bound = src.map(i => -i).bind(i => srcs[i]) + * let lastValue : string = undefined; + * bound.addCallbackAndRun(v => lastValue = v); + * lastValue // => "a" + * src.setData(-1) + * lastValue // => "b" + * srcs[1].setData("xyz") + * lastValue // => "xyz" + * srcs[0].setData("def") + * lastValue // => "xyz" + * src.setData(0) + * lastValue // => "def" + * + * + * + * // advanced test with bound + * const src = new UIEventSource(0) + * const srcs : UIEventSource[] = [new UIEventSource("a"), new UIEventSource("b")] + * const bound = src.bind(i => srcs[i]) + * let lastValue : string = undefined; + * bound.addCallbackAndRun(v => lastValue = v); + * lastValue // => "a" + * src.setData(1) + * lastValue // => "b" + * srcs[1].setData("xyz") + * lastValue // => "xyz" + * srcs[0].setData("def") + * lastValue // => "xyz" + * src.setData(0) + * lastValue // => "def" */ public bind(f: ((t: T) => Store)): Store { const mapped = this.map(f) @@ -195,6 +241,7 @@ export abstract class Store { return newSource; } + public AsPromise(condition?: ((t: T) => boolean)): Promise { const self = this; condition = condition ?? (t => t !== undefined) @@ -209,35 +256,42 @@ export abstract class Store { } }) } - + } export class ImmutableStore extends Store { public readonly data: T; + private static readonly pass: (() => void) = () => { + } + constructor(data: T) { super(); this.data = data; } - addCallback(callback: (data: T) => void) { + addCallback(callback: (data: T) => void): (() => void) { // pass: data will never change + return ImmutableStore.pass } - addCallbackAndRun(callback: (data: T) => void) { + addCallbackAndRun(callback: (data: T) => void): (() => void) { callback(this.data) // no callback registry: data will never change + return ImmutableStore.pass } - addCallbackAndRunD(callback: (data: T) => void) { - if(this.data !== undefined){ + addCallbackAndRunD(callback: (data: T) => void): (() => void) { + if (this.data !== undefined) { callback(this.data) } // no callback registry: data will never change + return ImmutableStore.pass } - addCallbackD(callback: (data: T) => void) { + addCallbackD(callback: (data: T) => void): (() => void) { // pass: data will never change + return ImmutableStore.pass } @@ -247,11 +301,196 @@ export class ImmutableStore extends Store { } +/** + * Keeps track of the callback functions + */ +class ListenerTracker { + private readonly _callbacks: ((t: T) => (boolean | void | any)) [] = []; + + /** + * Adds a callback which can be called; a function to unregister is returned + */ + public addCallback(callback: (t: T) => (boolean | void | any)): (() => void) { + if (callback === console.log) { + // This ^^^ actually works! + throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." + } + this._callbacks.push(callback); + + // Give back an unregister-function! + return () => { + const index = this._callbacks.indexOf(callback) + if (index >= 0) { + this._callbacks.splice(index, 1) + } + } + } + + /** + * Call all the callbacks. + * Returns the number of registered callbacks + */ + public ping(data: T): number { + let toDelete = undefined + let startTime = new Date().getTime() / 1000; + for (const callback of this._callbacks) { + if (callback(data) === true) { + // This callback wants to be deleted + // Note: it has to return precisely true in order to avoid accidental deletions + if (toDelete === undefined) { + toDelete = [callback] + } else { + toDelete.push(callback) + } + } + } + let endTime = new Date().getTime() / 1000 + if ((endTime - startTime) > 500) { + console.trace("Warning: a ping took more then 500ms; this is probably a performance issue") + } + if (toDelete !== undefined) { + for (const toDeleteElement of toDelete) { + this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1) + } + } + return this._callbacks.length + } + + length() { + return this._callbacks.length + } +} + + +/** + * The mapped store is a helper type which does the mapping of a function. + * It'll fuse + */ +class MappedStore extends Store { + + private _upstream: Store; + private _unregisterFromUpstream: (() => void) + private _f: (t: TIn) => T; + private readonly _extraStores: Store[] | undefined; + private _unregisterFromExtraStores: (() => void)[] | undefined + + private _callbacks: ListenerTracker = new ListenerTracker() + + private static readonly pass: () => {} + + + constructor(upstream: Store, f: (t: TIn) => T, extraStores: Store[] = undefined, initialData : T= undefined) { + super(); + this._upstream = upstream; + this._f = f; + this._data = initialData ?? f(upstream.data) + this._extraStores = extraStores; + } + + private _data: T; + private _callbacksAreRegistered = false + + get data(): T { + return this._data + } + + + map(f: (t: T) => J, extraStores: (Store)[] = undefined): Store { + let stores: Store[] = undefined + if (extraStores?.length > 0 || this._extraStores?.length > 0) { + stores = [] + } + if (extraStores?.length > 0) { + stores.push(...extraStores) + } + if (this._extraStores?.length > 0) { + this._extraStores?.forEach(store => { + if (stores.indexOf(store) < 0) { + stores.push(store) + } + }) + } + return new MappedStore( + this._upstream, + data => f(this._f(data)), + stores, + f(this._data) + ); + } + + private unregisterFromUpstream() { + console.log("Unregistering callbacks for", this.tag) + this._callbacksAreRegistered = false; + this._unregisterFromUpstream() + this._unregisterFromExtraStores?.forEach(unr => unr()) + } + + private update(): void { + const newData = this._f(this._upstream.data) + if (this._data == newData) { + return; + } + this._data = newData + this._callbacks.ping(this._data) + } + + addCallback(callback: (data: T) => (any | boolean | void)): (() => void) { + if (!this._callbacksAreRegistered) { + const self = this + // This is the first callback that is added + // We register this 'map' to the upstream object and all the streams + this._unregisterFromUpstream = this._upstream.addCallback( + _ => self.update() + ) + this._unregisterFromExtraStores = this._extraStores?.map(store => + store?.addCallback(_ => self.update()) + ) + this._callbacksAreRegistered = true; + } + const unregister = this._callbacks.addCallback(callback) + return () => { + unregister() + if (this._callbacks.length() == 0) { + this.unregisterFromUpstream() + } + } + } + + addCallbackAndRun(callback: (data: T) => (any | boolean | void)): (() => void) { + const unregister = this.addCallback(callback) + const doRemove = callback(this.data) + if (doRemove === true) { + unregister() + return MappedStore.pass + } + return unregister + } + + addCallbackAndRunD(callback: (data: T) => (any | boolean | void)): (() => void) { + return this.addCallbackAndRun(data => { + if (data !== undefined) { + return callback(data) + } + }) + } + + addCallbackD(callback: (data: T) => (any | boolean | void)): (() => void) { + return this.addCallback(data => { + if (data !== undefined) { + return callback(data) + } + }) + } + + +} export class UIEventSource extends Store { public data: T; - private _callbacks: ((t: T) => (boolean | void | any)) [] = []; + private _callbacks: ListenerTracker = new ListenerTracker() + + private static readonly pass: () => {} constructor(data: T, tag: string = "") { super(tag); @@ -284,13 +523,13 @@ export class UIEventSource extends Store { * Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated. * If the promise fails, the value will stay undefined, but 'onError' will be called */ - public static FromPromise(promise: Promise, onError :( (e: any) => void) = undefined): UIEventSource { + public static FromPromise(promise: Promise, onError: ((e: any) => void) = undefined): UIEventSource { const src = new UIEventSource(undefined) promise?.then(d => src.setData(d)) promise?.catch(err => { - if(onError !== undefined){ + if (onError !== undefined) { onError(err) - }else{ + } else { console.warn("Promise failed:", err); } }) @@ -332,21 +571,33 @@ export class UIEventSource extends Store { * If the result of the callback is 'true', the callback is considered finished and will be removed again * @param callback */ - public addCallback(callback: ((latestData: T) => (boolean | void | any))): UIEventSource { - if (callback === console.log) { - // This ^^^ actually works! - throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." - } - this._callbacks.push(callback); - return this; + public addCallback(callback: ((latestData: T) => (boolean | void | any))): (() => void) { + return this._callbacks.addCallback(callback); } - public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): UIEventSource { + public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): (() => void) { const doDeleteCallback = callback(this.data); if (doDeleteCallback !== true) { - this.addCallback(callback); + return this.addCallback(callback); + } else { + return UIEventSource.pass } - return this; + } + + public addCallbackAndRunD(callback: (data: T) => void): (() => void) { + return this.addCallbackAndRun(data => { + if (data !== undefined && data !== null) { + return callback(data) + } + }) + } + + public addCallbackD(callback: (data: T) => void): (() => void) { + return this.addCallback(data => { + if (data !== undefined && data !== null) { + return callback(data) + } + }) } public setData(t: T): UIEventSource { @@ -354,33 +605,12 @@ export class UIEventSource extends Store { return; } this.data = t; - this.ping(); + this._callbacks.ping(t) return this; } public ping(): void { - let toDelete = undefined - let startTime = new Date().getTime() / 1000; - for (const callback of this._callbacks) { - if (callback(this.data) === true) { - // This callback wants to be deleted - // Note: it has to return precisely true in order to avoid accidental deletions - if (toDelete === undefined) { - toDelete = [callback] - } else { - toDelete.push(callback) - } - } - } - let endTime = new Date().getTime() / 1000 - if ((endTime - startTime) > 500) { - console.trace("Warning: a ping of ", this.tag, " took more then 500ms; this is probably a performance issue") - } - if (toDelete !== undefined) { - for (const toDeleteElement of toDelete) { - this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1) - } - } + this._callbacks.ping(this.data) } /** @@ -388,32 +618,29 @@ export class UIEventSource extends Store { * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' * @param f: The transforming function * @param extraSources: also trigger the update if one of these sources change + * + * const src = new UIEventSource(10) + * const store = src.map(i => i * 2) + * store.data // => 20 + * let srcSeen = undefined; + * src.addCallback(v => { + * console.log("Triggered") + * srcSeen = v + * }) + * let lastSeen = undefined + * store.addCallback(v => { + * console.log("Triggered!") + * lastSeen = v + * }) + * src.setData(21) + * srcSeen // => 21 + * lastSeen // => 42 */ public map(f: ((t: T) => J), - extraSources: Store[] = []): Store { - const self = this; - - const stack = new Error().stack.split("\n"); - const callee = stack[1] - - const newSource = new UIEventSource( - f(this.data), - "map(" + this.tag + ")@" + callee - ); - - const update = function () { - newSource.setData(f(self.data)); - return false; - } - - this.addCallback(update); - for (const extraSource of extraSources) { - extraSource?.addCallback(update); - } - - return newSource; + extraSources: Store[] = []): Store { + return new MappedStore(this, f, extraSources); } - + /** * Two way sync with functions in both directions * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' @@ -423,9 +650,9 @@ export class UIEventSource extends Store { * @param allowUnregister: if set, the update will be halted if no listeners are registered */ public sync(f: ((t: T) => J), - extraSources: Store[], - g: ((j: J, t: T) => T) , - allowUnregister = false): UIEventSource { + extraSources: Store[], + g: ((j: J, t: T) => T), + allowUnregister = false): UIEventSource { const self = this; const stack = new Error().stack.split("\n"); @@ -438,7 +665,7 @@ export class UIEventSource extends Store { const update = function () { newSource.setData(f(self.data)); - return allowUnregister && newSource._callbacks.length === 0 + return allowUnregister && newSource._callbacks.length() === 0 } this.addCallback(update); @@ -471,20 +698,4 @@ export class UIEventSource extends Store { return this; } - addCallbackAndRunD(callback: (data: T) => void) { - this.addCallbackAndRun(data => { - if (data !== undefined && data !== null) { - return callback(data) - } - }) - } - - addCallbackD(callback: (data: T) => void) { - this.addCallback(data => { - if (data !== undefined && data !== null) { - return callback(data) - } - }) - } - } diff --git a/UI/Input/CombinedInputElement.ts b/UI/Input/CombinedInputElement.ts index 2409898cd..adb86603a 100644 --- a/UI/Input/CombinedInputElement.ts +++ b/UI/Input/CombinedInputElement.ts @@ -19,15 +19,15 @@ export default class CombinedInputElement extends InputElement { this._b = b; this._split = split; this._combined = new Combine([this._a, this._b]); - this._value = this._a.GetValue().map( + this._value = this._a.GetValue().sync( t => combine(t, this._b?.GetValue()?.data), [this._b.GetValue()], - ) - .addCallback(x => { + x => { const [t, j] = split(x) - this._a.GetValue()?.setData(t) this._b.GetValue()?.setData(j) - }) + return t + } + ) } GetValue(): UIEventSource { diff --git a/UI/Input/InputElement.ts b/UI/Input/InputElement.ts index 68df36c85..46ec456f3 100644 --- a/UI/Input/InputElement.ts +++ b/UI/Input/InputElement.ts @@ -3,6 +3,7 @@ import BaseUIElement from "../BaseUIElement"; export interface ReadonlyInputElement extends BaseUIElement{ GetValue(): Store; + IsValid(t: T): boolean; } diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index c4ac7d38c..9c2145101 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -4,6 +4,7 @@ import {Utils} from "../../Utils"; export class RadioButton extends InputElement { private static _nextId = 0; + private readonly value: UIEventSource; private _elements: InputElement[]; private _selectFirstAsDefault: boolean; diff --git a/UI/Input/Toggle.ts b/UI/Input/Toggle.ts index c9bc59967..cfefec351 100644 --- a/UI/Input/Toggle.ts +++ b/UI/Input/Toggle.ts @@ -11,7 +11,7 @@ export default class Toggle extends VariableUiElement { public readonly isEnabled: Store; - constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: Store = new UIEventSource(false)) { + constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: Store) { super( isEnabled?.map(isEnabled => isEnabled ? showEnabled : showDisabled) ); diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 15fe25b7e..2e7957128 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -25,6 +25,7 @@ import Title from "../Base/Title"; import InputElementMap from "./InputElementMap"; import Translations from "../i18n/Translations"; import {Translation} from "../i18n/Translation"; +import BaseLayer from "../../Models/BaseLayer"; export class TextFieldDef { @@ -71,7 +72,7 @@ export class TextFieldDef { placeholder?: string | BaseUIElement, country?: () => string, location?: [number /*lat*/, number /*lon*/], - mapBackgroundLayer?: UIEventSource, + mapBackgroundLayer?: UIEventSource, unit?: Unit, args?: (string | number | boolean)[] // Extra arguments for the inputHelper, feature?: any, diff --git a/UI/Input/VariableInputElement.ts b/UI/Input/VariableInputElement.ts index d4de12af6..b79baf317 100644 --- a/UI/Input/VariableInputElement.ts +++ b/UI/Input/VariableInputElement.ts @@ -1,4 +1,4 @@ -import {InputElement, ReadonlyInputElement} from "./InputElement"; +import {ReadonlyInputElement} from "./InputElement"; import {Store} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; @@ -7,9 +7,9 @@ export default class VariableInputElement extends BaseUIElement implements Re private readonly value: Store; private readonly element: BaseUIElement - private readonly upstream: Store>; + private readonly upstream: Store>; - constructor(upstream: Store>) { + constructor(upstream: Store>) { super() this.upstream = upstream; this.value = upstream.bind(v => v.GetValue()) diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index 278e4ddbb..de997dd8d 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -1,4 +1,4 @@ -import {Store, UIEventSource} from "../../Logic/UIEventSource"; +import {ImmutableStore, Store} from "../../Logic/UIEventSource"; import Translations from "../i18n/Translations"; import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import Toggle from "../Input/Toggle"; @@ -29,7 +29,7 @@ export class SaveButton extends Toggle { super( save, pleaseLogin, - osmConnection?.isLoggedIn ?? new UIEventSource(false) + osmConnection?.isLoggedIn ?? new ImmutableStore(false) ) } diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index a3635d654..0180829c0 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -40,7 +40,7 @@ export default class TagRenderingQuestion extends Combine { constructor(tags: UIEventSource, configuration: TagRenderingConfig, - state, + state?: FeaturePipelineState, options?: { units?: Unit[], afterSave?: () => void, @@ -50,7 +50,6 @@ export default class TagRenderingQuestion extends Combine { } ) { - const applicableMappingsSrc = Stores.ListStabilized(tags.map(tags => { const applicableMappings: { if: TagsFilter, icon?: string, then: TypedTranslation, ifnot?: TagsFilter, addExtraTags: Tag[] }[] = [] @@ -82,12 +81,11 @@ export default class TagRenderingQuestion extends Combine { const feedback = new UIEventSource(undefined) const inputElement: ReadonlyInputElement = - new VariableInputElement(applicableMappingsSrc.map(applicableMappings => - TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback) + new VariableInputElement(applicableMappingsSrc.map(applicableMappings => { + return TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback) + } )) - - const save = () => { const selection = inputElement.GetValue().data; if (selection) { @@ -132,7 +130,7 @@ export default class TagRenderingQuestion extends Combine { saveButton]).SetClass("flex justify-end flex-wrap-reverse") ]).SetClass("flex mt-2 justify-between"), - new Toggle(Translations.t.general.testing.SetClass("alert"), undefined, state.featureSwitchIsTesting) + new Toggle(Translations.t.general.testing.SetClass("alert"), undefined, state?.featureSwitchIsTesting) ]) @@ -141,17 +139,16 @@ export default class TagRenderingQuestion extends Combine { private static GenerateInputElement( - state, + state: FeaturePipelineState, configuration: TagRenderingConfig, applicableMappings: { if: TagsFilter, then: TypedTranslation, icon?: string, ifnot?: TagsFilter, addExtraTags: Tag[] }[], applicableUnit: Unit, tagsSource: UIEventSource, feedback: UIEventSource - ): InputElement { + ): ReadonlyInputElement { // FreeForm input will be undefined if not present; will already contain a special input element if applicable const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback); - const hasImages = applicableMappings.findIndex(mapping => mapping.icon !== undefined) >= 0 let inputEls: InputElement[]; @@ -370,7 +367,7 @@ export default class TagRenderingQuestion extends Combine { return new Combine([new Img(mapping.icon).SetClass("mapping-icon-"+(mapping.iconClass ?? "small")), text]).SetClass("flex") } - private static GenerateFreeform(state, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource, feedback: UIEventSource) + private static GenerateFreeform(state: FeaturePipelineState, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource, feedback: UIEventSource) : InputElement { const freeform = configuration.freeform; if (freeform === undefined) { @@ -414,12 +411,12 @@ export default class TagRenderingQuestion extends Combine { } const tagsData = tags.data; - const feature = state.allElements.ContainingFeatures.get(tagsData.id) - const center = GeoOperations.centerpointCoordinates(feature) + const feature = state?.allElements?.ContainingFeatures?.get(tagsData.id) + const center = feature != undefined ? GeoOperations.centerpointCoordinates(feature) : [0,0] const input: InputElement = ValidatedTextField.ForType(configuration.freeform.type).ConstructInputElement({ country: () => tagsData._country, location: [center[1], center[0]], - mapBackgroundLayer: state.backgroundLayer, + mapBackgroundLayer: state?.backgroundLayer, unit: applicableUnit, args: configuration.freeform.helperArgs, feature, @@ -427,10 +424,12 @@ export default class TagRenderingQuestion extends Combine { feedback }); + // Init with correct value input.GetValue().setData(tagsData[freeform.key] ?? freeform.default); - input.GetValue().addCallbackD(v => { - if(v.length >= 255){ + // Add a length check + input.GetValue().addCallbackD((v : string | undefined) => { + if(v?.length >= 255){ feedback.setData(Translations.t.validation.tooLong.Subs({count: v.length})) } }) @@ -441,11 +440,9 @@ export default class TagRenderingQuestion extends Combine { ); if (freeform.inline) { - inputTagsFilter.SetClass("w-48-imp") inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags, state) inputTagsFilter.SetClass("block") - } return inputTagsFilter; @@ -470,7 +467,8 @@ export default class TagRenderingQuestion extends Combine { return new FixedUiElement(tagsStr).SetClass("subtle"); } return tagsFilter.asHumanString(true, true, tags.data); - } + }, + [state?.osmConnection?.userDetails] ) ).SetClass("block break-all") } diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index e2892092e..5b00a0412 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -91,7 +91,8 @@ "hu": "Mi a neve ennek a nyilvános könyvespolcnak?" }, "freeform": { - "key": "name" + "key": "name", + "inline": true }, "mappings": [ { diff --git a/test.ts b/test.ts index 479e4d9be..a48df7091 100644 --- a/test.ts +++ b/test.ts @@ -1,14 +1,25 @@ -import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import Img from "./UI/Base/Img"; -import { Utils } from "./Utils"; +import {UIEventSource} from "./Logic/UIEventSource"; +import TagRenderingQuestion from "./UI/Popup/TagRenderingQuestion"; +import TagRenderingConfig from "./Models/ThemeConfig/TagRenderingConfig"; -new FixedUiElement("Hi").AttachTo("maindiv") +const config = new TagRenderingConfig({ + question: "What is the name?", + render: "The name is {name}", + freeform: { + key: 'name', + inline:true + }, + mappings:[ + { + if:"noname=yes", + then: "This feature has no name" + } + ] +}) -window.setTimeout(() => { - new FixedUiElement("Loading...").AttachTo("maindiv") -// new Img("http://4.bp.blogspot.com/-_vTDmo_fSTw/T3YTV0AfGiI/AAAAAAAAAX4/Zjh2HaoU5Zo/s1600/beautiful%2Bkitten.jpg").AttachTo("maindiv") - Utils.download("http://127.0.0.1:1234/somedata").then(data => { - console.log("Got ", data) - return new FixedUiElement(data).AttachTo("extradiv"); - }) -}, 1000) +const tags = new UIEventSource({ + name: "current feature name" +}) + +new TagRenderingQuestion( + tags, config, undefined).AttachTo("maindiv") \ No newline at end of file