diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index 9881c9abf0..6ee3f6a3a9 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -117,11 +117,12 @@ export default class TagRenderingConfig { let placeholder: Translation = Translations.T(json.freeform.placeholder) if (placeholder === undefined) { - const typeDescription = Translations.t.validation[type]?.description + const typeDescription = Translations.t.validation[type]?.description + const key = json.freeform.key; if(typeDescription !== undefined){ - placeholder = Translations.T(json.freeform.key+" ({"+type+"})").Subs({[type]: typeDescription}) + placeholder = typeDescription.OnEveryLanguage(l => key+" ("+l+")") }else{ - placeholder = Translations.T(json.freeform.key+" ("+type+")") + placeholder = Translations.T(key+" ("+type+")") } } diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts index 96b848ca89..3972b4d123 100644 --- a/UI/Base/MinimapImplementation.ts +++ b/UI/Base/MinimapImplementation.ts @@ -156,7 +156,6 @@ export default class MinimapImplementation extends BaseUIElement implements Mini } try { - console.log("SELF IS", self) self.leafletMap?.data?.invalidateSize() } catch (e) { console.warn("Could not invalidate size of a minimap:", e) diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index 44efc2d100..8243023600 100644 --- a/UI/BigComponents/SearchAndGo.ts +++ b/UI/BigComponents/SearchAndGo.ts @@ -7,6 +7,7 @@ import {Geocoding} from "../../Logic/Osm/Geocoding"; import Translations from "../i18n/Translations"; import Hash from "../../Logic/Web/Hash"; import Combine from "../Base/Combine"; +import Locale from "../i18n/Locale"; export default class SearchAndGo extends Combine { constructor(state: { @@ -21,7 +22,7 @@ export default class SearchAndGo extends Combine { Translations.t.general.search.search ); const searchField = new TextField({ - placeholder: new VariableUiElement(placeholder), + placeholder: placeholder.map(tr => tr.textFor(Locale.language.data), [Locale.language]), value: new UIEventSource(""), inputStyle: " background: transparent;\n" + diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index 4bc3f1d010..29ee2894cb 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -1,35 +1,123 @@ import {InputElement} from "./InputElement"; -import Translations from "../i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import {Store, UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; +import {Translation} from "../i18n/Translation"; +import Locale from "../i18n/Locale"; + + +interface TextFieldOptions { + placeholder?: string | Store | Translation, + value?: UIEventSource, + htmlType?: "area" | "text" | "time" | string, + inputMode?: string, + label?: BaseUIElement, + textAreaRows?: number, + inputStyle?: string, + isValid?: (s: string) => boolean +} export class TextField extends InputElement { public readonly enterPressed = new UIEventSource(undefined); private readonly value: UIEventSource; - private _element: HTMLElement; private _actualField : HTMLElement private readonly _isValid: (s: string) => boolean; - private _rawValue: UIEventSource + private readonly _rawValue: UIEventSource private _isFocused = false; + private readonly _options : TextFieldOptions; - constructor(options?: { - placeholder?: string | BaseUIElement, - value?: UIEventSource, - htmlType?: "area" | "text" | "time" | string, - inputMode?: string, - label?: BaseUIElement, - textAreaRows?: number, - inputStyle?: string, - isValid?: (s: string) => boolean - }) { + constructor(options?: TextFieldOptions) { super(); - const self = this; + this._options = options ?? {} options = options ?? {}; this.value = options?.value ?? new UIEventSource(undefined); this._rawValue = new UIEventSource("") this._isValid = options.isValid ?? (_ => true); + } - const placeholder = Translations.W(options.placeholder ?? "").ConstructElement().textContent.replace("'", "'"); + private static SetCursorPosition(textfield: HTMLElement, i: number) { + if (textfield === undefined || textfield === null) { + return; + } + if (i === -1) { + // @ts-ignore + i = textfield.value.length; + } + textfield.focus(); + // @ts-ignore + textfield.setSelectionRange(i, i); + + } + + GetValue(): UIEventSource { + return this.value; + } + + GetRawValue(): UIEventSource{ + return this._rawValue + } + + IsValid(t: string): boolean { + if (t === undefined || t === null) { + return false + } + return this._isValid(t); + } + + private static test(){ + const placeholder = new UIEventSource("placeholder") + const tf = new TextField({ + placeholder + }) + const html = tf.InnerConstructElement().children[0]; + html.placeholder // => 'placeholder' + placeholder.setData("another piece of text") + html.placeholder// => "another piece of text" + } + + + /** + * + * // should update placeholders dynamically + * const placeholder = new UIEventSource("placeholder") + * const tf = new TextField({ + * placeholder + * }) + * const html = tf.InnerConstructElement().children[0]; + * html.placeholder // => 'placeholder' + * placeholder.setData("another piece of text") + * html.placeholder// => "another piece of text" + * + * // should update translated placeholders dynamically + * const placeholder = new Translation({nl: "Nederlands", en: "English"}) + * Locale.language.setData("nl"); + * const tf = new TextField({ + * placeholder + * }) + * const html = tf.InnerConstructElement().children[0]; + * html.placeholder// => "Nederlands" + * Locale.language.setData("en"); + * html.placeholder // => 'English' + */ + protected InnerConstructElement(): HTMLElement { + const options = this._options; + const self = this; + let placeholderStore: Store + let placeholder : string = ""; + if(options.placeholder){ + if(typeof options.placeholder === "string"){ + placeholder = options.placeholder; + placeholderStore = undefined; + }else { + if ((options.placeholder instanceof Store) && options.placeholder["data"] !== undefined) { + placeholderStore = options.placeholder; + } else if ((options.placeholder instanceof Translation) && options.placeholder["translations"] !== undefined) { + placeholderStore = >Locale.language.map(l => (options.placeholder).textFor(l)) + } + placeholder = placeholderStore?.data ?? placeholder ?? ""; + } + } + + this.SetClass("form-text-field") let inputEl: HTMLElement @@ -41,6 +129,9 @@ export class TextField extends InputElement { el.cols = 50 el.style.width = "100%" inputEl = el; + if(placeholderStore){ + placeholderStore.addCallbackAndRunD(placeholder => el.placeholder = placeholder) + } } else { const el = document.createElement("input") el.type = options.htmlType ?? "text" @@ -48,8 +139,13 @@ export class TextField extends InputElement { el.placeholder = placeholder el.style.cssText = options.inputStyle ?? "width: 100%;" inputEl = el + if(placeholderStore){ + placeholderStore.addCallbackAndRunD(placeholder => el.placeholder = placeholder) + } } + + const form = document.createElement("form") form.appendChild(inputEl) form.onsubmit = () => false; @@ -58,7 +154,6 @@ export class TextField extends InputElement { form.appendChild(options.label.ConstructElement()) } - this._element = form; const field = inputEl; @@ -110,47 +205,13 @@ export class TextField extends InputElement { self.enterPressed.setData(field.value); } }); - + if(this._isFocused){ field.focus() } - + this._actualField = field; - - - } - - private static SetCursorPosition(textfield: HTMLElement, i: number) { - if (textfield === undefined || textfield === null) { - return; - } - if (i === -1) { - // @ts-ignore - i = textfield.value.length; - } - textfield.focus(); - // @ts-ignore - textfield.setSelectionRange(i, i); - - } - - GetValue(): UIEventSource { - return this.value; - } - - GetRawValue(): UIEventSource{ - return this._rawValue - } - - IsValid(t: string): boolean { - if (t === undefined || t === null) { - return false - } - return this._isValid(t); - } - - protected InnerConstructElement(): HTMLElement { - return this._element; + return form; } public focus() { diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 748346703f..e2d107ff23 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -70,7 +70,7 @@ export class TextFieldDef { value?: UIEventSource, inputStyle?: string, feedback?: UIEventSource - placeholder?: string | BaseUIElement, + placeholder?: string | Translation | UIEventSource, country?: () => string, location?: [number /*lat*/, number /*lon*/], mapBackgroundLayer?: UIEventSource, diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index 63d58b2b9c..e9988090a9 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -114,29 +114,42 @@ export class Translation extends BaseUIElement { /** * + * // Should actually change the content based on the current language * const tr = new Translation({"en":"English", nl: "Nederlands"}) * Locale.language.setData("en") * const html = tr.InnerConstructElement() * html.innerHTML // => "English" * Locale.language.setData("nl") * html.innerHTML // => "Nederlands" + * + * // Should include a link to weblate if context is set + * const tr = new Translation({"en":"English"}, "core:test.xyz") + * Locale.language.setData("nl") + * Locale.showLinkToWeblate.setData(true) + * const html = tr.InnerConstructElement() + * html.getElementsByTagName("a")[0].href // => "https://hosted.weblate.org/translate/mapcomplete/core/nl/?offset=1&q=context%3A%3D%22test.xyz%22" */ InnerConstructElement(): HTMLElement { const el = document.createElement("span") const self = this + + el.innerHTML = self.txt + if (self.translations["*"] !== undefined) { + return el; + } - - Locale.language.addCallbackAndRun(_ => { + + Locale.language.addCallback(_ => { if (self.isDestroyed) { return true } el.innerHTML = self.txt }) - - if (self.translations["*"] !== undefined || self.context === undefined || self.context?.indexOf(":") < 0) { + + if(self.context === undefined || self.context?.indexOf(":") < 0){ return el; } - + const linkToWeblate = new LinkToWeblate(self.context, self.translations) const wrapper = document.createElement("span") @@ -174,7 +187,10 @@ export class Translation extends BaseUIElement { public AllValues(): string[] { return this.SupportedLanguages().map(lng => this.translations[lng]); } - + + /** + * Constructs a new Translation where every contained string has been modified + */ public OnEveryLanguage(f: (s: string, language: string) => string, context?: string): Translation { const newTranslations = {}; for (const lang in this.translations) { @@ -197,6 +213,7 @@ export class Translation extends BaseUIElement { * const r = tr.replace("{key}", "value") * r.textFor("nl") // => "Een voorbeeldtekst met value en {key1}, en nogmaals value" * r.textFor("en") // => "Just a single value" + * */ public replace(a: string, b: string) { return this.OnEveryLanguage(str => str.replace(new RegExp(a, "g"), b)) @@ -290,6 +307,7 @@ export class TypedTranslation extends Translation { * const subbed = tr.Subs({part: subpart}) * subbed.textFor("en") // => "Full sentence with subpart" * subbed.textFor("nl") // => "Volledige zin met onderdeel" + * */ Subs(text: T, context?: string): Translation { return this.OnEveryLanguage((template, lang) => { diff --git a/test/testhooks.ts b/test/testhooks.ts index ac50825e62..f9ed803ec9 100644 --- a/test/testhooks.ts +++ b/test/testhooks.ts @@ -1,11 +1,13 @@ import ScriptUtils from "../scripts/ScriptUtils"; import {Utils} from "../Utils"; import * as fakedom from "fake-dom" +import Locale from "../UI/i18n/Locale"; export const mochaHooks = { beforeEach(done) { ScriptUtils.fixUtils(); + Locale.language.setData("en") if (fakedom === undefined || window === undefined) { throw "FakeDom not initialized"