import {UIElement} from "../UI/UIElement"; import {UIEventSource} from "../UI/UIEventSource"; import {And, Tag, TagsFilter, TagUtils} from "../Logic/TagsFilter"; import {UIRadioButton} from "../UI/Base/UIRadioButton"; import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {SaveButton} from "../UI/SaveButton"; import {Changes} from "../Logic/Changes"; import {TextField} from "../UI/Base/TextField"; import {UIInputElement} from "../UI/Base/UIInputElement"; import {UIRadioButtonWithOther} from "../UI/Base/UIRadioButtonWithOther"; import {VariableUiElement} from "../UI/Base/VariableUIElement"; export class TagRenderingOptions { /** * Notes: by not giving a 'question', one disables the question form alltogether */ public options: { priority?: number; question?: string; primer?: string; freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string; renderTemplate: string; placeholder?: string; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string; priority?: number, substitute?: boolean }[] }; constructor(options: { priority?: number question?: string, primer?: string, tagsPreprocessor?: ((tags: any) => any), freeform?: { key: string, template: string, renderTemplate: string placeholder?: string, extraTags?: TagsFilter, }, mappings?: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[] }) { this.options = options; } IsQuestioning(tags: any): boolean { const tagsKV = TagUtils.proprtiesToKV(tags); for (const oneOnOneElement of this.options.mappings) { if (oneOnOneElement.k.matches(tagsKV)) { return false; } } if (this.options.freeform !== undefined && tags[this.options.freeform.key] !== undefined) { return false; } if (this.options.question === undefined) { return false; } return true; } } export class TagRendering extends UIElement { public elementPriority: number; private _question: string; private _primer: string; private _mapping: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[]; private _tagsPreprocessor?: ((tags: any) => any); private _freeform: { key: string, template: string, renderTemplate: string, placeholder?: string, extraTags?: TagsFilter }; private readonly _questionElement: UIElement; private readonly _textField: TextField; // Only here to update private readonly _saveButton: UIElement; private readonly _skipButton: UIElement; private readonly _editButton: UIElement; private readonly _questionSkipped: UIEventSource = new UIEventSource(false); private readonly _editMode: UIEventSource = new UIEventSource(false); constructor(tags: UIEventSource, changes: Changes, options: { priority?: number question?: string, primer?: string, freeform?: { key: string, template: string, renderTemplate: string placeholder?: string, extraTags?: TagsFilter, }, tagsPreprocessor?: ((tags: any) => any), mappings?: { k: TagsFilter, txt: string, priority?: number, substitute?: boolean }[] }) { super(tags); const self = this; this.ListenTo(this._questionSkipped); this.ListenTo(this._editMode); this._question = options.question; this._primer = options.primer ?? ""; this._tagsPreprocessor = options.tagsPreprocessor; this._mapping = []; this._freeform = options.freeform; this.elementPriority = options.priority ?? 0; // Prepare the choices for the Radio buttons let i = 0; const choices: UIElement[] = []; for (const choice of options.mappings ?? []) { if (choice.k === null) { this._mapping.push(choice); continue; } let choiceSubbed = choice; if (choice.substitute) { choiceSubbed = { k : choice.k.substituteValues( options.tagsPreprocessor(this._source.data)), txt : this.ApplyTemplate(choice.txt), substitute: false, priority: choice.priority } } choices.push(new FixedUiElement(choiceSubbed.txt)); this._mapping.push(choiceSubbed); i++; } // Map radiobutton choice and textfield answer onto tagfilter. That tagfilter will be pushed into the changes later on const pickChoice = (i => { if (i === undefined || i === null) { return undefined } return self._mapping[i].k }); const pickString = (string) => { if (string === "" || string === undefined) { return undefined; } const tag = new Tag(self._freeform.key, string); if (self._freeform.extraTags === undefined) { return tag; } return new And([ self._freeform.extraTags, tag ] ); }; // Prepare the actual input element -> pick an appropriate implementation let inputElement: UIInputElement; if (this._freeform !== undefined && this._mapping !== undefined) { // Radio buttons with 'other' inputElement = new UIRadioButtonWithOther( choices, this._freeform.template, this._freeform.placeholder, pickChoice, pickString ); this._questionElement = inputElement; } else if (this._mapping !== undefined) { // This is a classic radio selection element inputElement = new UIRadioButton(new UIEventSource(choices), pickChoice) this._questionElement = inputElement; } else if (this._freeform !== undefined) { this._textField = new TextField(new UIEventSource(this._freeform.placeholder), pickString); inputElement = this._textField; this._questionElement = new FixedUiElement(this._freeform.template.replace("$$$", inputElement.Render())) } else { throw "Invalid questionRendering, expected at least choices or a freeform" } const save = () => { const selection = inputElement.GetValue().data; if (selection) { changes.addTag(tags.data.id, selection); } self._editMode.setData(false); } const cancel = () => { self._questionSkipped.setData(true); self._editMode.setData(false); } // Setup the save button and it's action this._saveButton = new SaveButton(inputElement.GetValue()) .onClick(save); if (this._question !== undefined) { this._editButton = new FixedUiElement("edit") .onClick(() => { console.log("Click", self._editButton); if (self._textField) { self._textField.value.setData(self._source.data["name"] ?? ""); } self._editMode.setData(true); }); } else { this._editButton = new FixedUiElement(""); } const cancelContents = this._editMode.map((isEditing) => { if (isEditing) { return "Annuleren"; } else { return "Ik weet het niet zeker..."; } }); // And at last, set up the skip button this._skipButton = new VariableUiElement(cancelContents).onClick(cancel); } private ApplyTemplate(template: string): string { let tags = this._source.data; if (this._tagsPreprocessor !== undefined) { tags = this._tagsPreprocessor(tags); } return TagUtils.ApplyTemplate(template, tags); } 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; } 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 RenderAnwser(): string { const tags = TagUtils.proprtiesToKV(this._source.data); let freeform = ""; let freeformScore = -10; if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) { freeform = this.ApplyTemplate(this._freeform.renderTemplate); freeformScore = 0; } if (this._mapping !== undefined) { 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._primer + this.ApplyTemplate(highestTemplate); } } else { return freeform; } } protected InnerRender(): string { if (this.IsQuestioning() || this._editMode.data) { // Not yet known or questioning, we have to ask a question return "
" + this._question + (this._question !== "" ? "
" : "") + this._questionElement.Render() + this._skipButton.Render() + this._saveButton.Render() + "
" } if (this.IsKnown()) { const html = this.RenderAnwser(); if (html == "") { return ""; } return "" + "" + html + "" + this._editButton.Render() + ""; } return ""; } InnerUpdate(htmlElement: HTMLElement) { super.InnerUpdate(htmlElement); this._questionElement.Update(); this._saveButton.Update(); this._skipButton.Update(); this._textField?.Update(); this._editButton.Update(); } }