forked from MapComplete/MapComplete
		
	Add date validation, add url validation (and tracker cleaning), add date picker, close #44
This commit is contained in:
		
							parent
							
								
									78368ef543
								
							
						
					
					
						commit
						415052af8a
					
				
					 8 changed files with 213 additions and 48 deletions
				
			
		|  | @ -95,10 +95,10 @@ export class Tag extends TagsFilter { | ||||||
|         this.key = key |         this.key = key | ||||||
|         this.value = value |         this.value = value | ||||||
|         if(key === undefined || key === ""){ |         if(key === undefined || key === ""){ | ||||||
|             throw "Invalid key"; |             throw "Invalid key: undefined or empty"; | ||||||
|         } |         } | ||||||
|         if(value === undefined){ |         if(value === undefined){ | ||||||
|             throw "Invalid value"; |             throw "Invalid value: value is undefined"; | ||||||
|         } |         } | ||||||
|         if(value === "*"){ |         if(value === "*"){ | ||||||
|          console.warn(`Got suspicious tag ${key}=*   ; did you mean ${key}~* ?`) |          console.warn(`Got suspicious tag ${key}=*   ; did you mean ${key}~* ?`) | ||||||
|  |  | ||||||
							
								
								
									
										35
									
								
								UI/Input/CombinedInputElement.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								UI/Input/CombinedInputElement.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | import {InputElement} from "./InputElement"; | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import Combine from "../Base/Combine"; | ||||||
|  | import {UIElement} from "../UIElement"; | ||||||
|  | 
 | ||||||
|  | export default class CombinedInputElement<T> extends InputElement<T> { | ||||||
|  |     private readonly _a: InputElement<T>; | ||||||
|  |     private readonly _b: UIElement; | ||||||
|  |     private readonly _combined: UIElement; | ||||||
|  |     public readonly IsSelected: UIEventSource<boolean>; | ||||||
|  | 
 | ||||||
|  |     constructor(a: InputElement<T>, b: InputElement<T>) { | ||||||
|  |         super(); | ||||||
|  |         this._a = a; | ||||||
|  |         this._b = b; | ||||||
|  |         this.IsSelected = this._a.IsSelected.map((isSelected) => { | ||||||
|  |             return isSelected || b.IsSelected.data | ||||||
|  |         }, [b.IsSelected]) | ||||||
|  |         this._combined = new Combine([this._a, this._b]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     GetValue(): UIEventSource<T> { | ||||||
|  |         return this._a.GetValue(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     InnerRender(): string { | ||||||
|  |         return this._combined.Render(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     IsValid(t: T): boolean { | ||||||
|  |         return this._a.IsValid(t); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								UI/Input/SimpleDatePicker.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								UI/Input/SimpleDatePicker.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | ||||||
|  | import {InputElement} from "./InputElement"; | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | 
 | ||||||
|  | export default class SimpleDatePicker extends InputElement<string> { | ||||||
|  | 
 | ||||||
|  |     private readonly value: UIEventSource<string> | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         value?: UIEventSource<string> | ||||||
|  |     ) { | ||||||
|  |         super(); | ||||||
|  |         this.value = value ?? new UIEventSource<string>(undefined); | ||||||
|  |         const self = this; | ||||||
|  |         this.value.addCallbackAndRun(v => { | ||||||
|  |             if(v === undefined){ | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             self.SetValue(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); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     GetValue(): UIEventSource<string> { | ||||||
|  |         return this.value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||||
|  | 
 | ||||||
|  |     IsValid(t: string): boolean { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -10,7 +10,9 @@ export class TextField extends InputElement<string> { | ||||||
|     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); |     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||||
|     private readonly _isArea: boolean; |     private readonly _isArea: boolean; | ||||||
|     private readonly _textAreaRows: number; |     private readonly _textAreaRows: number; | ||||||
|      | 
 | ||||||
|  |     private readonly _isValid: (string) => boolean; | ||||||
|  | 
 | ||||||
|     constructor(options?: { |     constructor(options?: { | ||||||
|         placeholder?: string | UIElement, |         placeholder?: string | UIElement, | ||||||
|         value?: UIEventSource<string>, |         value?: UIEventSource<string>, | ||||||
|  | @ -25,9 +27,8 @@ export class TextField extends InputElement<string> { | ||||||
|         this._isArea = options.textArea ?? false; |         this._isArea = options.textArea ?? false; | ||||||
|         this.value = options?.value ?? new UIEventSource<string>(undefined); |         this.value = options?.value ?? new UIEventSource<string>(undefined); | ||||||
| 
 | 
 | ||||||
|         // @ts-ignore
 |  | ||||||
|         this._fromString = options.fromString ?? ((str) => (str)) |  | ||||||
|         this._textAreaRows = options.textAreaRows; |         this._textAreaRows = options.textAreaRows; | ||||||
|  |         this._isValid = options.isValid ?? ((str) => true); | ||||||
| 
 | 
 | ||||||
|         this._placeholder = Translations.W(options.placeholder ?? ""); |         this._placeholder = Translations.W(options.placeholder ?? ""); | ||||||
|         this.ListenTo(this._placeholder._source); |         this.ListenTo(this._placeholder._source); | ||||||
|  | @ -36,16 +37,13 @@ export class TextField extends InputElement<string> { | ||||||
|             self.IsSelected.setData(true) |             self.IsSelected.setData(true) | ||||||
|         }); |         }); | ||||||
|         this.value.addCallback((t) => { |         this.value.addCallback((t) => { | ||||||
|             const field = document.getElementById(this.id); |             const field = document.getElementById("txt-"+this.id); | ||||||
|             if (field === undefined || field === null) { |             if (field === undefined || field === null) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             if (options.isValid) { |             field.className = self.IsValid(t) ? "" : "invalid"; | ||||||
|                 field.className = options.isValid(t) ? "" : "invalid"; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             if (t === undefined || t === null) { |             if (t === undefined || t === null) { | ||||||
|                 // @ts-ignore
 |  | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|  | @ -77,11 +75,18 @@ export class TextField extends InputElement<string> { | ||||||
|         const self = this; |         const self = this; | ||||||
|         field.oninput = () => { |         field.oninput = () => { | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|             var endDistance = field.value.length - field.selectionEnd; |             const endDistance = field.value.length - field.selectionEnd; | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|             self.value.setData(field.value); |             const 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
 |             // 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
 |             // See https://github.com/pietervdvn/MapComplete/issues/103
 | ||||||
|  |             // We reread the field value - it might have changed!
 | ||||||
|  |             // @ts-ignore
 | ||||||
|             self.SetCursorPosition(field.value.length - endDistance); |             self.SetCursorPosition(field.value.length - endDistance); | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|  | @ -119,7 +124,10 @@ export class TextField extends InputElement<string> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     IsValid(t: string): boolean { |     IsValid(t: string): boolean { | ||||||
|         return !(t === undefined || t === null); |         if (t === undefined || t === null) { | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         return this._isValid(t); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,6 +6,16 @@ import {InputElement} from "./InputElement"; | ||||||
| import {TextField} from "./TextField"; | import {TextField} from "./TextField"; | ||||||
| import {UIElement} from "../UIElement"; | import {UIElement} from "../UIElement"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import CombinedInputElement from "./CombinedInputElement"; | ||||||
|  | import SimpleDatePicker from "./SimpleDatePicker"; | ||||||
|  | 
 | ||||||
|  | interface TextFieldDef { | ||||||
|  |     name: string, | ||||||
|  |     explanation: string, | ||||||
|  |     isValid: ((s: string, country?: string) => boolean), | ||||||
|  |     reformat?: ((s: string, country?: string) => string), | ||||||
|  |     inputHelper?: (value:UIEventSource<string>) => InputElement<string> | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export default class ValidatedTextField { | export default class ValidatedTextField { | ||||||
| 
 | 
 | ||||||
|  | @ -13,12 +23,8 @@ export default class ValidatedTextField { | ||||||
|     private static tp(name: string, |     private static tp(name: string, | ||||||
|                       explanation: string, |                       explanation: string, | ||||||
|                       isValid?: ((s: string, country?: string) => boolean), |                       isValid?: ((s: string, country?: string) => boolean), | ||||||
|                       reformat?: ((s: string, country?: string) => string)): { |                       reformat?: ((s: string, country?: string) => string), | ||||||
|         name: string, |                       inputHelper?: (value: UIEventSource<string>) => InputElement<string>): TextFieldDef { | ||||||
|         explanation: string, |  | ||||||
|         isValid: ((s: string, country?: string) => boolean), |  | ||||||
|         reformat?: ((s: string, country?: string) => string) |  | ||||||
|     } { |  | ||||||
| 
 | 
 | ||||||
|         if (isValid === undefined) { |         if (isValid === undefined) { | ||||||
|             isValid = () => true; |             isValid = () => true; | ||||||
|  | @ -33,17 +39,36 @@ export default class ValidatedTextField { | ||||||
|             name: name, |             name: name, | ||||||
|             explanation: explanation, |             explanation: explanation, | ||||||
|             isValid: isValid, |             isValid: isValid, | ||||||
|             reformat: reformat |             reformat: reformat, | ||||||
|  |             inputHelper: inputHelper | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static tpList = [ |     public static tpList: TextFieldDef[] = [ | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|             "string", |             "string", | ||||||
|             "A basic string"), |             "A basic string"), | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|             "date", |             "date", | ||||||
|             "A date"), |             "A date", | ||||||
|  |             (str) => { | ||||||
|  |                 const time = Date.parse(str); | ||||||
|  |                 return !isNaN(time); | ||||||
|  |             }, | ||||||
|  |             (str) => { | ||||||
|  |                 const d = new Date(str); | ||||||
|  |                 let month = '' + (d.getMonth() + 1); | ||||||
|  |                 let day = '' + d.getDate(); | ||||||
|  |                 const year = d.getFullYear(); | ||||||
|  | 
 | ||||||
|  |                 if (month.length < 2) | ||||||
|  |                     month = '0' + month; | ||||||
|  |                 if (day.length < 2) | ||||||
|  |                     day = '0' + day; | ||||||
|  | 
 | ||||||
|  |                 return [year, month, day].join('-'); | ||||||
|  |             }, | ||||||
|  |             (value) => new SimpleDatePicker(value)), | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|             "wikidata", |             "wikidata", | ||||||
|             "A wikidata identifier, e.g. Q42"), |             "A wikidata identifier, e.g. Q42"), | ||||||
|  | @ -82,7 +107,30 @@ export default class ValidatedTextField { | ||||||
|             (str) => EmailValidator.validate(str)), |             (str) => EmailValidator.validate(str)), | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|             "url", |             "url", | ||||||
|             "A url"), |             "A url", | ||||||
|  |             (str) => { | ||||||
|  |                 try { | ||||||
|  |                     new URL(str); | ||||||
|  |                     return true; | ||||||
|  |                 } catch (e) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             }, (str) => { | ||||||
|  |                 try { | ||||||
|  |                     const url = new URL(str); | ||||||
|  |                     const blacklistedTrackingParams = [ | ||||||
|  |                         "fbclid",// Oh god, how I hate the fbclid. Let it burn, burn in hell!
 | ||||||
|  |                         "gclid", | ||||||
|  |                         "cmpid", "agid", "utm", "utm_source"] | ||||||
|  |                     for (const dontLike of blacklistedTrackingParams) { | ||||||
|  |                         url.searchParams.delete(dontLike) | ||||||
|  |                     } | ||||||
|  |                     return url.toString(); | ||||||
|  |                 } catch (e) { | ||||||
|  |                     console.error(e) | ||||||
|  |                     return undefined; | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|             "phone", |             "phone", | ||||||
|             "A phone number", |             "A phone number", | ||||||
|  | @ -114,15 +162,33 @@ export default class ValidatedTextField { | ||||||
|         } |         } | ||||||
|         return new DropDown<string>("", values) |         return new DropDown<string>("", values) | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     public static AllTypes = ValidatedTextField.allTypesDict(); |     public static AllTypes = ValidatedTextField.allTypesDict(); | ||||||
| 
 | 
 | ||||||
|     public static InputForType(type: string): TextField { |     public static InputForType(type: string, options?: { | ||||||
|          |         placeholder?: string | UIElement, | ||||||
|         return new TextField({ |         value?: UIEventSource<string>, | ||||||
|             placeholder: type, |         textArea?: boolean, | ||||||
|             isValid: ValidatedTextField.AllTypes[type] |         textAreaRows?: number, | ||||||
|         }) |         isValid?: ((s: string) => boolean) | ||||||
|  |     }): InputElement<string> { | ||||||
|  |         options = options ?? {}; | ||||||
|  |         options.placeholder = options.placeholder ?? type; | ||||||
|  |         const tp: TextFieldDef = ValidatedTextField.AllTypes[type] | ||||||
|  |         let isValid = tp.isValid; | ||||||
|  |         if (options.isValid) { | ||||||
|  |             const optValid = options.isValid; | ||||||
|  |             isValid = (str, country) => { | ||||||
|  |                 return ValidatedTextField.AllTypes[type](str, country) && optValid(str); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         options.isValid = isValid; | ||||||
|  | 
 | ||||||
|  |         let input: InputElement<string> = new TextField(options); | ||||||
|  |         if (tp.inputHelper) { | ||||||
|  |             input = new CombinedInputElement(input, tp.inputHelper(input.GetValue())); | ||||||
|  |         } | ||||||
|  |         return input; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined): InputElement<number> { |     public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined): InputElement<number> { | ||||||
|  | @ -181,13 +247,19 @@ export default class ValidatedTextField { | ||||||
| 
 | 
 | ||||||
|     static Mapped<T>(fromString: (str) => T, toString: (T) => string, options?: { |     static Mapped<T>(fromString: (str) => T, toString: (T) => string, options?: { | ||||||
|         placeholder?: string | UIElement, |         placeholder?: string | UIElement, | ||||||
|  |         type?: string, | ||||||
|         value?: UIEventSource<string>, |         value?: UIEventSource<string>, | ||||||
|         startValidated?: boolean, |         startValidated?: boolean, | ||||||
|         textArea?: boolean, |         textArea?: boolean, | ||||||
|         textAreaRows?: number, |         textAreaRows?: number, | ||||||
|         isValid?: ((string: string) => boolean) |         isValid?: ((string: string) => boolean) | ||||||
|     }): InputElement<T> { |     }): InputElement<T> { | ||||||
|         const textField = new TextField(options); |         let textField: InputElement<string>; | ||||||
|  |         if (options.type) { | ||||||
|  |             textField = ValidatedTextField.InputForType(options.type); | ||||||
|  |         } else { | ||||||
|  |             textField = new TextField(options); | ||||||
|  |         } | ||||||
|         return new InputElementMap( |         return new InputElementMap( | ||||||
|             textField, (a, b) => a === b, |             textField, (a, b) => a === b, | ||||||
|             fromString, toString |             fromString, toString | ||||||
|  |  | ||||||
|  | @ -325,10 +325,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { | ||||||
|             throw "Unkown type: "+type; |             throw "Unkown type: "+type; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let isValid = ValidatedTextField.AllTypes[type].isValid; |          | ||||||
|         if (isValid === undefined) { |  | ||||||
|             isValid = () => true; |  | ||||||
|         } |  | ||||||
|         let formatter = ValidatedTextField.AllTypes[type].reformat ?? ((str) => str); |         let formatter = ValidatedTextField.AllTypes[type].reformat ?? ((str) => str); | ||||||
| 
 | 
 | ||||||
|         const pickString = |         const pickString = | ||||||
|  | @ -336,17 +333,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement { | ||||||
|                 if (string === "" || string === undefined) { |                 if (string === "" || string === undefined) { | ||||||
|                     return undefined; |                     return undefined; | ||||||
|                 } |                 } | ||||||
|                 if (!isValid(string, this._source.data._country)) { |  | ||||||
|                     return undefined; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|                 const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); |                 const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); | ||||||
| 
 | 
 | ||||||
|                 if (tag.value.length > 255) { |  | ||||||
|                     return undefined; // Too long
 |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (freeform.extraTags === undefined) { |                 if (freeform.extraTags === undefined) { | ||||||
|                     return tag; |                     return tag; | ||||||
|                 } |                 } | ||||||
|  | @ -374,7 +363,8 @@ export class TagRendering extends UIElement implements TagDependantUIElement { | ||||||
| 
 | 
 | ||||||
|         return ValidatedTextField.Mapped(pickString, toString, { |         return ValidatedTextField.Mapped(pickString, toString, { | ||||||
|             placeholder: this._freeform.placeholder, |             placeholder: this._freeform.placeholder, | ||||||
|             isValid: isValid, |             type: type, | ||||||
|  |             isValid: (str) => (str.length <= 255), | ||||||
|             textArea: isTextArea |             textArea: isTextArea | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -83,7 +83,7 @@ export class UserBadge extends UIElement { | ||||||
| 
 | 
 | ||||||
|         let dryrun = ""; |         let dryrun = ""; | ||||||
|         if (user.dryRun) { |         if (user.dryRun) { | ||||||
|             dryrun = " <span class='alert'>TESTING</span>"; |             dryrun = new FixedUiElement("TESTING").SetClass("alert").Render(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (user.home !== undefined) { |         if (user.home !== undefined) { | ||||||
|  | @ -98,7 +98,7 @@ export class UserBadge extends UIElement { | ||||||
|         const settings = |         const settings = | ||||||
|             "<a href='https://www.openstreetmap.org/user/" + encodeURIComponent(user.name) + "/account' target='_blank'>" + |             "<a href='https://www.openstreetmap.org/user/" + encodeURIComponent(user.name) + "/account' target='_blank'>" + | ||||||
|             "<img class='small-userbadge-icon' src='./assets/gear.svg' alt='settings'>" + |             "<img class='small-userbadge-icon' src='./assets/gear.svg' alt='settings'>" + | ||||||
|             "</a> "; |             "</a>"; | ||||||
| 
 | 
 | ||||||
|         const userIcon = "<a href='https://www.openstreetmap.org/user/" + encodeURIComponent(user.name) + "' target='_blank'><img id='profile-pic' src='" + user.img + "' alt='profile-pic'/></a>"; |         const userIcon = "<a href='https://www.openstreetmap.org/user/" + encodeURIComponent(user.name) + "' target='_blank'><img id='profile-pic' src='" + user.img + "' alt='profile-pic'/></a>"; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -1,8 +1,8 @@ | ||||||
| import ValidatedTextField from "./UI/Input/ValidatedTextField"; |  | ||||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||||
|  | import SimpleDatePicker from "./UI/Input/SimpleDatePicker"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const vtf= ValidatedTextField.KeyInput(true); | const vtf=new SimpleDatePicker(); | ||||||
| vtf.AttachTo('maindiv') | vtf.AttachTo('maindiv') | ||||||
| vtf.GetValue().addCallback(console.log) | vtf.GetValue().addCallback(console.log) | ||||||
| new VariableUiElement(vtf.GetValue().map(n => ""+n)).AttachTo("extradiv") | new VariableUiElement(vtf.GetValue().map(n => ""+n)).AttachTo("extradiv") | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue