import {UIElement} from "../UI/UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; import {And, Tag, TagsFilter, TagUtils} from "../Logic/Tags"; import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {SaveButton} from "../UI/SaveButton"; import {VariableUiElement} from "../UI/Base/VariableUIElement"; import {TagDependantUIElement} from "./UIElementConstructor"; import {TextField, ValidatedTextField} from "../UI/Input/TextField"; import {InputElement} from "../UI/Input/InputElement"; import {InputElementWrapper} from "../UI/Input/InputElementWrapper"; import {FixedInputElement} from "../UI/Input/FixedInputElement"; import {RadioButton} from "../UI/Input/RadioButton"; import Translations from "../UI/i18n/Translations"; import Locale from "../UI/i18n/Locale"; import {State} from "../State"; import {TagRenderingOptions} from "./TagRenderingOptions"; import Translation from "../UI/i18n/Translation"; import Combine from "../UI/Base/Combine"; export class TagRendering extends UIElement implements TagDependantUIElement { private readonly _priority: number; private readonly _question: string | Translation; private readonly _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[]; private currentTags: UIEventSource; private readonly _freeform: { key: string, template: string | UIElement, renderTemplate: string | Translation, placeholder?: string | UIElement, extraTags?: TagsFilter }; private readonly _questionElement: InputElement; private readonly _saveButton: UIElement; private readonly _friendlyLogin: UIElement; private readonly _skipButton: UIElement; private readonly _editButton: UIElement; private readonly _appliedTags: UIElement; private readonly _questionSkipped: UIEventSource = new UIEventSource(false); private readonly _editMode: UIEventSource = new UIEventSource(false); static injectFunction() { // This is a workaround as not to import tagrendering into TagREnderingOptions TagRenderingOptions.tagRendering = (tags, options) => new TagRendering(tags, options); return true; } constructor(tags: UIEventSource, options: { priority?: number question?: string | Translation, freeform?: { key: string, template: string | Translation, renderTemplate: string | Translation, placeholder?: string | Translation, extraTags?: TagsFilter, }, tagsPreprocessor?: ((tags: any) => any), mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[] }) { super(tags); this.ListenTo(Locale.language); this.ListenTo(this._questionSkipped); this.ListenTo(this._editMode); this.ListenTo(State.state?.osmConnection?.userDetails); const self = this; this._priority = options.priority ?? 0; this.currentTags = this._source.map(tags => { if (options.tagsPreprocessor === undefined) { return tags; } // we clone the tags... let newTags = {}; for (const k in tags) { newTags[k] = tags[k]; } // ... in order to safely edit them here options.tagsPreprocessor(newTags); return newTags; } ); if (options.question !== undefined) { this._question = options.question; } this._mapping = []; this._freeform = options.freeform; for (const choice of options.mappings ?? []) { let choiceSubbed = { k: choice.k?.substituteValues(this.currentTags.data), txt: choice.txt, priority: choice.priority } this._mapping.push({ k: choiceSubbed.k, txt: choiceSubbed.txt }); } // Prepare the actual input element -> pick an appropriate implementation this._questionElement = this.InputElementFor(options) ?? new FixedInputElement("No input possible", new Tag("a","b")); const save = () => { const selection = self._questionElement.GetValue().data; console.log("Tagrendering: saving tags ", selection); if (selection) { State.state?.changes?.addTag(tags.data.id, selection); } self._editMode.setData(false); } this._appliedTags = new VariableUiElement( self._questionElement.GetValue().map( (tags: TagsFilter) => { if (tags === undefined) { return Translations.t.general.noTagsSelected.SetClass("subtle").Render(); } const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000; if (csCount < State.userJourney.tagsVisibleAt) { return ""; } if (csCount < State.userJourney.tagsVisibleAndWikiLinked) { const tagsStr = tags.asHumanString(false, true); return new FixedUiElement(tagsStr).SetClass("subtle").Render(); } return tags.asHumanString(true, true); } ) ).ListenTo(self._questionElement); const cancel = () => { self._questionSkipped.setData(true); self._editMode.setData(false); self._source.ping(); // Send a ping upstream to render the next question } // Setup the save button and it's action this._saveButton = new SaveButton(this._questionElement.GetValue()) .onClick(save); this._friendlyLogin = Translations.t.general.loginToStart .onClick(() => State.state.osmConnection.AttemptLogin()) this._editButton = new FixedUiElement(""); if (this._question !== undefined) { this._editButton = new FixedUiElement( "edit") .onClick(() => { self._editMode.setData(true); self._questionElement.GetValue().setData(self.CurrentValue()); }); } const cancelContents = this._editMode.map((isEditing) => { if (isEditing) { return "" + Translations.t.general.cancel.R() + ""; } else { return "" + Translations.t.general.skip.R() + ""; } }, [Locale.language]); // And at last, set up the skip button this._skipButton = new VariableUiElement(cancelContents).onClick(cancel); } private InputElementFor(options: { freeform?: { key: string, template: string | Translation, renderTemplate: string | Translation, placeholder?: string | Translation, extraTags?: TagsFilter, }, mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[] }): InputElement { const elements = []; let freeformElement = undefined; if (options.freeform !== undefined) { freeformElement = this.InputForFreeForm(options.freeform); } if (options.mappings !== undefined) { const previousTexts= []; for (const mapping of options.mappings) { if(mapping.k === null){ continue; } if(mapping.hideInAnswer){ continue; } previousTexts.push(this.ApplyTemplate(mapping.txt)); elements.push(this.InputElementForMapping(mapping, mapping.substitute)); } } if(freeformElement !== undefined) { elements.push(freeformElement); } if (elements.length == 0) { return new FixedInputElement("This should not happen: no tag renderings defined", undefined); } if (elements.length == 1) { return elements[0]; } return new RadioButton(elements, false); } private InputElementForMapping(mapping: { k: TagsFilter, txt: (string | Translation) }, substituteValues: boolean) { if (substituteValues) { return new FixedInputElement(this.ApplyTemplate(mapping.txt), mapping.k.substituteValues(this.currentTags.data), (t0, t1) => t0.isEquivalent(t1) ); } return new FixedInputElement(this.ApplyTemplate(mapping.txt),mapping.k, (t0, t1) => t0.isEquivalent(t1)); } private InputForFreeForm(freeform : { key: string, template: string | Translation, renderTemplate: string | Translation, placeholder?: string | Translation, extraTags?: TagsFilter, }): InputElement { if (freeform?.template === undefined) { return undefined; } const prepost = Translations.W(freeform.template).InnerRender() .replace("$$$", "$string$") .split("$"); const type = prepost[1]; let isValid = ValidatedTextField.inputValidation[type]; if (isValid === undefined) { isValid = () => true; } let formatter = ValidatedTextField.formatting[type] ?? ((str) => str); const pickString = (string: any) => { if (string === "" || string === undefined) { return undefined; } if (!isValid(string, this._source.data._country)) { return undefined; } const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); if (tag.value.length > 255) { return undefined; // Too long } if (freeform.extraTags === undefined) { return tag; } return new And([ tag, freeform.extraTags ] ); }; const toString = (tag) => { if (tag instanceof And) { for (const subtag of tag.and) { if(subtag instanceof Tag && subtag.key === freeform.key){ return subtag.value; } } return undefined; } else if (tag instanceof Tag) { return tag.value } return undefined; } const textField = new TextField({ placeholder: this._freeform.placeholder, fromString: pickString, toString: toString }); const pre = prepost[0] !== undefined ? this.ApplyTemplate(prepost[0]) : ""; const post = prepost[2] !== undefined ? this.ApplyTemplate(prepost[2]) : ""; return new InputElementWrapper(pre, textField, post); } IsKnown(): boolean { const tags = TagUtils.proprtiesToKV(this._source.data); for (const oneOnOneElement of this._mapping) { if (oneOnOneElement.k === null || oneOnOneElement.k === undefined || oneOnOneElement.k.matches(tags)) { return true; } } return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined; } IsSkipped(): boolean { return this._questionSkipped.data; } private CurrentValue(): TagsFilter { const tags = TagUtils.proprtiesToKV(this._source.data); for (const oneOnOneElement of this._mapping) { if (oneOnOneElement.k !== null && oneOnOneElement.k.matches(tags)) { return oneOnOneElement.k; } } if (this._freeform === undefined) { return undefined; } return new Tag(this._freeform.key, this._source.data[this._freeform.key]); } IsQuestioning(): boolean { if (this.IsKnown()) { return false; } if (this._question === undefined || this._question === "" || (this._freeform?.template === undefined && (this._mapping?.length ?? 0) == 0)) { // We don't ask this question in the first place return false; } if (this._questionSkipped.data) { // We don't ask for this question anymore, skipped by user return false; } return true; } private RenderAnswer(): UIElement { const tags = TagUtils.proprtiesToKV(this._source.data); let freeform: UIElement = new FixedUiElement(""); let freeformScore = -10; if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) { freeform = this.ApplyTemplate(this._freeform.renderTemplate); freeformScore = 0; } let highestScore = -100; let highestTemplate = undefined; for (const oneOnOneElement of this._mapping) { if (oneOnOneElement.k == null || oneOnOneElement.k.matches(tags)) { // We have found a matching key -> we use the template, but only if it scores better let score = oneOnOneElement.priority ?? (oneOnOneElement.k === null ? -1 : 0); if (score > highestScore) { highestScore = score; highestTemplate = oneOnOneElement.txt } } } if (freeformScore > highestScore) { return freeform; } if (highestTemplate !== undefined) { // we render the found template return this.ApplyTemplate(highestTemplate); } } InnerRender(): string { if (this.IsQuestioning() && (State.state !== undefined) // If State.state is undefined, we are testing/custom theme building -> show regular save && !State.state.osmConnection.userDetails.data.loggedIn) { const question = this.ApplyTemplate(this._question).SetClass('question-text'); return "
" + new Combine([ question.Render(), "
", this._questionElement, "", ]).Render() + "
"; } if (this.IsQuestioning() || this._editMode.data) { // Not yet known or questioning, we have to ask a question return "
" + new Combine([ "", this.ApplyTemplate(this._question), "", "
", "
", this._questionElement , "
", this._skipButton, this._saveButton, "
", this._appliedTags ]).Render() + "
" } if (this.IsKnown()) { const answer = this.RenderAnswer(); if(answer.IsEmpty()){ return ""; } let editButton; if (State.state === undefined || // state undefined -> we are custom testing State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) { editButton = this._editButton; } return new Combine([ "", "", answer, "", editButton ?? "", ""]).Render(); } return ""; } Priority(): number { return this._priority; } private ApplyTemplate(template: string | Translation): UIElement { if (template === undefined || template === null) { return undefined; } return new VariableUiElement(this.currentTags.map(tags => { const tr = Translations.WT(template); if (tr.Subs === undefined) { // This is a weird edge case return tr.InnerRender(); } return tr.Subs(tags).InnerRender() })); } }