forked from MapComplete/MapComplete
		
	First version of giving feedback to contriubtors on invalid values
This commit is contained in:
		
							parent
							
								
									e07b770e8c
								
							
						
					
					
						commit
						5221e91dcd
					
				
					 16 changed files with 586 additions and 436 deletions
				
			
		|  | @ -56,8 +56,8 @@ export default class FilterConfig { | ||||||
| 
 | 
 | ||||||
|             const fields: { name: string, type: string }[] = ((option.fields) ?? []).map((f, i) => { |             const fields: { name: string, type: string }[] = ((option.fields) ?? []).map((f, i) => { | ||||||
|                 const type = f.type ?? "string" |                 const type = f.type ?? "string" | ||||||
|                 if (!ValidatedTextField.AllTypes.has(type)) { |                 if (!ValidatedTextField.ForType(type) === undefined) { | ||||||
|                     throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(ValidatedTextField.AllTypes.keys()).join(",")}` |                       throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(ValidatedTextField.AvailableTypes()).join(",")}` | ||||||
|                 } |                 } | ||||||
|                 if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) { |                 if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) { | ||||||
|                     throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]` |                     throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]` | ||||||
|  |  | ||||||
|  | @ -133,7 +133,7 @@ export default class TagRenderingConfig { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|             if (!ValidatedTextField.AllTypes.has(this.freeform.type)) { |             if (!ValidatedTextField.ForType(this.freeform.key) === undefined) { | ||||||
|                 const knownKeys = ValidatedTextField.AvailableTypes().join(", "); |                 const knownKeys = ValidatedTextField.AvailableTypes().join(", "); | ||||||
|                 throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}` |                 throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}` | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -254,7 +254,7 @@ class AutomatonGui { | ||||||
| 
 | 
 | ||||||
|         LocalStorageSource.Get("automation-theme-id", "missing_streets").syncWith(themeSelect.GetValue()) |         LocalStorageSource.Get("automation-theme-id", "missing_streets").syncWith(themeSelect.GetValue()) | ||||||
| 
 | 
 | ||||||
|         const tilepath = ValidatedTextField.InputForType("url", { |         const tilepath = ValidatedTextField.ForType("url").ConstructInputElement({ | ||||||
|             placeholder: "Specifiy the path of the overview", |             placeholder: "Specifiy the path of the overview", | ||||||
|             inputStyle: "width: 100%" |             inputStyle: "width: 100%" | ||||||
|         }) |         }) | ||||||
|  | @ -305,7 +305,7 @@ class AutomatonGui { | ||||||
|             return Array.from(rezoomed) |             return Array.from(rezoomed) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const extraComment = ValidatedTextField.InputForType("text") |         const extraComment = ValidatedTextField.ForType("text").ConstructInputElement() | ||||||
|         LocalStorageSource.Get("automaton-extra-comment").syncWith(extraComment.GetValue()) |         LocalStorageSource.Get("automaton-extra-comment").syncWith(extraComment.GetValue()) | ||||||
| 
 | 
 | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|  |  | ||||||
|  | @ -181,7 +181,7 @@ export default class FilterView extends VariableUiElement { | ||||||
|         const properties = new UIEventSource<any>({}) |         const properties = new UIEventSource<any>({}) | ||||||
|         for (const {name, type} of filter.fields) { |         for (const {name, type} of filter.fields) { | ||||||
|             const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id) |             const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id) | ||||||
|             const field = ValidatedTextField.InputForType(type, { |             const field = ValidatedTextField.ForType(type).ConstructInputElement({ | ||||||
|                 value |                 value | ||||||
|             }).SetClass("inline-block") |             }).SetClass("inline-block") | ||||||
|             mappings.set(name, field) |             mappings.set(name, field) | ||||||
|  |  | ||||||
|  | @ -29,17 +29,17 @@ export class AskMetadata extends Combine implements FlowStep<{ | ||||||
| 
 | 
 | ||||||
|     constructor(params: ({ features: any[], layer: LayerConfig })) { |     constructor(params: ({ features: any[], layer: LayerConfig })) { | ||||||
| 
 | 
 | ||||||
|         const introduction = ValidatedTextField.InputForType("text", { |         const introduction = ValidatedTextField.ForType("text").ConstructInputElement({ | ||||||
|             value: LocalStorageSource.Get("import-helper-introduction-text"), |             value: LocalStorageSource.Get("import-helper-introduction-text"), | ||||||
|             inputStyle: "width: 100%" |             inputStyle: "width: 100%" | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const wikilink = ValidatedTextField.InputForType("string", { |         const wikilink = ValidatedTextField.ForType("string").ConstructInputElement({ | ||||||
|             value: LocalStorageSource.Get("import-helper-wikilink-text"), |             value: LocalStorageSource.Get("import-helper-wikilink-text"), | ||||||
|             inputStyle: "width: 100%" |             inputStyle: "width: 100%" | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const source = ValidatedTextField.InputForType("string", { |         const source = ValidatedTextField.ForType("string").ConstructInputElement({ | ||||||
|             value: LocalStorageSource.Get("import-helper-source-text"), |             value: LocalStorageSource.Get("import-helper-source-text"), | ||||||
|             inputStyle: "width: 100%" |             inputStyle: "width: 100%" | ||||||
|         }) |         }) | ||||||
|  | @ -59,7 +59,7 @@ export class AskMetadata extends Combine implements FlowStep<{ | ||||||
| 
 | 
 | ||||||
|         const theme = new DropDown("Which theme should be linked in the note?", options) |         const theme = new DropDown("Which theme should be linked in the note?", options) | ||||||
| 
 | 
 | ||||||
|         ValidatedTextField.InputForType("string", { |         ValidatedTextField.ForType("string").ConstructInputElement({ | ||||||
|             value: LocalStorageSource.Get("import-helper-theme-text"), |             value: LocalStorageSource.Get("import-helper-theme-text"), | ||||||
|             inputStyle: "width: 100%" |             inputStyle: "width: 100%" | ||||||
|         }) |         }) | ||||||
|  |  | ||||||
|  | @ -92,7 +92,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | ||||||
|         const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto) |         const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto) | ||||||
|         const location = new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1}) |         const location = new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1}) | ||||||
|         const currentBounds = new UIEventSource<BBox>(undefined) |         const currentBounds = new UIEventSource<BBox>(undefined) | ||||||
|         const zoomLevel = ValidatedTextField.InputForType("pnat") |         const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement() | ||||||
|         zoomLevel.SetClass("ml-1 border border-black") |         zoomLevel.SetClass("ml-1 border border-black") | ||||||
|         zoomLevel.GetValue().syncWith(LocalStorageSource.Get("importer-zoom-level", "14"), true) |         zoomLevel.GetValue().syncWith(LocalStorageSource.Get("importer-zoom-level", "14"), true) | ||||||
|         const osmLiveData = Minimap.createMiniMap({ |         const osmLiveData = Minimap.createMiniMap({ | ||||||
|  | @ -146,7 +146,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | ||||||
|             features: new StaticFeatureSource(toImport.features, false) |             features: new StaticFeatureSource(toImport.features, false) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const nearbyCutoff = ValidatedTextField.InputForType("pnat") |         const nearbyCutoff = ValidatedTextField.ForType("pnat").ConstructInputElement() | ||||||
|         nearbyCutoff.SetClass("ml-1 border border-black") |         nearbyCutoff.SetClass("ml-1 border border-black") | ||||||
|         nearbyCutoff.GetValue().syncWith(LocalStorageSource.Get("importer-cutoff", "25"), true) |         nearbyCutoff.GetValue().syncWith(LocalStorageSource.Get("importer-cutoff", "25"), true) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -47,7 +47,7 @@ interface NoteState { | ||||||
| 
 | 
 | ||||||
| class MassAction extends Combine { | class MassAction extends Combine { | ||||||
|     constructor(state: UserRelatedState, props: NoteProperties[]) { |     constructor(state: UserRelatedState, props: NoteProperties[]) { | ||||||
|         const textField = ValidatedTextField.InputForType("text") |         const textField = ValidatedTextField.ForType("text").ConstructInputElement() | ||||||
| 
 | 
 | ||||||
|         const actions = new DropDown<{ |         const actions = new DropDown<{ | ||||||
|             predicate: (p: NoteProperties) => boolean, |             predicate: (p: NoteProperties) => boolean, | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ export class TextField extends InputElement<string> { | ||||||
|     private readonly value: UIEventSource<string>; |     private readonly value: UIEventSource<string>; | ||||||
|     private _element: HTMLElement; |     private _element: HTMLElement; | ||||||
|     private readonly _isValid: (s: string, country?: () => string) => boolean; |     private readonly _isValid: (s: string, country?: () => string) => boolean; | ||||||
|  |     private _rawValue: UIEventSource<string>  | ||||||
|      |      | ||||||
|     constructor(options?: { |     constructor(options?: { | ||||||
|         placeholder?: string | BaseUIElement, |         placeholder?: string | BaseUIElement, | ||||||
|  | @ -23,6 +24,7 @@ export class TextField extends InputElement<string> { | ||||||
|         const self = this; |         const self = this; | ||||||
|         options = options ?? {}; |         options = options ?? {}; | ||||||
|         this.value = options?.value ?? new UIEventSource<string>(undefined); |         this.value = options?.value ?? new UIEventSource<string>(undefined); | ||||||
|  |         this._rawValue = new UIEventSource<string>("") | ||||||
|         this._isValid = options.isValid ?? (_ => true); |         this._isValid = options.isValid ?? (_ => true); | ||||||
| 
 | 
 | ||||||
|         const placeholder = Translations.W(options.placeholder ?? "").ConstructElement().innerText.replace("'", "'"); |         const placeholder = Translations.W(options.placeholder ?? "").ConstructElement().innerText.replace("'", "'"); | ||||||
|  | @ -77,6 +79,7 @@ export class TextField extends InputElement<string> { | ||||||
|             const endDistance = field.value.substring(field.selectionEnd).replace(/ /g, '').length; |             const endDistance = field.value.substring(field.selectionEnd).replace(/ /g, '').length; | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|             let val: string = field.value; |             let val: string = field.value; | ||||||
|  |             self._rawValue.setData(val) | ||||||
|             if (!self.IsValid(val)) { |             if (!self.IsValid(val)) { | ||||||
|                 self.value.setData(undefined); |                 self.value.setData(undefined); | ||||||
|             } else { |             } else { | ||||||
|  | @ -129,6 +132,10 @@ export class TextField extends InputElement<string> { | ||||||
|         return this.value; |         return this.value; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     GetRawValue(): UIEventSource<string>{ | ||||||
|  |         return this._rawValue | ||||||
|  |     } | ||||||
|  |      | ||||||
|     IsValid(t: string): boolean { |     IsValid(t: string): boolean { | ||||||
|         if (t === undefined || t === null) { |         if (t === undefined || t === null) { | ||||||
|             return false |             return false | ||||||
|  |  | ||||||
|  | @ -25,8 +25,9 @@ import Title from "../Base/Title"; | ||||||
| import InputElementMap from "./InputElementMap"; | import InputElementMap from "./InputElementMap"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import {Translation} from "../i18n/Translation"; | import {Translation} from "../i18n/Translation"; | ||||||
|  | import {NOTFOUND} from "dns"; | ||||||
| 
 | 
 | ||||||
| class SimpleTextFieldDef { | export class TextFieldDef { | ||||||
| 
 | 
 | ||||||
|     public readonly name: string; |     public readonly name: string; | ||||||
|     /* |     /* | ||||||
|  | @ -34,10 +35,11 @@ class SimpleTextFieldDef { | ||||||
|     * This can indicate which special input element is used, ... |     * This can indicate which special input element is used, ... | ||||||
|     * */ |     * */ | ||||||
|     public readonly explanation: string; |     public readonly explanation: string; | ||||||
|     public inputmode?: string = undefined |     protected inputmode?: string = undefined | ||||||
| 
 | 
 | ||||||
|     constructor(explanation: string | BaseUIElement, name?: string) { |     constructor(name: string, | ||||||
|         this.name = name ?? this.constructor.name.toLowerCase(); |                 explanation: string | BaseUIElement) { | ||||||
|  |         this.name = name; | ||||||
|         if (this.name.endsWith("textfield")) { |         if (this.name.endsWith("textfield")) { | ||||||
|             this.name = this.name.substr(0, this.name.length - "TextField".length) |             this.name = this.name.substr(0, this.name.length - "TextField".length) | ||||||
|         } |         } | ||||||
|  | @ -52,22 +54,174 @@ class SimpleTextFieldDef { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public reformat(s: string, country?: () => string): string { |     protectedisValid(s: string, _: (() => string) | undefined): boolean { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public getFeedback(s: string): Translation { | ||||||
|  |         const tr = Translations.t.validation[this.name] | ||||||
|  |         if(tr !== undefined){ | ||||||
|  |             return tr["feedback"] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public ConstructInputElement(options: { | ||||||
|  |         value?: UIEventSource<string>, | ||||||
|  |         inputStyle?: string, | ||||||
|  |         feedback?: UIEventSource<Translation> | ||||||
|  |         placeholder?: string | BaseUIElement, | ||||||
|  |         country?: () => string, | ||||||
|  |         location?: [number /*lat*/, number /*lon*/], | ||||||
|  |         mapBackgroundLayer?: UIEventSource<any>, | ||||||
|  |         unit?: Unit, | ||||||
|  |         args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
 | ||||||
|  |         feature?: any, | ||||||
|  |     } = {}): InputElement<string> { | ||||||
|  | 
 | ||||||
|  |         if (options.placeholder === undefined) { | ||||||
|  |             options.placeholder = Translations.t.validation[this.name]?.description ?? this.name | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         options["textArea"] = this.name === "text"; | ||||||
|  | 
 | ||||||
|  |         const self = this; | ||||||
|  | 
 | ||||||
|  |         if (options.unit !== undefined) { | ||||||
|  |             // Reformatting is handled by the unit in this case
 | ||||||
|  |             options["isValid"] = str => { | ||||||
|  |                 const denom = options.unit.findDenomination(str); | ||||||
|  |                 if (denom === undefined) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |                 const stripped = denom[0] | ||||||
|  |                 return self.isValid(stripped, options.country) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             options["isValid"] = self.isValid; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         options["inputMode"] = this.inputmode; | ||||||
|  |         if (this.inputmode === "text") { | ||||||
|  |             options["htmlType"] = "area" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const textfield = new TextField(options); | ||||||
|  |         let input: InputElement<string> = textfield | ||||||
|  |         if (options.feedback) { | ||||||
|  |             textfield.GetRawValue().addCallback(v => { | ||||||
|  |                 if(self.isValid(v, options.country)){ | ||||||
|  |                     options.feedback.setData(undefined) | ||||||
|  |                 }else{ | ||||||
|  |                 options.feedback.setData(self.getFeedback(v)) | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         if (this.reformat && options.unit === undefined) { | ||||||
|  |             input.GetValue().addCallbackAndRun(str => { | ||||||
|  |                 if (!options["isValid"](str, options.country)) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 const formatted = this.reformat(str, options.country); | ||||||
|  |                 input.GetValue().setData(formatted); | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (options.unit) { | ||||||
|  |             // We need to apply a unit.
 | ||||||
|  |             // This implies:
 | ||||||
|  |             // We have to create a dropdown with applicable denominations, and fuse those values
 | ||||||
|  |             const unit = options.unit | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             const isSingular = input.GetValue().map(str => str?.trim() === "1") | ||||||
|  | 
 | ||||||
|  |             const unitDropDown = | ||||||
|  |                 unit.denominations.length === 1 ? | ||||||
|  |                     new FixedInputElement(unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0]) | ||||||
|  |                     : new DropDown("", | ||||||
|  |                         unit.denominations.map(denom => { | ||||||
|  |                             return { | ||||||
|  |                                 shown: denom.getToggledHuman(isSingular), | ||||||
|  |                                 value: denom | ||||||
|  |                             } | ||||||
|  |                         }) | ||||||
|  |                     ) | ||||||
|  |             unitDropDown.GetValue().setData(unit.defaultDenom) | ||||||
|  |             unitDropDown.SetClass("w-min") | ||||||
|  | 
 | ||||||
|  |             const fixedDenom = unit.denominations.length === 1 ? unit.denominations[0] : undefined | ||||||
|  |             input = new CombinedInputElement( | ||||||
|  |                 input, | ||||||
|  |                 unitDropDown, | ||||||
|  |                 // combine the value from the textfield and the dropdown into the resulting value that should go into OSM
 | ||||||
|  |                 (text, denom) => { | ||||||
|  |                     if (denom === undefined) { | ||||||
|  |                         return text | ||||||
|  |                     } | ||||||
|  |                     return denom?.canonicalValue(text, true) | ||||||
|  |                 }, | ||||||
|  |                 (valueWithDenom: string) => { | ||||||
|  |                     // Take the value from OSM and feed it into the textfield and the dropdown
 | ||||||
|  |                     const withDenom = unit.findDenomination(valueWithDenom); | ||||||
|  |                     if (withDenom === undefined) { | ||||||
|  |                         // Not a valid value at all - we give it undefined and leave the details up to the other elements (but we keep the previous denomination)
 | ||||||
|  |                         return [undefined, fixedDenom] | ||||||
|  |                     } | ||||||
|  |                     const [strippedText, denom] = withDenom | ||||||
|  |                     if (strippedText === undefined) { | ||||||
|  |                         return [undefined, fixedDenom] | ||||||
|  |                     } | ||||||
|  |                     return [strippedText, denom] | ||||||
|  |                 } | ||||||
|  |             ).SetClass("flex") | ||||||
|  |         } | ||||||
|  |         const helper = this.inputHelper(input.GetValue(), { | ||||||
|  |             location: options.location, | ||||||
|  |             mapBackgroundLayer: options.mapBackgroundLayer, | ||||||
|  |             args: options.args, | ||||||
|  |             feature: options.feature | ||||||
|  |         })?.SetClass("block") | ||||||
|  |         if (helper !== undefined) { | ||||||
|  |             input = new CombinedInputElement(input, helper, | ||||||
|  |                 (a, _) => a, // We can ignore b, as they are linked earlier
 | ||||||
|  |                 a => [a, a] | ||||||
|  |             ).SetClass("block w-full"); | ||||||
|  |         } | ||||||
|  |         if (this.postprocess !== undefined) { | ||||||
|  |             input = new InputElementMap<string, string>(input, | ||||||
|  |                 (a, b) => a === b, | ||||||
|  |                 this.postprocess, | ||||||
|  |                 this.undoPostprocess | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return input; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected isValid(string: string, requestCountry: () => string): boolean { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected reformat(s: string, country?: () => string): string { | ||||||
|         return s; |         return s; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Modification to make before the string is uploaded to OSM |      * Modification to make before the string is uploaded to OSM | ||||||
|      */ |      */ | ||||||
|     public postprocess(s: string): string { |     protected postprocess(s: string): string { | ||||||
|         return s |         return s | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public undoPostprocess(s: string): string { |     protected undoPostprocess(s: string): string { | ||||||
|         return s; |         return s; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public inputHelper(value: UIEventSource<string>, options?: { |     protected inputHelper(value: UIEventSource<string>, options?: { | ||||||
|         location: [number, number], |         location: [number, number], | ||||||
|         mapBackgroundLayer?: UIEventSource<any>, |         mapBackgroundLayer?: UIEventSource<any>, | ||||||
|         args: (string | number | boolean | any)[] |         args: (string | number | boolean | any)[] | ||||||
|  | @ -76,36 +230,30 @@ class SimpleTextFieldDef { | ||||||
|         return undefined |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isValid(s: string, country: (() => string) | undefined): boolean { |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     getFeedback(s: string) : Translation { |  | ||||||
|         return undefined |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class WikidataTextField extends SimpleTextFieldDef { | class WikidataTextField extends TextFieldDef { | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(new Combine([ |         super( | ||||||
|             "A wikidata identifier, e.g. Q42.", |             "wikidata", | ||||||
|             new Title("Helper arguments"), |             new Combine([ | ||||||
|             new Table(["name", "doc"], |                 "A wikidata identifier, e.g. Q42.", | ||||||
|                 [ |                 new Title("Helper arguments"), | ||||||
|                     ["key", "the value of this tag will initialize search (default: name)"], |                 new Table(["name", "doc"], | ||||||
|                     ["options", new Combine(["A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.", |                     [ | ||||||
|                         new Table( |                         ["key", "the value of this tag will initialize search (default: name)"], | ||||||
|                             ["subarg", "doc"], |                         ["options", new Combine(["A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.", | ||||||
|                             [["removePrefixes", "remove these snippets of text from the start of the passed string to search"], |                             new Table( | ||||||
|                                 ["removePostfixes", "remove these snippets of text from the end of the passed string to search"], |                                 ["subarg", "doc"], | ||||||
|                             ] |                                 [["removePrefixes", "remove these snippets of text from the start of the passed string to search"], | ||||||
|                         )]) |                                     ["removePostfixes", "remove these snippets of text from the end of the passed string to search"], | ||||||
|                     ]]), |                                 ] | ||||||
|             new Title("Example usage"), |                             )]) | ||||||
|             `The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
 |                         ]]), | ||||||
|  |                 new Title("Example usage"), | ||||||
|  |                 `The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
 | ||||||
| 
 | 
 | ||||||
| \`\`\` | \`\`\` | ||||||
| "freeform": { | "freeform": { | ||||||
|  | @ -125,7 +273,7 @@ class WikidataTextField extends SimpleTextFieldDef { | ||||||
|     ] |     ] | ||||||
| } | } | ||||||
| \`\`\`` | \`\`\`` | ||||||
|         ])); |             ])); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -184,10 +332,12 @@ class WikidataTextField extends SimpleTextFieldDef { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class OpeningHoursTextField extends SimpleTextFieldDef { | class OpeningHoursTextField extends TextFieldDef { | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(new Combine([ |         super( | ||||||
|  |             "opening_hours", | ||||||
|  |             new Combine([ | ||||||
|                 "Has extra elements to easily input when a POI is opened.", |                 "Has extra elements to easily input when a POI is opened.", | ||||||
|                 new Title("Helper arguments"), |                 new Title("Helper arguments"), | ||||||
|                 new Table(["name", "doc"], |                 new Table(["name", "doc"], | ||||||
|  | @ -214,8 +364,7 @@ class OpeningHoursTextField extends SimpleTextFieldDef { | ||||||
|           "postfix":")" |           "postfix":")" | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
| }` + "\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`"]), | }` + "\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`"]),); | ||||||
|             "opening_hours"); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isValid() { |     isValid() { | ||||||
|  | @ -240,12 +389,12 @@ class OpeningHoursTextField extends SimpleTextFieldDef { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class UrlTextfieldDef extends SimpleTextFieldDef { | class UrlTextfieldDef extends TextFieldDef { | ||||||
| 
 | 
 | ||||||
|     inputmode: "url" |     inputmode: "url" | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super("The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user") |         super("url", "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     postprocess(str: string) { |     postprocess(str: string) { | ||||||
|  | @ -320,23 +469,23 @@ class UrlTextfieldDef extends SimpleTextFieldDef { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class StringTextField extends SimpleTextFieldDef { | class StringTextField extends TextFieldDef { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super("A simple piece of text"); |         super("string", "A simple piece of text"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class TextTextField extends SimpleTextFieldDef { | class TextTextField extends TextFieldDef { | ||||||
|     inputmode: "text" |     inputmode: "text" | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super("A longer piece of text"); |         super("text", "A longer piece of text"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class DateTextField extends SimpleTextFieldDef { | class DateTextField extends TextFieldDef { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super("A date with date picker"); |         super("date", "A date with date picker"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isValid = (str) => { |     isValid = (str) => { | ||||||
|  | @ -362,21 +511,280 @@ class DateTextField extends SimpleTextFieldDef { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class DirectionTextField extends SimpleTextFieldDef { | 
 | ||||||
|     inputMode = "numeric" | class LengthTextField extends TextFieldDef { | ||||||
|  |     inputMode: "decimal" | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super("A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)"); |         super( | ||||||
|  |             "decimal", "A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `[\"21\", \"map,photo\"]" | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isValid = (str) => { |     isValid = (str) => { | ||||||
|         str = "" + str; |         const t = Number(str) | ||||||
|         return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 |         return !isNaN(t) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     inputHelper = (value, options) => { |     inputHelper = (value, options) => { | ||||||
|  |         options = options ?? {} | ||||||
|  |         options.location = options.location ?? [0, 0] | ||||||
|  | 
 | ||||||
|         const args = options.args ?? [] |         const args = options.args ?? [] | ||||||
|         let zoom = 19 |         let zoom = 19 | ||||||
|  |         if (args[0]) { | ||||||
|  |             zoom = Number(args[0]) | ||||||
|  |             if (isNaN(zoom)) { | ||||||
|  |                 console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ", args[0], " (using 19 instead)") | ||||||
|  |                 zoom = 19 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Bit of a hack: we project the centerpoint to the closes point on the road - if available
 | ||||||
|  |         if (options?.feature !== undefined && options.feature.geometry.type !== "Point") { | ||||||
|  |             const lonlat = <[number, number]>[...options.location] | ||||||
|  |             lonlat.reverse() | ||||||
|  |             options.location = <[number, number]>GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates | ||||||
|  |             options.location.reverse() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const location = new UIEventSource<Loc>({ | ||||||
|  |             lat: options.location[0], | ||||||
|  |             lon: options.location[1], | ||||||
|  |             zoom: zoom | ||||||
|  |         }) | ||||||
|  |         if (args[1]) { | ||||||
|  |             // We have a prefered map!
 | ||||||
|  |             options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( | ||||||
|  |                 location, new UIEventSource<string[]>(args[1].split(",")) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         const li = new LengthInput(options?.mapBackgroundLayer, location, value) | ||||||
|  |         li.SetStyle("height: 20rem;") | ||||||
|  |         return li; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class FloatTextField extends TextFieldDef { | ||||||
|  |     inputmode = "decimal" | ||||||
|  | 
 | ||||||
|  |     constructor(name?: string, explanation?: string) { | ||||||
|  |         super(name ?? "float", explanation ?? "A decimal"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     isValid(str) { | ||||||
|  |         return !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     reformat( str): string { | ||||||
|  |         return "" + Number(str); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getFeedback(s: string): Translation { | ||||||
|  |         if (isNaN(Number(s))) { | ||||||
|  |             return Translations.t.validation.nat.notANumber | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return undefined | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class IntTextField extends FloatTextField { | ||||||
|  |     inputMode = "numeric" | ||||||
|  | 
 | ||||||
|  |     constructor(name?: string, explanation?: string) { | ||||||
|  |         super(name ?? "int", explanation ?? "A number"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     isValid(str): boolean { | ||||||
|  |         str = "" + str; | ||||||
|  |         return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getFeedback(s: string): Translation { | ||||||
|  |         const n = Number(s) | ||||||
|  |         if (isNaN(n)) { | ||||||
|  |             return Translations.t.validation.nat.notANumber | ||||||
|  |         } | ||||||
|  |         if (Math.floor(n) !== n) { | ||||||
|  |             return Translations.t.validation.nat.mustBeWhole | ||||||
|  |         } | ||||||
|  |         return undefined | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class NatTextField extends IntTextField { | ||||||
|  |     inputMode = "numeric" | ||||||
|  | 
 | ||||||
|  |     constructor(name?: string, explanation?: string) { | ||||||
|  |         super(name ?? "nat", explanation ?? "A positive number or zero"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     isValid(str): boolean { | ||||||
|  |         if (str === undefined) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         str = "" + str; | ||||||
|  | 
 | ||||||
|  |         return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     getFeedback(s: string): Translation { | ||||||
|  |         const spr = super.getFeedback(s) | ||||||
|  |         if (spr !== undefined) { | ||||||
|  |             return spr | ||||||
|  |         } | ||||||
|  |         const n = Number(s) | ||||||
|  |         if (n < 0) { | ||||||
|  |             return Translations.t.validation.nat.mustBePositive | ||||||
|  |         } | ||||||
|  |         return undefined | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class PNatTextField extends NatTextField { | ||||||
|  |     inputmode = "numeric" | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         super("pnat", "A strict positive number"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getFeedback(s: string): Translation { | ||||||
|  |         const spr = super.getFeedback(s); | ||||||
|  |         if (spr !== undefined) { | ||||||
|  |             return spr | ||||||
|  |         } | ||||||
|  |         if (Number(s) === 0) { | ||||||
|  |             return Translations.t.validation.pnat.noZero | ||||||
|  |         } | ||||||
|  |         return undefined | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     isValid = (str) => { | ||||||
|  |         if (!super.isValid(str)) { | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         return Number(str) > 0 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class PFloatTextField extends FloatTextField { | ||||||
|  |     inputmode = "decimal" | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         super("pfloat", "A positive decimal (inclusive zero)"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     isValid = (str) => !isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",") | ||||||
|  | 
 | ||||||
|  |     getFeedback(s: string): Translation { | ||||||
|  |         const spr = super.getFeedback(s); | ||||||
|  |         if (spr !== undefined) { | ||||||
|  |             return spr | ||||||
|  |         } | ||||||
|  |         if (Number(s) < 0) { | ||||||
|  |             return Translations.t.validation.nat.mustBePositive | ||||||
|  |         } | ||||||
|  |         return undefined; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class EmailTextField extends TextFieldDef { | ||||||
|  |     inputmode = "email" | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         super("email", "An email adress"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     isValid = (str) => { | ||||||
|  |         if (str === undefined) { | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         if (str.startsWith("mailto:")) { | ||||||
|  |             str = str.substring("mailto:".length) | ||||||
|  |         } | ||||||
|  |         return EmailValidator.validate(str); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     reformat = str => { | ||||||
|  |         if (str === undefined) { | ||||||
|  |             return undefined | ||||||
|  |         } | ||||||
|  |         if (str.startsWith("mailto:")) { | ||||||
|  |             str = str.substring("mailto:".length) | ||||||
|  |         } | ||||||
|  |         return str; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     getFeedback(s: string): Translation { | ||||||
|  |         if(s.indexOf('@') < 0){return Translations.t.validation.email.noAt} | ||||||
|  |          | ||||||
|  |         return super.getFeedback(s); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class PhoneTextField extends TextFieldDef { | ||||||
|  |     inputmode = "tel" | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         super("phone", "A phone number"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     isValid(str, country: () => string): boolean { | ||||||
|  |         if (str === undefined) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         if (str.startsWith("tel:")) { | ||||||
|  |             str = str.substring("tel:".length) | ||||||
|  |         } | ||||||
|  |         let countryCode = undefined | ||||||
|  |         if(country !== undefined){ | ||||||
|  |             countryCode = (country())?.toUpperCase() | ||||||
|  |         } | ||||||
|  |         return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     reformat = (str, country: () => string) => { | ||||||
|  |         if (str.startsWith("tel:")) { | ||||||
|  |             str = str.substring("tel:".length) | ||||||
|  |         } | ||||||
|  |         return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class ColorTextField extends TextFieldDef { | ||||||
|  |     constructor() { | ||||||
|  |         super("color", "Shows a color picker"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     inputHelper = (value) => { | ||||||
|  |         return new ColorPicker(value.map(color => { | ||||||
|  |             return Utils.ColourNameToHex(color ?? ""); | ||||||
|  |         }, [], str => Utils.HexToColourName(str))) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class DirectionTextField extends IntTextField { | ||||||
|  |     inputMode = "numeric" | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         super("direction", "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)"); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     reformat(str): string { | ||||||
|  |         const n = (Number(str) % 360)  | ||||||
|  |         return ""+n | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |    | ||||||
|  |     inputHelper = (value, options) => { | ||||||
|  |         const args = options.args ?? [] | ||||||
|  |         options.location = options.location ?? [0, 0] | ||||||
|  |         let zoom = 19 | ||||||
|         if (args[0]) { |         if (args[0]) { | ||||||
|             zoom = Number(args[0]) |             zoom = Number(args[0]) | ||||||
|             if (isNaN(zoom)) { |             if (isNaN(zoom)) { | ||||||
|  | @ -401,190 +809,10 @@ class DirectionTextField extends SimpleTextFieldDef { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class LengthTextField extends SimpleTextFieldDef { |  | ||||||
|     inputMode: "decimal" |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         super( |  | ||||||
|             "A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `[\"21\", \"map,photo\"]" |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     isValid = (str) => { |  | ||||||
|         const t = Number(str) |  | ||||||
|         return !isNaN(t) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     inputHelper = (value, options) => { |  | ||||||
|         const args = options.args ?? [] |  | ||||||
|         let zoom = 19 |  | ||||||
|         if (args[0]) { |  | ||||||
|             zoom = Number(args[0]) |  | ||||||
|             if (isNaN(zoom)) { |  | ||||||
|                 console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ", args[0], " (using 19 instead)") |  | ||||||
|                 zoom = 19 |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Bit of a hack: we project the centerpoint to the closes point on the road - if available
 |  | ||||||
|         if (options.feature !== undefined && options.feature.geometry.type !== "Point") { |  | ||||||
|             const lonlat = <[number, number]>[...options.location] |  | ||||||
|             lonlat.reverse() |  | ||||||
|             options.location = <[number, number]>GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates |  | ||||||
|             options.location.reverse() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const location = new UIEventSource<Loc>({ |  | ||||||
|             lat: options.location[0], |  | ||||||
|             lon: options.location[1], |  | ||||||
|             zoom: zoom |  | ||||||
|         }) |  | ||||||
|         if (args[1]) { |  | ||||||
|             // We have a prefered map!
 |  | ||||||
|             options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( |  | ||||||
|                 location, new UIEventSource<string[]>(args[1].split(",")) |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|         const li = new LengthInput(options.mapBackgroundLayer, location, value) |  | ||||||
|         li.SetStyle("height: 20rem;") |  | ||||||
|         return li; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class IntTextField extends SimpleTextFieldDef { |  | ||||||
|     inputMode = "numeric" |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         super("A number"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     isValid = (str) => { |  | ||||||
|         str = "" + str; |  | ||||||
|         return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     reformat = str => "" + Number(str) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class NatTextField extends SimpleTextFieldDef { |  | ||||||
|     inputMode = "numeric" |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         super("A positive number or zero"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     isValid = (str) => { |  | ||||||
|         str = "" + str; |  | ||||||
|         return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     reformat = str => "" + Number(str) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class PNatTextField extends SimpleTextFieldDef { |  | ||||||
|     inputmode = "numeric" |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         super("A strict positive number"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     isValid = (str) => { |  | ||||||
|         str = "" + str; |  | ||||||
|         return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     reformat = str => "" + Number(str) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class FloatTextField extends SimpleTextFieldDef { |  | ||||||
|     inputmode = "decimal" |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         super("A decimal"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     isValid = (str) => !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",") |  | ||||||
| 
 |  | ||||||
|     reformat = str => "" + Number(str) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class PFloatTextField extends SimpleTextFieldDef { |  | ||||||
|     inputmode = "decimal" |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         super("A positive decimal (inclusive zero)"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     isValid = (str) => !isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",") |  | ||||||
| 
 |  | ||||||
|     reformat = str => "" + Number(str) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class EmailTextField extends SimpleTextFieldDef { |  | ||||||
|     inputmode = "email" |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         super("An email adress"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     isValid = (str) => { |  | ||||||
|         if (str.startsWith("mailto:")) { |  | ||||||
|             str = str.substring("mailto:".length) |  | ||||||
|         } |  | ||||||
|         return EmailValidator.validate(str); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     reformat = str => { |  | ||||||
|         if (str === undefined) { |  | ||||||
|             return undefined |  | ||||||
|         } |  | ||||||
|         if (str.startsWith("mailto:")) { |  | ||||||
|             str = str.substring("mailto:".length) |  | ||||||
|         } |  | ||||||
|         return str; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class PhoneTextField extends SimpleTextFieldDef { |  | ||||||
|     inputmode = "tel" |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         super("A phone number"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     isValid = (str, country: () => string) => { |  | ||||||
|         if (str === undefined) { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|         if (str.startsWith("tel:")) { |  | ||||||
|             str = str.substring("tel:".length) |  | ||||||
|         } |  | ||||||
|         return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     reformat = (str, country: () => string) => { |  | ||||||
|         if (str.startsWith("tel:")) { |  | ||||||
|             str = str.substring("tel:".length) |  | ||||||
|         } |  | ||||||
|         return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class ColorTextField extends SimpleTextFieldDef { |  | ||||||
|     constructor() { |  | ||||||
|         super("Shows a color picker"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     inputHelper = (value) => { |  | ||||||
|         return new ColorPicker(value.map(color => { |  | ||||||
|             return Utils.ColourNameToHex(color ?? ""); |  | ||||||
|         }, [], str => Utils.HexToColourName(str))) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export default class ValidatedTextField { | export default class ValidatedTextField { | ||||||
| 
 | 
 | ||||||
|     private static allTextfieldDefs: SimpleTextFieldDef[] = [ |     private static AllTextfieldDefs: TextFieldDef[] = [ | ||||||
|         new StringTextField(), |         new StringTextField(), | ||||||
|         new TextTextField(), |         new TextTextField(), | ||||||
|         new DateTextField(), |         new DateTextField(), | ||||||
|  | @ -602,156 +830,14 @@ export default class ValidatedTextField { | ||||||
|         new OpeningHoursTextField(), |         new OpeningHoursTextField(), | ||||||
|         new ColorTextField() |         new ColorTextField() | ||||||
|     ] |     ] | ||||||
|     public static AllTypes: Map<string, SimpleTextFieldDef> = ValidatedTextField.allTypesDict(); |     public static allTypes: Map<string, TextFieldDef> = ValidatedTextField.allTypesDict(); | ||||||
| 
 |     public static ForType(type: string = "string"): TextFieldDef { | ||||||
|     public static InputForType(type: string, options?: { |         return ValidatedTextField.allTypes.get(type) | ||||||
|         placeholder?: string | BaseUIElement, |  | ||||||
|         value?: UIEventSource<string>, |  | ||||||
|         htmlType?: string, |  | ||||||
|         textArea?: boolean, |  | ||||||
|         inputMode?: string, |  | ||||||
|         textAreaRows?: number, |  | ||||||
|         isValid?: ((s: string, country: () => string) => boolean), |  | ||||||
|         country?: () => string, |  | ||||||
|         location?: [number /*lat*/, number /*lon*/], |  | ||||||
|         mapBackgroundLayer?: UIEventSource<any>, |  | ||||||
|         unit?: Unit, |  | ||||||
|         args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
 |  | ||||||
|         feature?: any, |  | ||||||
|         inputStyle?: string |  | ||||||
|     }): InputElement<string> { |  | ||||||
|         options = options ?? {}; |  | ||||||
|         if (options.placeholder === undefined) { |  | ||||||
|             options.placeholder = Translations.t.validation[type]?.description ?? type |  | ||||||
|         } |  | ||||||
|         const tp: SimpleTextFieldDef = ValidatedTextField.AllTypes.get(type) |  | ||||||
|         const isValidTp = tp.isValid; |  | ||||||
|         let isValid; |  | ||||||
|         options.textArea = options.textArea ?? type === "text"; |  | ||||||
|         if (options.isValid) { |  | ||||||
|             const optValid = options.isValid; |  | ||||||
|             isValid = (str, country) => { |  | ||||||
|                 if (str === undefined) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|                 if (options.unit) { |  | ||||||
|                     str = options.unit.stripUnitParts(str) |  | ||||||
|                 } |  | ||||||
|                 return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             isValid = isValidTp; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (options.unit !== undefined && isValid !== undefined) { |  | ||||||
|             // Reformatting is handled by the unit in this case
 |  | ||||||
|             options.isValid = str => { |  | ||||||
|                 const denom = options.unit.findDenomination(str); |  | ||||||
|                 if (denom === undefined) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|                 const stripped = denom[0] |  | ||||||
|                 console.log("Is valid? ", str, "stripped: ", stripped, "isValid:", isValid(stripped)) |  | ||||||
|                 return isValid(stripped) |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             options.isValid = isValid; |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         options.inputMode = tp.inputmode; |  | ||||||
|         if (tp.inputmode === "text") { |  | ||||||
|             options.htmlType = "area" |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         let input: InputElement<string> = new TextField(options); |  | ||||||
|         if (tp.reformat && options.unit === undefined) { |  | ||||||
|             input.GetValue().addCallbackAndRun(str => { |  | ||||||
|                 if (!options.isValid(str, options.country)) { |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|                 const formatted = tp.reformat(str, options.country); |  | ||||||
|                 input.GetValue().setData(formatted); |  | ||||||
|             }) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (options.unit) { |  | ||||||
|             // We need to apply a unit.
 |  | ||||||
|             // This implies:
 |  | ||||||
|             // We have to create a dropdown with applicable denominations, and fuse those values
 |  | ||||||
|             const unit = options.unit |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             const isSingular = input.GetValue().map(str => str?.trim() === "1") |  | ||||||
| 
 |  | ||||||
|             const unitDropDown = |  | ||||||
|                 unit.denominations.length === 1 ? |  | ||||||
|                     new FixedInputElement(unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0]) |  | ||||||
|                     : new DropDown("", |  | ||||||
|                         unit.denominations.map(denom => { |  | ||||||
|                             return { |  | ||||||
|                                 shown: denom.getToggledHuman(isSingular), |  | ||||||
|                                 value: denom |  | ||||||
|                             } |  | ||||||
|                         }) |  | ||||||
|                     ) |  | ||||||
|             unitDropDown.GetValue().setData(unit.defaultDenom) |  | ||||||
|             unitDropDown.SetClass("w-min") |  | ||||||
| 
 |  | ||||||
|             const fixedDenom = unit.denominations.length === 1 ? unit.denominations[0] : undefined |  | ||||||
|             input = new CombinedInputElement( |  | ||||||
|                 input, |  | ||||||
|                 unitDropDown, |  | ||||||
|                 // combine the value from the textfield and the dropdown into the resulting value that should go into OSM
 |  | ||||||
|                 (text, denom) => { |  | ||||||
|                     if (denom === undefined) { |  | ||||||
|                         return text |  | ||||||
|                     } |  | ||||||
|                     return denom?.canonicalValue(text, true) |  | ||||||
|                 }, |  | ||||||
|                 (valueWithDenom: string) => { |  | ||||||
|                     // Take the value from OSM and feed it into the textfield and the dropdown
 |  | ||||||
|                     const withDenom = unit.findDenomination(valueWithDenom); |  | ||||||
|                     if (withDenom === undefined) { |  | ||||||
|                         // Not a valid value at all - we give it undefined and leave the details up to the other elements (but we keep the previous denomination)
 |  | ||||||
|                         return [undefined, fixedDenom] |  | ||||||
|                     } |  | ||||||
|                     const [strippedText, denom] = withDenom |  | ||||||
|                     if (strippedText === undefined) { |  | ||||||
|                         return [undefined, fixedDenom] |  | ||||||
|                     } |  | ||||||
|                     return [strippedText, denom] |  | ||||||
|                 } |  | ||||||
|             ).SetClass("flex") |  | ||||||
|         } |  | ||||||
|         const helper = tp.inputHelper(input.GetValue(), { |  | ||||||
|             location: options.location, |  | ||||||
|             mapBackgroundLayer: options.mapBackgroundLayer, |  | ||||||
|             args: options.args, |  | ||||||
|             feature: options.feature |  | ||||||
|         })?.SetClass("block") |  | ||||||
|         if (helper !== undefined) { |  | ||||||
|             input = new CombinedInputElement(input, helper, |  | ||||||
|                 (a, _) => a, // We can ignore b, as they are linked earlier
 |  | ||||||
|                 a => [a, a] |  | ||||||
|             ).SetClass("block w-full"); |  | ||||||
|         } |  | ||||||
|         if (tp.postprocess !== undefined) { |  | ||||||
|             input = new InputElementMap<string, string>(input, |  | ||||||
|                 (a, b) => a === b, |  | ||||||
|                 tp.postprocess, |  | ||||||
|                 tp.undoPostprocess |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return input; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static HelpText(): BaseUIElement { |     public static HelpText(): BaseUIElement { | ||||||
|         const explanations: BaseUIElement[] = |         const explanations: BaseUIElement[] = | ||||||
|             ValidatedTextField.allTextfieldDefs.map(type => |             ValidatedTextField.AllTextfieldDefs.map(type => | ||||||
|                 new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")) |                 new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")) | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             new Title("Available types for text fields", 1), |             new Title("Available types for text fields", 1), | ||||||
|  | @ -761,12 +847,12 @@ export default class ValidatedTextField { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static AvailableTypes(): string[] { |     public static AvailableTypes(): string[] { | ||||||
|         return ValidatedTextField.allTextfieldDefs.map(tp => tp.name) |         return ValidatedTextField.AllTextfieldDefs.map(tp => tp.name) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static allTypesDict(): Map<string, SimpleTextFieldDef> { |     private static allTypesDict(): Map<string, TextFieldDef> { | ||||||
|         const types = new Map<string, SimpleTextFieldDef>(); |         const types = new Map<string, TextFieldDef>(); | ||||||
|         for (const tp of ValidatedTextField.allTextfieldDefs) { |         for (const tp of ValidatedTextField.AllTextfieldDefs) { | ||||||
|             types[tp.name] = tp; |             types[tp.name] = tp; | ||||||
|             types.set(tp.name, tp); |             types.set(tp.name, tp); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ export default class NewNoteUi extends Toggle { | ||||||
|         const t = Translations.t.notes; |         const t = Translations.t.notes; | ||||||
|         const isCreated = new UIEventSource(false); |         const isCreated = new UIEventSource(false); | ||||||
|         state.LastClickLocation.addCallbackAndRun(_ => isCreated.setData(false)) // Reset 'isCreated' on every click
 |         state.LastClickLocation.addCallbackAndRun(_ => isCreated.setData(false)) // Reset 'isCreated' on every click
 | ||||||
|         const text = ValidatedTextField.InputForType("text", { |         const text = ValidatedTextField.ForType("text").ConstructInputElement({ | ||||||
|             value: LocalStorageSource.Get("note-text") |             value: LocalStorageSource.Get("note-text") | ||||||
|         }) |         }) | ||||||
|         text.SetClass("border rounded-sm border-grey-500") |         text.SetClass("border rounded-sm border-grey-500") | ||||||
|  |  | ||||||
|  | @ -78,12 +78,14 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|             .SetClass("question-text"), 3); |             .SetClass("question-text"), 3); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |         const feedback = new UIEventSource<Translation>(undefined) | ||||||
|         const inputElement: InputElement<TagsFilter> = |         const inputElement: InputElement<TagsFilter> = | ||||||
|             new VariableInputElement(applicableMappingsSrc.map(applicableMappings => |             new VariableInputElement(applicableMappingsSrc.map(applicableMappings => | ||||||
|                 TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags) |                 TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback) | ||||||
|             )) |             )) | ||||||
|       |       | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         const save = () => { |         const save = () => { | ||||||
|             const selection = inputElement.GetValue().data; |             const selection = inputElement.GetValue().data; | ||||||
|             if (selection) { |             if (selection) { | ||||||
|  | @ -138,11 +140,17 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|         super([ |         super([ | ||||||
|             question, |             question, | ||||||
|             inputElement, |             inputElement, | ||||||
|             new Combine([options.cancelButton, |             new Combine([ | ||||||
|             saveButton]).SetClass("flex w-full justify-end flex-wrap-reverse"), |                 new VariableUiElement(feedback.map(t => t?.SetClass("alert") ?? "")).SetClass("grid justify-items-center"), | ||||||
|  |                 new Combine([ | ||||||
|  |                     new Combine([options.cancelButton]), | ||||||
|  |                     saveButton]).SetClass("flex justify-end flex-wrap-reverse") | ||||||
|  | 
 | ||||||
|  |             ]).SetClass("flex mt-2 justify-between") | ||||||
|  |             , | ||||||
|             bottomTags, |             bottomTags, | ||||||
|             new Toggle(Translations.t.general.testing.SetClass("alert"), undefined, state.featureSwitchIsTesting) |             new Toggle(Translations.t.general.testing.SetClass("alert"), undefined, state.featureSwitchIsTesting) | ||||||
|             ]) |         ]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         this.SetClass("question disable-links") |         this.SetClass("question disable-links") | ||||||
|  | @ -154,11 +162,12 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|         configuration: TagRenderingConfig, |         configuration: TagRenderingConfig, | ||||||
|         applicableMappings: { if: TagsFilter, then: any, ifnot?: TagsFilter, addExtraTags: Tag[] }[], |         applicableMappings: { if: TagsFilter, then: any, ifnot?: TagsFilter, addExtraTags: Tag[] }[], | ||||||
|         applicableUnit: Unit, |         applicableUnit: Unit, | ||||||
|         tagsSource: UIEventSource<any>) |         tagsSource: UIEventSource<any>, | ||||||
|         : InputElement<TagsFilter> { |         feedback: UIEventSource<Translation> | ||||||
|  |     ): InputElement<TagsFilter> { | ||||||
| 
 | 
 | ||||||
|         // FreeForm input will be undefined if not present; will already contain a special input element if applicable
 |         // FreeForm input will be undefined if not present; will already contain a special input element if applicable
 | ||||||
|         const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource); |         const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback); | ||||||
| 
 | 
 | ||||||
|         |         | ||||||
|         const hasImages = applicableMappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 |         const hasImages = applicableMappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 | ||||||
|  | @ -361,23 +370,24 @@ export default class TagRenderingQuestion extends Combine { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         return new FixedInputElement( |         return new FixedInputElement( | ||||||
|             TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state)  , |             TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state), | ||||||
|             tagging, |             tagging, | ||||||
|             (t0, t1) => t1.isEquivalent(t0)); |             (t0, t1) => t1.isEquivalent(t0)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GenerateMappingContent(  mapping: { |     private static GenerateMappingContent(mapping: { | ||||||
|         then: Translation, |         then: Translation, | ||||||
|         icon?: string |         icon?: string | ||||||
|     }, tagsSource: UIEventSource<any>, state: FeaturePipelineState): BaseUIElement{ |     }, tagsSource: UIEventSource<any>, state: FeaturePipelineState): BaseUIElement { | ||||||
|         const text =  new SubstitutedTranslation(mapping.then, tagsSource, state) |         const text = new SubstitutedTranslation(mapping.then, tagsSource, state) | ||||||
|         if (mapping.icon === undefined) { |         if (mapping.icon === undefined) { | ||||||
|             return text; |             return text; | ||||||
|         } |         } | ||||||
|         return new Combine([new Img(mapping.icon).SetClass("w-6 max-h-6 pr-2"), text]).SetClass("flex") |         return new Combine([new Img(mapping.icon).SetClass("w-6 max-h-6 pr-2"), text]).SetClass("flex") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GenerateFreeform(state, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>): InputElement<TagsFilter> { |     private static GenerateFreeform(state, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>, feedback: UIEventSource<Translation>) | ||||||
|  |         : InputElement<TagsFilter> { | ||||||
|         const freeform = configuration.freeform; |         const freeform = configuration.freeform; | ||||||
|         if (freeform === undefined) { |         if (freeform === undefined) { | ||||||
|             return undefined; |             return undefined; | ||||||
|  | @ -388,6 +398,9 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|                 if (string === "" || string === undefined) { |                 if (string === "" || string === undefined) { | ||||||
|                     return undefined; |                     return undefined; | ||||||
|                 } |                 } | ||||||
|  |                 if (string.length >= 255) { | ||||||
|  |                     return undefined | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 const tag = new Tag(freeform.key, string); |                 const tag = new Tag(freeform.key, string); | ||||||
| 
 | 
 | ||||||
|  | @ -418,19 +431,25 @@ export default class TagRenderingQuestion extends Combine { | ||||||
| 
 | 
 | ||||||
|         const tagsData = tags.data; |         const tagsData = tags.data; | ||||||
|         const feature = state.allElements.ContainingFeatures.get(tagsData.id) |         const feature = state.allElements.ContainingFeatures.get(tagsData.id) | ||||||
|         const input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, { |         const input: InputElement<string> = ValidatedTextField.ForType(configuration.freeform.type).ConstructInputElement({ | ||||||
|             isValid: (str) => (str.length <= 255), |  | ||||||
|             country: () => tagsData._country, |             country: () => tagsData._country, | ||||||
|             location: [tagsData._lat, tagsData._lon], |             location: [tagsData._lat, tagsData._lon], | ||||||
|             mapBackgroundLayer: state.backgroundLayer, |             mapBackgroundLayer: state.backgroundLayer, | ||||||
|             unit: applicableUnit, |             unit: applicableUnit, | ||||||
|             args: configuration.freeform.helperArgs, |             args: configuration.freeform.helperArgs, | ||||||
|             feature: feature, |             feature, | ||||||
|             placeholder: configuration.freeform.placeholder |             placeholder: configuration.freeform.placeholder, | ||||||
|  |             feedback | ||||||
|         }); |         }); | ||||||
|          |          | ||||||
|         input.GetValue().setData(tagsData[freeform.key] ?? freeform.default); |         input.GetValue().setData(tagsData[freeform.key] ?? freeform.default); | ||||||
|          |          | ||||||
|  |         input.GetValue().addCallbackD(v => { | ||||||
|  |             if(v.length >= 255){ | ||||||
|  |                 feedback.setData(Translations.t.validation.tooLong.Subs({count: v.length})) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|         let inputTagsFilter: InputElement<TagsFilter> = new InputElementMap( |         let inputTagsFilter: InputElement<TagsFilter> = new InputElementMap( | ||||||
|             input, (a, b) => a === b || (a?.isEquivalent(b) ?? false), |             input, (a, b) => a === b || (a?.isEquivalent(b) ?? false), | ||||||
|             pickString, toString |             pickString, toString | ||||||
|  |  | ||||||
|  | @ -721,7 +721,7 @@ export default class SpecialVisualizations { | ||||||
|                     constr: (state, tags, args) => { |                     constr: (state, tags, args) => { | ||||||
| 
 | 
 | ||||||
|                         const t = Translations.t.notes; |                         const t = Translations.t.notes; | ||||||
|                         const textField = ValidatedTextField.InputForType("text", {placeholder: t.addCommentPlaceholder}) |                         const textField = ValidatedTextField.ForType("text").ConstructInputElement({placeholder: t.addCommentPlaceholder}) | ||||||
|                         textField.SetClass("rounded-l border border-grey") |                         textField.SetClass("rounded-l border border-grey") | ||||||
|                         const txt = textField.GetValue() |                         const txt = textField.GetValue() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1304,6 +1304,10 @@ video { | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .justify-items-center { | ||||||
|  |   justify-items: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .gap-4 { | .gap-4 { | ||||||
|   gap: 1rem; |   gap: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -522,11 +522,26 @@ | ||||||
|     "string": { |     "string": { | ||||||
|       "description": "a piece of text" |       "description": "a piece of text" | ||||||
|     }, |     }, | ||||||
|  |     "email": { | ||||||
|  |       "feedback": "This is not a valid email address", | ||||||
|  |       "noAt": "An email address should contain an @" | ||||||
|  | }, | ||||||
|  |     "phone": { | ||||||
|  |       "feedback": "This is not a valid phone number" | ||||||
|  |     }, | ||||||
|  |     "url": { | ||||||
|  |       "feedback": "This is not a valid web address" | ||||||
|  |     }, | ||||||
|     "pnat": { |     "pnat": { | ||||||
|       "description": "a positive number" |       "description": "a positive number", | ||||||
|  |       "noZero": "Zero is not allowed" | ||||||
|     }, |     }, | ||||||
|     "nat": { |     "nat": { | ||||||
|       "description": "a positive number or zero" |       "description": "a positive number or zero", | ||||||
|     } |       "mustBePositive": "This number should be positive", | ||||||
|  |       "notANumber": "Enter a number", | ||||||
|  |       "mustBeWhole": "Only whole numbers are allowed" | ||||||
|  |     }, | ||||||
|  |     "tooLong": "Text is to long, at most 255 characters are allowed. You do have {count} characters now" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "npm run generate:layeroverview && npm run ", |     "start": "npm run generate:layeroverview && npm run ", | ||||||
|     "strt": "export NODE_OPTIONS=--max_old_space_size=8364 && parcel serve *.html UI/** Logic/** assets/*.json assets/svg/* assets/generated/* assets/layers/*/*.svg assets/layers/*/*.jpg assets/layers/*/*.png assets/layers/*/*.css assets/tagRenderings/*.json assets/themes/*/*.svg assets/themes/*/*.ttf assets/themes/*/*/*.ttf aassets/themes/*/*.otf assets/themes/*/*/*.otf ssets/themes/*/*.css assets/themes/*/*.jpg assets/themes/*/*.png vendor/* vendor/*/*", |     "strt": "export NODE_OPTIONS=--max_old_space_size=8364 && parcel serve *.html UI/** Logic/** assets/*.json assets/svg/* assets/generated/* assets/layers/*/*.svg assets/layers/*/*.jpg assets/layers/*/*.png assets/layers/*/*.css assets/tagRenderings/*.json assets/themes/*/*.svg assets/themes/*/*.ttf assets/themes/*/*/*.ttf aassets/themes/*/*.otf assets/themes/*/*/*.otf ssets/themes/*/*.css assets/themes/*/*.jpg assets/themes/*/*.png vendor/* vendor/*/*", | ||||||
|  |     "strttest": "export NODE_OPTIONS=--max_old_space_size=8364 && parcel serve test.html", | ||||||
|     "watch:css": "tailwindcss -i index.css -o css/index-tailwind-output.css --watch", |     "watch:css": "tailwindcss -i index.css -o css/index-tailwind-output.css --watch", | ||||||
|     "generate:css": "tailwindcss -i index.css -o css/index-tailwind-output.css", |     "generate:css": "tailwindcss -i index.css -o css/index-tailwind-output.css", | ||||||
|     "test": "ts-node test/TestAll.ts", |     "test": "ts-node test/TestAll.ts", | ||||||
|  |  | ||||||
							
								
								
									
										44
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										44
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -1,15 +1,33 @@ | ||||||
| import xml2js from 'xml2js'; | import Combine from "./UI/Base/Combine"; | ||||||
| import {readFileSync} from "fs"; | import ValidatedTextField from "./UI/Input/ValidatedTextField"; | ||||||
|  | import Title from "./UI/Base/Title"; | ||||||
|  | import {FixedUiElement} from "./UI/Base/FixedUiElement"; | ||||||
|  | import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||||
|  | import {UIEventSource} from "./Logic/UIEventSource"; | ||||||
|  | import {Translation} from "./UI/i18n/Translation"; | ||||||
| 
 | 
 | ||||||
| const xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + | new Combine( | ||||||
|     "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"375px\" height=\"375px\" viewBox=\"0 0 375 375\" version=\"1.1\">\n" + |     ValidatedTextField.AvailableTypes().map(key => { | ||||||
|     "  <g id=\"surface1\">\n" + |             let inp; | ||||||
|     "    <path style=\" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;\" d=\"M 54.046875 233.558594 C 75.683594 233.558594 93.1875 215.566406 93.1875 193.296875 C 93.1875 171.148438 75.683594 153.148438 54.046875 153.148438 C 32.496094 153.148438 15 171.148438 15 193.296875 C 15 215.558594 32.496094 233.558594 54.046875 233.558594 Z M 273.269531 302.675781 C 275.09375 301.386719 277.5 301.777344 278.828125 303.554688 L 286.066406 313.84375 C 287.296875 315.628906 286.921875 318.195312 285.195312 319.589844 L 232.09375 359.242188 C 230.378906 360.53125 227.878906 360.128906 226.523438 358.359375 L 219.308594 348.066406 C 218.070312 346.28125 218.460938 343.726562 220.175781 342.335938 Z M 213.246094 251.550781 L 192.1875 213.578125 L 181.132812 221.878906 L 202.191406 260.054688 Z M 164.113281 162.847656 L 82.214844 15 L 66.546875 15.105469 L 152.960938 171.253906 Z M 241.222656 241.074219 C 245.851562 237.613281 256.441406 232.5 262.5 232.5 L 337.5 232.5 C 352.304688 232.5 360 247.394531 360 262.53125 C 360 277.753906 352.304688 285 337.5 285 L 270 285 L 188.730469 348.765625 C 160.171875 369.90625 142.859375 346.679688 142.859375 346.679688 L 79.238281 281.210938 C 68.566406 268.066406 70.875 243.046875 83.671875 231.96875 L 171.710938 167.488281 C 180.457031 159.988281 193.441406 161.167969 200.851562 170.0625 C 208.171875 179.0625 207.015625 192.515625 198.253906 200.03125 L 141.890625 242.460938 L 182.386719 283.589844 Z M 241.222656 241.074219 \"/>\n" + |             const feedback = new UIEventSource<Translation>(undefined) | ||||||
|     "  </g>\n" + |             try { | ||||||
|     "</svg>" |                 inp = ValidatedTextField.ForType(key).ConstructInputElement({ | ||||||
|  |                     feedback, | ||||||
|  |                     country: () => "be", | ||||||
|  |                      | ||||||
|  |                 }); | ||||||
|  |             } catch (e) { | ||||||
|  |                 console.error(e) | ||||||
|  |                 inp = new FixedUiElement(e).SetClass("alert") | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return new Combine([ | ||||||
|  |                 new Title(key), | ||||||
|  |                 inp, | ||||||
|  |                 new VariableUiElement(inp.GetValue()), | ||||||
|  |                 new VariableUiElement(feedback.map(v => v?.SetClass("alert"))) | ||||||
|  |             ]); | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | ).AttachTo("maindiv") | ||||||
| 
 | 
 | ||||||
| xml2js.parseStringPromise(xml).then(svgResult => { |  | ||||||
|     const svg = svgResult.svg |  | ||||||
|     const builder = new xml2js.Builder(); |  | ||||||
|     console.log(builder.buildObject(svg)) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue