diff --git a/Logic/Tags.ts b/Logic/Tags.ts index 307ea06..b7d54cb 100644 --- a/Logic/Tags.ts +++ b/Logic/Tags.ts @@ -95,10 +95,10 @@ export class Tag extends TagsFilter { this.key = key this.value = value if(key === undefined || key === ""){ - throw "Invalid key"; + throw "Invalid key: undefined or empty"; } if(value === undefined){ - throw "Invalid value"; + throw "Invalid value: value is undefined"; } if(value === "*"){ console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`) diff --git a/UI/Input/CombinedInputElement.ts b/UI/Input/CombinedInputElement.ts new file mode 100644 index 0000000..b15c0a5 --- /dev/null +++ b/UI/Input/CombinedInputElement.ts @@ -0,0 +1,35 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import {UIElement} from "../UIElement"; + +export default class CombinedInputElement extends InputElement { + private readonly _a: InputElement; + private readonly _b: UIElement; + private readonly _combined: UIElement; + public readonly IsSelected: UIEventSource; + + constructor(a: InputElement, b: InputElement) { + super(); + this._a = a; + this._b = b; + this.IsSelected = this._a.IsSelected.map((isSelected) => { + return isSelected || b.IsSelected.data + }, [b.IsSelected]) + this._combined = new Combine([this._a, this._b]); + } + + GetValue(): UIEventSource { + return this._a.GetValue(); + } + + InnerRender(): string { + return this._combined.Render(); + } + + + IsValid(t: T): boolean { + return this._a.IsValid(t); + } + +} \ No newline at end of file diff --git a/UI/Input/SimpleDatePicker.ts b/UI/Input/SimpleDatePicker.ts new file mode 100644 index 0000000..3a6b203 --- /dev/null +++ b/UI/Input/SimpleDatePicker.ts @@ -0,0 +1,60 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; + +export default class SimpleDatePicker extends InputElement { + + private readonly value: UIEventSource + + constructor( + value?: UIEventSource + ) { + super(); + this.value = value ?? new UIEventSource(undefined); + const self = this; + this.value.addCallbackAndRun(v => { + if(v === undefined){ + return; + } + self.SetValue(v); + }); + } + + + InnerRender(): string { + return ``; + } + + private SetValue(date: string){ + const field = document.getElementById("date-" + this.id); + if (field === undefined || field === null) { + return; + } + // @ts-ignore + field.value = date; + } + + protected InnerUpdate() { + const field = document.getElementById("date-" + this.id); + if (field === undefined || field === null) { + return; + } + const self = this; + field.oninput = () => { + // Already in YYYY-MM-DD value! + // @ts-ignore + self.value.setData(field.value); + } + + } + + GetValue(): UIEventSource { + return this.value; + } + + IsSelected: UIEventSource = new UIEventSource(false); + + IsValid(t: string): boolean { + return false; + } + +} \ No newline at end of file diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index 1d75292..6a20faa 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -10,7 +10,9 @@ export class TextField extends InputElement { public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly _isArea: boolean; private readonly _textAreaRows: number; - + + private readonly _isValid: (string) => boolean; + constructor(options?: { placeholder?: string | UIElement, value?: UIEventSource, @@ -25,9 +27,8 @@ export class TextField extends InputElement { this._isArea = options.textArea ?? false; this.value = options?.value ?? new UIEventSource(undefined); - // @ts-ignore - this._fromString = options.fromString ?? ((str) => (str)) this._textAreaRows = options.textAreaRows; + this._isValid = options.isValid ?? ((str) => true); this._placeholder = Translations.W(options.placeholder ?? ""); this.ListenTo(this._placeholder._source); @@ -36,16 +37,13 @@ export class TextField extends InputElement { self.IsSelected.setData(true) }); this.value.addCallback((t) => { - const field = document.getElementById(this.id); + const field = document.getElementById("txt-"+this.id); if (field === undefined || field === null) { return; } - if (options.isValid) { - field.className = options.isValid(t) ? "" : "invalid"; - } + field.className = self.IsValid(t) ? "" : "invalid"; if (t === undefined || t === null) { - // @ts-ignore return; } // @ts-ignore @@ -77,11 +75,18 @@ export class TextField extends InputElement { const self = this; field.oninput = () => { // @ts-ignore - var endDistance = field.value.length - field.selectionEnd; + const endDistance = field.value.length - field.selectionEnd; // @ts-ignore - self.value.setData(field.value); + const val: string = field.value; + if (!self.IsValid(val)) { + self.value.setData(undefined); + } else { + self.value.setData(val); + } // Setting the value might cause the value to be set again. We keep the distance _to the end_ stable, as phone number formatting might cause the start to change // See https://github.com/pietervdvn/MapComplete/issues/103 + // We reread the field value - it might have changed! + // @ts-ignore self.SetCursorPosition(field.value.length - endDistance); }; @@ -119,7 +124,10 @@ export class TextField extends InputElement { } IsValid(t: string): boolean { - return !(t === undefined || t === null); + if (t === undefined || t === null) { + return false + } + return this._isValid(t); } } diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 9f2c1df..d6659d0 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -6,6 +6,16 @@ import {InputElement} from "./InputElement"; import {TextField} from "./TextField"; import {UIElement} from "../UIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; +import CombinedInputElement from "./CombinedInputElement"; +import SimpleDatePicker from "./SimpleDatePicker"; + +interface TextFieldDef { + name: string, + explanation: string, + isValid: ((s: string, country?: string) => boolean), + reformat?: ((s: string, country?: string) => string), + inputHelper?: (value:UIEventSource) => InputElement +} export default class ValidatedTextField { @@ -13,12 +23,8 @@ 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) - } { + reformat?: ((s: string, country?: string) => string), + inputHelper?: (value: UIEventSource) => InputElement): TextFieldDef { if (isValid === undefined) { isValid = () => true; @@ -33,17 +39,36 @@ export default class ValidatedTextField { name: name, explanation: explanation, isValid: isValid, - reformat: reformat + reformat: reformat, + inputHelper: inputHelper } } - public static tpList = [ + public static tpList: TextFieldDef[] = [ ValidatedTextField.tp( "string", "A basic string"), ValidatedTextField.tp( "date", - "A date"), + "A date", + (str) => { + const time = Date.parse(str); + return !isNaN(time); + }, + (str) => { + const d = new Date(str); + let month = '' + (d.getMonth() + 1); + let day = '' + d.getDate(); + const year = d.getFullYear(); + + if (month.length < 2) + month = '0' + month; + if (day.length < 2) + day = '0' + day; + + return [year, month, day].join('-'); + }, + (value) => new SimpleDatePicker(value)), ValidatedTextField.tp( "wikidata", "A wikidata identifier, e.g. Q42"), @@ -82,7 +107,30 @@ export default class ValidatedTextField { (str) => EmailValidator.validate(str)), ValidatedTextField.tp( "url", - "A url"), + "A url", + (str) => { + try { + new URL(str); + return true; + } catch (e) { + return false; + } + }, (str) => { + try { + const url = new URL(str); + const blacklistedTrackingParams = [ + "fbclid",// Oh god, how I hate the fbclid. Let it burn, burn in hell! + "gclid", + "cmpid", "agid", "utm", "utm_source"] + for (const dontLike of blacklistedTrackingParams) { + url.searchParams.delete(dontLike) + } + return url.toString(); + } catch (e) { + console.error(e) + return undefined; + } + }), ValidatedTextField.tp( "phone", "A phone number", @@ -114,15 +162,33 @@ export default class ValidatedTextField { } 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 InputForType(type: string, options?: { + placeholder?: string | UIElement, + value?: UIEventSource, + textArea?: boolean, + textAreaRows?: number, + isValid?: ((s: string) => boolean) + }): InputElement { + options = options ?? {}; + options.placeholder = options.placeholder ?? type; + const tp: TextFieldDef = ValidatedTextField.AllTypes[type] + let isValid = tp.isValid; + if (options.isValid) { + const optValid = options.isValid; + isValid = (str, country) => { + return ValidatedTextField.AllTypes[type](str, country) && optValid(str); + } + } + options.isValid = isValid; + + let input: InputElement = new TextField(options); + if (tp.inputHelper) { + input = new CombinedInputElement(input, tp.inputHelper(input.GetValue())); + } + return input; } public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined): InputElement { @@ -181,13 +247,19 @@ export default class ValidatedTextField { static Mapped(fromString: (str) => T, toString: (T) => string, options?: { placeholder?: string | UIElement, + type?: string, value?: UIEventSource, startValidated?: boolean, textArea?: boolean, textAreaRows?: number, isValid?: ((string: string) => boolean) }): InputElement { - const textField = new TextField(options); + let textField: InputElement; + if (options.type) { + textField = ValidatedTextField.InputForType(options.type); + } else { + textField = new TextField(options); + } return new InputElementMap( textField, (a, b) => a === b, fromString, toString diff --git a/UI/TagRendering.ts b/UI/TagRendering.ts index d2ef826..71cd2a5 100644 --- a/UI/TagRendering.ts +++ b/UI/TagRendering.ts @@ -325,10 +325,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement { throw "Unkown type: "+type; } - let isValid = ValidatedTextField.AllTypes[type].isValid; - if (isValid === undefined) { - isValid = () => true; - } + let formatter = ValidatedTextField.AllTypes[type].reformat ?? ((str) => str); const pickString = @@ -336,17 +333,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement { if (string === "" || string === undefined) { return undefined; } - if (!isValid(string, this._source.data._country)) { - return undefined; - } - const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); - if (tag.value.length > 255) { - return undefined; // Too long - } - if (freeform.extraTags === undefined) { return tag; } @@ -374,7 +363,8 @@ export class TagRendering extends UIElement implements TagDependantUIElement { return ValidatedTextField.Mapped(pickString, toString, { placeholder: this._freeform.placeholder, - isValid: isValid, + type: type, + isValid: (str) => (str.length <= 255), textArea: isTextArea }) } diff --git a/UI/UserBadge.ts b/UI/UserBadge.ts index db91c58..d3ea22f 100644 --- a/UI/UserBadge.ts +++ b/UI/UserBadge.ts @@ -83,7 +83,7 @@ export class UserBadge extends UIElement { let dryrun = ""; if (user.dryRun) { - dryrun = " TESTING"; + dryrun = new FixedUiElement("TESTING").SetClass("alert").Render(); } if (user.home !== undefined) { @@ -98,7 +98,7 @@ export class UserBadge extends UIElement { const settings = "" + "settings" + - " "; + ""; const userIcon = "profile-pic"; diff --git a/test.ts b/test.ts index 84de395..beb7711 100644 --- a/test.ts +++ b/test.ts @@ -1,8 +1,8 @@ -import ValidatedTextField from "./UI/Input/ValidatedTextField"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import SimpleDatePicker from "./UI/Input/SimpleDatePicker"; -const vtf= ValidatedTextField.KeyInput(true); +const vtf=new SimpleDatePicker(); vtf.AttachTo('maindiv') vtf.GetValue().addCallback(console.log) new VariableUiElement(vtf.GetValue().map(n => ""+n)).AttachTo("extradiv") \ No newline at end of file