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,7 +8,8 @@ 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,
|
||||||
value?: UIEventSource<string>,
|
value?: UIEventSource<string>,
|
||||||
|
@ -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 {
|
||||||
|
@ -128,7 +131,11 @@ export class TextField extends InputElement<string> {
|
||||||
GetValue(): UIEventSource<string> {
|
GetValue(): UIEventSource<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,10 +78,12 @@ 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 = () => {
|
||||||
|
@ -111,7 +113,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
const saveButton = new Combine([
|
const saveButton = new Combine([
|
||||||
options.saveButtonConstr(inputElement.GetValue()),
|
options.saveButtonConstr(inputElement.GetValue()),
|
||||||
])
|
])
|
||||||
|
|
||||||
let bottomTags: BaseUIElement;
|
let bottomTags: BaseUIElement;
|
||||||
if (options.bottomText !== undefined) {
|
if (options.bottomText !== undefined) {
|
||||||
bottomTags = options.bottomText(inputElement.GetValue())
|
bottomTags = options.bottomText(inputElement.GetValue())
|
||||||
|
@ -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,13 +162,14 @@ 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
|
||||||
let inputEls: InputElement<TagsFilter>[];
|
let inputEls: InputElement<TagsFilter>[];
|
||||||
|
|
||||||
|
@ -359,25 +368,26 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
tagging = new And([tagging, ...mapping.addExtraTags])
|
tagging = new And([tagging, ...mapping.addExtraTags])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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,18 +431,24 @@ 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),
|
||||||
|
|
|
@ -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