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
placeholder = Translations.T(json.freeform.key+" ("+type+")")
if(typeDescription !== undefined){
console.log(typeDescription)
placeholder = placeholder.Fuse(typeDescription, type)
}
}
@ -135,7 +134,7 @@ export default class TagRenderingConfig {
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}`
}
if (this.freeform.addExtraTags) {

View file

@ -24,30 +24,73 @@ import Combine from "../Base/Combine";
import Title from "../Base/Title";
import InputElementMap from "./InputElementMap";
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
*/
postprocess?: (s: string) => string;
undoPostprocess?: (s: string) => string;
inputHelper?: (value: UIEventSource<string>, options?: {
public postprocess(s: string): string {
return s
}
public undoPostprocess(s: string): string {
return s;
}
public inputHelper(value: UIEventSource<string>, options?: {
location: [number, number],
mapBackgroundLayer?: UIEventSource<any>,
args: (string | number | boolean | any)[]
feature?: any
}) => InputElement<string>,
inputmode?: string
}): InputElement<string> {
return undefined
}
class WikidataTextField implements TextFieldDef {
name = "wikidata"
explanation =
new Combine([
isValid(s: string, country: (() => string) | undefined): boolean {
return true;
}
getFeedback(s: string) : Translation {
return undefined
}
}
class WikidataTextField extends SimpleTextFieldDef {
constructor() {
super(new Combine([
"A wikidata identifier, e.g. Q42.",
new Title("Helper arguments"),
new Table(["name", "doc"],
@ -82,7 +125,8 @@ class WikidataTextField implements TextFieldDef {
]
}
\`\`\``
]).AsMarkdown()
]));
}
public isValid(str): boolean {
@ -140,10 +184,10 @@ class WikidataTextField implements TextFieldDef {
}
}
class OpeningHoursTextField implements TextFieldDef {
name = "opening_hours"
explanation =
new Combine([
class OpeningHoursTextField extends SimpleTextFieldDef {
constructor() {
super(new Combine([
"Has extra elements to easily input when a POI is opened.",
new Title("Helper arguments"),
new Table(["name", "doc"],
@ -170,8 +214,9 @@ class OpeningHoursTextField implements TextFieldDef {
"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() {
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"
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) {
if (str === 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(
"string",
"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"),
constructor() {
super("A longer piece of text");
}
}
ValidatedTextField.tp(
"date",
"A date",
(str) => {
class DateTextField extends SimpleTextFieldDef {
constructor() {
super("A date with date picker");
}
isValid = (str) => {
return !isNaN(new Date(str).getTime());
},
(str) => {
}
reformat(str) {
const d = new Date(str);
let month = '' + (d.getMonth() + 1);
let day = '' + d.getDate();
@ -306,16 +355,26 @@ export default class ValidatedTextField {
day = '0' + day;
return [year, month, day].join('-');
},
(value) => new SimpleDatePicker(value)),
ValidatedTextField.tp(
"direction",
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)",
(str) => {
}
inputHelper(value) {
return new SimpleDatePicker(value)
}
}
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;
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 ?? []
let zoom = 19
if (args[0]) {
@ -339,18 +398,24 @@ export default class ValidatedTextField {
di.SetStyle("max-width: 25rem;");
return di;
},
"numeric"
),
ValidatedTextField.tp(
"length",
"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) => {
}
}
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)
},
str => str,
(value, options) => {
}
inputHelper = (value, options) => {
const args = options.args ?? []
let zoom = 19
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
if (options.feature !== undefined && options.feature.geometry.type !== "Point") {
const lonlat: [number, number] = [...options.location]
const lonlat = <[number, number]>[...options.location]
lonlat.reverse()
options.location = <[number, number]>GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates
options.location.reverse()
@ -383,65 +448,93 @@ export default class ValidatedTextField {
const li = new LengthInput(options.mapBackgroundLayer, location, value)
li.SetStyle("height: 20rem;")
return li;
},
"decimal"
),
new WikidataTextField(),
}
}
ValidatedTextField.tp(
"int",
"A number",
(str) => {
class IntTextField extends SimpleTextFieldDef {
inputMode = "numeric"
constructor() {
super("A number");
}
isValid = (str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))
},
str => "" + Number(str),
undefined,
"numeric"),
ValidatedTextField.tp(
"nat",
"A positive number or zero",
(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
},
str => "" + Number(str),
undefined,
"numeric"),
ValidatedTextField.tp(
"pnat",
"A strict positive number",
(str) => {
}
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
},
str => "" + Number(str),
undefined,
"numeric"),
ValidatedTextField.tp(
"float",
"A decimal",
(str) => !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(","),
str => "" + Number(str),
undefined,
"decimal"),
ValidatedTextField.tp(
"pfloat",
"A positive decimal (incl zero)",
(str) => !isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(","),
str => "" + Number(str),
undefined,
"decimal"),
ValidatedTextField.tp(
"email",
"An email adress",
(str) => {
}
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);
},
str => {
}
reformat = str => {
if (str === undefined) {
return undefined
}
@ -449,14 +542,17 @@ export default class ValidatedTextField {
str = str.substring("mailto:".length)
}
return str;
},
undefined,
"email"),
new UrlTextfieldDef(),
ValidatedTextField.tp(
"phone",
"A phone number",
(str, country: () => string) => {
}
}
class PhoneTextField extends SimpleTextFieldDef {
inputmode = "tel"
constructor() {
super("A phone number");
}
isValid = (str, country: () => string) => {
if (str === undefined) {
return false;
}
@ -464,34 +560,49 @@ export default class ValidatedTextField {
str = str.substring("tel:".length)
}
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false
},
(str, country: () => string) => {
}
reformat = (str, country: () => string) => {
if (str.startsWith("tel:")) {
str = str.substring("tel:".length)
}
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational();
},
undefined,
"tel"
),
new OpeningHoursTextField(),
ValidatedTextField.tp(
"color",
"Shows a color picker",
() => true,
str => str,
(value) => {
}
}
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 {
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()
]
/**
* {string (typename) --> TextFieldDef}
*/
public static AllTypes: Map<string, TextFieldDef> = ValidatedTextField.allTypesDict();
private static Tranlations: string | BaseUIElement;
public static AllTypes: Map<string, SimpleTextFieldDef> = ValidatedTextField.allTypesDict();
public static InputForType(type: string, options?: {
placeholder?: string | BaseUIElement,
@ -513,7 +624,7 @@ export default class ValidatedTextField {
if (options.placeholder === undefined) {
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;
let isValid;
options.textArea = options.textArea ?? type === "text";
@ -615,13 +726,13 @@ export default class ValidatedTextField {
}
).SetClass("flex")
}
if (tp.inputHelper) {
const helper = tp.inputHelper(input.GetValue(), {
location: options.location,
mapBackgroundLayer: options.mapBackgroundLayer,
args: options.args,
feature: options.feature
}).SetClass("block")
})?.SetClass("block")
if (helper !== undefined) {
input = new CombinedInputElement(input, helper,
(a, _) => a, // We can ignore b, as they are linked earlier
a => [a, a]
@ -640,7 +751,7 @@ export default class ValidatedTextField {
public static HelpText(): 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"))
return new Combine([
new Title("Available types for text fields", 1),
@ -649,41 +760,13 @@ export default class ValidatedTextField {
]).SetClass("flex flex-col")
}
private static tp(name: string,
explanation: string,
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;
public static AvailableTypes(): string[] {
return ValidatedTextField.allTextfieldDefs.map(tp => tp.name)
}
if (reformat === undefined) {
reformat = (str, _) => str;
}
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) {
private static allTypesDict(): Map<string, SimpleTextFieldDef> {
const types = new Map<string, SimpleTextFieldDef>();
for (const tp of ValidatedTextField.allTextfieldDefs) {
types[tp.name] = tp;
types.set(tp.name, tp);
}