diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 0fe05d121..be2adc52d 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -12,12 +12,12 @@ import Combine from "../../UI/Base/Combine"; import {VariableUiElement} from "../../UI/Base/VariableUIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import {FixedUiElement} from "../../UI/Base/FixedUiElement"; -import {UIElement} from "../../UI/UIElement"; import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation"; import SourceConfig from "./SourceConfig"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {Tag} from "../../Logic/Tags/Tag"; import SubstitutingTag from "../../Logic/Tags/SubstitutingTag"; +import BaseUIElement from "../../UI/BaseUIElement"; export default class LayerConfig { @@ -294,7 +294,7 @@ export default class LayerConfig { { icon: { - html: UIElement, + html: BaseUIElement, iconSize: [number, number], iconAnchor: [number, number], popupAnchor: [number, number], @@ -361,7 +361,7 @@ export default class LayerConfig { const iconUrlStatic = render(this.icon); const self = this; const mappedHtml = tags.map(tgs => { - function genHtmlFromString(sourcePart: string): UIElement { + function genHtmlFromString(sourcePart: string): BaseUIElement { if (sourcePart.indexOf("html:") == 0) { // We use § as a replacement for ; const html = sourcePart.substring("html:".length) @@ -370,7 +370,7 @@ export default class LayerConfig { } const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; - let html: UIElement = new FixedUiElement(``); + let html: BaseUIElement = new FixedUiElement(``); const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/) if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { html = new Combine([ @@ -387,7 +387,7 @@ export default class LayerConfig { const iconUrl = render(self.icon); const rotation = render(self.rotation, "0deg"); - let htmlParts: UIElement[] = []; + let htmlParts: BaseUIElement[] = []; let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != "")); for (const sourcePart of sourceParts) { htmlParts.push(genHtmlFromString(sourcePart)) @@ -399,7 +399,7 @@ export default class LayerConfig { continue; } if (iconOverlay.badge) { - const badgeParts: UIElement[] = []; + const badgeParts: BaseUIElement[] = []; const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != ""); for (const badgePartStr of partDefs) { @@ -437,7 +437,7 @@ export default class LayerConfig { } catch (e) { console.error(e, tgs) } - return new Combine(htmlParts).Render(); + return new Combine(htmlParts); }) diff --git a/InitUiElements.ts b/InitUiElements.ts index 45f048068..e8386d7c0 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -1,5 +1,5 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import CheckBox from "./UI/Input/CheckBox"; +import Toggle from "./UI/Input/Toggle"; import {Basemap} from "./UI/BigComponents/Basemap"; import State from "./State"; import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource"; @@ -272,7 +272,7 @@ export class InitUiElements { // ?-Button on Desktop, opens panel with close-X. const help = new MapControlButton(Svg.help_svg()); - new CheckBox( + new Toggle( fullOptions .SetClass("welcomeMessage") .onClick(() => {/*Catch the click*/ @@ -307,7 +307,7 @@ export class InitUiElements { ) ; - const copyrightButton = new CheckBox( + const copyrightButton = new Toggle( copyrightNotice, new MapControlButton(Svg.osm_copyright_svg()), copyrightNotice.isShown @@ -316,13 +316,13 @@ export class InitUiElements { const layerControlPanel = new LayerControlPanel( State.state.layerControlIsOpened) .SetClass("block p-1 rounded-full"); - const layerControlButton = new CheckBox( + const layerControlButton = new Toggle( layerControlPanel, new MapControlButton(Svg.layers_svg()), State.state.layerControlIsOpened ) - const layerControl = new CheckBox( + const layerControl = new Toggle( layerControlButton, "", State.state.featureSwitchLayers diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 772f57304..90d6cec31 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -183,7 +183,6 @@ export default class GeoLocationHandler extends UIElement { self.StartGeolocating(false); } - this.HideOnEmpty(true); } private locate() { diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index f03961ae1..adb03a188 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -21,7 +21,6 @@ class TitleElement extends UIElement { this._allElementsStorage = allElementsStorage; this.ListenTo(Locale.language); this.ListenTo(this._selectedFeature) - this.dumbMode = false; } InnerRender(): string { @@ -63,7 +62,7 @@ export default class TitleHandler { selectedFeature.addCallbackAndRun(_ => { const title = new TitleElement(layoutToUse, selectedFeature, allElementsStorage) const d = document.createElement('div'); - d.innerHTML = title.InnerRender(); + d.innerHTML = title.InnerRenderAsString(); // We pass everything into a div to strip out images etc... document.title = (d.textContent || d.innerText); }) diff --git a/UI/Base/Combine.ts b/UI/Base/Combine.ts index 827e30e3f..9bf1cf950 100644 --- a/UI/Base/Combine.ts +++ b/UI/Base/Combine.ts @@ -1,11 +1,11 @@ -import {UIElement} from "../UIElement"; import {FixedUiElement} from "./FixedUiElement"; import {Utils} from "../../Utils"; +import BaseUIElement from "../BaseUIElement"; -export default class Combine extends UIElement { - private readonly uiElements: UIElement[]; +export default class Combine extends BaseUIElement { + private readonly uiElements: BaseUIElement[]; - constructor(uiElements: (string | UIElement)[]) { + constructor(uiElements: (string | BaseUIElement)[]) { super(); this.uiElements = Utils.NoNull(uiElements) .map(el => { @@ -15,18 +15,21 @@ export default class Combine extends UIElement { return el; }); } + + protected InnerConstructElement(): HTMLElement { + const el = document.createElement("span") - InnerRender(): string { - return this.uiElements.map(ui => { - if(ui === undefined || ui === null){ - return ""; + for (const subEl of this.uiElements) { + if(subEl === undefined || subEl === null){ + continue; } - if(ui.Render === undefined){ - console.error("Not a UI-element", ui); - return ""; + const subHtml = subEl.ConstructElement() + if(subHtml !== undefined){ + el.appendChild(subHtml) } - return ui.Render(); - }).join(""); + } + + return el; } - + } \ No newline at end of file diff --git a/UI/Base/FeatureSwitched.ts b/UI/Base/FeatureSwitched.ts index 6ff095a8a..7641a4801 100644 --- a/UI/Base/FeatureSwitched.ts +++ b/UI/Base/FeatureSwitched.ts @@ -12,11 +12,11 @@ export default class FeatureSwitched extends UIElement{ this._swtch = swtch; } - InnerRender(): string { + InnerRender(): UIElement | string { if(this._swtch.data){ return this._upstream.Render(); } - return ""; + return undefined; } } \ No newline at end of file diff --git a/UI/Base/FixedUiElement.ts b/UI/Base/FixedUiElement.ts index a0941e5ec..e27afb3a1 100644 --- a/UI/Base/FixedUiElement.ts +++ b/UI/Base/FixedUiElement.ts @@ -7,7 +7,7 @@ export class FixedUiElement extends UIElement { super(undefined); this._html = html ?? ""; } - + InnerRender(): string { return this._html; } diff --git a/UI/Base/Img.ts b/UI/Base/Img.ts index 76d00c59b..372ee7e03 100644 --- a/UI/Base/Img.ts +++ b/UI/Base/Img.ts @@ -1,19 +1,29 @@ -import Constants from "../../Models/Constants"; import {Utils} from "../../Utils"; +import BaseUIElement from "../BaseUIElement"; -export default class Img { +export default class Img extends BaseUIElement { + private _src: string; - public static runningFromConsole = false; + constructor(src: string) { + super(); + this._src = src; + } - static AsData(source:string){ - if(Utils.runningFromConsole){ - return source; - } - return `data:image/svg+xml;base64,${(btoa(source))}`; - } + static AsData(source: string) { + if (Utils.runningFromConsole) { + return source; + } + return `data:image/svg+xml;base64,${(btoa(source))}`; + } - static AsImageElement(source: string, css_class: string = "", style=""): string{ + static AsImageElement(source: string, css_class: string = "", style = ""): string { return ``; } + + protected InnerConstructElement(): HTMLElement { + const el = document.createElement("img") + el.src = this._src; + return el; + } } diff --git a/UI/Base/Link.ts b/UI/Base/Link.ts index dbe164c1e..d63a62011 100644 --- a/UI/Base/Link.ts +++ b/UI/Base/Link.ts @@ -1,24 +1,35 @@ -import {UIElement} from "../UIElement"; import Translations from "../i18n/Translations"; +import BaseUIElement from "../BaseUIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; -export default class Link extends UIElement { - private readonly _embeddedShow: UIElement; - private readonly _target: string; - private readonly _newTab: string; +export default class Link extends BaseUIElement { + private readonly _element: HTMLElement; - constructor(embeddedShow: UIElement | string, target: string, newTab: boolean = false) { + constructor(embeddedShow: BaseUIElement | string, target: string | UIEventSource, newTab: boolean = false) { super(); - this._embeddedShow = Translations.W(embeddedShow); - this._target = target; - this._newTab = ""; - if (newTab) { - this._newTab = "target='_blank'" + const _embeddedShow = Translations.W(embeddedShow); + + + const el = document.createElement("a") + + if(typeof target === "string"){ + el.href = target + }else{ + target.addCallbackAndRun(target => { + el.target = target; + }) } + if (newTab) { + el.target = "_blank" + } + el.appendChild(_embeddedShow.ConstructElement()) + this._element = el } - InnerRender(): string { - return `${this._embeddedShow.Render()}`; + protected InnerConstructElement(): HTMLElement { + + return this._element; } } \ No newline at end of file diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index b6ef10814..3f635ba67 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -7,7 +7,13 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import Hash from "../../Logic/Web/Hash"; /** - * Wraps some contents into a panel that scrolls the content _under_ the title + * + * The scrollableFullScreen is a bit of a peculiar component: + * - It shows a title and some contents, constructed from the respective functions passed into the constructor + * - When the element is 'activated', one clone of title+contents is attached to the fullscreen + * - The element itself will - upon rendering - also show the title and contents (allthough it'll be a different clone) + * + * */ export default class ScrollableFullScreen extends UIElement { private static readonly empty = new FixedUiElement(""); @@ -40,8 +46,8 @@ export default class ScrollableFullScreen extends UIElement { }) } - InnerRender(): string { - return this._component.Render(); + InnerRender(): UIElement { + return this._component; } Activate(): void { diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index 4a2f688d9..42636d431 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -1,55 +1,51 @@ -import {UIElement} from "../UIElement"; import Translations from "../i18n/Translations"; import Combine from "./Combine"; -import {FixedUiElement} from "./FixedUiElement"; +import BaseUIElement from "../BaseUIElement"; +import Link from "./Link"; +import Img from "./Img"; +import {UIEventSource} from "../../Logic/UIEventSource"; export class SubtleButton extends Combine { - constructor(imageUrl: string | UIElement, message: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined) { + constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, linkTo: { url: string | UIEventSource, newTab?: boolean } = undefined) { super(SubtleButton.generateContent(imageUrl, message, linkTo)); this.SetClass("block flex p-3 my-2 bg-blue-100 rounded-lg hover:shadow-xl hover:bg-blue-200 link-no-underline") } - private static generateContent(imageUrl: string | UIElement, messageT: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined): (UIElement | string)[] { + private static generateContent(imageUrl: string | BaseUIElement, messageT: string | BaseUIElement, linkTo: { url: string | UIEventSource, newTab?: boolean } = undefined): (BaseUIElement )[] { const message = Translations.W(messageT); - if (message !== null) { - message.dumbMode = false; - } let img; if ((imageUrl ?? "") === "") { - img = new FixedUiElement(""); + img = undefined; } else if (typeof (imageUrl) === "string") { - img = new FixedUiElement(``); + img = new Img(imageUrl).SetClass("w-full") } else { img = imageUrl; } - img.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0") + img?.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0") const image = new Combine([img]) .SetClass("flex-shrink-0"); - - - if (message !== null && message.IsEmpty()) { - // Message == null: special case to force empty text - return []; - } - - if (linkTo != undefined) { + + if (linkTo == undefined) { return [ - ``, image, - `
`, message, - `
`, - `
` ]; } - + + return [ - image, - message, + new Link( + new Combine([ + image, + message?.SetClass("block ml-4 overflow-ellipsis") + ]).SetClass("flex group"), + linkTo.url, + linkTo.newTab ?? false + ) ]; } diff --git a/UI/Base/TabbedComponent.ts b/UI/Base/TabbedComponent.ts index 308d34d5a..260317eb0 100644 --- a/UI/Base/TabbedComponent.ts +++ b/UI/Base/TabbedComponent.ts @@ -1,39 +1,41 @@ import {UIElement} from "../UIElement"; import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "./Combine"; export class TabbedComponent extends UIElement { - private headers: UIElement[] = []; + private readonly header: UIElement; private content: UIElement[] = []; constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab: (UIEventSource | number) = 0) { super(typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource(0))); const self = this; + const tabs: UIElement[] = [] + for (let i = 0; i < elements.length; i++) { let element = elements[i]; - this.headers.push(Translations.W(element.header).onClick(() => self._source.setData(i))); + const header = Translations.W(element.header).onClick(() => self._source.setData(i)) const content = Translations.W(element.content) this.content.push(content); - } - } - - InnerRender(): string { - let headerBar = ""; - for (let i = 0; i < this.headers.length; i++) { - let header = this.headers[i]; - if (!this.content[i].IsEmpty()) { - headerBar += `
` + - header.Render() + "
" + const tab = header.SetClass("block tab-single-header") + tabs.push(tab) } } + this.header = new Combine(tabs).SetClass("block tabs-header-bar") - headerBar = "
" + headerBar + "
" + + } + + InnerRender(): UIElement { const content = this.content[this._source.data]; - return headerBar + "
" + (content?.Render() ?? "") + "
"; + return new Combine([ + this.header, + content.SetClass("tab-content"), + ]) } } \ No newline at end of file diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index 6e5e246ef..2d813a0aa 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -1,16 +1,35 @@ -import {UIElement} from "../UIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; -export class VariableUiElement extends UIElement { - private _html: UIEventSource; +export class VariableUiElement extends BaseUIElement { - constructor(html: UIEventSource) { - super(html); - this._html = html; + private _element : HTMLElement; + + constructor(contents: UIEventSource) { + super(); + + this._element = document.createElement("span") + const el = this._element + contents.addCallbackAndRun(contents => { + while(el.firstChild){ + el.removeChild( + el.lastChild + ) + } + + if(contents === undefined){ + return + } + if(typeof contents === "string"){ + el.innerHTML = contents + }else{ + el.appendChild(contents.ConstructElement()) + } + }) } - InnerRender(): string { - return this._html.data; + protected InnerConstructElement(): HTMLElement { + return this._element; } } \ No newline at end of file diff --git a/UI/Base/VerticalCombine.ts b/UI/Base/VerticalCombine.ts deleted file mode 100644 index c83b1992b..000000000 --- a/UI/Base/VerticalCombine.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {UIElement} from "../UIElement"; - -export class VerticalCombine extends UIElement { - private readonly _elements: UIElement[]; - - constructor(elements: UIElement[]) { - super(undefined); - this._elements = elements; - } - - InnerRender(): string { - let html = ""; - for (const element of this._elements) { - if (element !== undefined && !element.IsEmpty()) { - html += "
" + element.Render() + "
"; - } - } - return html; - } -} \ No newline at end of file diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts new file mode 100644 index 000000000..aecf6695c --- /dev/null +++ b/UI/BaseUIElement.ts @@ -0,0 +1,154 @@ +import {Utils} from "../Utils"; +import {UIEventSource} from "../Logic/UIEventSource"; + +/** + * A thin wrapper around a html element, which allows to generate a HTML-element. + * + * Assumes a read-only configuration, so it has no 'ListenTo' + */ +export default abstract class BaseUIElement { + + private clss: Set = new Set(); + private style: string; + private _onClick: () => void; + private _onHover: UIEventSource; + + protected _constructedHtmlElement: HTMLElement; + + + protected abstract InnerConstructElement(): HTMLElement; + + public onClick(f: (() => void)) { + this._onClick = f; + this.SetClass("clickable") + if(this._constructedHtmlElement !== undefined){ + this._constructedHtmlElement.onclick = f; + } + return this; + } + + public IsHovered(): UIEventSource { + if (this._onHover !== undefined) { + return this._onHover; + } + // Note: we just save it. 'Update' will register that an eventsource exist and install the necessary hooks + this._onHover = new UIEventSource(false); + return this._onHover; + } + + + AttachTo(divId: string) { + let element = document.getElementById(divId); + if (element === null) { + throw "SEVERE: could not attach UIElement to " + divId; + } + + while (element.firstChild) { + //The list is LIVE so it will re-index each call + element.removeChild(element.firstChild); + } + const el = this.ConstructElement(); + if(el !== undefined){ + element.appendChild(el) + } + + return this; + } + /** + * Adds all the relevant classes, space seperated + * @param clss + * @constructor + */ + public SetClass(clss: string) { + const all = clss.split(" ").map(clsName => clsName.trim()); + let recordedChange = false; + for (const c of all) { + if (this.clss.has(clss)) { + continue; + } + this.clss.add(c); + recordedChange = true; + } + if (recordedChange) { + this._constructedHtmlElement?.classList.add(...Array.from(this.clss)); + } + return this; + } + + public RemoveClass(clss: string): BaseUIElement { + if (this.clss.has(clss)) { + this.clss.delete(clss); + this._constructedHtmlElement?.classList.remove(clss) + } + return this; + } + + public SetStyle(style: string): BaseUIElement { + this.style = style; + if(this._constructedHtmlElement !== undefined){ + this._constructedHtmlElement.style.cssText = style; + } + return this; + } + /** + * The same as 'Render', but creates a HTML element instead of the HTML representation + */ + public ConstructElement(): HTMLElement { + if (Utils.runningFromConsole) { + return undefined; + } + + if (this._constructedHtmlElement !== undefined) { + return this._constructedHtmlElement + } + + + const el = this.InnerConstructElement(); + + if(el === undefined){ + return undefined; + } + + this._constructedHtmlElement = el; + const style = this.style + if (style !== undefined && style !== "") { + el.style.cssText = style + } + if (this.clss.size > 0) { + try{ + el.classList.add(...Array.from(this.clss)) + }catch(e){ + console.error("Invalid class name detected in:", Array.from(this.clss).join(" "),"\nErr msg is ",e) + } + } + + if (this._onClick !== undefined) { + const self = this; + el.onclick = (e) => { + // @ts-ignore + if (e.consumed) { + return; + } + self._onClick(); + // @ts-ignore + e.consumed = true; + } + el.style.pointerEvents = "all"; + el.style.cursor = "pointer"; + } + + if (this._onHover !== undefined) { + const self = this; + el.addEventListener('mouseover', () => self._onHover.setData(true)); + el.addEventListener('mouseout', () => self._onHover.setData(false)); + } + + if (this._onHover !== undefined) { + const self = this; + el.addEventListener('mouseover', () => self._onHover.setData(true)); + el.addEventListener('mouseout', () => self._onHover.setData(false)); + } + + return el + } +} \ No newline at end of file diff --git a/UI/BigComponents/AttributionPanel.ts b/UI/BigComponents/AttributionPanel.ts index 760ec9b85..6b3b830ab 100644 --- a/UI/BigComponents/AttributionPanel.ts +++ b/UI/BigComponents/AttributionPanel.ts @@ -44,16 +44,14 @@ export default class AttributionPanel extends Combine { const contribs = links.join(", ") if (hiddenCount == 0) { - - return Translations.t.general.attribution.mapContributionsBy.Subs({ contributors: contribs - }).InnerRender() + }) } else { return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({ contributors: contribs, hiddenCount: hiddenCount - }).InnerRender(); + }); } diff --git a/UI/BigComponents/BackgroundSelector.ts b/UI/BigComponents/BackgroundSelector.ts index c04eba35d..a55ddcab9 100644 --- a/UI/BigComponents/BackgroundSelector.ts +++ b/UI/BigComponents/BackgroundSelector.ts @@ -4,10 +4,11 @@ import Translations from "../i18n/Translations"; import State from "../../State"; import {UIEventSource} from "../../Logic/UIEventSource"; import BaseLayer from "../../Models/BaseLayer"; +import BaseUIElement from "../BaseUIElement"; export default class BackgroundSelector extends UIElement { - private _dropdown: UIElement; + private _dropdown: BaseUIElement; private readonly _availableLayers: UIEventSource; constructor() { @@ -31,8 +32,8 @@ export default class BackgroundSelector extends UIElement { this._dropdown = new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer); } - InnerRender(): string { - return this._dropdown.Render(); + InnerRender(): BaseUIElement { + return this._dropdown; } } \ No newline at end of file diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 4622167bd..b521d0709 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -80,8 +80,8 @@ export default class FullWelcomePaneWithTabs extends UIElement { .ListenTo(userDetails); } - InnerRender(): string { - return this._component.Render(); + InnerRender(): UIElement { + return this._component; } diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index 0b75a3ac2..d92fb5b0a 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -2,7 +2,7 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import {UIElement} from "../UIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import State from "../../State"; -import CheckBox from "../Input/CheckBox"; +import Toggle from "../Input/Toggle"; import Combine from "../Base/Combine"; import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; @@ -65,7 +65,7 @@ export default class LayerSelection extends UIElement { })) const style = "display:flex;align-items:center;" const styleWhole = "display:flex; flex-wrap: wrap" - this._checkboxes.push(new CheckBox( + this._checkboxes.push(new Toggle( new Combine([new Combine([icon, name]).SetStyle(style), zoomStatus]) .SetStyle(styleWhole), new Combine([new Combine([iconUnselected, "", name, ""]).SetStyle(style), zoomStatus]) diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index f5b4eaecd..aee0e00b4 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; @@ -11,87 +10,93 @@ import * as personal from "../../assets/themes/personalLayout/personalLayout.jso import Constants from "../../Models/Constants"; import LanguagePicker from "../LanguagePicker"; import IndexText from "./IndexText"; +import BaseUIElement from "../BaseUIElement"; -export default class MoreScreen extends UIElement { - private readonly _onMainScreen: boolean; - - private _component: UIElement; +export default class MoreScreen extends Combine { constructor(onMainScreen: boolean = false) { - super(State.state.locationControl); - this._onMainScreen = onMainScreen; - this.ListenTo(State.state.osmConnection.userDetails); - this.ListenTo(State.state.installedThemes); + super(MoreScreen.Init(onMainScreen, State.state)); } - InnerRender(): string { - + private static Init(onMainScreen: boolean, state: State): BaseUIElement [] { const tr = Translations.t.general.morescreen; - - const els: UIElement[] = [] - - const themeButtons: UIElement[] = [] - - for (const layout of AllKnownLayouts.layoutsList) { - if (layout.id === personal.id) { - if (State.state.osmConnection.userDetails.data.csCount < Constants.userJourney.personalLayoutUnlock) { - continue; - } - } - themeButtons.push(this.createLinkButton(layout)); - } - - - els.push(new VariableUiElement( - State.state.osmConnection.userDetails.map(userDetails => { - if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) { - return new SubtleButton(null, tr.requestATheme, {url:"https://github.com/pietervdvn/MapComplete/issues", newTab: true}).Render(); - } - return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme, { - url: "./customGenerator.html", - newTab: false - }).Render(); - }) - )); - - els.push(new Combine(themeButtons)) - - - const customThemesNames = State.state.installedThemes.data ?? []; - - if (customThemesNames.length > 0) { - els.push(Translations.t.general.customThemeIntro) - - for (const installed of State.state.installedThemes.data) { - els.push(this.createLinkButton(installed.layout, installed.definition)); - } - } - - let intro: UIElement = tr.intro; - const themeButtonsElement = new Combine(els) - - if (this._onMainScreen) { + let intro: BaseUIElement = tr.intro; + let themeButtonStyle = "" + let themeListStyle = "" + if (onMainScreen) { intro = new Combine([ LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages()) .SetClass("absolute top-2 right-3"), new IndexText() ]) - themeButtons.map(e => e?.SetClass("h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden")) - themeButtonsElement.SetClass("md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4") + themeButtonStyle = "h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden" + themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4" } - - - this._component = new Combine([ + return[ intro, - themeButtonsElement, + MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle), + MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle), tr.streetcomplete.SetClass("block text-base mx-10 my-3 mb-10") - ]); - return this._component.Render(); + ]; + } + + private static createUnofficialThemeList(buttonClass: string): BaseUIElement{ + const customThemes = State.state.installedThemes.data ?? []; + const els : BaseUIElement[] = [] + if (customThemes.length > 0) { + els.push(Translations.t.general.customThemeIntro) + + const customThemesElement = new Combine( + customThemes.map(theme => MoreScreen.createLinkButton(theme.layout, theme.definition)?.SetClass(buttonClass)) + ) + els.push(customThemesElement) + } + return new Combine(els) } - private createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined) { + private static createOfficialThemesList(state: State, buttonClass: string): BaseUIElement { + let officialThemes = AllKnownLayouts.layoutsList + if (State.state.osmConnection.userDetails.data.csCount < Constants.userJourney.personalLayoutUnlock) { + officialThemes = officialThemes.filter(theme => theme.id !== personal.id) + } + let buttons = officialThemes.map((layout) => MoreScreen.createLinkButton(layout)?.SetClass(buttonClass)) + + let customGeneratorLink = MoreScreen.createCustomGeneratorButton(state) + buttons.splice(0, 0, customGeneratorLink); + + return new Combine(buttons) + } + + /* + * Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets + * */ + private static createCustomGeneratorButton(state: State): VariableUiElement { + const tr = Translations.t.general.morescreen; + return new VariableUiElement( + state.osmConnection.userDetails.map(userDetails => { + if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) { + return new SubtleButton(null, tr.requestATheme, { + url: "https://github.com/pietervdvn/MapComplete/issues", + newTab: true + }); + } + return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme, { + url: "./customGenerator.html", + newTab: false + }); + }) + ) + } + + /** + * Creates a button linking to the given theme + * @param layout + * @param customThemeDefinition + * @private + */ + private static createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined): BaseUIElement { if (layout === undefined) { return undefined; } @@ -100,17 +105,14 @@ export default class MoreScreen extends UIElement { return undefined; } if (layout.hideFromOverview) { - const pref = State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled"); - this.ListenTo(pref); - if (pref.data !== "true") { - return undefined; - } + return undefined; } if (layout.id === State.state.layoutToUse.data?.id) { return undefined; } - const currentLocation = State.state.locationControl.data; + const currentLocation = State.state.locationControl; + let path = window.location.pathname; // Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html' path = path.substr(0, path.lastIndexOf("/")); @@ -119,19 +121,23 @@ export default class MoreScreen extends UIElement { path = "." } - const params = `z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}` - let linkText = - `${path}/${layout.id.toLowerCase()}.html?${params}` - + let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?` + let linkSuffix = "" if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { - linkText = `${path}/index.html?layout=${layout.id}&${params}` + linkPrefix = `${path}/index.html?layout=${layout.id}&` } if (customThemeDefinition) { - linkText = `${path}/index.html?userlayout=${layout.id}&${params}#${customThemeDefinition}` - + linkPrefix = `${path}/index.html?userlayout=${layout.id}&` + linkSuffix = `#${customThemeDefinition}` } + const linkText = currentLocation.map(currentLocation => + `${linkPrefix}z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}${linkSuffix}`) + + + + let description = Translations.W(layout.shortDescription); return new SubtleButton(layout.icon, new Combine([ @@ -144,4 +150,5 @@ export default class MoreScreen extends UIElement { ]), {url: linkText, newTab: false}); } + } \ No newline at end of file diff --git a/UI/BigComponents/PersonalLayersPanel.ts b/UI/BigComponents/PersonalLayersPanel.ts index e11aac86e..6713f9699 100644 --- a/UI/BigComponents/PersonalLayersPanel.ts +++ b/UI/BigComponents/PersonalLayersPanel.ts @@ -5,7 +5,7 @@ import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; import Svg from "../../Svg"; import State from "../../State"; import Combine from "../Base/Combine"; -import CheckBox from "../Input/CheckBox"; +import Toggle from "../Input/Toggle"; import {SubtleButton} from "../Base/SubtleButton"; import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; @@ -79,7 +79,7 @@ export default class PersonalLayersPanel extends UIElement { ]) - const cb = new CheckBox( + const cb = new Toggle( new SubtleButton( icon, content), diff --git a/UI/BigComponents/ShareButton.ts b/UI/BigComponents/ShareButton.ts index b4c2eba51..0ad95828c 100644 --- a/UI/BigComponents/ShareButton.ts +++ b/UI/BigComponents/ShareButton.ts @@ -19,7 +19,6 @@ export default class ShareButton extends UIElement{ } protected InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); const self= this; htmlElement.addEventListener('click', () => { if (navigator.share) { diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index dcf087482..dc412f2c0 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -1,4 +1,3 @@ -import {VerticalCombine} from "../Base/VerticalCombine"; import {UIElement} from "../UIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import {Translation} from "../i18n/Translation"; @@ -9,7 +8,7 @@ import {SubtleButton} from "../Base/SubtleButton"; import {UIEventSource} from "../../Logic/UIEventSource"; import {Utils} from "../../Utils"; import State from "../../State"; -import CheckBox from "../Input/CheckBox"; +import Toggle from "../Input/Toggle"; import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import Constants from "../../Models/Constants"; @@ -40,7 +39,7 @@ export default class ShareScreen extends UIElement { return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;"); } - const includeLocation = new CheckBox( + const includeLocation = new Toggle( new Combine([check(), tr.fsIncludeCurrentLocation]), new Combine([nocheck(), tr.fsIncludeCurrentLocation]), true @@ -75,7 +74,7 @@ export default class ShareScreen extends UIElement { const currentBackground = new VariableUiElement(currentLayer.map(layer => { return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render(); })); - const includeCurrentBackground = new CheckBox( + const includeCurrentBackground = new Toggle( new Combine([check(), currentBackground]), new Combine([nocheck(), currentBackground]), true @@ -90,7 +89,7 @@ export default class ShareScreen extends UIElement { }, [currentLayer])); - const includeLayerChoices = new CheckBox( + const includeLayerChoices = new Toggle( new Combine([check(), tr.fsIncludeCurrentLayers]), new Combine([nocheck(), tr.fsIncludeCurrentLayers]), true @@ -120,7 +119,7 @@ export default class ShareScreen extends UIElement { for (const swtch of switches) { - const checkbox = new CheckBox( + const checkbox = new Toggle( new Combine([check(), Translations.W(swtch.human)]), new Combine([nocheck(), Translations.W(swtch.human)]), !swtch.reverse ); @@ -143,7 +142,7 @@ export default class ShareScreen extends UIElement { } - this._options = new VerticalCombine(optionCheckboxes) + this._options = new Combine(optionCheckboxes).SetClass("flex flex-col") const url = (currentLocation ?? new UIEventSource(undefined)).map(() => { const host = window.location.host; @@ -216,8 +215,8 @@ export default class ShareScreen extends UIElement { ).onClick(async () => { const shareData = { - title: Translations.W(layout.id)?.InnerRender() ?? "", - text: Translations.W(layout.description)?.InnerRender() ?? "", + title: Translations.W(layout.title)?.InnerRenderAsString() ?? "", + text: Translations.W(layout.description)?.InnerRenderAsString() ?? "", url: self._link.data, } @@ -251,11 +250,11 @@ export default class ShareScreen extends UIElement { } - InnerRender(): string { + InnerRender(): UIElement { const tr = Translations.t.general.sharescreen; - return new VerticalCombine([ + return new Combine([ this._editLayout, tr.intro, this._link, @@ -264,7 +263,7 @@ export default class ShareScreen extends UIElement { tr.embedIntro, this._options, this._iframeCode, - ]).Render() + ]).SetClass("flex flex-col") } } \ No newline at end of file diff --git a/UI/BigComponents/ThemeIntroductionPanel.ts b/UI/BigComponents/ThemeIntroductionPanel.ts index 3b9a8b2b1..61f7c2347 100644 --- a/UI/BigComponents/ThemeIntroductionPanel.ts +++ b/UI/BigComponents/ThemeIntroductionPanel.ts @@ -7,6 +7,7 @@ import Translations from "../i18n/Translations"; import {VariableUiElement} from "../Base/VariableUIElement"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; export default class ThemeIntroductionPanel extends UIElement { private languagePicker: UIElement; @@ -44,7 +45,7 @@ export default class ThemeIntroductionPanel extends UIElement { this.SetClass("link-underline") } - InnerRender(): string { + InnerRender(): BaseUIElement { const layout : LayoutConfig = this._layout.data; return new Combine([ layout.description, @@ -54,7 +55,7 @@ export default class ThemeIntroductionPanel extends UIElement { "
", this.languagePicker, ...layout.CustomCodeSnippets() - ]).Render() + ]) } diff --git a/UI/BigComponents/UserBadge.ts b/UI/BigComponents/UserBadge.ts index acb5dfb55..6248140e7 100644 --- a/UI/BigComponents/UserBadge.ts +++ b/UI/BigComponents/UserBadge.ts @@ -64,10 +64,10 @@ export default class UserBadge extends UIElement { } - InnerRender(): string { + InnerRender(): UIElement { const user = this._userDetails.data; if (!user.loggedIn) { - return this._loginButton.Render(); + return this._loginButton; } const linkStyle = "flex items-baseline" @@ -138,7 +138,7 @@ export default class UserBadge extends UIElement { return new Combine([ userIcon, usertext, - ]).Render() + ]) } diff --git a/UI/CenterMessageBox.ts b/UI/CenterMessageBox.ts index 7c3ed26ff..e1f15e498 100644 --- a/UI/CenterMessageBox.ts +++ b/UI/CenterMessageBox.ts @@ -13,34 +13,37 @@ export default class CenterMessageBox extends UIElement { this.ListenTo(State.state.layerUpdater.sufficientlyZoomed); } - private static prep(): { innerHtml: string, done: boolean } { + private static prep(): { innerHtml: string | UIElement, done: boolean } { if (State.state.centerMessage.data != "") { return {innerHtml: State.state.centerMessage.data, done: false}; } const lu = State.state.layerUpdater; if (lu.timeout.data > 0) { return { - innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.timeout.data}).Render(), + innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.timeout.data}), done: false }; } if (lu.runningQuery.data) { - return {innerHtml: Translations.t.centerMessage.loadingData.Render(), done: false}; + return {innerHtml: Translations.t.centerMessage.loadingData, done: false}; } if (!lu.sufficientlyZoomed.data) { - return {innerHtml: Translations.t.centerMessage.zoomIn.Render(), done: false}; + return {innerHtml: Translations.t.centerMessage.zoomIn, done: false}; } else { - return {innerHtml: Translations.t.centerMessage.ready.Render(), done: true}; + return {innerHtml: Translations.t.centerMessage.ready, done: true}; } } - InnerRender(): string { + InnerRender(): string | UIElement { return CenterMessageBox.prep().innerHtml; } InnerUpdate(htmlElement: HTMLElement) { + if(htmlElement.parentElement === null){ + return; + } const pstyle = htmlElement.parentElement.style; if (State.state.centerMessage.data != "") { pstyle.opacity = "1"; diff --git a/UI/CustomGenerator/AllLayersPanel.ts b/UI/CustomGenerator/AllLayersPanel.ts deleted file mode 100644 index 6f8ce2d93..000000000 --- a/UI/CustomGenerator/AllLayersPanel.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {UIElement} from "../UIElement"; -import {TabbedComponent} from "../Base/TabbedComponent"; -import {SubtleButton} from "../Base/SubtleButton"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; -import Combine from "../Base/Combine"; -import {GenerateEmpty} from "./GenerateEmpty"; -import LayerPanelWithPreview from "./LayerPanelWithPreview"; -import UserDetails from "../../Logic/Osm/OsmConnection"; -import {MultiInput} from "../Input/MultiInput"; -import TagRenderingPanel from "./TagRenderingPanel"; -import SingleSetting from "./SingleSetting"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import {DropDown} from "../Input/DropDown"; -import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; -import Svg from "../../Svg"; - -export default class AllLayersPanel extends UIElement { - - - private panel: UIElement; - private readonly _config: UIEventSource; - private readonly languages: UIEventSource; - private readonly userDetails: UserDetails; - private readonly currentlySelected: UIEventSource>; - - constructor(config: UIEventSource, - languages: UIEventSource, userDetails: UserDetails) { - super(undefined); - this.userDetails = userDetails; - this._config = config; - this.languages = languages; - - this.createPanels(userDetails); - const self = this; - this.dumbMode = false; - config.map(config => config.layers.length).addCallback(() => self.createPanels(userDetails)); - - } - - - private createPanels(userDetails: UserDetails) { - const self = this; - const tabs = []; - - - const roamingTags = new MultiInput("Add a tagrendering", - () => GenerateEmpty.createEmptyTagRendering(), - () => { - return new TagRenderingPanel(self.languages, self.currentlySelected, self.userDetails) - - }, undefined, {allowMovement: true}); - new SingleSetting(this._config, roamingTags, "roamingRenderings", "Roaming Renderings", "These tagrenderings are shown everywhere"); - - - const backgroundLayers = AvailableBaseLayers.layerOverview.map(baselayer => ({shown: - baselayer.name, value: baselayer.id})); - const dropDown = new DropDown("Choose the default background layer", - [{value: "osm",shown:"OpenStreetMap (default)"}, ...backgroundLayers]) - new SingleSetting(self._config, dropDown, "defaultBackgroundId", "Default background layer", - "Selects the background layer that is used by default. If this layer is not available at the given point, OSM-Carto will be ued"); - - const layers = this._config.data.layers; - for (let i = 0; i < layers.length; i++) { - tabs.push({ - header: new VariableUiElement(this._config.map((config: LayoutConfigJson) => { - const layer = config.layers[i]; - if (typeof layer !== "string") { - try { - const iconTagRendering = new TagRenderingConfig(layer["icon"], undefined, "icon") - const icon = iconTagRendering.GetRenderValue({"id": "node/-1"}).txt; - return `` - } catch (e) { - return Svg.bug_img - // Nothing to do here - } - } - return Svg.help_img; - })), - content: new LayerPanelWithPreview(this._config, this.languages, i, userDetails) - }); - } - tabs.push({ - header: Svg.layersAdd_img, - content: new Combine([ - "

Layer editor

", - "In this tab page, you can add and edit the layers of the theme. Click the layers above or add a new layer to get started.", - new SubtleButton( - Svg.layersAdd_ui(), - "Add a new layer" - ).onClick(() => { - self._config.data.layers.push(GenerateEmpty.createEmptyLayer()) - self._config.ping(); - }), - "

Default background layer

", - dropDown, - "

TagRenderings for every layer

", - "Define tag renderings and questions here that should be shown on every layer of the theme.", - roamingTags - ] - ), - }) - - this.panel = new TabbedComponent(tabs, new UIEventSource(Math.max(0, layers.length - 1))); - this.Update(); - } - - InnerRender(): string { - return this.panel.Render(); - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/CustomGeneratorPanel.ts b/UI/CustomGenerator/CustomGeneratorPanel.ts deleted file mode 100644 index e1a1592ed..000000000 --- a/UI/CustomGenerator/CustomGeneratorPanel.ts +++ /dev/null @@ -1,118 +0,0 @@ -import {UIElement} from "../UIElement"; -import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import SingleSetting from "./SingleSetting"; -import GeneralSettings from "./GeneralSettings"; -import Combine from "../Base/Combine"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {TabbedComponent} from "../Base/TabbedComponent"; -import PageSplit from "../Base/PageSplit"; -import AllLayersPanel from "./AllLayersPanel"; -import SharePanel from "./SharePanel"; -import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; -import {SubtleButton} from "../Base/SubtleButton"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import SavePanel from "./SavePanel"; -import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; -import HelpText from "./HelpText"; -import Svg from "../../Svg"; -import Constants from "../../Models/Constants"; -import LZString from "lz-string"; -import {Utils} from "../../Utils"; - -export default class CustomGeneratorPanel extends UIElement { - private mainPanel: UIElement; - private loginButton: UIElement; - - private readonly connection: OsmConnection; - - constructor(connection: OsmConnection, layout: LayoutConfigJson) { - super(connection.userDetails); - this.connection = connection; - this.SetClass("main-tabs"); - this.loginButton = new SubtleButton("", "Login to create a custom theme").onClick(() => connection.AttemptLogin()) - const self = this; - self.mainPanel = new FixedUiElement("Attempting to log in..."); - connection.OnLoggedIn(userDetails => { - self.InitMainPanel(layout, userDetails, connection); - self.Update(); - }) - } - - private InitMainPanel(layout: LayoutConfigJson, userDetails: UserDetails, connection: OsmConnection) { - const es = new UIEventSource(layout); - const encoded = es.map(config => LZString.compressToBase64(Utils.MinifyJSON(JSON.stringify(config, null, 0)))); - encoded.addCallback(encoded => LocalStorageSource.Get("last-custom-theme")) - const liveUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}#${encoded}`) - const testUrl = encoded.map(encoded => `./index.html?test=true&userlayout=${es.data.id}#${encoded}`) - const iframe = testUrl.map(url => ``); - const currentSetting = new UIEventSource>(undefined) - const generalSettings = new GeneralSettings(es, currentSetting); - const languages = generalSettings.languages; - - const chronic = UIEventSource.Chronic(120 * 1000) - .map(date => { - if (es.data.id == undefined) { - return undefined - } - if (es.data.id === "") { - return undefined; - } - const pref = connection.GetLongPreference("installed-theme-" + es.data.id); - pref.setData(encoded.data); - return date; - }); - - const preview = new Combine([ - new VariableUiElement(iframe) - ]).SetClass("preview") - this.mainPanel = new TabbedComponent([ - { - header: Svg.gear_img, - content: - new PageSplit( - generalSettings.SetStyle("width: 50vw;"), - new Combine([ - new HelpText(currentSetting).SetStyle("height:calc(100% - 65vh); width: 100%; display:block; overflow-y: auto"), - preview.SetStyle("height:65vh; width:100%; display:block") - ]).SetStyle("position:relative; width: 50%;") - ) - }, - { - header: Svg.layers_img, - content: new AllLayersPanel(es, languages, userDetails) - }, - { - header: Svg.floppy_img, - content: new SavePanel(this.connection, es, chronic) - - }, - { - header:Svg.share_img, - content: new SharePanel(es, liveUrl, userDetails) - } - ]) - } - - - InnerRender(): string { - const ud = this.connection.userDetails.data; - if (!ud.loggedIn) { - return new Combine([ - "

Not Logged in

", - "You need to be logged in in order to create a custom theme", - this.loginButton - ]).Render(); - } - const journey = Constants.userJourney; - if (ud.csCount <= journey.themeGeneratorReadOnlyUnlock) { - return new Combine([ - "

Too little experience

", - `

Creating your own (readonly) themes can only be done if you have more then ${journey.themeGeneratorReadOnlyUnlock} changesets made

`, - `

Making a theme including survey options can be done at ${journey.themeGeneratorFullUnlock} changesets

` - ]).Render(); - } - return this.mainPanel.Render() - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/GeneralSettings.ts b/UI/CustomGenerator/GeneralSettings.ts deleted file mode 100644 index b37fb31cc..000000000 --- a/UI/CustomGenerator/GeneralSettings.ts +++ /dev/null @@ -1,88 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; -import Combine from "../Base/Combine"; -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 { - private panel: Combine; - - public languages : UIEventSource; - - constructor(configuration: UIEventSource, currentSetting: UIEventSource>) { - super(undefined); - - - const languagesField = - ValidatedTextField.Mapped( - str => { - console.log("Language from str", str); - return str?.split(";")?.map(str => str.trim().toLowerCase()); - }, - languages => languages.join(";")); - this.languages = languagesField.GetValue(); - - 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); - - - const locationRemark = "
Note that, as soon as an URL-parameter sets the location or a location is known due to a previous visit, that the theme-set location is ignored" - - const settingsTable = new SettingsTable( - [ - 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"), - new SingleSetting(configuration, languagesField, "language", - "Supported languages", "Which languages do you want to support in this theme? Type the two letter code representing your language, seperated by ;. For example:en;nl "), - new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "title", - "Title", "The title as shown in the welcome message, in the browser title bar, in the more screen, ..."), - new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "shortDescription","Short description", - "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, 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, ValidatedTextField.NumberInput("nat", n => n < 23), "startZoom","Initial zoom level", - "When a user first loads MapComplete, this zoomlevel is shown."+locationRemark), - 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, 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, 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, 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); - - this.panel = new Combine([ - "

General theme settings

", - settingsTable - ]); - } - - - InnerRender(): string { - return this.panel.Render(); - } - - -} \ No newline at end of file diff --git a/UI/CustomGenerator/GenerateEmpty.ts b/UI/CustomGenerator/GenerateEmpty.ts deleted file mode 100644 index 34defb877..000000000 --- a/UI/CustomGenerator/GenerateEmpty.ts +++ /dev/null @@ -1,87 +0,0 @@ -import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson"; -import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; -import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson"; - -export class GenerateEmpty { - public static createEmptyLayer(): LayerConfigJson { - return { - id: "yourlayer", - name: {}, - minzoom: 12, - overpassTags: {and: [""]}, - title: {}, - description: {}, - tagRenderings: [], - hideUnderlayingFeaturesMinPercentage: 0, - icon: { - render: "./assets/svg/bug.svg" - }, - width: { - render: "8" - }, - iconSize: { - render: "40,40,center" - }, - color:{ - render: "#00f" - } - } - } - - public static createEmptyLayout(): LayoutConfigJson { - return { - id: "id", - title: {}, - shortDescription: {}, - description: {}, - language: [], - maintainer: "", - icon: "./assets/svg/bug.svg", - version: "0", - startLat: 0, - startLon: 0, - startZoom: 1, - widenFactor: 0.05, - socialImage: "", - - layers: [ - GenerateEmpty.createEmptyLayer() - ] - } - } - - public static createTestLayout(): LayoutConfigJson { - return { - id: "test", - title: {"en": "Test layout"}, - shortDescription: {}, - description: {"en": "A layout for testing"}, - language: ["en"], - maintainer: "Pieter Vander Vennet", - icon: "./assets/svg/bug.svg", - version: "0", - startLat: 0, - startLon: 0, - startZoom: 1, - widenFactor: 0.05, - socialImage: "", - layers: [{ - id: "testlayer", - name: {en:"Testing layer"}, - minzoom: 15, - overpassTags: {and: ["highway=residential"]}, - title: {}, - description: {"en": "Some Description"}, - icon: {render: {en: "./assets/svg/pencil.svg"}}, - width: {render: {en: "5"}}, - tagRenderings: [{ - render: {"en":"Test Rendering"} - }] - }] - } - } - - public static createEmptyTagRendering(): TagRenderingConfigJson { - return {}; - } -} \ No newline at end of file diff --git a/UI/CustomGenerator/HelpText.ts b/UI/CustomGenerator/HelpText.ts deleted file mode 100644 index 880fe65e3..000000000 --- a/UI/CustomGenerator/HelpText.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {SubtleButton} from "../Base/SubtleButton"; -import Combine from "../Base/Combine"; -import SingleSetting from "./SingleSetting"; -import Svg from "../../Svg"; - -export default class HelpText extends UIElement { - - private helpText: UIElement; - private returnButton: UIElement; - - constructor(currentSetting: UIEventSource>) { - super(); - this.returnButton = new SubtleButton(Svg.close_ui(), - new VariableUiElement( - currentSetting.map(currentSetting => { - if (currentSetting === undefined) { - return ""; - } - return "Return to general help"; - } - ) - )) - .ListenTo(currentSetting) - .SetClass("small-button") - .onClick(() => currentSetting.setData(undefined)); - - - this.helpText = new VariableUiElement(currentSetting.map((setting: SingleSetting) => { - if (setting === undefined) { - return "

Welcome to the Custom Theme Builder

" + - "Here, one can make their own custom mapcomplete themes.
" + - "Fill out the fields to get a working mapcomplete theme. More information on the selected field will appear here when you click it.
" + - "Want to see how the quests are doing in number of visits? All the stats are open on goatcounter"; - } - - return new Combine(["

", setting._name, "

", setting._description.Render()]).Render(); - })) - - - } - - InnerRender(): string { - return new Combine([this.helpText, - this.returnButton, - ]).Render(); - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/LayerPanel.ts b/UI/CustomGenerator/LayerPanel.ts deleted file mode 100644 index 787de027c..000000000 --- a/UI/CustomGenerator/LayerPanel.ts +++ /dev/null @@ -1,251 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; -import SettingsTable from "./SettingsTable"; -import SingleSetting from "./SingleSetting"; -import {SubtleButton} from "../Base/SubtleButton"; -import Combine from "../Base/Combine"; -import {TextField} from "../Input/TextField"; -import {InputElement} from "../Input/InputElement"; -import MultiLingualTextFields from "../Input/MultiLingualTextFields"; -import CheckBox from "../Input/CheckBox"; -import AndOrTagInput from "../Input/AndOrTagInput"; -import TagRenderingPanel from "./TagRenderingPanel"; -import {DropDown} from "../Input/DropDown"; -import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson"; -import {MultiInput} from "../Input/MultiInput"; -import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson"; -import PresetInputPanel from "./PresetInputPanel"; -import UserDetails from "../../Logic/Osm/OsmConnection"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import Svg from "../../Svg"; -import Constants from "../../Models/Constants"; - -/** - * Shows the configuration for a single layer - */ -export default class LayerPanel extends UIElement { - private readonly _config: UIEventSource; - - private readonly settingsTable: UIElement; - private readonly mapRendering: UIElement; - - private readonly deleteButton: UIElement; - - public readonly titleRendering: UIElement; - - public readonly selectedTagRendering: UIEventSource - = new UIEventSource(undefined); - private tagRenderings: UIElement; - private presetsPanel: UIElement; - - constructor(config: UIEventSource, - languages: UIEventSource, - index: number, - currentlySelected: UIEventSource>, - userDetails: UserDetails) { - super(); - this._config = config; - this.mapRendering = this.setupRenderOptions(config, languages, index, currentlySelected, userDetails); - - const actualDeleteButton = new SubtleButton( - Svg.delete_icon_ui(), - "Yes, delete this layer" - ).onClick(() => { - config.data.layers.splice(index, 1); - config.ping(); - }); - - this.deleteButton = new CheckBox( - new Combine( - [ - "

Confirm layer deletion

", - new SubtleButton( - Svg.close_ui(), - "No, don't delete" - ), - "Deleting a layer can not be undone!", - actualDeleteButton - ] - ), - new SubtleButton( - Svg.delete_icon_ui(), - "Remove this layer" - ) - ) - - function setting(input: InputElement, path: string | string[], name: string, description: string | UIElement): SingleSetting { - let pathPre = ["layers", index]; - if (typeof (path) === "string") { - pathPre.push(path); - } else { - pathPre = pathPre.concat(path); - } - - return new SingleSetting(config, input, pathPre, name, description); - } - - - this.settingsTable = new SettingsTable([ - 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(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(new AndOrTagInput(), ["overpassTags"], "Overpass query", - "The tags of the objects to load from overpass"), - - ], - currentlySelected); - const self = this; - - const popupTitleRendering = new TagRenderingPanel(languages, currentlySelected, userDetails, { - title: "Popup title", - description: "This is the rendering shown as title in the popup for this element", - disableQuestions: true - }); - - new SingleSetting(config, popupTitleRendering, ["layers", index, "title"], "Popup title", "This is the rendering shown as title in the popup"); - this.titleRendering = popupTitleRendering; - this.registerTagRendering(popupTitleRendering); - - - const renderings = config.map(config => { - const layer = config.layers[index] as LayerConfigJson; - // @ts-ignore - const renderings : TagRenderingConfigJson[] = layer.tagRenderings ; - return renderings; - }); - const tagRenderings = new MultiInput("Add a tag rendering/question", - () => ({}), - () => { - const tagPanel = new TagRenderingPanel(languages, currentlySelected, userDetails) - self.registerTagRendering(tagPanel); - return tagPanel; - }, renderings, - {allowMovement: true}); - - tagRenderings.GetValue().addCallback( - tagRenderings => { - (config.data.layers[index] as LayerConfigJson).tagRenderings = tagRenderings; - config.ping(); - } - ) - - if (userDetails.csCount >= Constants.userJourney.themeGeneratorFullUnlock) { - - const presetPanel = new MultiInput("Add a preset", - () => ({tags: [], title: {}}), - () => new PresetInputPanel(currentlySelected, languages), - undefined, {allowMovement: true}); - new SingleSetting(config, presetPanel, ["layers", index, "presets"], "Presets", "") - this.presetsPanel = presetPanel; - } else { - this.presetsPanel = new FixedUiElement(`Creating a custom theme which also edits OSM is only unlocked after ${Constants.userJourney.themeGeneratorFullUnlock} changesets`).SetClass("alert"); - } - - function loadTagRenderings() { - const values = (config.data.layers[index] as LayerConfigJson).tagRenderings; - const renderings: TagRenderingConfigJson[] = []; - for (const value of values) { - if (typeof (value) !== "string") { - renderings.push(value); - } - - } - tagRenderings.GetValue().setData(renderings); - } - - loadTagRenderings(); - - this.tagRenderings = tagRenderings; - - - } - - private setupRenderOptions(config: UIEventSource, - languages: UIEventSource, - index: number, - currentlySelected: UIEventSource>, - userDetails: UserDetails - ): UIElement { - const iconSelect = new TagRenderingPanel( - languages, currentlySelected, userDetails, - { - title: "Icon", - description: "A visual representation for this layer and for the points on the map.", - disableQuestions: true, - noLanguage: true - }); - const size = new TagRenderingPanel(languages, currentlySelected, userDetails, - { - title: "Icon Size", - description: "The size of the icons on the map in pixels. Can vary based on the tagging", - disableQuestions: true, - noLanguage: true - }); - const color = new TagRenderingPanel(languages, currentlySelected, userDetails, - { - title: "Way and area color", - description: "The color or a shown way or area. Can vary based on the tagging", - disableQuestions: true, - noLanguage: true - }); - const stroke = new TagRenderingPanel(languages, currentlySelected, userDetails, - { - title: "Stroke width", - description: "The width of lines representing ways and the outline of areas. Can vary based on the tags", - disableQuestions: true, - noLanguage: true - }); - this.registerTagRendering(iconSelect); - this.registerTagRendering(size); - this.registerTagRendering(color); - this.registerTagRendering(stroke); - - function setting(input: InputElement, path, isIcon: boolean = false): SingleSetting { - return new SingleSetting(config, input, ["layers", index, path], undefined, undefined) - } - - return new SettingsTable([ - setting(iconSelect, "icon"), - setting(size, "iconSize"), - setting(color, "color"), - setting(stroke, "width") - ], currentlySelected); - } - - private registerTagRendering( - tagRenderingPanel: TagRenderingPanel) { - - tagRenderingPanel.IsHovered().addCallback(isHovering => { - if (!isHovering) { - return; - } - this.selectedTagRendering.setData(tagRenderingPanel); - }) - } - - InnerRender(): string { - return new Combine([ - "

General layer settings

", - this.settingsTable, - "

Popup contents

", - this.titleRendering, - this.tagRenderings, - "

Presets

", - "Does this theme support adding a new point?
If this should be the case, add a preset. Make sure that the preset tags do match the overpass-tags, otherwise it might seem like the newly added points dissapear ", - this.presetsPanel, - "

Map rendering options

", - this.mapRendering, - "

Layer delete

", - this.deleteButton - ]).Render(); - } -} \ No newline at end of file diff --git a/UI/CustomGenerator/LayerPanelWithPreview.ts b/UI/CustomGenerator/LayerPanelWithPreview.ts deleted file mode 100644 index 3c0d19cc3..000000000 --- a/UI/CustomGenerator/LayerPanelWithPreview.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import SingleSetting from "./SingleSetting"; -import LayerPanel from "./LayerPanel"; -import HelpText from "./HelpText"; -import {MultiTagInput} from "../Input/MultiTagInput"; -import {FromJSON} from "../../Customizations/JSON/FromJSON"; -import Combine from "../Base/Combine"; -import PageSplit from "../Base/PageSplit"; -import TagRenderingPreview from "./TagRenderingPreview"; -import UserDetails from "../../Logic/Osm/OsmConnection"; - - -export default class LayerPanelWithPreview extends UIElement{ - private panel: UIElement; - constructor(config: UIEventSource, languages: UIEventSource, index: number, userDetails: UserDetails) { - super(); - - const currentlySelected = new UIEventSource<(SingleSetting)>(undefined); - const layer = new LayerPanel(config, languages, index, currentlySelected, userDetails); - const helpText = new HelpText(currentlySelected); - - const previewTagInput = new MultiTagInput(); - previewTagInput.GetValue().setData(["id=123456"]); - - const previewTagValue = previewTagInput.GetValue().map(tags => { - const properties = {}; - for (const str of tags) { - const tag = FromJSON.SimpleTag(str); - if (tag !== undefined) { - properties[tag.key] = tag.value; - } - } - return properties; - }); - - const preview = new TagRenderingPreview(layer.selectedTagRendering, previewTagValue); - - this.panel = new PageSplit( - layer.SetClass("scrollable"), - new Combine([ - helpText, - "
", - "

Testing tags

", - previewTagInput, - "

Tag Rendering preview

", - preview - - ]), 60 - ); - - } - - InnerRender(): string { - return this.panel.Render(); - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/MappingInput.ts b/UI/CustomGenerator/MappingInput.ts deleted file mode 100644 index 7c8ae6b1b..000000000 --- a/UI/CustomGenerator/MappingInput.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {InputElement} from "../Input/InputElement"; -import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import SettingsTable from "./SettingsTable"; -import SingleSetting from "./SingleSetting"; -import AndOrTagInput from "../Input/AndOrTagInput"; -import MultiLingualTextFields from "../Input/MultiLingualTextFields"; -import {DropDown} from "../Input/DropDown"; - -export default class MappingInput extends InputElement<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }> { - - private readonly _value: UIEventSource<{ if: AndOrTagConfigJson; then: any; hideInAnswer?: boolean }>; - private readonly _panel: UIElement; - - constructor(languages: UIEventSource, disableQuestions: boolean = false) { - super(); - const currentSelected = new UIEventSource>(undefined); - this._value = new UIEventSource<{ if: AndOrTagConfigJson, then: any, hideInAnswer?: boolean }>({ - if: undefined, - then: undefined - }); - const self = this; - - function setting(inputElement: InputElement, path: string, name: string, description: string | UIElement) { - return new SingleSetting(self._value, inputElement, path, name, description); - } - - const withQuestions = [setting(new DropDown("", - [{value: false, shown: "Can be used as answer"}, {value: true, shown: "Not an answer option"}]), - "hideInAnswer", "Answer option", - "Sometimes, multiple tags for the same meaning are used (e.g. access=yes and access=public)." + - "Use this toggle to disable an anwer. Alternatively an implied/assumed rendering can be used. In order to do this:" + - "use a single tag in the 'if' with no value defined, e.g. indoor=. The mapping will then be shown as default until explicitly changed" - )]; - - this._panel = new SettingsTable([ - setting(new AndOrTagInput(), "if", "If matches", "If this condition matches, the template then below will be used"), - setting(new MultiLingualTextFields(languages), - "then", "Then show", "If the condition above matches, this template then below will be shown to the user."), - ...(disableQuestions ? [] : withQuestions) - - ], currentSelected).SetClass("bordered tag-mapping"); - - } - - - InnerRender(): string { - return this._panel.Render(); - } - - - GetValue(): UIEventSource<{ if: AndOrTagConfigJson; then: any; hideInAnswer?: boolean }> { - return this._value; - } - - - IsSelected: UIEventSource = new UIEventSource(false); - - IsValid(t: { if: AndOrTagConfigJson; then: any; hideInAnswer: boolean }): boolean { - return false; - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/PresetInputPanel.ts b/UI/CustomGenerator/PresetInputPanel.ts deleted file mode 100644 index edb7a0c64..000000000 --- a/UI/CustomGenerator/PresetInputPanel.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {InputElement} from "../Input/InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import {MultiTagInput} from "../Input/MultiTagInput"; -import SettingsTable from "./SettingsTable"; -import SingleSetting from "./SingleSetting"; -import MultiLingualTextFields from "../Input/MultiLingualTextFields"; -import Combine from "../Base/Combine"; - -export default class PresetInputPanel extends InputElement<{ - title: string | any, - tags: string[], - description?: string | any -}> { - private readonly _value: UIEventSource<{ - title: string | any, - tags: string[], - description?: string | any - }>; - private readonly panel: UIElement; - - - constructor(currentlySelected: UIEventSource>, languages: UIEventSource) { - super(); - this._value = new UIEventSource({tags: [], title: {}}); - - - const self = this; - function s(input: InputElement, path: string, name: string, description: string){ - return new SingleSetting(self._value, input, path, name, description) - } - this.panel = new SettingsTable([ - s(new MultiTagInput(), "tags","Preset tags","These tags will be applied on the newly created point"), - s(new MultiLingualTextFields(languages), "title","Preset title","This little text is shown in bold on the 'create new point'-button" ), - s(new MultiLingualTextFields(languages), "description","Description", "This text is shown in the button as description when creating a new point") - ], currentlySelected).SetStyle("display: block; border: 1px solid black; border-radius: 1em;padding: 1em;"); - } - - - InnerRender(): string { - return new Combine([this.panel]).Render(); - } - - GetValue(): UIEventSource<{ - title: string | any, - tags: string[], - description?: string | any - }> { - return this._value; - } - - IsSelected: UIEventSource = new UIEventSource(false); - - IsValid(t: any): boolean { - return false; - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/SavePanel.ts b/UI/CustomGenerator/SavePanel.ts deleted file mode 100644 index 333796c60..000000000 --- a/UI/CustomGenerator/SavePanel.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {UIElement} from "../UIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; -import Combine from "../Base/Combine"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {TextField} from "../Input/TextField"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; - -export default class SavePanel extends UIElement { - private json: UIElement; - private lastSaveEl: UIElement; - private loadFromJson: UIElement; - - constructor( - connection: OsmConnection, - config: UIEventSource, - chronic: UIEventSource) { - super(); - - - this.lastSaveEl = new VariableUiElement(chronic - .map(date => { - if (date === undefined) { - return new FixedUiElement("Your theme will be saved automatically within two minutes... Click here to force saving").SetClass("alert").Render() - } - return "Your theme was last saved at " + date.toISOString() - })).onClick(() => chronic.setData(new Date())); - - const jsonStr = config.map(config => - JSON.stringify(config, null, 2)); - - - const jsonTextField = new TextField({ - placeholder: "JSON Config", - value: jsonStr, - textArea: true, - textAreaRows: 20 - }); - this.json = jsonTextField; - this.loadFromJson = new SubtleButton(Svg.reload_ui(), "Load the JSON file below") - .onClick(() => { - try{ - const json = jsonTextField.GetValue().data; - const parsed : LayoutConfigJson = JSON.parse(json); - config.setData(parsed); - }catch(e){ - alert("Invalid JSON: "+e) - } - }); - } - - InnerRender(): string { - return new Combine([ - "

Save your theme

", - this.lastSaveEl, - "

JSON configuration

", - "The url hash is actually no more then a BASE64-encoding of the below JSON. This json contains the full configuration of the theme.
" + - "This configuration is mainly useful for debugging", - "
", - this.loadFromJson, - this.json - ]).SetClass("scrollable") - .Render(); - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/SettingsTable.ts b/UI/CustomGenerator/SettingsTable.ts deleted file mode 100644 index 28bb4bc75..000000000 --- a/UI/CustomGenerator/SettingsTable.ts +++ /dev/null @@ -1,58 +0,0 @@ -import SingleSetting from "./SingleSetting"; -import {UIElement} from "../UIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import PageSplit from "../Base/PageSplit"; -import Combine from "../Base/Combine"; - -export default class SettingsTable extends UIElement { - - private _col1: UIElement[] = []; - private _col2: UIElement[] = []; - - public selectedSetting: UIEventSource>; - - constructor(elements: (SingleSetting | string)[], - currentSelectedSetting?: UIEventSource>) { - super(undefined); - const self = this; - this.selectedSetting = currentSelectedSetting ?? new UIEventSource>(undefined); - for (const element of elements) { - if(typeof element === "string"){ - this._col1.push(new FixedUiElement(element)); - this._col2.push(null); - continue; - } - - let title: UIElement = element._name === undefined ? null : new FixedUiElement(element._name); - this._col1.push(title); - this._col2.push(element._value); - element._value.SetStyle("display:block"); - element._value.IsSelected.addCallback(isSelected => { - if (isSelected) { - self.selectedSetting.setData(element); - } else if (self.selectedSetting.data === element) { - self.selectedSetting.setData(undefined); - } - }) - } - - } - - InnerRender(): string { - let elements = []; - - for (let i = 0; i < this._col1.length; i++) { - if(this._col1[i] !== null && this._col2[i] !== null){ - elements.push(new PageSplit(this._col1[i], this._col2[i], 25)); - }else if(this._col1[i] !== null){ - elements.push(this._col1[i]) - }else{ - elements.push(this._col2[i]) - } - } - - return new Combine(elements).Render(); - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/SharePanel.ts b/UI/CustomGenerator/SharePanel.ts deleted file mode 100644 index 6f0da7f92..000000000 --- a/UI/CustomGenerator/SharePanel.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; -import Combine from "../Base/Combine"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import UserDetails from "../../Logic/Osm/OsmConnection"; - -export default class SharePanel extends UIElement { - private _config: UIEventSource; - - private _panel: UIElement; - - constructor(config: UIEventSource, liveUrl: UIEventSource, userDetails: UserDetails) { - super(undefined); - this._config = config; - - this._panel = new Combine([ - "

Share

", - "Share the following link with friends:
", - new VariableUiElement(liveUrl.map(url => `${url}`)), - "

Publish on some website

", - - "It is possible to load a JSON-file from the wide internet, but you'll need some (public CORS-enabled) server.", - `Put the raw json online, and use ${window.location.host}?userlayout=https://.json`, - "Please note: it used to be possible to load from the wiki - this is not possible anymore due to technical reasons.", - "" - ]); - } - - InnerRender(): string { - return this._panel.Render(); - } - -} \ No newline at end of file diff --git a/UI/CustomGenerator/SingleSetting.ts b/UI/CustomGenerator/SingleSetting.ts deleted file mode 100644 index dbb4ff145..000000000 --- a/UI/CustomGenerator/SingleSetting.ts +++ /dev/null @@ -1,89 +0,0 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {InputElement} from "../Input/InputElement"; -import {UIElement} from "../UIElement"; -import Translations from "../i18n/Translations"; -import Combine from "../Base/Combine"; -import {VariableUiElement} from "../Base/VariableUIElement"; - -export default class SingleSetting { - public _value: InputElement; - public _name: string; - public _description: UIElement; - public _options: { showIconPreview?: boolean }; - - constructor(config: UIEventSource, - value: InputElement, - path: string | (string | number)[], - name: string, - description: string | UIElement, - options?: { - showIconPreview?: boolean - } - ) { - this._value = value; - this._name = name; - this._description = Translations.W(description); - - this._options = options ?? {}; - if (this._options.showIconPreview) { - this._description = new Combine([ - this._description, - "

Icon preview

", - new VariableUiElement(this._value.GetValue().map(url => ``)) - ]); - } - - if(typeof (path) === "string"){ - path = [path]; - } - const lastPart = path[path.length - 1]; - path.splice(path.length - 1, 1); - - function assignValue(value) { - if (value === undefined) { - return; - } - // We have to rewalk every time as parts might be new - let configPart = config.data; - for (const pathPart of path) { - let newConfigPart = configPart[pathPart]; - if (newConfigPart === undefined) { - if (typeof (pathPart) === "string") { - configPart[pathPart] = {}; - } else { - configPart[pathPart] = []; - } - newConfigPart = configPart[pathPart]; - } - configPart = newConfigPart; - } - configPart[lastPart] = value; - config.ping(); - } - - function loadValue() { - let configPart = config.data; - for (const pathPart of path) { - configPart = configPart[pathPart]; - if (configPart === undefined) { - return; - } - } - const loadedValue = configPart[lastPart]; - if (loadedValue !== undefined) { - value.GetValue().setData(loadedValue); - } - } - loadValue(); - config.addCallback(() => loadValue()); - - value.GetValue().addCallback(assignValue); - assignValue(this._value.GetValue().data); - - - } - - - - -} \ No newline at end of file diff --git a/UI/CustomGenerator/TagRenderingPanel.ts b/UI/CustomGenerator/TagRenderingPanel.ts deleted file mode 100644 index 302a1a3b4..000000000 --- a/UI/CustomGenerator/TagRenderingPanel.ts +++ /dev/null @@ -1,155 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {InputElement} from "../Input/InputElement"; -import SingleSetting from "./SingleSetting"; -import SettingsTable from "./SettingsTable"; -import {TextField} from "../Input/TextField"; -import Combine from "../Base/Combine"; -import MultiLingualTextFields from "../Input/MultiLingualTextFields"; -import AndOrTagInput from "../Input/AndOrTagInput"; -import {MultiTagInput} from "../Input/MultiTagInput"; -import {MultiInput} from "../Input/MultiInput"; -import MappingInput from "./MappingInput"; -import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson"; -import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson"; -import UserDetails from "../../Logic/Osm/OsmConnection"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import SpecialVisualizations from "../SpecialVisualizations"; -import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; -import Constants from "../../Models/Constants"; - -export default class TagRenderingPanel extends InputElement { - - public IsImage = false; - public options: { title?: string; description?: string; disableQuestions?: boolean; isImage?: boolean; }; - public readonly validText: UIElement; - IsSelected: UIEventSource = new UIEventSource(false); - private intro: UIElement; - private settingsTable: UIElement; - private readonly _value: UIEventSource; - - constructor(languages: UIEventSource, - currentlySelected: UIEventSource>, - userDetails: UserDetails, - options?: { - title?: string, - description?: string, - disableQuestions?: boolean, - isImage?: boolean, - noLanguage?: boolean - }) { - super(); - - this.SetClass("bordered"); - this.SetClass("min-height"); - - this.options = options ?? {}; - const questionsNotUnlocked = userDetails.csCount < Constants.userJourney.themeGeneratorFullUnlock; - this.options.disableQuestions = - (this.options.disableQuestions ?? false) || - questionsNotUnlocked; - - this.intro = new Combine(["

", options?.title ?? "TagRendering", "

", - options?.description ?? "A tagrendering converts OSM-tags into a value on screen. Fill out the field 'render' with the text that should appear. Note that `{key}` will be replaced with the corresponding `value`, if present.
For specific known tags (e.g. if `foo=bar`, make a mapping). "]) - this.IsImage = options?.isImage ?? false; - - const value = new UIEventSource({}); - this._value = value; - - function setting(input: InputElement, id: string | string[], name: string, description: string | UIElement): SingleSetting { - return new SingleSetting(value, input, id, name, description); - } - - this._value.addCallback(value => { - let doPing = false; - if (value?.freeform?.key == "") { - value.freeform = undefined; - doPing = true; - } - - if (value?.render == "") { - value.render = undefined; - doPing = true; - } - - if (doPing) { - this._value.ping(); - } - }) - - const questionSettings = [ - - - setting(options?.noLanguage ? new TextField({placeholder: "question"}) : new MultiLingualTextFields(languages) - , "question", "Question", "If the key or mapping doesn't match, this question is asked"), - - "

Freeform key

", - setting(ValidatedTextField.KeyInput(true), ["freeform", "key"], "Freeform key
", - "If specified, the rendering will search if this key is present." + - "If it is, the rendering above will be used to display the element.
" + - "The rendering will go into question mode if
  • this key is not present
  • No single mapping matches
  • A question is given
  • "), - - setting(ValidatedTextField.TypeDropdown(), ["freeform", "type"], "Freeform type", - "The type of this freeform text field, in order to validate"), - setting(new MultiTagInput(), ["freeform", "addExtraTags"], "Extra tags on freeform", - "When the freeform text field is used, the user might mean a predefined key. This field allows to add extra tags, e.g. fixme=User used a freeform field - to check"), - - ]; - - const settings: (string | SingleSetting)[] = [ - setting( - options?.noLanguage ? new TextField({placeholder: "Rendering"}) : - new MultiLingualTextFields(languages), "render", "Value to show", - "Renders this value. Note that {key}-parts are substituted by the corresponding values of the element. If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value." + - "

    " + - "Furhtermore, some special functions are supported:" + SpecialVisualizations.HelpMessage.Render()), - - questionsNotUnlocked ? `You need at least ${Constants.userJourney.themeGeneratorFullUnlock} changesets to unlock the 'question'-field and to use your theme to edit OSM data` : "", - ...(options?.disableQuestions ? [] : questionSettings), - - "

    Mappings

    ", - setting(new MultiInput<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }>("Add a mapping", - () => ({if: {and: []}, then: {}}), - () => new MappingInput(languages, options?.disableQuestions ?? false), - undefined, {allowMovement: true}), "mappings", - "If a tag matches, then show the first respective text", ""), - - "

    Condition

    ", - setting(new AndOrTagInput(), "condition", "Only show this tagrendering if the following condition applies", - "Only show this tag rendering if these tags matches. Optional field.
    Note that the Overpass-tags are already always included in this object"), - - - ]; - - this.settingsTable = new SettingsTable(settings, currentlySelected); - - - this.validText = new VariableUiElement(value.map((json: TagRenderingConfigJson) => { - try { - new TagRenderingConfig(json, undefined, options?.title ?? ""); - return ""; - } catch (e) { - return "" + e + "" - } - })); - - } - - InnerRender(): string { - return new Combine([ - this.intro, - this.settingsTable, - this.validText]).Render(); - } - - GetValue(): UIEventSource { - return this._value; - } - - IsValid(t: TagRenderingConfigJson): boolean { - return false; - } - - -} \ No newline at end of file diff --git a/UI/CustomGenerator/TagRenderingPreview.ts b/UI/CustomGenerator/TagRenderingPreview.ts deleted file mode 100644 index d758682b1..000000000 --- a/UI/CustomGenerator/TagRenderingPreview.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import TagRenderingPanel from "./TagRenderingPanel"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Combine from "../Base/Combine"; -import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; -import EditableTagRendering from "../Popup/EditableTagRendering"; - -export default class TagRenderingPreview extends UIElement { - - private readonly previewTagValue: UIEventSource; - private selectedTagRendering: UIEventSource; - private panel: UIElement; - - constructor(selectedTagRendering: UIEventSource, - previewTagValue: UIEventSource) { - super(selectedTagRendering); - this.selectedTagRendering = selectedTagRendering; - this.previewTagValue = previewTagValue; - this.panel = this.GetPanel(undefined); - const self = this; - this.selectedTagRendering.addCallback(trp => { - self.panel = self.GetPanel(trp); - self.Update(); - }) - - } - - private GetPanel(tagRenderingPanel: TagRenderingPanel): UIElement { - if (tagRenderingPanel === undefined) { - return new FixedUiElement("No tag rendering selected at the moment. Hover over a tag rendering to see what it looks like"); - } - - let es = tagRenderingPanel.GetValue(); - - let rendering: UIElement; - const self = this; - try { - rendering = - new VariableUiElement(es.map(tagRenderingConfig => { - try { - const tr = new EditableTagRendering(self.previewTagValue, new TagRenderingConfig(tagRenderingConfig, undefined,"preview")); - return tr.Render(); - } catch (e) { - return new Combine(["Could not show this tagrendering:", e.message]).Render(); - } - } - )); - - } catch (e) { - console.error("User defined tag rendering incorrect:", e); - rendering = new FixedUiElement(e).SetClass("alert"); - } - - return new Combine([ - "

    ", - tagRenderingPanel.options.title ?? "Extra tag rendering", - "

    ", - tagRenderingPanel.options.description ?? "This tag rendering will appear in the popup", - "

    ", - rendering]); - - } - - InnerRender(): string { - return this.panel.Render(); - } - -} \ No newline at end of file diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index 2211fb085..19c281465 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -1,7 +1,7 @@ import {UIElement} from "../UIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Translations from "../i18n/Translations"; -import CheckBox from "../Input/CheckBox"; +import Toggle from "../Input/Toggle"; import Combine from "../Base/Combine"; import State from "../../State"; import Svg from "../../Svg"; @@ -30,7 +30,7 @@ export default class DeleteImage extends UIElement { }); const cancelButton = Translations.t.general.cancel.SetClass("bg-white pl-4 pr-4").SetStyle( "border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;"); - this.deleteDialog = new CheckBox( + this.deleteDialog = new Toggle( new Combine([ deleteButton, cancelButton @@ -40,17 +40,17 @@ export default class DeleteImage extends UIElement { } - InnerRender(): string { + InnerRender() { if(! State.state?.featureSwitchUserbadge?.data){ return ""; } const value = this.tags.data[this.key]; if (value === undefined || value === "") { - return this.isDeletedBadge.Render(); + return this.isDeletedBadge; } - return this.deleteDialog.Render(); + return this.deleteDialog; } } \ No newline at end of file diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index c62968ff2..3352c64c3 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -31,7 +31,7 @@ export class ImageCarousel extends UIElement{ return uiElements; }); - this.slideshow = new SlideShow(uiElements).HideOnEmpty(true); + this.slideshow = new SlideShow(uiElements); this.SetClass("block w-full"); this.slideshow.SetClass("w-full"); } diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index e6a710a45..13a844049 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -9,9 +9,10 @@ import {DropDown} from "../Input/DropDown"; import Translations from "../i18n/Translations"; import Svg from "../../Svg"; import {Tag} from "../../Logic/Tags/Tag"; +import BaseUIElement from "../BaseUIElement"; export class ImageUploadFlow extends UIElement { - private readonly _licensePicker: UIElement; + private readonly _licensePicker: BaseUIElement; private readonly _tags: UIEventSource; private readonly _selectedLicence: UIEventSource; private readonly _isUploading: UIEventSource = new UIEventSource(0) @@ -35,10 +36,8 @@ export class ImageUploadFlow extends UIElement { {value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs}, {value: "CC-BY 4.0", shown: Translations.t.image.ccb} ], - State.state.osmConnection.GetPreference("pictures-license"), - "","", - "flex flex-col sm:flex-row" - ); + State.state.osmConnection.GetPreference("pictures-license") + ).SetClass("flex flex-col sm:flex-row"); licensePicker.SetStyle("float:left"); const t = Translations.t.image; @@ -186,8 +185,6 @@ export class ImageUploadFlow extends UIElement { } InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); - this._licensePicker.Update() const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement const selector = document.getElementById('fileselector-' + this.id) diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 13f7ae59e..1021a3c03 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -35,7 +35,6 @@ export class SlideShow extends UIElement { } protected InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); require("slick-carousel") if(this._embeddedElements.data.length == 0){ return; diff --git a/UI/Input/AndOrTagInput.ts b/UI/Input/AndOrTagInput.ts deleted file mode 100644 index 7f41ab11f..000000000 --- a/UI/Input/AndOrTagInput.ts +++ /dev/null @@ -1,164 +0,0 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import Combine from "../Base/Combine"; -import {SubtleButton} from "../Base/SubtleButton"; -import CheckBox from "./CheckBox"; -import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson"; -import {MultiTagInput} from "./MultiTagInput"; -import Svg from "../../Svg"; - -class AndOrConfig implements AndOrTagConfigJson { - public and: (string | AndOrTagConfigJson)[] = undefined; - public or: (string | AndOrTagConfigJson)[] = undefined; -} - - -export default class AndOrTagInput extends InputElement { - - private readonly _rawTags = new MultiTagInput(); - private readonly _subAndOrs: AndOrTagInput[] = []; - private readonly _isAnd: UIEventSource = new UIEventSource(true); - private readonly _isAndButton; - private readonly _addBlock: UIElement; - private readonly _value: UIEventSource = new UIEventSource(undefined); - - public bottomLeftButton: UIElement; - - IsSelected: UIEventSource; - - constructor() { - super(); - const self = this; - this._isAndButton = new CheckBox( - new SubtleButton(Svg.ampersand_ui(), null).SetClass("small-button"), - new SubtleButton(Svg.or_ui(), null).SetClass("small-button"), - this._isAnd); - - - this._addBlock = - new SubtleButton(Svg.addSmall_ui(), "Add an and/or-expression") - .SetClass("small-button") - .onClick(() => {self.createNewBlock()}); - - - this._isAnd.addCallback(() => self.UpdateValue()); - this._rawTags.GetValue().addCallback(() => { - self.UpdateValue() - }); - - this.IsSelected = this._rawTags.IsSelected; - - this._value.addCallback(tags => self.loadFromValue(tags)); - - } - - private createNewBlock(){ - const inputEl = new AndOrTagInput(); - inputEl.GetValue().addCallback(() => this.UpdateValue()); - const deleteButton = this.createDeleteButton(inputEl.id); - inputEl.bottomLeftButton = deleteButton; - this._subAndOrs.push(inputEl); - this.Update(); - } - - private createDeleteButton(elementId: string): UIElement { - const self = this; - return new SubtleButton(Svg.delete_icon_ui(), null).SetClass("small-button") - .onClick(() => { - for (let i = 0; i < self._subAndOrs.length; i++) { - if (self._subAndOrs[i].id === elementId) { - self._subAndOrs.splice(i, 1); - self.Update(); - self.UpdateValue(); - return; - } - } - }); - - } - - private loadFromValue(value: AndOrTagConfigJson) { - this._isAnd.setData(value.and !== undefined); - const tags = value.and ?? value.or; - const rawTags: string[] = []; - const subTags: AndOrTagConfigJson[] = []; - for (const tag of tags) { - - if (typeof (tag) === "string") { - rawTags.push(tag); - } else { - subTags.push(tag); - } - } - - for (let i = 0; i < rawTags.length; i++) { - if (this._rawTags.GetValue().data[i] !== rawTags[i]) { - // For some reason, 'setData' isn't stable as the comparison between the lists fails - // Probably because we generate a new list object every timee - // So we compare again here and update only if we find a difference - this._rawTags.GetValue().setData(rawTags); - break; - } - } - - while(this._subAndOrs.length < subTags.length){ - this.createNewBlock(); - } - - for (let i = 0; i < subTags.length; i++){ - let subTag = subTags[i]; - this._subAndOrs[i].GetValue().setData(subTag); - - } - - } - - private UpdateValue() { - const tags: (string | AndOrTagConfigJson)[] = []; - tags.push(...this._rawTags.GetValue().data); - - for (const subAndOr of this._subAndOrs) { - const subAndOrData = subAndOr._value.data; - if (subAndOrData === undefined) { - continue; - } - console.log(subAndOrData); - tags.push(subAndOrData); - } - - const tagConfig = new AndOrConfig(); - - if (this._isAnd.data) { - tagConfig.and = tags; - } else { - tagConfig.or = tags; - } - this._value.setData(tagConfig); - } - - GetValue(): UIEventSource { - return this._value; - } - - InnerRender(): string { - const leftColumn = new Combine([ - this._isAndButton, - "
    ", - this.bottomLeftButton ?? "" - ]); - const tags = new Combine([ - this._rawTags, - ...this._subAndOrs, - this._addBlock - ]).Render(); - return `
    ${leftColumn.Render()}${tags}
    `; - } - - - IsValid(t: AndOrTagConfigJson): boolean { - return true; - } - - -} \ No newline at end of file diff --git a/UI/Input/CheckBox.ts b/UI/Input/CheckBox.ts deleted file mode 100644 index e34707c7c..000000000 --- a/UI/Input/CheckBox.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {UIElement} from "../UIElement"; -import Translations from "../../UI/i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; - -export default class CheckBox extends UIElement{ - public readonly isEnabled: UIEventSource; - private readonly _showEnabled: UIElement; - private readonly _showDisabled: UIElement; - - constructor(showEnabled: string | UIElement, showDisabled: string | UIElement, data: UIEventSource | boolean = false) { - super(undefined); - this.isEnabled = - data instanceof UIEventSource ? data : new UIEventSource(data ?? false); - this.ListenTo(this.isEnabled); - this._showEnabled = Translations.W(showEnabled); - this._showDisabled =Translations.W(showDisabled); - const self = this; - this.onClick(() => { - self.isEnabled.setData(!self.isEnabled.data); - }) - - } - - InnerRender(): string { - if (this.isEnabled.data) { - return Translations.W(this._showEnabled).Render(); - } else { - return Translations.W(this._showDisabled).Render(); - } - } - -} \ No newline at end of file diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index 4a34ee746..cd0dbcac9 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -16,7 +16,6 @@ export default class CheckBoxes extends InputElement { constructor(elements: UIElement[]) { super(undefined); this._elements = Utils.NoNull(elements); - this.dumbMode = false; this.value = new UIEventSource([]) this.ListenTo(this.value); @@ -51,7 +50,6 @@ export default class CheckBoxes extends InputElement { } protected InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); const self = this; for (let i = 0; i < this._elements.length; i++) { diff --git a/UI/Input/ColorPicker.ts b/UI/Input/ColorPicker.ts index 8a0b5e88d..2fe6adf0d 100644 --- a/UI/Input/ColorPicker.ts +++ b/UI/Input/ColorPicker.ts @@ -1,6 +1,5 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; -import {Utils} from "../../Utils"; export default class ColorPicker extends InputElement { diff --git a/UI/Input/CombinedInputElement.ts b/UI/Input/CombinedInputElement.ts index b15c0a516..83672e9a6 100644 --- a/UI/Input/CombinedInputElement.ts +++ b/UI/Input/CombinedInputElement.ts @@ -1,14 +1,16 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; -import {UIElement} from "../UIElement"; +import BaseUIElement from "../BaseUIElement"; export default class CombinedInputElement extends InputElement { + protected InnerConstructElement(): HTMLElement { + return this._combined.ConstructElement(); + } private readonly _a: InputElement; - private readonly _b: UIElement; - private readonly _combined: UIElement; + private readonly _b: BaseUIElement; + private readonly _combined: BaseUIElement; public readonly IsSelected: UIEventSource; - constructor(a: InputElement, b: InputElement) { super(); this._a = a; @@ -23,11 +25,6 @@ export default class CombinedInputElement extends InputElement { return this._a.GetValue(); } - InnerRender(): string { - return this._combined.Render(); - } - - IsValid(t: T): boolean { return this._a.IsValid(t); } diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index 008d75bcd..bed2d9c92 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -14,7 +14,6 @@ export default class DirectionInput extends InputElement { constructor(value?: UIEventSource) { super(); - this.dumbMode = false; this.value = value ?? new UIEventSource(undefined); this.value.addCallbackAndRun(rotation => { @@ -48,7 +47,6 @@ export default class DirectionInput extends InputElement { } protected InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); const self = this; function onPosChange(x: number, y: number) { diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts index d9ecba05e..4e376e7ef 100644 --- a/UI/Input/DropDown.ts +++ b/UI/Input/DropDown.ts @@ -1,50 +1,81 @@ -import {UIElement} from "../UIElement"; import {InputElement} from "./InputElement"; import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; export class DropDown extends InputElement { - private readonly _label: UIElement; - private readonly _values: { value: T; shown: UIElement }[]; + private static _nextDropdownId = 0; + public IsSelected: UIEventSource = new UIEventSource(false); + + private readonly _element: HTMLElement; private readonly _value: UIEventSource; + private readonly _values: { value: T; shown: string | BaseUIElement }[]; - public IsSelected: UIEventSource = new UIEventSource(false); - private readonly _label_class: string; - private readonly _select_class: string; - private _form_style: string; - - constructor(label: string | UIElement, - values: { value: T, shown: string | UIElement }[], + constructor(label: string | BaseUIElement, + values: { value: T, shown: string | BaseUIElement }[], value: UIEventSource = undefined, - label_class: string = "", - select_class: string = "", - form_style: string = "flex") { - super(undefined); - this._form_style = form_style; - this._value = value ?? new UIEventSource(undefined); - this._label = Translations.W(label); - this._label_class = label_class || ''; - this._select_class = select_class || ''; - this._values = values.map(v => { - return { - value: v.value, - shown: Translations.W(v.shown) + options?: { + select_class?: string } - } - ); - for (const v of this._values) { - this.ListenTo(v.shown._source); + ) { + super(); +this._values = values; + if (values.length <= 1) { + return; } - this.ListenTo(this._value); - this.onClick(() => {}) // by registering a click, the click event is consumed and doesn't bubble furter to other elements, e.g. checkboxes + const id = DropDown._nextDropdownId; + DropDown._nextDropdownId++; + + + const el = document.createElement("form") + this._element = el; + el.id = "dropdown" + id; + + { + const labelEl = Translations.W(label).ConstructElement() + const labelHtml = document.createElement("label") + labelHtml.appendChild(labelEl) + labelHtml.htmlFor = el.id; + } + + + { + const select = document.createElement("select") + select.classList.add(...(options?.select_class?.split(" ") ?? [])) + for (let i = 0; i < values.length; i++) { + + const option = document.createElement("option") + option.value = "" + i + option.appendChild(Translations.W(values[i].shown).ConstructElement()) + } + + select.onchange = (() => { + var index = select.selectedIndex; + value.setData(values[index].value); + }); + + value.addCallbackAndRun(selected => { + for (let i = 0; i < values.length; i++) { + const value = values[i].value; + if (value === selected) { + select.selectedIndex = i; + } + } + }) + } + + + this.onClick(() => { + }) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes } GetValue(): UIEventSource { return this._value; } + IsValid(t: T): boolean { for (const value of this._values) { if (value.value === t) { @@ -54,44 +85,8 @@ export class DropDown extends InputElement { return false } - - InnerRender(): string { - if(this._values.length <=1){ - return ""; - } - - let options = ""; - for (let i = 0; i < this._values.length; i++) { - options += "" - } - - return `
    ` + - `` + - `` + - `
    `; + protected InnerConstructElement(): HTMLElement { + return this._element; } - protected InnerUpdate(element) { - var e = document.getElementById("dropdown-" + this.id); - if(e === null){ - return; - } - const self = this; - e.onchange = (() => { - // @ts-ignore - var index = parseInt(e.selectedIndex); - self._value.setData(self._values[index].value); - }); - - var t = this._value.data; - for (let i = 0; i < this._values.length ; i++) { - const value = this._values[i].value; - if (value === t) { - // @ts-ignore - e.selectedIndex = i; - } - } - } } \ No newline at end of file diff --git a/UI/Input/InputElement.ts b/UI/Input/InputElement.ts index 013168862..1b5eea403 100644 --- a/UI/Input/InputElement.ts +++ b/UI/Input/InputElement.ts @@ -1,7 +1,7 @@ -import {UIElement} from "../UIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; -export abstract class InputElement extends UIElement{ +export abstract class InputElement extends BaseUIElement{ abstract GetValue() : UIEventSource; abstract IsSelected: UIEventSource; diff --git a/UI/Input/InputElementMap.ts b/UI/Input/InputElementMap.ts index 7a0286b90..548e50363 100644 --- a/UI/Input/InputElementMap.ts +++ b/UI/Input/InputElementMap.ts @@ -3,13 +3,12 @@ import {UIEventSource} from "../../Logic/UIEventSource"; export default class InputElementMap extends InputElement { - + public readonly IsSelected: UIEventSource; private readonly _inputElement: InputElement; private isSame: (x0: X, x1: X) => boolean; 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, @@ -41,19 +40,19 @@ export default class InputElementMap extends InputElement { return this._value; } - InnerRender(): string { - return this._inputElement.InnerRender(); - } - IsValid(x: X): boolean { - if(x === undefined){ + if (x === undefined) { return false; } const t = this.fromX(x); - if(t === undefined){ + if (t === undefined) { return false; } return this._inputElement.IsValid(t); } - + + protected InnerConstructElement(): HTMLElement { + return this._inputElement.ConstructElement(); + } + } \ No newline at end of file diff --git a/UI/Input/MultiInput.ts b/UI/Input/MultiInput.ts deleted file mode 100644 index 48826f9be..000000000 --- a/UI/Input/MultiInput.ts +++ /dev/null @@ -1,125 +0,0 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import Combine from "../Base/Combine"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; - -export class MultiInput extends InputElement { - - private readonly _value: UIEventSource; - IsSelected: UIEventSource; - private elements: UIElement[] = []; - private inputElements: InputElement[] = []; - private addTag: UIElement; - private _options: { allowMovement?: boolean }; - - constructor( - addAElement: string, - newElement: (() => T), - createInput: (() => InputElement), - value: UIEventSource = undefined, - options?: { - allowMovement?: boolean - }) { - super(undefined); - this._value = value ?? new UIEventSource([]); - value = this._value; - this.ListenTo(value.map((latest : T[]) => latest.length)); - this._options = options ?? {}; - - this.addTag = new SubtleButton(Svg.addSmall_ui(), addAElement) - .SetClass("small-button") - .onClick(() => { - this.IsSelected.setData(true); - value.data.push(newElement()); - value.ping(); - }); - const self = this; - value.map((tags: string[]) => tags.length).addCallback(() => self.createElements(createInput)); - this.createElements(createInput); - - this._value.addCallback(tags => self.load(tags)); - this.IsSelected = new UIEventSource(false); - } - - private load(tags: T[]) { - if (tags === undefined) { - return; - } - for (let i = 0; i < tags.length; i++) { - this.inputElements[i].GetValue().setData(tags[i]); - } - } - - private UpdateIsSelected(){ - this.IsSelected.setData(this.inputElements.map(input => input.IsSelected.data).reduce((a,b) => a && b)) - } - - private createElements(createInput: (() => InputElement)) { - this.inputElements.splice(0, this.inputElements.length); - this.elements = []; - const self = this; - for (let i = 0; i < this._value.data.length; i++) { - const input = createInput(); - input.GetValue().addCallback(tag => { - self._value.data[i] = tag; - self._value.ping(); - } - ); - this.inputElements.push(input); - input.IsSelected.addCallback(() => this.UpdateIsSelected()); - - const moveUpBtn = Svg.up_ui() - .SetClass('small-image').onClick(() => { - const v = self._value.data[i]; - self._value.data[i] = self._value.data[i - 1]; - self._value.data[i - 1] = v; - self._value.ping(); - }); - - const moveDownBtn = - Svg.down_ui() - .SetClass('small-image') .onClick(() => { - const v = self._value.data[i]; - self._value.data[i] = self._value.data[i + 1]; - self._value.data[i + 1] = v; - self._value.ping(); - }); - - const controls = []; - if (i > 0 && this._options.allowMovement) { - controls.push(moveUpBtn); - } - - if (i + 1 < this._value.data.length && this._options.allowMovement) { - controls.push(moveDownBtn); - } - - - const deleteBtn = - Svg.delete_icon_ui().SetClass('small-image') - .onClick(() => { - self._value.data.splice(i, 1); - self._value.ping(); - }); - controls.push(deleteBtn); - this.elements.push(new Combine([input.SetStyle("width: calc(100% - 2em - 5px)"), new Combine(controls).SetStyle("display:flex;flex-direction:column;width:min-content;")]).SetClass("tag-input-row")) - } - - this.Update(); - } - - InnerRender(): string { - return new Combine([...this.elements, this.addTag]).Render(); - } - - IsValid(t: T[]): boolean { - return false; - } - - GetValue(): UIEventSource { - return this._value; - } - -} \ No newline at end of file diff --git a/UI/Input/MultiLingualTextFields.ts b/UI/Input/MultiLingualTextFields.ts deleted file mode 100644 index d1f69fbf1..000000000 --- a/UI/Input/MultiLingualTextFields.ts +++ /dev/null @@ -1,99 +0,0 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {TextField} from "./TextField"; - -export default class MultiLingualTextFields extends InputElement { - private _fields: Map = new Map(); - private readonly _value: UIEventSource; - public readonly IsSelected: UIEventSource = new UIEventSource(false); - constructor(languages: UIEventSource, - textArea: boolean = false, - value: UIEventSource>> = undefined) { - super(undefined); - this._value = value ?? new UIEventSource({}); - this._value.addCallbackAndRun(latestData => { - if (typeof (latestData) === "string") { - console.warn("Refusing string for multilingual input", latestData); - self._value.setData({}); - } - }) - - const self = this; - - function setup(languages: string[]) { - if (languages === undefined) { - return; - } - const newFields = new Map(); - for (const language of languages) { - if (language.length != 2) { - continue; - } - - let oldField = self._fields.get(language); - if (oldField === undefined) { - oldField = new TextField({textArea: textArea}); - oldField.GetValue().addCallback(str => { - self._value.data[language] = str; - self._value.ping(); - }); - oldField.GetValue().setData(self._value.data[language]); - - oldField.IsSelected.addCallback(() => { - let selected = false; - self._fields.forEach(value => {selected = selected || value.IsSelected.data}); - self.IsSelected.setData(selected); - }) - - } - newFields.set(language, oldField); - } - self._fields = newFields; - self.Update(); - - - } - - setup(languages.data); - languages.addCallback(setup); - - - function load(latest: any){ - if(latest === undefined){ - return; - } - for (const lang in latest) { - self._fields.get(lang)?.GetValue().setData(latest[lang]); - } - } - this._value.addCallback(load); - load(this._value.data); - } - - protected InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); - this._fields.forEach(value => value.Update()); - } - - GetValue(): UIEventSource>> { - return this._value; - } - - InnerRender(): string { - let html = ""; - this._fields.forEach((field, lang) => { - html += `${lang}${field.Render()}` - }) - if(html === ""){ - return "Please define one or more languages" - } - - return `${html}
    `; - } - - - IsValid(t: any): boolean { - return true; - } - -} \ No newline at end of file diff --git a/UI/Input/MultiTagInput.ts b/UI/Input/MultiTagInput.ts deleted file mode 100644 index 30295bc8a..000000000 --- a/UI/Input/MultiTagInput.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import TagInput from "./SingleTagInput"; -import {MultiInput} from "./MultiInput"; - -export class MultiTagInput extends MultiInput { - - constructor(value: UIEventSource = new UIEventSource([])) { - super("Add a new tag", - () => "", - () => new TagInput(), - value - ); - } - -} \ No newline at end of file diff --git a/UI/Input/NumberField.ts b/UI/Input/NumberField.ts deleted file mode 100644 index d825bdf2f..000000000 --- a/UI/Input/NumberField.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {UIElement} from "../UIElement"; -import {InputElement} from "./InputElement"; -import Translations from "../i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; - -export class NumberField extends InputElement { - private readonly value: UIEventSource; - public readonly enterPressed = new UIEventSource(undefined); - private readonly _placeholder: UIElement; - private options?: { - placeholder?: string | UIElement, - value?: UIEventSource, - isValid?: ((i: number) => boolean), - min?: number, - max?: number - }; - public readonly IsSelected: UIEventSource = new UIEventSource(false); - private readonly _isValid: (i:number) => boolean; - - constructor(options?: { - placeholder?: string | UIElement, - value?: UIEventSource, - isValid?: ((i:number) => boolean), - min?: number, - max?:number - }) { - super(undefined); - this.options = options; - const self = this; - this.value = new UIEventSource(undefined); - this.value = options?.value ?? new UIEventSource(undefined); - - this._isValid = options.isValid ?? ((i) => true); - - this._placeholder = Translations.W(options.placeholder ?? ""); - this.ListenTo(this._placeholder._source); - - this.onClick(() => { - self.IsSelected.setData(true) - }); - this.value.addCallback((t) => { - const field = document.getElementById("txt-"+this.id); - if (field === undefined || field === null) { - return; - } - field.className = self.IsValid(t) ? "" : "invalid"; - - if (t === undefined || t === null) { - return; - } - // @ts-ignore - field.value = t; - }); - this.dumbMode = false; - } - - GetValue(): UIEventSource { - return this.value; - } - - InnerRender(): string { - - const placeholder = this._placeholder.InnerRender().replace("'", "'"); - - let min = ""; - if(this.options.min){ - min = `min='${this.options.min}'`; - } - - let max = ""; - if(this.options.min){ - max = `max='${this.options.max}'`; - } - - return `
    ` + - `` + - `
    `; - } - - InnerUpdate() { - const field = document.getElementById("txt-" + this.id); - const self = this; - field.oninput = () => { - - // How much characters are on the right, not including spaces? - // @ts-ignore - const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length; - // @ts-ignore - let val: number = Number(field.value); - if (!self.IsValid(val)) { - self.value.setData(undefined); - } else { - self.value.setData(val); - } - - }; - - 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)); - - - field.addEventListener("keyup", function (event) { - if (event.key === "Enter") { - // @ts-ignore - self.enterPressed.setData(field.value); - } - }); - - } - - IsValid(t: number): boolean { - if (t === undefined || t === null) { - return false - } - return this._isValid(t); - } - -} \ No newline at end of file diff --git a/UI/Input/SimpleDatePicker.ts b/UI/Input/SimpleDatePicker.ts index 3a6b203be..78c9db746 100644 --- a/UI/Input/SimpleDatePicker.ts +++ b/UI/Input/SimpleDatePicker.ts @@ -5,48 +5,37 @@ export default class SimpleDatePicker extends InputElement { private readonly value: UIEventSource + private readonly _element: HTMLElement; + constructor( value?: UIEventSource ) { super(); this.value = value ?? new UIEventSource(undefined); const self = this; + + const el = document.createElement("input") + this._element = el; + el.type = "date" + el.oninput = () => { + // Already in YYYY-MM-DD value! + self.value.setData(el.value); + } + + this.value.addCallbackAndRun(v => { if(v === undefined){ return; } - self.SetValue(v); + el.value = 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); - } - } + protected InnerConstructElement(): HTMLElement { + return this._element + } GetValue(): UIEventSource { return this.value; } diff --git a/UI/Input/SingleTagInput.ts b/UI/Input/SingleTagInput.ts deleted file mode 100644 index e7525da01..000000000 --- a/UI/Input/SingleTagInput.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {DropDown} from "./DropDown"; -import {TextField} from "./TextField"; -import Combine from "../Base/Combine"; -import {Utils} from "../../Utils"; -import {UIElement} from "../UIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {FromJSON} from "../../Customizations/JSON/FromJSON"; -import ValidatedTextField from "./ValidatedTextField"; - -export default class SingleTagInput extends InputElement { - - private readonly _value: UIEventSource; - IsSelected: UIEventSource; - - private key: InputElement; - private value: InputElement; - private operator: DropDown - private readonly helpMessage: UIElement; - - constructor(value: UIEventSource = undefined) { - super(undefined); - this._value = value ?? new UIEventSource(""); - this.helpMessage = new VariableUiElement(this._value.map(tagDef => { - try { - FromJSON.Tag(tagDef, ""); - return ""; - } catch (e) { - return `
    ${e}` - } - } - )); - - this.key = ValidatedTextField.KeyInput(); - - this.value = new TextField({ - placeholder: "value - if blank, matches if key is NOT present", - value: new UIEventSource("") - } - ); - - this.operator = new DropDown("", [ - {value: "=", shown: "="}, - {value: "~", shown: "~"}, - {value: "!~", shown: "!~"} - ]); - this.operator.GetValue().setData("="); - - const self = this; - - function updateValue() { - if (self.key.GetValue().data === undefined || - self.value.GetValue().data === undefined || - self.operator.GetValue().data === undefined) { - return undefined; - } - self._value.setData(self.key.GetValue().data + self.operator.GetValue().data + self.value.GetValue().data); - } - - this.key.GetValue().addCallback(() => updateValue()); - this.operator.GetValue().addCallback(() => updateValue()); - this.value.GetValue().addCallback(() => updateValue()); - - - function loadValue(value: string) { - if (value === undefined) { - return; - } - let parts: string[]; - if (value.indexOf("=") >= 0) { - parts = Utils.SplitFirst(value, "="); - self.operator.GetValue().setData("="); - } else if (value.indexOf("!~") > 0) { - parts = Utils.SplitFirst(value, "!~"); - self.operator.GetValue().setData("!~"); - - } else if (value.indexOf("~") > 0) { - parts = Utils.SplitFirst(value, "~"); - self.operator.GetValue().setData("~"); - } else { - console.warn("Invalid value for tag: ", value) - return; - } - self.key.GetValue().setData(parts[0]); - self.value.GetValue().setData(parts[1]); - } - - self._value.addCallback(loadValue); - loadValue(self._value.data); - this.IsSelected = this.key.IsSelected.map( - isSelected => isSelected || this.value.IsSelected.data, [this.value.IsSelected] - ) - } - - IsValid(t: string): boolean { - return false; - } - - InnerRender(): string { - return new Combine([ - this.key, this.operator, this.value, - this.helpMessage - ]).Render(); - } - - - GetValue(): UIEventSource { - return this._value; - } - - -} \ No newline at end of file diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index ccb4da1e3..a3ce32a65 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -1,99 +1,85 @@ -import {UIElement} from "../UIElement"; import {InputElement} from "./InputElement"; import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; +import BaseUIElement from "../BaseUIElement"; export class TextField extends InputElement { private readonly value: UIEventSource; public readonly enterPressed = new UIEventSource(undefined); - private readonly _placeholder: UIElement; public readonly IsSelected: UIEventSource = new UIEventSource(false); - private readonly _htmlType: string; - private readonly _inputMode : string; - private readonly _textAreaRows: number; - - private readonly _isValid: (string,country) => boolean; - private _label: UIElement; + + private _element: HTMLElement; + private readonly _isValid: (s: string, country?: () => string) => boolean; constructor(options?: { - placeholder?: string | UIElement, + placeholder?: string | BaseUIElement, value?: UIEventSource, textArea?: boolean, htmlType?: string, inputMode?: string, - label?: UIElement, + label?: BaseUIElement, textAreaRows?: number, isValid?: ((s: string, country?: () => string) => boolean) }) { - super(undefined); + super(); const self = this; - this.value = new UIEventSource(""); options = options ?? {}; - this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text"); this.value = options?.value ?? new UIEventSource(undefined); - - this._label = options.label; - this._textAreaRows = options.textAreaRows; - this._isValid = options.isValid ?? ((str, country) => true); - - this._placeholder = Translations.W(options.placeholder ?? ""); - this._inputMode = options.inputMode; - this.ListenTo(this._placeholder._source); - + this._isValid = options.isValid ?? (_ => true); + this.onClick(() => { self.IsSelected.setData(true) }); - this.value.addCallback((t) => { - const field = document.getElementById("txt-"+this.id); - if (field === undefined || field === null) { - return; - } - field.className = self.IsValid(t) ? "" : "invalid"; - if (t === undefined || t === null) { + + + const placeholder = Translations.W(options. placeholder ?? "").ConstructElement().innerText.replace("'", "'"); + + this.SetClass("form-text-field") + let inputEl : HTMLElement + if(options.htmlType === "area"){ + const el = document.createElement("textarea") + el.placeholder = placeholder + el.rows = options.textAreaRows + el.cols = 50 + el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box" + inputEl = el; + }else{ + const el = document.createElement("input") + el.type = options.htmlType + el.inputMode = options.inputMode + el.placeholder = placeholder + inputEl = el + } + + const form = document.createElement("form") + form.onsubmit = () => false; + + if(options.label){ + form.appendChild(options.label.ConstructElement()) + } + + this._element = form; + + const field = inputEl; + + + this.value.addCallbackAndRun(value => { + if (!(value !== undefined && value !== null)) { return; } // @ts-ignore - field.value = t; - }); - this.dumbMode = false; - } + field.value = value; + if(self.IsValid(value)){ + self.RemoveClass("invalid") + }else{ + self.SetClass("invalid") + } - GetValue(): UIEventSource { - return this.value; - } + }) - InnerRender(): string { - - const placeholder = this._placeholder.InnerRender().replace("'", "'"); - if (this._htmlType === "area") { - return `` - } - - let label = ""; - if (this._label != undefined) { - label = this._label.Render(); - } - let inputMode = "" - if(this._inputMode !== undefined){ - inputMode = `inputmode="${this._inputMode}" ` - } - return new Combine([ - ``, - `
    `, - label, - ``, - `
    `, - `
    ` - ]).Render(); - } - - InnerUpdate() { - const field = document.getElementById("txt-" + this.id); - const self = this; field.oninput = () => { - + // How much characters are on the right, not including spaces? // @ts-ignore const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length; @@ -107,11 +93,11 @@ export class TextField extends InputElement { // 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 val = field.value; let newCursorPos = val.length - endDistance; - while(newCursorPos >= 0 && + while(newCursorPos >= 0 && // We count the number of _actual_ characters (non-space characters) on the right of the new value // This count should become bigger then the end distance val.substr(newCursorPos).replace(/ /g, '').length < endDistance @@ -119,14 +105,10 @@ export class TextField extends InputElement { newCursorPos --; } // @ts-ignore - self.SetCursorPosition(newCursorPos); + TextField.SetCursorPosition(newCursorPos); }; - 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)); @@ -136,22 +118,31 @@ export class TextField extends InputElement { // @ts-ignore self.enterPressed.setData(field.value); } - }); + }); + + } - public SetCursorPosition(i: number) { - const field = document.getElementById('txt-' + this.id); - if(field === undefined || field === null){ + GetValue(): UIEventSource { + return this.value; + } + + protected InnerConstructElement(): HTMLElement { + return this._element; + } + + private static SetCursorPosition(textfield: HTMLElement, i: number) { + if(textfield === undefined || textfield === null){ return; } if (i === -1) { // @ts-ignore - i = field.value.length; + i = textfield.value.length; } - field.focus(); + textfield.focus(); // @ts-ignore - field.setSelectionRange(i, i); + textfield.setSelectionRange(i, i); } diff --git a/UI/Input/Toggle.ts b/UI/Input/Toggle.ts new file mode 100644 index 000000000..f4fbdb20e --- /dev/null +++ b/UI/Input/Toggle.ts @@ -0,0 +1,22 @@ +import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; + +/** + * The 'Toggle' is a UIElement showing either one of two elements, depending on the state. + * It can be used to implement e.g. checkboxes or collapsible elements + */ +export default class Toggle extends VariableUiElement{ + + public readonly isEnabled: UIEventSource; + + constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, data: UIEventSource = new UIEventSource(false)) { + super( + data.map(isEnabled => isEnabled ? showEnabled : showDisabled) + ); + this.onClick(() => { + data.setData(!data.data); + }) + + } +} \ No newline at end of file diff --git a/UI/LanguagePicker.ts b/UI/LanguagePicker.ts index 61f276dda..e7c80e419 100644 --- a/UI/LanguagePicker.ts +++ b/UI/LanguagePicker.ts @@ -1,8 +1,6 @@ import {UIElement} from "./UIElement"; import {DropDown} from "./Input/DropDown"; import Locale from "./i18n/Locale"; -import Svg from "../Svg"; -import Img from "./Base/Img"; export default class LanguagePicker { @@ -18,7 +16,7 @@ export default class LanguagePicker { return new DropDown(label, languages.map(lang => { return {value: lang, shown: lang} } - ), Locale.language, '', 'bg-indigo-100 p-1 rounded hover:bg-indigo-200'); + ), Locale.language, { select_class: 'bg-indigo-100 p-1 rounded hover:bg-indigo-200'}); } diff --git a/UI/MapControlButton.ts b/UI/MapControlButton.ts index b5e4b2aac..877cd8553 100644 --- a/UI/MapControlButton.ts +++ b/UI/MapControlButton.ts @@ -13,8 +13,8 @@ export default class MapControlButton extends UIElement { this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);"); } - InnerRender(): string { - return this._contents.Render(); + InnerRender() { + return this._contents; } } \ No newline at end of file diff --git a/UI/OpeningHours/OhVisualization.ts b/UI/OpeningHours/OhVisualization.ts index e930228aa..6ac2b3324 100644 --- a/UI/OpeningHours/OhVisualization.ts +++ b/UI/OpeningHours/OhVisualization.ts @@ -9,21 +9,28 @@ import Constants from "../../Models/Constants"; import opening_hours from "opening_hours"; export default class OpeningHoursVisualization extends UIElement { + private static readonly weekdays = [ + Translations.t.general.weekdays.abbreviations.monday, + Translations.t.general.weekdays.abbreviations.tuesday, + Translations.t.general.weekdays.abbreviations.wednesday, + Translations.t.general.weekdays.abbreviations.thursday, + Translations.t.general.weekdays.abbreviations.friday, + Translations.t.general.weekdays.abbreviations.saturday, + Translations.t.general.weekdays.abbreviations.sunday, + ] private readonly _key: string; constructor(tags: UIEventSource, key: string) { super(tags); this._key = key; - this.ListenTo(UIEventSource.Chronic(60*1000)); // Automatically reload every minute + this.ListenTo(UIEventSource.Chronic(60 * 1000)); // Automatically reload every minute this.ListenTo(UIEventSource.Chronic(500, () => { return tags.data._country === undefined; })); - - - } - + } + private static GetRanges(oh: any, from: Date, to: Date): ({ isOpen: boolean, isSpecial: boolean, @@ -38,7 +45,7 @@ export default class OpeningHoursVisualization extends UIElement { const start = new Date(from); // We go one day more into the past, in order to force rendering of holidays in the start of the period start.setDate(from.getDate() - 1); - + const iterator = oh.getIterator(start); let prevValue = undefined; @@ -63,8 +70,8 @@ export default class OpeningHoursVisualization extends UIElement { // simply closed, nothing special here continue; } - - if(value.startDate < from){ + + if (value.startDate < from) { continue; } // Get day: sunday is 0, monday is 1. We move everything so that monday == 0 @@ -80,8 +87,190 @@ export default class OpeningHoursVisualization extends UIElement { return new Date(d.setDate(diff)); } + InnerRender(): string | UIElement { - private allChangeMoments(ranges: { + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const lastMonday = OpeningHoursVisualization.getMonday(today); + const nextSunday = new Date(lastMonday); + nextSunday.setDate(nextSunday.getDate() + 7); + + const tags = this._source.data; + if (tags._country === undefined) { + return "Loading country information..."; + } + let oh = null; + + try { + // noinspection JSPotentiallyInvalidConstructorUsage + oh = new opening_hours(tags[this._key], { + lat: tags._lat, + lon: tags._lon, + address: { + country_code: tags._country + } + }, {tag_key: this._key}); + } catch (e) { + console.log(e); + return new Combine([Translations.t.general.opening_hours.error_loading, + State.state?.osmConnection?.userDetails?.data?.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked ? + `${e}` + : "" + ]); + } + + if (!oh.getState() && !oh.getUnknown()) { + // POI is currently closed + const nextChange: Date = oh.getNextChange(); + if ( + // Shop isn't gonna open anymore in this timerange + nextSunday < nextChange + // And we are already in the weekend to show next week + && (today.getDay() == 0 || today.getDay() == 6) + ) { + // We mover further along + lastMonday.setDate(lastMonday.getDate() + 7); + nextSunday.setDate(nextSunday.getDate() + 7); + } + } + + // ranges[0] are all ranges for monday + const ranges = OpeningHoursVisualization.GetRanges(oh, lastMonday, nextSunday); + if (ranges.map(r => r.length).reduce((a, b) => a + b, 0) == 0) { + // Closed! + const opensAtDate = oh.getNextChange(); + if (opensAtDate === undefined) { + const comm = oh.getComment() ?? oh.getUnknown(); + if (!!comm) { + return new FixedUiElement(comm).SetClass("ohviz-closed"); + } + + if (oh.getState()) { + return Translations.t.general.opening_hours.open_24_7.SetClass("ohviz-closed") + } + return Translations.t.general.opening_hours.closed_permanently.SetClass("ohviz-closed") + } + const moment = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}` + return Translations.t.general.opening_hours.closed_until.Subs({date: moment}).SetClass("ohviz-closed") + } + + const isWeekstable = oh.isWeekStable(); + + let [changeHours, changeHourText] = OpeningHoursVisualization.allChangeMoments(ranges); + + // By default, we always show the range between 8 - 19h, in order to give a stable impression + // Ofc, a bigger range is used if needed + const earliestOpen = Math.min(8 * 60 * 60, ...changeHours); + let latestclose = Math.max(...changeHours); + // We always make sure there is 30m of leeway in order to give enough room for the closing entry + latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60) + + + const rows: UIElement[] = []; + const availableArea = latestclose - earliestOpen; + // @ts-ignore + const now = (100 * (((new Date() - today) / 1000) - earliestOpen)) / availableArea; + + + let header: UIElement[] = []; + + if (now >= 0 && now <= 100) { + header.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now")) + } + for (const changeMoment of changeHours) { + const offset = 100 * (changeMoment - earliestOpen) / availableArea; + if (offset < 0 || offset > 100) { + continue; + } + const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line"); + header.push(el); + } + + for (let i = 0; i < changeHours.length; i++) { + let changeMoment = changeHours[i]; + const offset = 100 * (changeMoment - earliestOpen) / availableArea; + if (offset < 0 || offset > 100) { + continue; + } + const el = new FixedUiElement( + `
    ${changeHourText[i]}
    ` + ) + .SetStyle(`left:${offset}%`) + .SetClass("ohviz-time-indication"); + header.push(el); + } + + rows.push(new Combine([` `, + ``, + new Combine(header), ``])); + + for (let i = 0; i < 7; i++) { + const dayRanges = ranges[i]; + const isToday = (new Date().getDay() + 6) % 7 === i; + let weekday = OpeningHoursVisualization.weekdays[i]; + + let dateToShow = "" + if (!isWeekstable) { + const day = new Date(lastMonday) + day.setDate(day.getDate() + i); + dateToShow = "" + day.getDate() + "/" + (day.getMonth() + 1); + } + + let innerContent: (string | UIElement)[] = []; + + // Add the lines + for (const changeMoment of changeHours) { + const offset = 100 * (changeMoment - earliestOpen) / availableArea; + innerContent.push(new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line")) + } + + // Add the actual ranges + for (const range of dayRanges) { + if (!range.isOpen && !range.isSpecial) { + innerContent.push( + new FixedUiElement(range.comment ?? dateToShow).SetClass("ohviz-day-off")) + continue; + } + + const startOfDay: Date = new Date(range.startDate); + startOfDay.setHours(0, 0, 0, 0); + // @ts-ignore + const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen; + // @ts-ignore + const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen); + const startPercentage = (100 * startpoint / availableArea); + innerContent.push( + new FixedUiElement(range.comment ?? dateToShow).SetStyle(`left:${startPercentage}%; width:${width}%`).SetClass("ohviz-range")) + } + + // Add line for 'now' + if (now >= 0 && now <= 100) { + innerContent.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now")) + } + + let clss = "" + if (isToday) { + clss = "ohviz-today" + } + + rows.push(new Combine( + [`${weekday}`, + ``, + ...innerContent, + ``])) + } + + + return new Combine([ + "", + ...rows.map(el => "" + el.Render() + ""), + "
    " + ]).SetClass("ohviz-container"); + } + + private static allChangeMoments(ranges: { isOpen: boolean, isSpecial: boolean, comment: string, @@ -131,194 +320,4 @@ export default class OpeningHoursVisualization extends UIElement { return [changeHours, changeHourText] } - private static readonly weekdays = [ - Translations.t.general.weekdays.abbreviations.monday, - Translations.t.general.weekdays.abbreviations.tuesday, - Translations.t.general.weekdays.abbreviations.wednesday, - Translations.t.general.weekdays.abbreviations.thursday, - Translations.t.general.weekdays.abbreviations.friday, - Translations.t.general.weekdays.abbreviations.saturday, - Translations.t.general.weekdays.abbreviations.sunday, - ] - - InnerRender(): string { - - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const lastMonday = OpeningHoursVisualization.getMonday(today); - const nextSunday = new Date(lastMonday); - nextSunday.setDate(nextSunday.getDate() + 7); - - const tags = this._source.data; - if (tags._country === undefined) { - return "Loading country information..."; - } - let oh = null; - - try { - oh = new opening_hours(tags[this._key], { - lat: tags._lat, - lon: tags._lon, - address: { - country_code: tags._country - } - }, {tag_key: this._key}); - } catch (e) { - console.log(e); - const msg = new Combine([Translations.t.general.opening_hours.error_loading, - State.state?.osmConnection?.userDetails?.data?.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked ? - `${e}` - : "" - ]); - return msg.Render(); - } - - if (!oh.getState() && !oh.getUnknown()) { - // POI is currently closed - const nextChange: Date = oh.getNextChange(); - if ( - // Shop isn't gonna open anymore in this timerange - nextSunday < nextChange - // And we are already in the weekend to show next week - && (today.getDay() == 0 || today.getDay() == 6) - ) { - // We mover further along - lastMonday.setDate(lastMonday.getDate() + 7); - nextSunday.setDate(nextSunday.getDate() + 7); - } - } - - // ranges[0] are all ranges for monday - const ranges = OpeningHoursVisualization.GetRanges(oh, lastMonday, nextSunday); - if (ranges.map(r => r.length).reduce((a, b) => a + b, 0) == 0) { - // Closed! - const opensAtDate = oh.getNextChange(); - if(opensAtDate === undefined){ - const comm = oh.getComment() ?? oh.getUnknown(); - if(!!comm){ - return new FixedUiElement(comm).SetClass("ohviz-closed").Render(); - } - - if(oh.getState()){ - return Translations.t.general.opening_hours.open_24_7.SetClass("ohviz-closed").Render() - } - return Translations.t.general.opening_hours.closed_permanently.SetClass("ohviz-closed").Render() - } - const moment = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}` - return Translations.t.general.opening_hours.closed_until.Subs({date: moment}).SetClass("ohviz-closed").Render() - } - - const isWeekstable = oh.isWeekStable(); - - let [changeHours, changeHourText] = this.allChangeMoments(ranges); - - // By default, we always show the range between 8 - 19h, in order to give a stable impression - // Ofc, a bigger range is used if needed - const earliestOpen = Math.min(8 * 60 * 60, ...changeHours); - let latestclose = Math.max(...changeHours); - // We always make sure there is 30m of leeway in order to give enough room for the closing entry - latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60) - - - const rows: UIElement[] = []; - const availableArea = latestclose - earliestOpen; - // @ts-ignore - const now = (100 * (((new Date() - today) / 1000) - earliestOpen)) / availableArea; - - - let header = ""; - - if (now >= 0 && now <= 100) { - header += new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now").Render() - } - for (const changeMoment of changeHours) { - const offset = 100 * (changeMoment - earliestOpen) / availableArea; - if (offset < 0 || offset > 100) { - continue; - } - const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line").Render(); - header += el; - } - - for (let i = 0; i < changeHours.length; i++) { - let changeMoment = changeHours[i]; - const offset = 100 * (changeMoment - earliestOpen) / availableArea; - if (offset < 0 || offset > 100) { - continue; - } - const el = new FixedUiElement( - `
    ${changeHourText[i]}
    ` - ) - .SetStyle(`left:${offset}%`) - .SetClass("ohviz-time-indication").Render(); - header += el; - } - - rows.push(new Combine([` `, - `${header}`])); - - for (let i = 0; i < 7; i++) { - const dayRanges = ranges[i]; - const isToday = (new Date().getDay() + 6) % 7 === i; - let weekday = OpeningHoursVisualization.weekdays[i].Render(); - - let dateToShow = "" - if (!isWeekstable) { - const day = new Date(lastMonday) - day.setDate(day.getDate() + i); - dateToShow = "" + day.getDate() + "/" + (day.getMonth() + 1); - } - - let innerContent: string[] = []; - - // Add the lines - for (const changeMoment of changeHours) { - const offset = 100 * (changeMoment - earliestOpen) / availableArea; - innerContent.push(new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line").Render()) - } - - // Add the actual ranges - for (const range of dayRanges) { - if (!range.isOpen && !range.isSpecial) { - innerContent.push( - new FixedUiElement(range.comment ?? dateToShow).SetClass("ohviz-day-off").Render()) - continue; - } - - const startOfDay: Date = new Date(range.startDate); - startOfDay.setHours(0, 0, 0, 0); - // @ts-ignore - const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen; - // @ts-ignore - const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen); - const startPercentage = (100 * startpoint / availableArea); - innerContent.push( - new FixedUiElement(range.comment ?? dateToShow).SetStyle(`left:${startPercentage}%; width:${width}%`).SetClass("ohviz-range").Render()) - } - - // Add line for 'now' - if (now >= 0 && now <= 100) { - innerContent.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now").Render()) - } - - let clss = "" - if (isToday) { - clss = "ohviz-today" - } - - rows.push(new Combine( - [`${weekday}`, - `${innerContent.join("")}`])) - } - - - return new Combine([ - "", - rows.map(el => "" + el.Render() + "").join(""), - "
    " - ]).SetClass("ohviz-container").Render(); - } - } \ No newline at end of file diff --git a/UI/OpeningHours/OpeningHoursInput.ts b/UI/OpeningHours/OpeningHoursInput.ts index 8e9a7c9b8..1ac60467f 100644 --- a/UI/OpeningHours/OpeningHoursInput.ts +++ b/UI/OpeningHours/OpeningHoursInput.ts @@ -5,7 +5,6 @@ */ import OpeningHoursPicker from "./OpeningHoursPicker"; import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import Combine from "../Base/Combine"; import {FixedUiElement} from "../Base/FixedUiElement"; @@ -14,21 +13,20 @@ import {InputElement} from "../Input/InputElement"; import PublicHolidayInput from "./PublicHolidayInput"; import Translations from "../i18n/Translations"; import {Utils} from "../../Utils"; +import BaseUIElement from "../BaseUIElement"; export default class OpeningHoursInput extends InputElement { + public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly _value: UIEventSource; - - private readonly _ohPicker: UIElement; - private readonly _leftoverWarning: UIElement; - private readonly _phSelector: UIElement; + private readonly _element: BaseUIElement; constructor(value: UIEventSource = new UIEventSource("")) { super(); - + const leftoverRules = value.map(str => { if (str === undefined) { return [] @@ -61,11 +59,11 @@ export default class OpeningHoursInput extends InputElement { } return ""; }) - this._phSelector = new PublicHolidayInput(ph); + const phSelector = new PublicHolidayInput(ph); function update() { const regular = OH.ToString(rulesFromOhPicker.data); - const rules : string[] = [ + const rules: string[] = [ regular, ...leftoverRules.data, ph.data @@ -76,39 +74,35 @@ export default class OpeningHoursInput extends InputElement { rulesFromOhPicker.addCallback(update); ph.addCallback(update); - this._leftoverWarning = new VariableUiElement(leftoverRules.map((leftovers: string[]) => { + const leftoverWarning = new VariableUiElement(leftoverRules.map((leftovers: string[]) => { if (leftovers.length == 0) { return ""; } return new Combine([ Translations.t.general.opening_hours.not_all_rules_parsed, - new FixedUiElement(leftovers.map(r => `${r}
    `).join("")).SetClass("subtle") - ]).Render(); + new FixedUiElement(leftovers.map(r => `${r}
    `).join("")).SetClass("subtle") + ]); })) - this._ohPicker = new OpeningHoursPicker(rulesFromOhPicker); - + const ohPicker = new OpeningHoursPicker(rulesFromOhPicker); + this._element = new Combine([ + leftoverWarning, + ohPicker, + phSelector + ]) } + protected InnerConstructElement(): HTMLElement { + return this._element.ConstructElement() + } GetValue(): UIEventSource { return this._value; } - InnerRender(): string { - return new Combine([ - this._leftoverWarning, - this._ohPicker, - this._phSelector - ]).Render(); - } - - - public readonly IsSelected: UIEventSource = new UIEventSource(false); - IsValid(t: string): boolean { return true; } diff --git a/UI/OpeningHours/OpeningHoursPicker.ts b/UI/OpeningHours/OpeningHoursPicker.ts index ad8c0cb5b..b620133ad 100644 --- a/UI/OpeningHours/OpeningHoursPicker.ts +++ b/UI/OpeningHours/OpeningHoursPicker.ts @@ -5,6 +5,7 @@ import Combine from "../Base/Combine"; import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; import {OH, OpeningHour} from "./OpeningHours"; import {InputElement} from "../Input/InputElement"; +import BaseUIElement from "../BaseUIElement"; export default class OpeningHoursPicker extends InputElement { private readonly _ohs: UIEventSource; @@ -12,7 +13,7 @@ export default class OpeningHoursPicker extends InputElement { private readonly _backgroundTable: OpeningHoursPickerTable; - private readonly _weekdays: UIEventSource = new UIEventSource([]); + private readonly _weekdays: UIEventSource = new UIEventSource([]); constructor(ohs: UIEventSource = new UIEventSource([])) { super(); @@ -49,8 +50,12 @@ export default class OpeningHoursPicker extends InputElement { } - InnerRender(): string { - return this._backgroundTable.Render(); + InnerRender(): BaseUIElement { + return this._backgroundTable; + } + + protected InnerConstructElement(): HTMLElement { + return this._backgroundTable.ConstructElement(); } GetValue(): UIEventSource { diff --git a/UI/OpeningHours/OpeningHoursPickerTable.ts b/UI/OpeningHours/OpeningHoursPickerTable.ts index 5192e7bfc..794f2d28b 100644 --- a/UI/OpeningHours/OpeningHoursPickerTable.ts +++ b/UI/OpeningHours/OpeningHoursPickerTable.ts @@ -8,12 +8,13 @@ import {Utils} from "../../Utils"; import {OpeningHour} from "./OpeningHours"; import {InputElement} from "../Input/InputElement"; import Translations from "../i18n/Translations"; +import BaseUIElement from "../BaseUIElement"; export default class OpeningHoursPickerTable extends InputElement { public readonly IsSelected: UIEventSource; - private readonly weekdays: UIEventSource; + private readonly weekdays: UIEventSource; - public static readonly days: UIElement[] = + public static readonly days: BaseUIElement[] = [ Translations.t.general.weekdays.abbreviations.monday, Translations.t.general.weekdays.abbreviations.tuesday, @@ -28,8 +29,8 @@ export default class OpeningHoursPickerTable extends InputElement private readonly source: UIEventSource; - constructor(weekdays: UIEventSource, source?: UIEventSource) { - super(weekdays); + constructor(weekdays: UIEventSource, source?: UIEventSource) { + super(); this.weekdays = weekdays; this.source = source ?? new UIEventSource([]); this.IsSelected = new UIEventSource(false); diff --git a/UI/OpeningHours/OpeningHoursRange.ts b/UI/OpeningHours/OpeningHoursRange.ts index 5f67f8e4a..68f73ea41 100644 --- a/UI/OpeningHours/OpeningHoursRange.ts +++ b/UI/OpeningHours/OpeningHoursRange.ts @@ -48,10 +48,10 @@ export default class OpeningHoursRange extends UIElement { } - InnerRender(): string { + InnerRender(): UIElement { const oh = this._oh.data; if (oh === undefined) { - return ""; + return undefined; } const height = this.getHeight(); @@ -62,7 +62,6 @@ export default class OpeningHoursRange extends UIElement { return new Combine(content) .SetClass("oh-timerange-inner") - .Render(); } private getHeight(): number { diff --git a/UI/OpeningHours/PublicHolidayInput.ts b/UI/OpeningHours/PublicHolidayInput.ts index bcdc3cef5..d61afdb31 100644 --- a/UI/OpeningHours/PublicHolidayInput.ts +++ b/UI/OpeningHours/PublicHolidayInput.ts @@ -143,7 +143,7 @@ export default class PublicHolidayInput extends InputElement { } } - InnerRender(): string { + InnerRender(): UIElement { const mode = this._mode.data; if (mode === " ") { return new Combine([this._dropdown, @@ -154,9 +154,9 @@ export default class PublicHolidayInput extends InputElement { " ", Translations.t.general.opening_hours.openTill, " ", - this._endHour]).Render(); + this._endHour]); } - return this._dropdown.Render(); + return this._dropdown; } GetValue(): UIEventSource { diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index c3f249c0b..14025b905 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -39,7 +39,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen { const titleIcons = new Combine( layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, "block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem !important;") - .HideOnEmpty(true) )) .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index 97d2d0fb9..76e8f8ed3 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -48,7 +48,7 @@ export default class QuestionBox extends UIElement { this.SetClass("block mb-8") } - InnerRender(): string { + InnerRender() { const allQuestions : UIElement[] = [] for (let i = 0; i < this._tagRenderingQuestions.length; i++) { let tagRendering = this._tagRenderings[i]; @@ -72,7 +72,7 @@ export default class QuestionBox extends UIElement { } - return new Combine(allQuestions).Render(); + return new Combine(allQuestions); } } \ No newline at end of file diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index 2c277c7fc..d01382c70 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -21,15 +21,15 @@ export class SaveButton extends UIElement { .onClick(() => osmConnection?.AttemptLogin()) } - InnerRender(): string { + InnerRender() { if(this._userDetails != undefined && !this._userDetails.data.loggedIn){ - return this._friendlyLogin.Render(); + return this._friendlyLogin; } let inactive_class = '' if (this._value.data === false || (this._value.data ?? "") === "") { inactive_class = "btn-disabled"; } - return Translations.t.general.save.Clone().SetClass(`btn ${inactive_class}`).Render(); + return Translations.t.general.save.Clone().SetClass(`btn ${inactive_class}`); } } \ No newline at end of file diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 43966d4a4..1138da448 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -30,7 +30,7 @@ export default class TagRenderingAnswer extends UIElement { this.SetStyle("word-wrap: anywhere;"); } - InnerRender(): string { + InnerRender(): string | UIElement{ if (this._configuration.condition !== undefined) { if (!this._configuration.condition.matchesProperties(this._tags.data)) { return ""; @@ -80,14 +80,14 @@ export default class TagRenderingAnswer extends UIElement { ]) } - return this._content.SetClass(this._contentClass).SetStyle(this._contentStyle).Render(); + return this._content.SetClass(this._contentClass).SetStyle(this._contentStyle); } } const tr = this._configuration.GetRenderValue(tags); if (tr !== undefined) { this._content = SubstitutedTranslation.construct(tr, this._tags); - return this._content.SetClass(this._contentClass).SetStyle(this._contentStyle).Render(); + return this._content.SetClass(this._contentClass).SetStyle(this._contentStyle); } return ""; diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index d4c1ee104..44b1cb3c1 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -94,7 +94,7 @@ export default class TagRenderingQuestion extends UIElement { ).SetClass("block") } - InnerRender(): string { + InnerRender() { return new Combine([ this._question, this._inputElement, @@ -103,7 +103,6 @@ export default class TagRenderingQuestion extends UIElement { this._appliedTags] ) .SetClass("question") - .Render() } private GenerateInputElement(): InputElement { diff --git a/UI/Reviews/ReviewElement.ts b/UI/Reviews/ReviewElement.ts index 9f123a22b..cdf6659e9 100644 --- a/UI/Reviews/ReviewElement.ts +++ b/UI/Reviews/ReviewElement.ts @@ -26,7 +26,7 @@ export default class ReviewElement extends UIElement { - InnerRender(): string { + InnerRender(): UIElement { const elements = []; const revs = this._reviews.data; @@ -56,7 +56,7 @@ export default class ReviewElement extends UIElement { .SetClass("review-attribution")) - return new Combine(elements).SetClass("block").Render(); + return new Combine(elements).SetClass("block"); } } \ No newline at end of file diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index 29cc9680e..59d86c792 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -86,10 +86,10 @@ export default class ReviewForm extends InputElement { return this._value; } - InnerRender(): string { + InnerRender(): UIElement { if(!this.userDetails.data.loggedIn){ - return Translations.t.reviews.plz_login.Render(); + return Translations.t.reviews.plz_login; } return new Combine([ @@ -103,7 +103,6 @@ export default class ReviewForm extends InputElement { Translations.t.reviews.tos.SetClass("subtle") ]) .SetClass("review-form") - .Render(); } IsSelected: UIEventSource = new UIEventSource(false); diff --git a/UI/Reviews/SingleReview.ts b/UI/Reviews/SingleReview.ts index 9a9edd819..b6f913add 100644 --- a/UI/Reviews/SingleReview.ts +++ b/UI/Reviews/SingleReview.ts @@ -26,7 +26,7 @@ export default class SingleReview extends UIElement{ scoreTen % 2 == 1 ? "" : "" ]).SetClass("flex w-max") } - InnerRender(): string { + InnerRender(): UIElement { const d = this._review.date; let review = this._review; const el= new Combine( @@ -51,7 +51,7 @@ export default class SingleReview extends UIElement{ if(review.made_by_user.data){ el.SetClass("border-attention-catch") } - return el.Render(); + return el; } } \ No newline at end of file diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 7b39e9ace..980503db0 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -6,11 +6,8 @@ import Combine from "./Base/Combine"; import State from "../State"; import {FixedUiElement} from "./Base/FixedUiElement"; import SpecialVisualizations from "./SpecialVisualizations"; -import {Utils} from "../Utils"; export class SubstitutedTranslation extends UIElement { - private static cachedTranslations: - Map, SubstitutedTranslation>>> = new Map, SubstitutedTranslation>>>(); private readonly tags: UIEventSource; private readonly translation: Translation; private content: UIElement[]; @@ -37,39 +34,24 @@ export class SubstitutedTranslation extends UIElement { public static construct( translation: Translation, tags: UIEventSource): SubstitutedTranslation { - - /* let cachedTranslations = Utils.getOrSetDefault(SubstitutedTranslation.cachedTranslations, SubstitutedTranslation.GenerateSubCache); - const innerMap = Utils.getOrSetDefault(cachedTranslations, translation, SubstitutedTranslation.GenerateMap); - - const cachedTranslation = innerMap.get(tags); - if (cachedTranslation !== undefined) { - return cachedTranslation; - }*/ - const st = new SubstitutedTranslation(translation, tags); - // innerMap.set(tags, st); - return st; + return new SubstitutedTranslation(translation, tags); } public static SubstituteKeys(txt: string, tags: any) { for (const key in tags) { + if(!tags.hasOwnProperty(key)) { + continue + } txt = txt.replace(new RegExp("{" + key + "}", "g"), tags[key]) } return txt; } - private static GenerateMap() { - return new Map, SubstitutedTranslation>() - } - - private static GenerateSubCache() { - return new Map, SubstitutedTranslation>>(); - } - - InnerRender(): string { + InnerRender() { if (this.content.length == 1) { - return this.content[0].Render(); + return this.content[0]; } - return new Combine(this.content).Render(); + return new Combine(this.content); } private CreateContent(): UIElement[] { @@ -118,11 +100,11 @@ export class SubstitutedTranslation extends UIElement { } // Let's to a small sanity check to help the theme designers: - if(template.search(/{[^}]+\([^}]*\)}/) >= 0){ + if (template.search(/{[^}]+\([^}]*\)}/) >= 0) { // Hmm, we might have found an invalid rendering name - console.warn("Found a suspicious special rendering value in: ", template, " did you mean one of: ", SpecialVisualizations.specialVisualizations.map(sp => sp.funcName+"()").join(", ")) + console.warn("Found a suspicious special rendering value in: ", template, " did you mean one of: ", SpecialVisualizations.specialVisualizations.map(sp => sp.funcName + "()").join(", ")) } - + // IF we end up here, no changes have to be made - except to remove any resting {} return [new FixedUiElement(template.replace(/{.*}/g, ""))]; } diff --git a/UI/UIElement.ts b/UI/UIElement.ts index b4f658da4..e817a8518 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -1,25 +1,19 @@ import {UIEventSource} from "../Logic/UIEventSource"; -import {Utils} from "../Utils"; +import BaseUIElement from "./BaseUIElement"; -export abstract class UIElement extends UIEventSource { +export abstract class UIElement extends BaseUIElement{ private static nextId: number = 0; public readonly id: string; public readonly _source: UIEventSource; - public dumbMode = false; - private clss: Set = new Set(); - private style: string; - private _hideIfEmpty = false; + private lastInnerRender: string; - private _onClick: () => void; - private _onHover: UIEventSource; protected constructor(source: UIEventSource = undefined) { - super(""); - this.id = "ui-element-" + UIElement.nextId; + super() + this.id = `ui-${this.constructor.name}-${UIElement.nextId}`; this._source = source; UIElement.nextId++; - this.dumbMode = true; this.ListenTo(source); } @@ -27,183 +21,97 @@ export abstract class UIElement extends UIEventSource { if (source === undefined) { return this; } - this.dumbMode = false; const self = this; source.addCallback(() => { self.lastInnerRender = undefined; - self.Update(); + if(self._constructedHtmlElement !== undefined){ + self.UpdateElement(self._constructedHtmlElement); + } + }) return this; } - public onClick(f: (() => void)) { - this.dumbMode = false; - this._onClick = f; - this.SetClass("clickable") - this.Update(); - return this; - } - public IsHovered(): UIEventSource { - this.dumbMode = false; - if (this._onHover !== undefined) { - return this._onHover; - } - // Note: we just save it. 'Update' will register that an eventsource exist and install the necessary hooks - this._onHover = new UIEventSource(false); - return this._onHover; - } Update(): void { - if (Utils.runningFromConsole) { - return; - } - let element = document.getElementById(this.id); - if (element === undefined || element === null) { - // The element is not painted or, in the case of 'dumbmode' this UI-element is not explicitely present - if (this.dumbMode) { - // We update all the children anyway - this.UpdateAllChildren(); - } - return; - } - const newRender = this.InnerRender(); - if (newRender !== this.lastInnerRender) { - this.lastInnerRender = newRender; - this.setData(this.InnerRender()); - element.innerHTML = this.data; - } - - if (this._hideIfEmpty) { - if (element.innerHTML === "") { - element.parentElement.style.display = "none"; - } else { - element.parentElement.style.display = ""; - } - } - - if (this._onClick !== undefined) { - const self = this; - element.onclick = (e) => { - // @ts-ignore - if (e.consumed) { - return; - } - self._onClick(); - // @ts-ignore - e.consumed = true; - } - element.style.pointerEvents = "all"; - element.style.cursor = "pointer"; - } - - if (this._onHover !== undefined) { - const self = this; - element.addEventListener('mouseover', () => self._onHover.setData(true)); - element.addEventListener('mouseout', () => self._onHover.setData(false)); - } - - this.InnerUpdate(element); - this.UpdateAllChildren(); - - } - - HideOnEmpty(hide: boolean): UIElement { - this._hideIfEmpty = hide; - this.Update(); - return this; } Render(): string { - this.lastInnerRender = this.InnerRender(); - if (this.dumbMode) { - return this.lastInnerRender; - } - - let style = ""; - if (this.style !== undefined && this.style !== "") { - style = `style="${this.style}" `; - } - let clss = ""; - if (this.clss.size > 0) { - clss = `class='${Array.from(this.clss).join(" ")}' `; - } - return `${this.lastInnerRender}` + return "Don't use Render!" } - AttachTo(divId: string) { - this.dumbMode = false; - let element = document.getElementById(divId); - if (element === null) { - throw "SEVERE: could not attach UIElement to " + divId; - } - element.innerHTML = this.Render(); - this.Update(); - return this; - } - public abstract InnerRender(): string; + public InnerRenderAsString(): string { + let rendered = this.InnerRender(); + if (typeof rendered !== "string") { + let html = rendered.ConstructElement() + return html.innerHTML + } + return rendered + } public IsEmpty(): boolean { - return this.InnerRender() === ""; + return this.InnerRender() === undefined || this.InnerRender() === ""; } + + /** - * Adds all the relevant classes, space seperated - * @param clss - * @constructor + * Should be overridden for specific HTML functionality */ - public SetClass(clss: string) { - this.dumbMode = false; - const all = clss.split(" "); - let recordedChange = false; - for (const c of all) { - if (this.clss.has(clss)) { - continue; + protected InnerConstructElement(): HTMLElement { + // Uses the old fashioned way to construct an element using 'InnerRender' + const innerRender = this.InnerRender(); + if (innerRender === undefined || innerRender === "") { + return undefined; + } + const el = document.createElement("span") + if (typeof innerRender === "string") { + el.innerHTML = innerRender + } else { + const subElement = innerRender.ConstructElement(); + if (subElement === undefined) { + return undefined; } - this.clss.add(c); - recordedChange = true; + el.appendChild(subElement) } - if (recordedChange) { - this.Update(); - } - return this; + return el; } - public RemoveClass(clss: string): UIElement { - if (this.clss.has(clss)) { - this.clss.delete(clss); - this.Update(); - } - return this; - } + protected UpdateElement(el: HTMLElement) : void{ + const innerRender = this.InnerRender(); - public SetStyle(style: string): UIElement { - this.dumbMode = false; - this.style = style; - this.Update(); - return this; - } - - // Called after the HTML has been replaced. Can be used for css tricks - protected InnerUpdate(htmlElement: HTMLElement) { - } - - private UpdateAllChildren() { - for (const i in this) { - const child = this[i]; - if (child instanceof UIElement) { - child.Update(); - } else if (child instanceof Array) { - for (const ch of child) { - if (ch instanceof UIElement) { - ch.Update(); - } - } + if (typeof innerRender === "string") { + if(el.innerHTML !== innerRender){ + el.innerHTML = innerRender } + } else { + const subElement = innerRender.ConstructElement(); + if(el.children.length === 1 && el.children[0] === subElement){ + return; // Nothing changed + } + + while (el.firstChild) { + el.removeChild(el.firstChild); + } + + if (subElement === undefined) { + return; + } + el.appendChild(subElement) } + } + + + + /** + * @deprecated The method should not be used + */ + protected abstract InnerRender(): string | BaseUIElement; + } diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index 5bdfa8249..fab879a8e 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -1,24 +1,22 @@ import {UIElement} from "../UIElement"; -import Combine from "../Base/Combine"; import Locale from "./Locale"; import {Utils} from "../../Utils"; +import BaseUIElement from "../BaseUIElement"; -export class Translation extends UIElement { +export class Translation extends BaseUIElement { public static forcedLanguage = undefined; public readonly translations: object - return - allIcons; constructor(translations: object, context?: string) { - super(Locale.language) + super() if (translations === undefined) { throw `Translation without content (${context})` } let count = 0; for (const translationsKey in translations) { - if(!translations.hasOwnProperty(translationsKey)){ + if (!translations.hasOwnProperty(translationsKey)) { continue } count++; @@ -46,15 +44,29 @@ export class Translation extends UIElement { return en; } for (const i in this.translations) { + if (!this.translations.hasOwnProperty(i)) { + continue; + } return this.translations[i]; // Return a random language } console.error("Missing language ", Locale.language.data, "for", this.translations) return ""; } + InnerConstructElement(): HTMLElement { + const el = document.createElement("span") + Locale.language.addCallbackAndRun(_ => { + el.innerHTML = this.txt + }) + return el; + } + public SupportedLanguages(): string[] { const langs = [] for (const translationsKey in this.translations) { + if (!this.translations.hasOwnProperty(translationsKey)) { + continue; + } if (translationsKey === "#") { continue; } @@ -66,9 +78,15 @@ export class Translation extends UIElement { public Subs(text: any): Translation { const newTranslations = {}; for (const lang in this.translations) { + if (!this.translations.hasOwnProperty(lang)) { + continue; + } let template: string = this.translations[lang]; for (const k in text) { - const combined = []; + if (!text.hasOwnProperty(k)) { + continue + } + const combined: (string)[] = []; const parts = template.split("{" + k + "}"); const el: string | UIElement = text[k]; if (el === undefined) { @@ -85,12 +103,12 @@ export class Translation extends UIElement { // @ts-ignore const date: Date = el; rtext = date.toLocaleString(); - } else if (el.InnerRender === undefined) { + } else if (el.InnerRenderAsString === undefined) { console.error("InnerREnder is not defined", el); throw "Hmmm, el.InnerRender is not defined?" } else { Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day - rtext = el.InnerRender(); + rtext = el.InnerRenderAsString(); } for (let i = 0; i < parts.length - 1; i++) { @@ -98,7 +116,7 @@ export class Translation extends UIElement { combined.push(rtext) } combined.push(parts[parts.length - 1]); - template = new Combine(combined).InnerRender(); + template = combined.join("") } newTranslations[lang] = template; } @@ -107,16 +125,11 @@ export class Translation extends UIElement { } - InnerRender(): string { - return this.txt - } - public replace(a: string, b: string) { if (a.startsWith("{") && a.endsWith("}")) { a = a.substr(1, a.length - 2); } - const result = this.Subs({[a]: b}); - return result; + return this.Subs({[a]: b}); } public Clone() { @@ -127,6 +140,9 @@ export class Translation extends UIElement { const tr = {}; for (const lng in this.translations) { + if (!this.translations.hasOwnProperty(lng)) { + continue + } let txt = this.translations[lng]; txt = txt.replace(/\..*/, ""); txt = Utils.EllipsesAfter(txt, 255); @@ -139,6 +155,9 @@ export class Translation extends UIElement { public ExtractImages(isIcon = false): string[] { const allIcons: string[] = [] for (const key in this.translations) { + if (!this.translations.hasOwnProperty(key)) { + continue; + } const render = this.translations[key] if (isIcon) { diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index a31aa4c70..d530c8261 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -2,6 +2,7 @@ import {UIElement} from "../UIElement"; import {FixedUiElement} from "../Base/FixedUiElement"; import AllTranslationAssets from "../../AllTranslationAssets"; import {Translation} from "./Translation"; +import BaseUIElement from "../BaseUIElement"; export default class Translations { @@ -10,7 +11,7 @@ export default class Translations { } static t = AllTranslationAssets.t; - public static W(s: string | UIElement): UIElement { + public static W(s: string | BaseUIElement): BaseUIElement { if (typeof (s) === "string") { return new FixedUiElement(s); } diff --git a/customGenerator.html b/customGenerator.html deleted file mode 100644 index 8f124a4e9..000000000 --- a/customGenerator.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - Custom Theme Generator for Mapcomplete - - - - - - -
    - Loading the MapComplete custom theme builder...
    - If this message persists, make sure javascript is enabled and no script blocker is blocking this. -
    - - - \ No newline at end of file diff --git a/customGenerator.ts b/customGenerator.ts deleted file mode 100644 index c0179c8d7..000000000 --- a/customGenerator.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {UIEventSource} from "./Logic/UIEventSource"; -import {GenerateEmpty} from "./UI/CustomGenerator/GenerateEmpty"; -import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; -import {OsmConnection} from "./Logic/Osm/OsmConnection"; -import CustomGeneratorPanel from "./UI/CustomGenerator/CustomGeneratorPanel"; -import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; -import {Utils} from "./Utils"; -import LZString from "lz-string"; - -let layout = GenerateEmpty.createEmptyLayout(); -if (window.location.hash.length > 10) { - const hash = window.location.hash.substr(1) - try{ - layout = JSON.parse(atob(hash)) as LayoutConfigJson; - }catch(e){ - console.log("Initial load of theme failed, attempt nr 2 with decompression", e) - layout = JSON.parse( Utils.UnMinify(LZString.decompressFromBase64(hash))) - } - -} else { - const hash = LocalStorageSource.Get("last-custom-theme").data - if (hash !== undefined) { - console.log("Using theme from local storage") - layout = JSON.parse(atob(hash)) as LayoutConfigJson; - } -} - -const connection = new OsmConnection(false, new UIEventSource(undefined), "customGenerator", false); - -new CustomGeneratorPanel(connection, layout) - .AttachTo("maindiv"); - diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index 09e96f0e4..b9b2553b2 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -14,7 +14,7 @@ import ValidatedTextField from "../UI/Input/ValidatedTextField"; const TurndownService = require('turndown') function WriteFile(filename, html: UIElement) : void { - const md = new TurndownService().turndown(html.InnerRender()); + const md = new TurndownService().turndown(html.InnerRenderAsString()); writeFileSync(filename, md); } diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 17024c930..dcfadc3da 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -88,8 +88,8 @@ async function createManifest(layout: LayoutConfig) { console.log(icon) throw "Icon is not an svg for " + layout.id } - const ogTitle = Translations.W(layout.title).InnerRender(); - const ogDescr = Translations.W(layout.description ?? "").InnerRender(); + const ogTitle = Translations.W(layout.title).InnerRenderAsString(); + const ogDescr = Translations.W(layout.description ?? "").InnerRenderAsString(); return { name: name, @@ -109,8 +109,8 @@ async function createLandingPage(layout: LayoutConfig, manifest) { Locale.language.setData(layout.language[0]); - const ogTitle = Translations.W(layout.title)?.InnerRender(); - const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRender(); + const ogTitle = Translations.W(layout.title)?.InnerRenderAsString(); + const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRenderAsString(); const ogImage = layout.socialImage; let customCss = ""; diff --git a/scripts/generateWikiPage.ts b/scripts/generateWikiPage.ts index dbbf1ca34..0ff0bf3aa 100644 --- a/scripts/generateWikiPage.ts +++ b/scripts/generateWikiPage.ts @@ -20,7 +20,7 @@ function generateWikiEntry(layout: LayoutConfig) { |region= Worldwide |lang= ${languages} |descr= A MapComplete theme: ${Translations.W(layout.description) - .InnerRender() + .InnerRenderAsString() .replace(".*<\/a>/, "]]") } diff --git a/test.ts b/test.ts index ddf4def81..7aae201d5 100644 --- a/test.ts +++ b/test.ts @@ -1,3 +1,15 @@ -import ValidatedTextField from "./UI/Input/ValidatedTextField"; +import {Translation} from "./UI/i18n/Translation"; +import Locale from "./UI/i18n/Locale"; +import Combine from "./UI/Base/Combine"; -ValidatedTextField.InputForType("phone").AttachTo("maindiv") \ No newline at end of file + +new Combine(["Some language:",new Translation({en:"English",nl:"Nederlands",fr:"Françcais"})]).AttachTo("maindiv") + +Locale.language.setData("nl") +window.setTimeout(() => { + Locale.language.setData("en") +}, 1000) + +window.setTimeout(() => { + Locale.language.setData("fr") +}, 5000) \ No newline at end of file diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 457e68075..0c1938a79 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -145,7 +145,7 @@ export default class TagSpec extends T{ equal("Has no name", tr.GetRenderValue({"noname": "yes"})?.txt); equal("Ook een {name}", tr.GetRenderValue({"name": "xyz"})?.txt); equal("Ook een xyz", SubstitutedTranslation.construct(tr.GetRenderValue({"name": "xyz"}), - new UIEventSource({"name": "xyz"})).InnerRender()); + new UIEventSource({"name": "xyz"})).InnerRenderAsString()); equal(undefined, tr.GetRenderValue({"foo": "bar"})); })], @@ -196,7 +196,7 @@ export default class TagSpec extends T{ const uiEl = new EditableTagRendering(new UIEventSource( {leisure: "park", "access": "no"}), constr ); - const rendered = uiEl.InnerRender(); + const rendered = uiEl.InnerRenderAsString(); equal(true, rendered.indexOf("Niet toegankelijk") > 0) } diff --git a/test/TagQuestion.spec.ts b/test/TagQuestion.spec.ts index 0e0dad1e7..f0415ad00 100644 --- a/test/TagQuestion.spec.ts +++ b/test/TagQuestion.spec.ts @@ -27,7 +27,7 @@ export default class TagQuestionSpec extends T { }, undefined, "Testing tag" ); const questionElement = new TagRenderingQuestion(tags, config); - const html = questionElement.InnerRender(); + const html = questionElement.InnerRenderAsString(); T.assertContains("What is the name of this bookcase?", html); T.assertContains("