Refactoring of ValidatedTextField-types

This commit is contained in:
Pieter Vander Vennet 2022-02-11 20:56:54 +01:00
parent 9ab4fbd6f5
commit e07b770e8c
2 changed files with 382 additions and 300 deletions

View file

@ -100,7 +100,6 @@ export default class TagRenderingConfig {
const typeDescription = Translations.t.validation[type]?.description const typeDescription = Translations.t.validation[type]?.description
placeholder = Translations.T(json.freeform.key+" ("+type+")") placeholder = Translations.T(json.freeform.key+" ("+type+")")
if(typeDescription !== undefined){ if(typeDescription !== undefined){
console.log(typeDescription)
placeholder = placeholder.Fuse(typeDescription, type) placeholder = placeholder.Fuse(typeDescription, type)
} }
} }
@ -135,7 +134,7 @@ export default class TagRenderingConfig {
if (!ValidatedTextField.AllTypes.has(this.freeform.type)) { if (!ValidatedTextField.AllTypes.has(this.freeform.type)) {
const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).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}`
} }
if (this.freeform.addExtraTags) { if (this.freeform.addExtraTags) {

View file

@ -24,30 +24,73 @@ import Combine from "../Base/Combine";
import Title from "../Base/Title"; 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";
class SimpleTextFieldDef {
public readonly name: string;
/*
* An explanation for the theme builder.
* This can indicate which special input element is used, ...
* */
public readonly explanation: string;
public inputmode?: string = undefined
constructor(explanation: string | BaseUIElement, name?: string) {
this.name = name ?? this.constructor.name.toLowerCase();
if (this.name.endsWith("textfield")) {
this.name = this.name.substr(0, this.name.length - "TextField".length)
}
if (this.name.endsWith("textfielddef")) {
this.name = this.name.substr(0, this.name.length - "TextFieldDef".length)
}
if (typeof explanation === "string") {
this.explanation = explanation
} else {
this.explanation = explanation.AsMarkdown();
}
}
public reformat(s: string, country?: () => string): string {
return s;
}
interface TextFieldDef {
name: string,
explanation: string,
isValid: ((s: string, country?: () => string) => boolean),
reformat?: ((s: string, country?: () => string) => string),
/** /**
* Modification to make before the string is uploaded to OSM * Modification to make before the string is uploaded to OSM
*/ */
postprocess?: (s: string) => string; public postprocess(s: string): string {
undoPostprocess?: (s: string) => string; return s
inputHelper?: (value: UIEventSource<string>, options?: { }
public undoPostprocess(s: string): string {
return s;
}
public 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)[]
feature?: any feature?: any
}) => InputElement<string>, }): InputElement<string> {
inputmode?: string return undefined
}
isValid(s: string, country: (() => string) | undefined): boolean {
return true;
}
getFeedback(s: string) : Translation {
return undefined
}
} }
class WikidataTextField implements TextFieldDef { class WikidataTextField extends SimpleTextFieldDef {
name = "wikidata"
explanation = constructor() {
new Combine([ super(new Combine([
"A wikidata identifier, e.g. Q42.", "A wikidata identifier, e.g. Q42.",
new Title("Helper arguments"), new Title("Helper arguments"),
new Table(["name", "doc"], new Table(["name", "doc"],
@ -82,10 +125,11 @@ class WikidataTextField implements TextFieldDef {
] ]
} }
\`\`\`` \`\`\``
]).AsMarkdown() ]));
}
public isValid(str) : boolean{ public isValid(str): boolean {
if (str === undefined) { if (str === undefined) {
return false; return false;
@ -140,10 +184,10 @@ class WikidataTextField implements TextFieldDef {
} }
} }
class OpeningHoursTextField implements TextFieldDef { class OpeningHoursTextField extends SimpleTextFieldDef {
name = "opening_hours"
explanation = constructor() {
new Combine([ super(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"],
@ -170,8 +214,9 @@ class OpeningHoursTextField implements TextFieldDef {
"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 )`"]).AsMarkdown() }` + "\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() {
return true return true
@ -195,12 +240,14 @@ class OpeningHoursTextField implements TextFieldDef {
} }
} }
class UrlTextfieldDef implements TextFieldDef { class UrlTextfieldDef extends SimpleTextFieldDef {
name = "url"
explanation = "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"
inputmode: "url" inputmode: "url"
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")
}
postprocess(str: string) { postprocess(str: string) {
if (str === undefined) { if (str === undefined) {
return undefined return undefined
@ -273,28 +320,30 @@ class UrlTextfieldDef implements TextFieldDef {
} }
} }
export default class ValidatedTextField { class StringTextField extends SimpleTextFieldDef {
constructor() {
super("A simple piece of text");
}
}
public static tpList: TextFieldDef[] = [ class TextTextField extends SimpleTextFieldDef {
inputmode: "text"
ValidatedTextField.tp( constructor() {
"string", super("A longer piece of text");
"A basic string"), }
ValidatedTextField.tp( }
"text",
"A string, but allows input of longer strings more comfortably and supports newlines (a text area)",
undefined,
undefined,
undefined,
"text"),
ValidatedTextField.tp( class DateTextField extends SimpleTextFieldDef {
"date", constructor() {
"A date", super("A date with date picker");
(str) => { }
isValid = (str) => {
return !isNaN(new Date(str).getTime()); return !isNaN(new Date(str).getTime());
}, }
(str) => {
reformat(str) {
const d = new Date(str); const d = new Date(str);
let month = '' + (d.getMonth() + 1); let month = '' + (d.getMonth() + 1);
let day = '' + d.getDate(); let day = '' + d.getDate();
@ -306,16 +355,26 @@ export default class ValidatedTextField {
day = '0' + day; day = '0' + day;
return [year, month, day].join('-'); return [year, month, day].join('-');
}, }
(value) => new SimpleDatePicker(value)),
ValidatedTextField.tp( inputHelper(value) {
"direction", return new SimpleDatePicker(value)
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)", }
(str) => { }
class DirectionTextField extends SimpleTextFieldDef {
inputMode = "numeric"
constructor() {
super("A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)");
}
isValid = (str) => {
str = "" + str; str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360
}, str => str, }
(value, options) => {
inputHelper = (value, options) => {
const args = options.args ?? [] const args = options.args ?? []
let zoom = 19 let zoom = 19
if (args[0]) { if (args[0]) {
@ -339,18 +398,24 @@ export default class ValidatedTextField {
di.SetStyle("max-width: 25rem;"); di.SetStyle("max-width: 25rem;");
return di; return di;
}, }
"numeric" }
),
ValidatedTextField.tp( class LengthTextField extends SimpleTextFieldDef {
"length", inputMode: "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\"]",
(str) => { 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) const t = Number(str)
return !isNaN(t) return !isNaN(t)
}, }
str => str,
(value, options) => { inputHelper = (value, options) => {
const args = options.args ?? [] const args = options.args ?? []
let zoom = 19 let zoom = 19
if (args[0]) { if (args[0]) {
@ -363,7 +428,7 @@ export default class ValidatedTextField {
// Bit of a hack: we project the centerpoint to the closes point on the road - if available // 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") { if (options.feature !== undefined && options.feature.geometry.type !== "Point") {
const lonlat: [number, number] = [...options.location] const lonlat = <[number, number]>[...options.location]
lonlat.reverse() lonlat.reverse()
options.location = <[number, number]>GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates options.location = <[number, number]>GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates
options.location.reverse() options.location.reverse()
@ -383,65 +448,93 @@ export default class ValidatedTextField {
const li = new LengthInput(options.mapBackgroundLayer, location, value) const li = new LengthInput(options.mapBackgroundLayer, location, value)
li.SetStyle("height: 20rem;") li.SetStyle("height: 20rem;")
return li; return li;
}, }
"decimal" }
),
new WikidataTextField(),
ValidatedTextField.tp( class IntTextField extends SimpleTextFieldDef {
"int", inputMode = "numeric"
"A number",
(str) => { constructor() {
super("A number");
}
isValid = (str) => {
str = "" + str; str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))
}, }
str => "" + Number(str),
undefined, reformat = str => "" + Number(str)
"numeric"), }
ValidatedTextField.tp(
"nat", class NatTextField extends SimpleTextFieldDef {
"A positive number or zero", inputMode = "numeric"
(str) => {
constructor() {
super("A positive number or zero");
}
isValid = (str) => {
str = "" + str; str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
}, }
str => "" + Number(str),
undefined, reformat = str => "" + Number(str)
"numeric"), }
ValidatedTextField.tp(
"pnat", class PNatTextField extends SimpleTextFieldDef {
"A strict positive number", inputmode = "numeric"
(str) => {
constructor() {
super("A strict positive number");
}
isValid = (str) => {
str = "" + str; str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0 return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0
}, }
str => "" + Number(str),
undefined, reformat = str => "" + Number(str)
"numeric"), }
ValidatedTextField.tp(
"float", class FloatTextField extends SimpleTextFieldDef {
"A decimal", inputmode = "decimal"
(str) => !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(","),
str => "" + Number(str), constructor() {
undefined, super("A decimal");
"decimal"), }
ValidatedTextField.tp(
"pfloat", isValid = (str) => !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",")
"A positive decimal (incl zero)",
(str) => !isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(","), reformat = str => "" + Number(str)
str => "" + Number(str), }
undefined,
"decimal"), class PFloatTextField extends SimpleTextFieldDef {
ValidatedTextField.tp( inputmode = "decimal"
"email",
"An email adress", constructor() {
(str) => { 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:")) { if (str.startsWith("mailto:")) {
str = str.substring("mailto:".length) str = str.substring("mailto:".length)
} }
return EmailValidator.validate(str); return EmailValidator.validate(str);
}, }
str => {
reformat = str => {
if (str === undefined) { if (str === undefined) {
return undefined return undefined
} }
@ -449,14 +542,17 @@ export default class ValidatedTextField {
str = str.substring("mailto:".length) str = str.substring("mailto:".length)
} }
return str; return str;
}, }
undefined, }
"email"),
new UrlTextfieldDef(), class PhoneTextField extends SimpleTextFieldDef {
ValidatedTextField.tp( inputmode = "tel"
"phone",
"A phone number", constructor() {
(str, country: () => string) => { super("A phone number");
}
isValid = (str, country: () => string) => {
if (str === undefined) { if (str === undefined) {
return false; return false;
} }
@ -464,34 +560,49 @@ export default class ValidatedTextField {
str = str.substring("tel:".length) str = str.substring("tel:".length)
} }
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false
}, }
(str, country: () => string) => {
reformat = (str, country: () => string) => {
if (str.startsWith("tel:")) { if (str.startsWith("tel:")) {
str = str.substring("tel:".length) str = str.substring("tel:".length)
} }
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational(); return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational();
}, }
undefined, }
"tel"
), class ColorTextField extends SimpleTextFieldDef {
new OpeningHoursTextField(), constructor() {
ValidatedTextField.tp( super("Shows a color picker");
"color", }
"Shows a color picker",
() => true, inputHelper = (value) => {
str => str,
(value) => {
return new ColorPicker(value.map(color => { return new ColorPicker(value.map(color => {
return Utils.ColourNameToHex(color ?? ""); return Utils.ColourNameToHex(color ?? "");
}, [], str => Utils.HexToColourName(str))) }, [], str => Utils.HexToColourName(str)))
} }
) }
export default class ValidatedTextField {
private static allTextfieldDefs: SimpleTextFieldDef[] = [
new StringTextField(),
new TextTextField(),
new DateTextField(),
new NatTextField(),
new IntTextField(),
new LengthTextField(),
new DirectionTextField(),
new WikidataTextField(),
new PNatTextField(),
new FloatTextField(),
new PFloatTextField(),
new EmailTextField(),
new UrlTextfieldDef(),
new PhoneTextField(),
new OpeningHoursTextField(),
new ColorTextField()
] ]
/** public static AllTypes: Map<string, SimpleTextFieldDef> = ValidatedTextField.allTypesDict();
* {string (typename) --> TextFieldDef}
*/
public static AllTypes: Map<string, TextFieldDef> = ValidatedTextField.allTypesDict();
private static Tranlations: string | BaseUIElement;
public static InputForType(type: string, options?: { public static InputForType(type: string, options?: {
placeholder?: string | BaseUIElement, placeholder?: string | BaseUIElement,
@ -510,10 +621,10 @@ export default class ValidatedTextField {
inputStyle?: string inputStyle?: string
}): InputElement<string> { }): InputElement<string> {
options = options ?? {}; options = options ?? {};
if(options.placeholder === undefined) { if (options.placeholder === undefined) {
options.placeholder = Translations.t.validation[type]?.description ?? type options.placeholder = Translations.t.validation[type]?.description ?? type
} }
const tp: TextFieldDef = ValidatedTextField.AllTypes.get(type) const tp: SimpleTextFieldDef = ValidatedTextField.AllTypes.get(type)
const isValidTp = tp.isValid; const isValidTp = tp.isValid;
let isValid; let isValid;
options.textArea = options.textArea ?? type === "text"; options.textArea = options.textArea ?? type === "text";
@ -615,13 +726,13 @@ export default class ValidatedTextField {
} }
).SetClass("flex") ).SetClass("flex")
} }
if (tp.inputHelper) {
const helper = tp.inputHelper(input.GetValue(), { const helper = tp.inputHelper(input.GetValue(), {
location: options.location, location: options.location,
mapBackgroundLayer: options.mapBackgroundLayer, mapBackgroundLayer: options.mapBackgroundLayer,
args: options.args, args: options.args,
feature: options.feature feature: options.feature
}).SetClass("block") })?.SetClass("block")
if (helper !== undefined) {
input = new CombinedInputElement(input, helper, input = new CombinedInputElement(input, helper,
(a, _) => a, // We can ignore b, as they are linked earlier (a, _) => a, // We can ignore b, as they are linked earlier
a => [a, a] a => [a, a]
@ -640,7 +751,7 @@ export default class ValidatedTextField {
public static HelpText(): BaseUIElement { public static HelpText(): BaseUIElement {
const explanations: BaseUIElement[] = const explanations: BaseUIElement[] =
ValidatedTextField.tpList.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),
@ -649,41 +760,13 @@ export default class ValidatedTextField {
]).SetClass("flex flex-col") ]).SetClass("flex flex-col")
} }
private static tp(name: string, public static AvailableTypes(): string[] {
explanation: string, return ValidatedTextField.allTextfieldDefs.map(tp => tp.name)
isValid?: ((s: string, country?: () => string) => boolean),
reformat?: ((s: string, country?: () => string) => string),
inputHelper?: (value: UIEventSource<string>, options?: {
location: [number, number],
mapBackgroundLayer: UIEventSource<any>,
args: string[],
feature: any
}) => InputElement<string>,
inputmode?: string): TextFieldDef {
if (isValid === undefined) {
isValid = () => true;
} }
if (reformat === undefined) { private static allTypesDict(): Map<string, SimpleTextFieldDef> {
reformat = (str, _) => str; const types = new Map<string, SimpleTextFieldDef>();
} for (const tp of ValidatedTextField.allTextfieldDefs) {
return {
name: name,
explanation: explanation,
isValid: isValid,
reformat: reformat,
inputHelper: inputHelper,
inputmode: inputmode
}
}
private static allTypesDict(): Map<string, TextFieldDef> {
const types = new Map<string, TextFieldDef>();
for (const tp of ValidatedTextField.tpList) {
types[tp.name] = tp; types[tp.name] = tp;
types.set(tp.name, tp); types.set(tp.name, tp);
} }