New question system

This commit is contained in:
Pieter Vander Vennet 2020-07-05 18:59:47 +02:00
parent 1738fc4252
commit d1f8080c24
45 changed files with 1391 additions and 689 deletions

View file

@ -1,24 +1,32 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
import {UIInputElement} from "./UIInputElement";
export class TextField extends UIElement {
export class TextField<T> extends UIInputElement<T> {
public value = new UIEventSource("");
public value: UIEventSource<string> = new UIEventSource<string>("");
/**
* Pings and has the value data
*/
public enterPressed = new UIEventSource<string>(undefined);
private _placeholder: UIEventSource<string>;
private _mapping: (string) => T;
constructor(placeholder : UIEventSource<string>) {
constructor(placeholder: UIEventSource<string>,
mapping: ((string) => T)) {
super(placeholder);
this._placeholder = placeholder;
this._mapping = mapping;
}
GetValue(): UIEventSource<T> {
return this.value.map(this._mapping);
}
protected InnerRender(): string {
return "<form onSubmit='return false' class='form-text-field'>" +
"<input type='text' placeholder='"+this._placeholder.data+"' id='text-" + this.id + "'>" +
"<input type='text' placeholder='" + (this._placeholder.data ?? "") + "' id='text-" + this.id + "'>" +
"</form>";
}
@ -27,19 +35,24 @@ export class TextField extends UIElement {
const field = document.getElementById('text-' + this.id);
const self = this;
field.oninput = () => {
// @ts-ignore
self.value.setData(field.value);
};
field.addEventListener("keyup", function (event) {
if (event.key === "Enter") {
// @ts-ignore
self.enterPressed.setData(field.value);
}
});
}
Clear() {
const field = document.getElementById('text-' + this.id);
if (field !== undefined) {
// @ts-ignore
field.value = "";
}
}

View file

@ -0,0 +1,8 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
export abstract class UIInputElement<T> extends UIElement{
abstract GetValue() : UIEventSource<T>;
}

View file

@ -1,30 +1,33 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../UIEventSource";
import {FixedUiElement} from "./FixedUiElement";
import $ from "jquery"
import {UIInputElement} from "./UIInputElement";
export class UIRadioButton extends UIElement {
export class UIRadioButton<T> extends UIInputElement<T> {
public readonly SelectedElementIndex: UIEventSource<{ index: number, value: string }>
= new UIEventSource<{ index: number, value: string }>(null);
public readonly SelectedElementIndex: UIEventSource<number>
= new UIEventSource<number>(null);
private readonly _elements: UIEventSource<{ element: UIElement, value: string }[]>
private readonly _elements: UIEventSource<UIElement[]>
private _selectFirstAsDefault: boolean;
private _valueMapping: (i: number) => T;
constructor(elements: UIEventSource<{ element: UIElement, value: string }[]>) {
constructor(elements: UIEventSource<UIElement[]>,
valueMapping: ((i: number) => T),
selectFirstAsDefault = true) {
super(elements);
this._elements = elements;
this._selectFirstAsDefault = selectFirstAsDefault;
const self = this;
this._valueMapping = valueMapping;
this.SelectedElementIndex.addCallback(() => {
self.InnerUpdate(undefined);
})
}
GetValue(): UIEventSource<T> {
return this.SelectedElementIndex.map(this._valueMapping);
}
static FromStrings(choices: string[]): UIRadioButton {
const wrapped = [];
for (const choice of choices) {
wrapped.push({
element: new FixedUiElement(choice),
value: choice
});
}
return new UIRadioButton(new UIEventSource<{ element: UIElement, value: string }[]>(wrapped))
}
private IdFor(i) {
return 'radio-' + this.id + '-' + i;
@ -35,12 +38,9 @@ export class UIRadioButton extends UIElement {
let body = "";
let i = 0;
for (const el of this._elements.data) {
const uielement = el.element;
const value = el.value;
const htmlElement =
'<input type="radio" id="' + this.IdFor(i) + '" name="radiogroup-' + this.id + '" value="' + value + '">' +
'<label for="' + this.IdFor(i) + '">' + uielement.Render() + '</label>' +
'<input type="radio" id="' + this.IdFor(i) + '" name="radiogroup-' + this.id + '">' +
'<label for="' + this.IdFor(i) + '">' + el.Render() + '</label>' +
'<br>';
body += htmlElement;
@ -51,7 +51,6 @@ export class UIRadioButton extends UIElement {
}
InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
const self = this;
function checkButtons() {
@ -59,8 +58,7 @@ export class UIRadioButton extends UIElement {
const el = document.getElementById(self.IdFor(i));
// @ts-ignore
if (el.checked) {
var v = {index: i, value: self._elements.data[i].value}
self.SelectedElementIndex.setData(v);
self.SelectedElementIndex.setData(i);
}
}
}
@ -74,29 +72,32 @@ export class UIRadioButton extends UIElement {
);
if (this.SelectedElementIndex.data == null) {
const el = document.getElementById(this.IdFor(0));
el.checked = true;
checkButtons();
if (this._selectFirstAsDefault) {
const el = document.getElementById(this.IdFor(0));
// @ts-ignore
el.checked = true;
checkButtons();
}
} else {
// We check that what is selected matches the previous rendering
var checked = -1;
var expected = -1
for (let i = 0; i < self._elements.data.length; i++) {
const el = document.getElementById(self.IdFor(i));
// @ts-ignore
if (el.checked) {
checked = i;
var expected = this.SelectedElementIndex.data;
if (expected) {
for (let i = 0; i < self._elements.data.length; i++) {
const el = document.getElementById(self.IdFor(i));
// @ts-ignore
if (el.checked) {
checked = i;
}
}
if (el.value === this.SelectedElementIndex.data.value) {
expected = i;
if (expected != checked) {
const el = document.getElementById(this.IdFor(expected));
// @ts-ignore
el.checked = true;
}
}
if (expected != checked) {
const el = document.getElementById(this.IdFor(expected));
// @ts-ignore
el.checked = true;
}
}

View file

@ -0,0 +1,72 @@
import {UIInputElement} from "./UIInputElement";
import {UIEventSource} from "../UIEventSource";
import {UIRadioButton} from "./UIRadioButton";
import {UIElement} from "../UIElement";
import {TextField} from "./TextField";
import {FixedUiElement} from "./FixedUiElement";
export class UIRadioButtonWithOther<T> extends UIInputElement<T> {
private readonly _radioSelector: UIRadioButton<T>;
private readonly _freeformText: TextField<T>;
private readonly _value: UIEventSource<T> = new UIEventSource<T>(undefined)
constructor(choices: UIElement[],
otherChoiceTemplate: string,
placeholder: string,
choiceToValue: ((i: number) => T),
stringToValue: ((string: string) => T)) {
super(undefined);
const self = this;
this._freeformText = new TextField(
new UIEventSource<string>(placeholder),
stringToValue);
const otherChoiceElement = new FixedUiElement(
otherChoiceTemplate.replace("$$$", this._freeformText.Render()));
choices.push(otherChoiceElement);
this._radioSelector = new UIRadioButton(new UIEventSource(choices),
(i) => {
if (i === undefined || i === null) {
return undefined;
}
if (i + 1 >= choices.length) {
return this._freeformText.GetValue().data
}
return choiceToValue(i);
},
false);
this._radioSelector.GetValue().addCallback(
(i) => {
self._value.setData(i);
});
this._freeformText.GetValue().addCallback((str) => {
self._value.setData(str);
}
);
this._freeformText.onClick(() => {
self._radioSelector.SelectedElementIndex.setData(choices.length - 1);
})
}
GetValue(): UIEventSource<T> {
return this._value;
}
protected InnerRender(): string {
return this._radioSelector.Render();
}
InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
this._radioSelector.Update();
this._freeformText.Update();
}
}

View file

@ -1,14 +1,15 @@
import {UIElement} from "./UIElement";
import {TagMapping, TagMappingOptions} from "./TagMapping";
import {Question, QuestionDefinition} from "../Logic/Question";
import {UIEventSource} from "./UIEventSource";
import {QuestionPicker} from "./QuestionPicker";
import {OsmImageUploadHandler} from "../Logic/OsmImageUploadHandler";
import {ImageCarousel} from "./Image/ImageCarousel";
import {Changes} from "../Logic/Changes";
import {UserDetails} from "../Logic/OsmConnection";
import {CommonTagMappings} from "../Layers/CommonTagMappings";
import {VerticalCombine} from "./Base/VerticalCombine";
import {TagRendering, TagRenderingOptions} from "../Customizations/TagRendering";
import {OsmLink} from "../Customizations/Questions/OsmLink";
import {WikipediaLink} from "../Customizations/Questions/WikipediaLink";
import {And} from "../Logic/TagsFilter";
export class FeatureInfoBox extends UIElement {
@ -17,7 +18,6 @@ export class FeatureInfoBox extends UIElement {
private _title: UIElement;
private _osmLink: UIElement;
private _infoElements: UIElement[]
private _questions: QuestionPicker;
@ -27,15 +27,16 @@ export class FeatureInfoBox extends UIElement {
private _imageElement: ImageCarousel;
private _pictureUploader: UIElement;
private _wikipedialink: UIElement;
private _infoboxes: TagRendering[];
constructor(
tagsES: UIEventSource<any>,
elementsToShow: (TagMappingOptions | QuestionDefinition | UIElement)[],
questions: QuestionDefinition[],
title: TagRenderingOptions,
elementsToShow: TagRenderingOptions[],
changes: Changes,
userDetails: UIEventSource<UserDetails>,
preferedPictureLicense : UIEventSource<string>
preferedPictureLicense: UIEventSource<string>
) {
super(tagsES);
this._tagsES = tagsES;
@ -45,48 +46,66 @@ export class FeatureInfoBox extends UIElement {
this._imageElement = new ImageCarousel(this._tagsES);
this._questions = new QuestionPicker(
this._changes.asQuestions(questions), this._tagsES);
var infoboxes: UIElement[] = [];
for (const uiElement of elementsToShow) {
if (uiElement instanceof QuestionDefinition) {
const questionDef = uiElement as QuestionDefinition;
const question = new Question(this._changes, questionDef);
infoboxes.push(question.CreateHtml(this._tagsES));
} else if (uiElement instanceof TagMappingOptions) {
const tagMappingOpt = uiElement as TagMappingOptions;
infoboxes.push(new TagMapping(tagMappingOpt, this._tagsES))
} else {
const ui = uiElement as UIElement;
infoboxes.push(ui);
this._infoboxes = [];
for (const tagRenderingOption of elementsToShow) {
if (tagRenderingOption.options === undefined) {
throw "Tagrendering.options not defined"
}
this._infoboxes.push(new TagRendering(this._tagsES, this._changes, tagRenderingOption.options))
}
this._title = infoboxes.shift();
this._infoElements = infoboxes;
title = title ?? new TagRenderingOptions(
{
mappings: [{k: new And([]), txt: ""}]
}
)
this._osmLink = new TagMapping(CommonTagMappings.osmLink, this._tagsES);
this._wikipedialink = new TagMapping(CommonTagMappings.wikipediaLink, this._tagsES);
this._title = new TagRendering(this._tagsES, this._changes, title.options);
this._osmLink = new TagRendering(this._tagsES, this._changes, new OsmLink().options);
this._wikipedialink = new TagRendering(this._tagsES, this._changes, new WikipediaLink().options);
this._pictureUploader = new OsmImageUploadHandler(tagsES, userDetails, preferedPictureLicense,
changes, this._imageElement.slideshow).getUI();
}
InnerRender(): string {
let questions = "";
if (this._userDetails.data.loggedIn) {
// Questions is embedded in a span, because it'll hide the parent when the questions dissappear
questions = "<span>"+this._questions.HideOnEmpty(true).Render()+"</span>";
const info = [];
const questions = [];
for (const infobox of this._infoboxes) {
if (infobox.IsKnown()) {
info.push(infobox);
} else if (infobox.IsQuestioning()) {
questions.push(infobox);
}
}
let questionsHtml = "";
if (this._userDetails.data.loggedIn && questions.length > 0) {
// We select the most important question and render that one
let mostImportantQuestion;
let score = -1000;
for (const question of questions) {
if (mostImportantQuestion === undefined || question.priority > score) {
mostImportantQuestion = question;
score = question.priority;
}
}
questionsHtml = mostImportantQuestion.Render();
}
return "<div class='featureinfobox'>" +
"<div class='featureinfoboxtitle'>" +
"<span>" + this._title.Render() + "</span>" +
"<span>" +
this._title.Render() +
"</span>" +
this._wikipedialink.Render() +
this._osmLink.Render() +
"</div>" +
@ -96,9 +115,9 @@ export class FeatureInfoBox extends UIElement {
this._imageElement.Render() +
this._pictureUploader.Render() +
new VerticalCombine(this._infoElements, 'infobox-information').HideOnEmpty(true).Render() +
new VerticalCombine(info, "infobox-information ").Render() +
questions +
questionsHtml +
"</div>" +
@ -110,11 +129,18 @@ export class FeatureInfoBox extends UIElement {
super.Activate();
this._imageElement.Activate();
this._pictureUploader.Activate();
for (const infobox of this._infoboxes) {
infobox.Activate();
}
}
Update() {
super.Update();
this._imageElement.Update();
this._pictureUploader.Update();
this._title.Update();
for (const infobox of this._infoboxes) {
infobox.Update();
}
}
}

View file

@ -39,7 +39,7 @@ export class QuestionPicker extends UIElement {
return "Er zijn geen vragen meer!";
}
return "<div class='infobox-questions'>" +
return "<div class='question'>" +
highestQ.CreateHtml(this.source).Render() +
"</div>";
}

25
UI/SaveButton.ts Normal file
View file

@ -0,0 +1,25 @@
import {UIEventSource} from "./UIEventSource";
import {UIElement} from "./UIElement";
export class SaveButton extends UIElement {
private _value: UIEventSource<any>;
constructor(value: UIEventSource<any>) {
super(value);
if(value === undefined){
throw "No event source for savebutton, something is wrong"
}
this._value = value;
}
protected InnerRender(): string {
if (this._value.data === undefined ||
this._value.data === null
|| this._value.data === ""
) {
return "<span class='save-non-active'>Opslaan</span>"
}
return "<span class='save'>Opslaan</span>";
}
}

View file

@ -60,8 +60,8 @@ export class SearchAndGo extends UIElement {
protected InnerRender(): string {
// "<img class='search' src='./assets/search.svg' alt='Search'> " +
return this._goButton.Render() +
this._searchField.Render();
return this._searchField.Render() +
this._goButton.Render();
}

View file

@ -1,80 +0,0 @@
import {UIElement} from "./UIElement";
import {UIEventSource} from "./UIEventSource";
export class TagMappingOptions {
key: string;// The key to show
mapping?: any;// dictionary for specific values, the values are substituted
template?: string; // The template, where {key} will be substituted
missing?: string// What to show when the key is not there
freeform?: ((string) => string) // Freeform template function, only applied on the value if nothing matches
constructor(options: {
key: string,
mapping?: any,
template?: string,
missing?: string
freeform?: ((string) => string)
}) {
this.key = options.key;
this.mapping = options.mapping;
this.template = options.template;
this.missing = options.missing;
this.freeform = options.freeform;
}
}
export class TagMapping extends UIElement {
private readonly tags;
private readonly options: TagMappingOptions;
constructor(
options: TagMappingOptions,
tags: UIEventSource<any>) {
super(tags);
this.tags = tags.data;
this.options = options;
}
IsEmpty(): boolean {
const o = this.options;
return this.tags[o.key] === undefined && o.missing === undefined;
}
protected InnerRender(): string {
const o = this.options;
const v = this.tags[o.key];
if (v === undefined) {
if (o.missing === undefined) {
return "";
}
return o.missing;
}
if (o.mapping !== undefined) {
const mapped = o.mapping[v];
if (mapped !== undefined) {
return mapped;
}
}
if (o.template !== undefined) {
return o.template.replace("{" + o.key + "}", v);
}
if(o.freeform !== undefined){
return o.freeform(v);
}
console.log("Warning: no match for " + o.key + "=" + v);
return v;
}
InnerUpdate(htmlElement: HTMLElement) {
}
}