forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			358 lines
		
	
	
		
			No EOL
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			358 lines
		
	
	
		
			No EOL
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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<TagsFilter>; // Only here to update
 | |
| 
 | |
|     private readonly _saveButton: UIElement;
 | |
|     private readonly _skipButton: UIElement;
 | |
|     private readonly _editButton: UIElement;
 | |
| 
 | |
|     private readonly _questionSkipped: UIEventSource<boolean> = new UIEventSource<boolean>(false);
 | |
| 
 | |
|     private readonly _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
 | |
| 
 | |
| 
 | |
|     constructor(tags: UIEventSource<any>, 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<TagsFilter>;
 | |
| 
 | |
|         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<string>(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("<img class='editbutton' src='./assets/pencil.svg' alt='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 "<span class='skip-button'>Annuleren</span>";
 | |
|             } else {
 | |
|                 return "<span class='skip-button'>Ik weet het niet zeker...</span>";
 | |
|             }
 | |
|         });
 | |
|         // 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 "<div class='question'>" +
 | |
|                 this._question +
 | |
|                 (this._question !== "" ? "<br/>" : "") +
 | |
|                 this._questionElement.Render() +
 | |
|                 this._skipButton.Render() +
 | |
|                 this._saveButton.Render() +
 | |
|                 "</div>"
 | |
|         }
 | |
| 
 | |
|         if (this.IsKnown()) {
 | |
|             const html = this.RenderAnwser();
 | |
|             if (html == "") {
 | |
|                 return "";
 | |
|             }
 | |
|             return "<span class='answer'>" +
 | |
|                 "<span class='answer-text'>" + html + "</span>" + this._editButton.Render() +
 | |
|                 "</span>";
 | |
|         }
 | |
| 
 | |
|         return "";
 | |
| 
 | |
|     }
 | |
| 
 | |
|     InnerUpdate(htmlElement: HTMLElement) {
 | |
|         super.InnerUpdate(htmlElement);
 | |
|         this._questionElement.Update();
 | |
|         this._saveButton.Update();
 | |
|         this._skipButton.Update();
 | |
|         this._textField?.Update();
 | |
|         this._editButton.Update();
 | |
|     }
 | |
| 
 | |
| } |