import {UIElement} from "../UI/UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; import {And, Tag, TagsFilter, TagUtils} from "../Logic/TagsFilter"; 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 {SubtleButton} from "../UI/Base/SubtleButton"; import Combine from "../UI/Base/Combine"; export class TagRendering extends UIElement implements TagDependantUIElement { private _priority: number; private _question: string | Translation; private _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[]; private _tagsPreprocessor?: ((tags: any) => any); private _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); private static injected = TagRendering.injectFunction(); 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 }[] }) { 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._tagsPreprocessor = function (properties) { if (options.tagsPreprocessor === undefined) { return properties; } const newTags = {}; for (const k in properties) { newTags[k] = properties[k]; } 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, txt: choice.txt, priority: choice.priority }; if (choice.substitute) { const newTags = this._tagsPreprocessor(this._source.data); choiceSubbed = { k: choice.k.substituteValues(newTags), 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); const save = () => { const selection = self._questionElement.GetValue().data; 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; if (csCount < State.userJourney.tagsVisibleAt) { return ""; } if (csCount < State.userJourney.tagsVisibleAndWikiLinked) { const tagsStr = tags.asHumanString(false); return new FixedUiElement(tagsStr).SetClass("subtle").Render(); } return tags.asHumanString(true); } ) ); 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 }[] }): InputElement { const elements = []; if (options.mappings !== undefined) { const previousTexts= []; for (const mapping of options.mappings) { if(mapping.k === null){ continue; } if(previousTexts.indexOf(mapping.txt) >= 0){ continue; } previousTexts.push(this.ApplyTemplate(mapping.txt)); elements.push(this.InputElementForMapping(mapping)); } } if (options.freeform !== undefined) { elements.push(this.InputForFreeForm(options.freeform)); } 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 }) { return new FixedInputElement(this.ApplyTemplate(mapping.txt), mapping.k); } private InputForFreeForm(freeform): InputElement { if (freeform === 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 = (str) => 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 (freeform.extraTags === undefined) { return tag; } return new And([ tag, freeform.extraTags ] ); }; const toString = (tag) => { if (tag instanceof And) { return toString(tag.and[0]) } else if (tag instanceof Tag) { return tag.value } return undefined; } let inputElement: InputElement; 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.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) { // 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?.osmConnection?.userDetails?.data?.loggedIn) { const question = this.ApplyTemplate(this._question).SetClass('question-text'); return "
" + new Combine([ question, "
", this._questionElement.Render(), "", ]).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 html = this.RenderAnswer().Render(); if (html === "") { return ""; } let editButton = ""; if (State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) { editButton = this._editButton.Render(); } return "" + "" + html + "" + editButton + ""; } return ""; } Priority(): number { return this._priority; } private ApplyTemplate(template: string | Translation): UIElement { if (template === undefined || template === null) { throw "Trying to apply a template, but the template is null/undefined" } const self = this; const tags = this._source.map(tags => self._tagsPreprocessor(self._source.data)); return new VariableUiElement(tags.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() })); } }