Cleanup of textfield code

This commit is contained in:
Pieter Vander Vennet 2020-09-25 12:44:04 +02:00
parent 1f41444726
commit 3667f28f15
9 changed files with 293 additions and 219 deletions

View file

@ -1,8 +1,6 @@
import {UIElement} from "./UIElement"; import {UIElement} from "./UIElement";
import {OsmConnection} from "../Logic/Osm/OsmConnection";
import Translations from "./i18n/Translations"; import Translations from "./i18n/Translations";
import {State} from "../State"; import {State} from "../State";
import {UIEventSource} from "../Logic/UIEventSource";
export class CenterMessageBox extends UIElement { export class CenterMessageBox extends UIElement {
@ -16,7 +14,7 @@ export class CenterMessageBox extends UIElement {
this.ListenTo(State.state.layerUpdater.sufficentlyZoomed); this.ListenTo(State.state.layerUpdater.sufficentlyZoomed);
} }
private prep(): { innerHtml: string, done: boolean } { private static prep(): { innerHtml: string, done: boolean } {
if (State.state.centerMessage.data != "") { if (State.state.centerMessage.data != "") {
return {innerHtml: State.state.centerMessage.data, done: false}; return {innerHtml: State.state.centerMessage.data, done: false};
} }
@ -37,7 +35,7 @@ export class CenterMessageBox extends UIElement {
} }
InnerRender(): string { InnerRender(): string {
return this.prep().innerHtml; return CenterMessageBox.prep().innerHtml;
} }
@ -50,7 +48,7 @@ export class CenterMessageBox extends UIElement {
} }
pstyle.pointerEvents = "none"; pstyle.pointerEvents = "none";
if (this.prep().done) { if (CenterMessageBox.prep().done) {
pstyle.opacity = "0"; pstyle.opacity = "0";
} else { } else {
pstyle.opacity = "0.5"; pstyle.opacity = "0.5";

View file

@ -6,6 +6,7 @@ import SettingsTable from "./SettingsTable";
import SingleSetting from "./SingleSetting"; import SingleSetting from "./SingleSetting";
import {TextField} from "../Input/TextField"; import {TextField} from "../Input/TextField";
import MultiLingualTextFields from "../Input/MultiLingualTextFields"; import MultiLingualTextFields from "../Input/MultiLingualTextFields";
import ValidatedTextField from "../Input/ValidatedTextField";
export default class GeneralSettingsPanel extends UIElement { export default class GeneralSettingsPanel extends UIElement {
@ -17,15 +18,13 @@ export default class GeneralSettingsPanel extends UIElement {
super(undefined); super(undefined);
const languagesField = new TextField<string[]>( const languagesField =
{ ValidatedTextField.Mapped(
fromString: str => str?.split(";")?.map(str => str.trim().toLowerCase()), str => str?.split(";")?.map(str => str.trim().toLowerCase()),
toString: languages => languages.join(";"), languages => languages.join(";"));
}
);
this.languages = languagesField.GetValue(); this.languages = languagesField.GetValue();
const version = TextField.StringInput(); const version = new TextField();
const current_datetime = new Date(); const current_datetime = new Date();
let formatted_date = current_datetime.getFullYear() + "-" + (current_datetime.getMonth() + 1) + "-" + current_datetime.getDate() + " " + current_datetime.getHours() + ":" + current_datetime.getMinutes() + ":" + current_datetime.getSeconds() let formatted_date = current_datetime.getFullYear() + "-" + (current_datetime.getMonth() + 1) + "-" + current_datetime.getDate() + " " + current_datetime.getHours() + ":" + current_datetime.getMinutes() + ":" + current_datetime.getSeconds()
version.GetValue().setData(formatted_date); version.GetValue().setData(formatted_date);
@ -35,7 +34,7 @@ export default class GeneralSettingsPanel extends UIElement {
const settingsTable = new SettingsTable( const settingsTable = new SettingsTable(
[ [
new SingleSetting(configuration, TextField.StringInput(), "id", new SingleSetting(configuration, new TextField({placeholder:"id"}), "id",
"Identifier", "The identifier of this theme. This should be a lowercase, unique string"), "Identifier", "The identifier of this theme. This should be a lowercase, unique string"),
new SingleSetting(configuration, version, "version", "Version", new SingleSetting(configuration, version, "version", "Version",
"A version to indicate the theme version. Ideal is the date you created or updated the theme"), "A version to indicate the theme version. Ideal is the date you created or updated the theme"),
@ -47,26 +46,26 @@ export default class GeneralSettingsPanel extends UIElement {
"The short description is shown as subtext in the social preview and on the 'more screen'-buttons. It should be at most one sentence of around ~25words"), "The short description is shown as subtext in the social preview and on the 'more screen'-buttons. It should be at most one sentence of around ~25words"),
new SingleSetting(configuration, new MultiLingualTextFields(this.languages, true), new SingleSetting(configuration, new MultiLingualTextFields(this.languages, true),
"description", "Description", "The description is shown in the welcome-message when opening MapComplete. It is a small text welcoming users"), "description", "Description", "The description is shown in the welcome-message when opening MapComplete. It is a small text welcoming users"),
new SingleSetting(configuration, TextField.StringInput(), "icon", new SingleSetting(configuration, new TextField({placeholder: "URL to icon"}), "icon",
"Icon", "A visual representation for your theme; used as logo in the welcomeMessage. If your theme is official, used as favicon and webapp logo", "Icon", "A visual representation for your theme; used as logo in the welcomeMessage. If your theme is official, used as favicon and webapp logo",
{ {
showIconPreview: true showIconPreview: true
}), }),
new SingleSetting(configuration, TextField.NumberInput("nat", n => n < 23), "startZoom","Initial zoom level", new SingleSetting(configuration, ValidatedTextField.NumberInput("nat", n => n < 23), "startZoom","Initial zoom level",
"When a user first loads MapComplete, this zoomlevel is shown."+locationRemark), "When a user first loads MapComplete, this zoomlevel is shown."+locationRemark),
new SingleSetting(configuration, TextField.NumberInput("float", n => (n < 90 && n > -90)), "startLat","Initial latitude", new SingleSetting(configuration, ValidatedTextField.NumberInput("float", n => (n < 90 && n > -90)), "startLat","Initial latitude",
"When a user first loads MapComplete, this latitude is shown as location."+locationRemark), "When a user first loads MapComplete, this latitude is shown as location."+locationRemark),
new SingleSetting(configuration, TextField.NumberInput("float", n => (n < 180 && n > -180)), "startLon","Initial longitude", new SingleSetting(configuration, ValidatedTextField.NumberInput("float", n => (n < 180 && n > -180)), "startLon","Initial longitude",
"When a user first loads MapComplete, this longitude is shown as location."+locationRemark), "When a user first loads MapComplete, this longitude is shown as location."+locationRemark),
new SingleSetting(configuration, TextField.NumberInput("pfloat", n => (n < 0.5 )), "widenFactor","Query widening", new SingleSetting(configuration, ValidatedTextField.NumberInput("pfloat", n => (n < 0.5 )), "widenFactor","Query widening",
"When a query is run, the data within bounds of the visible map is loaded.\n" + "When a query is run, the data within bounds of the visible map is loaded.\n" +
"However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data.\n" + "However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data.\n" +
"For this, the bounds are widened in order to make a small pan still within bounds of the loaded data.\n" + "For this, the bounds are widened in order to make a small pan still within bounds of the loaded data.\n" +
"IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries)"), "IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries)"),
new SingleSetting(configuration, TextField.StringInput(), "socialImage", new SingleSetting(configuration, new TextField({placeholder: "URL to social image"}), "socialImage",
"og:image (aka Social Image)", "<span class='alert'>Only works on incorporated themes</span>" + "og:image (aka Social Image)", "<span class='alert'>Only works on incorporated themes</span>" +
"The Social Image is set as og:image for the HTML-site and helps social networks to show a preview", {showIconPreview: true}) "The Social Image is set as og:image for the HTML-site and helps social networks to show a preview", {showIconPreview: true})
], currentSetting); ], currentSetting);

View file

@ -19,6 +19,7 @@ import PresetInputPanel from "./PresetInputPanel";
import {UserDetails} from "../../Logic/Osm/OsmConnection"; import {UserDetails} from "../../Logic/Osm/OsmConnection";
import {State} from "../../State"; import {State} from "../../State";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import ValidatedTextField from "../Input/ValidatedTextField";
/** /**
* Shows the configuration for a single layer * Shows the configuration for a single layer
@ -86,17 +87,17 @@ export default class LayerPanel extends UIElement {
this.settingsTable = new SettingsTable([ this.settingsTable = new SettingsTable([
setting(TextField.StringInput(), "id", "Id", "An identifier for this layer<br/>This should be a simple, lowercase, human readable string that is used to identify the layer."), setting(new TextField({placeholder:"Layer id"}), "id", "Id", "An identifier for this layer<br/>This should be a simple, lowercase, human readable string that is used to identify the layer."),
setting(new MultiLingualTextFields(languages), "name", "Name", "The human-readable name of this layer<br/>Used in the layer control panel and the 'Personal theme'"), setting(new MultiLingualTextFields(languages), "name", "Name", "The human-readable name of this layer<br/>Used in the layer control panel and the 'Personal theme'"),
setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.<br/>Shown in the layer selections and in the personal theme"), setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.<br/>Shown in the layer selections and in the personal theme"),
setting(TextField.NumberInput("nat", n => n < 23), "minzoom", "Minimal zoom", setting(ValidatedTextField.NumberInput("nat", n => n < 23), "minzoom", "Minimal zoom",
"The minimum zoomlevel needed to load and show this layer."), "The minimum zoomlevel needed to load and show this layer."),
setting(new DropDown("", [ setting(new DropDown("", [
{value: 0, shown: "Show ways and areas as ways and lines"}, {value: 0, shown: "Show ways and areas as ways and lines"},
{value: 2, shown: "Show both the ways/areas and the centerpoints"}, {value: 2, shown: "Show both the ways/areas and the centerpoints"},
{value: 1, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling", {value: 1, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling",
"Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"), "Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"),
setting(TextField.NumberInput("int", n => n <= 100), "hideUnderlayingFeaturesMinPercentage", "Max allowed overlap percentage", setting(ValidatedTextField.NumberInput("int", n => n <= 100), "hideUnderlayingFeaturesMinPercentage", "Max allowed overlap percentage",
"Consider that we want to show 'Nature Reserves' and 'Forests'. Now, ofter, there are pieces of forest mapped _in_ the nature reserve.<br/>" + "Consider that we want to show 'Nature Reserves' and 'Forests'. Now, ofter, there are pieces of forest mapped _in_ the nature reserve.<br/>" +
"Now, showing those pieces of forest overlapping with the nature reserve truly clutters the map and is very user-unfriendly.<br/>" + "Now, showing those pieces of forest overlapping with the nature reserve truly clutters the map and is very user-unfriendly.<br/>" +
"The features are placed layer by layer. If a feature below a feature on this layer overlaps for more then 'x'-percent, the underlying feature is hidden."), "The features are placed layer by layer. If a feature below a feature on this layer overlaps for more then 'x'-percent, the underlying feature is hidden."),

View file

@ -9,6 +9,7 @@ export default class InputElementMap<T, X> extends InputElement<X> {
private readonly fromX: (x: X) => T; private readonly fromX: (x: X) => T;
private readonly toX: (t: T) => X; private readonly toX: (t: T) => X;
private readonly _value: UIEventSource<X>; private readonly _value: UIEventSource<X>;
public readonly IsSelected: UIEventSource<boolean>;
constructor(inputElement: InputElement<T>, constructor(inputElement: InputElement<T>,
isSame: (x0: X, x1: X) => boolean, isSame: (x0: X, x1: X) => boolean,
@ -32,8 +33,7 @@ export default class InputElementMap<T, X> extends InputElement<X> {
} }
return newX; return newX;
}), extraSources, x => { }), extraSources, x => {
const newT = fromX(x); return fromX(x);
return newT;
}); });
} }
@ -45,10 +45,15 @@ export default class InputElementMap<T, X> extends InputElement<X> {
return this._inputElement.InnerRender(); return this._inputElement.InnerRender();
} }
IsSelected: UIEventSource<boolean>;
IsValid(x: X): boolean { IsValid(x: X): boolean {
return this._inputElement.IsValid(this.fromX(x)); if(x === undefined){
return false;
}
const t = this.fromX(x);
if(t === undefined){
return false;
}
return this._inputElement.IsValid(t);
} }
} }

View file

@ -2,208 +2,86 @@ import {UIElement} from "../UIElement";
import {InputElement} from "./InputElement"; import {InputElement} from "./InputElement";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import * as EmailValidator from "email-validator";
import {parsePhoneNumberFromString} from "libphonenumber-js";
import {DropDown} from "./DropDown";
export class ValidatedTextField {
public static explanations = {
"string": "A basic, 255-char string",
"date": "A date",
"wikidata": "A wikidata identifier, e.g. Q42",
"int": "A number",
"nat": "A positive number",
"float": "A decimal",
"pfloat": "A positive decimal",
"email": "An email adress",
"url": "A url",
"phone": "A phone number"
}
public static TypeDropdown() : DropDown<string>{
const values : {value: string, shown: string}[] = [];
const expl = ValidatedTextField.explanations;
for(const key in expl){
values.push({value: key, shown: `${key} - ${expl[key]}`})
}
return new DropDown<string>("", values)
}
public static inputValidation = {
"$": () => true,
"string": () => true,
"date": () => true, // TODO validate and add a date picker
"wikidata": () => true, // TODO validate wikidata IDS
"int": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))},
"nat": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0},
"float": (str) => !isNaN(Number(str)),
"pfloat": (str) => !isNaN(Number(str)) && Number(str) >= 0,
"email": (str) => EmailValidator.validate(str),
"url": (str) => str,
"phone": (str, country) => {
return parsePhoneNumberFromString(str, country?.toUpperCase())?.isValid() ?? false;
}
}
public static formatting = {
"phone": (str, country) => {
console.log("country formatting", country)
return parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational()
}
}
}
export class TextField<T> extends InputElement<T> {
public static StringInput(textArea: boolean = false): TextField<string> {
return new TextField<string>({
toString: str => str,
fromString: str => str,
textArea: textArea
});
}
public static KeyInput(allowEmpty : boolean = false): TextField<string>{
return new TextField<string>({
placeholder: "key",
fromString: str => {
if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) {
return str;
}
if(str === "" && allowEmpty){
return "";
}
return undefined
},
toString: str => str
});
}
public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{
const isValid = ValidatedTextField.inputValidation[type];
extraValidation = extraValidation ?? (() => true)
return new TextField({
fromString: str => {
if(!isValid(str)){
return undefined;
}
const n = Number(str);
if(!extraValidation(n)){
return undefined;
}
return n;
},
toString: num => ""+num,
placeholder: type
});
}
export class TextField extends InputElement<string> {
private readonly value: UIEventSource<string>; private readonly value: UIEventSource<string>;
private readonly mappedValue: UIEventSource<T>;
public readonly enterPressed = new UIEventSource<string>(undefined); public readonly enterPressed = new UIEventSource<string>(undefined);
private readonly _placeholder: UIElement; private readonly _placeholder: UIElement;
private readonly _fromString?: (string: string) => T;
private readonly _toString: (t: T) => string;
private readonly startValidated: boolean;
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _isArea: boolean; private readonly _isArea: boolean;
private readonly _textAreaRows: number; private readonly _textAreaRows: number;
constructor(options: { constructor(options?: {
/**
* Shown as placeholder
*/
placeholder?: string | UIElement, placeholder?: string | UIElement,
value?: UIEventSource<string>,
/**
* Converts the T to a (canonical) string
* @param t
*/
toString: (t: T) => string,
/**
* Converts the string to a T
* Returns undefined if invalid
* @param string
*/
fromString: (string: string) => T,
value?: UIEventSource<T>,
startValidated?: boolean,
textArea?: boolean, textArea?: boolean,
textAreaRows?: number, textAreaRows?: number,
isValid?: ((s: string) => boolean)
}) { }) {
super(undefined); super(undefined);
const self = this; const self = this;
this.value = new UIEventSource<string>(""); this.value = new UIEventSource<string>("");
options = options ?? {};
this._isArea = options.textArea ?? false; this._isArea = options.textArea ?? false;
this.startValidated = options.startValidated ?? false; this.value = options?.value ?? new UIEventSource<string>(undefined);
this.mappedValue = options?.value ?? new UIEventSource<T>(undefined);
this.mappedValue.addCallback(() => self.InnerUpdate());
// @ts-ignore // @ts-ignore
this._fromString = options.fromString ?? ((str) => (str)) this._fromString = options.fromString ?? ((str) => (str))
this.value.addCallback((str) => this.mappedValue.setData(options.fromString(str)));
this.mappedValue.addCallback((t) => this.value.setData(options.toString(t)));
this._textAreaRows = options.textAreaRows; this._textAreaRows = options.textAreaRows;
this._placeholder = Translations.W(options.placeholder ?? ""); this._placeholder = Translations.W(options.placeholder ?? "");
this.ListenTo(this._placeholder._source); this.ListenTo(this._placeholder._source);
this._toString = options.toString ?? ((t) => ("" + t));
this.onClick(() => { this.onClick(() => {
self.IsSelected.setData(true) self.IsSelected.setData(true)
}); });
this.mappedValue.addCallback((t) => { this.value.addCallback((t) => {
if (t === undefined || t === null) { const field = document.getElementById(this.id);
return;
}
const field = document.getElementById('text-' + this.id);
if (field === undefined || field === null) { if (field === undefined || field === null) {
return; return;
} }
if (options.isValid) {
field.className = options.isValid(t) ? "" : "invalid";
}
if (t === undefined || t === null) {
// @ts-ignore // @ts-ignore
field.value = options.toString(t); return;
}
// @ts-ignore
field.value = t;
}); });
} }
GetValue(): UIEventSource<T> { GetValue(): UIEventSource<string> {
return this.mappedValue; return this.value;
} }
InnerRender(): string { InnerRender(): string {
if(this._isArea){ if (this._isArea) {
return `<textarea id="text-${this.id}" class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea>` return `<textarea id="${this.id}" class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea>`
} }
const placeholder = this._placeholder.InnerRender().replace("'", "&#39"); const placeholder = this._placeholder.InnerRender().replace("'", "&#39");
return `<form onSubmit='return false' class='form-text-field'>` + return `<form onSubmit='return false' class='form-text-field'>` +
`<input type='text' placeholder='${placeholder}' id='text-${this.id}'>` + `<input type='text' placeholder='${placeholder}' id='${this.id}'>` +
`</form>`; `</form>`;
} }
InnerUpdate() { InnerUpdate(field) {
const field = document.getElementById('text-' + this.id);
if (field === null) {
return;
}
this.mappedValue.addCallback((data) => {
field.className = data !== undefined ? "valid" : "invalid";
});
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";
const self = this; const self = this;
field.oninput = () => { field.oninput = () => {
// @ts-ignore // @ts-ignore
self.value.setData(field.value); self.value.setData(field.value);
}; };
if (this.value.data !== undefined && this.value.data !== null) {
// @ts-ignore
field.value = this.value.data;
}
field.addEventListener("focusin", () => self.IsSelected.setData(true)); field.addEventListener("focusin", () => self.IsSelected.setData(true));
field.addEventListener("focusout", () => self.IsSelected.setData(false)); field.addEventListener("focusout", () => self.IsSelected.setData(false));
@ -215,16 +93,6 @@ export class TextField<T> extends InputElement<T> {
} }
}); });
if (this.IsValid(this.mappedValue.data)) {
const expected = this._toString(this.mappedValue.data);
// @ts-ignore
if (field.value !== expected) {
// @ts-ignore
field.value = expected;
}
}
} }
public SetCursorPosition(i: number) { public SetCursorPosition(i: number) {
@ -232,7 +100,7 @@ export class TextField<T> extends InputElement<T> {
if(field === undefined || field === null){ if(field === undefined || field === null){
return; return;
} }
if(i === -1){ if (i === -1) {
// @ts-ignore // @ts-ignore
i = field.value.length; i = field.value.length;
} }
@ -241,12 +109,8 @@ export class TextField<T> extends InputElement<T> {
field.setSelectionRange(i, i); field.setSelectionRange(i, i);
} }
IsValid(t: T): boolean { IsValid(t: string): boolean {
if (t === undefined || t === null) { return !(t === undefined || t === null);
return false;
}
const result = this._toString(t);
return result !== undefined && result !== null;
} }
} }

View file

@ -0,0 +1,196 @@
import {DropDown} from "./DropDown";
import * as EmailValidator from "email-validator";
import {parsePhoneNumberFromString} from "libphonenumber-js";
import InputElementMap from "./InputElementMap";
import {InputElement} from "./InputElement";
import {TextField} from "./TextField";
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
export default class ValidatedTextField {
private static tp(name: string,
explanation: string,
isValid?: ((s: string, country?: string) => boolean),
reformat?: ((s: string, country?: string) => string)): {
name: string,
explanation: string,
isValid: ((s: string, country?: string) => boolean),
reformat?: ((s: string, country?: string) => string)
} {
if (isValid === undefined) {
isValid = () => true;
}
if (reformat === undefined) {
reformat = (str, _) => str;
}
return {
name: name,
explanation: explanation,
isValid: isValid,
reformat: reformat
}
}
public static tpList = [
ValidatedTextField.tp(
"string",
"A basic string"),
ValidatedTextField.tp(
"date",
"A date"),
ValidatedTextField.tp(
"wikidata",
"A wikidata identifier, e.g. Q42"),
ValidatedTextField.tp(
"int",
"A number",
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))
}),
ValidatedTextField.tp(
"nat",
"A positive number or zero",
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
}),
ValidatedTextField.tp(
"pnat",
"A strict positive number",
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0
}),
ValidatedTextField.tp(
"float",
"A decimal",
(str) => !isNaN(Number(str))),
ValidatedTextField.tp(
"pfloat",
"A positive decimal (incl zero)",
(str) => !isNaN(Number(str)) && Number(str) >= 0),
ValidatedTextField.tp(
"email",
"An email adress",
(str) => EmailValidator.validate(str)),
ValidatedTextField.tp(
"url",
"A url"),
ValidatedTextField.tp(
"phone",
"A phone number",
(str, country: any) => {
return parsePhoneNumberFromString(str, country?.toUpperCase())?.isValid() ?? false
},
(str, country: any) => {
console.log("country formatting", country)
return parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational()
}
)
]
private static allTypesDict(){
const types = {};
for (const tp of ValidatedTextField.tpList) {
types[tp.name] = tp;
}
return types;
}
public static TypeDropdown(): DropDown<string> {
const values: { value: string, shown: string }[] = [];
const expl = ValidatedTextField.tpList;
for (const key in expl) {
values.push({value: key, shown: `${key} - ${expl[key]}`})
}
return new DropDown<string>("", values)
}
public static AllTypes = ValidatedTextField.allTypesDict();
public static InputForType(type: string): TextField {
return new TextField({
placeholder: type,
isValid: ValidatedTextField.AllTypes[type]
})
}
public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined): InputElement<number> {
const isValid = ValidatedTextField.AllTypes[type].isValid;
extraValidation = extraValidation ?? (() => true)
const fromString = str => {
if (!isValid(str)) {
return undefined;
}
const n = Number(str);
if (!extraValidation(n)) {
return undefined;
}
return n;
};
const toString = num => {
if (num === undefined) {
return undefined;
}
return "" + num;
};
const textField = ValidatedTextField.InputForType(type);
return new InputElementMap(textField, (n0, n1) => n0 === n1, fromString, toString)
}
public static KeyInput(allowEmpty: boolean = false): InputElement<string> {
function fromString(str) {
if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) {
return str;
}
if (str === "" && allowEmpty) {
return "";
}
return undefined
}
const toString = str => str
function isSame(str0, str1) {
return str0 === str1;
}
const textfield = new TextField({
placeholder: "key",
isValid: str => fromString(str) !== undefined,
value: new UIEventSource<string>("")
});
return new InputElementMap(textfield, isSame, fromString, toString);
}
static Mapped<T>(fromString: (str) => T, toString: (T) => string, options?: {
placeholder?: string | UIElement,
value?: UIEventSource<string>,
startValidated?: boolean,
textArea?: boolean,
textAreaRows?: number,
isValid?: ((string: string) => boolean)
}): InputElement<T> {
const textField = new TextField(options);
return new InputElementMap(
textField, (a, b) => a === b,
fromString, toString
);
}
}

View file

@ -14,9 +14,9 @@ import {InputElement} from "./Input/InputElement";
import {SaveButton} from "./SaveButton"; import {SaveButton} from "./SaveButton";
import {RadioButton} from "./Input/RadioButton"; import {RadioButton} from "./Input/RadioButton";
import {FixedInputElement} from "./Input/FixedInputElement"; import {FixedInputElement} from "./Input/FixedInputElement";
import {TextField, ValidatedTextField} from "./Input/TextField";
import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; import {TagRenderingOptions} from "../Customizations/TagRenderingOptions";
import {FixedUiElement} from "./Base/FixedUiElement"; import {FixedUiElement} from "./Base/FixedUiElement";
import ValidatedTextField from "./Input/ValidatedTextField";
export class TagRendering extends UIElement implements TagDependantUIElement { export class TagRendering extends UIElement implements TagDependantUIElement {
@ -202,7 +202,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
InputElement<TagsFilter> { InputElement<TagsFilter> {
let freeformElement: TextField<TagsFilter> = undefined; let freeformElement: InputElement<TagsFilter> = undefined;
if (options.freeform !== undefined) { if (options.freeform !== undefined) {
freeformElement = this.InputForFreeForm(options.freeform); freeformElement = this.InputForFreeForm(options.freeform);
} }
@ -278,7 +278,6 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
es.data.push(i); es.data.push(i);
es.ping(); es.ping();
} }
freeformElement.SetCursorPosition(-1);
}); });
return inputEl; return inputEl;
@ -305,7 +304,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
renderTemplate: string | Translation, renderTemplate: string | Translation,
placeholder?: string | Translation, placeholder?: string | Translation,
extraTags?: TagsFilter, extraTags?: TagsFilter,
}): TextField<TagsFilter> { }): InputElement<TagsFilter> {
if (freeform?.template === undefined) { if (freeform?.template === undefined) {
return undefined; return undefined;
} }
@ -313,13 +312,24 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
const prepost = Translations.W(freeform.template).InnerRender() const prepost = Translations.W(freeform.template).InnerRender()
.replace("$$$", "$string$") .replace("$$$", "$string$")
.split("$"); .split("$");
const type = prepost[1]; let type = prepost[1];
let isValid = ValidatedTextField.inputValidation[type]; let isTextArea = false;
if(type === "text"){
isTextArea = true;
type = "string";
}
if(ValidatedTextField.AllTypes[type] === undefined){
console.error("Type:",type, ValidatedTextField.AllTypes)
throw "Unkown type: "+type;
}
let isValid = ValidatedTextField.AllTypes[type].isValid;
if (isValid === undefined) { if (isValid === undefined) {
isValid = () => true; isValid = () => true;
} }
let formatter = ValidatedTextField.formatting[type] ?? ((str) => str); let formatter = ValidatedTextField.AllTypes[type].reformat ?? ((str) => str);
const pickString = const pickString =
(string: any) => { (string: any) => {
@ -361,12 +371,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
} }
return undefined; return undefined;
} }
return ValidatedTextField.Mapped(
return new TextField({ pickString, toString, {placeholder: this._freeform.placeholder, isValid: isValid, textArea: isTextArea}
placeholder: this._freeform.placeholder, )
fromString: pickString,
toString: toString,
});
} }

View file

@ -230,7 +230,7 @@
"de": "Betrieben von {operator}" "de": "Betrieben von {operator}"
}, },
"freeform": { "freeform": {
"type": "text", "type": "string",
"key": "operator" "key": "operator"
} }
}, },

10
test.ts
View file

@ -1,4 +1,8 @@
import {UIEventSource} from "./Logic/UIEventSource"; import ValidatedTextField from "./UI/Input/ValidatedTextField";
import DeleteImage from "./UI/Image/DeleteImage"; import {VariableUiElement} from "./UI/Base/VariableUIElement";
new DeleteImage("image", new UIEventSource<any>({"image":"url"})).AttachTo("maindiv");
const vtf= ValidatedTextField.KeyInput(true);
vtf.AttachTo('maindiv')
vtf.GetValue().addCallback(console.log)
new VariableUiElement(vtf.GetValue().map(n => ""+n)).AttachTo("extradiv")