Refactoring of ValidatedTextField-types
This commit is contained in:
parent
9ab4fbd6f5
commit
e07b770e8c
2 changed files with 382 additions and 300 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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,27 +184,27 @@ class WikidataTextField implements TextFieldDef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OpeningHoursTextField implements TextFieldDef {
|
class OpeningHoursTextField extends SimpleTextFieldDef {
|
||||||
name = "opening_hours"
|
|
||||||
explanation =
|
|
||||||
new Combine([
|
|
||||||
"Has extra elements to easily input when a POI is opened.",
|
|
||||||
new Title("Helper arguments"),
|
|
||||||
new Table(["name", "doc"],
|
|
||||||
[
|
|
||||||
["options", new Combine([
|
|
||||||
"A JSON-object of type `{ prefix: string, postfix: string }`. ",
|
|
||||||
new Table(["subarg", "doc"],
|
|
||||||
[
|
|
||||||
["prefix", "Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse"],
|
|
||||||
["postfix", "Piece of text that will always be added to the end of the generated opening hours"],
|
|
||||||
])
|
|
||||||
|
|
||||||
])
|
constructor() {
|
||||||
]
|
super(new Combine([
|
||||||
]),
|
"Has extra elements to easily input when a POI is opened.",
|
||||||
new Title("Example usage"),
|
new Title("Helper arguments"),
|
||||||
"To add a conditional (based on time) access restriction:\n\n```\n" + `
|
new Table(["name", "doc"],
|
||||||
|
[
|
||||||
|
["options", new Combine([
|
||||||
|
"A JSON-object of type `{ prefix: string, postfix: string }`. ",
|
||||||
|
new Table(["subarg", "doc"],
|
||||||
|
[
|
||||||
|
["prefix", "Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse"],
|
||||||
|
["postfix", "Piece of text that will always be added to the end of the generated opening hours"],
|
||||||
|
])
|
||||||
|
|
||||||
|
])
|
||||||
|
]
|
||||||
|
]),
|
||||||
|
new Title("Example usage"),
|
||||||
|
"To add a conditional (based on time) access restriction:\n\n```\n" + `
|
||||||
"freeform": {
|
"freeform": {
|
||||||
"key": "access:conditional",
|
"key": "access:conditional",
|
||||||
"type": "opening_hours",
|
"type": "opening_hours",
|
||||||
|
@ -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,225 +320,289 @@ class UrlTextfieldDef implements TextFieldDef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StringTextField extends SimpleTextFieldDef {
|
||||||
|
constructor() {
|
||||||
|
super("A simple piece of text");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextTextField extends SimpleTextFieldDef {
|
||||||
|
inputmode: "text"
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("A longer piece of text");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateTextField extends SimpleTextFieldDef {
|
||||||
|
constructor() {
|
||||||
|
super("A date with date picker");
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = (str) => {
|
||||||
|
return !isNaN(new Date(str).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
reformat(str) {
|
||||||
|
const d = new Date(str);
|
||||||
|
let month = '' + (d.getMonth() + 1);
|
||||||
|
let day = '' + d.getDate();
|
||||||
|
const year = d.getFullYear();
|
||||||
|
|
||||||
|
if (month.length < 2)
|
||||||
|
month = '0' + month;
|
||||||
|
if (day.length < 2)
|
||||||
|
day = '0' + day;
|
||||||
|
|
||||||
|
return [year, month, day].join('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
inputHelper = (value, options) => {
|
||||||
|
const args = options.args ?? []
|
||||||
|
let zoom = 19
|
||||||
|
if (args[0]) {
|
||||||
|
zoom = Number(args[0])
|
||||||
|
if (isNaN(zoom)) {
|
||||||
|
throw "Invalid zoom level for argument at 'length'-input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 di = new DirectionInput(options.mapBackgroundLayer, location, value)
|
||||||
|
di.SetStyle("max-width: 25rem;");
|
||||||
|
|
||||||
|
return di;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
public static tpList: TextFieldDef[] = [
|
private static allTextfieldDefs: SimpleTextFieldDef[] = [
|
||||||
|
new StringTextField(),
|
||||||
ValidatedTextField.tp(
|
new TextTextField(),
|
||||||
"string",
|
new DateTextField(),
|
||||||
"A basic string"),
|
new NatTextField(),
|
||||||
ValidatedTextField.tp(
|
new IntTextField(),
|
||||||
"text",
|
new LengthTextField(),
|
||||||
"A string, but allows input of longer strings more comfortably and supports newlines (a text area)",
|
new DirectionTextField(),
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"text"),
|
|
||||||
|
|
||||||
ValidatedTextField.tp(
|
|
||||||
"date",
|
|
||||||
"A date",
|
|
||||||
(str) => {
|
|
||||||
return !isNaN(new Date(str).getTime());
|
|
||||||
},
|
|
||||||
(str) => {
|
|
||||||
const d = new Date(str);
|
|
||||||
let month = '' + (d.getMonth() + 1);
|
|
||||||
let day = '' + d.getDate();
|
|
||||||
const year = d.getFullYear();
|
|
||||||
|
|
||||||
if (month.length < 2)
|
|
||||||
month = '0' + month;
|
|
||||||
if (day.length < 2)
|
|
||||||
day = '0' + day;
|
|
||||||
|
|
||||||
return [year, month, day].join('-');
|
|
||||||
},
|
|
||||||
(value) => new SimpleDatePicker(value)),
|
|
||||||
ValidatedTextField.tp(
|
|
||||||
"direction",
|
|
||||||
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)",
|
|
||||||
(str) => {
|
|
||||||
str = "" + str;
|
|
||||||
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360
|
|
||||||
}, str => str,
|
|
||||||
(value, options) => {
|
|
||||||
const args = options.args ?? []
|
|
||||||
let zoom = 19
|
|
||||||
if (args[0]) {
|
|
||||||
zoom = Number(args[0])
|
|
||||||
if (isNaN(zoom)) {
|
|
||||||
throw "Invalid zoom level for argument at 'length'-input"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 di = new DirectionInput(options.mapBackgroundLayer, location, value)
|
|
||||||
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) => {
|
|
||||||
const t = Number(str)
|
|
||||||
return !isNaN(t)
|
|
||||||
},
|
|
||||||
str => str,
|
|
||||||
(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;
|
|
||||||
},
|
|
||||||
"decimal"
|
|
||||||
),
|
|
||||||
new WikidataTextField(),
|
new WikidataTextField(),
|
||||||
|
new PNatTextField(),
|
||||||
ValidatedTextField.tp(
|
new FloatTextField(),
|
||||||
"int",
|
new PFloatTextField(),
|
||||||
"A number",
|
new EmailTextField(),
|
||||||
(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) => {
|
|
||||||
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) => {
|
|
||||||
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) => {
|
|
||||||
if (str.startsWith("mailto:")) {
|
|
||||||
str = str.substring("mailto:".length)
|
|
||||||
}
|
|
||||||
return EmailValidator.validate(str);
|
|
||||||
},
|
|
||||||
str => {
|
|
||||||
if (str === undefined) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
if (str.startsWith("mailto:")) {
|
|
||||||
str = str.substring("mailto:".length)
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
"email"),
|
|
||||||
new UrlTextfieldDef(),
|
new UrlTextfieldDef(),
|
||||||
ValidatedTextField.tp(
|
new PhoneTextField(),
|
||||||
"phone",
|
|
||||||
"A phone number",
|
|
||||||
(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
|
|
||||||
},
|
|
||||||
(str, country: () => string) => {
|
|
||||||
if (str.startsWith("tel:")) {
|
|
||||||
str = str.substring("tel:".length)
|
|
||||||
}
|
|
||||||
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational();
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
"tel"
|
|
||||||
),
|
|
||||||
new OpeningHoursTextField(),
|
new OpeningHoursTextField(),
|
||||||
ValidatedTextField.tp(
|
new ColorTextField()
|
||||||
"color",
|
|
||||||
"Shows a color picker",
|
|
||||||
() => true,
|
|
||||||
str => str,
|
|
||||||
(value) => {
|
|
||||||
return new ColorPicker(value.map(color => {
|
|
||||||
return Utils.ColourNameToHex(color ?? "");
|
|
||||||
}, [], str => Utils.HexToColourName(str)))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
/**
|
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) {
|
|
||||||
reformat = (str, _) => str;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
explanation: explanation,
|
|
||||||
isValid: isValid,
|
|
||||||
reformat: reformat,
|
|
||||||
inputHelper: inputHelper,
|
|
||||||
inputmode: inputmode
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.tpList) {
|
|
||||||
types[tp.name] = tp;
|
types[tp.name] = tp;
|
||||||
types.set(tp.name, tp);
|
types.set(tp.name, tp);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue