import {UIElement} from "../UIElement"; import {InputElement} from "./InputElement"; import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; import * as EmailValidator from "email-validator"; import {parsePhoneNumberFromString} from "libphonenumber-js"; import {DropDown} from "./DropDown"; export class ValidatedTextField { public static explanations = { "string": "A basic, 255-char string", "date": "A date", "wikidata": "A wikidata identifier, e.g. Q42", "int": "A number", "nat": "A positive number", "float": "A decimal", "pfloat": "A positive decimal", "email": "An email adress", "url": "A url", "phone": "A phone number" } public static TypeDropdown() : DropDown{ const values : {value: string, shown: string}[] = []; const expl = ValidatedTextField.explanations; for(const key in expl){ values.push({value: key, shown: `${key} - ${expl[key]}`}) } return new DropDown("", values) } public static inputValidation = { "$": () => true, "string": () => true, "date": () => true, // TODO validate and add a date picker "wikidata": () => true, // TODO validate wikidata IDS "int": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))}, "nat": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0}, "float": (str) => !isNaN(Number(str)), "pfloat": (str) => !isNaN(Number(str)) && Number(str) >= 0, "email": (str) => EmailValidator.validate(str), "url": (str) => str, "phone": (str, country) => { return parsePhoneNumberFromString(str, country?.toUpperCase())?.isValid() ?? false; } } public static formatting = { "phone": (str, country) => { console.log("country formatting", country) return parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational() } } } export class TextField extends InputElement { public static StringInput(textArea: boolean = false): TextField { return new TextField({ toString: str => str, fromString: str => str, textArea: textArea }); } public static KeyInput(allowEmpty : boolean = false): TextField{ return new TextField({ placeholder: "key", fromString: str => { if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) { return str; } if(str === "" && allowEmpty){ return ""; } return undefined }, toString: str => str }); } public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField{ const isValid = ValidatedTextField.inputValidation[type]; extraValidation = extraValidation ?? (() => true) return new TextField({ fromString: str => { if(!isValid(str)){ return undefined; } const n = Number(str); if(!extraValidation(n)){ return undefined; } return n; }, toString: num => ""+num, placeholder: type }); } private readonly value: UIEventSource; private readonly mappedValue: UIEventSource; public readonly enterPressed = new UIEventSource(undefined); private readonly _placeholder: UIElement; private readonly _fromString?: (string: string) => T; private readonly _toString: (t: T) => string; private readonly startValidated: boolean; public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly _isArea: boolean; private _textAreaRows: number; constructor(options: { /** * Shown as placeholder */ placeholder?: string | UIElement, /** * Converts the T to a (canonical) string * @param t */ toString: (t: T) => string, /** * Converts the string to a T * Returns undefined if invalid * @param string */ fromString: (string: string) => T, value?: UIEventSource, startValidated?: boolean, textArea?: boolean, textAreaRows?: number }) { super(undefined); const self = this; this.value = new UIEventSource(""); this._isArea = options.textArea ?? false; this.mappedValue = options?.value ?? new UIEventSource(undefined); this.mappedValue.addCallback(() => self.InnerUpdate()); // @ts-ignore this._fromString = options.fromString ?? ((str) => (str)) this.value.addCallback((str) => this.mappedValue.setData(options.fromString(str))); this.mappedValue.addCallback((t) => this.value.setData(options.toString(t))); this._textAreaRows = options.textAreaRows; this._placeholder = Translations.W(options.placeholder ?? ""); this.ListenTo(this._placeholder._source); this._toString = options.toString ?? ((t) => ("" + t)); this.onClick(() => { self.IsSelected.setData(true) }); this.mappedValue.addCallback((t) => { if (t === undefined || t === null) { return; } const field = document.getElementById('text-' + this.id); if (field === undefined || field === null) { return; } // @ts-ignore field.value = options.toString(t); }); this.startValidated = options.startValidated ?? false; } GetValue(): UIEventSource { return this.mappedValue; } InnerRender(): string { if(this._isArea){ return `` } const placeholder = this._placeholder.InnerRender().replace("'", "'"); return `
` + `` + `
`; } InnerUpdate() { const field = document.getElementById('text-' + this.id); if (field === null) { return; } this.mappedValue.addCallback((data) => { field.className = data !== undefined ? "valid" : "invalid"; }); field.className = this.mappedValue.data !== undefined ? "valid" : "invalid"; const self = this; field.oninput = () => { // @ts-ignore self.value.setData(field.value); }; 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); } }); if (this.IsValid(this.mappedValue.data)) { const expected = this._toString(this.mappedValue.data); // @ts-ignore if (field.value !== expected) { // @ts-ignore field.value = expected; } } } public SetCursorPosition(i: number) { const field = document.getElementById('text-' + 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: T): boolean { if (t === undefined || t === null) { return false; } const result = this._toString(t); return result !== undefined && result !== null; } }