diff --git a/UI/CenterMessageBox.ts b/UI/CenterMessageBox.ts index 7f44aa5..0810168 100644 --- a/UI/CenterMessageBox.ts +++ b/UI/CenterMessageBox.ts @@ -1,8 +1,6 @@ import {UIElement} from "./UIElement"; -import {OsmConnection} from "../Logic/Osm/OsmConnection"; import Translations from "./i18n/Translations"; import {State} from "../State"; -import {UIEventSource} from "../Logic/UIEventSource"; export class CenterMessageBox extends UIElement { @@ -16,7 +14,7 @@ export class CenterMessageBox extends UIElement { this.ListenTo(State.state.layerUpdater.sufficentlyZoomed); } - private prep(): { innerHtml: string, done: boolean } { + private static prep(): { innerHtml: string, done: boolean } { if (State.state.centerMessage.data != "") { return {innerHtml: State.state.centerMessage.data, done: false}; } @@ -37,7 +35,7 @@ export class CenterMessageBox extends UIElement { } InnerRender(): string { - return this.prep().innerHtml; + return CenterMessageBox.prep().innerHtml; } @@ -50,7 +48,7 @@ export class CenterMessageBox extends UIElement { } pstyle.pointerEvents = "none"; - if (this.prep().done) { + if (CenterMessageBox.prep().done) { pstyle.opacity = "0"; } else { pstyle.opacity = "0.5"; diff --git a/UI/CustomGenerator/GeneralSettings.ts b/UI/CustomGenerator/GeneralSettings.ts index c26e971..fc11fdf 100644 --- a/UI/CustomGenerator/GeneralSettings.ts +++ b/UI/CustomGenerator/GeneralSettings.ts @@ -6,6 +6,7 @@ import SettingsTable from "./SettingsTable"; import SingleSetting from "./SingleSetting"; import {TextField} from "../Input/TextField"; import MultiLingualTextFields from "../Input/MultiLingualTextFields"; +import ValidatedTextField from "../Input/ValidatedTextField"; export default class GeneralSettingsPanel extends UIElement { @@ -17,15 +18,13 @@ export default class GeneralSettingsPanel extends UIElement { super(undefined); - const languagesField = new TextField( - { - fromString: str => str?.split(";")?.map(str => str.trim().toLowerCase()), - toString: languages => languages.join(";"), - } - ); + const languagesField = + ValidatedTextField.Mapped( + str => str?.split(";")?.map(str => str.trim().toLowerCase()), + languages => languages.join(";")); this.languages = languagesField.GetValue(); - const version = TextField.StringInput(); + const version = new TextField(); 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() version.GetValue().setData(formatted_date); @@ -35,7 +34,7 @@ export default class GeneralSettingsPanel extends UIElement { 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"), new SingleSetting(configuration, version, "version", "Version", "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"), 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"), - 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", { 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), - 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), - 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), - 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" + "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" + "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)", "Only works on incorporated themes" + "The Social Image is set as og:image for the HTML-site and helps social networks to show a preview", {showIconPreview: true}) ], currentSetting); diff --git a/UI/CustomGenerator/LayerPanel.ts b/UI/CustomGenerator/LayerPanel.ts index 563d577..50552d0 100644 --- a/UI/CustomGenerator/LayerPanel.ts +++ b/UI/CustomGenerator/LayerPanel.ts @@ -19,6 +19,7 @@ import PresetInputPanel from "./PresetInputPanel"; import {UserDetails} from "../../Logic/Osm/OsmConnection"; import {State} from "../../State"; import {FixedUiElement} from "../Base/FixedUiElement"; +import ValidatedTextField from "../Input/ValidatedTextField"; /** * Shows the configuration for a single layer @@ -86,17 +87,17 @@ export default class LayerPanel extends UIElement { this.settingsTable = new SettingsTable([ - setting(TextField.StringInput(), "id", "Id", "An identifier for this layer
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
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
Used in the layer control panel and the 'Personal theme'"), setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.
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."), setting(new DropDown("", [ {value: 0, shown: "Show ways and areas as ways and lines"}, {value: 2, shown: "Show both the ways/areas and the centerpoints"}, {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"), - 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.
" + "Now, showing those pieces of forest overlapping with the nature reserve truly clutters the map and is very user-unfriendly.
" + "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."), diff --git a/UI/Input/InputElementMap.ts b/UI/Input/InputElementMap.ts index 5c8e31d..7a0286b 100644 --- a/UI/Input/InputElementMap.ts +++ b/UI/Input/InputElementMap.ts @@ -9,6 +9,7 @@ export default class InputElementMap extends InputElement { private readonly fromX: (x: X) => T; private readonly toX: (t: T) => X; private readonly _value: UIEventSource; + public readonly IsSelected: UIEventSource; constructor(inputElement: InputElement, isSame: (x0: X, x1: X) => boolean, @@ -32,8 +33,7 @@ export default class InputElementMap extends InputElement { } return newX; }), extraSources, x => { - const newT = fromX(x); - return newT; + return fromX(x); }); } @@ -45,10 +45,15 @@ export default class InputElementMap extends InputElement { return this._inputElement.InnerRender(); } - IsSelected: UIEventSource; - 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); } - + } \ No newline at end of file diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index 8871dc4..7eae970 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -2,208 +2,86 @@ import {UIElement} from "../UIElement"; import {InputElement} from "./InputElement"; import Translations from "../i18n/Translations"; 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{ - 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("", 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 extends InputElement { - - public static StringInput(textArea: boolean = false): TextField { - return new TextField({ - toString: str => str, - fromString: str => str, - textArea: textArea - }); - } - - public static KeyInput(allowEmpty : boolean = false): TextField{ - return new TextField({ - 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{ - 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 { private readonly value: UIEventSource; - private readonly mappedValue: UIEventSource; public readonly enterPressed = new UIEventSource(undefined); private readonly _placeholder: UIElement; - private readonly _fromString?: (string: string) => T; - private readonly _toString: (t: T) => string; - private readonly startValidated: boolean; public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly _isArea: boolean; private readonly _textAreaRows: number; - constructor(options: { - /** - * Shown as placeholder - */ + constructor(options?: { placeholder?: string | UIElement, - - /** - * 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, - startValidated?: boolean, + value?: UIEventSource, textArea?: boolean, textAreaRows?: number, + isValid?: ((s: string) => boolean) }) { super(undefined); const self = this; this.value = new UIEventSource(""); - + options = options ?? {}; this._isArea = options.textArea ?? false; - this.startValidated = options.startValidated ?? false; - this.mappedValue = options?.value ?? new UIEventSource(undefined); - this.mappedValue.addCallback(() => self.InnerUpdate()); + this.value = options?.value ?? new UIEventSource(undefined); // @ts-ignore 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._placeholder = Translations.W(options.placeholder ?? ""); this.ListenTo(this._placeholder._source); - this._toString = options.toString ?? ((t) => ("" + t)); this.onClick(() => { self.IsSelected.setData(true) }); - this.mappedValue.addCallback((t) => { - if (t === undefined || t === null) { - return; - } - const field = document.getElementById('text-' + this.id); + this.value.addCallback((t) => { + const field = document.getElementById(this.id); if (field === undefined || field === null) { return; } - // @ts-ignore - field.value = options.toString(t); - }); - } + if (options.isValid) { + field.className = options.isValid(t) ? "" : "invalid"; + } - GetValue(): UIEventSource { - return this.mappedValue; + if (t === undefined || t === null) { + // @ts-ignore + return; + } + // @ts-ignore + field.value = t; + }); } + + GetValue(): UIEventSource { + return this.value; + } + InnerRender(): string { - - if(this._isArea){ - return `` + + if (this._isArea) { + return `` } - + const placeholder = this._placeholder.InnerRender().replace("'", "'"); - + return `
` + - `` + + `` + `
`; } - InnerUpdate() { - 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"; - + InnerUpdate(field) { const self = this; field.oninput = () => { // @ts-ignore 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("focusout", () => self.IsSelected.setData(false)); @@ -215,16 +93,6 @@ export class TextField extends InputElement { } }); - 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) { @@ -232,7 +100,7 @@ export class TextField extends InputElement { if(field === undefined || field === null){ return; } - if(i === -1){ + if (i === -1) { // @ts-ignore i = field.value.length; } @@ -241,12 +109,8 @@ export class TextField extends InputElement { field.setSelectionRange(i, i); } - IsValid(t: T): boolean { - if (t === undefined || t === null) { - return false; - } - const result = this._toString(t); - return result !== undefined && result !== null; + IsValid(t: string): boolean { + return !(t === undefined || t === null); } } diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts new file mode 100644 index 0000000..b222363 --- /dev/null +++ b/UI/Input/ValidatedTextField.ts @@ -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 { + 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("", 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 { + 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 { + + 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("") + }); + + return new InputElementMap(textfield, isSame, fromString, toString); + } + + + + static Mapped(fromString: (str) => T, toString: (T) => string, options?: { + placeholder?: string | UIElement, + value?: UIEventSource, + startValidated?: boolean, + textArea?: boolean, + textAreaRows?: number, + isValid?: ((string: string) => boolean) + }): InputElement { + const textField = new TextField(options); + + return new InputElementMap( + textField, (a, b) => a === b, + fromString, toString + ); + + } +} \ No newline at end of file diff --git a/UI/TagRendering.ts b/UI/TagRendering.ts index 31a9773..d5cefbf 100644 --- a/UI/TagRendering.ts +++ b/UI/TagRendering.ts @@ -14,9 +14,9 @@ import {InputElement} from "./Input/InputElement"; import {SaveButton} from "./SaveButton"; import {RadioButton} from "./Input/RadioButton"; import {FixedInputElement} from "./Input/FixedInputElement"; -import {TextField, ValidatedTextField} from "./Input/TextField"; import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; import {FixedUiElement} from "./Base/FixedUiElement"; +import ValidatedTextField from "./Input/ValidatedTextField"; export class TagRendering extends UIElement implements TagDependantUIElement { @@ -202,7 +202,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { InputElement { - let freeformElement: TextField = undefined; + let freeformElement: InputElement = undefined; if (options.freeform !== undefined) { freeformElement = this.InputForFreeForm(options.freeform); } @@ -278,7 +278,6 @@ export class TagRendering extends UIElement implements TagDependantUIElement { es.data.push(i); es.ping(); } - freeformElement.SetCursorPosition(-1); }); return inputEl; @@ -305,7 +304,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { renderTemplate: string | Translation, placeholder?: string | Translation, extraTags?: TagsFilter, - }): TextField { + }): InputElement { if (freeform?.template === undefined) { return undefined; } @@ -313,13 +312,24 @@ export class TagRendering extends UIElement implements TagDependantUIElement { const prepost = Translations.W(freeform.template).InnerRender() .replace("$$$", "$string$") .split("$"); - const type = prepost[1]; + let type = prepost[1]; + + 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.inputValidation[type]; + let isValid = ValidatedTextField.AllTypes[type].isValid; if (isValid === undefined) { isValid = () => true; } - let formatter = ValidatedTextField.formatting[type] ?? ((str) => str); + let formatter = ValidatedTextField.AllTypes[type].reformat ?? ((str) => str); const pickString = (string: any) => { @@ -361,12 +371,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement { } return undefined; } - - return new TextField({ - placeholder: this._freeform.placeholder, - fromString: pickString, - toString: toString, - }); + return ValidatedTextField.Mapped( + pickString, toString, {placeholder: this._freeform.placeholder, isValid: isValid, textArea: isTextArea} + ) } diff --git a/assets/themes/bookcases/Bookcases.json b/assets/themes/bookcases/Bookcases.json index 1977ae7..9728bbb 100644 --- a/assets/themes/bookcases/Bookcases.json +++ b/assets/themes/bookcases/Bookcases.json @@ -230,7 +230,7 @@ "de": "Betrieben von {operator}" }, "freeform": { - "type": "text", + "type": "string", "key": "operator" } }, diff --git a/test.ts b/test.ts index feea3a4..84de395 100644 --- a/test.ts +++ b/test.ts @@ -1,4 +1,8 @@ -import {UIEventSource} from "./Logic/UIEventSource"; -import DeleteImage from "./UI/Image/DeleteImage"; +import ValidatedTextField from "./UI/Input/ValidatedTextField"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; -new DeleteImage("image", new UIEventSource({"image":"url"})).AttachTo("maindiv"); \ No newline at end of file + +const vtf= ValidatedTextField.KeyInput(true); +vtf.AttachTo('maindiv') +vtf.GetValue().addCallback(console.log) +new VariableUiElement(vtf.GetValue().map(n => ""+n)).AttachTo("extradiv") \ No newline at end of file