| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  | import {UIElement} from "../UIElement"; | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  | import {InputElement} from "./InputElement"; | 
					
						
							| 
									
										
										
										
											2020-07-21 00:38:03 +02:00
										 |  |  | import Translations from "../i18n/Translations"; | 
					
						
							| 
									
										
										
										
											2020-08-17 17:23:15 +02:00
										 |  |  | import {UIEventSource} from "../../Logic/UIEventSource"; | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  | import * as EmailValidator from "email-validator"; | 
					
						
							|  |  |  | import {parsePhoneNumberFromString} from "libphonenumber-js"; | 
					
						
							| 
									
										
										
										
											2020-09-02 11:37:34 +02:00
										 |  |  | import {DropDown} from "./DropDown"; | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | export class ValidatedTextField { | 
					
						
							| 
									
										
										
										
											2020-09-02 11:37:34 +02:00
										 |  |  |      | 
					
						
							|  |  |  |     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<string>{ | 
					
						
							|  |  |  |         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<string>("", values) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |      | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  |     public static inputValidation = { | 
					
						
							| 
									
										
										
										
											2020-08-30 01:13:18 +02:00
										 |  |  |         "$": () => true, | 
					
						
							|  |  |  |         "string": () => true, | 
					
						
							|  |  |  |         "date": () => true, // TODO validate and add a date picker
 | 
					
						
							|  |  |  |         "wikidata": () => true, // TODO validate wikidata IDS
 | 
					
						
							| 
									
										
										
										
											2020-08-23 01:49:19 +02:00
										 |  |  |         "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}, | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  |         "float": (str) => !isNaN(Number(str)), | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  |         "pfloat": (str) => !isNaN(Number(str)) && Number(str) >= 0, | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  |         "email": (str) => EmailValidator.validate(str), | 
					
						
							|  |  |  |         "url": (str) => str, | 
					
						
							|  |  |  |         "phone": (str, country) => { | 
					
						
							| 
									
										
										
										
											2020-09-11 02:14:16 +02:00
										 |  |  |             return parsePhoneNumberFromString(str, country?.toUpperCase())?.isValid() ?? false; | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public static formatting = { | 
					
						
							|  |  |  |         "phone": (str, country) => { | 
					
						
							|  |  |  |             console.log("country formatting", country) | 
					
						
							| 
									
										
										
										
											2020-09-11 02:14:16 +02:00
										 |  |  |             return parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational() | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  | export class TextField<T> extends InputElement<T> { | 
					
						
							| 
									
										
										
										
											2020-09-07 01:03:23 +02:00
										 |  |  |     | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  |     public static StringInput(textArea: boolean = false): TextField<string> { | 
					
						
							|  |  |  |         return new TextField<string>({ | 
					
						
							|  |  |  |             toString: str => str, | 
					
						
							|  |  |  |             fromString: str => str, | 
					
						
							|  |  |  |             textArea: textArea | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							| 
									
										
										
										
											2020-09-03 00:00:37 +02:00
										 |  |  |     public static KeyInput(allowEmpty : boolean = false): TextField<string>{ | 
					
						
							| 
									
										
										
										
											2020-09-02 11:37:34 +02:00
										 |  |  |         return new TextField<string>({ | 
					
						
							|  |  |  |             placeholder: "key", | 
					
						
							|  |  |  |             fromString: str => { | 
					
						
							|  |  |  |                 if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) { | 
					
						
							|  |  |  |                     return str; | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2020-09-03 00:00:37 +02:00
										 |  |  |                 if(str === "" && allowEmpty){ | 
					
						
							|  |  |  |                     return ""; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                  | 
					
						
							| 
									
										
										
										
											2020-09-02 11:37:34 +02:00
										 |  |  |                 return undefined | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             toString: str => str | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  |     public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{ | 
					
						
							|  |  |  |         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 | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-30 01:13:18 +02:00
										 |  |  |     private readonly value: UIEventSource<string>; | 
					
						
							|  |  |  |     private readonly mappedValue: UIEventSource<T>; | 
					
						
							|  |  |  |     public readonly enterPressed = new UIEventSource<string>(undefined); | 
					
						
							|  |  |  |     private readonly _placeholder: UIElement; | 
					
						
							|  |  |  |     private readonly _fromString?: (string: string) => T; | 
					
						
							|  |  |  |     private readonly _toString: (t: T) => string; | 
					
						
							|  |  |  |     private readonly startValidated: boolean; | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  |     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | 
					
						
							|  |  |  |     private readonly _isArea: boolean; | 
					
						
							| 
									
										
										
										
											2020-09-07 01:03:23 +02:00
										 |  |  |     private _textAreaRows: number; | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |     constructor(options: { | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  |         /** | 
					
						
							|  |  |  |          * Shown as placeholder | 
					
						
							|  |  |  |          */ | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |         placeholder?: string | UIElement, | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         /** | 
					
						
							|  |  |  |          * Converts the T to a (canonical) string | 
					
						
							|  |  |  |          * @param t | 
					
						
							|  |  |  |          */ | 
					
						
							| 
									
										
										
										
											2020-07-20 18:24:00 +02:00
										 |  |  |         toString: (t: T) => string, | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  |         /** | 
					
						
							|  |  |  |          * Converts the string to a T | 
					
						
							|  |  |  |          * Returns undefined if invalid | 
					
						
							|  |  |  |          * @param string | 
					
						
							|  |  |  |          */ | 
					
						
							| 
									
										
										
										
											2020-07-20 18:24:00 +02:00
										 |  |  |         fromString: (string: string) => T, | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  |         value?: UIEventSource<T>, | 
					
						
							|  |  |  |         startValidated?: boolean, | 
					
						
							| 
									
										
										
										
											2020-09-07 01:03:23 +02:00
										 |  |  |         textArea?: boolean, | 
					
						
							|  |  |  |         textAreaRows?: number | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |     }) { | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |         super(undefined); | 
					
						
							| 
									
										
										
										
											2020-07-20 21:39:07 +02:00
										 |  |  |         const self = this; | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |         this.value = new UIEventSource<string>(""); | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  |         this._isArea = options.textArea ?? false; | 
					
						
							| 
									
										
										
										
											2020-07-20 21:39:07 +02:00
										 |  |  |         this.mappedValue = options?.value ?? new UIEventSource<T>(undefined); | 
					
						
							|  |  |  |         this.mappedValue.addCallback(() => self.InnerUpdate()); | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |         // @ts-ignore
 | 
					
						
							|  |  |  |         this._fromString = options.fromString ?? ((str) => (str)) | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |         this.value.addCallback((str) => this.mappedValue.setData(options.fromString(str))); | 
					
						
							|  |  |  |         this.mappedValue.addCallback((t) => this.value.setData(options.toString(t))); | 
					
						
							| 
									
										
										
										
											2020-09-07 01:03:23 +02:00
										 |  |  |         this._textAreaRows = options.textAreaRows; | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-21 00:38:03 +02:00
										 |  |  |         this._placeholder = Translations.W(options.placeholder ?? ""); | 
					
						
							|  |  |  |         this.ListenTo(this._placeholder._source); | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |         this._toString = options.toString ?? ((t) => ("" + t)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-10 21:06:56 +02:00
										 |  |  |         this.onClick(() => { | 
					
						
							|  |  |  |             self.IsSelected.setData(true) | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |         this.mappedValue.addCallback((t) => { | 
					
						
							| 
									
										
										
										
											2020-07-20 18:24:00 +02:00
										 |  |  |             if (t === undefined || t === null) { | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |                 return; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             const field = document.getElementById('text-' + this.id); | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |             if (field === undefined || field === null) { | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |                 return; | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |             // @ts-ignore
 | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |             field.value = options.toString(t); | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  |         }); | 
					
						
							|  |  |  |         this.startValidated = options.startValidated ?? false; | 
					
						
							| 
									
										
										
										
											2020-07-05 18:59:47 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     GetValue(): UIEventSource<T> { | 
					
						
							| 
									
										
										
										
											2020-07-20 13:28:45 +02:00
										 |  |  |         return this.mappedValue; | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-07-20 21:03:55 +02:00
										 |  |  |     InnerRender(): string { | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  |          | 
					
						
							|  |  |  |         if(this._isArea){ | 
					
						
							| 
									
										
										
										
											2020-09-07 01:03:23 +02:00
										 |  |  |             return `<textarea id="text-${this.id}" class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea>` | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							| 
									
										
										
										
											2020-09-12 23:15:17 +02:00
										 |  |  |         const placeholder = this._placeholder.InnerRender().replace("'", "'"); | 
					
						
							|  |  |  |          | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  |         return `<form onSubmit='return false' class='form-text-field'>` + | 
					
						
							| 
									
										
										
										
											2020-09-12 23:15:17 +02:00
										 |  |  |             `<input type='text' placeholder='${placeholder}' id='text-${this.id}'>` + | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  |             `</form>`; | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-20 21:39:07 +02:00
										 |  |  |     InnerUpdate() { | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  |         const field = document.getElementById('text-' + this.id); | 
					
						
							| 
									
										
										
										
											2020-07-20 21:03:55 +02:00
										 |  |  |         if (field === null) { | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |             return; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         this.mappedValue.addCallback((data) => { | 
					
						
							| 
									
										
										
										
											2020-08-30 01:13:18 +02:00
										 |  |  |             field.className = data !== undefined ? "valid" : "invalid"; | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  |         }); | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-22 02:12:46 +02:00
										 |  |  |         field.className = this.mappedValue.data !== undefined ? "valid" : "invalid"; | 
					
						
							| 
									
										
										
										
											2020-07-23 17:32:18 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  |         const self = this; | 
					
						
							|  |  |  |         field.oninput = () => { | 
					
						
							| 
									
										
										
										
											2020-07-05 18:59:47 +02:00
										 |  |  |             // @ts-ignore
 | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  |             self.value.setData(field.value); | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-31 02:59:47 +02:00
										 |  |  |         field.addEventListener("focusin", () => self.IsSelected.setData(true)); | 
					
						
							|  |  |  |         field.addEventListener("focusout", () => self.IsSelected.setData(false)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-10 21:06:56 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  |         field.addEventListener("keyup", function (event) { | 
					
						
							|  |  |  |             if (event.key === "Enter") { | 
					
						
							| 
									
										
										
										
											2020-07-05 18:59:47 +02:00
										 |  |  |                 // @ts-ignore
 | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  |                 self.enterPressed.setData(field.value); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2020-07-05 18:59:47 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-20 21:03:55 +02:00
										 |  |  |         if (this.IsValid(this.mappedValue.data)) { | 
					
						
							| 
									
										
										
										
											2020-07-20 21:39:07 +02:00
										 |  |  |             const expected = this._toString(this.mappedValue.data); | 
					
						
							| 
									
										
										
										
											2020-07-20 21:03:55 +02:00
										 |  |  |             // @ts-ignore
 | 
					
						
							| 
									
										
										
										
											2020-07-20 21:39:07 +02:00
										 |  |  |             if (field.value !== expected) { | 
					
						
							|  |  |  |                 // @ts-ignore
 | 
					
						
							|  |  |  |                 field.value = expected; | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2020-07-20 21:03:55 +02:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-05 18:59:47 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-01 02:12:33 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-11 00:23:19 +02:00
										 |  |  |     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); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |     IsValid(t: T): boolean { | 
					
						
							| 
									
										
										
										
											2020-09-11 00:23:19 +02:00
										 |  |  |         if (t === undefined || t === null) { | 
					
						
							| 
									
										
										
										
											2020-07-20 15:54:50 +02:00
										 |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         const result = this._toString(t); | 
					
						
							|  |  |  |         return result !== undefined && result !== null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-12 23:15:17 +02:00
										 |  |  | } | 
					
						
							|  |  |  |                  |