forked from MapComplete/MapComplete
		
	Butchering the UI framework
This commit is contained in:
		
							parent
							
								
									8d404b1ba9
								
							
						
					
					
						commit
						6415e195d1
					
				
					 90 changed files with 1012 additions and 3101 deletions
				
			
		|  | @ -1,164 +0,0 @@ | |||
| import {InputElement} from "./InputElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {UIElement} from "../UIElement"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import CheckBox from "./CheckBox"; | ||||
| import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson"; | ||||
| import {MultiTagInput} from "./MultiTagInput"; | ||||
| import Svg from "../../Svg"; | ||||
| 
 | ||||
| class AndOrConfig implements AndOrTagConfigJson { | ||||
|     public and: (string | AndOrTagConfigJson)[] = undefined; | ||||
|     public or: (string | AndOrTagConfigJson)[] = undefined; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export default class AndOrTagInput extends InputElement<AndOrTagConfigJson> { | ||||
| 
 | ||||
|     private readonly _rawTags = new MultiTagInput(); | ||||
|     private readonly _subAndOrs: AndOrTagInput[] = []; | ||||
|     private readonly _isAnd: UIEventSource<boolean> = new UIEventSource<boolean>(true); | ||||
|     private readonly _isAndButton; | ||||
|     private readonly _addBlock: UIElement; | ||||
|     private readonly _value: UIEventSource<AndOrConfig> = new UIEventSource<AndOrConfig>(undefined); | ||||
| 
 | ||||
|     public bottomLeftButton: UIElement; | ||||
| 
 | ||||
|     IsSelected: UIEventSource<boolean>; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(); | ||||
|         const self = this; | ||||
|         this._isAndButton = new CheckBox( | ||||
|             new SubtleButton(Svg.ampersand_ui(), null).SetClass("small-button"), | ||||
|             new SubtleButton(Svg.or_ui(), null).SetClass("small-button"), | ||||
|             this._isAnd); | ||||
| 
 | ||||
| 
 | ||||
|         this._addBlock = | ||||
|             new SubtleButton(Svg.addSmall_ui(), "Add an and/or-expression") | ||||
|                 .SetClass("small-button") | ||||
|                 .onClick(() => {self.createNewBlock()}); | ||||
| 
 | ||||
| 
 | ||||
|         this._isAnd.addCallback(() => self.UpdateValue()); | ||||
|         this._rawTags.GetValue().addCallback(() => { | ||||
|             self.UpdateValue() | ||||
|         }); | ||||
| 
 | ||||
|         this.IsSelected = this._rawTags.IsSelected; | ||||
| 
 | ||||
|         this._value.addCallback(tags => self.loadFromValue(tags)); | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|     private createNewBlock(){ | ||||
|         const inputEl = new AndOrTagInput(); | ||||
|         inputEl.GetValue().addCallback(() => this.UpdateValue()); | ||||
|         const deleteButton = this.createDeleteButton(inputEl.id); | ||||
|         inputEl.bottomLeftButton = deleteButton; | ||||
|         this._subAndOrs.push(inputEl); | ||||
|         this.Update(); | ||||
|     } | ||||
| 
 | ||||
|     private createDeleteButton(elementId: string): UIElement { | ||||
|         const self = this; | ||||
|         return new SubtleButton(Svg.delete_icon_ui(), null).SetClass("small-button") | ||||
|             .onClick(() => { | ||||
|                 for (let i = 0; i < self._subAndOrs.length; i++) { | ||||
|                     if (self._subAndOrs[i].id === elementId) { | ||||
|                         self._subAndOrs.splice(i, 1); | ||||
|                         self.Update(); | ||||
|                         self.UpdateValue(); | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private loadFromValue(value: AndOrTagConfigJson) { | ||||
|         this._isAnd.setData(value.and !== undefined); | ||||
|         const tags = value.and ?? value.or; | ||||
|         const rawTags: string[] = []; | ||||
|         const subTags: AndOrTagConfigJson[] = []; | ||||
|         for (const tag of tags) { | ||||
| 
 | ||||
|             if (typeof (tag) === "string") { | ||||
|                 rawTags.push(tag); | ||||
|             } else { | ||||
|                 subTags.push(tag); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (let i = 0; i < rawTags.length; i++) { | ||||
|             if (this._rawTags.GetValue().data[i] !== rawTags[i]) { | ||||
|                 // For some reason, 'setData' isn't stable as the comparison between the lists fails
 | ||||
|                 // Probably because we generate a new list object every timee
 | ||||
|                 // So we compare again here and update only if we find a difference
 | ||||
|                 this._rawTags.GetValue().setData(rawTags); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         while(this._subAndOrs.length < subTags.length){ | ||||
|             this.createNewBlock(); | ||||
|         } | ||||
| 
 | ||||
|         for (let i = 0; i < subTags.length; i++){ | ||||
|             let subTag = subTags[i]; | ||||
|             this._subAndOrs[i].GetValue().setData(subTag); | ||||
|              | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private UpdateValue() { | ||||
|         const tags: (string | AndOrTagConfigJson)[] = []; | ||||
|         tags.push(...this._rawTags.GetValue().data); | ||||
| 
 | ||||
|         for (const subAndOr of this._subAndOrs) { | ||||
|             const subAndOrData = subAndOr._value.data; | ||||
|             if (subAndOrData === undefined) { | ||||
|                 continue; | ||||
|             } | ||||
|             console.log(subAndOrData); | ||||
|             tags.push(subAndOrData); | ||||
|         } | ||||
| 
 | ||||
|         const tagConfig = new AndOrConfig(); | ||||
| 
 | ||||
|         if (this._isAnd.data) { | ||||
|             tagConfig.and = tags; | ||||
|         } else { | ||||
|             tagConfig.or = tags; | ||||
|         } | ||||
|         this._value.setData(tagConfig); | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<AndOrTagConfigJson> { | ||||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         const leftColumn = new Combine([ | ||||
|             this._isAndButton, | ||||
|             "<br/>", | ||||
|             this.bottomLeftButton ?? "" | ||||
|         ]); | ||||
|         const tags = new Combine([ | ||||
|             this._rawTags, | ||||
|             ...this._subAndOrs, | ||||
|             this._addBlock | ||||
|         ]).Render(); | ||||
|         return `<span class="bordered"><table><tr><td>${leftColumn.Render()}</td><td>${tags}</td></tr></table></span>`; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     IsValid(t: AndOrTagConfigJson): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,32 +0,0 @@ | |||
| import {UIElement} from "../UIElement"; | ||||
| import Translations from "../../UI/i18n/Translations"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| 
 | ||||
| export default class CheckBox extends UIElement{ | ||||
|     public readonly isEnabled: UIEventSource<boolean>; | ||||
|     private readonly _showEnabled:  UIElement; | ||||
|     private readonly _showDisabled: UIElement; | ||||
| 
 | ||||
|     constructor(showEnabled: string | UIElement, showDisabled: string | UIElement, data: UIEventSource<boolean> | boolean = false) { | ||||
|         super(undefined); | ||||
|         this.isEnabled = | ||||
|             data instanceof UIEventSource ? data : new UIEventSource(data ?? false); | ||||
|         this.ListenTo(this.isEnabled); | ||||
|         this._showEnabled = Translations.W(showEnabled); | ||||
|         this._showDisabled =Translations.W(showDisabled); | ||||
|         const self = this; | ||||
|         this.onClick(() => { | ||||
|             self.isEnabled.setData(!self.isEnabled.data); | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         if (this.isEnabled.data) { | ||||
|             return Translations.W(this._showEnabled).Render(); | ||||
|         } else { | ||||
|             return Translations.W(this._showDisabled).Render(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -16,7 +16,6 @@ export default class CheckBoxes extends InputElement<number[]> { | |||
|     constructor(elements: UIElement[]) { | ||||
|         super(undefined); | ||||
|         this._elements = Utils.NoNull(elements); | ||||
|         this.dumbMode = false; | ||||
| 
 | ||||
|         this.value = new UIEventSource<number[]>([]) | ||||
|         this.ListenTo(this.value); | ||||
|  | @ -51,7 +50,6 @@ export default class CheckBoxes extends InputElement<number[]> { | |||
|     } | ||||
| 
 | ||||
|     protected InnerUpdate(htmlElement: HTMLElement) { | ||||
|         super.InnerUpdate(htmlElement); | ||||
|         const self = this; | ||||
| 
 | ||||
|         for (let i = 0; i < this._elements.length; i++) { | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import {InputElement} from "./InputElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| export default class ColorPicker extends InputElement<string> { | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,14 +1,16 @@ | |||
| import {InputElement} from "./InputElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {UIElement} from "../UIElement"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 
 | ||||
| export default class CombinedInputElement<T> extends InputElement<T> { | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|        return this._combined.ConstructElement(); | ||||
|     } | ||||
|     private readonly _a: InputElement<T>; | ||||
|     private readonly _b: UIElement; | ||||
|     private readonly _combined: UIElement; | ||||
|     private readonly _b: BaseUIElement; | ||||
|     private readonly _combined: BaseUIElement; | ||||
|     public readonly IsSelected: UIEventSource<boolean>; | ||||
| 
 | ||||
|     constructor(a: InputElement<T>, b: InputElement<T>) { | ||||
|         super(); | ||||
|         this._a = a; | ||||
|  | @ -23,11 +25,6 @@ export default class CombinedInputElement<T> extends InputElement<T> { | |||
|         return this._a.GetValue(); | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         return this._combined.Render(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     IsValid(t: T): boolean { | ||||
|         return this._a.IsValid(t); | ||||
|     } | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ export default class DirectionInput extends InputElement<string> { | |||
| 
 | ||||
|     constructor(value?: UIEventSource<string>) { | ||||
|         super(); | ||||
|         this.dumbMode = false; | ||||
|         this.value = value ?? new UIEventSource<string>(undefined); | ||||
| 
 | ||||
|         this.value.addCallbackAndRun(rotation => { | ||||
|  | @ -48,7 +47,6 @@ export default class DirectionInput extends InputElement<string> { | |||
|     } | ||||
| 
 | ||||
|     protected InnerUpdate(htmlElement: HTMLElement) { | ||||
|         super.InnerUpdate(htmlElement); | ||||
|         const self = this; | ||||
| 
 | ||||
|         function onPosChange(x: number, y: number) { | ||||
|  |  | |||
|  | @ -1,50 +1,81 @@ | |||
| import {UIElement} from "../UIElement"; | ||||
| import {InputElement} from "./InputElement"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 
 | ||||
| export class DropDown<T> extends InputElement<T> { | ||||
| 
 | ||||
|     private readonly _label: UIElement; | ||||
|     private readonly _values: { value: T; shown: UIElement }[]; | ||||
|     private static _nextDropdownId = 0; | ||||
|     public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
| 
 | ||||
|     private readonly _element: HTMLElement; | ||||
| 
 | ||||
|     private readonly _value: UIEventSource<T>; | ||||
|     private readonly _values: { value: T; shown: string | BaseUIElement }[]; | ||||
| 
 | ||||
|     public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     private readonly _label_class: string; | ||||
|     private readonly _select_class: string; | ||||
|     private _form_style: string; | ||||
| 
 | ||||
|     constructor(label: string | UIElement, | ||||
|                 values: { value: T, shown: string | UIElement }[], | ||||
|     constructor(label: string | BaseUIElement, | ||||
|                 values: { value: T, shown: string | BaseUIElement }[], | ||||
|                 value: UIEventSource<T> = undefined, | ||||
|                 label_class: string = "", | ||||
|                 select_class: string = "", | ||||
|                 form_style: string = "flex") { | ||||
|         super(undefined); | ||||
|         this._form_style = form_style; | ||||
|         this._value = value ?? new UIEventSource<T>(undefined); | ||||
|         this._label = Translations.W(label); | ||||
|         this._label_class = label_class || ''; | ||||
|         this._select_class = select_class || ''; | ||||
|         this._values = values.map(v => { | ||||
|             return { | ||||
|                 value: v.value, | ||||
|                     shown: Translations.W(v.shown) | ||||
|                 options?: { | ||||
|                     select_class?: string | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|         for (const v of this._values) { | ||||
|             this.ListenTo(v.shown._source); | ||||
|     ) { | ||||
|         super(); | ||||
| this._values = values; | ||||
|         if (values.length <= 1) { | ||||
|             return; | ||||
|         } | ||||
|         this.ListenTo(this._value); | ||||
| 
 | ||||
|         this.onClick(() => {}) // by registering a click, the click event is consumed and doesn't bubble furter to other elements, e.g. checkboxes
 | ||||
|         const id = DropDown._nextDropdownId; | ||||
|         DropDown._nextDropdownId++; | ||||
| 
 | ||||
| 
 | ||||
|         const el = document.createElement("form") | ||||
|         this._element = el; | ||||
|         el.id = "dropdown" + id; | ||||
| 
 | ||||
|         { | ||||
|             const labelEl = Translations.W(label).ConstructElement() | ||||
|             const labelHtml = document.createElement("label") | ||||
|             labelHtml.appendChild(labelEl) | ||||
|             labelHtml.htmlFor = el.id; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         { | ||||
|             const select = document.createElement("select") | ||||
|             select.classList.add(...(options?.select_class?.split(" ") ?? [])) | ||||
|             for (let i = 0; i < values.length; i++) { | ||||
| 
 | ||||
|                 const option = document.createElement("option") | ||||
|                 option.value = "" + i | ||||
|                 option.appendChild(Translations.W(values[i].shown).ConstructElement()) | ||||
|             } | ||||
| 
 | ||||
|             select.onchange = (() => { | ||||
|                 var index = select.selectedIndex; | ||||
|                 value.setData(values[index].value); | ||||
|             }); | ||||
|              | ||||
|             value.addCallbackAndRun(selected => { | ||||
|                 for (let i = 0; i < values.length; i++) { | ||||
|                     const value = values[i].value; | ||||
|                     if (value === selected) { | ||||
|                         select.selectedIndex = i; | ||||
|                     } | ||||
|                 }  | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.onClick(() => { | ||||
|         }) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes
 | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<T> { | ||||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: T): boolean { | ||||
|         for (const value of this._values) { | ||||
|             if (value.value === t) { | ||||
|  | @ -54,44 +85,8 @@ export class DropDown<T> extends InputElement<T> { | |||
|         return false | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         if(this._values.length <=1){ | ||||
|             return ""; | ||||
|         } | ||||
| 
 | ||||
|         let options = ""; | ||||
|         for (let i = 0; i < this._values.length; i++) { | ||||
|             options += "<option value='" + i + "'>" + this._values[i].shown.InnerRender() + "</option>" | ||||
|         } | ||||
| 
 | ||||
|         return `<form class="${this._form_style}">` + | ||||
|             `<label class='${this._label_class}' for='dropdown-${this.id}'>${this._label.Render()}</label>` + | ||||
|             `<select class='${this._select_class}' name='dropdown-${this.id}' id='dropdown-${this.id}'>` + | ||||
|             options + | ||||
|             `</select>` + | ||||
|             `</form>`; | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         return this._element; | ||||
|     } | ||||
| 
 | ||||
|     protected InnerUpdate(element) { | ||||
|         var e = document.getElementById("dropdown-" + this.id); | ||||
|         if(e === null){ | ||||
|             return; | ||||
|         } | ||||
|         const self = this; | ||||
|         e.onchange = (() => { | ||||
|             // @ts-ignore
 | ||||
|             var index = parseInt(e.selectedIndex); | ||||
|             self._value.setData(self._values[index].value); | ||||
|         }); | ||||
| 
 | ||||
|         var t = this._value.data; | ||||
|         for (let i = 0; i < this._values.length ; i++) { | ||||
|             const value = this._values[i].value; | ||||
|             if (value === t) { | ||||
|                 // @ts-ignore
 | ||||
|                 e.selectedIndex = i; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| import {UIElement} from "../UIElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 
 | ||||
| export abstract class InputElement<T> extends UIElement{ | ||||
| export abstract class InputElement<T> extends BaseUIElement{ | ||||
|      | ||||
|     abstract GetValue() : UIEventSource<T>; | ||||
|     abstract IsSelected: UIEventSource<boolean>; | ||||
|  |  | |||
|  | @ -3,13 +3,12 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| 
 | ||||
| 
 | ||||
| export default class InputElementMap<T, X> extends InputElement<X> { | ||||
| 
 | ||||
|     public readonly IsSelected: UIEventSource<boolean>; | ||||
|     private readonly _inputElement: InputElement<T>; | ||||
|     private isSame: (x0: X, x1: X) => boolean; | ||||
|     private readonly fromX: (x: X) => T; | ||||
|     private readonly toX: (t: T) => X; | ||||
|     private readonly _value: UIEventSource<X>; | ||||
|     public readonly  IsSelected: UIEventSource<boolean>; | ||||
| 
 | ||||
|     constructor(inputElement: InputElement<T>, | ||||
|                 isSame: (x0: X, x1: X) => boolean, | ||||
|  | @ -41,19 +40,19 @@ export default class InputElementMap<T, X> extends InputElement<X> { | |||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         return this._inputElement.InnerRender(); | ||||
|     } | ||||
| 
 | ||||
|     IsValid(x: X): boolean { | ||||
|         if(x === undefined){ | ||||
|         if (x === undefined) { | ||||
|             return false; | ||||
|         } | ||||
|         const t = this.fromX(x); | ||||
|         if(t === undefined){ | ||||
|         if (t === undefined) { | ||||
|             return false; | ||||
|         } | ||||
|         return this._inputElement.IsValid(t); | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         return this._inputElement.ConstructElement(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,125 +0,0 @@ | |||
| import {InputElement} from "./InputElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {UIElement} from "../UIElement"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import Svg from "../../Svg"; | ||||
| 
 | ||||
| export class MultiInput<T> extends InputElement<T[]> { | ||||
| 
 | ||||
|     private readonly _value: UIEventSource<T[]>; | ||||
|     IsSelected: UIEventSource<boolean>; | ||||
|     private elements: UIElement[] = []; | ||||
|     private inputElements: InputElement<T>[] = []; | ||||
|     private addTag: UIElement; | ||||
|     private _options: { allowMovement?: boolean }; | ||||
| 
 | ||||
|     constructor( | ||||
|         addAElement: string, | ||||
|         newElement: (() => T), | ||||
|         createInput: (() => InputElement<T>), | ||||
|         value: UIEventSource<T[]> = undefined, | ||||
|         options?: { | ||||
|             allowMovement?: boolean | ||||
|         }) { | ||||
|         super(undefined); | ||||
|         this._value = value ?? new UIEventSource<T[]>([]); | ||||
|         value = this._value; | ||||
|         this.ListenTo(value.map((latest : T[]) => latest.length)); | ||||
|         this._options = options ?? {}; | ||||
| 
 | ||||
|         this.addTag = new SubtleButton(Svg.addSmall_ui(), addAElement) | ||||
|             .SetClass("small-button") | ||||
|             .onClick(() => { | ||||
|                 this.IsSelected.setData(true); | ||||
|                 value.data.push(newElement()); | ||||
|                 value.ping(); | ||||
|             }); | ||||
|         const self = this; | ||||
|         value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements(createInput)); | ||||
|         this.createElements(createInput); | ||||
| 
 | ||||
|         this._value.addCallback(tags => self.load(tags)); | ||||
|         this.IsSelected = new UIEventSource<boolean>(false); | ||||
|     } | ||||
| 
 | ||||
|     private load(tags: T[]) { | ||||
|         if (tags === undefined) { | ||||
|             return; | ||||
|         } | ||||
|         for (let i = 0; i < tags.length; i++) { | ||||
|             this.inputElements[i].GetValue().setData(tags[i]); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private UpdateIsSelected(){ | ||||
|         this.IsSelected.setData(this.inputElements.map(input => input.IsSelected.data).reduce((a,b) => a && b)) | ||||
|     } | ||||
| 
 | ||||
|     private createElements(createInput: (() => InputElement<T>)) { | ||||
|         this.inputElements.splice(0, this.inputElements.length); | ||||
|         this.elements = []; | ||||
|         const self = this; | ||||
|         for (let i = 0; i < this._value.data.length; i++) { | ||||
|             const input = createInput(); | ||||
|             input.GetValue().addCallback(tag => { | ||||
|                     self._value.data[i] = tag; | ||||
|                     self._value.ping(); | ||||
|                 } | ||||
|             ); | ||||
|             this.inputElements.push(input); | ||||
|             input.IsSelected.addCallback(() => this.UpdateIsSelected()); | ||||
| 
 | ||||
|             const moveUpBtn = Svg.up_ui() | ||||
|                 .SetClass('small-image').onClick(() => { | ||||
|                     const v = self._value.data[i]; | ||||
|                     self._value.data[i] = self._value.data[i - 1]; | ||||
|                     self._value.data[i - 1] = v; | ||||
|                     self._value.ping(); | ||||
|                 }); | ||||
| 
 | ||||
|             const moveDownBtn =  | ||||
|                 Svg.down_ui() | ||||
|                     .SetClass('small-image') .onClick(() => { | ||||
|                     const v = self._value.data[i]; | ||||
|                     self._value.data[i] = self._value.data[i + 1]; | ||||
|                     self._value.data[i + 1] = v; | ||||
|                     self._value.ping(); | ||||
|                 }); | ||||
| 
 | ||||
|             const controls = []; | ||||
|             if (i > 0 && this._options.allowMovement) { | ||||
|                 controls.push(moveUpBtn); | ||||
|             } | ||||
| 
 | ||||
|             if (i + 1 < this._value.data.length && this._options.allowMovement) { | ||||
|                 controls.push(moveDownBtn); | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             const deleteBtn = | ||||
|                 Svg.delete_icon_ui().SetClass('small-image') | ||||
|                 .onClick(() => { | ||||
|                     self._value.data.splice(i, 1); | ||||
|                     self._value.ping(); | ||||
|                 }); | ||||
|             controls.push(deleteBtn); | ||||
|             this.elements.push(new Combine([input.SetStyle("width: calc(100% - 2em - 5px)"), new Combine(controls).SetStyle("display:flex;flex-direction:column;width:min-content;")]).SetClass("tag-input-row")) | ||||
|         } | ||||
|          | ||||
|         this.Update(); | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         return new Combine([...this.elements, this.addTag]).Render(); | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: T[]): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<T[]> { | ||||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,99 +0,0 @@ | |||
| import {InputElement} from "./InputElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {TextField} from "./TextField"; | ||||
| 
 | ||||
| export default class MultiLingualTextFields extends InputElement<any> { | ||||
|     private _fields: Map<string, TextField> = new Map<string, TextField>(); | ||||
|     private readonly _value: UIEventSource<any>; | ||||
|     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     constructor(languages: UIEventSource<string[]>, | ||||
|                 textArea: boolean = false, | ||||
|                 value: UIEventSource<Map<string, UIEventSource<string>>> = undefined) { | ||||
|         super(undefined); | ||||
|         this._value = value ?? new UIEventSource({}); | ||||
|         this._value.addCallbackAndRun(latestData => { | ||||
|             if (typeof (latestData) === "string") { | ||||
|                 console.warn("Refusing string for multilingual input", latestData); | ||||
|                 self._value.setData({}); | ||||
|             } | ||||
|         }) | ||||
|          | ||||
|         const self = this; | ||||
| 
 | ||||
|         function setup(languages: string[]) { | ||||
|             if (languages === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             const newFields = new Map<string, TextField>(); | ||||
|             for (const language of languages) { | ||||
|                 if (language.length != 2) { | ||||
|                     continue; | ||||
|                 } | ||||
|                  | ||||
|                 let oldField = self._fields.get(language); | ||||
|                 if (oldField === undefined) { | ||||
|                     oldField = new TextField({textArea: textArea}); | ||||
|                     oldField.GetValue().addCallback(str => { | ||||
|                         self._value.data[language] = str; | ||||
|                         self._value.ping(); | ||||
|                     }); | ||||
|                     oldField.GetValue().setData(self._value.data[language]); | ||||
|                      | ||||
|                     oldField.IsSelected.addCallback(() => { | ||||
|                         let selected = false; | ||||
|                         self._fields.forEach(value => {selected = selected || value.IsSelected.data}); | ||||
|                         self.IsSelected.setData(selected); | ||||
|                     }) | ||||
|                      | ||||
|                 } | ||||
|                 newFields.set(language, oldField); | ||||
|             } | ||||
|             self._fields = newFields; | ||||
|             self.Update(); | ||||
| 
 | ||||
|            | ||||
|         } | ||||
| 
 | ||||
|         setup(languages.data); | ||||
|         languages.addCallback(setup); | ||||
| 
 | ||||
| 
 | ||||
|         function load(latest: any){ | ||||
|             if(latest === undefined){ | ||||
|                 return; | ||||
|             } | ||||
|             for (const lang in latest) { | ||||
|                 self._fields.get(lang)?.GetValue().setData(latest[lang]); | ||||
|             } | ||||
|         } | ||||
|         this._value.addCallback(load); | ||||
|         load(this._value.data); | ||||
|     } | ||||
| 
 | ||||
|     protected InnerUpdate(htmlElement: HTMLElement) { | ||||
|         super.InnerUpdate(htmlElement); | ||||
|         this._fields.forEach(value => value.Update()); | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<Map<string, UIEventSource<string>>> { | ||||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         let html = ""; | ||||
|         this._fields.forEach((field, lang) => { | ||||
|             html += `<tr><td>${lang}</td><td>${field.Render()}</td></tr>` | ||||
|         }) | ||||
|         if(html === ""){ | ||||
|             return "Please define one or more languages" | ||||
|         } | ||||
|          | ||||
|         return `<table>${html}</table>`; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     IsValid(t: any): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,15 +0,0 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import TagInput from "./SingleTagInput"; | ||||
| import {MultiInput} from "./MultiInput"; | ||||
| 
 | ||||
| export class MultiTagInput extends MultiInput<string> { | ||||
| 
 | ||||
|     constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) { | ||||
|         super("Add a new tag", | ||||
|             () => "", | ||||
|             () => new TagInput(), | ||||
|             value | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,123 +0,0 @@ | |||
| import {UIElement} from "../UIElement"; | ||||
| import {InputElement} from "./InputElement"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| 
 | ||||
| export class NumberField extends InputElement<number> { | ||||
|     private readonly value: UIEventSource<number>; | ||||
|     public readonly enterPressed = new UIEventSource<string>(undefined); | ||||
|     private readonly _placeholder: UIElement; | ||||
|     private options?: { | ||||
|         placeholder?: string | UIElement, | ||||
|         value?: UIEventSource<number>, | ||||
|         isValid?: ((i: number) => boolean), | ||||
|         min?: number, | ||||
|         max?: number | ||||
|     }; | ||||
|     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     private readonly _isValid: (i:number) => boolean; | ||||
| 
 | ||||
|     constructor(options?: { | ||||
|         placeholder?: string | UIElement, | ||||
|         value?: UIEventSource<number>, | ||||
|         isValid?: ((i:number) => boolean), | ||||
|         min?: number, | ||||
|         max?:number | ||||
|     }) { | ||||
|         super(undefined); | ||||
|         this.options = options; | ||||
|         const self = this; | ||||
|         this.value = new UIEventSource<number>(undefined); | ||||
|         this.value = options?.value ?? new UIEventSource<number>(undefined); | ||||
| 
 | ||||
|         this._isValid = options.isValid ?? ((i) => true); | ||||
| 
 | ||||
|         this._placeholder = Translations.W(options.placeholder ?? ""); | ||||
|         this.ListenTo(this._placeholder._source); | ||||
| 
 | ||||
|         this.onClick(() => { | ||||
|             self.IsSelected.setData(true) | ||||
|         }); | ||||
|         this.value.addCallback((t) => { | ||||
|             const field = document.getElementById("txt-"+this.id); | ||||
|             if (field === undefined || field === null) { | ||||
|                 return; | ||||
|             } | ||||
|             field.className = self.IsValid(t) ? "" : "invalid"; | ||||
| 
 | ||||
|             if (t === undefined || t === null) { | ||||
|                 return; | ||||
|             } | ||||
|             // @ts-ignore
 | ||||
|             field.value = t; | ||||
|         }); | ||||
|         this.dumbMode = false; | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<number> { | ||||
|         return this.value; | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
| 
 | ||||
|         const placeholder = this._placeholder.InnerRender().replace("'", "'"); | ||||
|          | ||||
|         let min =  ""; | ||||
|         if(this.options.min){ | ||||
|             min = `min='${this.options.min}'`; | ||||
|         } | ||||
| 
 | ||||
|         let max =  ""; | ||||
|         if(this.options.min){ | ||||
|             max = `max='${this.options.max}'`; | ||||
|         } | ||||
| 
 | ||||
|         return `<span id="${this.id}"><form onSubmit='return false' class='form-text-field'>` + | ||||
|             `<input type='number' ${min} ${max} placeholder='${placeholder}' id='txt-${this.id}'>` + | ||||
|             `</form></span>`; | ||||
|     } | ||||
|      | ||||
|     InnerUpdate() { | ||||
|         const field = document.getElementById("txt-" + this.id); | ||||
|         const self = this; | ||||
|         field.oninput = () => { | ||||
|              | ||||
|             // How much characters are on the right, not including spaces?
 | ||||
|             // @ts-ignore
 | ||||
|             const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length; | ||||
|             // @ts-ignore
 | ||||
|             let val: number = Number(field.value); | ||||
|             if (!self.IsValid(val)) { | ||||
|                 self.value.setData(undefined); | ||||
|             } else { | ||||
|                 self.value.setData(val); | ||||
|             } | ||||
|     | ||||
|         }; | ||||
| 
 | ||||
|         if (this.value.data !== undefined && this.value.data !== null) { | ||||
|             // @ts-ignore
 | ||||
|             field.value = this.value.data; | ||||
|         } | ||||
| 
 | ||||
|         field.addEventListener("focusin", () => self.IsSelected.setData(true)); | ||||
|         field.addEventListener("focusout", () => self.IsSelected.setData(false)); | ||||
| 
 | ||||
| 
 | ||||
|         field.addEventListener("keyup", function (event) { | ||||
|             if (event.key === "Enter") { | ||||
|                 // @ts-ignore
 | ||||
|                 self.enterPressed.setData(field.value); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: number): boolean { | ||||
|         if (t === undefined || t === null) { | ||||
|             return false | ||||
|         } | ||||
|         return this._isValid(t); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -5,48 +5,37 @@ export default class SimpleDatePicker extends InputElement<string> { | |||
| 
 | ||||
|     private readonly value: UIEventSource<string> | ||||
| 
 | ||||
|     private readonly _element: HTMLElement; | ||||
|      | ||||
|     constructor( | ||||
|         value?: UIEventSource<string> | ||||
|     ) { | ||||
|         super(); | ||||
|         this.value = value ?? new UIEventSource<string>(undefined); | ||||
|         const self = this; | ||||
|         | ||||
|         const el = document.createElement("input") | ||||
|         this._element = el; | ||||
|         el.type = "date" | ||||
|         el.oninput = () => { | ||||
|             // Already in YYYY-MM-DD value! 
 | ||||
|             self.value.setData(el.value); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.value.addCallbackAndRun(v => { | ||||
|             if(v === undefined){ | ||||
|                 return; | ||||
|             } | ||||
|             self.SetValue(v); | ||||
|             el.value = v; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         return `<span id="${this.id}"><input type='date' id='date-${this.id}'></span>`; | ||||
|     } | ||||
|      | ||||
|     private SetValue(date: string){ | ||||
|         const field = document.getElementById("date-" + this.id); | ||||
|         if (field === undefined || field === null) { | ||||
|             return; | ||||
|         } | ||||
|         // @ts-ignore
 | ||||
|         field.value = date; | ||||
|     } | ||||
| 
 | ||||
|     protected InnerUpdate() { | ||||
|         const field = document.getElementById("date-" + this.id); | ||||
|         if (field === undefined || field === null) { | ||||
|             return; | ||||
|         } | ||||
|         const self = this; | ||||
|         field.oninput = () => { | ||||
|             // Already in YYYY-MM-DD value! 
 | ||||
|             // @ts-ignore
 | ||||
|             self.value.setData(field.value); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         return this._element | ||||
|     } | ||||
|     GetValue(): UIEventSource<string> { | ||||
|         return this.value; | ||||
|     } | ||||
|  |  | |||
|  | @ -1,113 +0,0 @@ | |||
| import {InputElement} from "./InputElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {DropDown} from "./DropDown"; | ||||
| import {TextField} from "./TextField"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {UIElement} from "../UIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {FromJSON} from "../../Customizations/JSON/FromJSON"; | ||||
| import ValidatedTextField from "./ValidatedTextField"; | ||||
| 
 | ||||
| export default class SingleTagInput extends InputElement<string> { | ||||
| 
 | ||||
|     private readonly _value: UIEventSource<string>; | ||||
|     IsSelected: UIEventSource<boolean>; | ||||
| 
 | ||||
|     private key: InputElement<string>; | ||||
|     private value: InputElement<string>; | ||||
|     private operator: DropDown<string> | ||||
|     private readonly helpMessage: UIElement; | ||||
| 
 | ||||
|     constructor(value: UIEventSource<string> = undefined) { | ||||
|         super(undefined); | ||||
|         this._value = value ?? new UIEventSource<string>(""); | ||||
|         this.helpMessage = new VariableUiElement(this._value.map(tagDef => { | ||||
|                 try { | ||||
|                     FromJSON.Tag(tagDef, ""); | ||||
|                     return ""; | ||||
|                 } catch (e) { | ||||
|                     return `<br/><span class='alert'>${e}</span>` | ||||
|                 } | ||||
|             } | ||||
|         )); | ||||
| 
 | ||||
|         this.key = ValidatedTextField.KeyInput(); | ||||
| 
 | ||||
|         this.value = new TextField({ | ||||
|                 placeholder: "value - if blank, matches if key is NOT present", | ||||
|                 value: new UIEventSource<string>("") | ||||
|             } | ||||
|         ); | ||||
|          | ||||
|         this.operator = new DropDown<string>("", [ | ||||
|             {value: "=", shown: "="}, | ||||
|             {value: "~", shown: "~"}, | ||||
|             {value: "!~", shown: "!~"} | ||||
|         ]); | ||||
|         this.operator.GetValue().setData("="); | ||||
| 
 | ||||
|         const self = this; | ||||
| 
 | ||||
|         function updateValue() { | ||||
|             if (self.key.GetValue().data === undefined || | ||||
|                 self.value.GetValue().data === undefined || | ||||
|                 self.operator.GetValue().data === undefined) { | ||||
|                 return undefined; | ||||
|             } | ||||
|             self._value.setData(self.key.GetValue().data + self.operator.GetValue().data + self.value.GetValue().data); | ||||
|         } | ||||
| 
 | ||||
|         this.key.GetValue().addCallback(() => updateValue()); | ||||
|         this.operator.GetValue().addCallback(() => updateValue()); | ||||
|         this.value.GetValue().addCallback(() => updateValue()); | ||||
| 
 | ||||
| 
 | ||||
|         function loadValue(value: string) { | ||||
|             if (value === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             let parts: string[]; | ||||
|             if (value.indexOf("=") >= 0) { | ||||
|                 parts = Utils.SplitFirst(value, "="); | ||||
|                 self.operator.GetValue().setData("="); | ||||
|             } else if (value.indexOf("!~") > 0) { | ||||
|                 parts = Utils.SplitFirst(value, "!~"); | ||||
|                 self.operator.GetValue().setData("!~"); | ||||
| 
 | ||||
|             } else if (value.indexOf("~") > 0) { | ||||
|                 parts = Utils.SplitFirst(value, "~"); | ||||
|                 self.operator.GetValue().setData("~"); | ||||
|             } else { | ||||
|                 console.warn("Invalid value for tag: ", value) | ||||
|                 return; | ||||
|             } | ||||
|             self.key.GetValue().setData(parts[0]); | ||||
|             self.value.GetValue().setData(parts[1]); | ||||
|         } | ||||
| 
 | ||||
|         self._value.addCallback(loadValue); | ||||
|         loadValue(self._value.data); | ||||
|         this.IsSelected = this.key.IsSelected.map( | ||||
|             isSelected => isSelected || this.value.IsSelected.data, [this.value.IsSelected] | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: string): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         return new Combine([ | ||||
|             this.key, this.operator, this.value, | ||||
|             this.helpMessage | ||||
|         ]).Render(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     GetValue(): UIEventSource<string> { | ||||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,99 +1,85 @@ | |||
| import {UIElement} from "../UIElement"; | ||||
| import {InputElement} from "./InputElement"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 
 | ||||
| export class TextField extends InputElement<string> { | ||||
|     private readonly value: UIEventSource<string>; | ||||
|     public readonly enterPressed = new UIEventSource<string>(undefined); | ||||
|     private readonly _placeholder: UIElement; | ||||
|     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     private readonly _htmlType: string; | ||||
|     private readonly _inputMode : string; | ||||
|     private readonly _textAreaRows: number; | ||||
| 
 | ||||
|     private readonly _isValid: (string,country) => boolean; | ||||
|     private _label: UIElement; | ||||
|      | ||||
|     private _element: HTMLElement; | ||||
|     private readonly _isValid: (s: string, country?: () => string) => boolean; | ||||
| 
 | ||||
|     constructor(options?: { | ||||
|         placeholder?: string | UIElement, | ||||
|         placeholder?: string | BaseUIElement, | ||||
|         value?: UIEventSource<string>, | ||||
|         textArea?: boolean, | ||||
|         htmlType?: string, | ||||
|         inputMode?: string, | ||||
|         label?: UIElement, | ||||
|         label?: BaseUIElement, | ||||
|         textAreaRows?: number, | ||||
|         isValid?: ((s: string, country?: () => string) => boolean) | ||||
|     }) { | ||||
|         super(undefined); | ||||
|         super(); | ||||
|         const self = this; | ||||
|         this.value = new UIEventSource<string>(""); | ||||
|         options = options ?? {}; | ||||
|         this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text"); | ||||
|         this.value = options?.value ?? new UIEventSource<string>(undefined); | ||||
| 
 | ||||
|         this._label = options.label; | ||||
|         this._textAreaRows = options.textAreaRows; | ||||
|         this._isValid = options.isValid ?? ((str, country) => true); | ||||
| 
 | ||||
|         this._placeholder = Translations.W(options.placeholder ?? ""); | ||||
|         this._inputMode = options.inputMode; | ||||
|         this.ListenTo(this._placeholder._source); | ||||
| 
 | ||||
|         this._isValid = options.isValid ?? (_ => true); | ||||
|          | ||||
|         this.onClick(() => { | ||||
|             self.IsSelected.setData(true) | ||||
|         }); | ||||
|         this.value.addCallback((t) => { | ||||
|             const field = document.getElementById("txt-"+this.id); | ||||
|             if (field === undefined || field === null) { | ||||
|                 return; | ||||
|             } | ||||
|             field.className = self.IsValid(t) ? "" : "invalid"; | ||||
| 
 | ||||
|             if (t === undefined || t === null) { | ||||
| 
 | ||||
| 
 | ||||
|         const placeholder = Translations.W(options. placeholder ?? "").ConstructElement().innerText.replace("'", "'"); | ||||
|          | ||||
|         this.SetClass("form-text-field") | ||||
|         let inputEl : HTMLElement | ||||
|         if(options.htmlType === "area"){ | ||||
|             const el = document.createElement("textarea") | ||||
|             el.placeholder = placeholder | ||||
|             el.rows = options.textAreaRows | ||||
|             el.cols = 50 | ||||
|             el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box" | ||||
|             inputEl = el; | ||||
|         }else{ | ||||
|             const el = document.createElement("input") | ||||
|             el.type = options.htmlType | ||||
|                 el.inputMode = options.inputMode | ||||
|             el.placeholder = placeholder | ||||
|             inputEl = el | ||||
|         } | ||||
| 
 | ||||
|         const form = document.createElement("form") | ||||
|         form.onsubmit = () => false; | ||||
|         | ||||
|        if(options.label){ | ||||
|            form.appendChild(options.label.ConstructElement()) | ||||
|        } | ||||
|          | ||||
|         this._element = form; | ||||
| 
 | ||||
|         const field = inputEl; | ||||
| 
 | ||||
| 
 | ||||
|         this.value.addCallbackAndRun(value => { | ||||
|             if (!(value !== undefined && value !== null)) { | ||||
|                 return; | ||||
|             } | ||||
|             // @ts-ignore
 | ||||
|             field.value = t; | ||||
|         }); | ||||
|         this.dumbMode = false; | ||||
|     } | ||||
|             field.value = value; | ||||
|             if(self.IsValid(value)){ | ||||
|                 self.RemoveClass("invalid") | ||||
|             }else{ | ||||
|                 self.SetClass("invalid") | ||||
|             } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<string> { | ||||
|         return this.value; | ||||
|     } | ||||
|         }) | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
| 
 | ||||
|         const placeholder = this._placeholder.InnerRender().replace("'", "'"); | ||||
|         if (this._htmlType === "area") { | ||||
|             return `<span id="${this.id}"><textarea id="txt-${this.id}" placeholder='${placeholder}' class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea></span>` | ||||
|         } | ||||
| 
 | ||||
|         let label = ""; | ||||
|         if (this._label != undefined) { | ||||
|             label = this._label.Render(); | ||||
|         } | ||||
|         let inputMode = "" | ||||
|         if(this._inputMode !== undefined){ | ||||
|             inputMode = `inputmode="${this._inputMode}" ` | ||||
|         } | ||||
|         return new Combine([ | ||||
|             `<span id="${this.id}">`, | ||||
|             `<form onSubmit='return false' class='form-text-field'>`, | ||||
|             label, | ||||
|             `<input type='${this._htmlType}' ${inputMode} placeholder='${placeholder}' id='txt-${this.id}'/>`, | ||||
|             `</form>`, | ||||
|             `</span>` | ||||
|         ]).Render(); | ||||
|     } | ||||
|      | ||||
|     InnerUpdate() { | ||||
|         const field = document.getElementById("txt-" + this.id); | ||||
|         const self = this; | ||||
|         field.oninput = () => { | ||||
|              | ||||
| 
 | ||||
|             // How much characters are on the right, not including spaces?
 | ||||
|             // @ts-ignore
 | ||||
|             const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length; | ||||
|  | @ -107,11 +93,11 @@ export class TextField extends InputElement<string> { | |||
|             // Setting the value might cause the value to be set again. We keep the distance _to the end_ stable, as phone number formatting might cause the start to change
 | ||||
|             // See https://github.com/pietervdvn/MapComplete/issues/103
 | ||||
|             // We reread the field value - it might have changed!
 | ||||
|              | ||||
| 
 | ||||
|             // @ts-ignore
 | ||||
|             val = field.value; | ||||
|             let newCursorPos = val.length - endDistance; | ||||
|             while(newCursorPos >= 0 &&  | ||||
|             while(newCursorPos >= 0 && | ||||
|                 // We count the number of _actual_ characters (non-space characters) on the right of the new value
 | ||||
|                 // This count should become bigger then the end distance
 | ||||
|                 val.substr(newCursorPos).replace(/ /g, '').length < endDistance | ||||
|  | @ -119,14 +105,10 @@ export class TextField extends InputElement<string> { | |||
|                 newCursorPos --; | ||||
|             } | ||||
|             // @ts-ignore
 | ||||
|             self.SetCursorPosition(newCursorPos); | ||||
|             TextField.SetCursorPosition(newCursorPos); | ||||
|         }; | ||||
| 
 | ||||
|         if (this.value.data !== undefined && this.value.data !== null) { | ||||
|             // @ts-ignore
 | ||||
|             field.value = this.value.data; | ||||
|         } | ||||
| 
 | ||||
|          | ||||
|         field.addEventListener("focusin", () => self.IsSelected.setData(true)); | ||||
|         field.addEventListener("focusout", () => self.IsSelected.setData(false)); | ||||
| 
 | ||||
|  | @ -136,22 +118,31 @@ export class TextField extends InputElement<string> { | |||
|                 // @ts-ignore
 | ||||
|                 self.enterPressed.setData(field.value); | ||||
|             } | ||||
|         }); | ||||
|         });         | ||||
|          | ||||
|          | ||||
|          | ||||
|     } | ||||
| 
 | ||||
|     public SetCursorPosition(i: number) { | ||||
|         const field = document.getElementById('txt-' + this.id); | ||||
|         if(field === undefined || field === null){ | ||||
|     GetValue(): UIEventSource<string> { | ||||
|         return this.value; | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         return this._element; | ||||
|     } | ||||
| 
 | ||||
|     private static SetCursorPosition(textfield: HTMLElement, i: number) { | ||||
|         if(textfield === undefined || textfield === null){ | ||||
|             return; | ||||
|         } | ||||
|         if (i === -1) { | ||||
|             // @ts-ignore
 | ||||
|             i = field.value.length; | ||||
|             i = textfield.value.length; | ||||
|         } | ||||
|         field.focus(); | ||||
|         textfield.focus(); | ||||
|         // @ts-ignore
 | ||||
|         field.setSelectionRange(i, i); | ||||
|         textfield.setSelectionRange(i, i); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										22
									
								
								UI/Input/Toggle.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								UI/Input/Toggle.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| 
 | ||||
| /** | ||||
|  * The 'Toggle' is a UIElement showing either one of two elements, depending on the state. | ||||
|  * It can be used to implement e.g. checkboxes or collapsible elements | ||||
|  */ | ||||
| export default class Toggle extends VariableUiElement{ | ||||
| 
 | ||||
|     public readonly isEnabled: UIEventSource<boolean>; | ||||
| 
 | ||||
|     constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, data: UIEventSource<boolean> = new UIEventSource<boolean>(false)) { | ||||
|         super( | ||||
|             data.map(isEnabled => isEnabled ? showEnabled : showDisabled) | ||||
|         ); | ||||
|         this.onClick(() => { | ||||
|             data.setData(!data.data); | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue