From dcf5d240021e80a5e2ee0f299004462645dae679 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 20 Jul 2020 13:28:45 +0200 Subject: [PATCH] Intermediary --- Logic/Changes.ts | 1 - Logic/Question.ts | 508 ------------------------------ UI/Base/FixedInputElement.ts | 25 ++ UI/Base/TextField.ts | 46 ++- UI/Base/UIInputElement.ts | 2 + UI/Base/UIRadioButton.ts | 38 ++- UI/Base/UIRadioButtonWithOther.ts | 72 ----- UI/UIEventSource.ts | 25 +- UI/UserBadge.ts | 1 + test.ts | 30 +- 10 files changed, 140 insertions(+), 608 deletions(-) delete mode 100644 Logic/Question.ts create mode 100644 UI/Base/FixedInputElement.ts delete mode 100644 UI/Base/UIRadioButtonWithOther.ts diff --git a/Logic/Changes.ts b/Logic/Changes.ts index e0dd9ce..488ea50 100644 --- a/Logic/Changes.ts +++ b/Logic/Changes.ts @@ -6,7 +6,6 @@ import {OsmConnection} from "./OsmConnection"; import {OsmNode, OsmObject} from "./OsmObject"; import {ElementStorage} from "./ElementStorage"; import {UIEventSource} from "../UI/UIEventSource"; -import {Question, QuestionDefinition} from "./Question"; import {And, Tag, TagsFilter} from "./TagsFilter"; export class Changes { diff --git a/Logic/Question.ts b/Logic/Question.ts deleted file mode 100644 index 3ffd646..0000000 --- a/Logic/Question.ts +++ /dev/null @@ -1,508 +0,0 @@ -import {Changes} from "./Changes"; -import {UIElement} from "../UI/UIElement"; -import {UIEventSource} from "../UI/UIEventSource"; - -export class QuestionUI extends UIElement { - private readonly _q: Question; - private readonly _tags: UIEventSource; - /** - * The ID of the calling question - used to trigger it's onsave - */ - private readonly _qid; - - constructor(q: Question, qid: number, tags: UIEventSource) { - super(tags); - this._q = q; - this._tags = tags; - this._qid = qid; - } - - - private RenderRadio() { - let radios = ""; - let c = 0; - for (let answer of this._q.question.answers) { - const human = answer.text; - const ansId = "q" + this._qid + "-answer" + c; - radios += - "" + - "" + - "
"; - c++; - } - return radios; - } - - private RenderRadioText() { - let radios = ""; - let c = 0; - for (let answer of this._q.question.answers) { - const human = answer.text; - const ansId = "q" + this._qid + "-answer" + c; - radios += - "" + - "" + - "
"; - c++; - } - const ansId = "q" + this._qid + "-answer" + c; - - radios += - "" + - "" + - "
"; - - return radios; - } - - - InnerRender(): string { - - if (!this._q.Applicable(this._tags.data)) { - return ""; - } - - - const q = this._q.question; - - - let answers = ""; - if (q.type == "radio") { - answers += this.RenderRadio(); - } else if (q.type == "text") { - answers += "
" - } else if (q.type == "radio+text") { - answers += this.RenderRadioText(); - } else { - alert("PLZ RENDER TYPE " + q.type); - } - - - const embeddedScriptSave = 'questionAnswered(' + this._qid + ', "' + this._tags.data.id + '", false )'; - const embeddedScriptSkip = 'questionAnswered(' + this._qid + ', "' + this._tags.data.id + '", true )'; - const saveButton = ""; - const skip = ""; - return q.question + "
" + answers + saveButton + skip; - } - - InnerUpdate(htmlElement: HTMLElement) { - } -} - - -export class QuestionDefinition { - - - static noNameOrNameQuestion(question: string, noExplicitName : string, severity : number) : QuestionDefinition{ - const q = new QuestionDefinition(question); - - q.type = 'radio+text'; - q.addAnwser(noExplicitName, "noname","yes"); - q.addUnrequiredTag("name", "*"); - q.addUnrequiredTag("noname", "yes"); - - q.key = "name"; - q.severity = severity; - return q; - } - - static textQuestion( - question: string, - key: string, - severity: number - ): QuestionDefinition { - const q = new QuestionDefinition(question); - q.type = 'text'; - q.key = key; - q.severity = severity; - q.addUnrequiredTag(key, '*'); - return q; - } - - static radioQuestionSimple( - question: string, - severity: number, - key: string, - answers: { text: string, value: string }[]) { - - - const answers0: { - text: string, - tags: { k: string, v: string }[], - }[] = []; - for (const i in answers) { - const answer = answers[i]; - answers0.push({text: answer.text, tags: [{k: key, v: answer.value}]}) - } - - var q = this.radioQuestion(question, severity, answers0); - q.key = key; - q.addUnrequiredTag(key, '*'); - return q; - } - - static radioAndTextQuestion( - question: string, - severity: number, - key: string, - answers: { text: string, value: string }[]) { - - const q = this.radioQuestionSimple(question, severity, key, answers); - q.type = 'radio+text'; - return q; - - } - - static radioQuestion( - question: string, - severity: number, - answers: - { - text: string, - tags: { k: string, v: string }[], - }[] - ): QuestionDefinition { - - - const q = new QuestionDefinition(question); - q.severity = severity; - q.type = 'radio'; - q.answers = answers; - for (const i in answers) { - const answer = answers[i]; - for (const j in answer.tags) { - const tag = answer.tags[j]; - q.addUnrequiredTag(tag.k, tag.v); - } - } - - return q; - } - - - static GrbNoNumberQuestion() : QuestionDefinition{ - const q = new QuestionDefinition("Heeft dit gebouw een huisnummer?"); - q.type = "radio"; - q.severity = 10; - q.answers = [{ - text: "Ja, het OSM-huisnummer is correct", - tags: [{k: "fixme", v: ""}] - }, { - - text: "Nee, het is een enkele garage", - tags: [{k: "building", v: "garage"}, {k: "fixme", v: ""}] - }, { - - text: "Nee, het zijn meerdere garages", - tags: [{k: "building", v: "garages"}, {k: "fixme", v: ""}] - } - - - ]; - q.addRequiredTag("fixme", "GRB thinks that this has number no number") - return q; - } - - static GrbHouseNumberQuestion() : QuestionDefinition{ - - - const q = new QuestionDefinition("Wat is het huisnummer?"); - q.type = "radio+text"; - q.severity = 10; - - q.answers = [{ - text: "Het OSM-huisnummer is correct", - tags: [{k: "fixme", v: ""}], - }] - q.key = "addr:housenumber"; - - - q.addRequiredTag("fixme", "*"); - - return q; - } - - - private constructor(question: string) { - this.question = question; - } - - /** - * Question for humans - */ - public question: string; - - /** - * 'type' indicates how the answers are rendered and must be one of: - * 'text' for a free to fill text field - * 'radio' for radiobuttons - * 'radio+text' for radiobuttons and a freefill text field - * 'dropdown' for a dropdown menu - * 'number' for a number field - * - * If 'text' or 'number' is specified, 'key' is used as tag for the answer. - * If 'radio' or 'dropdown' is specified, the answers are used from 'tags' - * - */ - public type: string = 'radio'; - /** - * Only used for 'text' or 'number' questions - */ - public key: string = null; - - public answers: { - text: string, - tags: { k: string, v: string }[] - }[]; - - /** - * Indicates that the element must have _all_ the tags defined below - * Dictionary 'key' => [values]; empty list is wildcard - */ - private mustHaveAllTags = []; - - /** - * Indicates that the element must _not_ have any of the tags defined below. - * Dictionary 'key' => [values] - */ - private mustNotHaveTags = []; - - /** - * Severity: how important the question is - * The higher, the sooner it'll be shown - */ - public severity: number = 0; - - addRequiredTag(key: string, value: string) { - if (this.mustHaveAllTags[key] === undefined) { - this.mustHaveAllTags[key] = [value]; - } else { - if(this.mustHaveAllTags[key] === []){ - // Wildcard - return; - } - this.mustHaveAllTags[key].push(value); - } - - if (value === '*') { - this.mustHaveAllTags[key] = []; - } - return this; - } - - addUnrequiredTag(key: string, value: string) { - let valueList = this.mustNotHaveTags[key]; - - if (valueList === undefined) { - valueList = [value]; - this.mustNotHaveTags[key] = valueList; - } else { - if (valueList === []) { - return; - } - valueList.push(value); - } - - if (value === '*') { - this.mustNotHaveTags[key] = []; - } - return this; - } - - private addAnwser(anwser: string, key: string, value: string) { - if (this.answers === undefined) { - this.answers = [{text: anwser, tags: [{k: key, v: value}]}]; - } else { - this.answers.push({text: anwser, tags: [{k: key, v: value}]}); - } - this.addUnrequiredTag(key, value); - } - - public isApplicable(alreadyExistingTags): boolean { - for (let k in this.mustHaveAllTags) { - - var actual = alreadyExistingTags[k]; - if (actual === undefined) { - return false; - } - - let possibleVals = this.mustHaveAllTags[k]; - if (possibleVals.length == 0) { - // Wildcard - continue; - } - - let index = possibleVals.indexOf(actual); - if (index < 0) { - return false - } - } - - for (var k in this.mustNotHaveTags) { - var actual = alreadyExistingTags[k]; - if (actual === undefined) { - continue; - } - let impossibleVals = this.mustNotHaveTags[k]; - if (impossibleVals.length == 0) { - // Wildcard - return false; - } - - let index = impossibleVals.indexOf(actual); - if (index >= 0) { - return false - } - } - - return true; - - } -} - - -export class Question { - - - // All the questions are stored in here, to be able to retrieve them globaly. This is a workaround, see below - static questions = Question.InitCallbackFunction(); - - static InitCallbackFunction(): Question[] { - - // This needs some explanation, as it is a workaround - Question.questions = []; - // The html in a popup is only created when the user actually clicks to open it - // This means that we can not bind code to an HTML-element (as it doesn't exist yet) - // We work around this, by letting the 'save' button just call the function 'questionAnswered' with the ID of the question - // THis defines and registers this global function - - - /** - * Calls back to the question with either the answer or 'skip' - * @param questionId - * @param elementId - */ - function questionAnswered(questionId, elementId, dontKnow) { - if (dontKnow) { - Question.questions[questionId].Skip(elementId); - } else { - Question.questions[questionId].OnSave(elementId); - } - } - - - function checkRadioButton(id) { - // @ts-ignore - document.getElementById(id).checked = true; - } - - // must cast as any to set property on window - // @ts-ignore - const _global = (window /* browser */ || global /* node */) as any; - _global.questionAnswered = questionAnswered; - _global.checkRadioButton = checkRadioButton; - return []; - } - - - public readonly question: QuestionDefinition; - private _changeHandler: Changes; - private readonly _qId; - public skippedElements: string[] = []; - - constructor( - changeHandler: Changes, - question: QuestionDefinition) { - - this.question = question; - - this._qId = Question.questions.length; - this._changeHandler = changeHandler; - Question.questions.push(this); - } - - /** - * SHould this question be asked? - * Returns false if question is already there or if a premise is missing - */ - public Applicable(tags): boolean { - - if (this.skippedElements.indexOf(tags.id) >= 0) { - return false; - } - - return this.question.isApplicable(tags); - } - - /** - * - * @param elementId: the OSM-id of the element to perform the change on, format 'way/123', 'node/456' or 'relation/789' - * @constructor - */ - protected OnSave(elementId: string) { - let tagsToApply: { k: string, v: string }[] = []; - const q: QuestionDefinition = this.question; - let tp = this.question.type; - if (tp === "radio") { - const selected = document.querySelector('input[name="q' + this._qId + '"]:checked'); - if (selected === null) { - console.log("No answer selected"); - return - } - let index = (selected as any).value; - tagsToApply = q.answers[index].tags; - } else if (tp === "text") { - // @ts-ignore - let value = document.getElementById("q-" + this._qId + "-textbox").value; - if (value === undefined || value.length == 0) { - console.log("Answer too short"); - return; - } - tagsToApply = [{k: q.key, v: value}]; - } else if (tp === "radio+text") { - const selected = document.querySelector('input[name="q' + this._qId + '"]:checked'); - if (selected === null) { - console.log("No answer selected"); - return - } - let index = (selected as any).value; - if (index < q.answers.length) { - // A 'proper' answer was selected - tagsToApply = q.answers[index].tags; - } else { - // The textfield was selected - // @ts-ignore - let value = document.getElementById("q-" + this._qId + "-textbox").value; - if (value === undefined || value.length < 3) { - console.log("Answer too short"); - return; - } - tagsToApply = [{k: q.key, v: value}]; - } - - } - - console.log("Question.ts: Applying tags",tagsToApply," to element ", elementId); - - for (const toApply of tagsToApply) { - this._changeHandler.addChange(elementId, toApply.k, toApply.v); - } - - } - - /** - * Creates the HTML question for this tag collection - */ - public CreateHtml(tags: UIEventSource): UIElement { - return new QuestionUI(this, this._qId, tags); - } - - - private Skip(elementId: any) { - this.skippedElements.push(elementId); - console.log("SKIP"); - // Yeah, this is cheating below - // It is an easy way to notify the UIElement that something has changed - this._changeHandler._allElements.getElement(elementId).ping(); - } -} \ No newline at end of file diff --git a/UI/Base/FixedInputElement.ts b/UI/Base/FixedInputElement.ts new file mode 100644 index 0000000..0d7cf0a --- /dev/null +++ b/UI/Base/FixedInputElement.ts @@ -0,0 +1,25 @@ +import {UIInputElement} from "./UIInputElement"; +import {UIEventSource} from "../UIEventSource"; +import {UIElement} from "../UIElement"; +import {FixedUiElement} from "./FixedUiElement"; + + +export class FixedInputElement extends UIInputElement { + private rendering: UIElement; + private value: UIEventSource; + + constructor(rendering: UIElement | string, value: T) { + super(undefined); + this.value = new UIEventSource(value); + this.rendering = typeof (rendering) === 'string' ? new FixedUiElement(rendering) : rendering; + } + + GetValue(): UIEventSource { + return this.value; + } + + protected InnerRender(): string { + return this.rendering.Render(); + } + +} \ No newline at end of file diff --git a/UI/Base/TextField.ts b/UI/Base/TextField.ts index ca0c4ea..31560d2 100644 --- a/UI/Base/TextField.ts +++ b/UI/Base/TextField.ts @@ -5,33 +5,59 @@ import {UIInputElement} from "./UIInputElement"; export class TextField extends UIInputElement { - public value: UIEventSource = new UIEventSource(""); + private value: UIEventSource; + private mappedValue: UIEventSource; /** * Pings and has the value data */ public enterPressed = new UIEventSource(undefined); private _placeholder: UIEventSource; - private _mapping: (string) => T; + private _pretext: string; + private _fromString: (string: string) => T; - constructor(placeholder: UIEventSource, - mapping: ((string) => T)) { - super(placeholder); - this._placeholder = placeholder; - this._mapping = mapping; + constructor(options: { + placeholder?: UIEventSource, + toString: (t: T) => string, + fromString: (string: string) => T, + pretext?: string, + value?: UIEventSource + }) { + super(options?.placeholder); + this.value = new UIEventSource(""); + this.mappedValue = options?.value ?? new UIEventSource(undefined); + + + this.value.addCallback((str) => this.mappedValue.setData(options.fromString(str))); + this.mappedValue.addCallback((t) => this.value.setData(options.toString(t))); + + + this._placeholder = options?.placeholder ?? new UIEventSource(""); + this._pretext = options?.pretext ?? ""; + + const self = this; + this.mappedValue.addCallback((t) => { + if (t === undefined && t === null) { + return; + } + const field = document.getElementById('text-' + this.id); + if (field === undefined && field === null) { + return; + } + field.value = options.toString(t); + }) } GetValue(): UIEventSource { - return this.value.map(this._mapping); + return this.mappedValue; } protected InnerRender(): string { - return "
" + + return this._pretext + "" + "" + "
"; } InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); const field = document.getElementById('text-' + this.id); const self = this; field.oninput = () => { diff --git a/UI/Base/UIInputElement.ts b/UI/Base/UIInputElement.ts index fd4a242..fc4b3a6 100644 --- a/UI/Base/UIInputElement.ts +++ b/UI/Base/UIInputElement.ts @@ -5,4 +5,6 @@ export abstract class UIInputElement extends UIElement{ abstract GetValue() : UIEventSource; + + } \ No newline at end of file diff --git a/UI/Base/UIRadioButton.ts b/UI/Base/UIRadioButton.ts index 2413073..12a1e24 100644 --- a/UI/Base/UIRadioButton.ts +++ b/UI/Base/UIRadioButton.ts @@ -7,25 +7,45 @@ export class UIRadioButton extends UIInputElement { public readonly SelectedElementIndex: UIEventSource = new UIEventSource(null); - private readonly _elements: UIEventSource + private value: UIEventSource; + private readonly _elements: UIInputElement[] private _selectFirstAsDefault: boolean; private _valueMapping: (i: number) => T; - constructor(elements: UIEventSource, - valueMapping: ((i: number) => T), + + constructor(elements: UIInputElement[], selectFirstAsDefault = true) { - super(elements); + super(undefined); this._elements = elements; this._selectFirstAsDefault = selectFirstAsDefault; const self = this; - this._valueMapping = valueMapping; this.SelectedElementIndex.addCallback(() => { self.InnerUpdate(undefined); }) + + + this.value = + UIEventSource.flatten(this.SelectedElementIndex.map( + (selectedIndex) => { + if (selectedIndex !== undefined && selectedIndex !== null) { + return elements[selectedIndex].GetValue() + } + } + ), elements.map(e => e.GetValue())) + ; + + + for (let i = 0; i < elements.length; i ++){ + elements[i].onClick(( ) => { + self.SelectedElementIndex.setData(i); + }); + } + + } GetValue(): UIEventSource { - return this.SelectedElementIndex.map(this._valueMapping); + return this.value; } @@ -37,7 +57,7 @@ export class UIRadioButton extends UIInputElement { let body = ""; let i = 0; - for (const el of this._elements.data) { + for (const el of this._elements) { const htmlElement = '' + '' + @@ -54,7 +74,7 @@ export class UIRadioButton extends UIInputElement { const self = this; function checkButtons() { - for (let i = 0; i < self._elements.data.length; i++) { + for (let i = 0; i < self._elements.length; i++) { const el = document.getElementById(self.IdFor(i)); // @ts-ignore if (el.checked) { @@ -87,7 +107,7 @@ export class UIRadioButton extends UIInputElement { var expected = this.SelectedElementIndex.data; if (expected) { - for (let i = 0; i < self._elements.data.length; i++) { + for (let i = 0; i < self._elements.length; i++) { const el = document.getElementById(self.IdFor(i)); // @ts-ignore if (el.checked) { diff --git a/UI/Base/UIRadioButtonWithOther.ts b/UI/Base/UIRadioButtonWithOther.ts deleted file mode 100644 index 806764d..0000000 --- a/UI/Base/UIRadioButtonWithOther.ts +++ /dev/null @@ -1,72 +0,0 @@ -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 extends UIInputElement { - private readonly _radioSelector: UIRadioButton; - private readonly _freeformText: TextField; - private readonly _value: UIEventSource = new UIEventSource(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(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 { - return this._value; - } - - protected InnerRender(): string { - return this._radioSelector.Render(); - } - - InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); - this._radioSelector.Update(); - this._freeformText.Update(); - } - -} \ No newline at end of file diff --git a/UI/UIEventSource.ts b/UI/UIEventSource.ts index eae2151..1e4aeb5 100644 --- a/UI/UIEventSource.ts +++ b/UI/UIEventSource.ts @@ -27,15 +27,32 @@ export class UIEventSource{ } } - public map(f: ((T) => J), - extraSources : UIEventSource[] = []): UIEventSource { - const self = this; + public static flatten(source: UIEventSource>, possibleSources: UIEventSource[]): UIEventSource { + const sink = new UIEventSource(source.data?.data); + + source.addCallback((latestData) => { + sink.setData(latestData?.data); + }); + + for (const possibleSource of possibleSources) { + possibleSource.addCallback(() => { + sink.setData(source.data?.data); + + }) + } + return sink; + } + + public map(f: ((T) => J), + extraSources: UIEventSource[] = []): UIEventSource { + const self = this; + const update = function () { newSource.setData(f(self.data)); newSource.ping(); } - + this.addCallback(update); for (const extraSource of extraSources) { extraSource.addCallback(update); diff --git a/UI/UserBadge.ts b/UI/UserBadge.ts index a3912e0..c9a2bd5 100644 --- a/UI/UserBadge.ts +++ b/UI/UserBadge.ts @@ -21,6 +21,7 @@ export class UserBadge extends UIElement { pendingChanges: UIElement, basemap: Basemap) { super(userDetails); + this._userDetails = userDetails; this._pendingChanges = pendingChanges; this._basemap = basemap; diff --git a/test.ts b/test.ts index b7e9bd3..eb72b65 100644 --- a/test.ts +++ b/test.ts @@ -7,11 +7,33 @@ import {OsmLink} from "./Customizations/Questions/OsmLink"; import {ConfirmDialog} from "./UI/ConfirmDialog"; import {Imgur} from "./Logic/Imgur"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import {UIRadioButton} from "./UI/Base/UIRadioButton"; +import {FixedInputElement} from "./UI/Base/FixedInputElement"; +import {TextField} from "./UI/Base/TextField"; -const html = new UIEventSource("Some text"); +const buttons = new UIRadioButton( + [new FixedInputElement("Five", 5), + new FixedInputElement("Ten", 10), + new TextField({ + fromString: (str) => parseInt(str), + toString: (i) => ("" + i), + }) + ] +).AttachTo("maindiv"); -const uielement = new VariableUiElement(html); -uielement.AttachTo("maindiv") +buttons.GetValue().addCallback(console.log); +buttons.GetValue().setData(10) -window.setTimeout(() => {html.setData("Different text")}, 1000) \ No newline at end of file +const value = new TextField({ + fromString: (str) => parseInt(str), + toString: (i) => { + if(isNaN(i)){ + return "" + } + return ("" + i) + }, +}).AttachTo("extradiv").GetValue(); + +value.setData(42); +value.addCallback(console.log) \ No newline at end of file