mapcomplete/Customizations/TagRendering.ts
Pieter Vander Vennet d1f8080c24 New question system
2020-07-05 18:59:47 +02:00

358 lines
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();
}
}