import {UIElement} from "../UIElement"; import {InputElement} from "./InputElement"; import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; export class TextField extends InputElement { private readonly value: UIEventSource; public readonly enterPressed = new UIEventSource(undefined); private readonly _placeholder: UIElement; public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly _htmlType: string; private readonly _textAreaRows: number; private readonly _isValid: (string, country) => boolean; constructor(options?: { placeholder?: string | UIElement, value?: UIEventSource, textArea?: boolean, htmlType?: string, textAreaRows?: number, isValid?: ((s: string, country?: string) => boolean) }) { super(undefined); const self = this; this.value = new UIEventSource(""); options = options ?? {}; this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text"); this.value = options?.value ?? new UIEventSource(undefined); this._textAreaRows = options.textAreaRows; this._isValid = options.isValid ?? ((str, country) => 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 { return this.value; } InnerRender(): string { if (this._htmlType === "area") { return `` } const placeholder = this._placeholder.InnerRender().replace("'", "'"); return `
` + `` + `
`; } 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: string = field.value; if (!self.IsValid(val)) { self.value.setData(undefined); } else { self.value.setData(val); } // 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 && // 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 ){ newCursorPos --; } // @ts-ignore self.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)); field.addEventListener("keyup", function (event) { if (event.key === "Enter") { // @ts-ignore self.enterPressed.setData(field.value); } }); } public SetCursorPosition(i: number) { if(this._htmlType !== "text" && this._htmlType !== "area"){ return; } const field = document.getElementById('txt-' + this.id); if(field === undefined || field === null){ return; } if (i === -1) { // @ts-ignore i = field.value.length; } field.focus(); // @ts-ignore field.setSelectionRange(i, i); } IsValid(t: string): boolean { if (t === undefined || t === null) { return false } return this._isValid(t, undefined); } }