forked from MapComplete/MapComplete
New question system
This commit is contained in:
parent
1738fc4252
commit
d1f8080c24
45 changed files with 1391 additions and 689 deletions
358
Customizations/TagRendering.ts
Normal file
358
Customizations/TagRendering.ts
Normal file
|
@ -0,0 +1,358 @@
|
|||
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();
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue