From 6415e195d13c99bc48c908f60b519c16d1ad6d85 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 10 Jun 2021 01:36:20 +0200 Subject: [PATCH 01/30] Butchering the UI framework --- Customizations/JSON/LayerConfig.ts | 14 +- InitUiElements.ts | 10 +- Logic/Actors/GeoLocationHandler.ts | 1 - Logic/Actors/TitleHandler.ts | 3 +- UI/Base/Combine.ts | 31 +- UI/Base/FeatureSwitched.ts | 4 +- UI/Base/FixedUiElement.ts | 2 +- UI/Base/Img.ts | 30 +- UI/Base/Link.ts | 37 +- UI/Base/ScrollableFullScreen.ts | 12 +- UI/Base/SubtleButton.ts | 46 ++- UI/Base/TabbedComponent.ts | 30 +- UI/Base/VariableUIElement.ts | 35 +- UI/Base/VerticalCombine.ts | 20 - UI/BaseUIElement.ts | 154 ++++++++ UI/BigComponents/AttributionPanel.ts | 6 +- UI/BigComponents/BackgroundSelector.ts | 7 +- UI/BigComponents/FullWelcomePaneWithTabs.ts | 4 +- UI/BigComponents/LayerSelection.ts | 4 +- UI/BigComponents/MoreScreen.ts | 161 ++++---- UI/BigComponents/PersonalLayersPanel.ts | 4 +- UI/BigComponents/ShareButton.ts | 1 - UI/BigComponents/ShareScreen.ts | 23 +- UI/BigComponents/ThemeIntroductionPanel.ts | 5 +- UI/BigComponents/UserBadge.ts | 6 +- UI/CenterMessageBox.ts | 15 +- UI/CustomGenerator/AllLayersPanel.ts | 113 ------ UI/CustomGenerator/CustomGeneratorPanel.ts | 118 ------ UI/CustomGenerator/GeneralSettings.ts | 88 ----- UI/CustomGenerator/GenerateEmpty.ts | 87 ----- UI/CustomGenerator/HelpText.ts | 51 --- UI/CustomGenerator/LayerPanel.ts | 251 ------------- UI/CustomGenerator/LayerPanelWithPreview.ts | 58 --- UI/CustomGenerator/MappingInput.ts | 64 ---- UI/CustomGenerator/PresetInputPanel.ts | 58 --- UI/CustomGenerator/SavePanel.ts | 69 ---- UI/CustomGenerator/SettingsTable.ts | 58 --- UI/CustomGenerator/SharePanel.ts | 34 -- UI/CustomGenerator/SingleSetting.ts | 89 ----- UI/CustomGenerator/TagRenderingPanel.ts | 155 -------- UI/CustomGenerator/TagRenderingPreview.ts | 70 ---- UI/Image/DeleteImage.ts | 10 +- UI/Image/ImageCarousel.ts | 2 +- UI/Image/ImageUploadFlow.ts | 11 +- UI/Image/SlideShow.ts | 1 - UI/Input/AndOrTagInput.ts | 164 -------- UI/Input/CheckBox.ts | 32 -- UI/Input/Checkboxes.ts | 2 - UI/Input/ColorPicker.ts | 1 - UI/Input/CombinedInputElement.ts | 15 +- UI/Input/DirectionInput.ts | 2 - UI/Input/DropDown.ts | 129 +++---- UI/Input/InputElement.ts | 4 +- UI/Input/InputElementMap.ts | 17 +- UI/Input/MultiInput.ts | 125 ------ UI/Input/MultiLingualTextFields.ts | 99 ----- UI/Input/MultiTagInput.ts | 15 - UI/Input/NumberField.ts | 123 ------ UI/Input/SimpleDatePicker.ts | 43 +-- UI/Input/SingleTagInput.ts | 113 ------ UI/Input/TextField.ts | 153 ++++---- UI/Input/Toggle.ts | 22 ++ UI/LanguagePicker.ts | 4 +- UI/MapControlButton.ts | 4 +- UI/OpeningHours/OhVisualization.ts | 397 ++++++++++---------- UI/OpeningHours/OpeningHoursInput.ts | 42 +-- UI/OpeningHours/OpeningHoursPicker.ts | 11 +- UI/OpeningHours/OpeningHoursPickerTable.ts | 9 +- UI/OpeningHours/OpeningHoursRange.ts | 5 +- UI/OpeningHours/PublicHolidayInput.ts | 6 +- UI/Popup/FeatureInfoBox.ts | 1 - UI/Popup/QuestionBox.ts | 4 +- UI/Popup/SaveButton.ts | 6 +- UI/Popup/TagRenderingAnswer.ts | 6 +- UI/Popup/TagRenderingQuestion.ts | 3 +- UI/Reviews/ReviewElement.ts | 4 +- UI/Reviews/ReviewForm.ts | 5 +- UI/Reviews/SingleReview.ts | 4 +- UI/SubstitutedTranslation.ts | 38 +- UI/UIElement.ts | 222 ++++------- UI/i18n/Translation.ts | 51 ++- UI/i18n/Translations.ts | 3 +- customGenerator.html | 109 ------ customGenerator.ts | 32 -- scripts/generateDocs.ts | 2 +- scripts/generateLayouts.ts | 8 +- scripts/generateWikiPage.ts | 2 +- test.ts | 16 +- test/Tag.spec.ts | 4 +- test/TagQuestion.spec.ts | 4 +- 90 files changed, 1012 insertions(+), 3101 deletions(-) delete mode 100644 UI/Base/VerticalCombine.ts create mode 100644 UI/BaseUIElement.ts delete mode 100644 UI/CustomGenerator/AllLayersPanel.ts delete mode 100644 UI/CustomGenerator/CustomGeneratorPanel.ts delete mode 100644 UI/CustomGenerator/GeneralSettings.ts delete mode 100644 UI/CustomGenerator/GenerateEmpty.ts delete mode 100644 UI/CustomGenerator/HelpText.ts delete mode 100644 UI/CustomGenerator/LayerPanel.ts delete mode 100644 UI/CustomGenerator/LayerPanelWithPreview.ts delete mode 100644 UI/CustomGenerator/MappingInput.ts delete mode 100644 UI/CustomGenerator/PresetInputPanel.ts delete mode 100644 UI/CustomGenerator/SavePanel.ts delete mode 100644 UI/CustomGenerator/SettingsTable.ts delete mode 100644 UI/CustomGenerator/SharePanel.ts delete mode 100644 UI/CustomGenerator/SingleSetting.ts delete mode 100644 UI/CustomGenerator/TagRenderingPanel.ts delete mode 100644 UI/CustomGenerator/TagRenderingPreview.ts delete mode 100644 UI/Input/AndOrTagInput.ts delete mode 100644 UI/Input/CheckBox.ts delete mode 100644 UI/Input/MultiInput.ts delete mode 100644 UI/Input/MultiLingualTextFields.ts delete mode 100644 UI/Input/MultiTagInput.ts delete mode 100644 UI/Input/NumberField.ts delete mode 100644 UI/Input/SingleTagInput.ts create mode 100644 UI/Input/Toggle.ts delete mode 100644 customGenerator.html delete mode 100644 customGenerator.ts diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 0fe05d121e..be2adc52df 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 45f0480683..e8386d7c0b 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 772f57304f..90d6cec310 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 f03961ae1d..adb03a1886 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 827e30e3f8..9bf1cf9508 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 6ff095a8a3..7641a4801a 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 a0941e5ec3..e27afb3a17 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 76d00c59b5..372ee7e031 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 dbe164c1ea..d63a620117 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 b6ef108147..3f635ba67e 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 4a2f688d9c..42636d4316 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 308d34d5a7..260317eb09 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 6e5e246ef2..2d813a0aa0 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 c83b1992b7..0000000000 --- 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 0000000000..aecf6695c2 --- /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 760ec9b85d..6b3b830abf 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 c04eba35d6..a55ddcab94 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 4622167bd5..b521d07090 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 0b75a3ac24..d92fb5b0ae 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 f5b4eaecd3..aee0e00b4d 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 e11aac86e8..6713f96992 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 b4c2eba51c..0ad95828ce 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 dcf0874824..dc412f2c06 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 3b9a8b2b15..61f7c23473 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 acb5dfb553..6248140e79 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 7c3ed26ff2..e1f15e4984 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 6f8ce2d932..0000000000 --- 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 e1a1592ed2..0000000000 --- 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 b37fb31cc0..0000000000 --- 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 34defb8776..0000000000 --- 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 880fe65e36..0000000000 --- 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 787de027ce..0000000000 --- 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 3c0d19cc37..0000000000 --- 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 7c8ae6b1b2..0000000000 --- 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 edb7a0c647..0000000000 --- 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 333796c605..0000000000 --- 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 28bb4bc752..0000000000 --- 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 6f0da7f926..0000000000 --- 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 dbb4ff145c..0000000000 --- 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 302a1a3b48..0000000000 --- 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 d758682b11..0000000000 --- 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 2211fb0850..19c281465b 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 c62968ff2d..3352c64c34 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 e6a710a459..13a8440495 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 13f7ae59e4..1021a3c039 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 7f41ab11f8..0000000000 --- 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 e34707c7c7..0000000000 --- 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 4a34ee7466..cd0dbcac9f 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 8a0b5e88de..2fe6adf0dc 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 b15c0a5160..83672e9a6b 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 008d75bcd8..bed2d9c92f 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 d9ecba05ec..4e376e7eff 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 0131688620..1b5eea4039 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 7a0286b908..548e50363e 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 48826f9be5..0000000000 --- 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 d1f69fbf1a..0000000000 --- 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 30295bc8a2..0000000000 --- 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 d825bdf2f7..0000000000 --- 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 3a6b203be2..78c9db746d 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 e7525da01f..0000000000 --- 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 ccb4da1e3f..a3ce32a656 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 0000000000..f4fbdb20e3 --- /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 61f276dda1..e7c80e419f 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 b5e4b2aacc..877cd8553c 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 e930228aa9..6ac2b33248 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 8e9a7c9b83..1ac60467fa 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 ad8c0cb5b0..b620133ad5 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 5192e7bfc1..794f2d28be 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 5f67f8e4af..68f73ea413 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 bcdc3cef55..d61afdb310 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 c3f249c0b6..14025b9054 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 97d2d0fb9c..76e8f8ed32 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 2c277c7fcd..d01382c70f 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 43966d4a48..1138da448a 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 d4c1ee1047..44b1cb3c19 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 9f123a22bf..cdf6659e92 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 29cc9680ec..59d86c7928 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 9a9edd8191..b6f913add5 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 7b39e9ace8..980503db02 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 b4f658da4b..e817a85188 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 5bdfa82495..fab879a8e8 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 a31aa4c708..d530c8261a 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 8f124a4e98..0000000000 --- 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 c0179c8d7a..0000000000 --- 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 09e96f0e4c..b9b2553b20 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 17024c9301..dcfadc3da4 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 dbbf1ca34d..0ff0bf3aa2 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 ddf4def815..7aae201d57 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 457e68075e..0c1938a79c 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 0e0dad1e78..f0415ad008 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(" Date: Thu, 10 Jun 2021 14:05:26 +0200 Subject: [PATCH 02/30] Further butchering the UI framework --- UI/Base/ScrollableFullScreen.ts | 12 +++--- UI/BigComponents/IndexText.ts | 1 - UI/BigComponents/LayerControlPanel.ts | 11 +++-- UI/BigComponents/SearchAndGo.ts | 18 ++++---- UI/BigComponents/ShareScreen.ts | 34 +++++++-------- UI/Input/DirectionInput.ts | 48 ++++++++++++---------- UI/OpeningHours/OpeningHoursPicker.ts | 2 +- UI/OpeningHours/OpeningHoursPickerTable.ts | 32 +++++++++------ UI/OpeningHours/OpeningHoursRange.ts | 19 +++++---- UI/OpeningHours/PublicHolidayInput.ts | 48 ++++++++++++---------- 10 files changed, 125 insertions(+), 100 deletions(-) diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index 3f635ba67e..7e146f4179 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -5,6 +5,7 @@ import Ornament from "./Ornament"; import {FixedUiElement} from "./FixedUiElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Hash from "../../Logic/Web/Hash"; +import BaseUIElement from "../BaseUIElement"; /** * @@ -18,13 +19,13 @@ import Hash from "../../Logic/Web/Hash"; export default class ScrollableFullScreen extends UIElement { private static readonly empty = new FixedUiElement(""); public isShown: UIEventSource; - private _component: UIElement; - private _fullscreencomponent: UIElement; + private _component: BaseUIElement; + private _fullscreencomponent: BaseUIElement; private static readonly _actor = ScrollableFullScreen.InitActor(); private _hashToSet: string; private static _currentlyOpen : ScrollableFullScreen; - constructor(title: ((mode: string) => UIElement), content: ((mode: string) => UIElement), + constructor(title: ((mode: string) => BaseUIElement), content: ((mode: string) => BaseUIElement), hashToSet: string, isShown: UIEventSource = new UIEventSource(false) ) { @@ -35,7 +36,6 @@ export default class ScrollableFullScreen extends UIElement { this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown) .SetClass("hidden md:block"); this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown); - this.dumbMode = false; const self = this; isShown.addCallback(isShown => { if (isShown) { @@ -46,7 +46,7 @@ export default class ScrollableFullScreen extends UIElement { }) } - InnerRender(): UIElement { + InnerRender(): BaseUIElement { return this._component; } @@ -61,7 +61,7 @@ export default class ScrollableFullScreen extends UIElement { fs.classList.remove("hidden") } - private BuildComponent(title: UIElement, content: UIElement, isShown: UIEventSource) { + private BuildComponent(title: BaseUIElement, content:BaseUIElement, isShown: UIEventSource) { const returnToTheMap = new Combine([ Svg.back_svg().SetClass("block md:hidden"), diff --git a/UI/BigComponents/IndexText.ts b/UI/BigComponents/IndexText.ts index 6e0399ba5a..86e5d89190 100644 --- a/UI/BigComponents/IndexText.ts +++ b/UI/BigComponents/IndexText.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; import {FixedUiElement} from "../Base/FixedUiElement"; diff --git a/UI/BigComponents/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index e945b446c3..42a3eda125 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import State from "../../State"; import BackgroundSelector from "./BackgroundSelector"; import LayerSelection from "./LayerSelection"; @@ -7,6 +6,7 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; export default class LayerControlPanel extends ScrollableFullScreen { @@ -14,13 +14,12 @@ export default class LayerControlPanel extends ScrollableFullScreen { super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown); } - private static GenTitle(): UIElement { - const title = Translations.t.general.layerSelection.title.SetClass("text-2xl break-words font-bold p-2") - return title.Clone(); + private static GenTitle():BaseUIElement { + return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2") } - private static GeneratePanel() { - let layerControlPanel: UIElement = new FixedUiElement(""); + private static GeneratePanel() : BaseUIElement { + let layerControlPanel: BaseUIElement = new FixedUiElement(""); if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { layerControlPanel = new BackgroundSelector(); layerControlPanel.SetStyle("margin:1em"); diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index e6df02930d..275828dfe2 100644 --- a/UI/BigComponents/SearchAndGo.ts +++ b/UI/BigComponents/SearchAndGo.ts @@ -9,11 +9,13 @@ import {TextField} from "../Input/TextField"; import {Geocoding} from "../../Logic/Osm/Geocoding"; import Translations from "../i18n/Translations"; import Hash from "../../Logic/Web/Hash"; +import Combine from "../Base/Combine"; +import BaseUIElement from "../BaseUIElement"; export default class SearchAndGo extends UIElement { - private _placeholder = new UIEventSource(Translations.t.general.search.search) - private _searchField = new TextField({ + private readonly _placeholder = new UIEventSource(Translations.t.general.search.search) + private readonly _searchField = new TextField({ placeholder: new VariableUiElement( this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language]) ), @@ -21,8 +23,9 @@ export default class SearchAndGo extends UIElement { } ); - private _foundEntries = new UIEventSource([]); - private _goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right'); + private readonly _foundEntries = new UIEventSource([]); + private readonly _goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right'); + private readonly _element: Combine; constructor() { super(undefined); @@ -36,12 +39,13 @@ export default class SearchAndGo extends UIElement { this._goButton.onClick(function () { self.RunSearch(); }); + this._element = new Combine([this._searchField, this._goButton]) } - InnerRender(): string { - return this._searchField.Render() + - this._goButton.Render(); + InnerRender(): BaseUIElement + { + return this._element } diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index dc412f2c06..13695b5a16 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -13,14 +13,15 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import Constants from "../../Models/Constants"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import BaseUIElement from "../BaseUIElement"; export default class ShareScreen extends UIElement { - private readonly _options: UIElement; - private readonly _iframeCode: UIElement; + private readonly _options: BaseUIElement; + private readonly _iframeCode: BaseUIElement; public iframe: UIEventSource; - private readonly _link: UIElement; - private readonly _linkStatus: UIEventSource; - private readonly _editLayout: UIElement; + private readonly _link: BaseUIElement; + private readonly _linkStatus: UIEventSource; + private readonly _editLayout: BaseUIElement; constructor(layout: LayoutConfig = undefined, layoutDefinition: string = undefined) { super(undefined) @@ -28,7 +29,7 @@ export default class ShareScreen extends UIElement { layoutDefinition = layoutDefinition ?? State.state?.layoutDefinition; const tr = Translations.t.general.sharescreen; - const optionCheckboxes: UIElement[] = [] + const optionCheckboxes: BaseUIElement[] = [] const optionParts: (UIEventSource)[] = []; this.SetClass("link-underline") function check() { @@ -42,7 +43,7 @@ export default class ShareScreen extends UIElement { const includeLocation = new Toggle( new Combine([check(), tr.fsIncludeCurrentLocation]), new Combine([nocheck(), tr.fsIncludeCurrentLocation]), - true + new UIEventSource(true) ) optionCheckboxes.push(includeLocation); @@ -72,12 +73,12 @@ export default class ShareScreen extends UIElement { const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = State.state.backgroundLayer; const currentBackground = new VariableUiElement(currentLayer.map(layer => { - return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render(); + return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}); })); const includeCurrentBackground = new Toggle( new Combine([check(), currentBackground]), new Combine([nocheck(), currentBackground]), - true + new UIEventSource(true) ) optionCheckboxes.push(includeCurrentBackground); optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => { @@ -92,7 +93,7 @@ export default class ShareScreen extends UIElement { const includeLayerChoices = new Toggle( new Combine([check(), tr.fsIncludeCurrentLayers]), new Combine([nocheck(), tr.fsIncludeCurrentLayers]), - true + new UIEventSource(true) ) optionCheckboxes.push(includeLayerChoices); @@ -121,7 +122,8 @@ export default class ShareScreen extends UIElement { const checkbox = new Toggle( new Combine([check(), Translations.W(swtch.human)]), - new Combine([nocheck(), Translations.W(swtch.human)]), !swtch.reverse + new Combine([nocheck(), Translations.W(swtch.human)]), + new UIEventSource(!swtch.reverse) ); optionCheckboxes.push(checkbox); optionParts.push(checkbox.isEnabled.map((isEn) => { @@ -198,7 +200,7 @@ export default class ShareScreen extends UIElement { return new SubtleButton(Svg.pencil_ui(), new Combine([tr.editThisTheme.SetClass("bold"), "
    ", tr.editThemeDescription]), - {url: `./customGenerator.html#${State.state.layoutDefinition}`, newTab: true}).Render(); + {url: `./customGenerator.html#${State.state.layoutDefinition}`, newTab: true}); } )); @@ -215,9 +217,9 @@ export default class ShareScreen extends UIElement { ).onClick(async () => { const shareData = { - title: Translations.W(layout.title)?.InnerRenderAsString() ?? "", - text: Translations.W(layout.description)?.InnerRenderAsString() ?? "", - url: self._link.data, + title: Translations.W(layout.title)?.ConstructElement().innerText ?? "", + text: Translations.W(layout.description)?.ConstructElement().innerText ?? "", + url: url.data, } function rejected() { @@ -250,7 +252,7 @@ export default class ShareScreen extends UIElement { } - InnerRender(): UIElement { + InnerRender(): BaseUIElement { const tr = Translations.t.general.sharescreen; diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index bed2d9c92f..254f804d24 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -11,31 +11,15 @@ export default class DirectionInput extends InputElement { private readonly value: UIEventSource; public readonly IsSelected: UIEventSource = new UIEventSource(false); + private _element: HTMLElement; constructor(value?: UIEventSource) { super(); this.value = value ?? new UIEventSource(undefined); - this.value.addCallbackAndRun(rotation => { - const selfElement = document.getElementById(this.id); - if (selfElement === null) { - return; - } - const cone = selfElement.getElementsByClassName("direction-svg")[0] as HTMLElement - cone.style.transform = `rotate(${rotation}deg)`; - - }) - - } - - - GetValue(): UIEventSource { - return this.value; - } - - InnerRender(): string { - return new Combine([ - `
    `, + + this._element = new Combine([ + `
    `, Svg.direction_svg().SetStyle( `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`) .SetClass("direction-svg"), @@ -43,9 +27,31 @@ export default class DirectionInput extends InputElement { "position: absolute;top: 0;left: 0;width: 100%;height: 100%;") ]) .SetStyle("position:relative;display:block;width: min(100%, 25em); padding-top: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") - .Render(); + .ConstructElement() + + +const self = this; + this.value.addCallbackAndRun(rotation => { + const selfElement = self._element; + if (selfElement === null) { + return; + } + const cone = selfElement.getElementsByClassName("direction-svg")[0] as HTMLElement + cone.style.transform = `rotate(${rotation}deg)`; + + }) } + protected InnerConstructElement(): HTMLElement { + return this._element + } + + + GetValue(): UIEventSource { + return this.value; + } + + protected InnerUpdate(htmlElement: HTMLElement) { const self = this; diff --git a/UI/OpeningHours/OpeningHoursPicker.ts b/UI/OpeningHours/OpeningHoursPicker.ts index b620133ad5..f38fb2565b 100644 --- a/UI/OpeningHours/OpeningHoursPicker.ts +++ b/UI/OpeningHours/OpeningHoursPicker.ts @@ -37,7 +37,7 @@ export default class OpeningHoursPicker extends InputElement { source.addCallback(_ => { self._ohs.setData(OH.MergeTimes(self._ohs.data)) }) - const r = new OpeningHoursRange(source, `oh-table-${this._backgroundTable.id}`); + const r = new OpeningHoursRange(source, this._backgroundTable); perWeekday[oh.weekday].push(r); } diff --git a/UI/OpeningHours/OpeningHoursPickerTable.ts b/UI/OpeningHours/OpeningHoursPickerTable.ts index 794f2d28be..cd157aca12 100644 --- a/UI/OpeningHours/OpeningHoursPickerTable.ts +++ b/UI/OpeningHours/OpeningHoursPickerTable.ts @@ -13,6 +13,7 @@ import BaseUIElement from "../BaseUIElement"; export default class OpeningHoursPickerTable extends InputElement { public readonly IsSelected: UIEventSource; private readonly weekdays: UIEventSource; + private readonly _element: HTMLTableElement public static readonly days: BaseUIElement[] = [ @@ -28,6 +29,7 @@ export default class OpeningHoursPickerTable extends InputElement private readonly source: UIEventSource; + private static _nextId = 0; constructor(weekdays: UIEventSource, source?: UIEventSource) { super(); @@ -36,9 +38,11 @@ export default class OpeningHoursPickerTable extends InputElement this.IsSelected = new UIEventSource(false); this.SetStyle("width:100%;height:100%;display:block;"); - } - InnerRender(): string { + const id = OpeningHoursPickerTable._nextId; +OpeningHoursPickerTable._nextId ++ ; + + let rows = ""; const self = this; for (let h = 0; h < 24; h++) { @@ -49,28 +53,32 @@ export default class OpeningHoursPickerTable extends InputElement rows += `${hs}:00` + - Utils.Times(weekday => ``, 7) + + Utils.Times(weekday => ``, 7) + '' + - Utils.Times(id => ``, 7) + + Utils.Times(id => ``, 7) + ''; } let days = OpeningHoursPickerTable.days.map((day, i) => { - const innerContent = self.weekdays.data[i]?.Render() ?? ""; - return day.Render() + ""+innerContent+""; + const innerContent = self.weekdays.data[i]?.ConstructElement()?.innerHTML ?? ""; + return day.ConstructElement().innerHTML + ""+innerContent+""; }).join(""); - return `${rows}
    ${days}
    `; + + this._element = document.createElement("table") + const el = this._element; + this.SetClass("oh-table") + el.innerHTML =`${days}${rows}`; } - protected InnerUpdate() { + protected InnerConstructElement(): HTMLElement { + return this._element + } + + private InnerUpdate(table: HTMLTableElement) { const self = this; - const table = (document.getElementById(`oh-table-${this.id}`) as HTMLTableElement); if (table === undefined || table === null) { return; } - for (const uielement of this.weekdays.data) { - uielement.Update(); - } let mouseIsDown = false; let selectionStart: [number, number] = undefined; diff --git a/UI/OpeningHours/OpeningHoursRange.ts b/UI/OpeningHours/OpeningHoursRange.ts index 68f73ea413..972a4a5b1b 100644 --- a/UI/OpeningHours/OpeningHoursRange.ts +++ b/UI/OpeningHours/OpeningHoursRange.ts @@ -8,16 +8,18 @@ import Svg from "../../Svg"; import {Utils} from "../../Utils"; import Combine from "../Base/Combine"; import {OH, OpeningHour} from "./OpeningHours"; +import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; +import BaseUIElement from "../BaseUIElement"; export default class OpeningHoursRange extends UIElement { private _oh: UIEventSource; - private readonly _startTime: UIElement; - private readonly _endTime: UIElement; - private readonly _deleteRange: UIElement; - private readonly _tableId: string; + private readonly _startTime: BaseUIElement; + private readonly _endTime: BaseUIElement; + private readonly _deleteRange: BaseUIElement; + private readonly _tableId: OpeningHoursPickerTable; - constructor(oh: UIEventSource, tableId: string) { + constructor(oh: UIEventSource, tableId: OpeningHoursPickerTable) { super(oh); this._tableId = tableId; const self = this; @@ -48,7 +50,7 @@ export default class OpeningHoursRange extends UIElement { } - InnerRender(): UIElement { + InnerRender(): BaseUIElement { const oh = this._oh.data; if (oh === undefined) { return undefined; @@ -71,8 +73,7 @@ export default class OpeningHoursRange extends UIElement { if (oh.endHour == 0 && oh.endMinutes == 0) { endhour = 24; } - const height = (endhour - oh.startHour + ((oh.endMinutes - oh.startMinutes) / 60)); - return height; + return (endhour - oh.startHour + ((oh.endMinutes - oh.startMinutes) / 60)); } protected InnerUpdate(el: HTMLElement) { @@ -85,7 +86,7 @@ export default class OpeningHoursRange extends UIElement { } // The header cell containing monday, tuesday, ... - const table = document.getElementById(this._tableId) as HTMLTableElement; + const table = this._tableId.ConstructElement() as HTMLTableElement; const bodyRect = document.body.getBoundingClientRect(); const rangeStart = table.rows[1].cells[1].getBoundingClientRect().top - bodyRect.top; diff --git a/UI/OpeningHours/PublicHolidayInput.ts b/UI/OpeningHours/PublicHolidayInput.ts index d61afdb310..e470dcf98b 100644 --- a/UI/OpeningHours/PublicHolidayInput.ts +++ b/UI/OpeningHours/PublicHolidayInput.ts @@ -1,21 +1,22 @@ - import {OH} from "./OpeningHours"; import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import Combine from "../Base/Combine"; import {TextField} from "../Input/TextField"; import {DropDown} from "../Input/DropDown"; import {InputElement} from "../Input/InputElement"; import Translations from "../i18n/Translations"; +import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; export default class PublicHolidayInput extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private readonly _value: UIEventSource; - private readonly _dropdown: UIElement; + private readonly _dropdown: BaseUIElement; private readonly _mode: UIEventSource; - private readonly _startHour: UIElement; - private readonly _endHour: UIElement; + private readonly _startHour: BaseUIElement; + private readonly _endHour: BaseUIElement; + private _element: VariableUiElement; constructor(value: UIEventSource = new UIEventSource("")) { super(); @@ -31,7 +32,6 @@ export default class PublicHolidayInput extends InputElement { ); this._dropdown = dropdown.SetStyle("display:inline-block;"); this._mode = dropdown.GetValue(); - this.ListenTo(this._mode); const start = new TextField({ placeholder: "starthour", @@ -97,6 +97,27 @@ export default class PublicHolidayInput extends InputElement { end.GetValue().addCallbackAndRun(() => { updateValue(); }); + + this._element = new VariableUiElement(this._mode.map( + mode => { + if (mode === " ") { + return new Combine([this._dropdown, + " ", + Translations.t.general.opening_hours.opensAt, + " ", + this._startHour, + " ", + Translations.t.general.opening_hours.openTill, + " ", + this._endHour]); + } + return this._dropdown; + + })) + } + + protected InnerConstructElement(): HTMLElement { + return this._element.ConstructElement(); } public static LoadValue(str: string): { @@ -143,21 +164,6 @@ export default class PublicHolidayInput extends InputElement { } } - InnerRender(): UIElement { - const mode = this._mode.data; - if (mode === " ") { - return new Combine([this._dropdown, - " ", - Translations.t.general.opening_hours.opensAt, - " ", - this._startHour, - " ", - Translations.t.general.opening_hours.openTill, - " ", - this._endHour]); - } - return this._dropdown; - } GetValue(): UIEventSource { return this._value; From 62f471df1e97cdf1a34e65f59d9bd63970807dd8 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 11 Jun 2021 22:51:45 +0200 Subject: [PATCH 03/30] More refactoring, still very broken --- Logic/Actors/TitleHandler.ts | 69 ++++--- Logic/Web/Imgur.ts | 6 +- Logic/Web/ImgurUploader.ts | 41 +++++ UI/Base/FileSelectorButton.ts | 62 +++++++ UI/Base/VariableUIElement.ts | 16 +- UI/BigComponents/LicensePicker.ts | 19 ++ UI/BigComponents/UploadFlowStateUI.ts | 54 ++++++ UI/Image/DeleteImage.ts | 5 +- UI/Image/ImageCarousel.ts | 15 +- UI/Image/ImageUploadFlow.ts | 256 +++++++++----------------- UI/Image/ImgurImage.ts | 11 +- UI/Image/MapillaryImage.ts | 11 +- UI/Image/SimpleImageElement.ts | 15 -- UI/Image/SlideShow.ts | 60 +++--- UI/Image/WikimediaImage.ts | 11 +- UI/Input/Checkboxes.ts | 22 ++- UI/Input/ColorPicker.ts | 47 ++--- UI/Popup/TagRenderingAnswer.ts | 8 +- UI/Reviews/ReviewElement.ts | 7 +- UI/Reviews/ReviewForm.ts | 26 +-- UI/Reviews/SingleReview.ts | 5 +- UI/SpecialVisualizations.ts | 11 +- UI/SubstitutedTranslation.ts | 7 +- 23 files changed, 428 insertions(+), 356 deletions(-) create mode 100644 Logic/Web/ImgurUploader.ts create mode 100644 UI/Base/FileSelectorButton.ts create mode 100644 UI/BigComponents/LicensePicker.ts create mode 100644 UI/BigComponents/UploadFlowStateUI.ts delete mode 100644 UI/Image/SimpleImageElement.ts diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index adb03a1886..b6f8906141 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -6,8 +6,11 @@ import {UIElement} from "../../UI/UIElement"; import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"; import {ElementStorage} from "../ElementStorage"; import Combine from "../../UI/Base/Combine"; +import BaseUIElement from "../../UI/BaseUIElement"; +import {FixedUiElement} from "../../UI/Base/FixedUiElement"; -class TitleElement extends UIElement { +class TitleElement extends UIEventSource { + private readonly _layoutToUse: UIEventSource; private readonly _selectedFeature: UIEventSource; private readonly _allElementsStorage: ElementStorage; @@ -15,41 +18,43 @@ class TitleElement extends UIElement { constructor(layoutToUse: UIEventSource, selectedFeature: UIEventSource, allElementsStorage: ElementStorage) { - super(layoutToUse); + super("MapComplete"); + this._layoutToUse = layoutToUse; this._selectedFeature = selectedFeature; this._allElementsStorage = allElementsStorage; - this.ListenTo(Locale.language); - this.ListenTo(this._selectedFeature) - } + + this.syncWith( + this._selectedFeature.map( + selected => { + const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ??"MapComplete" - InnerRender(): string { + if(selected === undefined){ + return defaultTitle + } - const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ?? "MapComplete" - const feature = this._selectedFeature.data; - - if (feature === undefined) { - return defaultTitle; - } + const layout = layoutToUse.data; + const tags = selected.properties; - const layout = this._layoutToUse.data; - const properties = this._selectedFeature.data.properties; + for (const layer of layout.layers) { + if (layer.title === undefined) { + continue; + } + if (layer.source.osmTags.matchesProperties(tags)) { + const title = new TagRenderingAnswer(tags, layer.title) + return new Combine([defaultTitle, " | ", title]).ConstructElement().innerText; + } + } - for (const layer of layout.layers) { - if (layer.title === undefined) { - continue; - } - if (layer.source.osmTags.matchesProperties(properties)) { - const tags = this._allElementsStorage.getEventSourceById(feature.properties.id); - if (tags == undefined) { - return defaultTitle; + return defaultTitle } - const title = new TagRenderingAnswer(tags, layer.title) - return new Combine([defaultTitle, " | ", title]).Render(); - } - } - return defaultTitle; + , [Locale.language, layoutToUse] + ) + + ) + + } } @@ -58,14 +63,8 @@ export default class TitleHandler { constructor(layoutToUse: UIEventSource, selectedFeature: UIEventSource, allElementsStorage: ElementStorage) { - - selectedFeature.addCallbackAndRun(_ => { - const title = new TitleElement(layoutToUse, selectedFeature, allElementsStorage) - const d = document.createElement('div'); - d.innerHTML = title.InnerRenderAsString(); - // We pass everything into a div to strip out images etc... - document.title = (d.textContent || d.innerText); + new TitleElement(layoutToUse, selectedFeature, allElementsStorage).addCallbackAndRun(title => { + document.title = title }) - } } \ No newline at end of file diff --git a/Logic/Web/Imgur.ts b/Logic/Web/Imgur.ts index 27a10535df..d48771e112 100644 --- a/Logic/Web/Imgur.ts +++ b/Logic/Web/Imgur.ts @@ -10,11 +10,8 @@ export class Imgur { handleSuccessfullUpload: ((imageURL: string) => void), allDone: (() => void), onFail: ((reason: string) => void), - offset:number) { + offset:number = 0) { - if(offset === undefined){ - throw "Offset undefined - not uploading to prevent to much uploads!" - } if (blobs.length == offset) { allDone(); return; @@ -36,6 +33,7 @@ export class Imgur { } + static getDescriptionOfImage(url: string, handleDescription: ((license: LicenseInfo) => void)) { diff --git a/Logic/Web/ImgurUploader.ts b/Logic/Web/ImgurUploader.ts new file mode 100644 index 0000000000..b21228b5e4 --- /dev/null +++ b/Logic/Web/ImgurUploader.ts @@ -0,0 +1,41 @@ +import {UIEventSource} from "../UIEventSource"; +import {Imgur} from "./Imgur"; + +export default class ImgurUploader { + + public queue: UIEventSource; + public failed: UIEventSource; + public success: UIEventSource + private readonly _handleSuccessUrl: (string) => void; + + constructor(handleSuccessUrl: (string) => void) { + this._handleSuccessUrl = handleSuccessUrl; + } + + public uploadMany(title: string, description: string, files: FileList) { + for (let i = 0; i < files.length; i++) { + this.queue.data.push(files.item(i).name) + } + this.queue.ping() + + const self = this; + this.queue.setData([...self.queue.data]) + Imgur.uploadMultiple(title, + description, + files, + function (url) { + console.log("File saved at", url); + self.success.setData([...self.success.data, url]); + this. handleSuccessUrl(url); + }, + function () { + console.log("All uploads completed"); + }, + + function (failReason) { + console.log("Upload failed due to ", failReason) + self.failed.setData([...self.failed.data, failReason]) + } + ); + } +} \ No newline at end of file diff --git a/UI/Base/FileSelectorButton.ts b/UI/Base/FileSelectorButton.ts new file mode 100644 index 0000000000..9964790694 --- /dev/null +++ b/UI/Base/FileSelectorButton.ts @@ -0,0 +1,62 @@ +import BaseUIElement from "../BaseUIElement"; +import {InputElement} from "../Input/InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; + +export default class FileSelectorButton extends InputElement { + + IsSelected: UIEventSource; + private readonly _value = new UIEventSource(undefined); + private readonly _label: BaseUIElement; + private readonly _acceptType: string; + + constructor(label: BaseUIElement, acceptType: string = "image/*") { + super(); + this._label = label; + this._acceptType = acceptType; + } + + GetValue(): UIEventSource { + return this._value; + } + + IsValid(t: FileList): boolean { + return true; + } + + protected InnerConstructElement(): HTMLElement { + const self = this; + const el = document.createElement("form") + { + const label = document.createElement("label") + label.appendChild(this._label.ConstructElement()) + el.appendChild(label) + } + { + const actualInputElement = document.createElement("input"); + actualInputElement.style.cssText = "display:none"; + actualInputElement.type = "file"; + actualInputElement.accept = this._acceptType; + actualInputElement.name = "picField"; + actualInputElement.multiple = true; + + actualInputElement.onchange = () => { + if (actualInputElement.files !== null) { + self._value.setData(actualInputElement.files) + } + } + + el.addEventListener('submit', e => { + if (actualInputElement.files !== null) { + self._value.setData(actualInputElement.files) + } + e.preventDefault() + }) + + el.appendChild(actualInputElement) + } + + return undefined; + } + + +} \ No newline at end of file diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index 2d813a0aa0..ca38f64d91 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -5,24 +5,28 @@ export class VariableUiElement extends BaseUIElement { private _element : HTMLElement; - constructor(contents: UIEventSource) { + constructor(contents: UIEventSource) { super(); this._element = document.createElement("span") const el = this._element contents.addCallbackAndRun(contents => { - while(el.firstChild){ + while (el.firstChild) { el.removeChild( el.lastChild ) } - - if(contents === undefined){ + + if (contents === undefined) { return } - if(typeof contents === "string"){ + if (typeof contents === "string") { el.innerHTML = contents - }else{ + } else if (contents instanceof Array) { + for (const content of contents) { + el.appendChild(content.ConstructElement()) + } + }else{ el.appendChild(contents.ConstructElement()) } }) diff --git a/UI/BigComponents/LicensePicker.ts b/UI/BigComponents/LicensePicker.ts new file mode 100644 index 0000000000..43d2cac1c4 --- /dev/null +++ b/UI/BigComponents/LicensePicker.ts @@ -0,0 +1,19 @@ +import {DropDown} from "../Input/DropDown"; +import Translations from "../i18n/Translations"; +import State from "../../State"; + +export default class LicensePicker extends DropDown{ + + constructor() { + super(Translations.t.image.willBePublished, + [ + {value: "CC0", shown: Translations.t.image.cco}, + {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") + ) + this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left"); + } + +} \ No newline at end of file diff --git a/UI/BigComponents/UploadFlowStateUI.ts b/UI/BigComponents/UploadFlowStateUI.ts new file mode 100644 index 0000000000..9c00649af2 --- /dev/null +++ b/UI/BigComponents/UploadFlowStateUI.ts @@ -0,0 +1,54 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Translations from "../i18n/Translations"; + +/** + * Shows that 'images are uploading', 'all images are uploaded' as relevant... + */ +export default class UploadFlowStateUI extends UIElement{ + + private readonly _element: BaseUIElement + + constructor(queue: UIEventSource, failed: UIEventSource, success: UIEventSource) { + super(); + const t = Translations.t.image; + + this._element = new VariableUiElement( + + queue.map(queue => { + const failedReasons = failed.data + const successCount = success.data.length + const pendingCount = queue.length - successCount - failedReasons.length; + + let stateMessages : BaseUIElement[] = [] + + if(pendingCount == 1){ + stateMessages.push(t.uploadingPicture.Clone().SetClass("alert")) + } + if(pendingCount > 1){ + stateMessages.push(t.uploadingMultiple.Subs({count: ""+pendingCount}).SetClass("alert")) + } + if(failedReasons.length > 0){ + stateMessages.push(t.uploadFailed.Clone().SetClass("alert")) + } + if(successCount > 0 && pendingCount == 0){ + stateMessages.push(t.uploadDone.SetClass("thanks")) + } + + stateMessages.forEach(msg => msg.SetStyle("display: block ruby")) + + return stateMessages + }, [failed, success]) + + + ); + + + } + + protected InnerRender(): string | BaseUIElement { + return this._element + } +} \ No newline at end of file diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index 19c281465b..df6de5e2ea 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -6,14 +6,15 @@ import Combine from "../Base/Combine"; import State from "../../State"; import Svg from "../../Svg"; import {Tag} from "../../Logic/Tags/Tag"; +import BaseUIElement from "../BaseUIElement"; export default class DeleteImage extends UIElement { private readonly key: string; private readonly tags: UIEventSource; - private readonly isDeletedBadge: UIElement; - private readonly deleteDialog: UIElement; + private readonly isDeletedBadge: BaseUIElement; + private readonly deleteDialog: BaseUIElement; constructor(key: string, tags: UIEventSource) { super(tags); diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 3352c64c34..2528aa32b6 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -6,16 +6,17 @@ import DeleteImage from "./DeleteImage"; import {WikimediaImage} from "./WikimediaImage"; import {ImgurImage} from "./ImgurImage"; import {MapillaryImage} from "./MapillaryImage"; -import {SimpleImageElement} from "./SimpleImageElement"; +import BaseUIElement from "../BaseUIElement"; +import Img from "../Base/Img"; export class ImageCarousel extends UIElement{ - public readonly slideshow: UIElement; + public readonly slideshow: BaseUIElement; constructor(images: UIEventSource<{key: string, url:string}[]>, tags: UIEventSource) { super(images); const uiElements = images.map((imageURLS: {key: string, url:string}[]) => { - const uiElements: UIElement[] = []; + const uiElements: BaseUIElement[] = []; for (const url of imageURLS) { let image = ImageCarousel.CreateImageElement(url.url) if(url.key !== undefined){ @@ -41,7 +42,7 @@ export class ImageCarousel extends UIElement{ * @param url * @constructor */ - private static CreateImageElement(url: string): UIElement { + private static CreateImageElement(url: string): BaseUIElement { // @ts-ignore if (url.startsWith("File:")) { return new WikimediaImage(url); @@ -53,11 +54,11 @@ export class ImageCarousel extends UIElement{ } else if (url.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) { return new MapillaryImage(url); } else { - return new SimpleImageElement(new UIEventSource(url)); + return new Img(url); } } - InnerRender(): string { - return this.slideshow.Render(); + InnerRender() { + return this.slideshow; } } \ No newline at end of file diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 13a8440495..9182406d21 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -1,207 +1,119 @@ -import $ from "jquery" import {UIEventSource} from "../../Logic/UIEventSource"; import {UIElement} from "../UIElement"; import State from "../../State"; import Combine from "../Base/Combine"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {Imgur} from "../../Logic/Web/Imgur"; -import {DropDown} from "../Input/DropDown"; import Translations from "../i18n/Translations"; import Svg from "../../Svg"; import {Tag} from "../../Logic/Tags/Tag"; import BaseUIElement from "../BaseUIElement"; +import LicensePicker from "../BigComponents/LicensePicker"; +import Toggle from "../Input/Toggle"; +import FileSelectorButton from "../Base/FileSelectorButton"; +import ImgurUploader from "../../Logic/Web/ImgurUploader"; +import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; +import LayerConfig from "../../Customizations/JSON/LayerConfig"; export class ImageUploadFlow extends UIElement { - private readonly _licensePicker: BaseUIElement; + + private readonly _element: BaseUIElement; + + private readonly _tags: UIEventSource; private readonly _selectedLicence: UIEventSource; - private readonly _isUploading: UIEventSource = new UIEventSource(0) - private readonly _didFail: UIEventSource = new UIEventSource(false); - private readonly _allDone: UIEventSource = new UIEventSource(false); - private readonly _connectButton: UIElement; + + private readonly _imagePrefix: string; - constructor(tags: UIEventSource, imagePrefix: string = "image") { + constructor(tagsSource: UIEventSource, imagePrefix: string = "image") { super(State.state.osmConnection.userDetails); - this._tags = tags; this._imagePrefix = imagePrefix; - this.ListenTo(this._isUploading); - this.ListenTo(this._didFail); - this.ListenTo(this._allDone); - const licensePicker = new DropDown(Translations.t.image.willBePublished, - [ - {value: "CC0", shown: Translations.t.image.cco}, - {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") - ).SetClass("flex flex-col sm:flex-row"); - licensePicker.SetStyle("float:left"); + const uploader = new ImgurUploader(url => { + // A file was uploaded - we add it to the tags of the object - const t = Translations.t.image; - - this._licensePicker = licensePicker; - this._selectedLicence = licensePicker.GetValue(); - - this._connectButton = t.pleaseLogin.Clone() - .onClick(() => State.state.osmConnection.AttemptLogin()) - .SetClass("login-button-friendly"); - - } - - InnerRender(): string { - - if(!State.state.featureSwitchUserbadge.data){ - return ""; - } - - const t = Translations.t.image; - if (State.state.osmConnection.userDetails === undefined) { - return ""; // No user details -> logging in is probably disabled or smthing - } - - if (!State.state.osmConnection.userDetails.data.loggedIn) { - return this._connectButton.Render(); - } - - let currentState: UIElement[] = []; - if (this._isUploading.data == 1) { - currentState.push(t.uploadingPicture); - } else if (this._isUploading.data > 0) { - currentState.push(t.uploadingMultiple.Subs({count: ""+this._isUploading.data})); - } - - if (this._didFail.data) { - currentState.push(t.uploadFailed); - } - - if (this._allDone.data) { - currentState.push(t.uploadDone) - } - - let currentStateHtml : UIElement = new FixedUiElement(""); - if (currentState.length > 0) { - currentStateHtml = new Combine(currentState); - if (!this._allDone.data) { - currentStateHtml.SetClass("alert"); - }else{ - currentStateHtml.SetClass("thanks"); + const tags = tagsSource.data + let key = imagePrefix + if (tags[imagePrefix] !== undefined) { + let freeIndex = 0; + while (tags[imagePrefix + ":" + freeIndex] !== undefined) { + freeIndex++; + } + key = imagePrefix + ":" + freeIndex; } - currentStateHtml.SetStyle("display:block ruby") - } + console.log("Adding image:" + key, url); + State.state.changes.addTag(tags.id, new Tag(key, url)); + }) - const extraInfo = new Combine([ - Translations.t.image.respectPrivacy.SetStyle("font-size:small;"), - "
    ", - this._licensePicker, - "
    ", - currentStateHtml, - "
    " - ]); + const licensePicker = new LicensePicker() + + const t = Translations.t.image; const label = new Combine([ Svg.camera_plus_svg().SetStyle("width: 36px;height: 36px;padding: 0.1em;margin-top: 5px;border-radius: 0;float: left;display:block"), Translations.t.image.addPicture ]).SetClass("image-upload-flow-button") - - const actualInputElement = - ``; - - const form = "
    " + - `" + - actualInputElement + - "
    "; + const fileSelector = new FileSelectorButton(label) + fileSelector.GetValue().addCallback(filelist => { + if (filelist === undefined) { + return; + } - return new Combine([ - form, - extraInfo + console.log("Received images from the user, starting upload") + const license = this._selectedLicence.data ?? "CC0" + + const tags = this._tags.data; + + const layout = State.state.layoutToUse.data + let matchingLayer: LayerConfig = undefined + for (const layer of layout.layers) { + if (layer.source.osmTags.matchesProperties(tags)) { + matchingLayer = layer; + break; + } + } + + + const title = matchingLayer?.title?.GetRenderValue(tags)?.ConstructElement().innerText ?? tags.name ?? "Unknown area"; + const description = [ + "author:" + State.state.osmConnection.userDetails.data.name, + "license:" + license, + "osmid:" + tags.id, + ].join("\n"); + + uploader.uploadMany(title, description, filelist) + + }) + + + const uploadStateUi = new UploadFlowStateUI(uploader.queue, uploader.failed, uploader.success) + + const uploadFlow: BaseUIElement = new Combine([ + fileSelector, + Translations.t.image.respectPrivacy.SetStyle("font-size:small;"), + licensePicker, + uploadStateUi ]).SetClass("image-upload-flow") - .SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;") - .Render(); - } + .SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;"); - private handleSuccessfulUpload(url) { - const tags = this._tags.data; - let key = this._imagePrefix; - if (tags[this._imagePrefix] !== undefined) { - - let freeIndex = 0; - while (tags[this._imagePrefix + ":" + freeIndex] !== undefined) { - freeIndex++; - } - key = this._imagePrefix + ":" + freeIndex; - } - console.log("Adding image:" + key, url); - State.state.changes.addTag(tags.id, new Tag(key, url)); - } - - private handleFiles(files) { - console.log("Received images from the user, starting upload") - this._isUploading.setData(files.length); - this._allDone.setData(false); - - if (this._selectedLicence.data === undefined) { - this._selectedLicence.setData("CC0"); - } - - - const tags = this._tags.data; - const title = tags.name ?? "Unknown area"; - const description = [ - "author:" + State.state.osmConnection.userDetails.data.name, - "license:" + (this._selectedLicence.data ?? "CC0"), - "wikidata:" + tags.wikidata, - "osmid:" + tags.id, - "name:" + tags.name - ].join("\n"); - - const self = this; - - Imgur.uploadMultiple(title, - description, - files, - function (url) { - console.log("File saved at", url); - self._isUploading.setData(self._isUploading.data - 1); - self.handleSuccessfulUpload(url); - }, - function () { - console.log("All uploads completed"); - self._allDone.setData(true); - }, - function (failReason) { - console.log("Upload failed due to ", failReason) - // No need to call something from the options -> we handle this here - self._didFail.setData(true); - self._isUploading.data--; - self._isUploading.ping(); - }, 0 + const pleaseLoginButton = t.pleaseLogin.Clone() + .onClick(() => State.state.osmConnection.AttemptLogin()) + .SetClass("login-button-friendly"); + this._element = new Toggle( + new Toggle( + /*We can show the actual upload button!*/ + uploadFlow, + /* User not logged in*/ pleaseLoginButton, + State.state.osmConnection.userDetails.map(userinfo => userinfo.loggedIn) + ), + undefined /* Nothing as the user badge is disabled*/, State.state.featureSwitchUserbadge ) + } - InnerUpdate(htmlElement: HTMLElement) { - this._licensePicker.Update() - const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement - const selector = document.getElementById('fileselector-' + this.id) - const self = this - - function submitHandler() { - self.handleFiles($(selector).prop('files')) - } - - if (selector != null && form != null) { - selector.onchange = function () { - submitHandler() - } - form.addEventListener('submit', e => { - e.preventDefault() - submitHandler() - }) - } + protected InnerRender(): string | BaseUIElement { + return this._element; } + } \ No newline at end of file diff --git a/UI/Image/ImgurImage.ts b/UI/Image/ImgurImage.ts index e8639dae40..176e163729 100644 --- a/UI/Image/ImgurImage.ts +++ b/UI/Image/ImgurImage.ts @@ -4,7 +4,8 @@ import {LicenseInfo} from "../../Logic/Web/Wikimedia"; import {Imgur} from "../../Logic/Web/Imgur"; import Combine from "../Base/Combine"; import Attribution from "./Attribution"; -import {SimpleImageElement} from "./SimpleImageElement"; +import BaseUIElement from "../BaseUIElement"; +import Img from "../Base/Img"; export class ImgurImage extends UIElement { @@ -35,11 +36,11 @@ export class ImgurImage extends UIElement { } - InnerRender(): string { - const image = new SimpleImageElement( new UIEventSource (this._imageLocation)); + InnerRender(): BaseUIElement { + const image = new Img( this._imageLocation); if(this._imageMeta.data === null){ - return image.Render(); + return image; } const meta = this._imageMeta.data; @@ -48,7 +49,7 @@ export class ImgurImage extends UIElement { new Attribution(meta.artist, meta.license, undefined), ]).SetClass('block relative') - .Render(); + ; } diff --git a/UI/Image/MapillaryImage.ts b/UI/Image/MapillaryImage.ts index 53c0e53e98..9b2edf5655 100644 --- a/UI/Image/MapillaryImage.ts +++ b/UI/Image/MapillaryImage.ts @@ -3,9 +3,10 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import {LicenseInfo} from "../../Logic/Web/Wikimedia"; import {Mapillary} from "../../Logic/Web/Mapillary"; import Svg from "../../Svg"; -import {SimpleImageElement} from "./SimpleImageElement"; import Combine from "../Base/Combine"; import Attribution from "./Attribution"; +import Img from "../Base/Img"; +import BaseUIElement from "../BaseUIElement"; export class MapillaryImage extends UIElement { @@ -40,19 +41,19 @@ export class MapillaryImage extends UIElement { } - InnerRender(): string { + InnerRender(): BaseUIElement { const url = `https://images.mapillary.com/${this._imageLocation}/thumb-640.jpg?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`; - const image = new SimpleImageElement(new UIEventSource(url)) + const image = new Img(url) const meta = this._imageMeta?.data; if (!meta) { - return image.Render(); + return image; } return new Combine([ image, new Attribution(meta.artist, meta.license, Svg.mapillary_svg()) - ]).SetClass("relative block").Render(); + ]).SetClass("relative block"); } diff --git a/UI/Image/SimpleImageElement.ts b/UI/Image/SimpleImageElement.ts deleted file mode 100644 index c17f0fa49a..0000000000 --- a/UI/Image/SimpleImageElement.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; - - -export class SimpleImageElement extends UIElement { - - constructor(source: UIEventSource) { - super(source); - } - - InnerRender(): string { - return "img"; - } - -} \ No newline at end of file diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 1021a3c039..8cf7b07194 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -1,46 +1,22 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import Combine from "../Base/Combine"; -// @ts-ignore -import $ from "jquery" +import BaseUIElement from "../BaseUIElement"; -export class SlideShow extends UIElement { +export class SlideShow extends BaseUIElement { - private readonly _embeddedElements: UIEventSource + private readonly _element: HTMLElement; + constructor( - embeddedElements: UIEventSource) { - super(embeddedElements); - this._embeddedElements = embeddedElements; - this._embeddedElements.addCallbackAndRun(elements => { - for (const element of elements ?? []) { - element.SetClass("slick-carousel-content") - } - }) - - } - - InnerRender(): string { - return new Combine( - this._embeddedElements.data, - ).SetClass("block slick-carousel") - .Render(); - } - - Update() { - super.Update(); - for (const uiElement of this._embeddedElements.data) { - uiElement.Update(); - } - } - - protected InnerUpdate(htmlElement: HTMLElement) { + embeddedElements: UIEventSource) { + super() + + const el = document.createElement("div") + this._element = el; + + el.classList.add("slick-carousel") require("slick-carousel") - if(this._embeddedElements.data.length == 0){ - return; - } // @ts-ignore - $('.slick-carousel').not('.slick-initialized').slick({ + el.slick({ autoplay: true, arrows: true, dots: true, @@ -48,8 +24,18 @@ export class SlideShow extends UIElement { variableWidth: true, centerMode: true, centerPadding: "60px", - adaptive: true + adaptive: true }); + embeddedElements.addCallbackAndRun(elements => { + for (const element of elements ?? []) { + element.SetClass("slick-carousel-content") + } + }); + + } + + protected InnerConstructElement(): HTMLElement { + return this._element; } } \ No newline at end of file diff --git a/UI/Image/WikimediaImage.ts b/UI/Image/WikimediaImage.ts index ceae32202b..754c8dab5c 100644 --- a/UI/Image/WikimediaImage.ts +++ b/UI/Image/WikimediaImage.ts @@ -4,8 +4,9 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import Svg from "../../Svg"; import Link from "../Base/Link"; import Combine from "../Base/Combine"; -import {SimpleImageElement} from "./SimpleImageElement"; import Attribution from "./Attribution"; +import BaseUIElement from "../BaseUIElement"; +import Img from "../Base/Img"; export class WikimediaImage extends UIElement { @@ -34,14 +35,14 @@ export class WikimediaImage extends UIElement { } - InnerRender(): string { + InnerRender(): BaseUIElement { const url = Wikimedia.ImageNameToUrl(this._imageLocation, 500, 400) .replace(/'/g, '%27'); - const image = new SimpleImageElement(new UIEventSource(url)) + const image = new Img(url) const meta = this._imageMeta?.data; if (!meta) { - return image.Render(); + return image; } new Link(Svg.wikimedia_commons_white_img, `https://commons.wikimedia.org/wiki/${this._imageLocation}`, true) @@ -50,7 +51,7 @@ export class WikimediaImage extends UIElement { return new Combine([ image, new Attribution(meta.artist, meta.license, Svg.wikimedia_commons_white_svg()) - ]).SetClass("relative block").Render() + ]).SetClass("relative block") } diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index cd0dbcac9f..aa378beb82 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -2,6 +2,7 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import {Utils} from "../../Utils"; import {UIElement} from "../UIElement"; +import BaseUIElement from "../BaseUIElement"; /** * Supports multi-input @@ -10,15 +11,24 @@ export default class CheckBoxes extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; - private readonly _elements: UIElement[] + private readonly _elements: BaseUIElement[] + + +private readonly _element : HTMLElement - - constructor(elements: UIElement[]) { - super(undefined); + constructor(elements: BaseUIElement[]) { + super(); this._elements = Utils.NoNull(elements); - this.value = new UIEventSource([]) - this.ListenTo(this.value); + + + const el = document.createElement() + this._element = el; + + } + + protected InnerConstructElement(): HTMLElement { + return this._element } diff --git a/UI/Input/ColorPicker.ts b/UI/Input/ColorPicker.ts index 2fe6adf0dc..ea0abda60e 100644 --- a/UI/Input/ColorPicker.ts +++ b/UI/Input/ColorPicker.ts @@ -4,46 +4,33 @@ import {UIEventSource} from "../../Logic/UIEventSource"; export default class ColorPicker extends InputElement { private readonly value: UIEventSource - +private readonly _element : HTMLElement constructor( - value?: UIEventSource + value: UIEventSource = new UIEventSource(undefined) ) { super(); - this.value = value ?? new UIEventSource(undefined); - const self = this; + this.value = value ; + + const el = document.createElement("input") + this._element = el; + + el.type = "color" + this.value.addCallbackAndRun(v => { if(v === undefined){ return; } - self.SetValue(v); + el.value =v }); + + el.oninput = () => { + const hex = el.value; + value.setData(hex); + } } - - InnerRender(): string { - return ``; - } - - private SetValue(color: string){ - const field = document.getElementById("color-" + this.id); - if (field === undefined || field === null) { - return; - } - // @ts-ignore - field.value = color; - } - - protected InnerUpdate() { - const field = document.getElementById("color-" + this.id); - if (field === undefined || field === null) { - return; - } - const self = this; - field.oninput = () => { - const hex = field["value"]; - self.value.setData(hex); - } - + protected InnerConstructElement(): HTMLElement { + return this._element; } GetValue(): UIEventSource { diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 1138da448a..dc880d3049 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -6,6 +6,7 @@ import Combine from "../Base/Combine"; import {SubstitutedTranslation} from "../SubstitutedTranslation"; import {Translation} from "../i18n/Translation"; import {TagUtils} from "../../Logic/Tags/TagUtils"; +import BaseUIElement from "../BaseUIElement"; /*** * Displays the correct value for a known tagrendering @@ -13,7 +14,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils"; export default class TagRenderingAnswer extends UIElement { private readonly _tags: UIEventSource; private _configuration: TagRenderingConfig; - private _content: UIElement; + private _content: BaseUIElement; private readonly _contentClass: string; private _contentStyle: string; @@ -30,7 +31,7 @@ export default class TagRenderingAnswer extends UIElement { this.SetStyle("word-wrap: anywhere;"); } - InnerRender(): string | UIElement{ + InnerRender(): string | BaseUIElement{ if (this._configuration.condition !== undefined) { if (!this._configuration.condition.matchesProperties(this._tags.data)) { return ""; @@ -74,8 +75,7 @@ export default class TagRenderingAnswer extends UIElement { this._content = valuesToRender[0]; } else { this._content = new Combine(["
      ", - ...valuesToRender.map(tr => new Combine(["
    • ", tr, "
    • "])) - , + ...valuesToRender.map(tr => new Combine(["
    • ", tr, "
    • "])) , "
    " ]) diff --git a/UI/Reviews/ReviewElement.ts b/UI/Reviews/ReviewElement.ts index cdf6659e92..38b9f226a0 100644 --- a/UI/Reviews/ReviewElement.ts +++ b/UI/Reviews/ReviewElement.ts @@ -8,13 +8,14 @@ import {UIElement} from "../UIElement"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; import SingleReview from "./SingleReview"; +import BaseUIElement from "../BaseUIElement"; export default class ReviewElement extends UIElement { private readonly _reviews: UIEventSource; private readonly _subject: string; - private readonly _middleElement: UIElement; + private readonly _middleElement: BaseUIElement; - constructor(subject: string, reviews: UIEventSource, middleElement: UIElement) { + constructor(subject: string, reviews: UIEventSource, middleElement: BaseUIElement) { super(reviews); this._middleElement = middleElement; if (reviews === undefined) { @@ -26,7 +27,7 @@ export default class ReviewElement extends UIElement { - InnerRender(): UIElement { + InnerRender(): BaseUIElement { const elements = []; const revs = this._reviews.data; diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index 59d86c7928..ce2ac3423e 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import {InputElement} from "../Input/InputElement"; import {Review} from "../../Logic/Web/Review"; import {UIEventSource} from "../../Logic/UIEventSource"; @@ -10,16 +9,18 @@ import {VariableUiElement} from "../Base/VariableUIElement"; import {SaveButton} from "../Popup/SaveButton"; import CheckBoxes from "../Input/Checkboxes"; import UserDetails from "../../Logic/Osm/OsmConnection"; +import BaseUIElement from "../BaseUIElement"; +import Toggle from "../Input/Toggle"; export default class ReviewForm extends InputElement { private readonly _value: UIEventSource; - private readonly _comment: UIElement; - private readonly _stars: UIElement; - private _saveButton: UIElement; - private readonly _isAffiliated: UIElement; + private readonly _comment: BaseUIElement; + private readonly _stars: BaseUIElement; + private _saveButton: BaseUIElement; + private readonly _isAffiliated: BaseUIElement; private userDetails: UIEventSource; - private readonly _postingAs: UIElement; + private readonly _postingAs: BaseUIElement; constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), userDetails: UIEventSource) { @@ -86,13 +87,9 @@ export default class ReviewForm extends InputElement { return this._value; } - InnerRender(): UIElement { + InnerConstructElement(): HTMLElement { - if(!this.userDetails.data.loggedIn){ - return Translations.t.reviews.plz_login; - } - - return new Combine([ + const form = new Combine([ new Combine([this._stars, this._postingAs]).SetClass("review-form-top"), this._comment, new Combine([ @@ -103,6 +100,11 @@ export default class ReviewForm extends InputElement { Translations.t.reviews.tos.SetClass("subtle") ]) .SetClass("review-form") + + + return new Toggle(form, Translations.t.reviews.plz_login, + this.userDetails.map(userdetails => userdetails.loggedIn)) + .ConstructElement() } IsSelected: UIEventSource = new UIEventSource(false); diff --git a/UI/Reviews/SingleReview.ts b/UI/Reviews/SingleReview.ts index b6f913add5..a595ba7f95 100644 --- a/UI/Reviews/SingleReview.ts +++ b/UI/Reviews/SingleReview.ts @@ -5,6 +5,7 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import {Utils} from "../../Utils"; import ReviewElement from "./ReviewElement"; +import BaseUIElement from "../BaseUIElement"; export default class SingleReview extends UIElement{ private _review: Review; @@ -13,7 +14,7 @@ export default class SingleReview extends UIElement{ this._review = review; } - public static GenStars(rating: number): UIElement { + public static GenStars(rating: number): BaseUIElement { if (rating === undefined) { return Translations.t.reviews.no_rating; } @@ -26,7 +27,7 @@ export default class SingleReview extends UIElement{ scoreTen % 2 == 1 ? "" : "" ]).SetClass("flex w-max") } - InnerRender(): UIElement { + InnerRender(): BaseUIElement { const d = this._review.date; let review = this._review; const el= new Combine( diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 0307fa7d14..1c46e4fc56 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -1,4 +1,3 @@ -import {UIElement} from "./UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; import {VariableUiElement} from "./Base/VariableUIElement"; import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; @@ -17,12 +16,15 @@ import OpeningHoursVisualization from "./OpeningHours/OhVisualization"; import State from "../State"; import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; +import BaseUIElement from "./BaseUIElement"; export default class SpecialVisualizations { + + public static specialVisualizations: { funcName: string, - constr: ((state: State, tagSource: UIEventSource, argument: string[]) => UIElement), + constr: ((state: State, tagSource: UIEventSource, argument: string[]) => BaseUIElement), docs: string, example?: string, args: { name: string, defaultValue?: string, doc: string }[] @@ -36,6 +38,9 @@ export default class SpecialVisualizations { return new VariableUiElement(tags.map(tags => { const parts = []; for (const key in tags) { + if(!tags.hasOwnProperty(key)){ + continue; + } parts.push(key + "=" + tags[key]); } return parts.join("
    ") @@ -179,7 +184,7 @@ export default class SpecialVisualizations { } ] - static HelpMessage: UIElement = SpecialVisualizations.GenHelpMessage(); + static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); private static GenHelpMessage() { diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 980503db02..e63a7f8e3d 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -6,11 +6,12 @@ import Combine from "./Base/Combine"; import State from "../State"; import {FixedUiElement} from "./Base/FixedUiElement"; import SpecialVisualizations from "./SpecialVisualizations"; +import BaseUIElement from "./BaseUIElement"; export class SubstitutedTranslation extends UIElement { private readonly tags: UIEventSource; private readonly translation: Translation; - private content: UIElement[]; + private content: BaseUIElement[]; private constructor( translation: Translation, @@ -54,7 +55,7 @@ export class SubstitutedTranslation extends UIElement { return new Combine(this.content); } - private CreateContent(): UIElement[] { + private CreateContent(): BaseUIElement[] { let txt = this.translation?.txt; if (txt === undefined) { return [] @@ -64,7 +65,7 @@ export class SubstitutedTranslation extends UIElement { return this.EvaluateSpecialComponents(txt); } - private EvaluateSpecialComponents(template: string): UIElement[] { + private EvaluateSpecialComponents(template: string): BaseUIElement[] { for (const knownSpecial of SpecialVisualizations.specialVisualizations) { From 3943100e5475df275846086088a8c6cae544426f Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 12 Jun 2021 02:58:32 +0200 Subject: [PATCH 04/30] More refactoring, stuff kindoff works --- InitUiElements.ts | 9 +- Logic/Actors/GeoLocationHandler.ts | 84 +++----- Logic/Actors/TitleHandler.ts | 6 +- Logic/Osm/OsmConnection.ts | 2 + State.ts | 4 - UI/Base/Button.ts | 3 +- UI/Base/FeatureSwitched.ts | 22 -- UI/Base/FixedUiElement.ts | 11 +- UI/Base/Img.ts | 3 + UI/Base/LazyElement.ts | 36 ---- UI/Base/Link.ts | 10 +- UI/Base/Ornament.ts | 3 - UI/Base/PageSplit.ts | 20 -- UI/Base/SubtleButton.ts | 42 ++-- UI/Base/TabbedComponent.ts | 44 ++-- UI/BaseUIElement.ts | 8 +- UI/BigComponents/Attribution.ts | 83 ++++---- UI/BigComponents/AttributionPanel.ts | 8 +- UI/BigComponents/BackgroundSelector.ts | 50 +++-- UI/BigComponents/Basemap.ts | 11 +- UI/BigComponents/FullWelcomePaneWithTabs.ts | 28 +-- UI/BigComponents/LayerSelection.ts | 41 ++-- UI/BigComponents/MoreScreen.ts | 12 +- UI/BigComponents/PersonalLayersPanel.ts | 29 +-- UI/BigComponents/SearchAndGo.ts | 118 ++++++----- UI/BigComponents/ShareButton.ts | 27 +-- UI/BigComponents/SimpleAddUI.ts | 45 ++-- UI/BigComponents/ThemeIntroductionPanel.ts | 69 +++---- UI/BigComponents/UserBadge.ts | 215 ++++++++++---------- UI/CenterMessageBox.ts | 84 ++++---- UI/Image/Attribution.ts | 3 +- UI/Image/ImageUploadFlow.ts | 2 +- UI/Input/Checkboxes.ts | 102 +++++----- UI/Input/DropDown.ts | 14 +- UI/{Base => Input}/FileSelectorButton.ts | 0 UI/Input/TextField.ts | 66 +++--- UI/Input/Toggle.ts | 7 +- UI/LanguagePicker.ts | 4 +- UI/MapControlButton.ts | 8 +- UI/OpeningHours/OhVisualization.ts | 11 +- UI/Popup/SaveButton.ts | 46 +++-- UI/Popup/TagRenderingAnswer.ts | 3 +- UI/Popup/TagRenderingQuestion.ts | 10 +- UI/ShowDataLayer.ts | 2 +- UI/UIElement.ts | 8 +- css/userbadge.css | 11 - index.css | 66 +----- index.html | 2 +- index.ts | 1 - langs/en.json | 1 + test.ts | 17 +- vendor/Leaflet.AccuratePosition.js | 134 ------------ 52 files changed, 635 insertions(+), 1010 deletions(-) delete mode 100644 UI/Base/FeatureSwitched.ts delete mode 100644 UI/Base/LazyElement.ts delete mode 100644 UI/Base/PageSplit.ts rename UI/{Base => Input}/FileSelectorButton.ts (100%) delete mode 100644 vendor/Leaflet.AccuratePosition.js diff --git a/InitUiElements.ts b/InitUiElements.ts index e8386d7c0b..8fd6f88be9 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -25,7 +25,6 @@ import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; import LayerResetter from "./Logic/Actors/LayerResetter"; import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; import LayerControlPanel from "./UI/BigComponents/LayerControlPanel"; -import FeatureSwitched from "./UI/Base/FeatureSwitched"; import ShowDataLayer from "./UI/ShowDataLayer"; import Hash from "./Logic/Web/Hash"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; @@ -39,7 +38,6 @@ import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; import AttributionPanel from "./UI/BigComponents/AttributionPanel"; import ContributorCount from "./Logic/ContributorCount"; import FeatureSource from "./Logic/FeatureSource/FeatureSource"; -import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; import AllKnownLayers from "./Customizations/AllKnownLayers"; import LayerConfig from "./Customizations/JSON/LayerConfig"; @@ -170,13 +168,14 @@ export class InitUiElements { marker.addTo(State.state.leafletMap.data) }); - const geolocationButton = new FeatureSwitched( + const geolocationButton = new Toggle( new MapControlButton( new GeoLocationHandler( State.state.currentGPSLocation, State.state.leafletMap, State.state.layoutToUse )), + undefined, State.state.featureSwitchGeolocation); const plus = new MapControlButton( @@ -193,7 +192,7 @@ export class InitUiElements { State.state.locationControl.ping(); }) - new Combine([plus, min, geolocationButton].map(el => el.SetClass("m-1"))) + new Combine([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) .SetClass("flex flex-col") .AttachTo("bottom-right"); @@ -212,8 +211,6 @@ export class InitUiElements { // Reset the loading message once things are loaded new CenterMessageBox().AttachTo("centermessage"); - // At last, zoom to the needed location if the focus is on an element - } diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 90d6cec310..64545ee906 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -6,6 +6,8 @@ import Svg from "../../Svg"; import Img from "../../UI/Base/Img"; import {LocalStorageSource} from "../Web/LocalStorageSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; +import BaseUIElement from "../../UI/BaseUIElement"; +import {VariableUiElement} from "../../UI/Base/VariableUIElement"; export default class GeoLocationHandler extends UIElement { @@ -52,19 +54,19 @@ export default class GeoLocationHandler extends UIElement { private readonly _previousLocationGrant: UIEventSource = LocalStorageSource.Get("geolocation-permissions"); private readonly _layoutToUse: UIEventSource; + + private readonly _element: BaseUIElement; + constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, leafletMap: UIEventSource, layoutToUse: UIEventSource) { - super(undefined); + super(); this._currentGPSLocation = currentGPSLocation; this._leafletMap = leafletMap; this._layoutToUse = layoutToUse; this._hasLocation = currentGPSLocation.map((location) => location !== undefined); - this.dumbMode = false; + const self = this; - import("../../vendor/Leaflet.AccuratePosition.js").then(() => { - self.init(); - }) const currentPointer = this._isActive.map(isActive => { if (isActive && !self._hasLocation.data) { @@ -76,60 +78,34 @@ export default class GeoLocationHandler extends UIElement { self.SetClass(pointerClass); self.Update() }) + this._element = new VariableUiElement( + this._hasLocation.map(hasLocation => { + + if (hasLocation) { + return Svg.crosshair_blue_ui() + } + if (self._isActive.data) { + return Svg.crosshair_blue_center_ui(); + } + return Svg.crosshair_ui(); + }, [this._isActive]) + ); + + this.onClick(() => self.init(true)) + + self.init(false) + } - InnerRender(): string { - if (this._hasLocation.data) { - return Svg.crosshair_blue_img; - } - if (this._isActive.data) { - return Svg.crosshair_blue_center_img; - } - return Svg.crosshair_img; + protected InnerRender(): string | BaseUIElement { + return this._element } - InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); + private init(askPermission: boolean) { const self = this; - htmlElement.onclick = function () { - self.StartGeolocating(); - } - - htmlElement.oncontextmenu = function (e) { - self.StartGeolocating(); - e.preventDefault(); - return false; - } - - } - - private init() { - this.ListenTo(this._hasLocation); - this.ListenTo(this._isActive); - this.ListenTo(this._permission); - - const self = this; - - function onAccuratePositionProgress(e) { - self._currentGPSLocation.setData({latlng: e.latlng, accuracy: e.accuracy}); - } - - function onAccuratePositionFound(e) { - self._currentGPSLocation.setData({latlng: e.latlng, accuracy: e.accuracy}); - } - - function onAccuratePositionError(e) { - console.log("onerror", e.message); - - } - const map = this._leafletMap.data; - map.on('accuratepositionprogress', onAccuratePositionProgress); - map.on('accuratepositionfound', onAccuratePositionFound); - map.on('accuratepositionerror', onAccuratePositionError); - this._currentGPSLocation.addCallback((location) => { self._previousLocationGrant.setData("granted"); @@ -178,7 +154,9 @@ export default class GeoLocationHandler extends UIElement { } catch (e) { console.error(e) } - if (this._previousLocationGrant.data === "granted") { + if (askPermission) { + self.StartGeolocating(true); + } else if (this._previousLocationGrant.data === "granted") { this._previousLocationGrant.setData(""); self.StartGeolocating(false); } @@ -210,7 +188,7 @@ export default class GeoLocationHandler extends UIElement { private MoveToCurrentLoction(targetZoom = 16) { const location = this._currentGPSLocation.data; this._lastUserRequest = undefined; - + if (this._currentGPSLocation.data.latlng[0] === 0 && this._currentGPSLocation.data.latlng[1] === 0) { console.debug("Not moving to GPS-location: it is null island") diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index b6f8906141..16459bac87 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -2,12 +2,9 @@ import {UIEventSource} from "../UIEventSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import Translations from "../../UI/i18n/Translations"; import Locale from "../../UI/i18n/Locale"; -import {UIElement} from "../../UI/UIElement"; import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"; import {ElementStorage} from "../ElementStorage"; import Combine from "../../UI/Base/Combine"; -import BaseUIElement from "../../UI/BaseUIElement"; -import {FixedUiElement} from "../../UI/Base/FixedUiElement"; class TitleElement extends UIEventSource { @@ -42,7 +39,8 @@ class TitleElement extends UIEventSource { continue; } if (layer.source.osmTags.matchesProperties(tags)) { - const title = new TagRenderingAnswer(tags, layer.title) + const tagsSource = allElementsStorage.getEventSourceById(tags.id) + const title = new TagRenderingAnswer(tagsSource, layer.title) return new Combine([defaultTitle, " | ", title]).ConstructElement().innerText; } } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index e74fb1c97f..f078ec6247 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -24,6 +24,7 @@ export class OsmConnection { public auth; public userDetails: UIEventSource; + public isLoggedIn: UIEventSource _dryRun: boolean; public preferencesHandler: OsmPreferences; @@ -42,6 +43,7 @@ export class OsmConnection { this.userDetails = new UIEventSource(new UserDetails(), "userDetails"); this.userDetails.data.dryRun = dryRun; + this.isLoggedIn = this.userDetails.map(user => user.loggedIn) this._dryRun = dryRun; this.updateAuthObject(); diff --git a/State.ts b/State.ts index 25053f497c..0e27ece694 100644 --- a/State.ts +++ b/State.ts @@ -70,10 +70,6 @@ export default class State { readonly layerDef: LayerConfig; }[]>([]) - /** - * The message that should be shown at the center of the screen - */ - public readonly centerMessage = new UIEventSource(""); /** The latest element that was selected diff --git a/UI/Base/Button.ts b/UI/Base/Button.ts index 254a1b0423..89364807bf 100644 --- a/UI/Base/Button.ts +++ b/UI/Base/Button.ts @@ -1,9 +1,10 @@ import {UIElement} from "../UIElement"; import Locale from "../i18n/Locale"; import Translations from "../i18n/Translations"; +import BaseUIElement from "../BaseUIElement"; export class Button extends UIElement { - private _text: UIElement; + private _text: BaseUIElement; private _onclick: () => void; private _clss: string; diff --git a/UI/Base/FeatureSwitched.ts b/UI/Base/FeatureSwitched.ts deleted file mode 100644 index 7641a4801a..0000000000 --- a/UI/Base/FeatureSwitched.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; - -export default class FeatureSwitched extends UIElement{ - private readonly _upstream: UIElement; - private readonly _swtch: UIEventSource; - - constructor(upstream :UIElement, - swtch: UIEventSource) { - super(swtch); - this._upstream = upstream; - this._swtch = swtch; - } - - InnerRender(): UIElement | string { - if(this._swtch.data){ - return this._upstream.Render(); - } - return undefined; - } - -} \ No newline at end of file diff --git a/UI/Base/FixedUiElement.ts b/UI/Base/FixedUiElement.ts index e27afb3a17..c89927f3f0 100644 --- a/UI/Base/FixedUiElement.ts +++ b/UI/Base/FixedUiElement.ts @@ -1,10 +1,11 @@ import {UIElement} from "../UIElement"; +import BaseUIElement from "../BaseUIElement"; -export class FixedUiElement extends UIElement { +export class FixedUiElement extends BaseUIElement { private _html: string; constructor(html: string) { - super(undefined); + super(); this._html = html ?? ""; } @@ -12,4 +13,10 @@ export class FixedUiElement extends UIElement { return this._html; } + protected InnerConstructElement(): HTMLElement { + const e = document.createElement("span") + e.innerHTML = this._html + return e; + } + } \ No newline at end of file diff --git a/UI/Base/Img.ts b/UI/Base/Img.ts index 372ee7e031..23c89fa33c 100644 --- a/UI/Base/Img.ts +++ b/UI/Base/Img.ts @@ -23,6 +23,9 @@ export default class Img extends BaseUIElement { protected InnerConstructElement(): HTMLElement { const el = document.createElement("img") el.src = this._src; + el.onload = () => { + el.style.opacity = "1" + } return el; } } diff --git a/UI/Base/LazyElement.ts b/UI/Base/LazyElement.ts deleted file mode 100644 index ea7d30c697..0000000000 --- a/UI/Base/LazyElement.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {UIElement} from "../UIElement"; - -export default class LazyElement extends UIElement { - - - public Activate: () => void; - private _content: UIElement = undefined; - private readonly _loadingContent: string; - - constructor(content: (() => UIElement), loadingContent = "Rendering...") { - super(); - this._loadingContent = loadingContent; - this.dumbMode = false; - const self = this; - this.Activate = () => { - if (this._content === undefined) { - self._content = content(); - } - self.Update(); - // @ts-ignore - if (this._content.Activate) { - // THis is ugly - I know - // @ts-ignore - this._content.Activate(); - } - } - } - - InnerRender(): string { - if (this._content === undefined) { - return this._loadingContent; - } - return this._content.Render(); - } - -} \ No newline at end of file diff --git a/UI/Base/Link.ts b/UI/Base/Link.ts index d63a620117..10d9f2508f 100644 --- a/UI/Base/Link.ts +++ b/UI/Base/Link.ts @@ -6,18 +6,18 @@ import {UIEventSource} from "../../Logic/UIEventSource"; export default class Link extends BaseUIElement { private readonly _element: HTMLElement; - constructor(embeddedShow: BaseUIElement | string, target: string | UIEventSource, newTab: boolean = false) { + constructor(embeddedShow: BaseUIElement | string, href: string | UIEventSource, newTab: boolean = false) { super(); const _embeddedShow = Translations.W(embeddedShow); const el = document.createElement("a") - if(typeof target === "string"){ - el.href = target + if(typeof href === "string"){ + el.href = href }else{ - target.addCallbackAndRun(target => { - el.target = target; + href.addCallbackAndRun(href => { + el.href = href; }) } if (newTab) { diff --git a/UI/Base/Ornament.ts b/UI/Base/Ornament.ts index 39f5f1bff5..dd715b4dfa 100644 --- a/UI/Base/Ornament.ts +++ b/UI/Base/Ornament.ts @@ -1,7 +1,4 @@ import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Svg from "../../Svg"; -import State from "../../State"; export default class Ornament extends UIElement { diff --git a/UI/Base/PageSplit.ts b/UI/Base/PageSplit.ts deleted file mode 100644 index 36e46b4f58..0000000000 --- a/UI/Base/PageSplit.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {UIElement} from "../UIElement"; - -export default class PageSplit extends UIElement{ - private _left: UIElement; - private _right: UIElement; - private _leftPercentage: number; - - constructor(left: UIElement, right:UIElement, - leftPercentage: number = 50) { - super(); - this._left = left; - this._right = right; - this._leftPercentage = leftPercentage; - } - - InnerRender(): string { - return `${this._left.Render()}${this._right.Render()}`; - } - -} \ No newline at end of file diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index 42636d4316..ec737381ac 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -4,18 +4,21 @@ import BaseUIElement from "../BaseUIElement"; import Link from "./Link"; import Img from "./Img"; import {UIEventSource} from "../../Logic/UIEventSource"; +import {UIElement} from "../UIElement"; -export class SubtleButton extends Combine { +export class SubtleButton extends UIElement { + + private readonly _element: BaseUIElement constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, linkTo: { url: string | UIEventSource, newTab?: boolean } = undefined) { - super(SubtleButton.generateContent(imageUrl, message, linkTo)); - + super(); + this._element = 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 | BaseUIElement, messageT: string | BaseUIElement, linkTo: { url: string | UIEventSource, newTab?: boolean } = undefined): (BaseUIElement )[] { + private static generateContent(imageUrl: string | BaseUIElement, messageT: string | BaseUIElement, linkTo: { url: string | UIEventSource, newTab?: boolean } = undefined): BaseUIElement { const message = Translations.W(messageT); let img; if ((imageUrl ?? "") === "") { @@ -28,26 +31,27 @@ export class SubtleButton extends Combine { img?.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0") const image = new Combine([img]) .SetClass("flex-shrink-0"); - + if (linkTo == undefined) { - return [ + return new Combine([ image, message, - ]; + ]); } - - - return [ - new Link( - new Combine([ - image, - message?.SetClass("block ml-4 overflow-ellipsis") - ]).SetClass("flex group"), - linkTo.url, - linkTo.newTab ?? false - ) - ]; + + return new Link( + new Combine([ + image, + message?.SetClass("block ml-4 overflow-ellipsis") + ]).SetClass("flex group"), + linkTo.url, + linkTo.newTab ?? false + ) + } + + protected InnerRender(): string | BaseUIElement { + return this._element; } diff --git a/UI/Base/TabbedComponent.ts b/UI/Base/TabbedComponent.ts index 260317eb09..0244b0e7b5 100644 --- a/UI/Base/TabbedComponent.ts +++ b/UI/Base/TabbedComponent.ts @@ -1,41 +1,33 @@ -import {UIElement} from "../UIElement"; import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "./Combine"; +import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "./VariableUIElement"; -export class TabbedComponent extends UIElement { +export class TabbedComponent extends Combine { - 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[] = [] + constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[], openedTab: (UIEventSource | number) = 0) { + const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource(0)) + + const tabs: BaseUIElement[] = [] + const contentElements: BaseUIElement[] = []; for (let i = 0; i < elements.length; i++) { let element = elements[i]; - const header = Translations.W(element.header).onClick(() => self._source.setData(i)) + const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i)) const content = Translations.W(element.content) - this.content.push(content); - if (!this.content[i].IsEmpty()) { - const tab = header.SetClass("block tab-single-header") - tabs.push(tab) - } + content.SetClass("tab-content") + contentElements.push(content); + const tab = header.SetClass("block tab-single-header") + tabs.push(tab) } - this.header = new Combine(tabs).SetClass("block tabs-header-bar") + const header = new Combine(tabs).SetClass("block tabs-header-bar") + const actualContent = new VariableUiElement( + openedTabSrc.map(i => contentElements[i]) + ) + super([header, actualContent]) - - } - - InnerRender(): UIElement { - - const content = this.content[this._source.data]; - return new Combine([ - this.header, - content.SetClass("tab-content"), - ]) } } \ No newline at end of file diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index aecf6695c2..456f9139c1 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -56,16 +56,18 @@ export default abstract class BaseUIElement { } /** * 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) { + for (let c of all) { + c = c.trim(); if (this.clss.has(clss)) { continue; } + if(c === undefined || c === ""){ + continue; + } this.clss.add(c); recordedChange = true; } diff --git a/UI/BigComponents/Attribution.ts b/UI/BigComponents/Attribution.ts index 630ee57477..bad254726a 100644 --- a/UI/BigComponents/Attribution.ts +++ b/UI/BigComponents/Attribution.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import Link from "../Base/Link"; import Svg from "../../Svg"; import Combine from "../Base/Combine"; @@ -8,67 +7,57 @@ import Constants from "../../Models/Constants"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import Loc from "../../Models/Loc"; import * as L from "leaflet" +import {VariableUiElement} from "../Base/VariableUIElement"; /** * The bottom right attribution panel in the leaflet map */ -export default class Attribution extends UIElement { - - private readonly _location: UIEventSource; - private readonly _layoutToUse: UIEventSource; - private readonly _userDetails: UIEventSource; - private readonly _leafletMap: UIEventSource; +export default class Attribution extends Combine { constructor(location: UIEventSource, userDetails: UIEventSource, layoutToUse: UIEventSource, leafletMap: UIEventSource) { - super(location); - this._layoutToUse = layoutToUse; - this.ListenTo(layoutToUse); - this._userDetails = userDetails; - this._leafletMap = leafletMap; - this.ListenTo(userDetails); - this._location = location; - this.SetClass("map-attribution"); - } - - InnerRender(): string { - const location: Loc = this._location?.data; - const userDetails = this._userDetails?.data; - + const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true); - const reportBug = new Link(Svg.bug_img, "https://github.com/pietervdvn/MapComplete/issues", true); + const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true); - const layoutId = this._layoutToUse?.data?.id; + const layoutId = layoutToUse?.data?.id; const osmChaLink = `https://osmcha.org/?filters=%7B%22comment%22%3A%5B%7B%22label%22%3A%22%23${layoutId}%22%2C%22value%22%3A%22%23${layoutId}%22%7D%5D%2C%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22MapComplete%22%2C%22value%22%3A%22MapComplete%22%7D%5D%7D` - const stats = new Link(Svg.statistics_img, osmChaLink, true) - let editHere: (UIElement | string) = ""; - let mapillary: UIElement = undefined; - if (location !== undefined) { - const idLink = `https://www.openstreetmap.org/edit?editor=id#map=${location.zoom}/${location.lat}/${location.lon}` - editHere = new Link(Svg.pencil_img, idLink, true); - - const mapillaryLink: string = `https://www.mapillary.com/app/?focus=map&lat=${location.lat}&lng=${location.lon}&z=${Math.max(location.zoom - 1, 1)}`; - mapillary = new Link(Svg.mapillary_black_img, mapillaryLink, true); - - } + const stats = new Link(Svg.statistics_ui().SetClass("small-image"), osmChaLink, true) - let editWithJosm: (UIElement | string) = "" - if (location !== undefined && - this._leafletMap?.data !== undefined && - userDetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) { - const bounds: any = this._leafletMap.data.getBounds(); - const top = bounds.getNorth(); - const bottom = bounds.getSouth(); - const right = bounds.getEast(); - const left = bounds.getWest(); + const idLink = location.map(location => `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`) + const editHere = new Link(Svg.pencil_ui().SetClass("small-image"), idLink, true) + + const mapillaryLink = location.map(location => `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`) + const mapillary = new Link(Svg.mapillary_black_ui().SetClass("small-image"), mapillaryLink, true); + + + + let editWithJosm = new VariableUiElement( + userDetails.map(userDetails => { + + if (userDetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) { + return undefined; + } + const bounds: any = leafletMap?.data?.getBounds(); + if(bounds === undefined){ + return undefined + } + const top = bounds.getNorth(); + const bottom = bounds.getSouth(); + const right = bounds.getEast(); + const left = bounds.getWest(); + + const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` + return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true); + }, + [location] + ) + ) + super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]); - const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` - editWithJosm = new Link(Svg.josm_logo_img, josmLink, true); - } - return new Combine([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]).Render(); } diff --git a/UI/BigComponents/AttributionPanel.ts b/UI/BigComponents/AttributionPanel.ts index 6b3b830abf..191320f995 100644 --- a/UI/BigComponents/AttributionPanel.ts +++ b/UI/BigComponents/AttributionPanel.ts @@ -10,8 +10,8 @@ import SmallLicense from "../../Models/smallLicense"; import {Utils} from "../../Utils"; import Link from "../Base/Link"; import {VariableUiElement} from "../Base/VariableUIElement"; -import {UIElement} from "../UIElement"; import * as contributors from "../../assets/contributors.json" +import BaseUIElement from "../BaseUIElement"; /** * The attribution panel shown on mobile @@ -26,7 +26,7 @@ export default class AttributionPanel extends Combine { ((layoutToUse.data.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}), layoutToUse.data.credits, "
    ", - new Attribution(undefined, undefined, State.state.layoutToUse, undefined), + new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.leafletMap), "
    ", new VariableUiElement(contributions.map(contributions => { @@ -66,7 +66,7 @@ export default class AttributionPanel extends Combine { this.SetStyle("max-width: calc(100vw - 5em); width: 40em;") } - private static CodeContributors(): UIElement { + private static CodeContributors(): BaseUIElement { const total = contributors.contributors.length; let filtered = contributors.contributors @@ -87,7 +87,7 @@ export default class AttributionPanel extends Combine { }); } - private static IconAttribution(iconPath: string): UIElement { + private static IconAttribution(iconPath: string): BaseUIElement { if (iconPath.startsWith("http")) { iconPath = "." + new URL(iconPath).pathname; } diff --git a/UI/BigComponents/BackgroundSelector.ts b/UI/BigComponents/BackgroundSelector.ts index a55ddcab94..8a92e359d9 100644 --- a/UI/BigComponents/BackgroundSelector.ts +++ b/UI/BigComponents/BackgroundSelector.ts @@ -1,39 +1,35 @@ -import {UIElement} from "../UIElement"; import {DropDown} from "../Input/DropDown"; import Translations from "../i18n/Translations"; import State from "../../State"; -import {UIEventSource} from "../../Logic/UIEventSource"; import BaseLayer from "../../Models/BaseLayer"; -import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; -export default class BackgroundSelector extends UIElement { - - private _dropdown: BaseUIElement; - private readonly _availableLayers: UIEventSource; +export default class BackgroundSelector extends VariableUiElement { constructor() { - super(); - const self = this; - this._availableLayers = State.state.availableBackgroundLayers; - this._availableLayers.addCallbackAndRun(available => self.CreateDropDown(available)); - } + const available = State.state.availableBackgroundLayers.map(available => { + const baseLayers: { value: BaseLayer, shown: string }[] = []; + for (const i in available) { + if(!available.hasOwnProperty(i)){ + continue; + } + const layer: BaseLayer = available[i]; + baseLayers.push({value: layer, shown: layer.name ?? "id:" + layer.id}); + } + return baseLayers + } + ) - private CreateDropDown(available) { - if(available.length === 0){ - return; - } - - const baseLayers: { value: BaseLayer, shown: string }[] = []; - for (const i in available) { - const layer: BaseLayer = available[i]; - baseLayers.push({value: layer, shown: layer.name ?? "id:" + layer.id}); - } + super( + available.map(baseLayers => { + if (baseLayers.length <= 1) { + return undefined; + } + return new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer) + } + ) + ) - this._dropdown = new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer); - } - - InnerRender(): BaseUIElement { - return this._dropdown; } } \ No newline at end of file diff --git a/UI/BigComponents/Basemap.ts b/UI/BigComponents/Basemap.ts index 9384671b9a..5db429e0f1 100644 --- a/UI/BigComponents/Basemap.ts +++ b/UI/BigComponents/Basemap.ts @@ -1,8 +1,8 @@ import * as L from "leaflet" import {UIEventSource} from "../../Logic/UIEventSource"; import Loc from "../../Models/Loc"; -import {UIElement} from "../UIElement"; import BaseLayer from "../../Models/BaseLayer"; +import BaseUIElement from "../BaseUIElement"; export class Basemap { @@ -13,13 +13,12 @@ export class Basemap { location: UIEventSource, currentLayer: UIEventSource, lastClickLocation: UIEventSource<{ lat: number, lon: number }>, - extraAttribution: UIElement) { + extraAttribution: BaseUIElement) { this.map = L.map(leafletElementId, { center: [location.data.lat ?? 0, location.data.lon ?? 0], zoom: location.data.zoom ?? 2, layers: [currentLayer.data.layer], - zoomControl: false - + zoomControl: false, }); L.control.scale( @@ -36,8 +35,10 @@ export class Basemap { [[-100, -200], [100, 200]] ); this.map.attributionControl.setPrefix( - extraAttribution.Render() + " |
    OpenStreetMap"); + " | OpenStreetMap"); + extraAttribution.AttachTo('leaflet-attribution') + const self = this; let previousLayer = currentLayer.data; diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index b521d07090..29b53f56f7 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -16,21 +16,14 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import UserDetails from "../../Logic/Osm/OsmConnection"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; +import BaseUIElement from "../BaseUIElement"; -export default class FullWelcomePaneWithTabs extends UIElement { - private readonly _layoutToUse: UIEventSource; - private readonly _userDetails: UIEventSource; +export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { - private readonly _component: UIElement; constructor(isShown: UIEventSource) { - super(State.state.layoutToUse); - this._layoutToUse = State.state.layoutToUse; - this._userDetails = State.state.osmConnection.userDetails; - const layoutToUse = this._layoutToUse.data; - - - this._component = new ScrollableFullScreen( + const layoutToUse = State.state.layoutToUse.data; + super ( () => layoutToUse.title.Clone(), () => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails), "welcome" ,isShown @@ -43,11 +36,11 @@ export default class FullWelcomePaneWithTabs extends UIElement { if (layoutToUse.id === personal.id) { welcome = new PersonalLayersPanel(); } - const tabs = [ + const tabs : {header: string | BaseUIElement, content: BaseUIElement}[] = [ {header: ``, content: welcome}, { header: Svg.osm_logo_img, - content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline") as UIElement + content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline") }, ] @@ -71,18 +64,13 @@ export default class FullWelcomePaneWithTabs extends UIElement { if (userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) { return "" } - return new Combine([Translations.t.general.aboutMapcomplete, "
    Version " + Constants.vNumber]).SetClass("link-underline").Render(); + return new Combine([Translations.t.general.aboutMapcomplete, "
    Version " + Constants.vNumber]).SetClass("link-underline"); }, [Locale.language])) } ); - return new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab) - .ListenTo(userDetails); + return new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab); } - InnerRender(): UIElement { - return this._component; - - } } \ No newline at end of file diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index d92fb5b0ae..08a8f68a56 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -1,56 +1,43 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import State from "../../State"; import Toggle from "../Input/Toggle"; import Combine from "../Base/Combine"; -import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import BaseUIElement from "../BaseUIElement"; /** * Shows the panel with all layers and a toggle for each of them */ -export default class LayerSelection extends UIElement { +export default class LayerSelection extends Combine { - private _checkboxes: UIElement[]; - private activeLayers: UIEventSource<{ - readonly isDisplayed: UIEventSource, - readonly layerDef: LayerConfig; - }[]>; constructor(activeLayers: UIEventSource<{ readonly isDisplayed: UIEventSource, readonly layerDef: LayerConfig; }[]>) { - super(activeLayers); - if(activeLayers === undefined){ + + if (activeLayers === undefined) { throw "ActiveLayers should be defined..." } - this.activeLayers = activeLayers; - } - InnerRender(): string { + const checkboxes: BaseUIElement[] = []; - this._checkboxes = []; - - for (const layer of this.activeLayers.data) { + for (const layer of activeLayers.data) { const leafletStyle = layer.layerDef.GenerateLeafletStyle( new UIEventSource({id: "node/-1"}), false) - const leafletHtml = leafletStyle.icon.html; - const icon = - new FixedUiElement(leafletHtml.Render()) - .SetClass("single-layer-selection-toggle") - let iconUnselected: UIElement = new FixedUiElement(leafletHtml.Render()) + const icon = new Combine([leafletStyle.icon.html]).SetClass("single-layer-selection-toggle") + let iconUnselected: BaseUIElement = new Combine([leafletStyle.icon.html]) .SetClass("single-layer-selection-toggle") .SetStyle("opacity:0.2;"); const name = Translations.WT(layer.layerDef.name)?.Clone() ?.SetStyle("font-size:large;margin-left: 0.5em;"); - if((name ?? "") === ""){ + if ((name ?? "") === "") { continue } @@ -59,13 +46,12 @@ export default class LayerSelection extends UIElement { return Translations.t.general.layerSelection.zoomInToSeeThisLayer .SetClass("alert") .SetStyle("display: block ruby;width:min-content;") - .Render(); } return "" })) const style = "display:flex;align-items:center;" const styleWhole = "display:flex; flex-wrap: wrap" - this._checkboxes.push(new Toggle( + checkboxes.push(new Toggle( new Combine([new Combine([icon, name]).SetStyle(style), zoomStatus]) .SetStyle(styleWhole), new Combine([new Combine([iconUnselected, "", name, ""]).SetStyle(style), zoomStatus]) @@ -76,9 +62,8 @@ export default class LayerSelection extends UIElement { } - return new Combine(this._checkboxes) - .SetStyle("display:flex;flex-direction:column;") - .Render(); - } + super(checkboxes) + this.SetStyle("display:flex;flex-direction:column;") + } } \ No newline at end of file diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index aee0e00b4d..f3a9c799bf 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -132,8 +132,16 @@ export default class MoreScreen extends Combine { linkSuffix = `#${customThemeDefinition}` } - const linkText = currentLocation.map(currentLocation => - `${linkPrefix}z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}${linkSuffix}`) + const linkText = currentLocation.map(currentLocation => { + const params = [ + ["z", currentLocation?.zoom], + ["lat", currentLocation?.lat], + ["lon",currentLocation?.lon] + ].filter(part => part[1] !== undefined) + .map(part => part[0]+"="+part[1]) + .join("&") + return `${linkPrefix}${params}${linkSuffix}`; + }) diff --git a/UI/BigComponents/PersonalLayersPanel.ts b/UI/BigComponents/PersonalLayersPanel.ts index 6713f96992..d6a27e49d4 100644 --- a/UI/BigComponents/PersonalLayersPanel.ts +++ b/UI/BigComponents/PersonalLayersPanel.ts @@ -7,12 +7,13 @@ import State from "../../State"; import Combine from "../Base/Combine"; import Toggle from "../Input/Toggle"; import {SubtleButton} from "../Base/SubtleButton"; -import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import * as personal from "../../assets/themes/personalLayout/personalLayout.json" import Locale from "../i18n/Locale"; +import BaseUIElement from "../BaseUIElement"; + export default class PersonalLayersPanel extends UIElement { - private checkboxes: UIElement[] = []; + private checkboxes: BaseUIElement[] = []; constructor() { super(State.state.favouriteLayers); @@ -60,9 +61,9 @@ export default class PersonalLayersPanel extends UIElement { if (typeof layer === "string") { continue; } - let icon :UIElement = layer.GenerateLeafletStyle(new UIEventSource({id:"node/-1"}), false).icon.html + let icon :BaseUIElement = layer.GenerateLeafletStyle(new UIEventSource({id:"node/-1"}), false).icon.html ?? Svg.checkmark_svg(); - let iconUnset =new FixedUiElement(icon.Render()); + let iconUnset =new Combine([icon]); icon.SetClass("single-layer-selection-toggle") iconUnset.SetClass("single-layer-selection-toggle") @@ -121,17 +122,17 @@ export default class PersonalLayersPanel extends UIElement { } - InnerRender(): string { + InnerRender(): BaseUIElement { const t = Translations.t.favourite; - const userDetails = State.state.osmConnection.userDetails.data; - if(!userDetails.loggedIn){ - return t.loginNeeded.Render(); - } - - return new Combine([ - t.panelIntro, - ...this.checkboxes - ]).Render(); + return new Toggle( + new Combine([ + t.panelIntro, + ...this.checkboxes + ]), + t.loginNeeded, + State.state.osmConnection.isLoggedIn + + ) } diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index 275828dfe2..825ba2c96e 100644 --- a/UI/BigComponents/SearchAndGo.ts +++ b/UI/BigComponents/SearchAndGo.ts @@ -1,6 +1,5 @@ import Locale from "../i18n/Locale"; import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import {Translation} from "../i18n/Translation"; import {VariableUiElement} from "../Base/VariableUIElement"; import Svg from "../../Svg"; @@ -10,78 +9,75 @@ import {Geocoding} from "../../Logic/Osm/Geocoding"; import Translations from "../i18n/Translations"; import Hash from "../../Logic/Web/Hash"; import Combine from "../Base/Combine"; -import BaseUIElement from "../BaseUIElement"; -export default class SearchAndGo extends UIElement { - - private readonly _placeholder = new UIEventSource(Translations.t.general.search.search) - private readonly _searchField = new TextField({ - placeholder: new VariableUiElement( - this._placeholder.map(uiElement => uiElement.InnerRender(), [Locale.language]) - ), - value: new UIEventSource("") - } - ); - - private readonly _foundEntries = new UIEventSource([]); - private readonly _goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right'); - private readonly _element: Combine; +export default class SearchAndGo extends Combine { constructor() { - super(undefined); - this.ListenTo(this._foundEntries); + const goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right'); - const self = this; - this._searchField.enterPressed.addCallback(() => { - self.RunSearch(); - }); + const placeholder = new UIEventSource(Translations.t.general.search.search) + const searchField = new TextField({ + placeholder: new VariableUiElement( + placeholder.map(uiElement => uiElement, [Locale.language]) + ), + value: new UIEventSource(""), + + inputStyle: " background: transparent;\n" + + " border: none;\n" + + " font-size: large;\n" + + " width: 100%;\n" + + " box-sizing: border-box;\n" + + " color: var(--foreground-color);" + + } + ); + + searchField.SetClass("relative float-left mt-0 ml-2") + searchField.SetStyle("width: calc(100% - 3em)") - this._goButton.onClick(function () { - self.RunSearch(); - }); - this._element = new Combine([this._searchField, this._goButton]) + super([searchField, goButton]) - } + this.SetClass("block h-8") + this.SetStyle("background: var(--background-color); color: var(--foreground-color); pointer-evetns:all;") - InnerRender(): BaseUIElement - { - return this._element - } + // Triggered by 'enter' or onclick + function runSearch() { + const searchString = searchField.GetValue().data; + if (searchString === undefined || searchString === "") { + return; + } + searchField.GetValue().setData(""); + placeholder.setData(Translations.t.general.search.searching); + Geocoding.Search(searchString, (result) => { + + console.log("Search result", result) + if (result.length == 0) { + placeholder.setData(Translations.t.general.search.nothing); + return; + } + + const poi = result[0]; + const bb = poi.boundingbox; + const bounds: [[number, number], [number, number]] = [ + [bb[0], bb[2]], + [bb[1], bb[3]] + ] + State.state.selectedElement.setData(undefined); + Hash.hash.setData(poi.osm_type + "/" + poi.osm_id); + State.state.leafletMap.data.fitBounds(bounds); + placeholder.setData(Translations.t.general.search.search); + }, + () => { + searchField.GetValue().setData(""); + placeholder.setData(Translations.t.general.search.error); + }); - // Triggered by 'enter' or onclick - private RunSearch() { - const searchString = this._searchField.GetValue().data; - if (searchString === undefined || searchString === "") { - return; } - this._searchField.GetValue().setData(""); - this._placeholder.setData(Translations.t.general.search.searching); - const self = this; - Geocoding.Search(searchString, (result) => { - console.log("Search result", result) - if (result.length == 0) { - self._placeholder.setData(Translations.t.general.search.nothing); - return; - } - - const poi = result[0]; - const bb = poi.boundingbox; - const bounds: [[number, number], [number, number]] = [ - [bb[0], bb[2]], - [bb[1], bb[3]] - ] - State.state.selectedElement. setData(undefined); - Hash.hash.setData(poi.osm_type+"/"+poi.osm_id); - State.state.leafletMap.data.fitBounds(bounds); - self._placeholder.setData(Translations.t.general.search.search); - }, - () => { - self._searchField.GetValue().setData(""); - self._placeholder.setData(Translations.t.general.search.error); - }); + searchField.enterPressed.addCallback(runSearch); + goButton.onClick(runSearch); } diff --git a/UI/BigComponents/ShareButton.ts b/UI/BigComponents/ShareButton.ts index 0ad95828ce..474abe7cd8 100644 --- a/UI/BigComponents/ShareButton.ts +++ b/UI/BigComponents/ShareButton.ts @@ -1,10 +1,10 @@ -import {UIElement} from "../UIElement"; +import BaseUIElement from "../BaseUIElement"; -export default class ShareButton extends UIElement{ - private _embedded: UIElement; +export default class ShareButton extends BaseUIElement{ + private _embedded: BaseUIElement; private _shareData: { text: string; title: string; url: string }; - constructor(embedded: UIElement, shareData: { + constructor(embedded: BaseUIElement, shareData: { text: string, title: string, url: string @@ -12,17 +12,17 @@ export default class ShareButton extends UIElement{ super(); this._embedded = embedded; this._shareData = shareData; - } - - InnerRender(): string { - return `` + this.SetClass("share-button") } - protected InnerUpdate(htmlElement: HTMLElement) { - const self= this; - htmlElement.addEventListener('click', () => { + protected InnerConstructElement(): HTMLElement { + const e = document.createElement("button") + e.type = "button" + e.appendChild(this._embedded.ConstructElement()) + + e.addEventListener('click', () => { if (navigator.share) { - navigator.share(self._shareData).then(() => { + navigator.share(this._shareData).then(() => { console.log('Thanks for sharing!'); }) .catch(err => { @@ -32,6 +32,9 @@ export default class ShareButton extends UIElement{ console.log('web share not supported'); } }); + + return e; } + } \ No newline at end of file diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 9f1a109a12..c6248681e3 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -8,20 +8,20 @@ import Svg from "../../Svg"; import {SubtleButton} from "../Base/SubtleButton"; import State from "../../State"; import Combine from "../Base/Combine"; -import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import Constants from "../../Models/Constants"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; import {Tag} from "../../Logic/Tags/Tag"; import {TagUtils} from "../../Logic/Tags/TagUtils"; +import BaseUIElement from "../BaseUIElement"; export default class SimpleAddUI extends UIElement { - private readonly _loginButton: UIElement; + private readonly _loginButton: BaseUIElement; private readonly _confirmPreset: UIEventSource<{ - description: string | UIElement, - name: string | UIElement, - icon: UIElement, + description: string | BaseUIElement, + name: string | BaseUIElement, + icon: BaseUIElement, tags: Tag[], layerToAddTo: { layerDef: LayerConfig, @@ -30,11 +30,11 @@ export default class SimpleAddUI extends UIElement { }> = new UIEventSource(undefined); - private _component: UIElement; + private _component:BaseUIElement; - private readonly openLayerControl: UIElement; - private readonly cancelButton: UIElement; - private readonly goToInboxButton: UIElement = new SubtleButton(Svg.envelope_ui(), + private readonly openLayerControl: BaseUIElement; + private readonly cancelButton: BaseUIElement; + private readonly goToInboxButton: BaseUIElement = new SubtleButton(Svg.envelope_ui(), Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false}); constructor(isShown: UIEventSource) { @@ -75,16 +75,15 @@ export default class SimpleAddUI extends UIElement { State.state.LastClickLocation.addCallback(() => { self._confirmPreset.setData(undefined) }) - - } - - InnerRender(): string { this._component = this.CreateContent(); - return this._component.Render(); + } + + InnerRender(): BaseUIElement { + return this._component; } - private CreatePresetsPanel(): UIElement { + private CreatePresetsPanel(): BaseUIElement { const userDetails = State.state.osmConnection.userDetails; if (userDetails === undefined) { return undefined; @@ -121,21 +120,17 @@ export default class SimpleAddUI extends UIElement { } - private CreateContent(): UIElement { + private CreateContent(): BaseUIElement { const confirmPanel = this.CreateConfirmPanel(); if (confirmPanel !== undefined) { return confirmPanel; } - let intro: UIElement = Translations.t.general.add.intro; + let intro: BaseUIElement = Translations.t.general.add.intro; - let testMode: UIElement = undefined; + let testMode: BaseUIElement = undefined; if (State.state.osmConnection?.userDetails?.data?.dryRun) { - testMode = new Combine([ - "", - "Test mode - changes won't be saved", - "" - ]); + testMode = Translations.t.general.testing.Clone().SetClass("alert") } let presets = this.CreatePresetsPanel(); @@ -144,7 +139,7 @@ export default class SimpleAddUI extends UIElement { } - private CreateConfirmPanel(): UIElement { + private CreateConfirmPanel(): BaseUIElement { const preset = this._confirmPreset.data; if (preset === undefined) { return undefined; @@ -195,7 +190,7 @@ export default class SimpleAddUI extends UIElement { const presets = layer.layerDef.presets; for (const preset of presets) { const tags = TagUtils.KVtoProperties(preset.tags ?? []); - let icon: UIElement = new FixedUiElement(layer.layerDef.GenerateLeafletStyle(new UIEventSource(tags), false).icon.html.Render()).SetClass("simple-add-ui-icon"); + let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource(tags), false).icon.html.SetClass("simple-add-ui-icon"); const csCount = State.state.osmConnection.userDetails.data.csCount; let tagInfo = undefined; diff --git a/UI/BigComponents/ThemeIntroductionPanel.ts b/UI/BigComponents/ThemeIntroductionPanel.ts index 61f7c23473..f3a9073201 100644 --- a/UI/BigComponents/ThemeIntroductionPanel.ts +++ b/UI/BigComponents/ThemeIntroductionPanel.ts @@ -1,62 +1,51 @@ -import Locale from "../i18n/Locale"; -import {UIElement} from "../UIElement"; import State from "../../State"; import Combine from "../Base/Combine"; import LanguagePicker from "../LanguagePicker"; 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; - - private readonly loginStatus: UIElement; - private _layout: UIEventSource; +import Toggle from "../Input/Toggle"; +export default class ThemeIntroductionPanel extends VariableUiElement { constructor() { - super(State.state.osmConnection.userDetails); - this.ListenTo(Locale.language); - this.languagePicker = LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language, Translations.t.general.pickLanguage); - this._layout = State.state.layoutToUse; - this.ListenTo(State.state.layoutToUse); + + const languagePicker = + new VariableUiElement( + State.state.layoutToUse.map(layout => LanguagePicker.CreateLanguagePicker(layout.language, Translations.t.general.pickLanguage)) + ) + ; const plzLogIn = Translations.t.general.loginWithOpenStreetMap .onClick(() => { State.state.osmConnection.AttemptLogin() }); - - - const welcomeBack = Translations.t.general.welcomeBack; - - this.loginStatus = new VariableUiElement( - State.state.osmConnection.userDetails.map( - userdetails => { - if (State.state.featureSwitchUserbadge.data) { - return ""; - } - return (userdetails.loggedIn ? welcomeBack : plzLogIn).Render(); - } - ) - ) - this.SetClass("link-underline") - } - InnerRender(): BaseUIElement { - const layout : LayoutConfig = this._layout.data; - return new Combine([ + + const welcomeBack = Translations.t.general.welcomeBack; + + const loginStatus = + new Toggle( + new Toggle( + welcomeBack, + plzLogIn, + State.state.osmConnection.isLoggedIn + ), + undefined, + State.state.featureSwitchUserbadge + ) + + + super(State.state.layoutToUse.map (layout => new Combine([ layout.description, "

    ", - this.loginStatus, + loginStatus, layout.descriptionTail, "
    ", - this.languagePicker, + languagePicker, ...layout.CustomCodeSnippets() - ]) + ]))) + + this.SetClass("link-underline") } - - } diff --git a/UI/BigComponents/UserBadge.ts b/UI/BigComponents/UserBadge.ts index 6248140e79..26214661af 100644 --- a/UI/BigComponents/UserBadge.ts +++ b/UI/BigComponents/UserBadge.ts @@ -1,10 +1,7 @@ /** * Handles and updates the user badge */ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; -import UserDetails from "../../Logic/Osm/OsmConnection"; import Svg from "../../Svg"; import State from "../../State"; import Combine from "../Base/Combine"; @@ -12,133 +9,127 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import LanguagePicker from "../LanguagePicker"; import Translations from "../i18n/Translations"; import Link from "../Base/Link"; +import Toggle from "../Input/Toggle"; +import Img from "../Base/Img"; -export default class UserBadge extends UIElement { - private _userDetails: UIEventSource; - private _logout: UIElement; - private _homeButton: UIElement; - private _languagePicker: UIElement; - - private _loginButton: UIElement; +export default class UserBadge extends Toggle { constructor() { - super(State.state.osmConnection.userDetails); - this._userDetails = State.state.osmConnection.userDetails; - this._languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language) ?? new FixedUiElement("")) - .SetStyle("width:min-content;"); - this._loginButton = Translations.t.general.loginWithOpenStreetMap + + const userDetails = State.state.osmConnection.userDetails; + + const loginButton = Translations.t.general.loginWithOpenStreetMap .Clone() .SetClass("userbadge-login pt-3 w-full") .onClick(() => State.state.osmConnection.AttemptLogin()); - this._logout = + + + const logout = Svg.logout_svg() .onClick(() => { State.state.osmConnection.LogOut(); }); - this._userDetails.addCallback(function () { - const profilePic = document.getElementById("profile-pic"); - if (profilePic) { - profilePic.onload = function () { - profilePic.style.opacity = "1" - }; - } - }); + const userBadge = userDetails.map(user => { + { + const homeButton = new VariableUiElement( + userDetails.map((userinfo) => { + if (userinfo.home) { + return Svg.home_ui(); + } + return " "; + }) + ).onClick(() => { + const home = State.state.osmConnection.userDetails.data?.home; + if (home === undefined) { + return; + } + State.state.leafletMap.data.setView([home.lat, home.lon], 16); + }); - this._homeButton = new VariableUiElement( - this._userDetails.map((userinfo) => { - if (userinfo.home) { - return Svg.home_ui().Render(); + const linkStyle = "flex items-baseline" + const languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language) ?? new FixedUiElement("")) + .SetStyle("width:min-content;"); + + let messageSpan = + new Link( + new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle), + 'https://www.openstreetmap.org/messages/inbox', + true + ) + + + const csCount = + new Link( + new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle), + `https://www.openstreetmap.org/user/${user.name}/history`, + true); + + + if (user.unreadMessages > 0) { + messageSpan = new Link( + new Combine([Svg.envelope, "" + user.unreadMessages]), + 'https://www.openstreetmap.org/messages/inbox', + true + ).SetClass("alert") } - return " "; - }) - ).onClick(() => { - const home = State.state.osmConnection.userDetails.data?.home; - if (home === undefined) { - return; + + let dryrun = new FixedUiElement(""); + if (user.dryRun) { + dryrun = new FixedUiElement("TESTING").SetClass("alert"); + } + + const settings = + new Link(Svg.gear_svg(), + `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`, + true) + + + const userIcon = new Link( + new Img(user.img) + .SetClass("rounded-full opacity-0 m-0 p-0 duration-500 w-16 h16 float-left") + , + `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}`, + true + ); + + + const userName = new Link( + new FixedUiElement(user.name), + `https://www.openstreetmap.org/user/${user.name}`, + true); + + + const userStats = new Combine([ + homeButton, + settings, + messageSpan, + csCount, + languagePicker, + logout + ]) + .SetClass("userstats") + + const usertext = new Combine([ + userName, + dryrun, + userStats + ]).SetClass("usertext") + + return new Combine([ + userIcon, + usertext, + ]).SetClass("h-16") } - State.state.leafletMap.data.setView([home.lat, home.lon], 16); }); - } - - InnerRender(): UIElement { - const user = this._userDetails.data; - if (!user.loggedIn) { - return this._loginButton; - } - - const linkStyle = "flex items-baseline" - - let messageSpan: UIElement = - new Link( - new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle), - 'https://www.openstreetmap.org/messages/inbox', - true - ) - - - const csCount = - new Link( - new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle), - `https://www.openstreetmap.org/user/${user.name}/history`, - true); - - - if (user.unreadMessages > 0) { - messageSpan = new Link( - new Combine([Svg.envelope, "" + user.unreadMessages]), - 'https://www.openstreetmap.org/messages/inbox', - true - ).SetClass("alert") - } - - let dryrun: UIElement = new FixedUiElement(""); - if (user.dryRun) { - dryrun = new FixedUiElement("TESTING").SetClass("alert"); - } - - const settings = - new Link(Svg.gear_svg(), - `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`, - true) - - - const userIcon = new Link( - new FixedUiElement(`profile-pic`), - `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}`, - true - ); - - - const userName = new Link( - new FixedUiElement(user.name), - `https://www.openstreetmap.org/user/${user.name}`, - true); - - - const userStats = new Combine([ - this._homeButton, - settings, - messageSpan, - csCount, - this._languagePicker, - this._logout - ]) - .SetClass("userstats") - - const usertext = new Combine([ - userName, - dryrun, - userStats - ]).SetClass("usertext") - - return new Combine([ - userIcon, - usertext, - ]) + super( + new VariableUiElement(userBadge), + loginButton, + State.state.osmConnection.isLoggedIn + ) } diff --git a/UI/CenterMessageBox.ts b/UI/CenterMessageBox.ts index e1f15e4984..d136ea1d16 100644 --- a/UI/CenterMessageBox.ts +++ b/UI/CenterMessageBox.ts @@ -1,62 +1,46 @@ -import {UIElement} from "./UIElement"; import Translations from "./i18n/Translations"; import State from "../State"; +import {VariableUiElement} from "./Base/VariableUIElement"; -export default class CenterMessageBox extends UIElement { +export default class CenterMessageBox extends VariableUiElement { constructor() { - super(State.state.centerMessage); + const state = State.state; + const updater = State.state.layerUpdater; + const t = Translations.t.centerMessage; + const message = updater.runningQuery.map( + isRunning => { + if (isRunning) { + return {el: t.loadingData}; + } + if (!updater.sufficientlyZoomed.data) { + return {el: t.zoomIn} + } + if (updater.timeout.data > 0) { + return {el: t.retrying.Subs({count: "" + updater.timeout.data})} + } + return {el: t.ready, isDone: true} - this.ListenTo(State.state.locationControl); - this.ListenTo(State.state.layerUpdater.timeout); - this.ListenTo(State.state.layerUpdater.runningQuery); - this.ListenTo(State.state.layerUpdater.sufficientlyZoomed); - } + }, + [updater.timeout, updater.sufficientlyZoomed, state.locationControl] + ) + + super(message.map(toShow => toShow.el)) + + this.SetClass("block " + + "rounded-3xl bg-white text-xl font-bold text-center pointer-events-none p-4") + this.SetStyle("transition: opacity 750ms linear") - 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}), - done: false - }; - } + message.addCallbackAndRun(toShow => { + const isDone = toShow.isDone ?? false; + if (isDone) { + this.SetStyle("transition: opacity 750ms linear; opacity: 0") + } else { + this.SetStyle("transition: opacity 750ms linear; opacity: 0.75") - if (lu.runningQuery.data) { - return {innerHtml: Translations.t.centerMessage.loadingData, done: false}; + } + }) - } - if (!lu.sufficientlyZoomed.data) { - return {innerHtml: Translations.t.centerMessage.zoomIn, done: false}; - } else { - return {innerHtml: Translations.t.centerMessage.ready, done: true}; - } - } - - 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"; - pstyle.pointerEvents = "all"; - return; - } - pstyle.pointerEvents = "none"; - - if (CenterMessageBox.prep().done) { - pstyle.opacity = "0"; - } else { - pstyle.opacity = "0.5"; - } } } diff --git a/UI/Image/Attribution.ts b/UI/Image/Attribution.ts index 8f0745697e..78b6171d17 100644 --- a/UI/Image/Attribution.ts +++ b/UI/Image/Attribution.ts @@ -1,10 +1,11 @@ import {UIElement} from "../UIElement"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; +import BaseUIElement from "../BaseUIElement"; export default class Attribution extends Combine { - constructor(author: UIElement | string, license: UIElement | string, icon: UIElement) { + constructor(author: BaseUIElement | string, license: BaseUIElement | string, icon: BaseUIElement) { super([ icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em"), new Combine([ diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 9182406d21..1cdae491a3 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -8,7 +8,7 @@ import {Tag} from "../../Logic/Tags/Tag"; import BaseUIElement from "../BaseUIElement"; import LicensePicker from "../BigComponents/LicensePicker"; import Toggle from "../Input/Toggle"; -import FileSelectorButton from "../Base/FileSelectorButton"; +import FileSelectorButton from "../Input/FileSelectorButton"; import ImgurUploader from "../../Logic/Web/ImgurUploader"; import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index aa378beb82..025d8e550f 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -1,7 +1,6 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import {Utils} from "../../Utils"; -import {UIElement} from "../UIElement"; import BaseUIElement from "../BaseUIElement"; /** @@ -10,20 +9,57 @@ import BaseUIElement from "../BaseUIElement"; export default class CheckBoxes extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); - private readonly value: UIEventSource; - private readonly _elements: BaseUIElement[] - private readonly _element : HTMLElement - - constructor(elements: BaseUIElement[]) { + + private static _nextId = 0; +private readonly value: UIEventSource + constructor(elements: BaseUIElement[], value =new UIEventSource([])) { super(); - this._elements = Utils.NoNull(elements); - this.value = new UIEventSource([]) + this.value = value; + elements = Utils.NoNull(elements); - - const el = document.createElement() - this._element = el; + const el = document.createElement("form") + + for (let i = 0; i < elements.length; i++) { + + let inputI = elements[i]; + const input = document.createElement("input") + const id = CheckBoxes._nextId + CheckBoxes._nextId ++; + input.id = "checkbox"+id + + input.type = "checkbox" + const label = document.createElement("label") + label.htmlFor = input.id + label.appendChild(inputI.ConstructElement()) + + value.addCallbackAndRun(selectedValues =>{ + if(selectedValues === undefined){ + return; + } + if(selectedValues.indexOf(i) >= 0){ + input.checked = true; + } + }) + + input.onchange = () => { + const index = value.data.indexOf(i); + if(input.checked && index < 0){ + value.data.push(i); + value.ping(); + }else if(index >= 0){ + value.data.splice(index,1); + value.ping(); + } + } + + + el.appendChild(input) + el.appendChild(document.createElement("br")) + } + + } @@ -42,50 +78,6 @@ private readonly _element : HTMLElement } - private IdFor(i) { - return 'checkmark-' + this.id + '-' + i; - } - - InnerRender(): string { - let body = ""; - for (let i = 0; i < this._elements.length; i++) { - let el = this._elements[i]; - const htmlElement = - `
    `; - body += htmlElement; - - } - - return `
    ${body}
    `; - } - - protected InnerUpdate(htmlElement: HTMLElement) { - const self = this; - - for (let i = 0; i < this._elements.length; i++) { - const el = document.getElementById(this.IdFor(i)); - - if(this.value.data.indexOf(i) >= 0){ - // @ts-ignore - el.checked = true; - } - - el.onchange = () => { - const index = self.value.data.indexOf(i); - // @ts-ignore - if(el.checked && index < 0){ - self.value.data.push(i); - self.value.ping(); - }else if(index >= 0){ - self.value.data.splice(index,1); - self.value.ping(); - } - } - - } - - - } } \ No newline at end of file diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts index 4e376e7eff..4a187ed8b1 100644 --- a/UI/Input/DropDown.ts +++ b/UI/Input/DropDown.ts @@ -21,7 +21,7 @@ export class DropDown extends InputElement { } ) { super(); -this._values = values; + this._values = values; if (values.length <= 1) { return; } @@ -36,9 +36,11 @@ this._values = values; { const labelEl = Translations.W(label).ConstructElement() - const labelHtml = document.createElement("label") - labelHtml.appendChild(labelEl) - labelHtml.htmlFor = el.id; + if (labelEl !== undefined) { + const labelHtml = document.createElement("label") + labelHtml.appendChild(labelEl) + labelHtml.htmlFor = el.id; + } } @@ -56,14 +58,14 @@ this._values = values; 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; } - } + } }) } diff --git a/UI/Base/FileSelectorButton.ts b/UI/Input/FileSelectorButton.ts similarity index 100% rename from UI/Base/FileSelectorButton.ts rename to UI/Input/FileSelectorButton.ts diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index a3ce32a656..2071d74807 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -4,10 +4,9 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; export class TextField extends InputElement { - private readonly value: UIEventSource; public readonly enterPressed = new UIEventSource(undefined); public readonly IsSelected: UIEventSource = new UIEventSource(false); - + private readonly value: UIEventSource; private _element: HTMLElement; private readonly _isValid: (s: string, country?: () => string) => boolean; @@ -19,6 +18,7 @@ export class TextField extends InputElement { inputMode?: string, label?: BaseUIElement, textAreaRows?: number, + inputStyle?: string, isValid?: ((s: string, country?: () => string) => boolean) }) { super(); @@ -26,39 +26,40 @@ export class TextField extends InputElement { options = options ?? {}; this.value = options?.value ?? new UIEventSource(undefined); this._isValid = options.isValid ?? (_ => true); - + this.onClick(() => { self.IsSelected.setData(true) }); + const placeholder = Translations.W(options.placeholder ?? "").ConstructElement().innerText.replace("'", "'"); - const placeholder = Translations.W(options. placeholder ?? "").ConstructElement().innerText.replace("'", "'"); - this.SetClass("form-text-field") - let inputEl : HTMLElement - if(options.htmlType === "area"){ + 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{ + } else { const el = document.createElement("input") - el.type = options.htmlType - el.inputMode = options.inputMode + el.type = options.htmlType ?? "text" + el.inputMode = options.inputMode el.placeholder = placeholder + el.style.cssText = options.inputStyle inputEl = el } const form = document.createElement("form") + form.appendChild(inputEl) form.onsubmit = () => false; - - if(options.label){ - form.appendChild(options.label.ConstructElement()) - } - + + if (options.label) { + form.appendChild(options.label.ConstructElement()) + } + this._element = form; const field = inputEl; @@ -70,9 +71,9 @@ export class TextField extends InputElement { } // @ts-ignore field.value = value; - if(self.IsValid(value)){ + if (self.IsValid(value)) { self.RemoveClass("invalid") - }else{ + } else { self.SetClass("invalid") } @@ -82,7 +83,7 @@ export class TextField extends InputElement { // How much characters are on the right, not including spaces? // @ts-ignore - const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length; + const endDistance = field.value.substring(field.selectionEnd).replace(/ /g, '').length; // @ts-ignore let val: string = field.value; if (!self.IsValid(val)) { @@ -97,18 +98,18 @@ export class TextField extends InputElement { // @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 - ){ - newCursorPos --; + ) { + newCursorPos--; } // @ts-ignore TextField.SetCursorPosition(newCursorPos); }; - + field.addEventListener("focusin", () => self.IsSelected.setData(true)); field.addEventListener("focusout", () => self.IsSelected.setData(false)); @@ -118,22 +119,13 @@ export class TextField extends InputElement { // @ts-ignore self.enterPressed.setData(field.value); } - }); - - - - } + }); - GetValue(): UIEventSource { - return this.value; - } - protected InnerConstructElement(): HTMLElement { - return this._element; } private static SetCursorPosition(textfield: HTMLElement, i: number) { - if(textfield === undefined || textfield === null){ + if (textfield === undefined || textfield === null) { return; } if (i === -1) { @@ -146,6 +138,10 @@ export class TextField extends InputElement { } + GetValue(): UIEventSource { + return this.value; + } + IsValid(t: string): boolean { if (t === undefined || t === null) { return false @@ -153,4 +149,8 @@ export class TextField extends InputElement { return this._isValid(t, undefined); } + protected InnerConstructElement(): HTMLElement { + return this._element; + } + } \ No newline at end of file diff --git a/UI/Input/Toggle.ts b/UI/Input/Toggle.ts index f4fbdb20e3..f3a4eb74f7 100644 --- a/UI/Input/Toggle.ts +++ b/UI/Input/Toggle.ts @@ -10,12 +10,13 @@ export default class Toggle extends VariableUiElement{ public readonly isEnabled: UIEventSource; - constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, data: UIEventSource = new UIEventSource(false)) { + constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: UIEventSource = new UIEventSource(false)) { super( - data.map(isEnabled => isEnabled ? showEnabled : showDisabled) + isEnabled.map(isEnabled => isEnabled ? showEnabled : showDisabled) ); + this.isEnabled = isEnabled this.onClick(() => { - data.setData(!data.data); + isEnabled.setData(!isEnabled.data); }) } diff --git a/UI/LanguagePicker.ts b/UI/LanguagePicker.ts index e7c80e419f..686a62b12e 100644 --- a/UI/LanguagePicker.ts +++ b/UI/LanguagePicker.ts @@ -1,13 +1,13 @@ -import {UIElement} from "./UIElement"; import {DropDown} from "./Input/DropDown"; import Locale from "./i18n/Locale"; +import BaseUIElement from "./BaseUIElement"; export default class LanguagePicker { public static CreateLanguagePicker( languages : string[] , - label: string | UIElement = "") { + label: string | BaseUIElement = "") { if (languages.length <= 1) { return undefined; diff --git a/UI/MapControlButton.ts b/UI/MapControlButton.ts index 877cd8553c..6276885b1c 100644 --- a/UI/MapControlButton.ts +++ b/UI/MapControlButton.ts @@ -1,14 +1,16 @@ import {UIElement} from "./UIElement"; +import BaseUIElement from "./BaseUIElement"; +import Combine from "./Base/Combine"; /** * A button floating above the map, in a uniform style */ export default class MapControlButton extends UIElement { - private _contents: UIElement; + private _contents: BaseUIElement; - constructor(contents: UIElement) { + constructor(contents: BaseUIElement) { super(); - this._contents = contents; + this._contents = new Combine([contents]); this.SetClass("relative block rounded-full w-10 h-10 p-1 pointer-events-auto z-above-map subtle-background") this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);"); } diff --git a/UI/OpeningHours/OhVisualization.ts b/UI/OpeningHours/OhVisualization.ts index 6ac2b33248..e060d0e6ae 100644 --- a/UI/OpeningHours/OhVisualization.ts +++ b/UI/OpeningHours/OhVisualization.ts @@ -7,6 +7,7 @@ import {OH} from "./OpeningHours"; import Translations from "../i18n/Translations"; import Constants from "../../Models/Constants"; import opening_hours from "opening_hours"; +import BaseUIElement from "../BaseUIElement"; export default class OpeningHoursVisualization extends UIElement { private static readonly weekdays = [ @@ -87,7 +88,7 @@ export default class OpeningHoursVisualization extends UIElement { return new Date(d.setDate(diff)); } - InnerRender(): string | UIElement { + InnerRender(): string | BaseUIElement { const today = new Date(); @@ -168,13 +169,13 @@ export default class OpeningHoursVisualization extends UIElement { latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60) - const rows: UIElement[] = []; + const rows: BaseUIElement[] = []; const availableArea = latestclose - earliestOpen; // @ts-ignore const now = (100 * (((new Date() - today) / 1000) - earliestOpen)) / availableArea; - let header: UIElement[] = []; + let header: BaseUIElement[] = []; if (now >= 0 && now <= 100) { header.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now")) @@ -218,7 +219,7 @@ export default class OpeningHoursVisualization extends UIElement { dateToShow = "" + day.getDate() + "/" + (day.getMonth() + 1); } - let innerContent: (string | UIElement)[] = []; + let innerContent: (string | BaseUIElement)[] = []; // Add the lines for (const changeMoment of changeHours) { @@ -265,7 +266,7 @@ export default class OpeningHoursVisualization extends UIElement { return new Combine([ "", - ...rows.map(el => "" + el.Render() + ""), + ...rows.map(el => new Combine(["" ,el , ""])), "
    " ]).SetClass("ohviz-container"); } diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index d01382c70f..89d7aa6db8 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -1,35 +1,47 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import {UIElement} from "../UIElement"; import Translations from "../i18n/Translations"; -import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import BaseUIElement from "../BaseUIElement"; +import Toggle from "../Input/Toggle"; export class SaveButton extends UIElement { - private readonly _value: UIEventSource; - private readonly _friendlyLogin: UIElement; - private readonly _userDetails: UIEventSource; + + private readonly _element: BaseUIElement; constructor(value: UIEventSource, osmConnection: OsmConnection) { super(value); - this._userDetails = osmConnection?.userDetails; - if(value === undefined){ + if (value === undefined) { throw "No event source for savebutton, something is wrong" } - this._value = value; - this._friendlyLogin = Translations.t.general.loginToStart.Clone() + + const pleaseLogin = Translations.t.general.loginToStart.Clone() .SetClass("login-button-friendly") .onClick(() => osmConnection?.AttemptLogin()) + + + const isSaveable = value.map(v => v !== false && (v ?? "") !== "") + + + const saveEnabled = Translations.t.general.save.Clone().SetClass(`btn`); + const saveDisabled = Translations.t.general.save.Clone().SetClass(`btn btn-disabled`); + const save = new Toggle( + saveEnabled, + saveDisabled, + isSaveable + ) + this._element = new Toggle( + save + , pleaseLogin, + osmConnection?.userDetails?.map(userDetails => userDetails.loggedIn) ?? new UIEventSource(false) + ) + } - InnerRender() { - if(this._userDetails != undefined && !this._userDetails.data.loggedIn){ - 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}`); + InnerRender(): BaseUIElement { + return this._element + } } \ No newline at end of file diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index dc880d3049..1b716208c7 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -19,8 +19,9 @@ export default class TagRenderingAnswer extends UIElement { private _contentStyle: string; constructor(tags: UIEventSource, configuration: TagRenderingConfig, contentClasses: string = "", contentStyle: string = "") { - super(tags); + super(); this._tags = tags; + this.ListenTo(tags) this._configuration = configuration; this._contentClass = contentClasses; this._contentStyle = contentStyle; diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 44b1cb3c19..cdf197c77d 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -22,6 +22,7 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {Tag} from "../../Logic/Tags/Tag"; import {And} from "../../Logic/Tags/And"; import {TagUtils} from "../../Logic/Tags/TagUtils"; +import BaseUIElement from "../BaseUIElement"; /** * Shows the question element. @@ -35,7 +36,7 @@ export default class TagRenderingQuestion extends UIElement { private _inputElement: InputElement; private _cancelButton: UIElement; - private _appliedTags: UIElement; + private _appliedTags: BaseUIElement; private _question: UIElement; constructor(tags: UIEventSource, @@ -82,16 +83,19 @@ export default class TagRenderingQuestion extends UIElement { return ""; } if (tags === undefined) { - return Translations.t.general.noTagsSelected.SetClass("subtle").Render(); + return Translations.t.general.noTagsSelected.SetClass("subtle"); } if (csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { const tagsStr = tags.asHumanString(false, true, self._tags.data); - return new FixedUiElement(tagsStr).SetClass("subtle").Render(); + return new FixedUiElement(tagsStr).SetClass("subtle"); } return tags.asHumanString(true, true, self._tags.data); } ) ).SetClass("block") + + + } InnerRender() { diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 2eb7437f4c..bb15b6dca7 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -128,7 +128,7 @@ export default class ShowDataLayer { const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)); return L.marker(latLng, { icon: L.divIcon({ - html: style.icon.html.Render(), + html: style.icon.html.ConstructElement(), className: style.icon.className, iconAnchor: style.icon.iconAnchor, iconUrl: style.icon.iconUrl, diff --git a/UI/UIElement.ts b/UI/UIElement.ts index e817a85188..e228d70d3c 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -21,6 +21,7 @@ export abstract class UIElement extends BaseUIElement{ if (source === undefined) { return this; } + console.trace("Got a listenTo in ", this.constructor.name) const self = this; source.addCallback(() => { self.lastInnerRender = undefined; @@ -39,7 +40,7 @@ export abstract class UIElement extends BaseUIElement{ } Render(): string { - return "Don't use Render!" + return this.InnerRenderAsString() } @@ -52,11 +53,6 @@ export abstract class UIElement extends BaseUIElement{ return rendered } - public IsEmpty(): boolean { - return this.InnerRender() === undefined || this.InnerRender() === ""; - } - - /** * Should be overridden for specific HTML functionality diff --git a/css/userbadge.css b/css/userbadge.css index b515cf5011..72ae5e59ce 100644 --- a/css/userbadge.css +++ b/css/userbadge.css @@ -55,17 +55,6 @@ display: block; } -#profile-pic { - float: left; - width: 4em; - height: 4em; - padding: 0; - margin: 0; - opacity: 0; - transition: opacity 500ms linear; - border-radius: 999em; -} - .usertext { display: block; width: max-content; diff --git a/index.css b/index.css index cdc5508741..c6b0dabbb7 100644 --- a/index.css +++ b/index.css @@ -324,52 +324,6 @@ li::marker { } -.activate-osm-authentication { - cursor: pointer; - color: blue; - text-decoration: underline; -} - - -#searchbox { - display: inline-block; - text-align: left; - background-color: var(--background-color); - color: var(--foreground-color); - - transition: all 500ms linear; - pointer-events: all; - margin: 0 0 0.5em; - width: 100%; -} - -.search { - position: relative; - float: left; - height: 2em; - margin-right: 0.5em; -} - -#searchbox { - width: 100% -} - -#searchbox .form-text-field { - position: relative; - float: left; - margin-top: 0.2em; - margin-left: 1em; - width: calc(100% - 4em) -} - -#searchbox input[type="text"] { - background: transparent; - border: none; - font-size: large; - width: 100%; - box-sizing: border-box; - color: var(--foreground-color); -} /**************************************/ @@ -409,25 +363,9 @@ li::marker { } -#centermessage { - z-index: 4000; - pointer-events: none; - transition: opacity 500ms linear; -} - - - /***************** Info box (box containing features and questions ******************/ -.map-attribution img { - width: 1em; - height: 1em; - fill: black; - border-radius: 0; - display: inline; -} - .leaflet-popup-content { width: 45em !important; } @@ -461,3 +399,7 @@ li::marker { max-width: 1em; } +.small-image { + height: 1em; + max-width: 1em; +} diff --git a/index.html b/index.html index bb3bbb633a..9330ff1489 100644 --- a/index.html +++ b/index.html @@ -74,7 +74,7 @@
    + class="clutter absolute h-24 left-24 right-24 top-56" style="z-index: 4000"> Loading MapComplete, hang on...
    diff --git a/index.ts b/index.ts index 47cf7ebb08..e787b76491 100644 --- a/index.ts +++ b/index.ts @@ -98,7 +98,6 @@ new Combine(["Initializing...
    ", })]) .AttachTo("centermessage"); // Add an initialization and reset button if something goes wrong - document.getElementById("decoration-desktop").remove(); diff --git a/langs/en.json b/langs/en.json index 47d533a446..287f85dc3a 100644 --- a/langs/en.json +++ b/langs/en.json @@ -31,6 +31,7 @@ "loginWithOpenStreetMap": "Login with OpenStreetMap", "welcomeBack": "You are logged in, welcome back!", "loginToStart": "Login to answer this question", + "testing":"Testing - changes won't be saved", "search": { "search": "Search a location", "searching": "Searchingโ€ฆ", diff --git a/test.ts b/test.ts index 7aae201d57..b243a09fbc 100644 --- a/test.ts +++ b/test.ts @@ -1,15 +1,6 @@ -import {Translation} from "./UI/i18n/Translation"; -import Locale from "./UI/i18n/Locale"; -import Combine from "./UI/Base/Combine"; +import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler"; +import LayoutConfig from "./Customizations/JSON/LayoutConfig"; +import {UIEventSource} from "./Logic/UIEventSource"; -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 +new GeoLocationHandler(new UIEventSource<{latlng: any; accuracy: number}>(undefined), undefined, new UIEventSource(undefined)).AttachTo("maindiv") \ No newline at end of file diff --git a/vendor/Leaflet.AccuratePosition.js b/vendor/Leaflet.AccuratePosition.js deleted file mode 100644 index ae14051b8c..0000000000 --- a/vendor/Leaflet.AccuratePosition.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Leaflet.AccuratePosition aims to provide an accurate device location when simply calling map.locate() doesnโ€™t. - * https://github.com/m165437/Leaflet.AccuratePosition - * - * Greg Wilson's getAccurateCurrentPosition() forked to be a Leaflet plugin - * https://github.com/gwilson/getAccurateCurrentPosition - * - * Copyright (C) 2013 Greg Wilson, 2014 Michael Schmidt-Voigt - */ - -L.Map.include({ - _defaultAccuratePositionOptions: { - maxWait: 10000, - desiredAccuracy: 20 - }, - - findAccuratePosition: function (options) { - - if (!navigator.geolocation) { - this._handleAccuratePositionError({ - code: 0, - message: 'Geolocation not supported.' - }); - return this; - } - - this._accuratePositionEventCount = 0; - this._accuratePositionOptions = L.extend(this._defaultAccuratePositionOptions, options); - this._accuratePositionOptions.enableHighAccuracy = true; - this._accuratePositionOptions.maximumAge = 0; - - if (!this._accuratePositionOptions.timeout) - this._accuratePositionOptions.timeout = this._accuratePositionOptions.maxWait; - - var onResponse = L.bind(this._checkAccuratePosition, this), - onError = L.bind(this._handleAccuratePositionError, this), - onTimeout = L.bind(this._handleAccuratePositionTimeout, this); - - this._accuratePositionWatchId = navigator.geolocation.watchPosition( - onResponse, - onError, - this._accuratePositionOptions); - - this._accuratePositionTimerId = setTimeout( - onTimeout, - this._accuratePositionOptions.maxWait); - }, - - _handleAccuratePositionTimeout: function() { - navigator.geolocation.clearWatch(this._accuratePositionWatchId); - - if (typeof this._lastCheckedAccuratePosition !== 'undefined') { - this._handleAccuratePositionResponse(this._lastCheckedAccuratePosition); - } else { - this._handleAccuratePositionError({ - code: 3, - message: 'Timeout expired' - }); - } - - return this; - }, - - _cleanUpAccuratePositioning: function () { - clearTimeout(this._accuratePositionTimerId); - navigator.geolocation.clearWatch(this._accuratePositionWatchId); - }, - - _checkAccuratePosition: function (pos) { - var accuracyReached = pos.coords.accuracy <= this._accuratePositionOptions.desiredAccuracy; - - this._lastCheckedAccuratePosition = pos; - this._accuratePositionEventCount = this._accuratePositionEventCount + 1; - - if (accuracyReached && (this._accuratePositionEventCount > 1)) { - this._cleanUpAccuratePositioning(); - this._handleAccuratePositionResponse(pos); - } else { - this._handleAccuratePositionProgress(pos); - } - }, - - _prepareAccuratePositionData: function (pos) { - var lat = pos.coords.latitude, - lng = pos.coords.longitude, - latlng = new L.LatLng(lat, lng), - - latAccuracy = 180 * pos.coords.accuracy / 40075017, - lngAccuracy = latAccuracy / Math.cos(Math.PI / 180 * lat), - - bounds = L.latLngBounds( - [lat - latAccuracy, lng - lngAccuracy], - [lat + latAccuracy, lng + lngAccuracy]); - - var data = { - latlng: latlng, - bounds: bounds, - timestamp: pos.timestamp - }; - - for (var i in pos.coords) { - if (typeof pos.coords[i] === 'number') { - data[i] = pos.coords[i]; - } - } - - return data; - }, - - _handleAccuratePositionProgress: function (pos) { - var data = this._prepareAccuratePositionData(pos); - this.fire('accuratepositionprogress', data); - }, - - _handleAccuratePositionResponse: function (pos) { - var data = this._prepareAccuratePositionData(pos); - this.fire('accuratepositionfound', data); - }, - - _handleAccuratePositionError: function (error) { - var c = error.code, - message = error.message || - (c === 1 ? 'permission denied' : - (c === 2 ? 'position unavailable' : 'timeout')); - - this._cleanUpAccuratePositioning(); - - this.fire('accuratepositionerror', { - code: c, - message: 'Geolocation error: ' + message + '.' - }); - } -}); -console.log("Find accurate position script loaded"); \ No newline at end of file From 49c821268e7c1ddd4965de1cfdc0d1ae82e09653 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 13 Jun 2021 15:04:55 +0200 Subject: [PATCH 05/30] Further fixing the refactoring --- UI/BigComponents/FullWelcomePaneWithTabs.ts | 27 ++++--- UI/BigComponents/MoreScreen.ts | 14 ++-- UI/BigComponents/ShareScreen.ts | 83 +++++++++------------ UI/BigComponents/ThemeIntroductionPanel.ts | 9 ++- UI/Input/DropDown.ts | 4 + UI/LanguagePicker.ts | 2 +- css/userbadge.css | 10 --- test.ts | 3 +- 8 files changed, 66 insertions(+), 86 deletions(-) diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 29b53f56f7..754d82b84f 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import State from "../../State"; import ThemeIntroductionPanel from "./ThemeIntroductionPanel"; import * as personal from "../../assets/themes/personalLayout/personalLayout.json"; @@ -7,16 +6,15 @@ import Svg from "../../Svg"; import Translations from "../i18n/Translations"; import ShareScreen from "./ShareScreen"; import MoreScreen from "./MoreScreen"; -import {VariableUiElement} from "../Base/VariableUIElement"; import Constants from "../../Models/Constants"; import Combine from "../Base/Combine"; -import Locale from "../i18n/Locale"; import {TabbedComponent} from "../Base/TabbedComponent"; import {UIEventSource} from "../../Logic/UIEventSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import UserDetails from "../../Logic/Osm/OsmConnection"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import BaseUIElement from "../BaseUIElement"; +import Toggle from "../Input/Toggle"; export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { @@ -32,11 +30,11 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource) { - let welcome: UIElement = new ThemeIntroductionPanel(); + let welcome: BaseUIElement = new ThemeIntroductionPanel(); if (layoutToUse.id === personal.id) { welcome = new PersonalLayersPanel(); } - const tabs : {header: string | BaseUIElement, content: BaseUIElement}[] = [ + const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [ {header: ``, content: welcome}, { header: Svg.osm_logo_img, @@ -58,19 +56,20 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { } - tabs.push({ + const tabsWithAboutMc = [...tabs] + tabsWithAboutMc.push({ header: Svg.help, - content: new VariableUiElement(userDetails.map(userdetails => { - if (userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) { - return "" - } - return new Combine([Translations.t.general.aboutMapcomplete, "
    Version " + Constants.vNumber]).SetClass("link-underline"); - }, [Locale.language])) + content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "
    Version " + Constants.vNumber]) + .SetClass("link-underline") } ); - return new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab); + return new Toggle( + new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab), + new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), + userDetails.map(userdetails => + userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) + ) } - } \ No newline at end of file diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index f3a9c799bf..a5de7896cf 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -21,7 +21,7 @@ export default class MoreScreen extends Combine { private static Init(onMainScreen: boolean, state: State): BaseUIElement [] { const tr = Translations.t.general.morescreen; - let intro: BaseUIElement = tr.intro; + let intro: BaseUIElement = tr.intro.Clone(); let themeButtonStyle = "" let themeListStyle = "" if (onMainScreen) { @@ -38,7 +38,7 @@ export default class MoreScreen extends Combine { intro, MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle), MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle), - tr.streetcomplete.SetClass("block text-base mx-10 my-3 mb-10") + tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10") ]; } @@ -77,12 +77,12 @@ export default class MoreScreen extends Combine { return new VariableUiElement( state.osmConnection.userDetails.map(userDetails => { if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) { - return new SubtleButton(null, tr.requestATheme, { + return new SubtleButton(null, tr.requestATheme.Clone(), { url: "https://github.com/pietervdvn/MapComplete/issues", newTab: true }); } - return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme, { + return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), { url: "./customGenerator.html", newTab: false }); @@ -146,14 +146,14 @@ export default class MoreScreen extends Combine { - let description = Translations.W(layout.shortDescription); + let description = Translations.WT(layout.shortDescription).Clone(); return new SubtleButton(layout.icon, new Combine([ `
    `, - Translations.W(layout.title), + Translations.WT(layout.title).Clone(), `
    `, `
    `, - description ?? "", + description.Clone().SetClass("subtle") ?? "", `
    `, ]), {url: linkText, newTab: false}); } diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index 13695b5a16..70a9ab3c54 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import {Translation} from "../i18n/Translation"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; @@ -15,23 +14,16 @@ import Constants from "../../Models/Constants"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; import BaseUIElement from "../BaseUIElement"; -export default class ShareScreen extends UIElement { - private readonly _options: BaseUIElement; - private readonly _iframeCode: BaseUIElement; - public iframe: UIEventSource; - private readonly _link: BaseUIElement; - private readonly _linkStatus: UIEventSource; - private readonly _editLayout: BaseUIElement; +export default class ShareScreen extends Combine { constructor(layout: LayoutConfig = undefined, layoutDefinition: string = undefined) { - super(undefined) layout = layout ?? State.state?.layoutToUse?.data; layoutDefinition = layoutDefinition ?? State.state?.layoutDefinition; const tr = Translations.t.general.sharescreen; const optionCheckboxes: BaseUIElement[] = [] const optionParts: (UIEventSource)[] = []; - this.SetClass("link-underline") + function check() { return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;"); } @@ -41,8 +33,8 @@ export default class ShareScreen extends UIElement { } const includeLocation = new Toggle( - new Combine([check(), tr.fsIncludeCurrentLocation]), - new Combine([nocheck(), tr.fsIncludeCurrentLocation]), + new Combine([check(), tr.fsIncludeCurrentLocation.Clone()]), + new Combine([nocheck(), tr.fsIncludeCurrentLocation.Clone()]), new UIEventSource(true) ) optionCheckboxes.push(includeLocation); @@ -91,8 +83,8 @@ export default class ShareScreen extends UIElement { const includeLayerChoices = new Toggle( - new Combine([check(), tr.fsIncludeCurrentLayers]), - new Combine([nocheck(), tr.fsIncludeCurrentLayers]), + new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]), + new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]), new UIEventSource(true) ) optionCheckboxes.push(includeLayerChoices); @@ -121,8 +113,8 @@ export default class ShareScreen extends UIElement { for (const swtch of switches) { const checkbox = new Toggle( - new Combine([check(), Translations.W(swtch.human)]), - new Combine([nocheck(), Translations.W(swtch.human)]), + new Combine([check(), Translations.W(swtch.human.Clone())]), + new Combine([nocheck(), Translations.W(swtch.human.Clone())]), new UIEventSource(!swtch.reverse) ); optionCheckboxes.push(checkbox); @@ -144,7 +136,7 @@ export default class ShareScreen extends UIElement { } - this._options = new Combine(optionCheckboxes).SetClass("flex flex-col") + const options = new Combine(optionCheckboxes).SetClass("flex flex-col") const url = (currentLocation ?? new UIEventSource(undefined)).map(() => { const host = window.location.host; @@ -174,12 +166,12 @@ export default class ShareScreen extends UIElement { }, optionParts); - this.iframe = url.map(url => `<iframe src="${url}" width="100%" height="100%" title="${layout?.title?.txt ?? "MapComplete"} with MapComplete"></iframe>`); + const iframe = url.map(url => `<iframe src="${url}" width="100%" height="100%" title="${layout?.title?.txt ?? "MapComplete"} with MapComplete"></iframe>`); - this._iframeCode = new VariableUiElement( + const iframeCode = new VariableUiElement( url.map((url) => { return ` - <iframe src="${url}" width="100%" height="100%" title="${layout.title?.txt ?? "MapComplete"} with MapComplete"></iframe> + <iframe src="${url}" width="100%" height="100%" style="min-width: 25Opx; min-height: 250ox" title="${layout.title?.txt ?? "MapComplete"} with MapComplete"></iframe> ` }) ); @@ -187,9 +179,9 @@ export default class ShareScreen extends UIElement { - this._editLayout = new FixedUiElement(""); + let editLayout : BaseUIElement= new FixedUiElement(""); if ((layoutDefinition !== undefined && State.state?.osmConnection !== undefined)) { - this._editLayout = + editLayout = new VariableUiElement( State.state.osmConnection.userDetails.map( userDetails => { @@ -198,8 +190,8 @@ export default class ShareScreen extends UIElement { } return new SubtleButton(Svg.pencil_ui(), - new Combine([tr.editThisTheme.SetClass("bold"), "
    ", - tr.editThemeDescription]), + new Combine([tr.editThisTheme.Clone().SetClass("bold"), "
    ", + tr.editThemeDescription.Clone()]), {url: `./customGenerator.html#${State.state.layoutDefinition}`, newTab: true}); } @@ -207,13 +199,9 @@ export default class ShareScreen extends UIElement { } - this._linkStatus = new UIEventSource(""); - this.ListenTo(this._linkStatus); - const self = this; - this._link = new VariableUiElement( - url.map((url) => { - return `` - }) + const linkStatus = new UIEventSource(""); + const link = new VariableUiElement( + url.map((url) => ``) ).onClick(async () => { const shareData = { @@ -231,17 +219,17 @@ export default class ShareScreen extends UIElement { copyText.setSelectionRange(0, 99999); /*For mobile devices*/ document.execCommand("copy"); - const copied = tr.copiedToClipboard; + const copied = tr.copiedToClipboard.Clone(); copied.SetClass("thanks") - self._linkStatus.setData(copied); + linkStatus.setData(copied); } try { navigator.share(shareData) .then(() => { - const thx = tr.thanksForSharing; + const thx = tr.thanksForSharing.Clone(); thx.SetClass("thanks"); - this._linkStatus.setData(thx); + linkStatus.setData(thx); }, rejected) .catch(rejected) } catch (err) { @@ -250,22 +238,19 @@ export default class ShareScreen extends UIElement { }); - } - InnerRender(): BaseUIElement { + super ([ + editLayout, + tr.intro.Clone(), + link, + new VariableUiElement(linkStatus), + tr.addToHomeScreen.Clone(), + tr.embedIntro.Clone(), + options, + iframeCode, + ]) + this.SetClass("flex flex-col link-underline") - const tr = Translations.t.general.sharescreen; - - return new Combine([ - this._editLayout, - tr.intro, - this._link, - Translations.W(this._linkStatus.data), - tr.addToHomeScreen, - tr.embedIntro, - this._options, - this._iframeCode, - ]).SetClass("flex flex-col") } } \ No newline at end of file diff --git a/UI/BigComponents/ThemeIntroductionPanel.ts b/UI/BigComponents/ThemeIntroductionPanel.ts index f3a9073201..fc1750f96c 100644 --- a/UI/BigComponents/ThemeIntroductionPanel.ts +++ b/UI/BigComponents/ThemeIntroductionPanel.ts @@ -11,18 +11,19 @@ export default class ThemeIntroductionPanel extends VariableUiElement { const languagePicker = new VariableUiElement( - State.state.layoutToUse.map(layout => LanguagePicker.CreateLanguagePicker(layout.language, Translations.t.general.pickLanguage)) + State.state.layoutToUse.map(layout => LanguagePicker.CreateLanguagePicker(layout.language, Translations.t.general.pickLanguage.Clone())) ) ; const plzLogIn = Translations.t.general.loginWithOpenStreetMap + .Clone() .onClick(() => { State.state.osmConnection.AttemptLogin() }); - const welcomeBack = Translations.t.general.welcomeBack; + const welcomeBack = Translations.t.general.welcomeBack.Clone(); const loginStatus = new Toggle( @@ -37,10 +38,10 @@ export default class ThemeIntroductionPanel extends VariableUiElement { super(State.state.layoutToUse.map (layout => new Combine([ - layout.description, + layout.description.Clone(), "

    ", loginStatus, - layout.descriptionTail, + layout.descriptionTail.Clone(), "
    ", languagePicker, ...layout.CustomCodeSnippets() diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts index 4a187ed8b1..dffecae384 100644 --- a/UI/Input/DropDown.ts +++ b/UI/Input/DropDown.ts @@ -40,6 +40,7 @@ export class DropDown extends InputElement { const labelHtml = document.createElement("label") labelHtml.appendChild(labelEl) labelHtml.htmlFor = el.id; + el.appendChild(labelHtml) } } @@ -52,7 +53,10 @@ export class DropDown extends InputElement { const option = document.createElement("option") option.value = "" + i option.appendChild(Translations.W(values[i].shown).ConstructElement()) + select.appendChild(option) } + el.appendChild(select) + select.onchange = (() => { var index = select.selectedIndex; diff --git a/UI/LanguagePicker.ts b/UI/LanguagePicker.ts index 686a62b12e..5616707dd4 100644 --- a/UI/LanguagePicker.ts +++ b/UI/LanguagePicker.ts @@ -3,7 +3,7 @@ import Locale from "./i18n/Locale"; import BaseUIElement from "./BaseUIElement"; export default class LanguagePicker { - + public static CreateLanguagePicker( languages : string[] , diff --git a/css/userbadge.css b/css/userbadge.css index 72ae5e59ce..2fe2efec16 100644 --- a/css/userbadge.css +++ b/css/userbadge.css @@ -12,16 +12,6 @@ overflow-x: hidden; } -#userbadge a { - text-decoration: none; - color: var(--foreground-color); -} - - -#userbadge form { - width: unset !important; -} - .userstats { display: flex; align-items: center; diff --git a/test.ts b/test.ts index b243a09fbc..2cf2c82ffb 100644 --- a/test.ts +++ b/test.ts @@ -1,6 +1,7 @@ import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler"; import LayoutConfig from "./Customizations/JSON/LayoutConfig"; import {UIEventSource} from "./Logic/UIEventSource"; +import LanguagePicker from "./UI/LanguagePicker"; -new GeoLocationHandler(new UIEventSource<{latlng: any; accuracy: number}>(undefined), undefined, new UIEventSource(undefined)).AttachTo("maindiv") \ No newline at end of file +LanguagePicker.CreateLanguagePicker(["nl","en"]).AttachTo("maindiv") \ No newline at end of file From 9cc721abad38681ed1ab5d8f682e7efabb9a8f82 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 14 Jun 2021 02:39:23 +0200 Subject: [PATCH 06/30] More refactoring and fixes --- Customizations/JSON/LayerConfig.ts | 2 +- Customizations/JSON/TagRenderingConfig.ts | 42 +++ InitUiElements.ts | 10 +- Logic/Actors/OverpassFeatureSource.ts | 1 - Logic/Actors/StrayClickHandler.ts | 4 +- Logic/Osm/OsmConnection.ts | 26 +- Models/Constants.ts | 1 - UI/Base/FixedUiElement.ts | 1 - UI/Base/List.ts | 34 ++ UI/Base/SubtleButton.ts | 4 +- UI/Base/VariableUIElement.ts | 13 +- UI/BaseUIElement.ts | 3 + UI/BigComponents/FullWelcomePaneWithTabs.ts | 2 +- UI/BigComponents/LayerSelection.ts | 2 +- UI/BigComponents/PersonalLayersPanel.ts | 2 +- UI/BigComponents/ShareScreen.ts | 8 +- UI/BigComponents/SimpleAddUI.ts | 362 ++++++++++---------- UI/Image/DeleteImage.ts | 2 +- UI/Input/FixedInputElement.ts | 38 +- UI/Input/RadioButton.ts | 135 +++----- UI/Input/Toggle.ts | 8 +- UI/Popup/EditableTagRendering.ts | 94 +++-- UI/Popup/FeatureInfoBox.ts | 11 +- UI/Popup/SaveButton.ts | 15 +- UI/Popup/TagRenderingAnswer.ts | 105 ++---- UI/Popup/TagRenderingQuestion.ts | 12 +- UI/Reviews/ReviewForm.ts | 4 +- UI/ShowDataLayer.ts | 11 +- UI/SubstitutedTranslation.ts | 84 ++--- UI/UIElement.ts | 2 +- UI/i18n/Translation.ts | 12 +- Utils.ts | 14 + css/tagrendering.css | 53 --- index.css | 26 -- langs/en.json | 3 +- test/Tag.spec.ts | 4 +- test/TagQuestion.spec.ts | 1 - 37 files changed, 519 insertions(+), 632 deletions(-) create mode 100644 UI/Base/List.ts diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index be2adc52df..fa615d7a63 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -325,7 +325,7 @@ export default class LayerConfig { function render(tr: TagRenderingConfig, deflt?: string) { const str = (tr?.GetRenderValue(tags.data)?.txt ?? deflt); - return SubstitutedTranslation.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); + return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); } const iconSize = render(this.iconSize, "40,40,center").split(","); diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index 157bc2d1bf..80e51c603b 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -7,6 +7,8 @@ import {Utils} from "../../Utils"; import {TagUtils} from "../../Logic/Tags/TagUtils"; import {And} from "../../Logic/Tags/And"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; +import {UIElement} from "../../UI/UIElement"; +import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation"; /*** * The parsed version of TagRenderingConfigJSON @@ -240,6 +242,46 @@ export default class TagRenderingConfig { return this.question === null && this.condition === null; } + /** + * Gets all the render values. Will return multiple render values if 'multianswer' is enabled. + * The result will equal [GetRenderValue] if not 'multiAnswer' + * @param tags + * @constructor + */ + public GetRenderValues(tags: any): Translation[]{ + if(!this.multiAnswer){ + return [this.GetRenderValue(tags)] + } + + // A flag to check that the freeform key isn't matched multiple times + // If it is undefined, it is "used" already, or at least we don't have to check for it anymore + let freeformKeyUsed = this.freeform?.key === undefined; + // We run over all the mappings first, to check if the mapping matches + const applicableMappings: Translation[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => { + if (mapping.if === undefined) { + return mapping.then; + } + if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) { + if(!freeformKeyUsed){ + if(mapping.if.usedKeys().indexOf(this.freeform.key) >= 0){ + // This mapping matches the freeform key - we mark the freeform key to be ignored! + freeformKeyUsed = true; + } + } + return mapping.then; + } + return undefined; + })) + + + + if (!freeformKeyUsed + && tags[this.freeform.key] !== undefined) { + applicableMappings.push(this.render) + } + return applicableMappings + } + /** * Gets the correct rendering value (or undefined if not known) * @constructor diff --git a/InitUiElements.ts b/InitUiElements.ts index 8fd6f88be9..7858dba2a8 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -269,11 +269,10 @@ export class InitUiElements { // ?-Button on Desktop, opens panel with close-X. const help = new MapControlButton(Svg.help_svg()); + help.onClick(() => isOpened.setData(true)) new Toggle( fullOptions - .SetClass("welcomeMessage") - .onClick(() => {/*Catch the click*/ - }), + .SetClass("welcomeMessage"), help , isOpened ).AttachTo("messagesbox"); @@ -308,7 +307,8 @@ export class InitUiElements { copyrightNotice, new MapControlButton(Svg.osm_copyright_svg()), copyrightNotice.isShown - ).SetClass("p-0.5") + ).ToggleOnClick() + .SetClass("p-0.5") const layerControlPanel = new LayerControlPanel( State.state.layerControlIsOpened) @@ -317,7 +317,7 @@ export class InitUiElements { layerControlPanel, new MapControlButton(Svg.layers_svg()), State.state.layerControlIsOpened - ) + ).ToggleOnClick() const layerControl = new Toggle( layerControlButton, diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/Actors/OverpassFeatureSource.ts index b9b7866d99..d76ef12064 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/Actors/OverpassFeatureSource.ts @@ -53,7 +53,6 @@ export default class OverpassFeatureSource implements FeatureSource { return false; } let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); - console.debug("overpass source: minzoom is ", minzoom) return location.zoom >= minzoom; }, [layoutToUse] ); diff --git a/Logic/Actors/StrayClickHandler.ts b/Logic/Actors/StrayClickHandler.ts index 064da814c5..b4d6300700 100644 --- a/Logic/Actors/StrayClickHandler.ts +++ b/Logic/Actors/StrayClickHandler.ts @@ -47,13 +47,13 @@ export default class StrayClickHandler { popupAnchor: [0, -45] }) }); - const popup = L.popup().setContent(uiToShow.Render()); + const popup = L.popup().setContent("
    "); self._lastMarker.addTo(leafletMap.data); self._lastMarker.bindPopup(popup); self._lastMarker.on("click", () => { + uiToShow.AttachTo("strayclick") uiToShow.Activate(); - uiToShow.Update(); }); }); diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index f140f2fd2a..004cefa2a2 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -65,7 +65,14 @@ export class OsmConnection { this.userDetails = new UIEventSource(new UserDetails(), "userDetails"); this.userDetails.data.dryRun = dryRun; - this.isLoggedIn = this.userDetails.map(user => user.loggedIn) + const self =this; + this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { + if(self.userDetails.data.loggedIn == false){ + // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do + // This means someone attempted to toggle this; so we attempt to login! + self.AttemptLogin() + } + }); this._dryRun = dryRun; this.updateAuthObject(); @@ -217,14 +224,15 @@ export class OsmConnection { }); } - private CheckForMessagesContinuously() { - const self = this; - window.setTimeout(() => { - if (self.userDetails.data.loggedIn) { - console.log("Checking for messages") - this.AttemptLogin(); - } - }, 5 * 60 * 1000); + private CheckForMessagesContinuously(){ + const self =this; + UIEventSource.Chronic(5 * 60 * 1000).addCallback(_ => { + if (self.isLoggedIn .data) { + console.log("Checking for messages") + self.AttemptLogin(); + } + }); + } diff --git a/Models/Constants.ts b/Models/Constants.ts index a350603c75..e7ec48ba98 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -6,7 +6,6 @@ export default class Constants { // The user journey states thresholds when a new feature gets unlocked public static userJourney = { - addNewPointsUnlock: 0, moreScreenUnlock: 1, personalLayoutUnlock: 15, historyLinkVisible: 20, diff --git a/UI/Base/FixedUiElement.ts b/UI/Base/FixedUiElement.ts index c89927f3f0..a65b80e6f1 100644 --- a/UI/Base/FixedUiElement.ts +++ b/UI/Base/FixedUiElement.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import BaseUIElement from "../BaseUIElement"; export class FixedUiElement extends BaseUIElement { diff --git a/UI/Base/List.ts b/UI/Base/List.ts new file mode 100644 index 0000000000..d7b45a399a --- /dev/null +++ b/UI/Base/List.ts @@ -0,0 +1,34 @@ +import {Utils} from "../../Utils"; +import BaseUIElement from "../BaseUIElement"; +import Translations from "../i18n/Translations"; + +export default class List extends BaseUIElement { + private readonly uiElements: BaseUIElement[]; + private readonly _ordered: boolean; + + constructor(uiElements: (string | BaseUIElement)[], ordered = false) { + super(); + this._ordered = ordered; + this.uiElements = Utils.NoNull(uiElements) + .map(Translations.W); + } + + protected InnerConstructElement(): HTMLElement { + const el = document.createElement(this._ordered ? "ol" : "ul") + + for (const subEl of this.uiElements) { + if(subEl === undefined || subEl === null){ + continue; + } + const subHtml = subEl.ConstructElement() + if(subHtml !== undefined){ + const item = document.createElement("li") + item.appendChild(subHtml) + el.appendChild(item) + } + } + + return el; + } + +} \ No newline at end of file diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index ec737381ac..c87e121583 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -35,8 +35,8 @@ export class SubtleButton extends UIElement { if (linkTo == undefined) { return new Combine([ image, - message, - ]); + message?.SetClass("blcok ml-4 overflow-ellipsis"), + ]).SetClass("flex group"); } diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index ca38f64d91..8d82858733 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -24,10 +24,17 @@ export class VariableUiElement extends BaseUIElement { el.innerHTML = contents } else if (contents instanceof Array) { for (const content of contents) { - el.appendChild(content.ConstructElement()) + const c = content.ConstructElement(); + if (c !== undefined && c !== null) { + el.appendChild(c) + } + + } + } else { + const c = contents.ConstructElement(); + if (c !== undefined && c !== null) { + el.appendChild(c) } - }else{ - el.appendChild(contents.ConstructElement()) } }) } diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 456f9139c1..840814530e 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -104,6 +104,9 @@ export default abstract class BaseUIElement { return this._constructedHtmlElement } + if(this.InnerConstructElement === undefined){ + throw "ERROR! This is not a correct baseUIElement: "+this.constructor.name + } const el = this.InnerConstructElement(); diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 754d82b84f..63f513488e 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -65,8 +65,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { ); return new Toggle( - new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab), new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), + new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab), userDetails.map(userdetails => userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) ) diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index 08a8f68a56..7810c7b10f 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -56,7 +56,7 @@ export default class LayerSelection extends Combine { .SetStyle(styleWhole), new Combine([new Combine([iconUnselected, "", name, ""]).SetStyle(style), zoomStatus]) .SetStyle(styleWhole), - layer.isDisplayed) + layer.isDisplayed).ToggleOnClick() .SetStyle("margin:0.3em;") ); } diff --git a/UI/BigComponents/PersonalLayersPanel.ts b/UI/BigComponents/PersonalLayersPanel.ts index d6a27e49d4..2bf2f800e1 100644 --- a/UI/BigComponents/PersonalLayersPanel.ts +++ b/UI/BigComponents/PersonalLayersPanel.ts @@ -91,7 +91,7 @@ export default class PersonalLayersPanel extends UIElement { "" ])), controls[layer.id] ?? (favs.indexOf(layer.id) >= 0) - ); + ).ToggleOnClick(); cb.SetClass("custom-layer-checkbox"); controls[layer.id] = cb.isEnabled; diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index 70a9ab3c54..7f32802f53 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -36,7 +36,7 @@ export default class ShareScreen extends Combine { new Combine([check(), tr.fsIncludeCurrentLocation.Clone()]), new Combine([nocheck(), tr.fsIncludeCurrentLocation.Clone()]), new UIEventSource(true) - ) + ).ToggleOnClick() optionCheckboxes.push(includeLocation); const currentLocation = State.state?.locationControl; @@ -71,7 +71,7 @@ export default class ShareScreen extends Combine { new Combine([check(), currentBackground]), new Combine([nocheck(), currentBackground]), new UIEventSource(true) - ) + ).ToggleOnClick() optionCheckboxes.push(includeCurrentBackground); optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => { if (includeBG) { @@ -86,7 +86,7 @@ export default class ShareScreen extends Combine { new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]), new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]), new UIEventSource(true) - ) + ).ToggleOnClick() optionCheckboxes.push(includeLayerChoices); optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => { @@ -116,7 +116,7 @@ export default class ShareScreen extends Combine { new Combine([check(), Translations.W(swtch.human.Clone())]), new Combine([nocheck(), Translations.W(swtch.human.Clone())]), new UIEventSource(!swtch.reverse) - ); + ).ToggleOnClick(); optionCheckboxes.push(checkbox); optionParts.push(checkbox.isEnabled.map((isEn) => { if (isEn) { diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index c6248681e3..78730096f5 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -1,9 +1,7 @@ /** * Asks to add a feature at the last clicked location, at least if zoom is sufficient */ -import Locale from "../i18n/Locale"; import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import Svg from "../../Svg"; import {SubtleButton} from "../Base/SubtleButton"; import State from "../../State"; @@ -14,118 +12,163 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig"; import {Tag} from "../../Logic/Tags/Tag"; import {TagUtils} from "../../Logic/Tags/TagUtils"; import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Toggle from "../Input/Toggle"; +import UserDetails from "../../Logic/Osm/OsmConnection"; +import {Translation} from "../i18n/Translation"; -export default class SimpleAddUI extends UIElement { - private readonly _loginButton: BaseUIElement; +/* +* The SimpleAddUI is a single panel, which can have multiple states: +* - A list of presets which can be added by the user +* - A 'confirm-selection' button (or alternatively: please enable the layer) +* - A 'something is wrong - please soom in further' +* - A 'read your unread messages before adding a point' + */ - private readonly _confirmPreset: UIEventSource<{ - description: string | BaseUIElement, - name: string | BaseUIElement, - icon: BaseUIElement, - tags: Tag[], - layerToAddTo: { - layerDef: LayerConfig, - isDisplayed: UIEventSource - } - }> - = new UIEventSource(undefined); +interface PresetInfo { + description: string | Translation, + name: string | BaseUIElement, + icon: BaseUIElement, + tags: Tag[], + layerToAddTo: { + layerDef: LayerConfig, + isDisplayed: UIEventSource + } +} - private _component:BaseUIElement; - - private readonly openLayerControl: BaseUIElement; - private readonly cancelButton: BaseUIElement; - private readonly goToInboxButton: BaseUIElement = new SubtleButton(Svg.envelope_ui(), - Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false}); +export default class SimpleAddUI extends Toggle { constructor(isShown: UIEventSource) { - super(State.state.locationControl.map(loc => loc.zoom)); - const self = this; - this.ListenTo(Locale.language); - this.ListenTo(State.state.osmConnection.userDetails); - this.ListenTo(State.state.layerUpdater.runningQuery); - this.ListenTo(this._confirmPreset); - this.ListenTo(State.state.locationControl); - State.state.filteredLayers.data?.map(layer => { - self.ListenTo(layer.isDisplayed) - }) - this._loginButton = Translations.t.general.add.pleaseLogin.Clone().onClick(() => State.state.osmConnection.AttemptLogin()); + + const loginButton = Translations.t.general.add.pleaseLogin.Clone().onClick(State.state.osmConnection.AttemptLogin); + const readYourMessages = new Combine([ + Translations.t.general.readYourMessages.Clone().SetClass("alert"), + new SubtleButton(Svg.envelope_ui(), + Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false}) + ]); + + + + const selectedPreset = new UIEventSource(undefined); + isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened + + function createNewPoint(tags: any[]){ + const loc = State.state.LastClickLocation.data; + let feature = State.state.changes.createElement(tags, loc.lat, loc.lon); + State.state.selectedElement.setData(feature); + } + + const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) + + const addUi = new VariableUiElement( + selectedPreset.map(preset => { + if (preset === undefined) { + return presetsOverview + } + return SimpleAddUI.CreateConfirmButton(preset, + tags => { + createNewPoint(tags) + selectedPreset.setData(undefined) + }, () => { + selectedPreset.setData(undefined) + }) + } + )) + + + super( + new Toggle( + new Toggle( + new Toggle( + Translations.t.general.add.stillLoading.Clone().SetClass("alert"), + addUi, + State.state.layerUpdater.runningQuery + ), + Translations.t.general.add.zoomInFurther.Clone().SetClass("alert") , + State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) + ), + readYourMessages, + State.state.osmConnection.userDetails.map((userdetails: UserDetails) => + userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock || + userdetails.unreadMessages == 0) + ), + loginButton, + State.state.osmConnection.isLoggedIn + ) + this.SetStyle("font-size:large"); - this.cancelButton = new SubtleButton(Svg.close_ui(), - Translations.t.general.cancel - ).onClick(() => { - self._confirmPreset.setData(undefined); - }) + } - this.openLayerControl = new SubtleButton(Svg.layers_ui(), - Translations.t.general.add.openLayerControl - ).onClick(() => { - State.state.layerControlIsOpened.setData(true); - }) + + + private static CreateConfirmButton(preset: PresetInfo, + confirm: (tags: any[]) => void, + cancel: () => void): BaseUIElement { + + + const confirmButton = new SubtleButton(preset.icon, + new Combine([ + Translations.t.general.add.addNew.Subs({category: preset.name}), + Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert") + ]).SetClass("flex flex-col") + ).SetClass("font-bold break-words") + .onClick(() => confirm(preset.tags)); + + + const openLayerControl = + new SubtleButton( + Svg.layers_ui(), + new Combine([ + Translations.t.general.add.layerNotEnabled + .Subs({layer: preset.layerToAddTo.layerDef.name}) + .SetClass("alert"), + Translations.t.general.add.openLayerControl + ]) + ) + + .onClick(() => State.state.layerControlIsOpened.setData(true)) - // IS shown is the state of the dialog - we reset the choice if the dialog dissappears - isShown.addCallback(isShown => - { - if(!isShown){ - self._confirmPreset.setData(undefined) - } - }) - // If the click location changes, we reset the dialog as well - State.state.LastClickLocation.addCallback(() => { - self._confirmPreset.setData(undefined) - }) - this._component = this.CreateContent(); - } + const openLayerOrConfirm = new Toggle( + confirmButton, + openLayerControl, + preset.layerToAddTo.isDisplayed + ) + const tagInfo = SimpleAddUI.CreateTagInfoFor(preset); - InnerRender(): BaseUIElement { - return this._component; + const cancelButton = new SubtleButton(Svg.close_ui(), + Translations.t.general.cancel + ).onClick(cancel ) + + return new Combine([ + Translations.t.general.add.confirmIntro.Subs({title: preset.name}), + State.state.osmConnection.userDetails.data.dryRun ? + Translations.t.general.testing.Clone().SetClass("alert") : undefined , + openLayerOrConfirm, + cancelButton, + preset.description, + tagInfo + + ]).SetClass("flex flex-col") } - private CreatePresetsPanel(): BaseUIElement { - const userDetails = State.state.osmConnection.userDetails; - if (userDetails === undefined) { - return undefined; - } + private static CreateTagInfoFor(preset: PresetInfo, optionallyLinkToWiki = true) { + const csCount = State.state.osmConnection.userDetails.data.csCount; + return new Toggle( + Translations.t.general.presetInfo.Subs({ + tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"), - if (!userDetails.data.loggedIn) { - return this._loginButton; - } + }), - if (userDetails.data.unreadMessages > 0 && userDetails.data.csCount < Constants.userJourney.addNewPointWithUnreadMessagesUnlock) { - return new Combine([ - Translations.t.general.readYourMessages.Clone().SetClass("alert"), - this.goToInboxButton - ]); - - } - - if (userDetails.data.csCount < Constants.userJourney.addNewPointsUnlock) { - return new Combine(["", - Translations.t.general.fewChangesBefore, - ""]); - } - - if (State.state.locationControl.data.zoom < Constants.userJourney.minZoomLevelToAddNewPoints) { - return Translations.t.general.add.zoomInFurther.SetClass("alert") - } - - if (State.state.layerUpdater.runningQuery.data) { - return Translations.t.general.add.stillLoading - } - - const presetButtons = this.CreatePresetButtons() - return new Combine(presetButtons) + undefined, + State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt) + ); } - - private CreateContent(): BaseUIElement { - const confirmPanel = this.CreateConfirmPanel(); - if (confirmPanel !== undefined) { - return confirmPanel; - } - + private static CreateAllPresetsPanel(selectedPreset: UIEventSource): BaseUIElement { + const presetButtons = SimpleAddUI.CreatePresetButtons(selectedPreset) let intro: BaseUIElement = Translations.t.general.add.intro; let testMode: BaseUIElement = undefined; @@ -133,113 +176,58 @@ export default class SimpleAddUI extends UIElement { testMode = Translations.t.general.testing.Clone().SetClass("alert") } - let presets = this.CreatePresetsPanel(); - return new Combine([intro, testMode, presets]) - + return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col") } - private CreateConfirmPanel(): BaseUIElement { - const preset = this._confirmPreset.data; - if (preset === undefined) { - return undefined; - } + private static CreatePresetSelectButton(preset: PresetInfo){ - const confirmButton = new SubtleButton(preset.icon, + const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false); + return new SubtleButton( + preset.icon, new Combine([ - "", - Translations.t.general.add.confirmButton.Subs({category: preset.name}), - ""])).SetClass("break-words"); - confirmButton.onClick( - this.CreatePoint(preset.tags) - ); - - if (!this._confirmPreset.data.layerToAddTo.isDisplayed.data) { - return new Combine([ - Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.layerDef.name}) - .SetClass("alert"), - this.openLayerControl, - - this.cancelButton - ]); - } - - let tagInfo = ""; - const csCount = State.state.osmConnection.userDetails.data.csCount; - if (csCount > Constants.userJourney.tagsVisibleAt) { - tagInfo = this._confirmPreset.data.tags.map(t => t.asHumanString(csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"); - tagInfo = `
    More information about the preset: ${tagInfo}` - } - - return new Combine([ - Translations.t.general.add.confirmIntro.Subs({title: this._confirmPreset.data.name}), - State.state.osmConnection.userDetails.data.dryRun ? "TESTING - changes won't be saved" : "", - confirmButton, - this.cancelButton, - preset.description, - tagInfo - - ]) - + Translations.t.general.add.addNew.Subs({ + category: preset.name + }).SetClass("font-bold"), + Translations.WT(preset.description)?.FirstSentence(), + tagInfo?.SetClass("subtle") + ]).SetClass("flex flex-col") + ) } - - private CreatePresetButtons() { + +/* +* Generates the list with all the buttons.*/ + private static CreatePresetButtons(selectedPreset: UIEventSource): BaseUIElement { const allButtons = []; - const self = this; for (const layer of State.state.filteredLayers.data) { + + if(layer.isDisplayed.data === false && State.state.featureSwitchLayers){ + continue; + } + const presets = layer.layerDef.presets; for (const preset of presets) { - const tags = TagUtils.KVtoProperties(preset.tags ?? []); - let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource(tags), false).icon.html.SetClass("simple-add-ui-icon"); - const csCount = State.state.osmConnection.userDetails.data.csCount; - let tagInfo = undefined; - if (csCount > Constants.userJourney.tagsVisibleAt) { - const presets = preset.tags.map(t => new Combine([t.asHumanString(false, true), " "]).SetClass("subtle break-words")) - tagInfo = new Combine(presets) + const tags = TagUtils.KVtoProperties(preset.tags ?? []); + let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource(tags), false).icon.html + .SetClass("w-12 h-12 block relative"); + const presetInfo: PresetInfo = { + tags: preset.tags, + layerToAddTo: layer, + name: preset.title, + description: preset.description, + icon: icon } - const button: UIElement = - new SubtleButton( - icon, - new Combine([ - "", - preset.title, - "", - preset.description !== undefined ? new Combine(["
    ", preset.description.FirstSentence()]) : "", - "
    ", - tagInfo - ]) - ).onClick( - () => { - self._confirmPreset.setData({ - tags: preset.tags, - layerToAddTo: layer, - name: preset.title, - description: preset.description, - icon: icon - }); - self.Update(); - } - ) + + const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); + button.onClick(() => { + selectedPreset.setData(presetInfo) + }) allButtons.push(button); } } - return allButtons; + return new Combine(allButtons).SetClass("flex flex-col"); } - private CreatePoint(tags: Tag[]) { - return () => { - console.log("Create Point Triggered") - const loc = State.state.LastClickLocation.data; - let feature = State.state.changes.createElement(tags, loc.lat, loc.lon); - State.state.selectedElement.setData(feature); - this._confirmPreset.setData(undefined); - } - } - - public OnClose(){ - console.log("On close triggered") - this._confirmPreset.setData(undefined) - } } \ No newline at end of file diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index df6de5e2ea..656b9160d2 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -37,7 +37,7 @@ export default class DeleteImage extends UIElement { cancelButton ]).SetClass("flex flex-col background-black"), Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;") - ) + ).ToggleOnClick() } diff --git a/UI/Input/FixedInputElement.ts b/UI/Input/FixedInputElement.ts index bc2424939d..de0f2b593e 100644 --- a/UI/Input/FixedInputElement.ts +++ b/UI/Input/FixedInputElement.ts @@ -1,44 +1,46 @@ import {InputElement} from "./InputElement"; -import {UIElement} from "../UIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; import {UIEventSource} from "../../Logic/UIEventSource"; +import Translations from "../i18n/Translations"; +import BaseUIElement from "../BaseUIElement"; export class FixedInputElement extends InputElement { - private readonly rendering: UIElement; private readonly value: UIEventSource; public readonly IsSelected : UIEventSource = new UIEventSource(false); private readonly _comparator: (t0: T, t1: T) => boolean; - constructor(rendering: UIElement | string, + private readonly _el : HTMLElement; + + constructor(rendering: BaseUIElement | string, value: T, comparator: ((t0: T, t1: T) => boolean ) = undefined) { - super(undefined); + super(); this._comparator = comparator ?? ((t0, t1) => t0 == t1); this.value = new UIEventSource(value); - this.rendering = typeof (rendering) === 'string' ? new FixedUiElement(rendering) : rendering; - const self = this; + + const selected = this.IsSelected; + this._el = document.createElement("span") + this._el.addEventListener("mouseout", () => selected.setData(false)) + const e = Translations.W(rendering)?.ConstructElement() + if(e){ + this._el.appendChild( e) + } + this.onClick(() => { - self.IsSelected.setData(true) + selected.setData(true) }) } + protected InnerConstructElement(): HTMLElement { + return undefined; + } + GetValue(): UIEventSource { return this.value; } - InnerRender(): string { - return this.rendering.Render(); - } IsValid(t: T): boolean { return this._comparator(t, this.value.data); } - protected InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); - const self = this; - htmlElement.addEventListener("mouseout", () => self.IsSelected.setData(false)) - - } - } \ No newline at end of file diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 3ead32abf5..ac0828f75b 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -3,24 +3,19 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import {Utils} from "../../Utils"; export class RadioButton extends InputElement { + private static _nextId = 0; IsSelected: UIEventSource = new UIEventSource(false); - - private readonly _selectedElementIndex: UIEventSource - = new UIEventSource(null); - private readonly value: UIEventSource; - private readonly _elements: InputElement[] - private readonly _selectFirstAsDefault: boolean; + private _elements: InputElement[]; + private readonly _element: HTMLElement; constructor(elements: InputElement[], selectFirstAsDefault = true) { - super(undefined); - this._elements = Utils.NoNull(elements); - this._selectFirstAsDefault = selectFirstAsDefault; - const self = this; - - this.value = - UIEventSource.flatten(this._selectedElementIndex.map( + super() + elements = Utils.NoNull(elements); + const selectedElementIndex: UIEventSource = new UIEventSource(null); + const value = + UIEventSource.flatten(selectedElementIndex.map( (selectedIndex) => { if (selectedIndex !== undefined && selectedIndex !== null) { return elements[selectedIndex].GetValue() @@ -28,26 +23,63 @@ export class RadioButton extends InputElement { } ), elements.map(e => e?.GetValue())); - this.value.addCallback((t) => { - self?.ShowValue(t); - }) + + /* + value.addCallback((t) => { + self?.ShowValue(t); + })*/ for (let i = 0; i < elements.length; i++) { // If an element is clicked, the radio button corresponding with it should be selected as well elements[i]?.onClick(() => { - self._selectedElementIndex.setData(i); + selectedElementIndex.setData(i); }); elements[i].IsSelected.addCallback(isSelected => { if (isSelected) { - self._selectedElementIndex.setData(i); + selectedElementIndex.setData(i); } }) elements[i].GetValue().addCallback(() => { - self._selectedElementIndex.setData(i); + selectedElementIndex.setData(i); }) } - this.dumbMode = false; + + + const groupId = "radiogroup" + RadioButton._nextId + RadioButton._nextId++ + + const form = document.createElement("form") + this._element = form; + for (let i1 = 0; i1 < elements.length; i1++) { + let element = elements[i1]; + const labelHtml = element.ConstructElement(); + if (labelHtml === undefined) { + continue; + } + + const input = document.createElement("input") + input.id = "radio" + groupId + "-" + i1; + input.name = groupId; + input.type = "radio" + + + const label = document.createElement("label") + label.appendChild(labelHtml) + label.htmlFor = input.id; + input.appendChild(label) + + form.appendChild(input) + + form.addEventListener("change", () => { + // TODO FIXME + } + ); + } + + + this.value = value; + this._elements = elements; } @@ -65,25 +97,11 @@ export class RadioButton extends InputElement { } - private IdFor(i) { - return 'radio-' + this.id + '-' + i; - } - - InnerRender(): string { - let body = ""; - for (let i = 0; i < this._elements.length; i++){ - const el = this._elements[i]; - const htmlElement = - ``; - body += htmlElement; - } - - return `
    ${body}
    `; + protected InnerConstructElement(): HTMLElement { + return this._element; } + /* public ShowValue(t: T): boolean { if (t === undefined) { return false; @@ -104,48 +122,7 @@ export class RadioButton extends InputElement { } } - } - - InnerUpdate(htmlElement: HTMLElement) { - const self = this; - - function checkButtons() { - for (let i = 0; i < self._elements.length; i++) { - const el = document.getElementById(self.IdFor(i)); - // @ts-ignore - if (el.checked) { - self._selectedElementIndex.setData(i); - } - } - } - - const el = document.getElementById(this.id); - el.addEventListener("change", - function () { - checkButtons(); - } - ); - if (this._selectedElementIndex.data !== null) { - const el = document.getElementById(this.IdFor(this._selectedElementIndex.data)); - if (el) { - // @ts-ignore - el.checked = true; - checkButtons(); - } - } else if (this._selectFirstAsDefault) { - this.ShowValue(this.value.data); - if (this._selectedElementIndex.data === null || this._selectedElementIndex.data === undefined) { - const el = document.getElementById(this.IdFor(0)); - if (el) { - // @ts-ignore - el.checked = true; - checkButtons(); - } - } - } - - - }; + }*/ } \ No newline at end of file diff --git a/UI/Input/Toggle.ts b/UI/Input/Toggle.ts index f3a4eb74f7..ca9fd6fbab 100644 --- a/UI/Input/Toggle.ts +++ b/UI/Input/Toggle.ts @@ -15,9 +15,13 @@ export default class Toggle extends VariableUiElement{ isEnabled.map(isEnabled => isEnabled ? showEnabled : showDisabled) ); this.isEnabled = isEnabled + } + + public ToggleOnClick(): Toggle{ + const self = this; this.onClick(() => { - isEnabled.setData(!isEnabled.data); + self. isEnabled.setData(!self.isEnabled.data); }) - + return this; } } \ No newline at end of file diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 0019910a5a..db89a4fd5b 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingQuestion from "./TagRenderingQuestion"; @@ -7,80 +6,65 @@ import Combine from "../Base/Combine"; import TagRenderingAnswer from "./TagRenderingAnswer"; import State from "../../State"; import Svg from "../../Svg"; +import Toggle from "../Input/Toggle"; +import BaseUIElement from "../BaseUIElement"; -export default class EditableTagRendering extends UIElement { - private readonly _tags: UIEventSource; - private readonly _configuration: TagRenderingConfig; - - private _editMode: UIEventSource = new UIEventSource(false); - private _editButton: UIElement; - - private _question: UIElement; - private _answer: UIElement; +export default class EditableTagRendering extends Toggle { constructor(tags: UIEventSource, configuration: TagRenderingConfig) { - super(tags); - this._tags = tags; - this._configuration = configuration; - this.ListenTo(this._editMode); - this.ListenTo(State.state?.osmConnection?.userDetails) + const editMode = new UIEventSource(false); - this._answer = new TagRenderingAnswer(tags, configuration); - this._answer.SetClass("w-full") - this._question = this.GenerateQuestion(); - this.dumbMode = false; + const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration) + let rendering = answer; - if (this._configuration.question !== undefined) { - if (State.state?.featureSwitchUserbadge?.data) { - // 2.3em total width - const self = this; - this._editButton = - Svg.pencil_svg().SetClass("edit-button") - .onClick(() => { - self._editMode.setData(true); - }); - } - } - } + if (configuration.question !== undefined && State.state?.featureSwitchUserbadge?.data) { + // We have a question and editing is enabled + const editButton = + new Combine([Svg.pencil_ui()]).SetClass("block relative h-10 w-10 p-2 float-right").SetStyle("border: 1px solid black; border-radius: 0.7em") + .onClick(() => { + editMode.setData(true); + }); - InnerRender(): string { - if (!this._configuration?.condition?.matchesProperties(this._tags.data)) { - return ""; - } - if (this._editMode.data) { - return this._question.Render(); - } - if(!this._configuration.IsKnown(this._tags.data)){ - // Even though it is not known, we hide the question here - // It is the questionbox's task to show the question in edit mode - return ""; - } - return new Combine([this._answer, - (State.state?.osmConnection?.userDetails?.data?.loggedIn ?? true) ? this._editButton : undefined - ]).SetClass("flex w-full break-word justify-between text-default landscape:w-1/2 landscape:p-2 pb-2 border-b border-gray-300 mb-2") - .Render(); - } + const answerWithEditButton = new Combine([answer, + new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)]).SetClass("w-full") + - private GenerateQuestion() { - const self = this; - if (this._configuration.question !== undefined) { - // And at last, set up the skip button const cancelbutton = Translations.t.general.cancel.Clone() .SetClass("btn btn-secondary mr-3") .onClick(() => { - self._editMode.setData(false) + editMode.setData(false) }); - return new TagRenderingQuestion(this._tags, this._configuration, + const question = new TagRenderingQuestion(tags, configuration, () => { - self._editMode.setData(false) + editMode.setData(false) }, cancelbutton) + + + rendering = new Toggle( + question, + answerWithEditButton, + editMode + ) } + answer.SetClass("flex w-full break-word justify-between text-default landscape:w-1/2 landscape:p-2 pb-2 border-b border-gray-300 mb-2") + rendering.SetClass("flex m-1 p-1 border-b border-gray-300 mb-2 pb-2") + // The tagrendering is hidden if: + // The answer is unknown. The questionbox will then show the question + // There is a condition hiding the answer + const renderingIsShown = tags.map(tags => + !configuration.IsKnown(tags) && + (configuration?.condition?.matchesProperties(tags) ?? true)) + super( + rendering, + undefined, + renderingIsShown + ) } } \ No newline at end of file diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 14025b9054..64ffe59a2c 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -11,10 +11,11 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import {Tag} from "../../Logic/Tags/Tag"; import Constants from "../../Models/Constants"; import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; +import BaseUIElement from "../BaseUIElement"; export default class FeatureInfoBox extends ScrollableFullScreen { - private constructor( + public constructor( tags: UIEventSource, layerConfig: LayerConfig ) { @@ -28,12 +29,8 @@ export default class FeatureInfoBox extends ScrollableFullScreen { } - static construct(tags: UIEventSource, layer: LayerConfig): FeatureInfoBox { - return new FeatureInfoBox(tags, layer) - } - private static GenerateTitleBar(tags: UIEventSource, - layerConfig: LayerConfig): UIElement { + layerConfig: LayerConfig): BaseUIElement { const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI", undefined)) .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2"); const titleIcons = new Combine( @@ -48,7 +45,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { } private static GenerateContent(tags: UIEventSource, - layerConfig: LayerConfig): UIElement { + layerConfig: LayerConfig): BaseUIElement { let questionBox: UIElement = undefined; if (State.state.featureSwitchUserbadge.data) { diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index 89d7aa6db8..415b30da56 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -1,17 +1,11 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import Translations from "../i18n/Translations"; import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import BaseUIElement from "../BaseUIElement"; import Toggle from "../Input/Toggle"; -export class SaveButton extends UIElement { - - - private readonly _element: BaseUIElement; +export class SaveButton extends Toggle { constructor(value: UIEventSource, osmConnection: OsmConnection) { - super(value); if (value === undefined) { throw "No event source for savebutton, something is wrong" } @@ -31,7 +25,7 @@ export class SaveButton extends UIElement { saveDisabled, isSaveable ) - this._element = new Toggle( + super( save , pleaseLogin, osmConnection?.userDetails?.map(userDetails => userDetails.loggedIn) ?? new UIEventSource(false) @@ -39,9 +33,4 @@ export class SaveButton extends UIElement { } - InnerRender(): BaseUIElement { - return this._element - - } - } \ No newline at end of file diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 1b716208c7..8000361515 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -1,98 +1,43 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; -import {UIElement} from "../UIElement"; import {Utils} from "../../Utils"; -import Combine from "../Base/Combine"; import {SubstitutedTranslation} from "../SubstitutedTranslation"; -import {Translation} from "../i18n/Translation"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import List from "../Base/List"; +import {FixedUiElement} from "../Base/FixedUiElement"; /*** * Displays the correct value for a known tagrendering */ -export default class TagRenderingAnswer extends UIElement { - private readonly _tags: UIEventSource; - private _configuration: TagRenderingConfig; - private _content: BaseUIElement; - private readonly _contentClass: string; - private _contentStyle: string; +export default class TagRenderingAnswer extends VariableUiElement { - constructor(tags: UIEventSource, configuration: TagRenderingConfig, contentClasses: string = "", contentStyle: string = "") { - super(); - this._tags = tags; - this.ListenTo(tags) - this._configuration = configuration; - this._contentClass = contentClasses; - this._contentStyle = contentStyle; + constructor(tagsSource: UIEventSource, configuration: TagRenderingConfig, contentClasses: string = "", contentStyle: string = "") { if (configuration === undefined) { throw "Trying to generate a tagRenderingAnswer without configuration..." } + super(tagsSource.map(tags => { + if(tags === undefined){ + return undefined; + } + const trs = Utils.NoNull(configuration.GetRenderValues(tags)); + if(trs.length === 0){ + return undefined; + } + trs.forEach(tr => console.log("Rendering ", tr)) + const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource)) + + if(valuesToRender.length === 1){ + return valuesToRender[0]; + }else if(valuesToRender.length > 1){ + return new List(valuesToRender) + } + return undefined; + }).map(innerComponent => innerComponent?.SetClass(contentClasses)?.SetStyle(contentStyle)) + ) + this.SetClass("flex items-center flex-row text-lg link-underline") this.SetStyle("word-wrap: anywhere;"); } - InnerRender(): string | BaseUIElement{ - if (this._configuration.condition !== undefined) { - if (!this._configuration.condition.matchesProperties(this._tags.data)) { - return ""; - } - } - - const tags = this._tags.data; - if (tags === undefined) { - return ""; - } - - // The render value doesn't work well with multi-answers (checkboxes), so we have to check for them manually - if (this._configuration.multiAnswer) { - - let freeformKeyUsed = this._configuration.freeform?.key === undefined; // If it is undefined, it is "used" already, or at least we don't have to check for it anymore - const applicableThens: Translation[] = Utils.NoNull(this._configuration.mappings?.map(mapping => { - if (mapping.if === undefined) { - return mapping.then; - } - if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) { - if(!freeformKeyUsed){ - if(mapping.if.usedKeys().indexOf(this._configuration.freeform.key) >= 0){ - freeformKeyUsed = true; - } - } - return mapping.then; - } - return undefined; - }) ?? []) - - if (!freeformKeyUsed - && tags[this._configuration.freeform.key] !== undefined) { - applicableThens.push(this._configuration.render) - } - - const self = this - const valuesToRender: UIElement[] = applicableThens.map(tr => SubstitutedTranslation.construct(tr, self._tags)) - - if (valuesToRender.length >= 0) { - if (valuesToRender.length === 1) { - this._content = valuesToRender[0]; - } else { - this._content = new Combine(["
      ", - ...valuesToRender.map(tr => new Combine(["
    • ", tr, "
    • "])) , - "
    " - ]) - - } - 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); - } - - return ""; - - } - } \ No newline at end of file diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index cdf197c77d..292b530212 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -32,23 +32,23 @@ export default class TagRenderingQuestion extends UIElement { private readonly _tags: UIEventSource; private _configuration: TagRenderingConfig; - private _saveButton: UIElement; + private _saveButton: BaseUIElement; private _inputElement: InputElement; - private _cancelButton: UIElement; + private _cancelButton: BaseUIElement; private _appliedTags: BaseUIElement; - private _question: UIElement; + private _question: BaseUIElement; constructor(tags: UIEventSource, configuration: TagRenderingConfig, afterSave?: () => void, - cancelButton?: UIElement + cancelButton?: BaseUIElement ) { super(tags); this._tags = tags; this._configuration = configuration; this._cancelButton = cancelButton; - this._question = SubstitutedTranslation.construct(this._configuration.question, tags) + this._question = new SubstitutedTranslation(this._configuration.question, tags) .SetClass("question-text"); if (configuration === undefined) { throw "A question is needed for a question visualization" @@ -242,7 +242,7 @@ export default class TagRenderingQuestion extends UIElement { return undefined; } return new FixedInputElement( - SubstitutedTranslation.construct(mapping.then, this._tags), + new SubstitutedTranslation(mapping.then, this._tags), mapping.if, (t0, t1) => t1.isEquivalent(t0)); } diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index ce2ac3423e..fb0ecc32f4 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -102,8 +102,8 @@ export default class ReviewForm extends InputElement { .SetClass("review-form") - return new Toggle(form, Translations.t.reviews.plz_login, - this.userDetails.map(userdetails => userdetails.loggedIn)) + return new Toggle(form, Translations.t.reviews.plz_login.Clone(), + this.userDetails.map(userdetails => userdetails.loggedIn)).ToggleOnClick() .ConstructElement() } diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index bb15b6dca7..70560ad7e8 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -87,10 +87,10 @@ export default class ShowDataLayer { } marker.openPopup(); - const popup = marker.getPopup(); + const tags = State.state.allElements.getEventSourceById(selected.properties.id); const layer: LayerConfig = this._layerDict[selected._matching_layer_id]; - const infoBox = FeatureInfoBox.construct(tags, layer); + const infoBox = new FeatureInfoBox(tags, layer); infoBox.isShown.addCallback(isShown => { if (!isShown) { @@ -98,9 +98,8 @@ export default class ShowDataLayer { } }); - popup.setContent(infoBox.Render()); + infoBox.AttachTo(`popup-${selected.properties.id}`) infoBox.Activate(); - infoBox.Update(); }) } @@ -156,11 +155,13 @@ export default class ShowDataLayer { }, leafletLayer); // By setting 50vh, leaflet will attempt to fit the entire screen and move the feature down - popup.setContent("
    Rendering
    "); + popup.setContent(``); leafletLayer.bindPopup(popup); + leafletLayer.on("popupopen", () => { State.state.selectedElement.setData(feature) + // The feature info box is bound via the selected element callback, as there are multiple ways to open the popup (e.g. a trigger via the URLยฐ }); this._popups.set(feature, leafletLayer); diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index e63a7f8e3d..bd43e127f4 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -1,71 +1,37 @@ -import {UIElement} from "./UIElement"; import {UIEventSource} from "../Logic/UIEventSource"; import {Translation} from "./i18n/Translation"; import Locale from "./i18n/Locale"; -import Combine from "./Base/Combine"; import State from "../State"; import {FixedUiElement} from "./Base/FixedUiElement"; import SpecialVisualizations from "./SpecialVisualizations"; import BaseUIElement from "./BaseUIElement"; +import {Utils} from "../Utils"; +import {VariableUiElement} from "./Base/VariableUIElement"; +import Combine from "./Base/Combine"; -export class SubstitutedTranslation extends UIElement { - private readonly tags: UIEventSource; - private readonly translation: Translation; - private content: BaseUIElement[]; +export class SubstitutedTranslation extends VariableUiElement { - private constructor( + public constructor( translation: Translation, tags: UIEventSource) { - super(tags); - this.translation = translation; - this.tags = tags; - const self = this; - tags.addCallbackAndRun(() => { - self.content = self.CreateContent(); - self.Update(); - }); - - Locale.language.addCallback(() => { - self.content = self.CreateContent(); - self.Update(); - }); + super( + tags.map(tags => { + const txt = Utils.SubstituteKeys(translation.txt, tags) + if (txt === undefined) { + return "no tags subs tr" + } + const contents = SubstitutedTranslation.EvaluateSpecialComponents(txt, tags) + console.log("Substr has contents", contents) + return new Combine(contents) + }, [Locale.language]) + ) + + this.SetClass("w-full") } - public static construct( - translation: Translation, - tags: UIEventSource): SubstitutedTranslation { - 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; - } - - InnerRender() { - if (this.content.length == 1) { - return this.content[0]; - } - return new Combine(this.content); - } - - private CreateContent(): BaseUIElement[] { - let txt = this.translation?.txt; - if (txt === undefined) { - return [] - } - const tags = this.tags.data; - txt = SubstitutedTranslation.SubstituteKeys(txt, tags); - return this.EvaluateSpecialComponents(txt); - } - - private EvaluateSpecialComponents(template: string): BaseUIElement[] { + private static EvaluateSpecialComponents(template: string, tags: UIEventSource): BaseUIElement[] { for (const knownSpecial of SpecialVisualizations.specialVisualizations) { @@ -74,9 +40,9 @@ export class SubstitutedTranslation extends UIElement { if (matched != null) { // We found a special component that should be brought to live - const partBefore = this.EvaluateSpecialComponents(matched[1]); + const partBefore = SubstitutedTranslation.EvaluateSpecialComponents(matched[1], tags); const argument = matched[2].trim(); - const partAfter = this.EvaluateSpecialComponents(matched[3]); + const partAfter = SubstitutedTranslation.EvaluateSpecialComponents(matched[3], tags); try { const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); if (argument.length > 0) { @@ -91,7 +57,13 @@ export class SubstitutedTranslation extends UIElement { } - const element = knownSpecial.constr(State.state, this.tags, args); + let element: BaseUIElement = new FixedUiElement(`Constructing ${knownSpecial}(${args.join(", ")})`) + try{ + element = knownSpecial.constr(State.state, tags, args); + }catch(e){ + element = new FixedUiElement(`Could not generate special renering for ${knownSpecial}(${args.join(", ")}) ${e}`).SetClass("alert") + } + return [...partBefore, element, ...partAfter] } catch (e) { console.error(e); diff --git a/UI/UIElement.ts b/UI/UIElement.ts index e228d70d3c..83b5f67526 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -21,7 +21,7 @@ export abstract class UIElement extends BaseUIElement{ if (source === undefined) { return this; } - console.trace("Got a listenTo in ", this.constructor.name) + //console.trace("Got a listenTo in ", this.constructor.name) const self = this; source.addCallback(() => { self.lastInnerRender = undefined; diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index ab13e4b403..508d9c0261 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -32,10 +32,14 @@ export class Translation extends BaseUIElement { } get txt(): string { + return this.textFor(Translation.forcedLanguage ?? Locale.language.data) + } + + public textFor(language: string): string{ if (this.translations["*"]) { return this.translations["*"]; } - const txt = this.translations[Translation.forcedLanguage ?? Locale.language.data]; + const txt = this.translations[language]; if (txt !== undefined) { return txt; } @@ -52,7 +56,7 @@ export class Translation extends BaseUIElement { console.error("Missing language ", Locale.language.data, "for", this.translations) return ""; } - + InnerConstructElement(): HTMLElement { const el = document.createElement("span") Locale.language.addCallbackAndRun(_ => { @@ -106,12 +110,12 @@ export class Translation extends BaseUIElement { // @ts-ignore const date: Date = el; rtext = date.toLocaleString(); - } else if (el.InnerRenderAsString === undefined) { + } else if (el.ConstructElement() === 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.InnerRenderAsString(); + rtext = el.ConstructElement().innerHTML; } for (let i = 0; i < parts.length - 1; i++) { diff --git a/Utils.ts b/Utils.ts index 9b41a260de..b8ae81e7e8 100644 --- a/Utils.ts +++ b/Utils.ts @@ -149,6 +149,16 @@ export class Utils { return [a.substr(0, index), a.substr(index + sep.length)]; } + 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; + } + // Date will be undefined on failure public static LoadCustomCss(location: string) { const head = document.getElementsByTagName('head')[0]; @@ -251,6 +261,10 @@ export class Utils { public static UnMinify(minified: string): string { + if(minified === undefined || minified === null){ + return undefined; + } + const parts = minified.split("|"); let result = parts.shift(); const keys = Utils.knownKeys.concat(Utils.extraKeys); diff --git a/css/tagrendering.css b/css/tagrendering.css index 5c73f84ede..a87f67a471 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -68,57 +68,4 @@ input:checked + label .question-option-with-border { width: 100%; } -.edit-button img { - width: 1.3em; - height: 1.3em; - padding: 0.5em; - border-radius: 0.65em; - border: solid var(--popup-border) 1px; - font-size: medium; - float: right; -} -.edit-button svg { - width: 1.3em; - height: 1.3em; - padding: 0.5em; - border-radius: 0.65em; - border: solid var(--foreground-color) 1px; - stroke: var(--foreground-color) !important; - fill: var(--foreground-color) !important; - font-size: medium; - float: right; -} - -.edit-button svg path { - stroke: var(--foreground-color) !important; - fill: var(--foreground-color) !important; -} - - - -.to-the-map span { - font-size: xx-large; -} - -.to-the-map { - background: var(--catch-detail-color); - height: var(--return-to-the-map-height); - color: var(--catch-detail-color-contrast); - font-weight: bold; - pointer-events: all; - cursor: pointer; - padding-top: 0.4em; - text-align: center; - box-sizing: border-box; - display: block; - max-height: var(--return-to-the-map-height); - position: fixed; - width: 100vw; - bottom: 0; - z-index: 100000; -} - -.to-the-map-inner{ - font-size: xx-large; -} diff --git a/index.css b/index.css index c6b0dabbb7..10c8df5ee7 100644 --- a/index.css +++ b/index.css @@ -222,23 +222,6 @@ li::marker { max-width: 2em !important; } -.simple-add-ui-icon { - position: relative; - display: block; - width: 4em; - height: 3.5em; -} - -.simple-add-ui-icon img { - max-height: 3.5em !important; - max-width: 3.5em !important; -} - -.simple-add-ui-icon svg { - max-height: 3.5em !important; - max-width: 3.5em !important; -} - /**************** GENERIC ****************/ @@ -292,14 +275,10 @@ li::marker { } .link-underline .subtle a { - color: var(--foreground-color); text-decoration: underline 1px #7193bb88; color: #7193bb; } -.bold { - font-weight: bold; -} .thanks { background-color: #43d904; @@ -318,11 +297,6 @@ li::marker { pointer-events: none !important; } -.page-split { - display: flex; - height: 100%; -} - /**************************************/ diff --git a/langs/en.json b/langs/en.json index 287f85dc3a..b92fc6e2b6 100644 --- a/langs/en.json +++ b/langs/en.json @@ -54,7 +54,7 @@ "zoomInFurther": "Zoom in further to add a point.", "stillLoading": "The data is still loading. Please wait a bit before you add a new point.", "confirmIntro": "

    Add a {title} here?

    The point you create here will be visible for everyone. Please, only add things on to the map if they truly exist. A lot of applications use this data.", - "confirmButton": "Add a {category} here.
    Your addition is visible for everyone
    ", + "warnVisibleForEveryone": "Your addition will be visible for everyone", "openLayerControl": "Open the layer control box", "layerNotEnabled": "The layer {layer} is not enabled. Enable this layer to add a point" }, @@ -108,6 +108,7 @@ "createYourOwnTheme": "Create your own MapComplete theme from scratch" }, "readYourMessages": "Please, read all your OpenStreetMap-messages before adding a new point.", + "presetInfo": "The new POI will have {tags}", "fewChangesBefore": "Please, answer a few questions of existing points before adding a new point.", "goToInbox": "Open inbox", "getStartedLogin": "Login with OpenStreetMap to get started", diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 0c1938a79c..b4d07702c9 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -144,8 +144,6 @@ export default class TagSpec extends T{ equal(undefined, tr.GetRenderValue({"foo": "bar"})); 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"})).InnerRenderAsString()); equal(undefined, tr.GetRenderValue({"foo": "bar"})); })], @@ -196,7 +194,7 @@ export default class TagSpec extends T{ const uiEl = new EditableTagRendering(new UIEventSource( {leisure: "park", "access": "no"}), constr ); - const rendered = uiEl.InnerRenderAsString(); + const rendered = uiEl.ConstructElement().innerHTML; equal(true, rendered.indexOf("Niet toegankelijk") > 0) } diff --git a/test/TagQuestion.spec.ts b/test/TagQuestion.spec.ts index f0415ad008..b094986df5 100644 --- a/test/TagQuestion.spec.ts +++ b/test/TagQuestion.spec.ts @@ -5,7 +5,6 @@ Utils.runningFromConsole = true; import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion"; import {UIEventSource} from "../Logic/UIEventSource"; import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; -import EditableTagRendering from "../UI/Popup/EditableTagRendering"; export default class TagQuestionSpec extends T { constructor() { From 8ad9b816acdd37cfee3acba70075b50723a61d6f Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 14 Jun 2021 17:28:11 +0200 Subject: [PATCH 07/30] Fix popups and core functionality --- UI/Base/VariableUIElement.ts | 2 +- UI/BaseUIElement.ts | 1 + UI/BigComponents/ShareScreen.ts | 7 ++-- UI/Input/FixedInputElement.ts | 2 +- UI/Input/RadioButton.ts | 39 ++++++++++++++---- UI/Popup/EditableTagRendering.ts | 10 +++-- UI/Popup/TagRenderingAnswer.ts | 25 +++++------- UI/ShowDataLayer.ts | 70 ++++++++++++++++---------------- UI/SubstitutedTranslation.ts | 4 +- test.ts | 28 ++++++++++++- test/Tag.spec.ts | 6 --- test/TagQuestion.spec.ts | 62 ---------------------------- test/TestAll.ts | 4 -- 13 files changed, 116 insertions(+), 144 deletions(-) delete mode 100644 test/TagQuestion.spec.ts diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index 8d82858733..c064f9ce6e 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -26,7 +26,7 @@ export class VariableUiElement extends BaseUIElement { for (const content of contents) { const c = content.ConstructElement(); if (c !== undefined && c !== null) { - el.appendChild(c) + el.appendChild(c) } } diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 840814530e..125d1ffc41 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -43,6 +43,7 @@ export default abstract class BaseUIElement { throw "SEVERE: could not attach UIElement to " + divId; } + console.log("Attaching to ", element) while (element.firstChild) { //The list is LIVE so it will re-index each call element.removeChild(element.firstChild); diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index 7f32802f53..7396c15682 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -46,7 +46,10 @@ export default class ShareScreen extends Combine { return null; } if (includeL) { - return `z=${currentLocation.data.zoom}&lat=${currentLocation.data.lat}&lon=${currentLocation.data.lon}` + return [["z", currentLocation.data?.zoom], ["lat", currentLocation.data?.lat], ["lon", currentLocation.data?.lon]] + .filter(p => p[1] !== undefined) + .map(p => p[0]+"="+p[1]) + .join("&") } else { return null; } @@ -166,8 +169,6 @@ export default class ShareScreen extends Combine { }, optionParts); - const iframe = url.map(url => `<iframe src="${url}" width="100%" height="100%" title="${layout?.title?.txt ?? "MapComplete"} with MapComplete"></iframe>`); - const iframeCode = new VariableUiElement( url.map((url) => { return ` diff --git a/UI/Input/FixedInputElement.ts b/UI/Input/FixedInputElement.ts index de0f2b593e..6e46ae5fc5 100644 --- a/UI/Input/FixedInputElement.ts +++ b/UI/Input/FixedInputElement.ts @@ -31,7 +31,7 @@ export class FixedInputElement extends InputElement { } protected InnerConstructElement(): HTMLElement { - return undefined; + return this._el; } GetValue(): UIEventSource { diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index ac0828f75b..760c371940 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -22,13 +22,24 @@ export class RadioButton extends InputElement { } } ), elements.map(e => e?.GetValue())); + + if(selectFirstAsDefault){ + + value.addCallbackAndRun(selected =>{ + if(selected === undefined){ + for (const element of elements) { + const v = element.GetValue().data; + if(v !== undefined){ + value.setData(v) + break; + } + } + + + } + }) - - /* - value.addCallback((t) => { - self?.ShowValue(t); - })*/ - + } for (let i = 0; i < elements.length; i++) { // If an element is clicked, the radio button corresponding with it should be selected as well @@ -63,14 +74,25 @@ export class RadioButton extends InputElement { input.name = groupId; input.type = "radio" + input.onchange = () => { + if(input.checked){ + selectedElementIndex.setData(i1) + } + } + + value.addCallbackAndRun( + selected => input.checked = element.IsValid(selected) + ) const label = document.createElement("label") label.appendChild(labelHtml) label.htmlFor = input.id; - input.appendChild(label) - form.appendChild(input) + const block = document.createElement("div") + block.appendChild(input) + block.appendChild(label) + form.appendChild(block) form.addEventListener("change", () => { // TODO FIXME } @@ -81,6 +103,7 @@ export class RadioButton extends InputElement { this.value = value; this._elements = elements; + this.SetClass("flex flex-col") } IsValid(t: T): boolean { diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index db89a4fd5b..373f29ef67 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -8,6 +8,7 @@ import State from "../../State"; import Svg from "../../Svg"; import Toggle from "../Input/Toggle"; import BaseUIElement from "../BaseUIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; export default class EditableTagRendering extends Toggle { @@ -28,8 +29,10 @@ export default class EditableTagRendering extends Toggle { }); + const answerWithEditButton = new Combine([answer, - new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)]).SetClass("w-full") + new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)]) + .SetClass("flex justify-between w-full") const cancelbutton = @@ -52,13 +55,12 @@ export default class EditableTagRendering extends Toggle { editMode ) } - answer.SetClass("flex w-full break-word justify-between text-default landscape:w-1/2 landscape:p-2 pb-2 border-b border-gray-300 mb-2") - rendering.SetClass("flex m-1 p-1 border-b border-gray-300 mb-2 pb-2") + rendering.SetClass("block w-full break-word text-default m-1 p-1 border-b border-gray-200 mb-2 pb-2") // The tagrendering is hidden if: // The answer is unknown. The questionbox will then show the question // There is a condition hiding the answer const renderingIsShown = tags.map(tags => - !configuration.IsKnown(tags) && + configuration.IsKnown(tags) && (configuration?.condition?.matchesProperties(tags) ?? true)) super( rendering, diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 8000361515..86db6437ef 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -1,11 +1,10 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import {Utils} from "../../Utils"; -import {SubstitutedTranslation} from "../SubstitutedTranslation"; import BaseUIElement from "../BaseUIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import List from "../Base/List"; -import {FixedUiElement} from "../Base/FixedUiElement"; +import {SubstitutedTranslation} from "../SubstitutedTranslation"; /*** * Displays the correct value for a known tagrendering @@ -24,19 +23,17 @@ export default class TagRenderingAnswer extends VariableUiElement { if(trs.length === 0){ return undefined; } - trs.forEach(tr => console.log("Rendering ", tr)) - const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource)) + + const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource)) + if(valuesToRender.length === 1){ + return valuesToRender[0]; + }else if(valuesToRender.length > 1){ + return new List(valuesToRender) + } + return undefined; + }).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) - if(valuesToRender.length === 1){ - return valuesToRender[0]; - }else if(valuesToRender.length > 1){ - return new List(valuesToRender) - } - return undefined; - }).map(innerComponent => innerComponent?.SetClass(contentClasses)?.SetStyle(contentStyle)) - ) - - this.SetClass("flex items-center flex-row text-lg link-underline") + this.SetClass("flex items-center flex-row text-lg link-underline tag-renering-answer") this.SetStyle("word-wrap: anywhere;"); } diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 70560ad7e8..6a2a8953dd 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -14,8 +14,7 @@ export default class ShowDataLayer { private _layerDict; private readonly _leafletMap: UIEventSource; - - private readonly _popups = new Map(); + private _cleanCount = 0; constructor(features: UIEventSource<{ feature: any, freshness: Date }[]>, leafletMap: UIEventSource, @@ -44,6 +43,7 @@ export default class ShowDataLayer { return; } + self._cleanCount++ // clean all the old stuff away, if any if (geoLayer !== undefined) { mp.removeLayer(geoLayer); @@ -74,34 +74,6 @@ export default class ShowDataLayer { features.addCallback(() => update()); leafletMap.addCallback(() => update()); update(); - - - State.state.selectedElement.addCallbackAndRun(selected => { - if (selected === undefined) { - mp.closePopup(); - return; - } - const marker = self._popups.get(selected); - if (marker === undefined) { - return; - } - marker.openPopup(); - - - const tags = State.state.allElements.getEventSourceById(selected.properties.id); - const layer: LayerConfig = this._layerDict[selected._matching_layer_id]; - const infoBox = new FeatureInfoBox(tags, layer); - - infoBox.isShown.addCallback(isShown => { - if (!isShown) { - State.state.selectedElement.setData(undefined); - } - }); - - infoBox.AttachTo(`popup-${selected.properties.id}`) - infoBox.Activate(); - }) - } @@ -154,17 +126,43 @@ export default class ShowDataLayer { closeButton: false }, leafletLayer); - // By setting 50vh, leaflet will attempt to fit the entire screen and move the feature down - popup.setContent(``); - - leafletLayer.bindPopup(popup); + leafletLayer.bindPopup(popup); + let infobox : FeatureInfoBox = undefined; + + const id = `popup-${feature.properties.id}-${this._cleanCount}` + popup.setContent(`
    Rendering
    `) + leafletLayer.on("popupopen", () => { State.state.selectedElement.setData(feature) - // The feature info box is bound via the selected element callback, as there are multiple ways to open the popup (e.g. a trigger via the URLยฐ + if (infobox === undefined) { + const tags = State.state.allElements.getEventSourceById(feature.properties.id); + infobox = new FeatureInfoBox(tags, layer); + + infobox.isShown.addCallback(isShown => { + if (!isShown) { + State.state.selectedElement.setData(undefined); + } + }); + } + + + infobox.AttachTo(id) + infobox.Activate(); }); + const self = this; + State.state.selectedElement.addCallbackAndRun(selected => { + if (selected === undefined || self._leafletMap.data === undefined) { + return; + } + if (popup.isOpen()) { + return; + } + if (selected.properties.id === feature.properties.id) { + leafletLayer.openPopup() + } + }) - this._popups.set(feature, leafletLayer); } private CreateGeojsonLayer(): L.Layer { diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index bd43e127f4..d48e56cb99 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -20,9 +20,7 @@ export class SubstitutedTranslation extends VariableUiElement { if (txt === undefined) { return "no tags subs tr" } - const contents = SubstitutedTranslation.EvaluateSpecialComponents(txt, tags) - console.log("Substr has contents", contents) - return new Combine(contents) + return new Combine(SubstitutedTranslation.EvaluateSpecialComponents(txt, tags)) }, [Locale.language]) ) diff --git a/test.ts b/test.ts index d4a6ab22e2..1ee32d4091 100644 --- a/test.ts +++ b/test.ts @@ -1,3 +1,27 @@ -import TestAll from "./test/TestAll"; +import {RadioButton} from "./UI/Input/RadioButton"; +import {FixedInputElement} from "./UI/Input/FixedInputElement"; +import {SubstitutedTranslation} from "./UI/SubstitutedTranslation"; +import {UIEventSource} from "./Logic/UIEventSource"; +import {Translation} from "./UI/i18n/Translation"; +import TagRenderingAnswer from "./UI/Popup/TagRenderingAnswer"; +import TagRenderingConfig from "./Customizations/JSON/TagRenderingConfig"; +import EditableTagRendering from "./UI/Popup/EditableTagRendering"; -new TestAll().testAll(); + +const tagsSource = new UIEventSource({ + id:'id', + name:'name', + surface:'asphalt' +}) + +const config = new TagRenderingConfig({ + render: "Rendering {name} {id} {surface}" +}, null, "test") + +new EditableTagRendering( + tagsSource, + config +).AttachTo("extradiv") + + +window.v = tagsSource \ No newline at end of file diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index b4d07702c9..b1bc0ae103 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -190,12 +190,6 @@ export default class TagSpec extends T{ ] }; - const constr = new TagRenderingConfig(def, undefined, "test"); - const uiEl = new EditableTagRendering(new UIEventSource( - {leisure: "park", "access": "no"}), constr - ); - const rendered = uiEl.ConstructElement().innerHTML; - equal(true, rendered.indexOf("Niet toegankelijk") > 0) } ], [ diff --git a/test/TagQuestion.spec.ts b/test/TagQuestion.spec.ts deleted file mode 100644 index b094986df5..0000000000 --- a/test/TagQuestion.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import T from "./TestHelper"; -import {Utils} from "../Utils"; - -Utils.runningFromConsole = true; -import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion"; -import {UIEventSource} from "../Logic/UIEventSource"; -import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; - -export default class TagQuestionSpec extends T { - constructor() { - super("TagQuestionElement", - [ - ["Freeform has textfield", () => { - const tags = new UIEventSource({ - id: "way/123", - amenity: 'public_bookcases' - }); - const config = new TagRenderingConfig( - { - render: "The name is {name}", - question: "What is the name of this bookcase?", - freeform: { - key: "name", - type: "string" - } - }, undefined, "Testing tag" - ); - const questionElement = new TagRenderingQuestion(tags, config); - const html = questionElement.InnerRenderAsString(); - T.assertContains("What is the name of this bookcase?", html); - T.assertContains(" { - const tags = new UIEventSource({ - id: "way/123", - amenity: 'public_bookcases' - }); - const config = new TagRenderingConfig( - { - render: "The name is {name}", - question: "What is the name of this bookcase?", - freeform: { - key: "name", - type: "string" - }, - mappings: [ - { - "if": "noname=yes", - "then": "This bookcase has no name" - } - ] - }, undefined, "Testing tag" - ); - const questionElement = new TagRenderingQuestion(tags, config); - const html = questionElement.InnerRenderAsString(); - T.assertContains("What is the name of this bookcase?", html); - T.assertContains("This bookcase has no name", html); - T.assertContains(" Date: Mon, 14 Jun 2021 17:42:26 +0200 Subject: [PATCH 08/30] More small fixes to the refactoring --- Logic/Osm/OsmConnection.ts | 2 +- UI/BaseUIElement.ts | 1 - UI/BigComponents/BackgroundSelector.ts | 2 +- UI/BigComponents/FullWelcomePaneWithTabs.ts | 20 +++++++++++++------- UI/BigComponents/LayerSelection.ts | 2 +- UI/Input/DropDown.ts | 5 ++++- UI/LanguagePicker.ts | 2 +- 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 004cefa2a2..c085c5738d 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -67,7 +67,7 @@ export class OsmConnection { this.userDetails.data.dryRun = dryRun; const self =this; this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { - if(self.userDetails.data.loggedIn == false){ + if(self.userDetails.data.loggedIn == false && isLoggedIn == true){ // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do // This means someone attempted to toggle this; so we attempt to login! self.AttemptLogin() diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 125d1ffc41..840814530e 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -43,7 +43,6 @@ export default abstract class BaseUIElement { throw "SEVERE: could not attach UIElement to " + divId; } - console.log("Attaching to ", element) while (element.firstChild) { //The list is LIVE so it will re-index each call element.removeChild(element.firstChild); diff --git a/UI/BigComponents/BackgroundSelector.ts b/UI/BigComponents/BackgroundSelector.ts index 8a92e359d9..3387ba0925 100644 --- a/UI/BigComponents/BackgroundSelector.ts +++ b/UI/BigComponents/BackgroundSelector.ts @@ -25,7 +25,7 @@ export default class BackgroundSelector extends VariableUiElement { if (baseLayers.length <= 1) { return undefined; } - return new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer) + return new DropDown(Translations.t.general.backgroundMap.Clone(), baseLayers, State.state.backgroundLayer) } ) ) diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 63f513488e..918df9cda4 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -27,8 +27,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { "welcome" ,isShown ) } - - private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource) { + + private static ConstructBaseTabs(layoutToUse: LayoutConfig): { header: string | BaseUIElement; content: BaseUIElement }[]{ let welcome: BaseUIElement = new ThemeIntroductionPanel(); if (layoutToUse.id === personal.id) { @@ -55,8 +55,13 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { }); } + return tabs; + } - const tabsWithAboutMc = [...tabs] + private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource) { + + const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse) + const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse)] tabsWithAboutMc.push({ header: Svg.help, content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "
    Version " + Constants.vNumber]) @@ -65,10 +70,11 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { ); return new Toggle( - new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), - new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab), - userDetails.map(userdetails => - userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) + new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), + new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab), + userDetails.map((userdetails: UserDetails) => + userdetails.loggedIn && + userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock) ) } diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index 7810c7b10f..bb82a18a5c 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -43,7 +43,7 @@ export default class LayerSelection extends Combine { const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => { if (location.zoom < layer.layerDef.minzoom) { - return Translations.t.general.layerSelection.zoomInToSeeThisLayer + return Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone() .SetClass("alert") .SetStyle("display: block ruby;width:min-content;") } diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts index dffecae384..91f1d700e0 100644 --- a/UI/Input/DropDown.ts +++ b/UI/Input/DropDown.ts @@ -43,11 +43,14 @@ export class DropDown extends InputElement { el.appendChild(labelHtml) } } + + options = options ?? {} + options.select_class = options.select_class ?? 'bg-indigo-100 p-1 rounded hover:bg-indigo-200' { const select = document.createElement("select") - select.classList.add(...(options?.select_class?.split(" ") ?? [])) + select.classList.add(...(options.select_class.split(" ") ?? [])) for (let i = 0; i < values.length; i++) { const option = document.createElement("option") diff --git a/UI/LanguagePicker.ts b/UI/LanguagePicker.ts index 5616707dd4..0b30baf3ca 100644 --- a/UI/LanguagePicker.ts +++ b/UI/LanguagePicker.ts @@ -16,7 +16,7 @@ export default class LanguagePicker { return new DropDown(label, languages.map(lang => { return {value: lang, shown: lang} } - ), Locale.language, { select_class: 'bg-indigo-100 p-1 rounded hover:bg-indigo-200'}); + ), Locale.language); } From e480c97676ccba6b6fe05c7eec2e5bba962c6a42 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 14 Jun 2021 19:21:33 +0200 Subject: [PATCH 09/30] Fixed part of the special renderings --- Logic/Web/ImgurUploader.ts | 6 +-- UI/Base/TabbedComponent.ts | 11 ++++- UI/BigComponents/LicensePicker.ts | 3 +- UI/Image/ImageCarousel.ts | 29 ++++++------ UI/Image/ImageUploadFlow.ts | 46 +++++++------------ UI/Image/SlideShow.ts | 74 +++++++++++++++++++------------ UI/Input/FileSelectorButton.ts | 58 +++++++++++++----------- UI/Reviews/ReviewForm.ts | 21 +++++---- UI/SpecialVisualizations.ts | 6 +-- css/tabbedComponent.css | 11 ----- test.ts | 38 +++++++++------- 11 files changed, 156 insertions(+), 147 deletions(-) diff --git a/Logic/Web/ImgurUploader.ts b/Logic/Web/ImgurUploader.ts index b21228b5e4..851b9c53f5 100644 --- a/Logic/Web/ImgurUploader.ts +++ b/Logic/Web/ImgurUploader.ts @@ -3,9 +3,9 @@ import {Imgur} from "./Imgur"; export default class ImgurUploader { - public queue: UIEventSource; - public failed: UIEventSource; - public success: UIEventSource + public readonly queue: UIEventSource = new UIEventSource([]); + public readonly failed: UIEventSource = new UIEventSource([]); + public readonly success: UIEventSource = new UIEventSource([]); private readonly _handleSuccessUrl: (string) => void; constructor(handleSuccessUrl: (string) => void) { diff --git a/UI/Base/TabbedComponent.ts b/UI/Base/TabbedComponent.ts index 0244b0e7b5..46c602132e 100644 --- a/UI/Base/TabbedComponent.ts +++ b/UI/Base/TabbedComponent.ts @@ -15,8 +15,17 @@ export class TabbedComponent extends Combine { for (let i = 0; i < elements.length; i++) { let element = elements[i]; const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i)) + openedTabSrc.addCallbackAndRun(selected => { + if(selected === i){ + header.SetClass("tab-active") + header.RemoveClass("tab-non-active") + }else{ + header.SetClass("tab-non-active") + header.RemoveClass("tab-active") + } + }) const content = Translations.W(element.content) - content.SetClass("tab-content") + content.SetClass("relative p-4 w-full inline-block") contentElements.push(content); const tab = header.SetClass("block tab-single-header") tabs.push(tab) diff --git a/UI/BigComponents/LicensePicker.ts b/UI/BigComponents/LicensePicker.ts index 43d2cac1c4..f87d8c775b 100644 --- a/UI/BigComponents/LicensePicker.ts +++ b/UI/BigComponents/LicensePicker.ts @@ -1,6 +1,7 @@ import {DropDown} from "../Input/DropDown"; import Translations from "../i18n/Translations"; import State from "../../State"; +import {UIEventSource} from "../../Logic/UIEventSource"; export default class LicensePicker extends DropDown{ @@ -11,7 +12,7 @@ export default class LicensePicker extends DropDown{ {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") + State.state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource("CC0") ) this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left"); } diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 2528aa32b6..4a29d6a0b4 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -1,4 +1,3 @@ -import {UIElement} from "../UIElement"; import {SlideShow} from "./SlideShow"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; @@ -8,33 +7,35 @@ import {ImgurImage} from "./ImgurImage"; import {MapillaryImage} from "./MapillaryImage"; import BaseUIElement from "../BaseUIElement"; import Img from "../Base/Img"; +import Toggle from "../Input/Toggle"; -export class ImageCarousel extends UIElement{ +export class ImageCarousel extends Toggle { - public readonly slideshow: BaseUIElement; - - constructor(images: UIEventSource<{key: string, url:string}[]>, tags: UIEventSource) { - super(images); - const uiElements = images.map((imageURLS: {key: string, url:string}[]) => { + constructor(images: UIEventSource<{ key: string, url: string }[]>, tags: UIEventSource) { + const uiElements = images.map((imageURLS: { key: string, url: string }[]) => { const uiElements: BaseUIElement[] = []; for (const url of imageURLS) { let image = ImageCarousel.CreateImageElement(url.url) - if(url.key !== undefined){ + if (url.key !== undefined) { image = new Combine([ image, new DeleteImage(url.key, tags).SetClass("delete-image-marker absolute top-0 left-0 pl-3") ]).SetClass("relative"); } - image - .SetClass("w-full block") + image + .SetClass("w-full block") + .SetStyle("min-width: 50px; background: grey;") uiElements.push(image); } return uiElements; }); - this.slideshow = new SlideShow(uiElements); + super( + new SlideShow(uiElements).SetClass("w-full"), + undefined, + uiElements.map(els => els.length > 0) + ) this.SetClass("block w-full"); - this.slideshow.SetClass("w-full"); } /*** @@ -57,8 +58,4 @@ export class ImageCarousel extends UIElement{ return new Img(url); } } - - InnerRender() { - return this.slideshow; - } } \ No newline at end of file diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 1cdae491a3..8de6ad4f90 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -1,5 +1,4 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import State from "../../State"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; @@ -13,22 +12,9 @@ import ImgurUploader from "../../Logic/Web/ImgurUploader"; import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; -export class ImageUploadFlow extends UIElement { - - private readonly _element: BaseUIElement; - - - private readonly _tags: UIEventSource; - private readonly _selectedLicence: UIEventSource; - - - private readonly _imagePrefix: string; +export class ImageUploadFlow extends Toggle { constructor(tagsSource: UIEventSource, imagePrefix: string = "image") { - super(State.state.osmConnection.userDetails); - this._imagePrefix = imagePrefix; - - const uploader = new ImgurUploader(url => { // A file was uploaded - we add it to the tags of the object @@ -50,9 +36,10 @@ export class ImageUploadFlow extends UIElement { const t = Translations.t.image; const label = new Combine([ - Svg.camera_plus_svg().SetStyle("width: 36px;height: 36px;padding: 0.1em;margin-top: 5px;border-radius: 0;float: left;display:block"), - Translations.t.image.addPicture - ]).SetClass("image-upload-flow-button") + Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1"), + Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3") + ]).SetClass("p-2 border-4 border-black rounded-full text-4xl font-bold h-full align-middle w-full flex justify-center") + const fileSelector = new FileSelectorButton(label) fileSelector.GetValue().addCallback(filelist => { if (filelist === undefined) { @@ -60,13 +47,13 @@ export class ImageUploadFlow extends UIElement { } console.log("Received images from the user, starting upload") - const license = this._selectedLicence.data ?? "CC0" + const license = licensePicker.GetValue().data ?? "CC0" - const tags = this._tags.data; + const tags = tagsSource.data; - const layout = State.state.layoutToUse.data + const layout = State.state?.layoutToUse?.data let matchingLayer: LayerConfig = undefined - for (const layer of layout.layers) { + for (const layer of layout?.layers ?? []) { if (layer.source.osmTags.matchesProperties(tags)) { matchingLayer = layer; break; @@ -90,30 +77,27 @@ export class ImageUploadFlow extends UIElement { const uploadFlow: BaseUIElement = new Combine([ fileSelector, - Translations.t.image.respectPrivacy.SetStyle("font-size:small;"), + Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"), licensePicker, uploadStateUi - ]).SetClass("image-upload-flow") - .SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;"); + ]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center") const pleaseLoginButton = t.pleaseLogin.Clone() .onClick(() => State.state.osmConnection.AttemptLogin()) .SetClass("login-button-friendly"); - this._element = new Toggle( + super( new Toggle( /*We can show the actual upload button!*/ uploadFlow, /* User not logged in*/ pleaseLoginButton, - State.state.osmConnection.userDetails.map(userinfo => userinfo.loggedIn) + State.state?.osmConnection?.isLoggedIn ), - undefined /* Nothing as the user badge is disabled*/, State.state.featureSwitchUserbadge + undefined /* Nothing as the user badge is disabled*/, + State.state.featureSwitchUserbadge ) } - protected InnerRender(): string | BaseUIElement { - return this._element; - } } \ No newline at end of file diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 8cf7b07194..692bb87488 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -1,41 +1,59 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; +import $ from "jquery" export class SlideShow extends BaseUIElement { - private readonly _element: HTMLElement; - - constructor( - embeddedElements: UIEventSource) { - super() - - const el = document.createElement("div") - this._element = el; - - el.classList.add("slick-carousel") - require("slick-carousel") - // @ts-ignore - el.slick({ - autoplay: true, - arrows: true, - dots: true, - lazyLoad: 'progressive', - variableWidth: true, - centerMode: true, - centerPadding: "60px", - adaptive: true - }); - embeddedElements.addCallbackAndRun(elements => { - for (const element of elements ?? []) { - element.SetClass("slick-carousel-content") - } - }); + private readonly embeddedElements: UIEventSource; + constructor(embeddedElements: UIEventSource) { + super() + this.embeddedElements = embeddedElements; } protected InnerConstructElement(): HTMLElement { - return this._element; + const el = document.createElement("div") + el.classList.add("slic-carousel") + + el.onchange = () => { + console.log("Parent is now ", el.parentElement) + } + + const mutationObserver = new MutationObserver(mutations => { + console.log("Mutations are: ", mutations) + + + mutationObserver.disconnect() + require("slick-carousel") + // @ts-ignore + el.slick({ + autoplay: true, + arrows: true, + dots: true, + lazyLoad: 'progressive', + variableWidth: true, + centerMode: true, + centerPadding: "60px", + adaptive: true + }); + }) + + mutationObserver.observe(el, { + childList: true, + characterData: true, + subtree: true + }) + + + this.embeddedElements.addCallbackAndRun(elements => { + for (const element of elements ?? []) { + element.SetClass("slick-carousel-content") + el.appendChild(element.ConstructElement()) + } + }); + + return el; } } \ No newline at end of file diff --git a/UI/Input/FileSelectorButton.ts b/UI/Input/FileSelectorButton.ts index 9964790694..c6e1cf1f8b 100644 --- a/UI/Input/FileSelectorButton.ts +++ b/UI/Input/FileSelectorButton.ts @@ -1,9 +1,10 @@ import BaseUIElement from "../BaseUIElement"; -import {InputElement} from "../Input/InputElement"; +import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; export default class FileSelectorButton extends InputElement { + private static _nextid; IsSelected: UIEventSource; private readonly _value = new UIEventSource(undefined); private readonly _label: BaseUIElement; @@ -13,6 +14,8 @@ export default class FileSelectorButton extends InputElement { super(); this._label = label; this._acceptType = acceptType; + this.SetClass("block cursor-pointer") + label.SetClass("cursor-pointer") } GetValue(): UIEventSource { @@ -26,36 +29,37 @@ export default class FileSelectorButton extends InputElement { protected InnerConstructElement(): HTMLElement { const self = this; const el = document.createElement("form") - { - const label = document.createElement("label") - label.appendChild(this._label.ConstructElement()) - el.appendChild(label) - } - { - const actualInputElement = document.createElement("input"); - actualInputElement.style.cssText = "display:none"; - actualInputElement.type = "file"; - actualInputElement.accept = this._acceptType; - actualInputElement.name = "picField"; - actualInputElement.multiple = true; + const label = document.createElement("label") + label.appendChild(this._label.ConstructElement()) + el.appendChild(label) - actualInputElement.onchange = () => { - if (actualInputElement.files !== null) { - self._value.setData(actualInputElement.files) - } + const actualInputElement = document.createElement("input"); + actualInputElement.style.cssText = "display:none"; + actualInputElement.type = "file"; + actualInputElement.accept = this._acceptType; + actualInputElement.name = "picField"; + actualInputElement.multiple = true; + actualInputElement.id = "fileselector" + FileSelectorButton._nextid; + FileSelectorButton._nextid++; + + label.htmlFor = actualInputElement.id; + + actualInputElement.onchange = () => { + if (actualInputElement.files !== null) { + self._value.setData(actualInputElement.files) } - - el.addEventListener('submit', e => { - if (actualInputElement.files !== null) { - self._value.setData(actualInputElement.files) - } - e.preventDefault() - }) - - el.appendChild(actualInputElement) } - return undefined; + el.addEventListener('submit', e => { + if (actualInputElement.files !== null) { + self._value.setData(actualInputElement.files) + } + e.preventDefault() + }) + + el.appendChild(actualInputElement) + + return el; } diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index fb0ecc32f4..1f8f743d5a 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -8,9 +8,10 @@ import Svg from "../../Svg"; import {VariableUiElement} from "../Base/VariableUIElement"; import {SaveButton} from "../Popup/SaveButton"; import CheckBoxes from "../Input/Checkboxes"; -import UserDetails from "../../Logic/Osm/OsmConnection"; +import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; import BaseUIElement from "../BaseUIElement"; import Toggle from "../Input/Toggle"; +import State from "../../State"; export default class ReviewForm extends InputElement { @@ -19,19 +20,19 @@ export default class ReviewForm extends InputElement { private readonly _stars: BaseUIElement; private _saveButton: BaseUIElement; private readonly _isAffiliated: BaseUIElement; - private userDetails: UIEventSource; private readonly _postingAs: BaseUIElement; + private readonly _osmConnection: OsmConnection; - constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), userDetails: UIEventSource) { + constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), osmConnection: OsmConnection) { super(); - this.userDetails = userDetails; + this._osmConnection = osmConnection; const t = Translations.t.reviews; this._value = new UIEventSource({ made_by_user: new UIEventSource(true), rating: undefined, comment: undefined, - author: userDetails.data.name, + author: osmConnection.userDetails.data.name, affiliated: false, date: new Date() }); @@ -48,7 +49,7 @@ export default class ReviewForm extends InputElement { const self = this; this._postingAs = - new Combine([t.posting_as, new VariableUiElement(userDetails.map((ud: UserDetails) => ud.name)).SetClass("review-author")]) + new Combine([t.posting_as, new VariableUiElement(osmConnection.userDetails.map((ud: UserDetails) => ud.name)).SetClass("review-author")]) .SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-left: auto;") this._saveButton = new SaveButton(this._value.map(r => self.IsValid(r)), undefined) @@ -100,10 +101,12 @@ export default class ReviewForm extends InputElement { Translations.t.reviews.tos.SetClass("subtle") ]) .SetClass("review-form") + + const connection = this._osmConnection; + const login = Translations.t.reviews.plz_login.Clone().onClick(() => connection.AttemptLogin()) - - return new Toggle(form, Translations.t.reviews.plz_login.Clone(), - this.userDetails.map(userdetails => userdetails.loggedIn)).ToggleOnClick() + return new Toggle(form,login , + connection.isLoggedIn) .ConstructElement() } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 1c46e4fc56..2bcae10271 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -107,7 +107,7 @@ export default class SpecialVisualizations { state.mangroveIdentity, state.osmConnection._dryRun ); - const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection.userDetails); + const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection); return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form); } }, @@ -160,7 +160,7 @@ export default class SpecialVisualizations { ], constr: (state: State, tagSource: UIEventSource, args) => { if (window.navigator.share) { - const title = state.layoutToUse.data.title.txt; + const title = state?.layoutToUse?.data?.title?.txt ?? "MapComplete"; let name = tagSource.data.name; if (name) { name = `${name} (${title})` @@ -174,7 +174,7 @@ export default class SpecialVisualizations { return new ShareButton(Svg.share_ui(), { title: name, url: url, - text: state.layoutToUse.data.shortDescription.txt + text: state?.layoutToUse?.data?.shortDescription?.txt ?? "MapComplete" }) } else { return new FixedUiElement("") diff --git a/css/tabbedComponent.css b/css/tabbedComponent.css index d895fc9aee..a4b7bc1821 100644 --- a/css/tabbedComponent.css +++ b/css/tabbedComponent.css @@ -30,17 +30,6 @@ } -.tab-content { - z-index: 5002; - background-color: var(--background-color); - color: var(--foreground-color); - position: relative; - padding: 1em; - display: inline-block; - width: 100%; - box-sizing: border-box; -} - .tab-single-header { border-top-left-radius: 1em; border-top-right-radius: 1em; diff --git a/test.ts b/test.ts index 1ee32d4091..46333efa38 100644 --- a/test.ts +++ b/test.ts @@ -1,27 +1,31 @@ -import {RadioButton} from "./UI/Input/RadioButton"; -import {FixedInputElement} from "./UI/Input/FixedInputElement"; -import {SubstitutedTranslation} from "./UI/SubstitutedTranslation"; import {UIEventSource} from "./Logic/UIEventSource"; -import {Translation} from "./UI/i18n/Translation"; -import TagRenderingAnswer from "./UI/Popup/TagRenderingAnswer"; -import TagRenderingConfig from "./Customizations/JSON/TagRenderingConfig"; -import EditableTagRendering from "./UI/Popup/EditableTagRendering"; +import SpecialVisualizations from "./UI/SpecialVisualizations"; +import State from "./State"; +import Combine from "./UI/Base/Combine"; +import {FixedUiElement} from "./UI/Base/FixedUiElement"; const tagsSource = new UIEventSource({ id:'id', name:'name', - surface:'asphalt' + surface:'asphalt', + image: "https://i.imgur.com/kX3rl3v.jpg", + "image:1": "https://i.imgur.com/kX3rl3v.jpg", + _country:"be", + // "opening_hours":"mo-fr 09:00-18:00" }) -const config = new TagRenderingConfig({ - render: "Rendering {name} {id} {surface}" -}, null, "test") +const state = new State(undefined) +State.state = state -new EditableTagRendering( - tagsSource, - config -).AttachTo("extradiv") +const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { + try{ - -window.v = tagsSource \ No newline at end of file + return new Combine([spec.funcName, spec.constr(state, tagsSource, spec.args.map(a => a.defaultValue ?? "")).SetClass("block")]) + .SetClass("flex flex-col border border-black p-2 m-2"); + }catch(e){ + console.error(e) + return new FixedUiElement("Could not construct "+spec.funcName+" due to "+e).SetClass("alert") + } +}) +new Combine(allSpecials).AttachTo("maindiv") \ No newline at end of file From 8e72b70742cad39802c7957cf15608ae89d1339c Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 15 Jun 2021 00:28:59 +0200 Subject: [PATCH 10/30] Fix deployment, fix documentation generation, add a small markdown generator --- Docs/CalculatedTags.md | 150 +++++++++++++----- Docs/SpecialRenderings.md | 62 +------- Docs/TagInfo/mapcomplete_fietsstraten.json | 24 +-- Docs/URL_Parameters.md | 172 +++++++++++---------- InitUiElements.ts | 12 +- Logic/Actors/GeoLocationHandler.ts | 1 - Logic/ExtraFunction.ts | 94 ++++++----- Logic/SimpleMetaTagger.ts | 51 +++--- Logic/Web/QueryParameters.ts | 100 +++++++----- State.ts | 21 ++- UI/Base/Combine.ts | 4 + UI/Base/FixedUiElement.ts | 4 + UI/Base/List.ts | 9 ++ UI/Base/Title.ts | 37 +++++ UI/BaseUIElement.ts | 20 ++- UI/BigComponents/PersonalLayersPanel.ts | 1 - UI/SpecialVisualizations.ts | 1 - UI/UIElement.ts | 21 --- Utils.ts | 1 - assets/contributors.json | 2 +- css/imageUploadFlow.css | 23 --- index.html | 1 - index.manifest | 2 - index.ts | 5 - package.json | 1 - scripts/generateDocs.ts | 50 ++++-- scripts/generateLayouts.ts | 8 +- 27 files changed, 478 insertions(+), 399 deletions(-) create mode 100644 UI/Base/Title.ts delete mode 100644 css/imageUploadFlow.css diff --git a/Docs/CalculatedTags.md b/Docs/CalculatedTags.md index 6347c09a85..f9e2476748 100644 --- a/Docs/CalculatedTags.md +++ b/Docs/CalculatedTags.md @@ -1,5 +1,8 @@ -Metatags --------- + + Metatags +========== + + Metatags are extra tags available, in order to display more data or to give better questions. @@ -7,85 +10,154 @@ The are calculated automatically on every feature when the data arrives in the w **Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object -### \_lat, \_lon + + Metatags calculated by MapComplete +------------------------------------ + + + +The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme + + +### _lat, _lon + + The latitude and longitude of the point (or centerpoint in the case of a way/area) -### \_surface, \_surface:ha + +### _surface, _surface:ha + + The surface area of the feature, in square meters and in hectare. Not set on points and ways -### \_length, \_length:km -The total length of a feature in meters (and in kilometers, rounded to one decimal for '\_length:km'). For a surface, the length of the perimeter +### _length, _length:km + + + +The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter + + +### _country + -### \_country The country code of the property (with latlon2country) -### \_isOpen, \_isOpen:description -If 'opening\_hours' is present, it will add the current state of the feature (being 'yes' or 'no') +### _isOpen, _isOpen:description + + + +If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no') + + +### _width:needed, _width:needed:no_pedestrians, _width:difference + -### \_width:needed, \_width:needed:no\_pedestrians, \_width:difference Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present -### \_direction:numerical, \_direction:leftright -\_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). \_direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map +### _direction:numerical, _direction:leftright + + + +_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map + + +### _now:date, _now:datetime, _loaded:date, _loaded:_datetime + -### \_now:date, \_now:datetime, \_loaded:date, \_loaded:\_datetime Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely -### \_last\_edit:contributor, \_last\_edit:contributor:uid, \_last\_edit:changeset, \_last\_edit:timestamp, \_version\_number + +### _last_edit:contributor, _last_edit:contributor:uid, _last_edit:changeset, _last_edit:timestamp, _version_number + + Information about the last edit of this object. -Calculating tags with Javascript --------------------------------- -In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. **lat**, **lon**, **\_country**), as detailed above. + Calculating tags with Javascript +---------------------------------- + + + +In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above. It is also possible to calculate your own tags - but this requires some javascript knowledge. + + Before proceeding, some warnings: -* DO NOT DO THIS AS BEGINNER -* **Only do this if all other techniques fail**. This should _not_ be done to create a rendering effect, only to calculate a specific value -* **THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES**. As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs. -In the layer object, add a field **calculatedTags**, e.g.: -"calculatedTags": \[ "\_someKey=javascript-expression", "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator", "\_distanceCloserThen3Km=feat.distanceTo( some\_lon, some\_lat) < 3 ? 'yes' : 'no'" \] + - DO NOT DO THIS AS BEGINNER + - **Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to calculate a specific value + - **THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs. -The above code will be executed for every feature in the layer. The feature is accessible as **feat** and is an amended geojson object: - **area** contains the surface area (in square meters) of the object - **lat** and **lon** contain the latitude and longitude Some advanced functions are available on **feat** as well: -* distanceTo -* overlapWith -* closest -* memberships +To enable this feature, add a field `calculatedTags` in the layer object, e.g.: -### distanceTo +```` -Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object +"calculatedTags": [ -* longitude -* latitude + "_someKey=javascript-expression", -### overlapWith + "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator", -Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in mยฒ) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point + "_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'" -* ...layerIds - one or more layer ids of the layer from which every feature is checked for overlap) + ] -### closest +```` -Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. -* list of features -### memberships +The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object: + + + + - `area` contains the surface area (in square meters) of the object + - `lat` and `lon` contain the latitude and longitude + + +Some advanced functions are available on **feat** as well: + + - distanceTo + - overlapWith + - closest + - memberships + +### distanceTo + + Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object + + 0. longitude + 1. latitude + +### overlapWith + + Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in mยฒ) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point + + 0. ...layerIds - one or more layer ids of the layer from which every feature is checked for overlap) + +### closest + + Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. + + 0. list of features + +### memberships + + Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. + +For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')` + -Gives a list of `{role: string, relation: Relation}`\-objects, containing all the relations that this feature is part of. For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')` \ No newline at end of file diff --git a/Docs/SpecialRenderings.md b/Docs/SpecialRenderings.md index a0b77e3238..6de976f173 100644 --- a/Docs/SpecialRenderings.md +++ b/Docs/SpecialRenderings.md @@ -1,61 +1 @@ -### Special tag renderings - -In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.General usage is **{func\_name()}** or **{func\_name(arg, someotherarg)}**. Note that you _do not_ need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args - -### all\_tags - -Prints all key-value pairs of the object - used for debugging - -**Example usage:** {all\_tags()} - -### image\_carousel - -Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links) - -1. **image key/prefix**: The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Default: image -2. **smart search**: Also include images given via 'Wikidata', 'wikimedia\_commons' and 'mapillary Default: true - -**Example usage:** {image\_carousel(image,true)} - -### image\_upload - -Creates a button where a user can upload an image to IMGUR - -1. **image-key**: Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added) Default: image - -**Example usage:** {image\_upload(image)} - -### reviews - -Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten - -1. **subjectKey**: The key to use to determine the subject. If specified, the subject will be **tags\[subjectKey\]** Default: name -2. **fallback**: The identifier to use, if _tags\[subjectKey\]_ as specified above is not available. This is effectively a fallback value - -**Example usage:** **{reviews()} **for a vanilla review, **{reviews(name, play\_forest)}** to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play\_forest' is used**** - -### ****opening\_hours\_table**** - -****Creates an opening-hours table. Usage: {opening\_hours\_table(opening\_hours)} to create a table of the tag 'opening\_hours'. - -1. **key**: The tagkey from which the table is constructed. Default: opening\_hours - -**Example usage:** {opening\_hours\_table(opening\_hours)} - -### live - -Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json\[x\]\[y\]\[z\], other: json\[a\]\[b\]\[c\] out of it and will return 'other' or 'json\[a\]\[b\]\[c\]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed\_value)} - -1. **Url**: The URL to load -2. **Shorthands**: A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ; -3. **path**: The path (or shorthand) that should be returned - -**Example usage:** {live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour\_cnt;day:data.day\_cnt;year:data.year\_cnt,hour)} - -### share\_link - -Creates a link that (attempts to) open the native 'share'-screen - -1. **url**: The url to share (default: current URL) - -**Example usage:** {share\_link()} to share the current page, {share\_link()} to share the given url**** \ No newline at end of file +

    Special tag renderings

    In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's. General usage is {func_name()} or {func_name(arg, someotherarg)}. Note that you do not need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args

    all_tags

    Prints all key-value pairs of the object - used for debugging
    Example usage: {all_tags()}

    image_carousel

    Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
    1. image key/prefix: The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Default: image
    2. smart search: Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary Default: true
    Example usage: {image_carousel(image,true)}

    image_upload

    Creates a button where a user can upload an image to IMGUR
    1. image-key: Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added) Default: image
    Example usage: {image_upload(image)}

    reviews

    Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten
    1. subjectKey: The key to use to determine the subject. If specified, the subject will be tags[subjectKey] Default: name
    2. fallback: The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value
    Example usage: {reviews()} for a vanilla review, {reviews(name, play_forest)} to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used

    opening_hours_table

    Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.
    1. key: The tagkey from which the table is constructed. Default: opening_hours
    Example usage: {opening_hours_table(opening_hours)}

    live

    Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}
    1. Url: The URL to load
    2. Shorthands: A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ;
    3. path: The path (or shorthand) that should be returned
    Example usage: {live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}

    share_link

    Creates a link that (attempts to) open the native 'share'-screen
    1. url: The url to share (default: current URL)
    Example usage: {share_link()} to share the current page, {share_link()} to share the given url \ No newline at end of file diff --git a/Docs/TagInfo/mapcomplete_fietsstraten.json b/Docs/TagInfo/mapcomplete_fietsstraten.json index e8b411615b..b5b234baf9 100644 --- a/Docs/TagInfo/mapcomplete_fietsstraten.json +++ b/Docs/TagInfo/mapcomplete_fietsstraten.json @@ -33,22 +33,22 @@ }, { "key": "cyclestreet", - "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "yes" }, { "key": "maxspeed", - "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "30" }, { "key": "overtaking:motor_vehicle", - "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "no" }, { "key": "proposed:cyclestreet", - "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", "value": "" }, { @@ -113,22 +113,22 @@ }, { "key": "cyclestreet", - "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "yes" }, { "key": "maxspeed", - "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "30" }, { "key": "overtaking:motor_vehicle", - "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "no" }, { "key": "proposed:cyclestreet", - "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", "value": "" }, { @@ -203,22 +203,22 @@ }, { "key": "cyclestreet", - "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "yes" }, { "key": "maxspeed", - "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "30" }, { "key": "overtaking:motor_vehicle", - "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", "value": "no" }, { "key": "proposed:cyclestreet", - "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a maxspeeld of 30km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", "value": "" }, { diff --git a/Docs/URL_Parameters.md b/Docs/URL_Parameters.md index fa0e0aaead..3894dfb141 100644 --- a/Docs/URL_Parameters.md +++ b/Docs/URL_Parameters.md @@ -1,3 +1,4 @@ + URL-parameters and URL-hash ============================ @@ -18,125 +19,128 @@ the URL-parameters are stated in the part between the `?` and the `#`. There are Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case. -custom-css (broken) ------------- -If specified, the custom css from the given link will be loaded additionaly - -test ------- -If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org -The default value is _false_ - -layout --------- -The layout to load into MapComplete - -userlayout ------------- -If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: - -- The hash of the URL contains a base64-encoded .json-file containing the theme definition -- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator -- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme - The default value is _false_ - -layer-control-toggle + layer-control-toggle ---------------------- -Whether or not the layer control is shown -The default value is _false_ -tab + Whether or not the layer control is shown The default value is _false_ + + + tab ----- -The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) -The default value is _0_ -z + The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_ + + + z --- -The initial/current zoom level -The default value is set by the theme -lat + The initial/current zoom level The default value is _0_ + + + lat ----- -The initial/current latitude -The default value is set by the theme -lon + The initial/current latitude The default value is _0_ + + + lon ----- -The initial/current longitude of the app -The default value is set by the theme -fs-userbadge + The initial/current longitude of the app The default value is _0_ + + + fs-userbadge -------------- -Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. -The default value is _true_ -fs-search + Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_ + + + fs-search ----------- -Disables/Enables the search bar -The default value is _true_ -fs-layers + Disables/Enables the search bar The default value is _true_ + + + fs-layers ----------- -Disables/Enables the layer control -The default value is _true_ -fs-add-new + Disables/Enables the layer control The default value is _true_ + + + fs-add-new ------------ -Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) -The default value is _true_ -fs-welcome-message + Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_ + + + fs-welcome-message -------------------- -Disables/enables the help menu or welcome message -The default value is _true_ -fs-iframe + Disables/enables the help menu or welcome message The default value is _true_ + + + fs-iframe ----------- -Disables/Enables the iframe-popup -The default value is _false_ -fs-more-quests + Disables/Enables the iframe-popup The default value is _false_ + + + fs-more-quests ---------------- -Disables/Enables the 'More Quests'-tab in the welcome message -The default value is _true_ -fs-share-screen + Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_ + + + fs-share-screen ----------------- -Disables/Enables the 'Share-screen'-tab in the welcome message -The default value is _true_ -fs-geolocation + Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_ + + + fs-geolocation ---------------- -Disables/Enables the geolocation button -The default value is _true_ -fs-all-questions + Disables/Enables the geolocation button The default value is _true_ + + + fs-all-questions ------------------ -Always show all questions -The default value is _false_ -debug + Always show all questions The default value is _false_ + + + test +------ + + If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_ + + + debug ------- -If true, shows some extra debugging help such as all the available tags on every object -The default value is _false_ -backend + If true, shows some extra debugging help such as all the available tags on every object The default value is _false_ + + + backend --------- -The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using osm-test -The default value is _osm_ -oauth_token -------------- -Used to complete the login -No default value set + The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_ -background + + custom-css ------------ -The id of the background layer to start with -The default value is _OSM_ (overridden by the theme) -layer- --------------- -Wether or not layer with layer-id is shown -The default value is _true_ + If specified, the custom css from the given link will be loaded additionaly The default value is __ + + + background +------------ + + The id of the background layer to start with The default value is _osm_ + + + layer- +------------------ + + Wether or not the layer with id is shown The default value is _true_ \ No newline at end of file diff --git a/InitUiElements.ts b/InitUiElements.ts index 7858dba2a8..75b5824a4d 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -21,7 +21,6 @@ import * as L from "leaflet"; import Img from "./UI/Base/Img"; import UserDetails from "./Logic/Osm/OsmConnection"; import Attribution from "./UI/BigComponents/Attribution"; -import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; import LayerResetter from "./Logic/Actors/LayerResetter"; import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; import LayerControlPanel from "./UI/BigComponents/LayerControlPanel"; @@ -40,6 +39,7 @@ import ContributorCount from "./Logic/ContributorCount"; import FeatureSource from "./Logic/FeatureSource/FeatureSource"; import AllKnownLayers from "./Customizations/AllKnownLayers"; import LayerConfig from "./Customizations/JSON/LayerConfig"; +import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; export class InitUiElements { @@ -348,9 +348,8 @@ export class InitUiElements { private static InitBaseMap() { State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers; - State.state.backgroundLayer = QueryParameters.GetQueryParameter("background", - State.state.layoutToUse.data.defaultBackgroundId ?? AvailableBaseLayers.osmCarto.id, - "The id of the background layer to start with") + + State.state.backgroundLayer = State.state.backgroundLayerId .map((selectedId: string) => { const available = State.state.availableBackgroundLayers.data; for (const layer of available) { @@ -359,9 +358,8 @@ export class InitUiElements { } } return AvailableBaseLayers.osmCarto; - }, [], layer => layer.id); - - + }, [State.state.availableBackgroundLayers], layer => layer.id); + new LayerResetter( State.state.backgroundLayer, State.state.locationControl, State.state.availableBackgroundLayers, State.state.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId)); diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 64545ee906..740fc77ce3 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -76,7 +76,6 @@ export default class GeoLocationHandler extends UIElement { }, [this._hasLocation]) currentPointer.addCallbackAndRun(pointerClass => { self.SetClass(pointerClass); - self.Update() }) this._element = new VariableUiElement( this._hasLocation.map(hasLocation => { diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts index 51f4ff9f93..b94f925d9c 100644 --- a/Logic/ExtraFunction.ts +++ b/Logic/ExtraFunction.ts @@ -1,46 +1,48 @@ import {GeoOperations} from "./GeoOperations"; -import {UIElement} from "../UI/UIElement"; import Combine from "../UI/Base/Combine"; import {Relation} from "./Osm/ExtractRelations"; import State from "../State"; import {Utils} from "../Utils"; +import BaseUIElement from "../UI/BaseUIElement"; +import List from "../UI/Base/List"; +import Title from "../UI/Base/Title"; export class ExtraFunction { - static readonly intro = `

    Calculating tags with Javascript

    + static readonly intro = new Combine([ + new Title("Calculating tags with Javascript", 2), + "In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above.", + "It is also possible to calculate your own tags - but this requires some javascript knowledge.", + "", + "Before proceeding, some warnings:", + new List([ + "DO NOT DO THIS AS BEGINNER", + "**Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to calculate a specific value", + "**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs." + ]), + "To enable this feature, add a field `calculatedTags` in the layer object, e.g.:", + "````", + "\"calculatedTags\": [", + " \"_someKey=javascript-expression\",", + " \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",", + " \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ", + " ]", + "````", + "", + "The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:", -

    In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. lat, lon, _country), as detailed above.

    + new List([ + "`area` contains the surface area (in square meters) of the object", + "`lat` and `lon` contain the latitude and longitude" + ]), + "Some advanced functions are available on **feat** as well:" + ]).SetClass("flex-col").AsMarkdown(); -

    It is also possible to calculate your own tags - but this requires some javascript knowledge.

    -Before proceeding, some warnings: - -
      -
    • DO NOT DO THIS AS BEGINNER
    • -
    • Only do this if all other techniques fail. This should not be done to create a rendering effect, only to calculate a specific value
    • -
    • THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES. As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs.
    • -
    -In the layer object, add a field calculatedTags, e.g.: - -
    - "calculatedTags": [ - "_someKey=javascript-expression", - "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator", - "_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'" - ] -
    - -The above code will be executed for every feature in the layer. The feature is accessible as feat and is an amended geojson object: -- area contains the surface area (in square meters) of the object -- lat and lon contain the latitude and longitude - -Some advanced functions are available on feat as well: - -` private static readonly OverlapFunc = new ExtraFunction( "overlapWith", - "Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is { feat: GeoJSONFeature, overlap: number}[] where overlap is the overlapping surface are (in mยฒ) for areas, the overlapping length (in meter) if the current feature is a line or undefined if the current feature is a point", + "Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point are given. The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in mยฒ) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point", ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"], (params, feat) => { return (...layerIds: string[]) => { @@ -72,7 +74,7 @@ Some advanced functions are available on feat as well: if (typeof arg0 === "string") { // This is an identifier const feature = State.state.allElements.ContainingFeatures.get(arg0); - if(feature === undefined){ + if (feature === undefined) { return undefined; } arg0 = feature; @@ -138,9 +140,9 @@ Some advanced functions are available on feat as well: private static readonly Memberships = new ExtraFunction( "memberships", - "Gives a list of {role: string, relation: Relation}-objects, containing all the relations that this feature is part of. " + + "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " + "\n\n" + - "For example: _part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')", + "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`", [], (params, _) => { return () => params.relations ?? []; @@ -167,25 +169,19 @@ Some advanced functions are available on feat as well: } } - public static HelpText(): UIElement { + public static HelpText(): BaseUIElement { + + const elems = [] + for (const func of ExtraFunction.allFuncs) { + elems.push(new Title(func._name, 3), + func._doc, + new List(func._args, true)) + } + return new Combine([ ExtraFunction.intro, - "
      ", - ...ExtraFunction.allFuncs.map(func => - new Combine([ - "
    • ", func._name, "
    • " - ]) - ), - "
    ", - ...ExtraFunction.allFuncs.map(func => - new Combine([ - "

    " + func._name + "

    ", - func._doc, - "
      ", - ...func._args.map(arg => "
    • " + arg + "
    • "), - "
    " - ]) - ) + new List(ExtraFunction.allFuncs.map(func => func._name)), + ...elems ]); } diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index df40a91954..4d13af7247 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -5,13 +5,15 @@ import {Tag} from "./Tags/Tag"; import {Or} from "./Tags/Or"; import {Utils} from "../Utils"; import opening_hours from "opening_hours"; -import {UIElement} from "../UI/UIElement"; import Combine from "../UI/Base/Combine"; +import BaseUIElement from "../UI/BaseUIElement"; +import Title from "../UI/Base/Title"; +import {FixedUiElement} from "../UI/Base/FixedUiElement"; const cardinalDirections = { - N: 0, NNE: 22.5, NE: 45, ENE: 67.5, - E: 90, ESE: 112.5, SE: 135, SSE: 157.5, + N: 0, NNE: 22.5, NE: 45, ENE: 67.5, + E: 90, ESE: 112.5, SE: 135, SSE: 157.5, S: 180, SSW: 202.5, SW: 225, WSW: 247.5, W: 270, WNW: 292.5, NW: 315, NNW: 337.5 } @@ -31,20 +33,20 @@ export default class SimpleMetaTagger { (feature) => {/*Note: also handled by 'UpdateTagsFromOsmAPI'*/ const tgs = feature.properties; - - function move(src: string, target: string){ - if(tgs[src] === undefined){ + + function move(src: string, target: string) { + if (tgs[src] === undefined) { return; } tgs[target] = tgs[src] delete tgs[src] } - - move("user","_last_edit:contributor") - move("uid","_last_edit:contributor:uid") - move("changeset","_last_edit:changeset") - move("timestamp","_last_edit:timestamp") - move("version","_version_number") + + move("user", "_last_edit:contributor") + move("uid", "_last_edit:contributor:uid") + move("changeset", "_last_edit:changeset") + move("timestamp", "_last_edit:timestamp") + move("version", "_version_number") } ) private static latlon = new SimpleMetaTagger({ @@ -375,28 +377,27 @@ export default class SimpleMetaTagger { SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback) } - static HelpText(): UIElement { - const subElements: UIElement[] = [ + static HelpText(): BaseUIElement { + const subElements: (string | BaseUIElement)[] = [ new Combine([ - "

    Metatags

    ", - "

    Metatags are extra tags available, in order to display more data or to give better questions.

    ", - "

    The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.

    ", - "

    Hint: when using metatags, add the query parameter debug=true to the URL. This will include a box in the popup for features which shows all the properties of the object

    " - ]) - + new Title("Metatags", 1), + "Metatags are extra tags available, in order to display more data or to give better questions.", + "The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.", + "**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object" + ]).SetClass("flex-col") ]; + subElements.push(new Title("Metatags calculated by MapComplete", 2)) + subElements.push(new FixedUiElement("The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme")) for (const metatag of SimpleMetaTagger.metatags) { subElements.push( - new Combine([ - "

    ", metatag.keys.join(", "), "

    ", - metatag.doc] - ) + new Title(metatag.keys.join(", "), 3), + metatag.doc ) } - return new Combine(subElements) + return new Combine(subElements).SetClass("flex-col") } addMetaTags(features: { feature: any, freshness: Date }[]) { diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index d08df0fdf0..a7bca6e8d9 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -3,6 +3,9 @@ */ import {UIEventSource} from "../UIEventSource"; import Hash from "./Hash"; +import {Utils} from "../../Utils"; +import Title from "../../UI/Base/Title"; +import Combine from "../../UI/Base/Combine"; export class QueryParameters { @@ -12,6 +15,58 @@ export class QueryParameters { private static defaults = {} private static documentation = {} + private static QueryParamDocsIntro = "\n" + + "URL-parameters and URL-hash\n" + + "============================\n" + + "\n" + + "This document gives an overview of which URL-parameters can be used to influence MapComplete.\n" + + "\n" + + "What is a URL parameter?\n" + + "------------------------\n" + + "\n" + + "URL-parameters are extra parts of the URL used to set the state.\n" + + "\n" + + "For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`,\n" + + "the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all seperated by `&`, namely:\n" + + "\n" + + "- The url-parameter `lat` is `51.0` in this instance\n" + + "- The url-parameter `lon` is `4.3` in this instance\n" + + "- The url-parameter `z` is `5` in this instance\n" + + "- The url-parameter `test` is `true` in this instance\n" + + "\n" + + "Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case." + + public static GetQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource { + if (!this.initialized) { + this.init(); + } + QueryParameters.documentation[key] = documentation; + if (deflt !== undefined) { + QueryParameters.defaults[key] = deflt; + } + if (QueryParameters.knownSources[key] !== undefined) { + return QueryParameters.knownSources[key]; + } + QueryParameters.addOrder(key); + const source = new UIEventSource(deflt, "&" + key); + QueryParameters.knownSources[key] = source; + source.addCallback(() => QueryParameters.Serialize()) + return source; + } + + public static GenerateQueryParameterDocs(): string { + const docs = [QueryParameters.QueryParamDocsIntro]; + for (const key in QueryParameters.documentation) { + const c = new Combine([ + new Title(key, 2), + QueryParameters.documentation[key], + QueryParameters.defaults[key] === undefined ? "No default value set" : `The default value is _${QueryParameters.defaults[key]}_` + + ]) + docs.push(c.AsMarkdown()) + } + return docs.join("\n\n"); + } private static addOrder(key) { if (this.order.indexOf(key) < 0) { @@ -25,7 +80,11 @@ export class QueryParameters { return; } this.initialized = true; - + + if (Utils.runningFromConsole) { + return; + } + if (window?.location?.search) { const params = window.location.search.substr(1).split("&"); for (const param of params) { @@ -38,7 +97,7 @@ export class QueryParameters { QueryParameters.knownSources[key] = source; } } - + window["mapcomplete_query_parameter_overview"] = () => { console.log(QueryParameters.GenerateQueryParameterDocs()) } @@ -50,7 +109,7 @@ export class QueryParameters { if (QueryParameters.knownSources[key]?.data === undefined) { continue; } - + if (QueryParameters.knownSources[key].data === "undefined") { continue; } @@ -62,41 +121,8 @@ export class QueryParameters { parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data)) } // Don't pollute the history every time a parameter changes - + history.replaceState(null, "", "?" + parts.join("&") + Hash.Current()); } - - public static GetQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource { - if(!this.initialized){ - this.init(); - } - QueryParameters.documentation[key] = documentation; - if (deflt !== undefined) { - QueryParameters.defaults[key] = deflt; - } - if (QueryParameters.knownSources[key] !== undefined) { - return QueryParameters.knownSources[key]; - } - QueryParameters.addOrder(key); - const source = new UIEventSource(deflt, "&"+key); - QueryParameters.knownSources[key] = source; - source.addCallback(() => QueryParameters.Serialize()) - return source; - } - - public static GenerateQueryParameterDocs(): string { - const docs = []; - for (const key in QueryParameters.documentation) { - docs.push([ - " "+key+" ", - "-".repeat(key.length + 2), - QueryParameters.documentation[key], - QueryParameters.defaults[key] === undefined ? "No default value set" : `The default value is _${QueryParameters.defaults[key]}_` - - ].join("\n")) - } - return docs.join("\n\n"); - } - } \ No newline at end of file diff --git a/State.ts b/State.ts index 786c51c289..18fc3f9725 100644 --- a/State.ts +++ b/State.ts @@ -102,6 +102,8 @@ export default class State { */ public readonly locationControl = new UIEventSource(undefined); public backgroundLayer; + public readonly backgroundLayerId: UIEventSource; + /* Last location where a click was registered */ public readonly LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined) @@ -123,7 +125,7 @@ export default class State { public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map( str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n ); - + constructor(layoutToUse: LayoutConfig) { const self = this; @@ -210,8 +212,25 @@ export default class State { "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'") } + { + // Some other feature switches + const customCssQP = QueryParameters.GetQueryParameter("custom-css", "", "If specified, the custom css from the given link will be loaded additionaly"); + if (customCssQP.data !== undefined && customCssQP.data !== "") { + Utils.LoadCustomCss(customCssQP.data); + } + this.backgroundLayerId = QueryParameters.GetQueryParameter("background", + layoutToUse.defaultBackgroundId ?? "osm", + "The id of the background layer to start with") + + } + + + if(Utils.runningFromConsole){ + return; + } + this.osmConnection = new OsmConnection( this.featureSwitchIsTesting.data, QueryParameters.GetQueryParameter("oauth_token", undefined, diff --git a/UI/Base/Combine.ts b/UI/Base/Combine.ts index 9bf1cf9508..79d4a8f2fc 100644 --- a/UI/Base/Combine.ts +++ b/UI/Base/Combine.ts @@ -32,4 +32,8 @@ export default class Combine extends BaseUIElement { return el; } + AsMarkdown(): string { + return this.uiElements.map(el => el.AsMarkdown()).join(this.HasClass("flex-col") ? "\n\n" : " "); + } + } \ No newline at end of file diff --git a/UI/Base/FixedUiElement.ts b/UI/Base/FixedUiElement.ts index a65b80e6f1..b0552caac6 100644 --- a/UI/Base/FixedUiElement.ts +++ b/UI/Base/FixedUiElement.ts @@ -17,5 +17,9 @@ export class FixedUiElement extends BaseUIElement { e.innerHTML = this._html return e; } + + AsMarkdown(): string { + return this._html; + } } \ No newline at end of file diff --git a/UI/Base/List.ts b/UI/Base/List.ts index d7b45a399a..9a9bd87762 100644 --- a/UI/Base/List.ts +++ b/UI/Base/List.ts @@ -30,5 +30,14 @@ export default class List extends BaseUIElement { return el; } + + AsMarkdown(): string { + if(this._ordered){ + return "\n\n"+this.uiElements.map((el, i) => " "+i+". "+el.AsMarkdown().replace(/\n/g, ' \n') ).join("\n") + "\n" + }else{ + return "\n\n"+this.uiElements.map(el => " - "+el.AsMarkdown().replace(/\n/g, ' \n') ).join("\n")+"\n" + + } + } } \ No newline at end of file diff --git a/UI/Base/Title.ts b/UI/Base/Title.ts new file mode 100644 index 0000000000..2fd3b069da --- /dev/null +++ b/UI/Base/Title.ts @@ -0,0 +1,37 @@ +import {UIElement} from "../UIElement"; +import BaseUIElement from "../BaseUIElement"; +import Translations from "../i18n/Translations"; + +export default class Title extends BaseUIElement{ + private readonly _embedded: BaseUIElement; + private readonly _level: number; + constructor(embedded: string | BaseUIElement, level: number =3 ) { + super() + this._embedded = Translations.W(embedded); + this._level = level; + } + + protected InnerConstructElement(): HTMLElement { + const el = this._embedded.ConstructElement() + if(el === undefined){ + return undefined; + } + const h = document.createElement("h"+this._level) + h.appendChild(el) + return h; + } + + AsMarkdown(): string { + const embedded = " " +this._embedded.AsMarkdown()+" "; + + if(this._level == 1){ + return "\n"+embedded+"\n"+"=".repeat(embedded.length)+"\n\n" + } + + if(this._level == 2){ + return "\n"+embedded+"\n"+"-".repeat(embedded.length)+"\n\n" + } + + return "\n"+"#".repeat( this._level)+embedded +"\n\n"; + } +} \ No newline at end of file diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 840814530e..8bafff884f 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -26,17 +26,7 @@ export default abstract class BaseUIElement { } 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) { @@ -84,6 +74,10 @@ export default abstract class BaseUIElement { } return this; } + + public HasClass(clss: string): boolean{ + return this.clss.has(clss) + } public SetStyle(style: string): BaseUIElement { this.style = style; @@ -156,4 +150,8 @@ export default abstract class BaseUIElement { return el } + + public AsMarkdown(): string{ + throw "AsMarkdown is not implemented by "+this.constructor.name + } } \ No newline at end of file diff --git a/UI/BigComponents/PersonalLayersPanel.ts b/UI/BigComponents/PersonalLayersPanel.ts index 2bf2f800e1..51438c7599 100644 --- a/UI/BigComponents/PersonalLayersPanel.ts +++ b/UI/BigComponents/PersonalLayersPanel.ts @@ -23,7 +23,6 @@ export default class PersonalLayersPanel extends UIElement { const self = this; State.state.installedThemes.addCallback(extraThemes => { self.UpdateView(extraThemes.map(layout => layout.layout)); - self.Update(); }) } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 2bcae10271..5ee4591c52 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -216,7 +216,6 @@ export default class SpecialVisualizations { "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.", "General usage is {func_name()} or {func_name(arg, someotherarg)}. Note that you do not need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args", ...helpTexts - ] ); } diff --git a/UI/UIElement.ts b/UI/UIElement.ts index 83b5f67526..565af7830c 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -33,27 +33,6 @@ export abstract class UIElement extends BaseUIElement{ return this; } - - - Update(): void { - - } - - Render(): string { - return this.InnerRenderAsString() - } - - - public InnerRenderAsString(): string { - let rendered = this.InnerRender(); - if (typeof rendered !== "string") { - let html = rendered.ConstructElement() - return html.innerHTML - } - return rendered - } - - /** * Should be overridden for specific HTML functionality */ diff --git a/Utils.ts b/Utils.ts index b8ae81e7e8..dc1d416a43 100644 --- a/Utils.ts +++ b/Utils.ts @@ -159,7 +159,6 @@ export class Utils { return txt; } - // Date will be undefined on failure public static LoadCustomCss(location: string) { const head = document.getElementsByTagName('head')[0]; const link = document.createElement('link'); diff --git a/assets/contributors.json b/assets/contributors.json index eda3444b78..02c30c2ced 100644 --- a/assets/contributors.json +++ b/assets/contributors.json @@ -1 +1 @@ -{"contributors":[{"contributor":"Pieter Vander Vennet", "commits":714},{"contributor":"pietervdvn", "commits":650},{"contributor":"Tobias", "commits":35},{"contributor":"Christian Neumann", "commits":33},{"contributor":"Win Olario", "commits":31},{"contributor":"Pieter Fiers", "commits":31},{"contributor":"Sebastian Kรผrten", "commits":16},{"contributor":"ToastHawaii", "commits":15},{"contributor":"Weblate", "commits":14},{"contributor":"Marco", "commits":14},{"contributor":"Bavo Vanderghote", "commits":12},{"contributor":"Joost", "commits":11},{"contributor":"Midgard", "commits":8},{"contributor":"Jacque Fresco", "commits":8},{"contributor":"Artem", "commits":8},{"contributor":"yopaseopor", "commits":7},{"contributor":"Flo Edelmann", "commits":7},{"contributor":"Binnette", "commits":7},{"contributor":"pelderson", "commits":6},{"contributor":"Mateusz Konieczny", "commits":6},{"contributor":"lvgx", "commits":6},{"contributor":"dependabot[bot]", "commits":6},{"contributor":"Alexey Shabanov", "commits":6},{"contributor":"SiegbjornSitumeang", "commits":4},{"contributor":"Polgรกr Sรกndor", "commits":4},{"contributor":"Lรฉo Villeveygoux", "commits":3},{"contributor":"Hosted Weblate", "commits":3},{"contributor":"David Haberthรผr", "commits":3},{"contributor":"Wiktor Przybylski", "commits":2},{"contributor":"Stanislas Gueniffey", "commits":2},{"contributor":"Robin van der Linde", "commits":2},{"contributor":"riiga", "commits":2},{"contributor":"pbarban", "commits":2},{"contributor":"Leo Alcaraz", "commits":2},{"contributor":"Jan Zabel", "commits":2},{"contributor":"graveelius", "commits":2},{"contributor":"Vinicius", "commits":1},{"contributor":"Tomas Fiers", "commits":1},{"contributor":"Thibault Molleman", "commits":1},{"contributor":"tbowdecl97", "commits":1},{"contributor":"Schouppe Joost", "commits":1},{"contributor":"Noรฉmie", "commits":1},{"contributor":"mozita", "commits":1},{"contributor":"Carlos Ramos Carreรฑo", "commits":1},{"contributor":"Beardhatcode", "commits":1}]} \ No newline at end of file +{"contributors":[{"contributor":"Pieter Vander Vennet", "commits":738},{"contributor":"pietervdvn", "commits":718},{"contributor":"Weblate", "commits":35},{"contributor":"Tobias", "commits":35},{"contributor":"Christian Neumann", "commits":33},{"contributor":"Win Olario", "commits":31},{"contributor":"Pieter Fiers", "commits":31},{"contributor":"Sebastian Kรผrten", "commits":16},{"contributor":"Marco", "commits":16},{"contributor":"Joost", "commits":16},{"contributor":"ToastHawaii", "commits":15},{"contributor":"J. Lavoie", "commits":14},{"contributor":"Bavo Vanderghote", "commits":12},{"contributor":"Artem", "commits":12},{"contributor":"Supaplex", "commits":9},{"contributor":"Jacque Fresco", "commits":9},{"contributor":"Midgard", "commits":8},{"contributor":"Mateusz Konieczny", "commits":8},{"contributor":"yopaseopor", "commits":7},{"contributor":"Flo Edelmann", "commits":7},{"contributor":"Binnette", "commits":7},{"contributor":"Allan Nordhรธy", "commits":7},{"contributor":"pelderson", "commits":6},{"contributor":"lvgx", "commits":6},{"contributor":"dependabot[bot]", "commits":6},{"contributor":"Alexey Shabanov", "commits":6},{"contributor":"SiegbjornSitumeang", "commits":4},{"contributor":"Polgรกr Sรกndor", "commits":4},{"contributor":"Hiroshi Miura", "commits":4},{"contributor":"vankos", "commits":3},{"contributor":"Lรฉo Villeveygoux", "commits":3},{"contributor":"JCGF-OSM", "commits":3},{"contributor":"Jan Zabel", "commits":3},{"contributor":"Hosted Weblate", "commits":3},{"contributor":"David Haberthรผr", "commits":3},{"contributor":"ๅฟซไน็š„่€้ผ ๅฎๅฎ", "commits":2},{"contributor":"Wiktor Przybylski", "commits":2},{"contributor":"Vinicius", "commits":2},{"contributor":"Stanislas Gueniffey", "commits":2},{"contributor":"Robin van der Linde", "commits":2},{"contributor":"riiga", "commits":2},{"contributor":"pbarban", "commits":2},{"contributor":"mic140", "commits":2},{"contributor":"Leo Alcaraz", "commits":2},{"contributor":"Jose Luis Infante", "commits":2},{"contributor":"Heiko", "commits":2},{"contributor":"graveelius", "commits":2},{"contributor":"Tomas Fiers", "commits":1},{"contributor":"Thibault Molleman", "commits":1},{"contributor":"tbowdecl97", "commits":1},{"contributor":"Sebastian", "commits":1},{"contributor":"Sean Young", "commits":1},{"contributor":"Schouppe Joost", "commits":1},{"contributor":"Noรฉmie", "commits":1},{"contributor":"mozita", "commits":1},{"contributor":"Michaล‚ Targoล„ski", "commits":1},{"contributor":"Ivรกns", "commits":1},{"contributor":"Eric Armijo", "commits":1},{"contributor":"Damian Puล‚ka", "commits":1},{"contributor":"Carlos Ramos Carreรฑo", "commits":1},{"contributor":"Beardhatcode", "commits":1}]} \ No newline at end of file diff --git a/css/imageUploadFlow.css b/css/imageUploadFlow.css deleted file mode 100644 index c36b33b64d..0000000000 --- a/css/imageUploadFlow.css +++ /dev/null @@ -1,23 +0,0 @@ -.image-upload-flow-button span { - width: max-content; - font-size: 28px; - font-weight: bold; - margin-top: 4px; - padding-top: 4px; - padding-bottom: 4px; - padding-left: 13px; -} - -.image-upload-flow-button { - display: flex; - cursor: pointer; - padding: 0.5em; - border-radius: 1em; - border: 3px solid var(--foreground-color); - box-sizing: border-box; -} - -.image-upload-flow svg { - fill: var(--foreground-color); - stroke: var(--foreground-color); -} diff --git a/index.html b/index.html index 9330ff1489..1f9df50a6c 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,6 @@ - diff --git a/index.manifest b/index.manifest index 1eb1aec3ce..9b311e9008 100644 --- a/index.manifest +++ b/index.manifest @@ -1,10 +1,8 @@ { "name": "index", - "short_name": "MapComplete", "start_url": "index.html", "display": "standalone", "background_color": "#fff", - "description": "A thematic map viewer and editor based on OpenStreetMap", "orientation": "portrait-primary, landscape-primary", "icons": [ { diff --git a/index.ts b/index.ts index e787b76491..79a81b2ea0 100644 --- a/index.ts +++ b/index.ts @@ -33,10 +33,6 @@ if (location.href.indexOf("buurtnatuur.be") >= 0) { defaultLayout = "buurtnatuur" } -const customCssQP = QueryParameters.GetQueryParameter("custom-css", "", "If specified, the custom css from the given link will be loaded additionaly"); -if (customCssQP.data !== undefined && customCssQP.data !== "") { - Utils.LoadCustomCss(customCssQP.data); -} let testing: UIEventSource; @@ -87,7 +83,6 @@ if (layoutToUse?.id === "cyclofix") { const layoutFromBase64 = decodeURIComponent(userLayoutParam.data); -document.getElementById('centermessage').innerText = 'Initilai'; new Combine(["Initializing...
    ", new FixedUiElement("If this message persist, something went wrong - click here to try again") diff --git a/package.json b/package.json index 80b07d36e8..b62ffd445b 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,6 @@ "ts-node": "^9.0.0", "ts-node-dev": "^1.0.0-pre.63", "tslint-no-circular-imports": "^0.7.0", - "turndown": "^7.0.0", "typescript": "^3.9.7", "write-file": "^1.0.0" } diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index b9b2553b20..c76cb15838 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -1,25 +1,53 @@ import {Utils} from "../Utils"; + Utils.runningFromConsole = true; import SpecialVisualizations from "../UI/SpecialVisualizations"; -import {writeFileSync} from "fs"; -import {UIElement} from "../UI/UIElement"; import SimpleMetaTagger from "../Logic/SimpleMetaTagger"; import Combine from "../UI/Base/Combine"; import {ExtraFunction} from "../Logic/ExtraFunction"; import ValidatedTextField from "../UI/Input/ValidatedTextField"; +import BaseUIElement from "../UI/BaseUIElement"; +import Translations from "../UI/i18n/Translations"; +import {writeFileSync} from "fs"; +import LayoutConfig from "../Customizations/JSON/LayoutConfig"; +import State from "../State"; +import {QueryParameters} from "../Logic/Web/QueryParameters"; - - -const TurndownService = require('turndown') - -function WriteFile(filename, html: UIElement) : void { - const md = new TurndownService().turndown(html.InnerRenderAsString()); - writeFileSync(filename, md); +function WriteFile(filename, html: string | BaseUIElement): void { + writeFileSync(filename, Translations.W(html).AsMarkdown()); } WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage) -WriteFile("./Docs/CalculatedTags.md", new Combine([SimpleMetaTagger.HelpText(), ExtraFunction.HelpText()])) -writeFileSync("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText()); +WriteFile("./Docs/CalculatedTags.md", new Combine([SimpleMetaTagger.HelpText(), ExtraFunction.HelpText()]).SetClass("flex-col")) +WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText()); + + +new State(new LayoutConfig({ + language: ["en"], + id: "", + maintainer: "pietervdvn", + version: "0", + title: "", + description: "A theme to generate docs with", + startLat: 0, + startLon: 0, + startZoom: 0, + icon: undefined, + layers: [ + { + name: "", + id: "", + source: { + osmTags: "id~*" + } + } + ] + +})) +QueryParameters.GetQueryParameter("layer-", "true", "Wether or not the layer with id is shown") + +WriteFile("./Docs/URL_Parameters.md", QueryParameters.GenerateQueryParameterDocs()) + console.log("Generated docs") diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index dcfadc3da4..8c1a365d58 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).InnerRenderAsString(); - const ogDescr = Translations.W(layout.description ?? "").InnerRenderAsString(); + const ogTitle = Translations.W(layout.title).ConstructElement()?.innerText; + const ogDescr = Translations.W(layout.description ?? "").ConstructElement()?.innerText; 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)?.InnerRenderAsString(); - const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRenderAsString(); + const ogTitle = Translations.W(layout.title)?.ConstructElement()?.innerText; + const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.ConstructElement()?.innerText; const ogImage = layout.socialImage; let customCss = ""; From afbe765ce9c7e96f1afcf955fa9a61b0dd603982 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 15 Jun 2021 00:55:12 +0200 Subject: [PATCH 11/30] Add table, fix preferences view --- Models/Constants.ts | 2 +- State.ts | 2 +- UI/Base/Button.ts | 38 ++++++++------------ UI/Base/Table.ts | 59 +++++++++++++++++++++++++++++++ UI/BigComponents/LicensePicker.ts | 8 ++--- preferences.ts | 29 ++++++++------- 6 files changed, 95 insertions(+), 43 deletions(-) create mode 100644 UI/Base/Table.ts diff --git a/Models/Constants.ts b/Models/Constants.ts index e7ec48ba98..71625ac88a 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.7.5b"; + public static vNumber = "0.8.0"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/State.ts b/State.ts index 18fc3f9725..8e4322d654 100644 --- a/State.ts +++ b/State.ts @@ -221,7 +221,7 @@ export default class State { this.backgroundLayerId = QueryParameters.GetQueryParameter("background", - layoutToUse.defaultBackgroundId ?? "osm", + layoutToUse?.defaultBackgroundId ?? "osm", "The id of the background layer to start with") } diff --git a/UI/Base/Button.ts b/UI/Base/Button.ts index 89364807bf..e671e09791 100644 --- a/UI/Base/Button.ts +++ b/UI/Base/Button.ts @@ -1,39 +1,29 @@ import {UIElement} from "../UIElement"; -import Locale from "../i18n/Locale"; import Translations from "../i18n/Translations"; import BaseUIElement from "../BaseUIElement"; -export class Button extends UIElement { +export class Button extends BaseUIElement { private _text: BaseUIElement; private _onclick: () => void; - private _clss: string; - constructor(text: string | UIElement, onclick: (() => void), clss: string = "") { - super(Locale.language); + constructor(text: string | UIElement, onclick: (() => void)) { + super(); this._text = Translations.W(text); this._onclick = onclick; - if (clss !== "") { - - this._clss = "class='" + clss + "'"; - }else{ - this._clss = ""; - } } - - InnerRender(): string { - - return "
    " + - "" + - "
    "; - } - - InnerUpdate(htmlElement: HTMLElement) { - super.InnerUpdate(htmlElement); - const self = this; - document.getElementById("button-"+this.id).onclick = function(){ - self._onclick(); + protected InnerConstructElement(): HTMLElement { + const el = this._text.ConstructElement(); + if(el === undefined){ + return undefined; } + const form = document.createElement("form") + const button = document.createElement("button") + button.type = "button" + button.appendChild(el) + button.onclick = this._onclick + form.appendChild(button) + return form; } } \ No newline at end of file diff --git a/UI/Base/Table.ts b/UI/Base/Table.ts new file mode 100644 index 0000000000..f02f8c49dd --- /dev/null +++ b/UI/Base/Table.ts @@ -0,0 +1,59 @@ +import BaseUIElement from "../BaseUIElement"; +import {Utils} from "../../Utils"; +import Translations from "../i18n/Translations"; + +export default class Table extends BaseUIElement { + + private readonly _header: BaseUIElement[]; + private readonly _contents: BaseUIElement[][]; + + constructor(header: (BaseUIElement | string)[], contents: (BaseUIElement | string)[][]) { + super(); + this._header = header.map(Translations.W); + this._contents = contents.map(row => row.map(Translations.W)); + } + + protected InnerConstructElement(): HTMLElement { + const table = document.createElement("table") + + const headerElems = Utils.NoNull((this._header ?? []).map(elems => elems.ConstructElement())) + if (headerElems.length > 0) { + + const tr = document.createElement("tr"); + headerElems.forEach(headerElem => { + const td = document.createElement("th") + td.appendChild(headerElem) + tr.appendChild(td) + }) + table.appendChild(tr) + } + + for (const row of this._contents) { + const tr = document.createElement("tr") + for (const elem of row) { + const htmlElem = elem.ConstructElement() + if (htmlElem === undefined) { + continue; + } + + const td = document.createElement("td") + td.appendChild(htmlElem) + tr.appendChild(td) + } + table.appendChild(tr) + } + + return table; + } + + AsMarkdown(): string { + + const headerMarkdownParts = this._header.map(hel => hel?.AsMarkdown() ?? " ") + const header =headerMarkdownParts.join(" | "); + const headerSep = headerMarkdownParts.map(part => '-'.repeat(part.length + 2)).join("|") + const table = this._contents.map(row => row.map(el => el.AsMarkdown()?? " ").join("|")).join("\n") + + return [header, headerSep, table, ""].join("\n") + } + +} \ No newline at end of file diff --git a/UI/BigComponents/LicensePicker.ts b/UI/BigComponents/LicensePicker.ts index f87d8c775b..efdd6aafd0 100644 --- a/UI/BigComponents/LicensePicker.ts +++ b/UI/BigComponents/LicensePicker.ts @@ -6,11 +6,11 @@ import {UIEventSource} from "../../Logic/UIEventSource"; export default class LicensePicker extends DropDown{ constructor() { - super(Translations.t.image.willBePublished, + super(Translations.t.image.willBePublished.Clone(), [ - {value: "CC0", shown: Translations.t.image.cco}, - {value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs}, - {value: "CC-BY 4.0", shown: Translations.t.image.ccb} + {value: "CC0", shown: Translations.t.image.cco.Clone()}, + {value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs.Clone()}, + {value: "CC-BY 4.0", shown: Translations.t.image.ccb.Clone()} ], State.state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource("CC0") ) diff --git a/preferences.ts b/preferences.ts index 7f390c7976..4b1dce30ab 100644 --- a/preferences.ts +++ b/preferences.ts @@ -9,6 +9,8 @@ import {Utils} from "./Utils"; import {SubtleButton} from "./UI/Base/SubtleButton"; import LZString from "lz-string"; import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; +import BaseUIElement from "./UI/BaseUIElement"; +import Table from "./UI/Base/Table"; const connection = new OsmConnection(false, new UIEventSource(undefined), ""); @@ -99,11 +101,14 @@ function createTable(preferences: any) { return; } rendered = true; - const prefs = []; + const prefs: (BaseUIElement|string)[][] = []; for (const key in preferences) { + if(!preferences.hasOwnProperty(key)){ + continue; + } const pref = connection.GetPreference(key, ""); - let value: UIElement = new FixedUiElement(pref.data); + let value: BaseUIElement = new FixedUiElement(pref.data); if (connection.userDetails.data.csCount > 500 && (key.startsWith("mapcomplete") || connection.userDetails.data.csCount > 2500)) { value = new TextField({ @@ -111,24 +116,22 @@ function createTable(preferences: any) { }); } - const c = [ - "", + const row = [ key, - "", - new Button("delete", () => pref.setData("")), - "", - value, - "" + new Button("delete", () => pref.setData(null)), + value ]; - prefs.push(...c); + prefs.push(row); } new Combine( [ ...salvageThemes(preferences).map(theme => SalvageButton(theme)), - "", - ...prefs, - "
    ", + new Table( + ["Key","","Value"], + prefs + + ), new SubtleButton("./assets/svg/delete_icon.svg", "Delete all mapcomplete preferences (mangrove identies are preserved)").onClick(() => clearAll(preferences))] ).AttachTo("maindiv"); } From 42d13f564caf90e03b8f98a985e1419995b4586a Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 15 Jun 2021 01:24:04 +0200 Subject: [PATCH 12/30] Fix sharebutton --- Customizations/JSON/LayoutConfig.ts | 5 ++- UI/BaseUIElement.ts | 1 + UI/BigComponents/ShareButton.ts | 8 ++--- UI/Image/SlideShow.ts | 30 ----------------- UI/Popup/TagRenderingAnswer.ts | 7 ++++ UI/SpecialVisualizations.ts | 50 ++++++++++++++++++----------- UI/SubstitutedTranslation.ts | 9 +++--- 7 files changed, 51 insertions(+), 59 deletions(-) diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index 00764d490f..4bf3ae2a29 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -152,11 +152,10 @@ export default class LayoutConfig { ); } - const defaultClustering = { + this.clustering = { maxZoom: 16, minNeededElements: 500 }; - this.clustering = defaultClustering; if (json.clustering) { this.clustering = { maxZoom: json.clustering.maxZoom ?? 18, @@ -164,7 +163,7 @@ export default class LayoutConfig { } for (const layer of this.layers) { if (layer.wayHandling !== LayerConfig.WAYHANDLING_CENTER_ONLY) { - console.error("WARNING: In order to allow clustering, every layer must be set to CENTER_ONLY. Layer", layer.id, "does not respect this for layout", this.id); + console.debug("WARNING: In order to allow clustering, every layer must be set to CENTER_ONLY. Layer", layer.id, "does not respect this for layout", this.id); } } } diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 8bafff884f..1570dd3ead 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -86,6 +86,7 @@ export default abstract class BaseUIElement { } return this; } + /** * The same as 'Render', but creates a HTML element instead of the HTML representation */ diff --git a/UI/BigComponents/ShareButton.ts b/UI/BigComponents/ShareButton.ts index 474abe7cd8..7b3d0255be 100644 --- a/UI/BigComponents/ShareButton.ts +++ b/UI/BigComponents/ShareButton.ts @@ -2,16 +2,16 @@ import BaseUIElement from "../BaseUIElement"; export default class ShareButton extends BaseUIElement{ private _embedded: BaseUIElement; - private _shareData: { text: string; title: string; url: string }; + private _shareData: () => { text: string; title: string; url: string }; - constructor(embedded: BaseUIElement, shareData: { + constructor(embedded: BaseUIElement, generateShareData: () => { text: string, title: string, url: string }) { super(); this._embedded = embedded; - this._shareData = shareData; + this._shareData = generateShareData; this.SetClass("share-button") } @@ -22,7 +22,7 @@ export default class ShareButton extends BaseUIElement{ e.addEventListener('click', () => { if (navigator.share) { - navigator.share(this._shareData).then(() => { + navigator.share(this._shareData()).then(() => { console.log('Thanks for sharing!'); }) .catch(err => { diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 692bb87488..01b6cbe5e3 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -16,36 +16,6 @@ export class SlideShow extends BaseUIElement { const el = document.createElement("div") el.classList.add("slic-carousel") - el.onchange = () => { - console.log("Parent is now ", el.parentElement) - } - - const mutationObserver = new MutationObserver(mutations => { - console.log("Mutations are: ", mutations) - - - mutationObserver.disconnect() - require("slick-carousel") - // @ts-ignore - el.slick({ - autoplay: true, - arrows: true, - dots: true, - lazyLoad: 'progressive', - variableWidth: true, - centerMode: true, - centerPadding: "60px", - adaptive: true - }); - }) - - mutationObserver.observe(el, { - childList: true, - characterData: true, - subtree: true - }) - - this.embeddedElements.addCallbackAndRun(elements => { for (const element of elements ?? []) { element.SetClass("slick-carousel-content") diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 86db6437ef..6c8fd257ec 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -19,6 +19,13 @@ export default class TagRenderingAnswer extends VariableUiElement { if(tags === undefined){ return undefined; } + + if(configuration.condition){ + if(!configuration.condition.matchesProperties(tags)){ + return undefined; + } + } + const trs = Utils.NoNull(configuration.GetRenderValues(tags)); if(trs.length === 0){ return undefined; diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 5ee4591c52..b1f1a5559d 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -17,10 +17,10 @@ import OpeningHoursVisualization from "./OpeningHours/OhVisualization"; import State from "../State"; import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; import BaseUIElement from "./BaseUIElement"; +import LayerConfig from "../Customizations/JSON/LayerConfig"; export default class SpecialVisualizations { - - + public static specialVisualizations: { funcName: string, @@ -38,7 +38,7 @@ export default class SpecialVisualizations { return new VariableUiElement(tags.map(tags => { const parts = []; for (const key in tags) { - if(!tags.hasOwnProperty(key)){ + if (!tags.hasOwnProperty(key)) { continue; } parts.push(key + "=" + tags[key]); @@ -160,22 +160,36 @@ export default class SpecialVisualizations { ], constr: (state: State, tagSource: UIEventSource, args) => { if (window.navigator.share) { - const title = state?.layoutToUse?.data?.title?.txt ?? "MapComplete"; - let name = tagSource.data.name; - if (name) { - name = `${name} (${title})` - } else { - name = title; + + const generateShareData = () => { + + + const title = state?.layoutToUse?.data?.title?.txt ?? "MapComplete"; + + let matchingLayer: LayerConfig = undefined; + for (const layer of (state?.layoutToUse?.data?.layers ?? [])) { + if (layer.source.osmTags.matchesProperties(tagSource?.data)) { + matchingLayer = layer + } + } + let name = matchingLayer?.title?.GetRenderValue(tagSource.data)?.txt ?? tagSource.data?.name ?? "POI"; + if (name) { + name = `${name} (${title})` + } else { + name = title; + } + let url = args[0] ?? "" + if (url === "") { + url = window.location.href + } + return { + title: name, + url: url, + text: state?.layoutToUse?.data?.shortDescription?.txt ?? "MapComplete" + } } - let url = args[0] ?? "" - if (url === "") { - url = window.location.href - } - return new ShareButton(Svg.share_ui(), { - title: name, - url: url, - text: state?.layoutToUse?.data?.shortDescription?.txt ?? "MapComplete" - }) + + return new ShareButton(Svg.share_ui(), generateShareData) } else { return new FixedUiElement("") } diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index d48e56cb99..2a51d15e47 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -13,14 +13,14 @@ export class SubstitutedTranslation extends VariableUiElement { public constructor( translation: Translation, - tags: UIEventSource) { + tagsSource: UIEventSource) { super( - tags.map(tags => { + tagsSource.map(tags => { const txt = Utils.SubstituteKeys(translation.txt, tags) if (txt === undefined) { - return "no tags subs tr" + return undefined } - return new Combine(SubstitutedTranslation.EvaluateSpecialComponents(txt, tags)) + return new Combine(SubstitutedTranslation.EvaluateSpecialComponents(txt, tagsSource)) }, [Locale.language]) ) @@ -59,6 +59,7 @@ export class SubstitutedTranslation extends VariableUiElement { try{ element = knownSpecial.constr(State.state, tags, args); }catch(e){ + console.error("SPECIALRENDERING FAILED for", tags.data.id, e) element = new FixedUiElement(`Could not generate special renering for ${knownSpecial}(${args.join(", ")}) ${e}`).SetClass("alert") } From 94f9a0de567329f1ea150a1f3de1db3506ffef5e Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 15 Jun 2021 16:18:58 +0200 Subject: [PATCH 13/30] More fixes to the refactored code --- Logic/Web/ImgurUploader.ts | 2 +- UI/Image/DeleteImage.ts | 54 ++++++++++++++++++------------------- UI/Image/ImageUploadFlow.ts | 4 +-- UI/Image/SlideShow.ts | 19 ++++++++++--- UI/Input/DropDown.ts | 4 ++- UI/Input/TextField.ts | 2 +- UI/Popup/FeatureInfoBox.ts | 2 +- UI/ShowDataLayer.ts | 15 ++++++----- Utils.ts | 2 +- css/mobile.css | 4 +++ css/slideshow.css | 30 +++++++++------------ index.css | 15 ----------- index.html | 1 - package.json | 2 -- test.ts | 4 +-- 15 files changed, 78 insertions(+), 82 deletions(-) diff --git a/Logic/Web/ImgurUploader.ts b/Logic/Web/ImgurUploader.ts index 851b9c53f5..67392086db 100644 --- a/Logic/Web/ImgurUploader.ts +++ b/Logic/Web/ImgurUploader.ts @@ -26,7 +26,7 @@ export default class ImgurUploader { function (url) { console.log("File saved at", url); self.success.setData([...self.success.data, url]); - this. handleSuccessUrl(url); + self._handleSuccessUrl(url); }, function () { console.log("All uploads completed"); diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index 656b9160d2..f49deeb1bf 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -9,49 +9,47 @@ import {Tag} from "../../Logic/Tags/Tag"; import BaseUIElement from "../BaseUIElement"; -export default class DeleteImage extends UIElement { - private readonly key: string; - private readonly tags: UIEventSource; - - private readonly isDeletedBadge: BaseUIElement; - private readonly deleteDialog: BaseUIElement; +export default class DeleteImage extends Toggle { constructor(key: string, tags: UIEventSource) { - super(tags); - this.tags = tags; - this.key = key; - - this.isDeletedBadge = Translations.t.image.isDeleted; + const oldValue = tags.data[key] + const isDeletedBadge = Translations.t.image.isDeleted.Clone() + .SetClass("rounded-full p-1") + .SetStyle("color:white;background:#ff8c8c") + .onClick(() => { + State.state?.changes?.addTag(tags.data.id, new Tag(key, oldValue), tags); + }); const deleteButton = Translations.t.image.doDelete.Clone() .SetClass("block w-full pl-4 pr-4") .SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;") .onClick(() => { - State.state?.changes.addTag(tags.data.id, new Tag(key, "")); + State.state?.changes?.addTag(tags.data.id, new Tag(key, ""), tags); }); - 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 Toggle( + const cancelButton = Translations.t.general.cancel.Clone().SetClass("bg-white pl-4 pr-4").SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;"); + const openDelete = Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;") + const deleteDialog = new Toggle( new Combine([ deleteButton, cancelButton ]).SetClass("flex flex-col background-black"), - Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;") - ).ToggleOnClick() + openDelete + ) - } + cancelButton.onClick(() => deleteDialog.isEnabled.setData(false)) + openDelete.onClick(() => deleteDialog.isEnabled.setData(true)) - InnerRender() { - if(! State.state?.featureSwitchUserbadge?.data){ - return ""; - } - - const value = this.tags.data[this.key]; - if (value === undefined || value === "") { - return this.isDeletedBadge; - } - - return this.deleteDialog; + super( + new Toggle( + deleteDialog, + isDeletedBadge, + tags.map(tags => (tags[key] ?? "") !== "") + ), + undefined /*Login (and thus editing) is disabled*/, + State.state?.featureSwitchUserbadge ?? new UIEventSource(true) + ) + this.SetClass("cursor-pointer") } } \ No newline at end of file diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 8de6ad4f90..cf5881f64e 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -28,7 +28,7 @@ export class ImageUploadFlow extends Toggle { key = imagePrefix + ":" + freeIndex; } console.log("Adding image:" + key, url); - State.state.changes.addTag(tags.id, new Tag(key, url)); + State.state.changes.addTag(tags.id, new Tag(key, url), tagsSource); }) @@ -47,7 +47,7 @@ export class ImageUploadFlow extends Toggle { } console.log("Received images from the user, starting upload") - const license = licensePicker.GetValue().data ?? "CC0" + const license = licensePicker.GetValue()?.data ?? "CC0" const tags = tagsSource.data; diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 01b6cbe5e3..8c7fb1a2cf 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -1,6 +1,5 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; -import $ from "jquery" export class SlideShow extends BaseUIElement { @@ -15,15 +14,29 @@ export class SlideShow extends BaseUIElement { protected InnerConstructElement(): HTMLElement { const el = document.createElement("div") el.classList.add("slic-carousel") + el.style.overflowX = "auto" + el.style.width = "min-content" + el.style.minWidth = "min-content" + el.style.display = "flex" this.embeddedElements.addCallbackAndRun(elements => { + while (el.firstChild) { + el.removeChild(el.lastChild) + } + for (const element of elements ?? []) { - element.SetClass("slick-carousel-content") + element.SetClass("block ml-1") + .SetStyle("width: 300px; max-height: var(--image-carousel-height); height: var(--image-carousel-height)") + el.appendChild(element.ConstructElement()) } }); - return el; + const wrapper = document.createElement("div") + wrapper.style.maxWidth = "100%" + wrapper.style.overflowX = "auto" + wrapper.appendChild(el) + return wrapper; } } \ No newline at end of file diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts index 91f1d700e0..fe8f8bc987 100644 --- a/UI/Input/DropDown.ts +++ b/UI/Input/DropDown.ts @@ -21,6 +21,8 @@ export class DropDown extends InputElement { } ) { super(); + value = value ?? new UIEventSource(undefined) + this._value = value this._values = values; if (values.length <= 1) { return; @@ -43,7 +45,7 @@ export class DropDown extends InputElement { el.appendChild(labelHtml) } } - + options = options ?? {} options.select_class = options.select_class ?? 'bg-indigo-100 p-1 rounded hover:bg-indigo-200' diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index 2071d74807..ff91013815 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -106,7 +106,7 @@ export class TextField extends InputElement { newCursorPos--; } // @ts-ignore - TextField.SetCursorPosition(newCursorPos); + TextField.SetCursorPosition(field, newCursorPos); }; diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 64ffe59a2c..7f375d298a 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -53,7 +53,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { } let questionBoxIsUsed = false; - const renderings = layerConfig.tagRenderings.map(tr => { + const renderings : BaseUIElement[] = layerConfig.tagRenderings.map(tr => { if (tr.question === null) { // This is the question box! questionBoxIsUsed = true; diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 6a2a8953dd..d559b7cf2c 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -126,22 +126,23 @@ export default class ShowDataLayer { closeButton: false }, leafletLayer); - leafletLayer.bindPopup(popup); - - let infobox : FeatureInfoBox = undefined; - + leafletLayer.bindPopup(popup); + + let infobox: FeatureInfoBox = undefined; + const id = `popup-${feature.properties.id}-${this._cleanCount}` - popup.setContent(`
    Rendering
    `) + popup.setContent(`
    Rendering
    `) leafletLayer.on("popupopen", () => { State.state.selectedElement.setData(feature) - if (infobox === undefined) { + if (infobox === undefined) { const tags = State.state.allElements.getEventSourceById(feature.properties.id); infobox = new FeatureInfoBox(tags, layer); infobox.isShown.addCallback(isShown => { if (!isShown) { State.state.selectedElement.setData(undefined); + leafletLayer.closePopup() } }); } @@ -162,7 +163,7 @@ export default class ShowDataLayer { leafletLayer.openPopup() } }) - + } private CreateGeojsonLayer(): L.Layer { diff --git a/Utils.ts b/Utils.ts index dc1d416a43..ba18b98d99 100644 --- a/Utils.ts +++ b/Utils.ts @@ -108,7 +108,7 @@ export class Utils { } public static EllipsesAfter(str: string, l: number = 100) { - if (str === undefined) { + if (str === undefined || str === null) { return undefined; } if (str.length <= l) { diff --git a/css/mobile.css b/css/mobile.css index 1d0b523ebf..a9b321f2d0 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -55,6 +55,10 @@ Contains tweaks for small screens .leaflet-control-attribution{ display: none; } + + .leaflet-popup { + display: none; + } } diff --git a/css/slideshow.css b/css/slideshow.css index 7fe59c113c..c5ab215d67 100644 --- a/css/slideshow.css +++ b/css/slideshow.css @@ -1,22 +1,18 @@ -.slick-next { - top: unset; - bottom: -25px; - right: 15px; - z-index: 10000; + + +.slick-carousel-content { + width: 300px; + max-height: var(--image-carousel-height); + display: block; + margin-left: 10px; } -.slick-prev { - top: unset; - bottom: -25px; - left: 0; - z-index: 10000; +.slick-carousel-content img { + /** +Workaround to patch images within a slick carousel + */ + height: var(--image-carousel-height); + width: auto; } -.slick-next::before { - font-size: 35px; -} - -.slick-prev::before { - font-size: 35px; -} \ No newline at end of file diff --git a/index.css b/index.css index 10c8df5ee7..afa2d28f9c 100644 --- a/index.css +++ b/index.css @@ -65,21 +65,6 @@ --image-carousel-height: 400px; } -.slick-carousel-content { - width: 300px; - max-height: var(--image-carousel-height); - display: block; - margin-left: 10px; -} - -.slick-carousel-content img { - /** -Workaround to patch images within a slick carousel - */ - height: var(--image-carousel-height); - width: auto; -} - html, body { height: 100%; min-height: 100vh; diff --git a/index.html b/index.html index 1f9df50a6c..e8354f59a9 100644 --- a/index.html +++ b/index.html @@ -14,7 +14,6 @@ - diff --git a/package.json b/package.json index b62ffd445b..40b378ec1a 100644 --- a/package.json +++ b/package.json @@ -80,14 +80,12 @@ "postcss": "^7.0.35", "prompt-sync": "^4.2.0", "sharp": "^0.27.0", - "slick-carousel": "^1.8.1", "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2", "tslint": "^6.1.3" }, "devDependencies": { "@babel/polyfill": "^7.10.4", "@types/node": "^7.0.5", - "@types/slick-carousel": "^1.6.34", "assert": "^2.0.0", "fs": "0.0.1-security", "marked": "^2.0.0", diff --git a/test.ts b/test.ts index 46333efa38..ee47136d1c 100644 --- a/test.ts +++ b/test.ts @@ -10,9 +10,9 @@ const tagsSource = new UIEventSource({ name:'name', surface:'asphalt', image: "https://i.imgur.com/kX3rl3v.jpg", - "image:1": "https://i.imgur.com/kX3rl3v.jpg", + "image:1": "https://i.imgur.com/oHAJqMB.jpg", + // "opening_hours":"mo-fr 09:00-18:00", _country:"be", - // "opening_hours":"mo-fr 09:00-18:00" }) const state = new State(undefined) From 64ec06bfc8efd893f8f48f59c436f1456bc031a1 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 16 Jun 2021 14:23:53 +0200 Subject: [PATCH 14/30] Fix opening hours input element --- Logic/SimpleMetaTagger.ts | 8 +- UI/Base/Combine.ts | 8 + UI/Base/Table.ts | 22 +- UI/Base/VariableUIElement.ts | 2 +- UI/BaseUIElement.ts | 10 +- UI/OpeningHours/OhVisualization.ts | 324 ------------------- UI/OpeningHours/OpeningHours.ts | 129 ++++++++ UI/OpeningHours/OpeningHoursPicker.ts | 47 +-- UI/OpeningHours/OpeningHoursPickerTable.ts | 157 +++++---- UI/OpeningHours/OpeningHoursRange.ts | 99 ++---- UI/OpeningHours/OpeningHoursVisualization.ts | 292 +++++++++++++++++ UI/Popup/EditableTagRendering.ts | 1 + UI/ShowDataLayer.ts | 3 +- UI/SpecialVisualizations.ts | 8 +- UI/UIElement.ts | 2 - Utils.ts | 8 + css/openinghourstable.css | 94 +----- langs/en.json | 1 + test.ts | 27 +- 19 files changed, 643 insertions(+), 599 deletions(-) delete mode 100644 UI/OpeningHours/OhVisualization.ts create mode 100644 UI/OpeningHours/OpeningHoursVisualization.ts diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 4d13af7247..8e6516c6d7 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -102,9 +102,13 @@ export default class SimpleMetaTagger { SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => { try { + const oldCountry = feature.properties["_country"]; feature.properties["_country"] = countries[0].trim().toLowerCase(); - const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); - tagsSource.ping(); + if (oldCountry !== feature.properties["_country"]) { + const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); + tagsSource.ping(); + } + } catch (e) { console.warn(e) } diff --git a/UI/Base/Combine.ts b/UI/Base/Combine.ts index 79d4a8f2fc..d76c839348 100644 --- a/UI/Base/Combine.ts +++ b/UI/Base/Combine.ts @@ -19,6 +19,9 @@ export default class Combine extends BaseUIElement { protected InnerConstructElement(): HTMLElement { const el = document.createElement("span") + try{ + + for (const subEl of this.uiElements) { if(subEl === undefined || subEl === null){ continue; @@ -28,6 +31,11 @@ export default class Combine extends BaseUIElement { el.appendChild(subHtml) } } + }catch(e){ + const domExc = e as DOMException + console.error("DOMException: ", domExc.name) + el.appendChild(new FixedUiElement("Could not generate this combine!").SetClass("alert").ConstructElement()) + } return el; } diff --git a/UI/Base/Table.ts b/UI/Base/Table.ts index f02f8c49dd..fe4ee2be7f 100644 --- a/UI/Base/Table.ts +++ b/UI/Base/Table.ts @@ -6,10 +6,14 @@ export default class Table extends BaseUIElement { private readonly _header: BaseUIElement[]; private readonly _contents: BaseUIElement[][]; + private readonly _contentStyle: string[][]; - constructor(header: (BaseUIElement | string)[], contents: (BaseUIElement | string)[][]) { + constructor(header: (BaseUIElement | string)[], + contents: (BaseUIElement | string)[][], + contentStyle?: string[][]) { super(); - this._header = header.map(Translations.W); + this._contentStyle = contentStyle ?? []; + this._header = header?.map(Translations.W); this._contents = contents.map(row => row.map(Translations.W)); } @@ -28,15 +32,23 @@ export default class Table extends BaseUIElement { table.appendChild(tr) } - for (const row of this._contents) { + for (let i = 0; i < this._contents.length; i++){ + let row = this._contents[i]; const tr = document.createElement("tr") - for (const elem of row) { - const htmlElem = elem.ConstructElement() + for (let j = 0; j < row.length; j++){ + let elem = row[j]; + const htmlElem = elem?.ConstructElement() if (htmlElem === undefined) { continue; } + let style = undefined; + if(this._contentStyle !== undefined && this._contentStyle[i] !== undefined && this._contentStyle[j]!== undefined){ + style = this._contentStyle[i][j] + } + const td = document.createElement("td") + td.style.cssText = style; td.appendChild(htmlElem) tr.appendChild(td) } diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index c064f9ce6e..e7effe17fd 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -18,7 +18,7 @@ export class VariableUiElement extends BaseUIElement { } if (contents === undefined) { - return + return el; } if (typeof contents === "string") { el.innerHTML = contents diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 1570dd3ead..a919f0609c 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -102,6 +102,8 @@ export default abstract class BaseUIElement { if(this.InnerConstructElement === undefined){ throw "ERROR! This is not a correct baseUIElement: "+this.constructor.name } +try{ + const el = this.InnerConstructElement(); @@ -149,7 +151,13 @@ export default abstract class BaseUIElement { el.addEventListener('mouseout', () => self._onHover.setData(false)); } - return el + return el}catch(e){ + const domExc = e as DOMException; + if(domExc){ + console.log("An exception occured", domExc.code, domExc.message, domExc.name ) + } + console.error(e) +} } public AsMarkdown(): string{ diff --git a/UI/OpeningHours/OhVisualization.ts b/UI/OpeningHours/OhVisualization.ts deleted file mode 100644 index e060d0e6ae..0000000000 --- a/UI/OpeningHours/OhVisualization.ts +++ /dev/null @@ -1,324 +0,0 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import Combine from "../Base/Combine"; -import State from "../../State"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {OH} from "./OpeningHours"; -import Translations from "../i18n/Translations"; -import Constants from "../../Models/Constants"; -import opening_hours from "opening_hours"; -import BaseUIElement from "../BaseUIElement"; - -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(500, () => { - return tags.data._country === undefined; - })); - - - } - - private static GetRanges(oh: any, from: Date, to: Date): ({ - isOpen: boolean, - isSpecial: boolean, - comment: string, - startDate: Date, - endDate: Date - }[])[] { - - - const values = [[], [], [], [], [], [], []]; - - 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; - while (iterator.advance(to)) { - - if (prevValue) { - prevValue.endDate = iterator.getDate() as Date - } - const endDate = new Date(iterator.getDate()) as Date; - endDate.setHours(0, 0, 0, 0) - endDate.setDate(endDate.getDate() + 1); - const value = { - isSpecial: iterator.getUnknown(), - isOpen: iterator.getState(), - comment: iterator.getComment(), - startDate: iterator.getDate() as Date, - endDate: endDate // Should be overwritten by the next iteration - } - prevValue = value; - - if (value.comment === undefined && !value.isOpen && !value.isSpecial) { - // simply closed, nothing special here - continue; - } - - if (value.startDate < from) { - continue; - } - // Get day: sunday is 0, monday is 1. We move everything so that monday == 0 - values[(value.startDate.getDay() + 6) % 7].push(value); - } - return values; - } - - private static getMonday(d) { - d = new Date(d); - const day = d.getDay(); - const diff = d.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday - return new Date(d.setDate(diff)); - } - - InnerRender(): string | BaseUIElement { - - - 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: BaseUIElement[] = []; - const availableArea = latestclose - earliestOpen; - // @ts-ignore - const now = (100 * (((new Date() - today) / 1000) - earliestOpen)) / availableArea; - - - let header: BaseUIElement[] = []; - - 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 | BaseUIElement)[] = []; - - // 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 => new Combine(["" ,el , ""])), - "
    " - ]).SetClass("ohviz-container"); - } - - private static allChangeMoments(ranges: { - isOpen: boolean, - isSpecial: boolean, - comment: string, - startDate: Date, - endDate: Date - }[][]): [number[], string[]] { - const changeHours: number[] = [] - const changeHourText: string[] = []; - const extrachangeHours: number[] = [] - const extrachangeHourText: string[] = []; - - for (const weekday of ranges) { - for (const range of weekday) { - if (!range.isOpen && !range.isSpecial) { - continue; - } - const startOfDay: Date = new Date(range.startDate); - startOfDay.setHours(0, 0, 0, 0); - // @ts-ignore - const changeMoment: number = (range.startDate - startOfDay) / 1000; - if (changeHours.indexOf(changeMoment) < 0) { - changeHours.push(changeMoment); - changeHourText.push(OH.hhmm(range.startDate.getHours(), range.startDate.getMinutes())) - } - - // @ts-ignore - let changeMomentEnd: number = (range.endDate - startOfDay) / 1000; - if (changeMomentEnd >= 24 * 60 * 60) { - if (extrachangeHours.indexOf(changeMomentEnd) < 0) { - extrachangeHours.push(changeMomentEnd); - extrachangeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) - } - } else if (changeHours.indexOf(changeMomentEnd) < 0) { - changeHours.push(changeMomentEnd); - changeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) - } - } - } - - changeHourText.sort(); - changeHours.sort(); - extrachangeHourText.sort(); - extrachangeHours.sort(); - changeHourText.push(...extrachangeHourText); - changeHours.push(...extrachangeHours); - - return [changeHours, changeHourText] - } - -} \ No newline at end of file diff --git a/UI/OpeningHours/OpeningHours.ts b/UI/OpeningHours/OpeningHours.ts index 5164e963e8..ede3e87c40 100644 --- a/UI/OpeningHours/OpeningHours.ts +++ b/UI/OpeningHours/OpeningHours.ts @@ -8,6 +8,9 @@ export interface OpeningHour { endMinutes: number } +/** + * Various utilities manipulating opening hours + */ export class OH { @@ -163,6 +166,12 @@ export class OH { } + /** + * Gives the number of hours since the start of day. + * E.g. + * startTime({startHour: 9, startMinuts: 15}) == 9.25 + * @param oh + */ public static startTime(oh: OpeningHour): number { return oh.startHour + oh.startMinutes / 60; } @@ -348,5 +357,125 @@ export class OH { return ohs; } + + /* + This function converts a number of ranges (generated by OpeningHours.js) into all the hours of day that a change occurs. + E.g. + Monday, some business is opended from 9:00 till 17:00 + Tuesday from 9:30 till 18:00 + Wednesday from 9:30 till 12:30 + This function will extract all those moments of change and will return 9:00, 9:30, 12:30, 17:00 and 18:00 + This list will be sorted + */ + public static allChangeMoments(ranges: { + isOpen: boolean, + isSpecial: boolean, + comment: string, + startDate: Date, + endDate: Date + }[][]): [number[], string[]] { + const changeHours: number[] = [] + const changeHourText: string[] = []; + + const extrachangeHours: number[] = [] + const extrachangeHourText: string[] = []; + + for (const weekday of ranges) { + for (const range of weekday) { + if (!range.isOpen && !range.isSpecial) { + continue; + } + const startOfDay: Date = new Date(range.startDate); + startOfDay.setHours(0, 0, 0, 0); + + // The number of seconds since the start of the day + // @ts-ignore + const changeMoment: number = (range.startDate - startOfDay) / 1000; + if (changeHours.indexOf(changeMoment) < 0) { + changeHours.push(changeMoment); + changeHourText.push(OH.hhmm(range.startDate.getHours(), range.startDate.getMinutes())) + } + + // The number of seconds till between the start of the day and closing + // @ts-ignore + let changeMomentEnd: number = (range.endDate - startOfDay) / 1000; + if (changeMomentEnd >= 24 * 60 * 60) { + if (extrachangeHours.indexOf(changeMomentEnd) < 0) { + extrachangeHours.push(changeMomentEnd); + extrachangeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) + } + } else if (changeHours.indexOf(changeMomentEnd) < 0) { + changeHours.push(changeMomentEnd); + changeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) + } + } + } + + // Note that 'changeHours' and 'changeHourText' will be more or less in sync - one is in numbers, the other in 'HH:MM' format. + // But both can be sorted without problem; they'll stay in sync + changeHourText.sort(); + changeHours.sort(); + extrachangeHourText.sort(); + extrachangeHours.sort(); + + changeHourText.push(...extrachangeHourText); + changeHours.push(...extrachangeHours); + + return [changeHours, changeHourText] + } + + /* + Calculates when the business is opened (or on holiday) between two dates. + Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ... + */ + public static GetRanges(oh: any, from: Date, to: Date): ({ + isOpen: boolean, + isSpecial: boolean, + comment: string, + startDate: Date, + endDate: Date + }[])[] { + + + const values = [[], [], [], [], [], [], []]; + + 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; + while (iterator.advance(to)) { + + if (prevValue) { + prevValue.endDate = iterator.getDate() as Date + } + const endDate = new Date(iterator.getDate()) as Date; + endDate.setHours(0, 0, 0, 0) + endDate.setDate(endDate.getDate() + 1); + const value = { + isSpecial: iterator.getUnknown(), + isOpen: iterator.getState(), + comment: iterator.getComment(), + startDate: iterator.getDate() as Date, + endDate: endDate // Should be overwritten by the next iteration + } + prevValue = value; + + if (value.comment === undefined && !value.isOpen && !value.isSpecial) { + // simply closed, nothing special here + continue; + } + + if (value.startDate < from) { + continue; + } + // Get day: sunday is 0, monday is 1. We move everything so that monday == 0 + values[(value.startDate.getDay() + 6) % 7].push(value); + } + return values; + } + } diff --git a/UI/OpeningHours/OpeningHoursPicker.ts b/UI/OpeningHours/OpeningHoursPicker.ts index f38fb2565b..a74c41c4db 100644 --- a/UI/OpeningHours/OpeningHoursPicker.ts +++ b/UI/OpeningHours/OpeningHoursPicker.ts @@ -1,5 +1,4 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import OpeningHoursRange from "./OpeningHoursRange"; import Combine from "../Base/Combine"; import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; @@ -8,63 +7,39 @@ import {InputElement} from "../Input/InputElement"; import BaseUIElement from "../BaseUIElement"; export default class OpeningHoursPicker extends InputElement { - private readonly _ohs: UIEventSource; public readonly IsSelected: UIEventSource = new UIEventSource(false); - + private readonly _ohs: UIEventSource; private readonly _backgroundTable: OpeningHoursPickerTable; - private readonly _weekdays: UIEventSource = new UIEventSource([]); constructor(ohs: UIEventSource = new UIEventSource([])) { super(); this._ohs = ohs; - this._backgroundTable = new OpeningHoursPickerTable(this._weekdays, this._ohs); - const self = this; - - - this._ohs.addCallback(ohs => { - self._ohs.setData(OH.MergeTimes(ohs)); + + ohs.addCallback(oh => { + ohs.setData(OH.MergeTimes(oh)); }) - ohs.addCallbackAndRun(ohs => { - const perWeekday: UIElement[][] = []; - for (let i = 0; i < 7; i++) { - perWeekday[i] = []; - } - - for (const oh of ohs) { - const source = new UIEventSource(oh) - source.addCallback(_ => { - self._ohs.setData(OH.MergeTimes(self._ohs.data)) - }) - const r = new OpeningHoursRange(source, this._backgroundTable); - perWeekday[oh.weekday].push(r); - } - - for (let i = 0; i < 7; i++) { - self._weekdays.data[i] = new Combine(perWeekday[i]); - } - self._weekdays.ping(); - - }); + this._backgroundTable = new OpeningHoursPickerTable(this._ohs); + this._backgroundTable.ConstructElement() + ohs.ping(); } InnerRender(): BaseUIElement { return this._backgroundTable; } - protected InnerConstructElement(): HTMLElement { - return this._backgroundTable.ConstructElement(); - } - GetValue(): UIEventSource { return this._ohs } - IsValid(t: OpeningHour[]): boolean { return true; } + protected InnerConstructElement(): HTMLElement { + return this._backgroundTable.ConstructElement(); + } + } \ No newline at end of file diff --git a/UI/OpeningHours/OpeningHoursPickerTable.ts b/UI/OpeningHours/OpeningHoursPickerTable.ts index cd157aca12..bbdee204fb 100644 --- a/UI/OpeningHours/OpeningHoursPickerTable.ts +++ b/UI/OpeningHours/OpeningHoursPickerTable.ts @@ -3,19 +3,18 @@ * It will genarate the currently selected opening hour. */ import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; import {Utils} from "../../Utils"; import {OpeningHour} from "./OpeningHours"; import {InputElement} from "../Input/InputElement"; import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; +import {Translation} from "../i18n/Translation"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Combine from "../Base/Combine"; +import OpeningHoursRange from "./OpeningHoursRange"; export default class OpeningHoursPickerTable extends InputElement { - public readonly IsSelected: UIEventSource; - private readonly weekdays: UIEventSource; - private readonly _element: HTMLTableElement - - public static readonly days: BaseUIElement[] = + public static readonly days: Translation[] = [ Translations.t.general.weekdays.abbreviations.monday, Translations.t.general.weekdays.abbreviations.tuesday, @@ -25,60 +24,106 @@ export default class OpeningHoursPickerTable extends InputElement Translations.t.general.weekdays.abbreviations.saturday, Translations.t.general.weekdays.abbreviations.sunday ] - - + public readonly IsSelected: UIEventSource; private readonly source: UIEventSource; + + /* + These html-elements are an overlay over the table columns and act as a host for the ranges in the weekdays + */ + public readonly weekdayElements : HTMLElement[] = Utils.TimesT(7, () => document.createElement("div")) - private static _nextId = 0; - - constructor(weekdays: UIEventSource, source?: UIEventSource) { + constructor(source?: UIEventSource) { super(); - this.weekdays = weekdays; this.source = source ?? new UIEventSource([]); this.IsSelected = new UIEventSource(false); this.SetStyle("width:100%;height:100%;display:block;"); + } + IsValid(t: OpeningHour[]): boolean { + return true; + } - const id = OpeningHoursPickerTable._nextId; -OpeningHoursPickerTable._nextId ++ ; - - - let rows = ""; - const self = this; - for (let h = 0; h < 24; h++) { - let hs = "" + h; - if (hs.length == 1) { - hs = "0" + hs; - } - - - rows += `${hs}:00` + - Utils.Times(weekday => ``, 7) + - '' + - Utils.Times(id => ``, 7) + - ''; - } - let days = OpeningHoursPickerTable.days.map((day, i) => { - const innerContent = self.weekdays.data[i]?.ConstructElement()?.innerHTML ?? ""; - return day.ConstructElement().innerHTML + ""+innerContent+""; - }).join(""); - - this._element = document.createElement("table") - const el = this._element; - this.SetClass("oh-table") - el.innerHTML =`${days}${rows}`; + GetValue(): UIEventSource { + return this.source; } protected InnerConstructElement(): HTMLElement { - return this._element - } - private InnerUpdate(table: HTMLTableElement) { - const self = this; - if (table === undefined || table === null) { - return; + const table = document.createElement("table") + table.classList.add("oh-table") + + const headerRow = document.createElement("tr") + headerRow.appendChild(document.createElement("th")) + for (let i = 0; i < OpeningHoursPickerTable.days.length; i++) { + let weekday = OpeningHoursPickerTable.days[i].Clone(); + const cell = document.createElement("th") + cell.style.width = "14%" + cell.appendChild(weekday.ConstructElement()) + const fullColumnSpan = this.weekdayElements[i] + fullColumnSpan.classList.add("w-full","h-full","relative") + fullColumnSpan.style.height = "42rem" + + + const ranges = new VariableUiElement( + this.source.map(ohs => ohs.filter((oh : OpeningHour) => oh.weekday === i)) + .map(ohsForToday => { + return new Combine(ohsForToday.map(oh => new OpeningHoursRange(oh, () =>{ + this.source.data.splice(this.source.data.indexOf(oh), 1) + this.source.ping() + }))) + }) + ) + fullColumnSpan.appendChild(ranges.ConstructElement()) + + + + + const fullColumnSpanWrapper = document.createElement("div") + fullColumnSpanWrapper.classList.add("absolute") + fullColumnSpanWrapper.style.zIndex = "10" + fullColumnSpanWrapper.style.width = "13.5%" + fullColumnSpanWrapper.style.pointerEvents = "none" + + fullColumnSpanWrapper.appendChild(fullColumnSpan) + + cell.appendChild(fullColumnSpanWrapper) + headerRow.appendChild(cell) } + table.appendChild(headerRow) + + const self = this; + for (let h = 0; h < 24; h++) { + + const hs = Utils.TwoDigits(h); + const firstCell = document.createElement("td") + firstCell.rowSpan = 2 + firstCell.classList.add("oh-left-col", "oh-timecell-full", "border-box","h-2") + firstCell.appendChild(new FixedUiElement(hs + ":00").ConstructElement()) + + const evenRow = document.createElement("tr") + evenRow.appendChild(firstCell); + + for (let weekday = 0; weekday < 7; weekday++) { + const cell = document.createElement("td") + cell.classList.add("oh-timecell", "oh-timecell-full", `oh-timecell-${weekday}`) + evenRow.appendChild(cell) + } + table.appendChild(evenRow) + + const oddRow = document.createElement("tr") + + for (let weekday = 0; weekday < 7; weekday++) { + const cell = document.createElement("td") + cell.classList.add("oh-timecell", "oh-timecell-half", `oh-timecell-${weekday}`) + oddRow.appendChild(cell) + } + table.appendChild(oddRow) + } + + + /**** Event handling below ***/ + let mouseIsDown = false; let selectionStart: [number, number] = undefined; @@ -123,6 +168,7 @@ OpeningHoursPickerTable._nextId ++ ; oh.endMinutes = 0; } self.source.data.push(oh); + console.log("Created ", oh) } self.source.ping(); @@ -149,6 +195,7 @@ OpeningHoursPickerTable._nextId ++ ; }; let lastSelectionIend, lastSelectionJEnd; + function selectAllBetween(iEnd, jEnd) { if (lastSelectionIend === iEnd && lastSelectionJEnd === jEnd) { @@ -218,9 +265,9 @@ OpeningHoursPickerTable._nextId ++ ; jStart <= j + offset && j + offset <= jEnd) { cell?.classList?.add("oh-timecell-selected") } else { - cell?.classList?.remove("oh-timecell-selected") + cell?.classList?.remove("oh-timecell-selected") } - + } @@ -263,7 +310,7 @@ OpeningHoursPickerTable._nextId ++ ; ev.preventDefault(); for (const k in ev.targetTouches) { const touch = ev.targetTouches[k]; - if(touch.clientX === undefined || touch.clientY === undefined){ + if (touch.clientX === undefined || touch.clientY === undefined) { continue; } const elUnderTouch = document.elementFromPoint( @@ -287,15 +334,7 @@ OpeningHoursPickerTable._nextId ++ ; } - - } - - IsValid(t: OpeningHour[]): boolean { - return true; - } - - GetValue(): UIEventSource { - return this.source; + return table } } \ No newline at end of file diff --git a/UI/OpeningHours/OpeningHoursRange.ts b/UI/OpeningHours/OpeningHoursRange.ts index 972a4a5b1b..24ec3db4d5 100644 --- a/UI/OpeningHours/OpeningHoursRange.ts +++ b/UI/OpeningHours/OpeningHoursRange.ts @@ -1,73 +1,57 @@ /** * A single opening hours range, shown on top of the OH-picker table */ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; import Svg from "../../Svg"; import {Utils} from "../../Utils"; import Combine from "../Base/Combine"; import {OH, OpeningHour} from "./OpeningHours"; -import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; import BaseUIElement from "../BaseUIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; -export default class OpeningHoursRange extends UIElement { - private _oh: UIEventSource; +export default class OpeningHoursRange extends BaseUIElement { + private _oh: OpeningHour; - private readonly _startTime: BaseUIElement; - private readonly _endTime: BaseUIElement; - private readonly _deleteRange: BaseUIElement; - private readonly _tableId: OpeningHoursPickerTable; + private readonly _onDelete: () => void; - constructor(oh: UIEventSource, tableId: OpeningHoursPickerTable) { - super(oh); - this._tableId = tableId; - const self = this; + constructor(oh: OpeningHour, onDelete: () => void) { + super(); this._oh = oh; + this._onDelete = onDelete; this.SetClass("oh-timerange"); - oh.addCallbackAndRun(() => { - const el = document.getElementById(this.id) as HTMLElement; - self.InnerUpdate(el); - }) - - this._deleteRange = - Svg.delete_icon_ui() - .SetClass("oh-delete-range") - .onClick(() => { - oh.data.weekday = undefined; - oh.ping(); - }); - - - this._startTime = new VariableUiElement(oh.map(oh => { - return Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes); - })).SetClass("oh-timerange-label") - - this._endTime = new VariableUiElement(oh.map(oh => { - return Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes); - })).SetClass("oh-timerange-label") - } - InnerRender(): BaseUIElement { - const oh = this._oh.data; - if (oh === undefined) { - return undefined; - } + InnerConstructElement(): HTMLElement { const height = this.getHeight(); + const oh = this._oh; + const startTime = new FixedUiElement(Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes)).SetClass("oh-timerange-label") + const endTime = new FixedUiElement(Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes)).SetClass("oh-timerange-label") - let content = [this._deleteRange] + + const deleteRange = + Svg.delete_icon_ui() + .SetClass("oh-delete-range") + .onClick(() => { + this._onDelete() + }); + + + let content = [deleteRange] if (height > 2) { - content = [this._startTime, this._deleteRange, this._endTime]; + content = [startTime, deleteRange, endTime]; } - return new Combine(content) - .SetClass("oh-timerange-inner") + const el = new Combine(content) + .SetClass("oh-timerange-inner").ConstructElement(); + + el.style.top = (100 * OH.startTime(oh) / 24) + "%" + el.style.height = (100 * (OH.endTime(oh) - OH.startTime(oh)) / 24) + "%" + return el; } + private getHeight(): number { - const oh = this._oh.data; + const oh = this._oh; let endhour = oh.endHour; if (oh.endHour == 0 && oh.endMinutes == 0) { @@ -76,28 +60,5 @@ export default class OpeningHoursRange extends UIElement { return (endhour - oh.startHour + ((oh.endMinutes - oh.startMinutes) / 60)); } - protected InnerUpdate(el: HTMLElement) { - if (el == null) { - return; - } - const oh = this._oh.data; - if (oh === undefined) { - return; - } - - // The header cell containing monday, tuesday, ... - const table = this._tableId.ConstructElement() as HTMLTableElement; - - const bodyRect = document.body.getBoundingClientRect(); - const rangeStart = table.rows[1].cells[1].getBoundingClientRect().top - bodyRect.top; - const rangeEnd = table.rows[table.rows.length - 1].cells[1].getBoundingClientRect().bottom - bodyRect.top; - - const pixelsPerHour = (rangeEnd - rangeStart) / 24; - - el.style.top = (pixelsPerHour * OH.startTime(oh)) + "px"; - el.style.height = (pixelsPerHour * (OH.endTime(oh) - OH.startTime(oh))) + "px"; - - } - } \ No newline at end of file diff --git a/UI/OpeningHours/OpeningHoursVisualization.ts b/UI/OpeningHours/OpeningHoursVisualization.ts new file mode 100644 index 0000000000..73d3b08ccd --- /dev/null +++ b/UI/OpeningHours/OpeningHoursVisualization.ts @@ -0,0 +1,292 @@ +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import State from "../../State"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {OH} from "./OpeningHours"; +import Translations from "../i18n/Translations"; +import Constants from "../../Models/Constants"; +import opening_hours from "opening_hours"; +import BaseUIElement from "../BaseUIElement"; +import Toggle from "../Input/Toggle"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Table from "../Base/Table"; +import {Translation} from "../i18n/Translation"; +import {UIElement} from "../UIElement"; + +export default class OpeningHoursVisualization extends UIElement { + private static readonly weekdays: Translation[] = [ + 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 _tags: UIEventSource; + private readonly _key: string; + + constructor(tags: UIEventSource, key: string) { + super() + this._tags = tags; + this._key = key; + } + + InnerRender(): BaseUIElement { + const tags = this._tags; + const key = this._key; + const tagsDirect = tags.data; + const ohTable = new VariableUiElement(tags + .map(tags => tags[key]) // This mapping will absorb all other changes to tags in order to prevent regeneration + .map(ohtext => { + try { + // noinspection JSPotentiallyInvalidConstructorUsage + const oh = new opening_hours(ohtext, { + lat: tagsDirect._lat, + lon: tagsDirect._lon, + address: { + country_code: tagsDirect._country + } + }, {tag_key: this._key}); + + return OpeningHoursVisualization.CreateFullVisualisation(oh) + } catch (e) { + console.log(e); + return new Combine([Translations.t.general.opening_hours.error_loading, + new Toggle( + new FixedUiElement(e).SetClass("subtle"), + undefined, + State.state?.osmConnection?.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) + ) + ]); + } + + } + )) + + return new Toggle( + ohTable, + Translations.t.general.loadingCountry.Clone(), + tags.map(tgs => tgs._country !== undefined) + ); + } + + private static CreateFullVisualisation(oh: any): BaseUIElement { + + /** First, we determine which range of dates we want to visualize: this week or next week?**/ + + 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); + + 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 move the range to next week! + lastMonday.setDate(lastMonday.getDate() + 7); + nextSunday.setDate(nextSunday.getDate() + 7); + } + } + + + /* We calculate the ranges when it is opened! */ + const ranges = OH.GetRanges(oh, lastMonday, nextSunday); + + /* First, a small sanity check. The business might be permanently closed, 24/7 opened or something other special + * So, we have to handle the case that ranges is completely empty*/ + if (ranges.filter(range => range.length > 0).length === 0) { + return OpeningHoursVisualization.ShowSpecialCase(oh).SetClass("p-4 rounded-full block bg-gray-200") + } + + /** With all the edge cases handled, we can actually construct the table! **/ + + return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday) + + + } + + private static ConstructVizTable(oh: any, ranges: { isOpen: boolean; isSpecial: boolean; comment: string; startDate: Date; endDate: Date }[][], + rangeStart: Date): BaseUIElement { + + + const isWeekstable: boolean = oh.isWeekStable(); + let [changeHours, changeHourText] = OH.allChangeMoments(ranges); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // @ts-ignore + const todayIndex = Math.ceil((today - rangeStart) / (1000 * 60 * 60 * 24)) + // 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 availableArea = latestclose - earliestOpen; + + /* + * The OH-visualisation is a table, consisting of 8 rows and 2 columns: + * The first row is a header row (which is NOT passed as header but just as a normal row!) containing empty for the first column and one object giving all the end times + * The other rows are one for each weekday: the first element showing 'mo', 'tu', ..., the second element containing the bars. + * Note that the bars are actually an embedded
    spanning the full width, containing multiple sub-elements + * */ + + const [header, headerHeight] = OpeningHoursVisualization.ConstructHeaderElement(availableArea, changeHours, changeHourText, earliestOpen) + + const weekdays = [] + const weekdayStyles = [] + for (let i = 0; i < 7; i++) { + + const day = OpeningHoursVisualization.weekdays[i].Clone(); + day.SetClass("w-full h-full block") + + const rangesForDay = ranges[i].map(range => + OpeningHoursVisualization.CreateRangeElem(availableArea, earliestOpen, latestclose, range, isWeekstable) + ) + const allRanges = new Combine([ + ...OpeningHoursVisualization.CreateLinesAtChangeHours(changeHours, availableArea, earliestOpen) , + ...rangesForDay]).SetClass("w-full block"); + + let extraStyle = "" + if (todayIndex == i) { + extraStyle = "background-color: var(--subtle-detail-color);" + allRanges.SetClass("ohviz-today") + } else if (i >= 5) { + extraStyle = "background-color: rgba(230, 231, 235, 1);" + } + weekdays.push([day, allRanges]) + weekdayStyles.push(["padding-left: 0.5em;" + extraStyle, `position: relative;` + extraStyle]) + } + return new Table(undefined, + [[" ", header], ...weekdays], + [["width: 5%", `position: relative; height: ${headerHeight}`], ...weekdayStyles] + ).SetClass("w-full") + .SetStyle("border-collapse: collapse; word-break; word-break: normal; word-wrap: normal") + + + } + + private static CreateRangeElem(availableArea: number, earliestOpen: number, latestclose: number, + range: { isOpen: boolean; isSpecial: boolean; comment: string; startDate: Date; endDate: Date }, + isWeekstable: boolean): BaseUIElement { + + const textToShow = range.comment ?? (isWeekstable ? "" : range.startDate.toLocaleDateString()); + + if (!range.isOpen && !range.isSpecial) { + return new FixedUiElement(textToShow).SetClass("ohviz-day-off") + } + + 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); + return new FixedUiElement(textToShow).SetStyle(`left:${startPercentage}%; width:${width}%`) + .SetClass("ohviz-range"); + } + + private static CreateLinesAtChangeHours(changeHours: number[], availableArea: number, earliestOpen: number): + BaseUIElement[] { + + const allLines: BaseUIElement[] = [] + 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"); + allLines.push(el); + } + return allLines; + } + + + /** + * The OH-Visualization header element, a single bar with hours + * @param availableArea + * @param changeHours + * @param changeHourText + * @param earliestOpen + * @constructor + * @private + */ + private static ConstructHeaderElement(availableArea: number, changeHours: number[], changeHourText: string[], earliestOpen: number) + : [BaseUIElement, string] { + let header: BaseUIElement[] = []; + + header.push(...OpeningHoursVisualization.CreateLinesAtChangeHours(changeHours, availableArea, earliestOpen)) + + let showHigher = false; + let showHigherUsed = false; + for (let i = 0; i < changeHours.length; i++) { + let changeMoment = changeHours[i]; + const offset = 100 * (changeMoment - earliestOpen) / availableArea; + if (offset < 0 || offset > 100) { + continue; + } + + if (i > 0 && ((changeMoment - changeHours[i - 1]) / (60*60)) < 2) { + // Quite close to the previous value + // We alternate the heights + showHigherUsed = true; + showHigher = !showHigher; + } else { + showHigher = false; + } + + const el = new Combine([ + + new FixedUiElement(changeHourText[i]) + .SetClass("relative bg-white pl-1 pr-1 h-3 font-sm rounded-xl border-2 border-black border-opacity-50") + .SetStyle("left: -50%; word-break:initial") + + ]) + .SetStyle(`left:${offset}%;margin-top: ${showHigher ? '1.4rem;' : "0.1rem"}`) + .SetClass("block absolute top-0 m-0 h-full box-border ohviz-time-indication"); + header.push(el); + } + const headerElem = new Combine(header).SetClass(`w-full absolute block ${showHigherUsed ? "h-16" : "h-8"}`) + .SetStyle("margin-top: -1rem") + const headerHeight = showHigherUsed ? "4rem" : "2rem"; + return [headerElem, headerHeight] + + } + + /* + * Visualizes any special case: e.g. not open for a long time, 24/7 open, ... + * */ + private static ShowSpecialCase(oh: any) { + const opensAtDate = oh.getNextChange(); + if (opensAtDate === undefined) { + const comm = oh.getComment() ?? oh.getUnknown(); + if (!!comm) { + return new FixedUiElement(comm) + } + + if (oh.getState()) { + return Translations.t.general.opening_hours.open_24_7.Clone() + } + return Translations.t.general.opening_hours.closed_permanently.Clone() + } + const willOpenAt = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}` + return Translations.t.general.opening_hours.closed_until.Subs({date: willOpenAt}) + } + + private static getMonday(d) { + d = new Date(d); + const day = d.getDay(); + const diff = d.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday + return new Date(d.setDate(diff)); + } + +} \ No newline at end of file diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 373f29ef67..a420c73b8e 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -18,6 +18,7 @@ export default class EditableTagRendering extends Toggle { const editMode = new UIEventSource(false); const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration) + answer.SetClass("w-full") let rendering = answer; if (configuration.question !== undefined && State.state?.featureSwitchUserbadge?.data) { diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index d559b7cf2c..128a904a48 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -109,7 +109,6 @@ export default class ShowDataLayer { }); } - private postProcessFeature(feature, leafletLayer: L.Layer) { const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; if (layer === undefined) { @@ -156,7 +155,7 @@ export default class ShowDataLayer { if (selected === undefined || self._leafletMap.data === undefined) { return; } - if (popup.isOpen()) { + if (leafletLayer.getPopup().isOpen()) { return; } if (selected.properties.id === feature.properties.id) { diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index b1f1a5559d..86766c5b42 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -12,7 +12,7 @@ import ReviewElement from "./Reviews/ReviewElement"; import MangroveReviews from "../Logic/Web/MangroveReviews"; import Translations from "./i18n/Translations"; import ReviewForm from "./Reviews/ReviewForm"; -import OpeningHoursVisualization from "./OpeningHours/OhVisualization"; +import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; import State from "../State"; import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; @@ -120,11 +120,7 @@ export default class SpecialVisualizations { doc: "The tagkey from which the table is constructed." }], constr: (state: State, tagSource: UIEventSource, args) => { - let keyname = args[0]; - if (keyname === undefined || keyname === "") { - keyname = keyname ?? "opening_hours" - } - return new OpeningHoursVisualization(tagSource, keyname) + return new OpeningHoursVisualization(tagSource, args[0]) } }, diff --git a/UI/UIElement.ts b/UI/UIElement.ts index 565af7830c..d1802bcdae 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -79,8 +79,6 @@ export abstract class UIElement extends BaseUIElement{ } } - - /** * @deprecated The method should not be used diff --git a/Utils.ts b/Utils.ts index ba18b98d99..a7cbcb3e6c 100644 --- a/Utils.ts +++ b/Utils.ts @@ -73,6 +73,14 @@ export class Utils { return res; } + public static TimesT(count : number, f: ((i: number) => T)): T[] { + let res : T[] = []; + for (let i = 0; i < count; i++) { + res .push(f(i)); + } + return res; + } + static DoEvery(millis: number, f: (() => void)) { if (Utils.runningFromConsole) { return; diff --git a/css/openinghourstable.css b/css/openinghourstable.css index 20d76cf29a..a18958a5e1 100644 --- a/css/openinghourstable.css +++ b/css/openinghourstable.css @@ -103,40 +103,31 @@ border-right: 10px solid var(--catch-detail-color) !important; } - -.oh-draggable-header { - background-color: blue; - height: 0.5em; -} - .oh-timerange { + color: var(--catch-detail-color-contrast); border-radius: 0.5em; - margin-left: 2px; display: block; position: absolute; top: 0; left: 0; - width: calc(100% - 4px); + margin-left: calc(5% - 1px); + width: 90%; background: var(--catch-detail-color); z-index: 1; box-sizing: border-box; + border: 2px solid var(--catch-detail-color); + overflow: unset; } .oh-timerange-inner { display: flex; flex-direction: column; - overflow-x: hidden; - justify-content: space-between; + justify-content: center; align-content: center; height: 100%; - overflow-y: hidden; + overflow-x: unset; } -.oh-timerange-inner input { - width: 100%; - box-sizing: border-box; - } - .oh-timerange-inner-small { display: flex; flex-direction: row; @@ -144,12 +135,6 @@ height: 100%; width:100%; } - -.oh-timerange-inner-small input { - width: min-content; - box-sizing: border-box; -} - .oh-delete-range{ width: 1.5em; height: 1.5em; @@ -162,10 +147,6 @@ max-width: 2em; } -.oh-timerange-label { - color: white; -} - /**** Opening hours visualization table ****/ @@ -190,7 +171,6 @@ .ohviz-today .ohviz-range { border: 1.5px solid black; - } .ohviz-day-off { @@ -235,70 +215,12 @@ border-radius: 1em; } -.ohviz-now { - position: absolute; - top: 0; - margin: 0; - height: 100%; - border: 1px solid black; - box-sizing: border-box -} .ohviz-line { position: absolute; top: 0; margin: 0; height: 100%; - border-left: 1px solid #ccc; + border-left: 1px solid #999; box-sizing: border-box } - - -.ohviz-time-indication > div { - position: relative; - background-color: white; - left: -50%; - padding-left: 0.3em; - padding-right: 0.3em; - font-size: smaller; - border-radius: 0.3em; - border: 1px solid #ccc; - word-break: initial; - -} - -.ohviz-time-indication { - position: absolute; - top: 0; - margin: 0; - height: 100%; - box-sizing: border-box; -} - - -.ohviz-today { - background-color: var(--subtle-detail-color); -} - -.ohviz-weekday { - padding-left: 0.5em; - word-break: normal; -} - - -.ohviz { - border-collapse: collapse; -} - -.ohviz-container { - border: 0.5em solid var(--subtle-detail-color); - border-radius: 1em; - display: block; -} - -.ohviz-closed { - padding: 1em; - background-color: #eee; - border-radius: 1em; - display: block; -} \ No newline at end of file diff --git a/langs/en.json b/langs/en.json index b92fc6e2b6..f5cd5594e6 100644 --- a/langs/en.json +++ b/langs/en.json @@ -121,6 +121,7 @@ "zoomInToSeeThisLayer": "Zoom in to see this layer", "title": "Select layers" }, + "loadingCountry": "Determining country...", "weekdays": { "abbreviations": { "monday": "Mon", diff --git a/test.ts b/test.ts index ee47136d1c..eeb50e7af4 100644 --- a/test.ts +++ b/test.ts @@ -3,21 +3,36 @@ import SpecialVisualizations from "./UI/SpecialVisualizations"; import State from "./State"; import Combine from "./UI/Base/Combine"; import {FixedUiElement} from "./UI/Base/FixedUiElement"; +import OpeningHoursVisualization from "./UI/OpeningHours/OpeningHoursVisualization"; +import OpeningHoursPickerTable from "./UI/OpeningHours/OpeningHoursPickerTable"; +import OpeningHoursPicker from "./UI/OpeningHours/OpeningHoursPicker"; +import {OH, OpeningHour} from "./UI/OpeningHours/OpeningHours"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; const tagsSource = new UIEventSource({ - id:'id', - name:'name', - surface:'asphalt', + id: 'id', + name: 'name', + surface: 'asphalt', image: "https://i.imgur.com/kX3rl3v.jpg", "image:1": "https://i.imgur.com/oHAJqMB.jpg", - // "opening_hours":"mo-fr 09:00-18:00", - _country:"be", + "opening_hours": "mo-fr 09:00-18:00", + _country: "be", }) const state = new State(undefined) State.state = state +const ohData = new UIEventSource([{ + weekday: 1, + startHour: 10, + startMinutes: 0 + , endHour: 12, + endMinutes: 0 +}]) +new OpeningHoursPicker(ohData).AttachTo("maindiv") +new VariableUiElement(ohData.map(OH.ToString)).AttachTo("extradiv") +/* const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { try{ @@ -28,4 +43,4 @@ const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { return new FixedUiElement("Could not construct "+spec.funcName+" due to "+e).SetClass("alert") } }) -new Combine(allSpecials).AttachTo("maindiv") \ No newline at end of file +new Combine(allSpecials).AttachTo("maindiv")*/ \ No newline at end of file From 48f66bd17ecb340ebbb76930329430e00b946f5a Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 16 Jun 2021 16:39:48 +0200 Subject: [PATCH 15/30] Last fixes to the OH picker --- InitUiElements.ts | 1 + UI/OpeningHours/OpeningHours.ts | 256 +++++++++++++-------- UI/OpeningHours/OpeningHoursInput.ts | 6 +- UI/OpeningHours/OpeningHoursPicker.ts | 4 - UI/OpeningHours/OpeningHoursPickerTable.ts | 18 +- UI/OpeningHours/OpeningHoursRange.ts | 20 +- UI/OpeningHours/PublicHolidayInput.ts | 243 ++++++++----------- css/openinghourstable.css | 30 +-- langs/en.json | 3 +- test.ts | 12 +- test/Tag.spec.ts | 45 +++- 11 files changed, 327 insertions(+), 311 deletions(-) diff --git a/InitUiElements.ts b/InitUiElements.ts index 75b5824a4d..cb7cdc5623 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -210,6 +210,7 @@ export class InitUiElements { // Reset the loading message once things are loaded new CenterMessageBox().AttachTo("centermessage"); + document.getElementById("centermessage").classList.add("pointer-events-none") } diff --git a/UI/OpeningHours/OpeningHours.ts b/UI/OpeningHours/OpeningHours.ts index ede3e87c40..1b43f16c7b 100644 --- a/UI/OpeningHours/OpeningHours.ts +++ b/UI/OpeningHours/OpeningHours.ts @@ -49,10 +49,10 @@ export class OH { let rangeStart = 0; let rangeEnd = 0; - - function pushRule(){ + + function pushRule() { const rule = stringPerWeekday[rangeStart]; - if(rule === ""){ + if (rule === "") { return; } if (rangeStart == (rangeEnd - 1)) { @@ -61,7 +61,7 @@ export class OH { ); } else { rules.push( - `${OH.days[rangeStart]}-${OH.days[rangeEnd-1]} ${rule}` + `${OH.days[rangeStart]}-${OH.days[rangeEnd - 1]} ${rule}` ); } } @@ -90,13 +90,22 @@ export class OH { * @constructor */ public static MergeTimes(ohs: OpeningHour[]): OpeningHour[] { - const queue = [...ohs]; + const queue = ohs.map(oh => { + if (oh.endHour === 0 && oh.endMinutes === 0) { + const newOh = { + ...oh + } + newOh.endHour = 24 + return newOh + } + return oh; + }); const newList = []; while (queue.length > 0) { let maybeAdd = queue.pop(); let doAddEntry = true; - if(maybeAdd.weekday == undefined){ + if (maybeAdd.weekday == undefined) { doAddEntry = false; } @@ -140,8 +149,8 @@ export class OH { queue.push({ startHour: startHour, startMinutes: startMinutes, - endHour:endHour, - endMinutes:endMinutes, + endHour: endHour, + endMinutes: endMinutes, weekday: guard.weekday }); @@ -190,21 +199,6 @@ export class OH { OH.endTime(checked) <= OH.endTime(mightLieIn) } - private static parseHHMM(hhmm: string): { hours: number, minutes: number } { - if(hhmm === undefined || hhmm == null){ - return null; - } - const spl = hhmm.trim().split(":"); - if(spl.length != 2){ - return null; - } - const hm = {hours: Number(spl[0].trim()), minutes: Number(spl[1].trim())}; - if(isNaN(hm.hours) || isNaN(hm.minutes) ){ - return null; - } - return hm; - } - public static parseHHMMRange(hhmmhhmm: string): { startHour: number, startMinutes: number, @@ -226,80 +220,6 @@ export class OH { } } - private static ParseHhmmRanges(hhmms: string): { - startHour: number, - startMinutes: number, - endHour: number, - endMinutes: number - }[] { - if (hhmms === "off") { - return []; - } - return hhmms.split(",") - .map(s => s.trim()) - .filter(str => str !== "") - .map(OH.parseHHMMRange) - .filter(v => v != null) - } - - private static ParseWeekday(weekday: string): number { - return OH.daysIndexed[weekday.trim().toLowerCase()]; - } - - private static ParseWeekdayRange(weekdays: string): number[] { - const split = weekdays.split("-"); - if (split.length == 1) { - const parsed = OH.ParseWeekday(weekdays); - if(parsed == null){ - return null; - } - return [parsed]; - } else if (split.length == 2) { - let start = OH.ParseWeekday(split[0]); - let end = OH.ParseWeekday(split[1]); - if ((start ?? null) === null || (end ?? null) === null) { - return null; - } - let range = []; - for (let i = start; i <= end; i++) { - range.push(i); - } - return range; - } else { - return null; - } - } - - private static ParseWeekdayRanges(weekdays: string): number[] { - let ranges = []; - let split = weekdays.split(","); - for (const weekday of split) { - const parsed = OH.ParseWeekdayRange(weekday) - if (parsed === undefined || parsed === null) { - return null; - } - ranges.push(...parsed); - } - return ranges; - } - - private static multiply(weekdays: number[], timeranges: { startHour: number, startMinutes: number, endHour: number, endMinutes: number }[]) { - if ((weekdays ?? null) == null || (timeranges ?? null) == null) { - return null; - } - const ohs: OpeningHour[] = [] - for (const timerange of timeranges) { - for (const weekday of weekdays) { - ohs.push({ - weekday: weekday, - startHour: timerange.startHour, startMinutes: timerange.startMinutes, - endHour: timerange.endHour, endMinutes: timerange.endMinutes, - }); - } - } - return ohs; - } - public static ParseRule(rule: string): OpeningHour[] { try { if (rule.trim() == "24/7") { @@ -331,6 +251,49 @@ export class OH { } } + public static ParsePHRule(str: string): { + mode: string, + start?: string, + end?: string + } { + str = str.trim(); + if (!str.startsWith("PH")) { + return null; + } + + str = str.trim(); + if (str === "PH off") { + return { + mode: "off" + } + } + + if (str === "PH open") { + return { + mode: "open" + } + } + + if (!str.startsWith("PH ")) { + return null; + } + try { + + const timerange = OH.parseHHMMRange(str.substring(2)); + if (timerange === null) { + return null; + } + + return { + mode: " ", + start: OH.hhmm(timerange.startHour, timerange.startMinutes), + end: OH.hhmm(timerange.endHour, timerange.endMinutes), + + } + } catch (e) { + return null; + } + } static Parse(rules: string) { if (rules === undefined || rules === "") { @@ -342,7 +305,7 @@ export class OH { const split = rules.split(";"); for (const rule of split) { - if(rule === ""){ + if (rule === "") { continue; } try { @@ -376,7 +339,7 @@ export class OH { }[][]): [number[], string[]] { const changeHours: number[] = [] const changeHourText: string[] = []; - + const extrachangeHours: number[] = [] const extrachangeHourText: string[] = []; @@ -387,7 +350,7 @@ export class OH { } const startOfDay: Date = new Date(range.startDate); startOfDay.setHours(0, 0, 0, 0); - + // The number of seconds since the start of the day // @ts-ignore const changeMoment: number = (range.startDate - startOfDay) / 1000; @@ -417,7 +380,7 @@ export class OH { changeHours.sort(); extrachangeHourText.sort(); extrachangeHours.sort(); - + changeHourText.push(...extrachangeHourText); changeHours.push(...extrachangeHours); @@ -476,6 +439,95 @@ export class OH { } return values; } - + + private static parseHHMM(hhmm: string): { hours: number, minutes: number } { + if (hhmm === undefined || hhmm == null) { + return null; + } + const spl = hhmm.trim().split(":"); + if (spl.length != 2) { + return null; + } + const hm = {hours: Number(spl[0].trim()), minutes: Number(spl[1].trim())}; + if (isNaN(hm.hours) || isNaN(hm.minutes)) { + return null; + } + return hm; + } + + private static ParseHhmmRanges(hhmms: string): { + startHour: number, + startMinutes: number, + endHour: number, + endMinutes: number + }[] { + if (hhmms === "off") { + return []; + } + return hhmms.split(",") + .map(s => s.trim()) + .filter(str => str !== "") + .map(OH.parseHHMMRange) + .filter(v => v != null) + } + + private static ParseWeekday(weekday: string): number { + return OH.daysIndexed[weekday.trim().toLowerCase()]; + } + + private static ParseWeekdayRange(weekdays: string): number[] { + const split = weekdays.split("-"); + if (split.length == 1) { + const parsed = OH.ParseWeekday(weekdays); + if (parsed == null) { + return null; + } + return [parsed]; + } else if (split.length == 2) { + let start = OH.ParseWeekday(split[0]); + let end = OH.ParseWeekday(split[1]); + if ((start ?? null) === null || (end ?? null) === null) { + return null; + } + let range = []; + for (let i = start; i <= end; i++) { + range.push(i); + } + return range; + } else { + return null; + } + } + + private static ParseWeekdayRanges(weekdays: string): number[] { + let ranges = []; + let split = weekdays.split(","); + for (const weekday of split) { + const parsed = OH.ParseWeekdayRange(weekday) + if (parsed === undefined || parsed === null) { + return null; + } + ranges.push(...parsed); + } + return ranges; + } + + private static multiply(weekdays: number[], timeranges: { startHour: number, startMinutes: number, endHour: number, endMinutes: number }[]) { + if ((weekdays ?? null) == null || (timeranges ?? null) == null) { + return null; + } + const ohs: OpeningHour[] = [] + for (const timerange of timeranges) { + for (const weekday of weekdays) { + ohs.push({ + weekday: weekday, + startHour: timerange.startHour, startMinutes: timerange.startMinutes, + endHour: timerange.endHour, endMinutes: timerange.endMinutes, + }); + } + } + return ohs; + } + } diff --git a/UI/OpeningHours/OpeningHoursInput.ts b/UI/OpeningHours/OpeningHoursInput.ts index 1ac60467fa..f19a2cf6e3 100644 --- a/UI/OpeningHours/OpeningHoursInput.ts +++ b/UI/OpeningHours/OpeningHoursInput.ts @@ -37,14 +37,14 @@ export default class OpeningHoursInput extends InputElement { if (OH.ParseRule(rule) !== null) { continue; } - if (PublicHolidayInput.LoadValue(rule) !== null) { + if (OH.ParsePHRule(rule) !== null) { continue; } leftOvers.push(rule); } return leftOvers; }) - // NOte: MUST be bound AFTER the leftover rules! + // Note: MUST be bound AFTER the leftover rules! const rulesFromOhPicker = value.map(OH.Parse); const ph = value.map(str => { @@ -53,7 +53,7 @@ export default class OpeningHoursInput extends InputElement { } const rules = str.split(";"); for (const rule of rules) { - if (PublicHolidayInput.LoadValue(rule) !== null) { + if (OH.ParsePHRule(rule) !== null) { return rule; } } diff --git a/UI/OpeningHours/OpeningHoursPicker.ts b/UI/OpeningHours/OpeningHoursPicker.ts index a74c41c4db..e0c339f5b1 100644 --- a/UI/OpeningHours/OpeningHoursPicker.ts +++ b/UI/OpeningHours/OpeningHoursPicker.ts @@ -1,6 +1,4 @@ import {UIEventSource} from "../../Logic/UIEventSource"; -import OpeningHoursRange from "./OpeningHoursRange"; -import Combine from "../Base/Combine"; import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; import {OH, OpeningHour} from "./OpeningHours"; import {InputElement} from "../Input/InputElement"; @@ -22,8 +20,6 @@ export default class OpeningHoursPicker extends InputElement { this._backgroundTable = new OpeningHoursPickerTable(this._ohs); this._backgroundTable.ConstructElement() - - ohs.ping(); } InnerRender(): BaseUIElement { diff --git a/UI/OpeningHours/OpeningHoursPickerTable.ts b/UI/OpeningHours/OpeningHoursPickerTable.ts index bbdee204fb..f9a430ce37 100644 --- a/UI/OpeningHours/OpeningHoursPickerTable.ts +++ b/UI/OpeningHours/OpeningHoursPickerTable.ts @@ -51,17 +51,23 @@ export default class OpeningHoursPickerTable extends InputElement const table = document.createElement("table") table.classList.add("oh-table") + + const cellHeightInPx = 14; const headerRow = document.createElement("tr") headerRow.appendChild(document.createElement("th")) + headerRow.classList.add("relative") for (let i = 0; i < OpeningHoursPickerTable.days.length; i++) { let weekday = OpeningHoursPickerTable.days[i].Clone(); const cell = document.createElement("th") cell.style.width = "14%" cell.appendChild(weekday.ConstructElement()) + const fullColumnSpan = this.weekdayElements[i] - fullColumnSpan.classList.add("w-full","h-full","relative") - fullColumnSpan.style.height = "42rem" + fullColumnSpan.classList.add("w-full","relative") + + // We need to round! The table height is rounded as following, we use this to calculate the actual number of pixels afterwards + fullColumnSpan.style.height = ( (cellHeightInPx) * 48) + "px" const ranges = new VariableUiElement( @@ -98,7 +104,7 @@ export default class OpeningHoursPickerTable extends InputElement const hs = Utils.TwoDigits(h); const firstCell = document.createElement("td") firstCell.rowSpan = 2 - firstCell.classList.add("oh-left-col", "oh-timecell-full", "border-box","h-2") + firstCell.classList.add("oh-left-col", "oh-timecell-full", "border-box") firstCell.appendChild(new FixedUiElement(hs + ":00").ConstructElement()) const evenRow = document.createElement("tr") @@ -109,6 +115,9 @@ export default class OpeningHoursPickerTable extends InputElement cell.classList.add("oh-timecell", "oh-timecell-full", `oh-timecell-${weekday}`) evenRow.appendChild(cell) } + evenRow.style.height = (cellHeightInPx)+"px"; + evenRow.style.maxHeight = evenRow.style.height; + evenRow.style.minHeight = evenRow.style.height; table.appendChild(evenRow) const oddRow = document.createElement("tr") @@ -118,6 +127,9 @@ export default class OpeningHoursPickerTable extends InputElement cell.classList.add("oh-timecell", "oh-timecell-half", `oh-timecell-${weekday}`) oddRow.appendChild(cell) } + oddRow.style.minHeight = evenRow.style.height; + oddRow.style.maxHeight = evenRow.style.height; + table.appendChild(oddRow) } diff --git a/UI/OpeningHours/OpeningHoursRange.ts b/UI/OpeningHours/OpeningHoursRange.ts index 24ec3db4d5..439aac490e 100644 --- a/UI/OpeningHours/OpeningHoursRange.ts +++ b/UI/OpeningHours/OpeningHoursRange.ts @@ -24,28 +24,28 @@ export default class OpeningHoursRange extends BaseUIElement { InnerConstructElement(): HTMLElement { const height = this.getHeight(); const oh = this._oh; - const startTime = new FixedUiElement(Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes)).SetClass("oh-timerange-label") - const endTime = new FixedUiElement(Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes)).SetClass("oh-timerange-label") - + const startTime = new FixedUiElement(Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes)) + const endTime = new FixedUiElement(Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes)) const deleteRange = Svg.delete_icon_ui() - .SetClass("oh-delete-range") + .SetClass("rounded-full w-6 h-6 block bg-black") .onClick(() => { this._onDelete() }); - let content = [deleteRange] + let content: BaseUIElement; if (height > 2) { - content = [startTime, deleteRange, endTime]; + content = new Combine([startTime, deleteRange, endTime]).SetClass("flex flex-col h-full").SetStyle("justify-content: space-between;"); + } else { + content = new Combine([deleteRange]).SetClass("flex flex-col h-full").SetStyle("flex-content: center; overflow-x: unset;") } - const el = new Combine(content) - .SetClass("oh-timerange-inner").ConstructElement(); + const el = new Combine([content]).ConstructElement(); - el.style.top = (100 * OH.startTime(oh) / 24) + "%" - el.style.height = (100 * (OH.endTime(oh) - OH.startTime(oh)) / 24) + "%" + el.style.top = `${100 * OH.startTime(oh) / 24}%` + el.style.height = `${100 * this.getHeight() / 24}%` return el; } diff --git a/UI/OpeningHours/PublicHolidayInput.ts b/UI/OpeningHours/PublicHolidayInput.ts index e470dcf98b..dd3c7b9f5f 100644 --- a/UI/OpeningHours/PublicHolidayInput.ts +++ b/UI/OpeningHours/PublicHolidayInput.ts @@ -5,166 +5,19 @@ import {TextField} from "../Input/TextField"; import {DropDown} from "../Input/DropDown"; import {InputElement} from "../Input/InputElement"; import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; +import Toggle from "../Input/Toggle"; export default class PublicHolidayInput extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private readonly _value: UIEventSource; - private readonly _dropdown: BaseUIElement; - private readonly _mode: UIEventSource; - private readonly _startHour: BaseUIElement; - private readonly _endHour: BaseUIElement; - private _element: VariableUiElement; constructor(value: UIEventSource = new UIEventSource("")) { super(); this._value = value; - - const dropdown = new DropDown( - Translations.t.general.opening_hours.open_during_ph, - [ - {shown: Translations.t.general.opening_hours.ph_not_known, value: ""}, - {shown: Translations.t.general.opening_hours.ph_closed, value: "off"}, - {shown:Translations.t.general.opening_hours.ph_open, value: " "} - ] - ); - this._dropdown = dropdown.SetStyle("display:inline-block;"); - this._mode = dropdown.GetValue(); - - const start = new TextField({ - placeholder: "starthour", - htmlType: "time" - }); - const end = new TextField({ - placeholder: "starthour", - htmlType: "time" - }); - this._startHour = start.SetStyle("display:inline-block;"); - this._endHour = end.SetStyle("display:inline-block;"); - const self = this; - - this._value.addCallbackAndRun(ph => { - if (ph === undefined) { - return; - } - const parsed = PublicHolidayInput.LoadValue(ph); - if (parsed === null) { - return; - } - - dropdown.GetValue().setData(parsed.mode); - if (parsed.start) { - start.GetValue().setData(parsed.start); - } - if (parsed.end) { - end.GetValue().setData(parsed.end); - } - - }) - - - function updateValue() { - const phStart = dropdown.GetValue().data; - if (phStart === undefined || phStart === "") { - // Unknown - self._value.setData(""); - return; - } - - if (phStart === " ") { - // THey are open, we need to include the start- and enddate - const startV = start.GetValue().data; - const endV = end.GetValue().data; - if (startV === undefined || endV === undefined) { - self._value.setData(`PH open`); - return; - } - - self._value.setData(`PH ${startV}-${endV}`); - return; - } - self._value.setData(`PH ${phStart}`); - } - - dropdown.GetValue().addCallbackAndRun(() => { - updateValue(); - }); - start.GetValue().addCallbackAndRun(() => { - updateValue(); - }); - end.GetValue().addCallbackAndRun(() => { - updateValue(); - }); - - this._element = new VariableUiElement(this._mode.map( - mode => { - if (mode === " ") { - return new Combine([this._dropdown, - " ", - Translations.t.general.opening_hours.opensAt, - " ", - this._startHour, - " ", - Translations.t.general.opening_hours.openTill, - " ", - this._endHour]); - } - return this._dropdown; - - })) } - protected InnerConstructElement(): HTMLElement { - return this._element.ConstructElement(); - } - - public static LoadValue(str: string): { - mode: string, - start?: string, - end?: string - } { - str = str.trim(); - if (!str.startsWith("PH")) { - return null; - } - - str = str.trim(); - if (str === "PH off") { - return { - mode: "off" - } - } - - if(str === "PH open"){ - return { - mode: " " - } - } - - if (!str.startsWith("PH ")) { - return null; - } - try { - - const timerange = OH.parseHHMMRange(str.substring(2)); - if (timerange === null) { - return null; - } - - return { - mode: " ", - start: OH.hhmm(timerange.startHour, timerange.startMinutes), - end: OH.hhmm(timerange.endHour, timerange.endMinutes), - - } - } catch (e) { - return null; - } - } - - + GetValue(): UIEventSource { return this._value; } @@ -172,5 +25,97 @@ export default class PublicHolidayInput extends InputElement { IsValid(t: string): boolean { return true; } + + private SetupDataSync(mode: UIEventSource, startTime: UIEventSource, endTime: UIEventSource) { + + const value = this._value; + value.addCallbackAndRun(ph => { + if (ph === undefined) { + return; + } + const parsed = OH.ParsePHRule(ph); + if (parsed === null) { + return; + } + mode.setData(parsed.mode) + startTime.setData(parsed.start) + endTime.setData(parsed.end) + }) + + // We use this as a 'addCallbackAndRun' + mode.map(mode => { + if (mode === undefined || mode === "") { + // not known + value.setData(undefined) + return + } + if (mode === "off") { + value.setData("PH off"); + return; + } + if (mode === "open") { + value.setData("PH open"); + return; + } + + + // Open during PH with special hours + if (startTime.data === undefined || endTime.data === undefined) { + // hours not filled in - not saveable + value.setData(undefined) + return + } + const oh = `PH ${startTime.data}-${endTime.data}` + value.setData(oh) + + + }, [startTime, endTime] + ) + } + + + protected InnerConstructElement(): HTMLElement { + const dropdown = new DropDown( + Translations.t.general.opening_hours.open_during_ph.Clone(), + [ + {shown: Translations.t.general.opening_hours.ph_not_known.Clone(), value: ""}, + {shown: Translations.t.general.opening_hours.ph_closed.Clone(), value: "off"}, + {shown:Translations.t.general.opening_hours.ph_open_as_usual.Clone(), value: "open"}, + {shown: Translations.t.general.opening_hours.ph_open.Clone(), value: " "}, + ] + ).SetClass("inline-block"); + /* + * Either "" (unknown), " " (opened) or "off" (closed) + * */ + const mode = dropdown.GetValue(); + + + const start = new TextField({ + placeholder: "starthour", + htmlType: "time" + }).SetClass("inline-block"); + const end = new TextField({ + placeholder: "starthour", + htmlType: "time" + }).SetClass("inline-block"); + + const askHours = new Toggle( + new Combine([ + Translations.t.general.opening_hours.opensAt.Clone(), + start, + Translations.t.general.opening_hours.openTill.Clone(), + end + ]), + undefined, + mode.map(mode => mode === " ") + ) + + this.SetupDataSync(mode, start.GetValue(), end.GetValue()) + + return new Combine([ + dropdown, + askHours + ]).ConstructElement() + } } \ No newline at end of file diff --git a/css/openinghourstable.css b/css/openinghourstable.css index a18958a5e1..fddff2496e 100644 --- a/css/openinghourstable.css +++ b/css/openinghourstable.css @@ -64,6 +64,7 @@ font-size: large; padding: 0; padding-right: 0.2em; + box-sizing: border-box; } .oh-timecell-0 { @@ -119,35 +120,6 @@ overflow: unset; } -.oh-timerange-inner { - display: flex; - flex-direction: column; - justify-content: center; - align-content: center; - height: 100%; - overflow-x: unset; -} - -.oh-timerange-inner-small { - display: flex; - flex-direction: row; - justify-content: space-between; - height: 100%; - width:100%; -} -.oh-delete-range{ - width: 1.5em; - height: 1.5em; - background:black; - border-radius:0.75em; -} - -.oh-delete-range img { - height: 100%; - max-width: 2em; -} - - /**** Opening hours visualization table ****/ .ohviz-table { diff --git a/langs/en.json b/langs/en.json index f5cd5594e6..b62ea003f8 100644 --- a/langs/en.json +++ b/langs/en.json @@ -151,7 +151,8 @@ "open_24_7": "Opened around the clock", "ph_not_known": " ", "ph_closed": "closed", - "ph_open": "opened" + "ph_open": "opened with different hours", + "ph_open_as_usual": "opened as usual" } }, "favourite": { diff --git a/test.ts b/test.ts index eeb50e7af4..5e1525b224 100644 --- a/test.ts +++ b/test.ts @@ -8,6 +8,7 @@ import OpeningHoursPickerTable from "./UI/OpeningHours/OpeningHoursPickerTable"; import OpeningHoursPicker from "./UI/OpeningHours/OpeningHoursPicker"; import {OH, OpeningHour} from "./UI/OpeningHours/OpeningHours"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import PublicHolidayInput from "./UI/OpeningHours/PublicHolidayInput"; const tagsSource = new UIEventSource({ @@ -23,15 +24,8 @@ const tagsSource = new UIEventSource({ const state = new State(undefined) State.state = state -const ohData = new UIEventSource([{ - weekday: 1, - startHour: 10, - startMinutes: 0 - , endHour: 12, - endMinutes: 0 -}]) -new OpeningHoursPicker(ohData).AttachTo("maindiv") -new VariableUiElement(ohData.map(OH.ToString)).AttachTo("extradiv") +const ohData = new UIEventSource("") +new OpeningHoursPicker().AttachTo("maindiv") /* const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { try{ diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index b1bc0ae103..70f3b19055 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -352,6 +352,47 @@ export default class TagSpec extends T{ ]); equal(rules, "Tu 10:00-12:00; Su 13:00-17:00"); }], + ["JOIN OH with end hours", () =>{ + const rules = OH.ToString( + OH.MergeTimes([ + + { + weekday: 1, + endHour: 23, + endMinutes: 30, + startHour: 23, + startMinutes: 0 + }, { + weekday: 1, + endHour: 24, + endMinutes: 0, + startHour: 23, + startMinutes: 30 + }, + + ])); + equal(rules, "Tu 23:00-00:00"); + }], ["JOIN OH with overflowed hours", () =>{ + const rules = OH.ToString( + OH.MergeTimes([ + + { + weekday: 1, + endHour: 23, + endMinutes: 30, + startHour: 23, + startMinutes: 0 + }, { + weekday: 1, + endHour: 0, + endMinutes: 0, + startHour: 23, + startMinutes: 30 + }, + + ])); + equal(rules, "Tu 23:00-00:00"); + }], ["OH 24/7", () => { const rules = OH.Parse("24/7"); equal(rules.length, 7); @@ -368,8 +409,10 @@ export default class TagSpec extends T{ equal(rules, null); }], ["OH Parse PH 12:00-17:00", () => { - const rules = PublicHolidayInput.LoadValue("PH 12:00-17:00"); + const rules = OH.ParsePHRule("PH 12:00-17:00"); equal(rules.mode, " "); + equal(rules.start, "12:00") + equal(rules.end, "17:00") }], ["Round", () => { equal(Utils.Round(15), "15.0") From 910970e4a4692c3cc605de563ef984c28122f7a0 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 16 Jun 2021 17:09:32 +0200 Subject: [PATCH 16/30] Fix input elements --- UI/Base/Link.ts | 45 ++++++++++++++++++++-------------- UI/Input/DirectionInput.ts | 29 +++++++++++----------- UI/Input/ValidatedTextField.ts | 25 ++++++++++++++++--- index.html | 2 -- scripts/generateDocs.ts | 16 +++++++----- test.ts | 42 +++++-------------------------- 6 files changed, 78 insertions(+), 81 deletions(-) diff --git a/UI/Base/Link.ts b/UI/Base/Link.ts index 10d9f2508f..72357b1862 100644 --- a/UI/Base/Link.ts +++ b/UI/Base/Link.ts @@ -4,32 +4,41 @@ import {UIEventSource} from "../../Logic/UIEventSource"; export default class Link extends BaseUIElement { - private readonly _element: HTMLElement; + private readonly _href: string | UIEventSource; + private readonly _embeddedShow: BaseUIElement; + private readonly _newTab: boolean; constructor(embeddedShow: BaseUIElement | string, href: string | UIEventSource, newTab: boolean = false) { super(); - const _embeddedShow = Translations.W(embeddedShow); + this._embeddedShow =Translations.W(embeddedShow); + this._href = href; + this._newTab = newTab; - - const el = document.createElement("a") - - if(typeof href === "string"){ - el.href = href - }else{ - href.addCallbackAndRun(href => { - el.href = href; - }) - } - if (newTab) { - el.target = "_blank" - } - el.appendChild(_embeddedShow.ConstructElement()) - this._element = el } protected InnerConstructElement(): HTMLElement { + const embeddedShow = this._embeddedShow?.ConstructElement(); + if(embeddedShow === undefined){ + return undefined; + } + const el = document.createElement("a") + if(typeof this._href === "string"){ + el.href = this._href + }else{ + this._href.addCallbackAndRun(href => { + el.href = href; + }) + } + if (this._newTab) { + el.target = "_blank" + } + el.appendChild(embeddedShow) + return el; + } - return this._element; + AsMarkdown(): string { + // @ts-ignore + return `[${this._embeddedShow.AsMarkdown()}](${this._href.data ?? this._href})`; } } \ No newline at end of file diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index 254f804d24..858c9b73cd 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -2,6 +2,7 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; import Svg from "../../Svg"; +import {FixedUiElement} from "../Base/FixedUiElement"; /** @@ -11,15 +12,18 @@ export default class DirectionInput extends InputElement { private readonly value: UIEventSource; public readonly IsSelected: UIEventSource = new UIEventSource(false); - private _element: HTMLElement; constructor(value?: UIEventSource) { super(); this.value = value ?? new UIEventSource(undefined); - - this._element = new Combine([ - `
    `, + } + + protected InnerConstructElement(): HTMLElement { + + + const element = new Combine([ + new FixedUiElement("").SetClass("w-full h-full absolute top-0 left-O rounded-full"), Svg.direction_svg().SetStyle( `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`) .SetClass("direction-svg"), @@ -30,20 +34,15 @@ export default class DirectionInput extends InputElement { .ConstructElement() -const self = this; this.value.addCallbackAndRun(rotation => { - const selfElement = self._element; - if (selfElement === null) { - return; - } - const cone = selfElement.getElementsByClassName("direction-svg")[0] as HTMLElement + const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement cone.style.transform = `rotate(${rotation}deg)`; }) - } - - protected InnerConstructElement(): HTMLElement { - return this._element + + this.RegisterTriggers(element) + + return element; } @@ -52,7 +51,7 @@ const self = this; } - protected InnerUpdate(htmlElement: HTMLElement) { + private RegisterTriggers(htmlElement: HTMLElement) { const self = this; function onPosChange(x: number, y: number) { diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 27f1f90516..51b24b2e4b 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -39,7 +39,7 @@ export default class ValidatedTextField { undefined, undefined, "text"), - + ValidatedTextField.tp( "date", "A date", @@ -63,7 +63,24 @@ export default class ValidatedTextField { (value) => new SimpleDatePicker(value)), ValidatedTextField.tp( "wikidata", - "A wikidata identifier, e.g. Q42"), + "A wikidata identifier, e.g. Q42", + (str) => { + if (str === undefined) { + return false; + } + return (str.length > 1 && (str.startsWith("q") || str.startsWith("Q")) || str.startsWith("https://www.wikidata.org/wiki/Q")) + }, + (str) => { + if (str === undefined) { + return undefined; + } + const wd = "https://www.wikidata.org/wiki/"; + if (str.startsWith(wd)) { + str = str.substr(wd.length) + } + return str.toUpperCase(); + }), + ValidatedTextField.tp( "int", "A number", @@ -213,8 +230,8 @@ export default class ValidatedTextField { placeholder?: string | UIElement, value?: UIEventSource, htmlType?: string, - textArea?:boolean, - inputMode?:string, + textArea?: boolean, + inputMode?: string, textAreaRows?: number, isValid?: ((s: string, country: () => string) => boolean), country?: () => string, diff --git a/index.html b/index.html index e8354f59a9..c2b1e8f7e7 100644 --- a/index.html +++ b/index.html @@ -12,8 +12,6 @@ - - diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index c76cb15838..f9a9c5a32c 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -12,15 +12,19 @@ import {writeFileSync} from "fs"; import LayoutConfig from "../Customizations/JSON/LayoutConfig"; import State from "../State"; import {QueryParameters} from "../Logic/Web/QueryParameters"; +import Link from "../UI/Base/Link"; -function WriteFile(filename, html: string | BaseUIElement): void { - writeFileSync(filename, Translations.W(html).AsMarkdown()); +function WriteFile(filename, html: string | BaseUIElement, autogenSource: string): void { + writeFileSync(filename, new Combine([Translations.W(html), + new Link("Generated from "+autogenSource, "../../../"+autogenSource) + ]).AsMarkdown()); } -WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage) -WriteFile("./Docs/CalculatedTags.md", new Combine([SimpleMetaTagger.HelpText(), ExtraFunction.HelpText()]).SetClass("flex-col")) -WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText()); +WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage, "UI/SpecialVisualisations.ts") +WriteFile("./Docs/CalculatedTags.md", new Combine([SimpleMetaTagger.HelpText(), ExtraFunction.HelpText()]).SetClass("flex-col"), + "SimpleMetaTagger and ExtraFunction") +WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText(),"ValidatedTextField.ts"); new State(new LayoutConfig({ @@ -47,7 +51,7 @@ new State(new LayoutConfig({ })) QueryParameters.GetQueryParameter("layer-", "true", "Wether or not the layer with id is shown") -WriteFile("./Docs/URL_Parameters.md", QueryParameters.GenerateQueryParameterDocs()) +WriteFile("./Docs/URL_Parameters.md", QueryParameters.GenerateQueryParameterDocs(), "QueryParameters") console.log("Generated docs") diff --git a/test.ts b/test.ts index 5e1525b224..604b2ebb74 100644 --- a/test.ts +++ b/test.ts @@ -1,40 +1,10 @@ -import {UIEventSource} from "./Logic/UIEventSource"; -import SpecialVisualizations from "./UI/SpecialVisualizations"; -import State from "./State"; +import ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; -import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import OpeningHoursVisualization from "./UI/OpeningHours/OpeningHoursVisualization"; -import OpeningHoursPickerTable from "./UI/OpeningHours/OpeningHoursPickerTable"; -import OpeningHoursPicker from "./UI/OpeningHours/OpeningHoursPicker"; -import {OH, OpeningHour} from "./UI/OpeningHours/OpeningHours"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; -import PublicHolidayInput from "./UI/OpeningHours/PublicHolidayInput"; -const tagsSource = new UIEventSource({ - id: 'id', - name: 'name', - surface: 'asphalt', - image: "https://i.imgur.com/kX3rl3v.jpg", - "image:1": "https://i.imgur.com/oHAJqMB.jpg", - "opening_hours": "mo-fr 09:00-18:00", - _country: "be", -}) - -const state = new State(undefined) -State.state = state - -const ohData = new UIEventSource("") -new OpeningHoursPicker().AttachTo("maindiv") -/* -const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { - try{ - - return new Combine([spec.funcName, spec.constr(state, tagsSource, spec.args.map(a => a.defaultValue ?? "")).SetClass("block")]) - .SetClass("flex flex-col border border-black p-2 m-2"); - }catch(e){ - console.error(e) - return new FixedUiElement("Could not construct "+spec.funcName+" due to "+e).SetClass("alert") - } -}) -new Combine(allSpecials).AttachTo("maindiv")*/ \ No newline at end of file +new Combine(ValidatedTextField.tpList.map(tp => { + const tf = ValidatedTextField.InputForType(tp.name); + + return new Combine([tf, new VariableUiElement(tf.GetValue()).SetClass("alert")]); +})).AttachTo("maindiv") \ No newline at end of file From 88d25ebb79859dcaf3f36944f127de294c3cb02a Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 16 Jun 2021 17:15:02 +0200 Subject: [PATCH 17/30] Fix build --- test.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/test.html b/test.html index 437e113aad..0318ff7033 100644 --- a/test.html +++ b/test.html @@ -9,8 +9,6 @@ - - From 2ae380f1a6220404cb4e63a6966732b634c23abb Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 16 Jun 2021 17:21:37 +0200 Subject: [PATCH 18/30] Regenerate docs --- Docs/CalculatedTags.md | 1 + Docs/SpecialInputElements.md | 2 +- Docs/SpecialRenderings.md | 2 +- Docs/URL_Parameters.md | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Docs/CalculatedTags.md b/Docs/CalculatedTags.md index f9e2476748..166f1d7929 100644 --- a/Docs/CalculatedTags.md +++ b/Docs/CalculatedTags.md @@ -161,3 +161,4 @@ Some advanced functions are available on **feat** as well: For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')` + Generated from SimpleMetaTagger, ExtraFunction \ No newline at end of file diff --git a/Docs/SpecialInputElements.md b/Docs/SpecialInputElements.md index 5f807ab478..3eb09bbb2b 100644 --- a/Docs/SpecialInputElements.md +++ b/Docs/SpecialInputElements.md @@ -60,4 +60,4 @@ Has extra elements to easily input when a POI is opened ## color -Shows a color picker \ No newline at end of file +Shows a color picker Generated from ValidatedTextField.ts \ No newline at end of file diff --git a/Docs/SpecialRenderings.md b/Docs/SpecialRenderings.md index 6de976f173..4f301afcd4 100644 --- a/Docs/SpecialRenderings.md +++ b/Docs/SpecialRenderings.md @@ -1 +1 @@ -

    Special tag renderings

    In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's. General usage is {func_name()} or {func_name(arg, someotherarg)}. Note that you do not need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args

    all_tags

    Prints all key-value pairs of the object - used for debugging
    Example usage: {all_tags()}

    image_carousel

    Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
    1. image key/prefix: The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Default: image
    2. smart search: Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary Default: true
    Example usage: {image_carousel(image,true)}

    image_upload

    Creates a button where a user can upload an image to IMGUR
    1. image-key: Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added) Default: image
    Example usage: {image_upload(image)}

    reviews

    Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten
    1. subjectKey: The key to use to determine the subject. If specified, the subject will be tags[subjectKey] Default: name
    2. fallback: The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value
    Example usage: {reviews()} for a vanilla review, {reviews(name, play_forest)} to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used

    opening_hours_table

    Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.
    1. key: The tagkey from which the table is constructed. Default: opening_hours
    Example usage: {opening_hours_table(opening_hours)}

    live

    Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}
    1. Url: The URL to load
    2. Shorthands: A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ;
    3. path: The path (or shorthand) that should be returned
    Example usage: {live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}

    share_link

    Creates a link that (attempts to) open the native 'share'-screen
    1. url: The url to share (default: current URL)
    Example usage: {share_link()} to share the current page, {share_link()} to share the given url \ No newline at end of file +

    Special tag renderings

    In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's. General usage is {func_name()} or {func_name(arg, someotherarg)}. Note that you do not need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args

    all_tags

    Prints all key-value pairs of the object - used for debugging
    Example usage: {all_tags()}

    image_carousel

    Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
    1. image key/prefix: The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Default: image
    2. smart search: Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary Default: true
    Example usage: {image_carousel(image,true)}

    image_upload

    Creates a button where a user can upload an image to IMGUR
    1. image-key: Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added) Default: image
    Example usage: {image_upload(image)}

    reviews

    Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten
    1. subjectKey: The key to use to determine the subject. If specified, the subject will be tags[subjectKey] Default: name
    2. fallback: The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value
    Example usage: {reviews()} for a vanilla review, {reviews(name, play_forest)} to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used

    opening_hours_table

    Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.
    1. key: The tagkey from which the table is constructed. Default: opening_hours
    Example usage: {opening_hours_table(opening_hours)}

    live

    Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}
    1. Url: The URL to load
    2. Shorthands: A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ;
    3. path: The path (or shorthand) that should be returned
    Example usage: {live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}

    share_link

    Creates a link that (attempts to) open the native 'share'-screen
    1. url: The url to share (default: current URL)
    Example usage: {share_link()} to share the current page, {share_link()} to share the given url Generated from UI/SpecialVisualisations.ts \ No newline at end of file diff --git a/Docs/URL_Parameters.md b/Docs/URL_Parameters.md index 3894dfb141..6f299adcf9 100644 --- a/Docs/URL_Parameters.md +++ b/Docs/URL_Parameters.md @@ -143,4 +143,4 @@ Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case. layer- ------------------ - Wether or not the layer with id is shown The default value is _true_ \ No newline at end of file + Wether or not the layer with id is shown The default value is _true_ Generated from QueryParameters \ No newline at end of file From 9a73ae4c47047672b02799eebd33f7886776ac12 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 16 Jun 2021 21:23:03 +0200 Subject: [PATCH 19/30] More fixes --- Customizations/JSON/TagRenderingConfig.ts | 2 - Models/Constants.ts | 2 +- UI/Base/SubtleButton.ts | 5 +- UI/BigComponents/FullWelcomePaneWithTabs.ts | 12 +- UI/BigComponents/ThemeIntroductionPanel.ts | 26 ++++- UI/Image/SlideShow.ts | 6 +- UI/Input/Checkboxes.ts | 118 ++++++++++---------- UI/Input/DirectionInput.ts | 1 + UI/Input/RadioButton.ts | 47 +++++--- UI/Input/TextField.ts | 4 +- UI/Popup/EditableTagRendering.ts | 8 +- UI/Popup/SaveButton.ts | 4 +- UI/Popup/TagRenderingQuestion.ts | 9 +- css/mobile.css | 15 +-- css/slideshow.css | 18 --- index.css | 4 - index.html | 2 +- langs/en.json | 2 + scripts/generateDocs.ts | 15 ++- test.html | 1 - test.ts | 50 ++++++++- 21 files changed, 203 insertions(+), 148 deletions(-) delete mode 100644 css/slideshow.css diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index 80e51c603b..52e214c377 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -7,8 +7,6 @@ import {Utils} from "../../Utils"; import {TagUtils} from "../../Logic/Tags/TagUtils"; import {And} from "../../Logic/Tags/And"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import {UIElement} from "../../UI/UIElement"; -import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation"; /*** * The parsed version of TagRenderingConfigJSON diff --git a/Models/Constants.ts b/Models/Constants.ts index 71625ac88a..06b2a2c4a8 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.8.0"; + public static vNumber = "0.8.0-rc0"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index c87e121583..9d87c903d0 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -20,6 +20,7 @@ export class SubtleButton extends UIElement { private static generateContent(imageUrl: string | BaseUIElement, messageT: string | BaseUIElement, linkTo: { url: string | UIEventSource, newTab?: boolean } = undefined): BaseUIElement { const message = Translations.W(messageT); + message let img; if ((imageUrl ?? "") === "") { img = undefined; @@ -36,7 +37,7 @@ export class SubtleButton extends UIElement { return new Combine([ image, message?.SetClass("blcok ml-4 overflow-ellipsis"), - ]).SetClass("flex group"); + ]).SetClass("flex group w-full"); } @@ -44,7 +45,7 @@ export class SubtleButton extends UIElement { new Combine([ image, message?.SetClass("block ml-4 overflow-ellipsis") - ]).SetClass("flex group"), + ]).SetClass("flex group w-full"), linkTo.url, linkTo.newTab ?? false ) diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 918df9cda4..b05c26d0d2 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -23,14 +23,14 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { const layoutToUse = State.state.layoutToUse.data; super ( () => layoutToUse.title.Clone(), - () => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails), + () => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown), "welcome" ,isShown ) } - private static ConstructBaseTabs(layoutToUse: LayoutConfig): { header: string | BaseUIElement; content: BaseUIElement }[]{ + private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource): { header: string | BaseUIElement; content: BaseUIElement }[]{ - let welcome: BaseUIElement = new ThemeIntroductionPanel(); + let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown); if (layoutToUse.id === personal.id) { welcome = new PersonalLayersPanel(); } @@ -58,10 +58,10 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { return tabs; } - private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource) { + private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource, isShown: UIEventSource) { - const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse) - const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse)] + const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown) + const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)] tabsWithAboutMc.push({ header: Svg.help, content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "
    Version " + Constants.vNumber]) diff --git a/UI/BigComponents/ThemeIntroductionPanel.ts b/UI/BigComponents/ThemeIntroductionPanel.ts index fc1750f96c..7ea3701da9 100644 --- a/UI/BigComponents/ThemeIntroductionPanel.ts +++ b/UI/BigComponents/ThemeIntroductionPanel.ts @@ -4,26 +4,45 @@ import LanguagePicker from "../LanguagePicker"; import Translations from "../i18n/Translations"; import {VariableUiElement} from "../Base/VariableUIElement"; import Toggle from "../Input/Toggle"; +import {SubtleButton} from "../Base/SubtleButton"; +import Svg from "../../Svg"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {FixedUiElement} from "../Base/FixedUiElement"; export default class ThemeIntroductionPanel extends VariableUiElement { - constructor() { + constructor(isShown: UIEventSource) { const languagePicker = new VariableUiElement( State.state.layoutToUse.map(layout => LanguagePicker.CreateLanguagePicker(layout.language, Translations.t.general.pickLanguage.Clone())) ) ; + + const toTheMap = new SubtleButton( + new FixedUiElement(""), + Translations.t.general.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center") + ).onClick(() =>{ + isShown.setData(false) + }).SetClass("only-on-mobile") const plzLogIn = - Translations.t.general.loginWithOpenStreetMap - .Clone() + new SubtleButton( + Svg.osm_logo_ui(), + + new Combine([Translations.t.general.loginWithOpenStreetMap + .Clone().SetClass("text-xl font-bold"), + Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold")] + ).SetClass("flex flex-col text-center w-full") + ) .onClick(() => { State.state.osmConnection.AttemptLogin() }); const welcomeBack = Translations.t.general.welcomeBack.Clone(); + + const loginStatus = new Toggle( @@ -40,6 +59,7 @@ export default class ThemeIntroductionPanel extends VariableUiElement { super(State.state.layoutToUse.map (layout => new Combine([ layout.description.Clone(), "

    ", + toTheMap, loginStatus, layout.descriptionTail.Clone(), "
    ", diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 8c7fb1a2cf..6a1ef5dac4 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -13,7 +13,6 @@ export class SlideShow extends BaseUIElement { protected InnerConstructElement(): HTMLElement { const el = document.createElement("div") - el.classList.add("slic-carousel") el.style.overflowX = "auto" el.style.width = "min-content" el.style.minWidth = "min-content" @@ -25,8 +24,9 @@ export class SlideShow extends BaseUIElement { } for (const element of elements ?? []) { - element.SetClass("block ml-1") - .SetStyle("width: 300px; max-height: var(--image-carousel-height); height: var(--image-carousel-height)") + element + .SetClass("block ml-1; bg-gray-200") + .SetStyle("min-width: 150; max-height: var(--image-carousel-height); min-height: var(--image-carousel-height)") el.appendChild(element.ConstructElement()) } diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index 025d8e550f..e46227ecf4 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -7,77 +7,83 @@ import BaseUIElement from "../BaseUIElement"; * Supports multi-input */ export default class CheckBoxes extends InputElement { - IsSelected: UIEventSource = new UIEventSource(false); - - -private readonly _element : HTMLElement - private static _nextId = 0; -private readonly value: UIEventSource - constructor(elements: BaseUIElement[], value =new UIEventSource([])) { + IsSelected: UIEventSource = new UIEventSource(false); + private readonly value: UIEventSource + private readonly _elements: BaseUIElement[]; + + constructor(elements: BaseUIElement[], value = new UIEventSource([])) { super(); this.value = value; - elements = Utils.NoNull(elements); - - const el = document.createElement("form") - - for (let i = 0; i < elements.length; i++) { - - let inputI = elements[i]; - const input = document.createElement("input") - const id = CheckBoxes._nextId - CheckBoxes._nextId ++; - input.id = "checkbox"+id + this._elements = Utils.NoNull(elements); + this.SetClass("flex flex-col") - input.type = "checkbox" - const label = document.createElement("label") - label.htmlFor = input.id - label.appendChild(inputI.ConstructElement()) - - value.addCallbackAndRun(selectedValues =>{ - if(selectedValues === undefined){ - return; - } - if(selectedValues.indexOf(i) >= 0){ - input.checked = true; - } - }) - - input.onchange = () => { - const index = value.data.indexOf(i); - if(input.checked && index < 0){ - value.data.push(i); - value.ping(); - }else if(index >= 0){ - value.data.splice(index,1); - value.ping(); - } - } - - - el.appendChild(input) - el.appendChild(document.createElement("br")) - } - - - } - protected InnerConstructElement(): HTMLElement { - return this._element - } - - IsValid(ts: number[]): boolean { return ts !== undefined; - + } GetValue(): UIEventSource { return this.value; } + protected InnerConstructElement(): HTMLElement { + const el = document.createElement("form") + const value = this.value; + const elements = this._elements; + + for (let i = 0; i < elements.length; i++) { + + let inputI = elements[i]; + const input = document.createElement("input") + const id = CheckBoxes._nextId + CheckBoxes._nextId++; + input.id = "checkbox" + id + + input.type = "checkbox" + input.classList.add("p-1","cursor-pointer","ml-3","pl-3") + + const label = document.createElement("label") + label.htmlFor = input.id + label.appendChild(inputI.ConstructElement()) + label.classList.add("block","w-full","p-2","cursor-pointer","bg-red") + + const wrapper = document.createElement("span") + wrapper.classList.add("flex","w-full","border", "border-gray-400") + wrapper.appendChild(input) + wrapper.appendChild(label) + el.appendChild(wrapper) + + value.addCallbackAndRun(selectedValues => { + if (selectedValues === undefined) { + return; + } + if (selectedValues.indexOf(i) >= 0) { + input.checked = true; + } + }) + + input.onchange = () => { + // Index = index in the list of already checked items + const index = value.data.indexOf(i); + if (input.checked && index < 0) { + value.data.push(i); + value.ping(); + } else if (index >= 0) { + value.data.splice(index, 1); + value.ping(); + } + } + + + } + + + return el; + } } \ No newline at end of file diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index 858c9b73cd..f5b1b05ef7 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -18,6 +18,7 @@ export default class DirectionInput extends InputElement { this.value = value ?? new UIEventSource(undefined); } + protected InnerConstructElement(): HTMLElement { diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 760c371940..0ed44402c9 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -7,12 +7,19 @@ export class RadioButton extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; private _elements: InputElement[]; - private readonly _element: HTMLElement; + private _selectFirstAsDefault: boolean; constructor(elements: InputElement[], selectFirstAsDefault = true) { super() - elements = Utils.NoNull(elements); + this._selectFirstAsDefault = selectFirstAsDefault; + this._elements = Utils.NoNull(elements); + this.value = new UIEventSource(undefined) + } + protected InnerConstructElement(): HTMLElement { + const elements = this._elements; + const selectFirstAsDefault = this._selectFirstAsDefault; + const selectedElementIndex: UIEventSource = new UIEventSource(null); const value = UIEventSource.flatten(selectedElementIndex.map( @@ -22,6 +29,7 @@ export class RadioButton extends InputElement { } } ), elements.map(e => e?.GetValue())); + value.syncWith(this.value) if(selectFirstAsDefault){ @@ -61,7 +69,20 @@ export class RadioButton extends InputElement { RadioButton._nextId++ const form = document.createElement("form") - this._element = form; + const inputs = [] + + value.addCallbackAndRun( + selected => { + + let somethingChecked = false; + for (let i = 0; i < inputs.length; i++){ + let input = inputs[i]; + input.checked = !somethingChecked && elements[i].IsValid(selected); + somethingChecked = somethingChecked || input.checked + } + } + ) + for (let i1 = 0; i1 < elements.length; i1++) { let element = elements[i1]; const labelHtml = element.ConstructElement(); @@ -73,6 +94,7 @@ export class RadioButton extends InputElement { input.id = "radio" + groupId + "-" + i1; input.name = groupId; input.type = "radio" + input.classList.add("p-1","cursor-pointer","ml-2","pl-2","pr-0","m-0","ml-3") input.onchange = () => { if(input.checked){ @@ -80,30 +102,26 @@ export class RadioButton extends InputElement { } } - value.addCallbackAndRun( - selected => input.checked = element.IsValid(selected) - ) + + inputs.push(input) const label = document.createElement("label") label.appendChild(labelHtml) label.htmlFor = input.id; + label.classList.add("block","w-full","p-2","cursor-pointer","bg-red") + const block = document.createElement("div") block.appendChild(input) block.appendChild(label) + block.classList.add("flex","w-full","border", "rounded-full", "border-gray-400") form.appendChild(block) - form.addEventListener("change", () => { - // TODO FIXME - } - ); } - this.value = value; - this._elements = elements; - this.SetClass("flex flex-col") + return form; } IsValid(t: T): boolean { @@ -120,9 +138,6 @@ export class RadioButton extends InputElement { } - protected InnerConstructElement(): HTMLElement { - return this._element; - } /* public ShowValue(t: T): boolean { diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index ff91013815..4c878ede54 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -67,10 +67,10 @@ export class TextField extends InputElement { this.value.addCallbackAndRun(value => { if (!(value !== undefined && value !== null)) { + field["value"] = ""; return; } - // @ts-ignore - field.value = value; + field["value"] = value; if (self.IsValid(value)) { self.RemoveClass("invalid") } else { diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index a420c73b8e..85952d9a61 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -8,15 +8,13 @@ import State from "../../State"; import Svg from "../../Svg"; import Toggle from "../Input/Toggle"; import BaseUIElement from "../BaseUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; export default class EditableTagRendering extends Toggle { constructor(tags: UIEventSource, - configuration: TagRenderingConfig) { - - const editMode = new UIEventSource(false); - + configuration: TagRenderingConfig, + editMode = new UIEventSource(false) + ) { const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration) answer.SetClass("w-full") let rendering = answer; diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index 415b30da56..85c7b3076e 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -26,8 +26,8 @@ export class SaveButton extends Toggle { isSaveable ) super( - save - , pleaseLogin, + save, + pleaseLogin, osmConnection?.userDetails?.map(userDetails => userDetails.loggedIn) ?? new UIEventSource(false) ) diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 292b530212..70da15be8e 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -71,7 +71,8 @@ export default class TagRenderingQuestion extends UIElement { } - this._saveButton = new SaveButton(this._inputElement.GetValue(), State.state?.osmConnection) + this._saveButton = new SaveButton(this._inputElement.GetValue(), + State.state?.osmConnection) .onClick(save) @@ -92,7 +93,7 @@ export default class TagRenderingQuestion extends UIElement { return tags.asHumanString(true, true, self._tags.data); } ) - ).SetClass("block") + ).SetClass("block break-all") @@ -156,7 +157,9 @@ export default class TagRenderingQuestion extends UIElement { oppositeTags.push(notSelected); } tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); - return TagUtils.FlattenMultiAnswer(tags); + const actualTags = TagUtils.FlattenMultiAnswer(tags); + console.log("Converted ", indices.join(","), "into", actualTags.asHumanString(false, false, {}), "with elems", elements) + return actualTags; }, (tags: TagsFilter) => { // {key --> values[]} diff --git a/css/mobile.css b/css/mobile.css index a9b321f2d0..7165f0cd7d 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -2,19 +2,14 @@ Contains tweaks for small screens */ -.only-on-mobile { - display: none !important; - background-color: var(--background-color); - color: var(--foreground-color); -} - -@media only screen and (max-width: 600px), only screen and (max-height: 600px) { +@media only screen and (min-width: 769px) { .only-on-mobile { - display: unset !important; - background-color: var(--background-color); - color: var(--foreground-color); + display: none !important; } +} +@media only screen and (max-width: 768px), only screen and (max-height: 768px) { + .hidden-on-mobile { display: none !important; diff --git a/css/slideshow.css b/css/slideshow.css deleted file mode 100644 index c5ab215d67..0000000000 --- a/css/slideshow.css +++ /dev/null @@ -1,18 +0,0 @@ - - - -.slick-carousel-content { - width: 300px; - max-height: var(--image-carousel-height); - display: block; - margin-left: 10px; -} - -.slick-carousel-content img { - /** -Workaround to patch images within a slick carousel - */ - height: var(--image-carousel-height); - width: auto; -} - diff --git a/index.css b/index.css index afa2d28f9c..c825383fd8 100644 --- a/index.css +++ b/index.css @@ -226,10 +226,6 @@ li::marker { width: 100%; } -.question form input[type="radio"] { - margin-right: 0.5em; -} - .invalid { box-shadow: 0 0 10px #ff5353; height: min-content; diff --git a/index.html b/index.html index c2b1e8f7e7..eb45c805af 100644 --- a/index.html +++ b/index.html @@ -70,7 +70,7 @@
    + class="clutter absolute h-24 left-24 right-24 top-56 text-xl text-center" style="z-index: 4000"> Loading MapComplete, hang on...
    diff --git a/langs/en.json b/langs/en.json index b62ea003f8..ebc3ec7d55 100644 --- a/langs/en.json +++ b/langs/en.json @@ -29,6 +29,7 @@ }, "general": { "loginWithOpenStreetMap": "Login with OpenStreetMap", + "loginOnlyNeededToEdit": "to make changes to the map", "welcomeBack": "You are logged in, welcome back!", "loginToStart": "Login to answer this question", "testing":"Testing - changes won't be saved", @@ -39,6 +40,7 @@ "error": "Something went wrongโ€ฆ" }, "returnToTheMap": "Return to the map", + "openTheMap": "Open the map", "save": "Save", "cancel": "Cancel", "skip": "Skip this question", diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index f9a9c5a32c..1cfe1418a9 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -1,5 +1,4 @@ import {Utils} from "../Utils"; - Utils.runningFromConsole = true; import SpecialVisualizations from "../UI/SpecialVisualizations"; import SimpleMetaTagger from "../Logic/SimpleMetaTagger"; @@ -12,19 +11,19 @@ import {writeFileSync} from "fs"; import LayoutConfig from "../Customizations/JSON/LayoutConfig"; import State from "../State"; import {QueryParameters} from "../Logic/Web/QueryParameters"; -import Link from "../UI/Base/Link"; -function WriteFile(filename, html: string | BaseUIElement, autogenSource: string): void { + +function WriteFile(filename, html: string | BaseUIElement, autogenSource: string[]): void { writeFileSync(filename, new Combine([Translations.W(html), - new Link("Generated from "+autogenSource, "../../../"+autogenSource) + "Generated from "+autogenSource.join(", ") ]).AsMarkdown()); } -WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage, "UI/SpecialVisualisations.ts") +WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage, ["UI/SpecialVisualisations.ts"]) WriteFile("./Docs/CalculatedTags.md", new Combine([SimpleMetaTagger.HelpText(), ExtraFunction.HelpText()]).SetClass("flex-col"), - "SimpleMetaTagger and ExtraFunction") -WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText(),"ValidatedTextField.ts"); + ["SimpleMetaTagger","ExtraFunction"]) +WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText(),["ValidatedTextField.ts"]); new State(new LayoutConfig({ @@ -51,7 +50,7 @@ new State(new LayoutConfig({ })) QueryParameters.GetQueryParameter("layer-", "true", "Wether or not the layer with id is shown") -WriteFile("./Docs/URL_Parameters.md", QueryParameters.GenerateQueryParameterDocs(), "QueryParameters") +WriteFile("./Docs/URL_Parameters.md", QueryParameters.GenerateQueryParameterDocs(), ["QueryParameters"]) console.log("Generated docs") diff --git a/test.html b/test.html index 0318ff7033..839114efe4 100644 --- a/test.html +++ b/test.html @@ -3,7 +3,6 @@ Small tests - diff --git a/test.ts b/test.ts index 604b2ebb74..92dfa62737 100644 --- a/test.ts +++ b/test.ts @@ -1,10 +1,50 @@ import ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import {UIEventSource} from "./Logic/UIEventSource"; +import TagRenderingConfig from "./Customizations/JSON/TagRenderingConfig"; +import State from "./State"; +import TagRenderingQuestion from "./UI/Popup/TagRenderingQuestion"; -new Combine(ValidatedTextField.tpList.map(tp => { - const tf = ValidatedTextField.InputForType(tp.name); - - return new Combine([tf, new VariableUiElement(tf.GetValue()).SetClass("alert")]); -})).AttachTo("maindiv") \ No newline at end of file +function TestTagRendering(){ + State.state = new State(undefined) + const tagsSource = new UIEventSource({ + id:"node/1" + }) + new TagRenderingQuestion( + tagsSource, + new TagRenderingConfig({ + multiAnswer: false, + freeform: { + key:"valve" + }, + question: "What valves are supported?", + render: "This pump supports {valve}", + mappings: [ + { + if: "valve=dunlop", + then: "This pump supports dunlop" + }, + { + if:"valve=shrader", + then:"shrader is supported", + } + ], + + }, undefined, "test") + ).AttachTo("maindiv") + new VariableUiElement(tagsSource.map(tags => tags["valves"])).SetClass("alert").AttachTo("extradiv") +} + +function TestAllInputMethods(){ + + new Combine(ValidatedTextField.tpList.map(tp => { + const tf = ValidatedTextField.InputForType(tp.name); + + return new Combine([tf, new VariableUiElement(tf.GetValue()).SetClass("alert")]); + })).AttachTo("maindiv") +} + + +TestTagRendering() \ No newline at end of file From 6c3b8b202020dc4ea14d13822dac07bce5931400 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 17 Jun 2021 00:37:57 +0200 Subject: [PATCH 20/30] Fix loading of the hash of a custom theme, even if the custom theme is not loaded via the URL. Fixes 352 --- InitUiElements.ts | 4 ++-- index.ts | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/InitUiElements.ts b/InitUiElements.ts index cb7cdc5623..a5b738e9ca 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -215,7 +215,7 @@ export class InitUiElements { } - static LoadLayoutFromHash(userLayoutParam: UIEventSource) { + static LoadLayoutFromHash(userLayoutParam: UIEventSource): [LayoutConfig, string]{ try { let hash = location.hash.substr(1); const layoutFromBase64 = userLayoutParam.data; @@ -247,7 +247,7 @@ export class InitUiElements { // @ts-ignore const layoutToUse = new LayoutConfig(json, false); userLayoutParam.setData(layoutToUse.id); - return layoutToUse; + return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; } catch (e) { new FixedUiElement("Error: could not parse the custom layout:
    " + e).AttachTo("centermessage"); diff --git a/index.ts b/index.ts index 79a81b2ea0..472fb943fb 100644 --- a/index.ts +++ b/index.ts @@ -5,7 +5,6 @@ import {QueryParameters} from "./Logic/Web/QueryParameters"; import {UIEventSource} from "./Logic/UIEventSource"; import * as $ from "jquery"; import LayoutConfig from "./Customizations/JSON/LayoutConfig"; -import {Utils} from "./Utils"; import MoreScreen from "./UI/BigComponents/MoreScreen"; import State from "./State"; import Combine from "./UI/Base/Combine"; @@ -34,7 +33,6 @@ if (location.href.indexOf("buurtnatuur.be") >= 0) { } - let testing: UIEventSource; if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { testing = QueryParameters.GetQueryParameter("test", "true"); @@ -88,7 +86,7 @@ new Combine(["Initializing...
    ", new FixedUiElement("If this message persist, something went wrong - click here to try again") .SetClass("link-underline small") .onClick(() => { - localStorage.clear(); + localStorage.clear(); window.location.reload(true); })]) @@ -136,8 +134,8 @@ if (layoutFromBase64.startsWith("http")) { }); } else if (layoutFromBase64 !== "false") { - layoutToUse = InitUiElements.LoadLayoutFromHash(userLayoutParam); - InitUiElements.InitAll(layoutToUse, layoutFromBase64, testing, defaultLayout, location.hash.substr(1)); + let [layoutToUse, encoded] = InitUiElements.LoadLayoutFromHash(userLayoutParam); + InitUiElements.InitAll(layoutToUse, layoutFromBase64, testing, defaultLayout, encoded); } else if (layoutToUse !== undefined) { // This is the default case: a builtin theme InitUiElements.InitAll(layoutToUse, layoutFromBase64, testing, defaultLayout); From d443c7ecb00973c56e23110b532c000b5060e2a9 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 17 Jun 2021 00:54:06 +0200 Subject: [PATCH 21/30] Fix unofficial theme display --- UI/BigComponents/MoreScreen.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index a5de7896cf..d4632b6280 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -43,17 +43,18 @@ export default class MoreScreen extends Combine { } 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) + return new VariableUiElement(State.state.installedThemes.map(customThemes => { + const els : BaseUIElement[] = [] + if (customThemes.length > 0) { + els.push(Translations.t.general.customThemeIntro.Clone()) - const customThemesElement = new Combine( - customThemes.map(theme => MoreScreen.createLinkButton(theme.layout, theme.definition)?.SetClass(buttonClass)) - ) - els.push(customThemesElement) - } - return new Combine(els) + const customThemesElement = new Combine( + customThemes.map(theme => MoreScreen.createLinkButton(theme.layout, theme.definition)?.SetClass(buttonClass)) + ) + els.push(customThemesElement) + } + return els; + })); } private static createOfficialThemesList(state: State, buttonClass: string): BaseUIElement { From 7b52dee3203426f4953c86fc16f72c949af8eb00 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 17 Jun 2021 13:17:16 +0200 Subject: [PATCH 22/30] Small fixes to the Slideshow --- UI/Image/ImgurImage.ts | 17 ++++------------- UI/Image/MapillaryImage.ts | 2 +- UI/Image/SlideShow.ts | 13 ++++++++----- UI/Image/WikimediaImage.ts | 2 +- index.css | 7 ++++++- test.ts | 17 ++++++++++++++++- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/UI/Image/ImgurImage.ts b/UI/Image/ImgurImage.ts index 176e163729..93355df4e3 100644 --- a/UI/Image/ImgurImage.ts +++ b/UI/Image/ImgurImage.ts @@ -6,6 +6,7 @@ import Combine from "../Base/Combine"; import Attribution from "./Attribution"; import BaseUIElement from "../BaseUIElement"; import Img from "../Base/Img"; +import {VariableUiElement} from "../Base/VariableUIElement"; export class ImgurImage extends UIElement { @@ -19,7 +20,7 @@ export class ImgurImage extends UIElement { private readonly _imageLocation: string; constructor(source: string) { - super(undefined) + super() this._imageLocation = source; if (ImgurImage.allLicenseInfos[source] !== undefined) { this._imageMeta = ImgurImage.allLicenseInfos[source]; @@ -31,25 +32,15 @@ export class ImgurImage extends UIElement { self._imageMeta.setData(license) }) } - - this.ListenTo(this._imageMeta); - } InnerRender(): BaseUIElement { const image = new Img( this._imageLocation); - if(this._imageMeta.data === null){ - return image; - } - - const meta = this._imageMeta.data; return new Combine([ image, - new Attribution(meta.artist, meta.license, undefined), - - ]).SetClass('block relative') - ; + new VariableUiElement(this._imageMeta.map(meta => (meta === undefined || meta === null) ? undefined : new Attribution(meta.artist, meta.license, undefined))) + ]).SetClass('block relative h-full'); } diff --git a/UI/Image/MapillaryImage.ts b/UI/Image/MapillaryImage.ts index 9b2edf5655..53ec504313 100644 --- a/UI/Image/MapillaryImage.ts +++ b/UI/Image/MapillaryImage.ts @@ -53,7 +53,7 @@ export class MapillaryImage extends UIElement { return new Combine([ image, new Attribution(meta.artist, meta.license, Svg.mapillary_svg()) - ]).SetClass("relative block"); + ]).SetClass("relative block h-full"); } diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 6a1ef5dac4..eea8ea5668 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -1,5 +1,7 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; +import {Utils} from "../../Utils"; +import Combine from "../Base/Combine"; export class SlideShow extends BaseUIElement { @@ -8,7 +10,7 @@ export class SlideShow extends BaseUIElement { constructor(embeddedElements: UIEventSource) { super() - this.embeddedElements = embeddedElements; + this.embeddedElements =embeddedElements; } protected InnerConstructElement(): HTMLElement { @@ -23,11 +25,12 @@ export class SlideShow extends BaseUIElement { el.removeChild(el.lastChild) } + elements = Utils.NoNull(elements).map(el => new Combine([el]) + .SetClass("block relative ml-1 bg-gray-200 m-1 rounded slideshow-item") + .SetStyle("min-width: 150px; width: max-content; height: var(--image-carousel-height);max-height: var(--image-carousel-height);") + ) + for (const element of elements ?? []) { - element - .SetClass("block ml-1; bg-gray-200") - .SetStyle("min-width: 150; max-height: var(--image-carousel-height); min-height: var(--image-carousel-height)") - el.appendChild(element.ConstructElement()) } }); diff --git a/UI/Image/WikimediaImage.ts b/UI/Image/WikimediaImage.ts index 754c8dab5c..85b4c50a49 100644 --- a/UI/Image/WikimediaImage.ts +++ b/UI/Image/WikimediaImage.ts @@ -51,7 +51,7 @@ export class WikimediaImage extends UIElement { return new Combine([ image, new Attribution(meta.artist, meta.license, Svg.wikimedia_commons_white_svg()) - ]).SetClass("relative block") + ]).SetClass("relative block h-full") } diff --git a/index.css b/index.css index c825383fd8..7e22c57b99 100644 --- a/index.css +++ b/index.css @@ -89,7 +89,6 @@ svg, img { display: unset; } - a { color: var(--foreground-color); } @@ -358,3 +357,9 @@ li::marker { height: 1em; max-width: 1em; } + + +.slideshow-item img{ + height: 100%; + width: unset; +} \ No newline at end of file diff --git a/test.ts b/test.ts index 92dfa62737..8dc3264023 100644 --- a/test.ts +++ b/test.ts @@ -5,8 +5,23 @@ import {UIEventSource} from "./Logic/UIEventSource"; import TagRenderingConfig from "./Customizations/JSON/TagRenderingConfig"; import State from "./State"; import TagRenderingQuestion from "./UI/Popup/TagRenderingQuestion"; +import {SlideShow} from "./UI/Image/SlideShow"; +import {FixedUiElement} from "./UI/Base/FixedUiElement"; +import Img from "./UI/Base/Img"; +import {ImgurImage} from "./UI/Image/ImgurImage"; +function TestSlideshow(){ + const elems = new UIEventSource([ + new FixedUiElement("A"), + new FixedUiElement("qmsldkfjqmlsdkjfmqlskdjfmqlksdf").SetClass("text-xl"), + new Img("https://i.imgur.com/8lIQ5Hv.jpg"), + new ImgurImage("https://i.imgur.com/y5XudzW.jpg"), + new Img("https://www.grunge.com/img/gallery/the-real-reason-your-cat-sleeps-so-much/intro-1601496900.webp") + ]) + new SlideShow(elems).AttachTo("maindiv") +} + function TestTagRendering(){ State.state = new State(undefined) const tagsSource = new UIEventSource({ @@ -47,4 +62,4 @@ function TestAllInputMethods(){ } -TestTagRendering() \ No newline at end of file +TestSlideshow() \ No newline at end of file From 1538d44018719336f229d27288fd09743f3ac761 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 17 Jun 2021 16:45:18 +0200 Subject: [PATCH 23/30] More documentation --- Docs/Architecture.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Docs/Architecture.md b/Docs/Architecture.md index 01a214bda6..d42ab7e7f2 100644 --- a/Docs/Architecture.md +++ b/Docs/Architecture.md @@ -28,7 +28,16 @@ Note that 'map' will also absorb some changes, e.g. `const someEventSource : UIE An object which receives an UIEventSource is responsible of responding onto changes of this object. This is especially true for UI-components UI --- +--``` + +export default class MyComponent { + + constructor(neededParameters, neededUIEventSources) { + + } + +} +``` The Graphical User Interface is composed of various UI-elements. For every UI-element, there is a BaseUIElement which creates the actual HTMLElement when needed. @@ -177,9 +186,22 @@ export default class MyComponent extends Combine { ``` +Assets +------ + +### Themes + +Theme and layer configuration files go into /assets/layers and assets/themes + +### Images + +Other files (mostly images that are part of the core of mapcomplete) go into 'assets/svg' and are usable with `Svg.image_file_ui()`. Run `npm run generate:images` if you added a new image + Logic ----- -With the +The last part is the business logic of the application, found in 'Logic'. Actors are small objects which react to UIEventSources to update other eventSources. + +State.state is a big singleton object containing a lot of the state of the entire application. That one is a bit a mess From 6ba4cb18c6332e88802abf9cec333f69eb271323 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 18 Jun 2021 01:24:31 +0200 Subject: [PATCH 24/30] Fix layout generation --- scripts/generateLayouts.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 8c1a365d58..747de7ead3 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).ConstructElement()?.innerText; - const ogDescr = Translations.W(layout.description ?? "").ConstructElement()?.innerText; + const ogTitle = Translations.WT(layout.title).txt; + const ogDescr = Translations.WT(layout.description ?? "").txt; 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)?.ConstructElement()?.innerText; - const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.ConstructElement()?.innerText; + const ogTitle = Translations.WT(layout.title).txt; + const ogDescr = Translations.WT(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap").txt; const ogImage = layout.socialImage; let customCss = ""; From 1609c63f3b4dbee29fc6a512ff5a89ba9041c831 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 18 Jun 2021 01:25:13 +0200 Subject: [PATCH 25/30] Refactoring of Attribute Images, fix more or less decent slideshow. Turns out a few lines of css can get us there! --- Logic/Web/ImageAttributionSource.ts | 29 +++++ Logic/Web/Imgur.ts | 110 ++++++++++-------- Logic/Web/Mapillary.ts | 53 +++++++-- Logic/Web/Wikimedia.ts | 110 ++++++++++++------ Svg.ts | 126 ++++++++++----------- UI/Base/Img.ts | 11 +- UI/Base/SubtleButton.ts | 6 +- UI/BigComponents/Basemap.ts | 2 +- UI/BigComponents/ThemeIntroductionPanel.ts | 2 +- UI/Image/AttributedImage.ts | 19 ++++ UI/Image/Attribution.ts | 36 ++++-- UI/Image/ImageCarousel.ts | 21 ++-- UI/Image/ImgurImage.ts | 48 -------- UI/Image/MapillaryImage.ts | 61 ---------- UI/Image/SlideShow.ts | 14 ++- UI/Image/WikimediaImage.ts | 59 ---------- index.css | 8 +- index.manifest | 2 + scripts/generateIncludedImages.ts | 2 +- test.ts | 5 +- 20 files changed, 363 insertions(+), 361 deletions(-) create mode 100644 Logic/Web/ImageAttributionSource.ts create mode 100644 UI/Image/AttributedImage.ts delete mode 100644 UI/Image/ImgurImage.ts delete mode 100644 UI/Image/MapillaryImage.ts delete mode 100644 UI/Image/WikimediaImage.ts diff --git a/Logic/Web/ImageAttributionSource.ts b/Logic/Web/ImageAttributionSource.ts new file mode 100644 index 0000000000..689a32c46c --- /dev/null +++ b/Logic/Web/ImageAttributionSource.ts @@ -0,0 +1,29 @@ +import {UIEventSource} from "../UIEventSource"; +import {LicenseInfo} from "./Wikimedia"; +import BaseUIElement from "../../UI/BaseUIElement"; + + +export default abstract class ImageAttributionSource { + + + private _cache = new Map>() + + GetAttributionFor(url: string): UIEventSource { + const cached = this._cache.get(url); + if (cached !== undefined) { + return cached; + } + const src = this.DownloadAttribution(url) + this._cache.set(url, src) + return src; + } + + + + public abstract SourceIcon(backlinkSource?: string) : BaseUIElement; + protected abstract DownloadAttribution(url: string): UIEventSource; + public PrepareUrl(value: string): string{ + return value; + } + +} \ No newline at end of file diff --git a/Logic/Web/Imgur.ts b/Logic/Web/Imgur.ts index d48771e112..a1ecd7eeed 100644 --- a/Logic/Web/Imgur.ts +++ b/Logic/Web/Imgur.ts @@ -1,16 +1,24 @@ // @ts-ignore import $ from "jquery" import {LicenseInfo} from "./Wikimedia"; +import ImageAttributionSource from "./ImageAttributionSource"; +import {UIEventSource} from "../UIEventSource"; +import BaseUIElement from "../../UI/BaseUIElement"; -export class Imgur { +export class Imgur extends ImageAttributionSource { + + public static readonly singleton = new Imgur(); + private constructor() { + super(); + } static uploadMultiple( title: string, description: string, blobs: FileList, handleSuccessfullUpload: ((imageURL: string) => void), allDone: (() => void), onFail: ((reason: string) => void), - offset:number = 0) { + offset: number = 0) { if (blobs.length == offset) { allDone(); @@ -32,56 +40,11 @@ export class Imgur { ); - } - - static getDescriptionOfImage(url: string, - handleDescription: ((license: LicenseInfo) => void)) { - - const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; - - const apiUrl = 'https://api.imgur.com/3/image/'+hash; - const apiKey = '7070e7167f0a25a'; - - const settings = { - async: true, - crossDomain: true, - processData: false, - contentType: false, - type: 'GET', - url: apiUrl, - headers: { - Authorization: 'Client-ID ' + apiKey, - Accept: 'application/json', - }, - }; - // @ts-ignore - $.ajax(settings).done(function (response) { - const descr: string = response.data.description ?? ""; - const data: any = {}; - for (const tag of descr.split("\n")) { - const kv = tag.split(":"); - const k = kv[0]; - const v = kv[1].replace("\r", ""); - data[k] = v; - } - - - const licenseInfo = new LicenseInfo(); - - licenseInfo.licenseShortName = data.license; - licenseInfo.artist = data.author; - - handleDescription(licenseInfo); - - }).fail((reason) => { - console.log("Getting metadata from to IMGUR failed", reason) - }); - } static uploadImage(title: string, description: string, blob, handleSuccessfullUpload: ((imageURL: string) => void), - onFail: (reason:string) => void) { + onFail: (reason: string) => void) { const apiUrl = 'https://api.imgur.com/3/image'; const apiKey = '7070e7167f0a25a'; @@ -119,4 +82,55 @@ export class Imgur { }); } + SourceIcon(): BaseUIElement { + return undefined; + } + + protected DownloadAttribution(url: string): UIEventSource { + const src = new UIEventSource(undefined) + + + const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; + + const apiUrl = 'https://api.imgur.com/3/image/' + hash; + const apiKey = '7070e7167f0a25a'; + + const settings = { + async: true, + crossDomain: true, + processData: false, + contentType: false, + type: 'GET', + url: apiUrl, + headers: { + Authorization: 'Client-ID ' + apiKey, + Accept: 'application/json', + }, + }; + // @ts-ignore + $.ajax(settings).done(function (response) { + const descr: string = response.data.description ?? ""; + const data: any = {}; + for (const tag of descr.split("\n")) { + const kv = tag.split(":"); + const k = kv[0]; + data[k] = kv[1].replace("\r", ""); + } + + + const licenseInfo = new LicenseInfo(); + + licenseInfo.licenseShortName = data.license; + licenseInfo.artist = data.author; + + src.setData(licenseInfo) + + }).fail((reason) => { + console.log("Getting metadata from to IMGUR failed", reason) + }); + + return src; + } + + } \ No newline at end of file diff --git a/Logic/Web/Mapillary.ts b/Logic/Web/Mapillary.ts index 048e637163..cad71f93b7 100644 --- a/Logic/Web/Mapillary.ts +++ b/Logic/Web/Mapillary.ts @@ -1,26 +1,57 @@ import $ from "jquery" import {LicenseInfo} from "./Wikimedia"; +import ImageAttributionSource from "./ImageAttributionSource"; +import BaseUIElement from "../../UI/BaseUIElement"; +import {UIEventSource} from "../UIEventSource"; +import Svg from "../../Svg"; -export class Mapillary { +export class Mapillary extends ImageAttributionSource { + public static readonly singleton = new Mapillary(); - static getDescriptionOfImage(key: string, - handleDescription: ((license: LicenseInfo) => void)) { - const url = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` + private constructor() { + super(); + } - const settings = { - async: true, - type: 'GET', - url: url - }; - $.getJSON(url, function(data) { + private static ExtractKeyFromURL(value: string) { + if (value.startsWith("https://a.mapillary.com")) { + return value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1); + } + const matchApi = value.match(/https?:\/\/images.mapillary.com\/([^/]*)/) + if (matchApi !== null) { + return matchApi[1]; + } + + if (value.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) { + // Extract the key of the image + value = value.substring("https://www.mapillary.com/map/im/".length); + } + return value; + } + + SourceIcon(backlinkSource?: string): BaseUIElement { + return Svg.mapillary_svg(); + } + + PrepareUrl(value: string): string { + const key = Mapillary.ExtractKeyFromURL(value) + return `https://images.mapillary.com/${key}/thumb-640.jpg?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` + } + + protected DownloadAttribution(url: string): UIEventSource { + + const key = Mapillary.ExtractKeyFromURL(url) + const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` + const source = new UIEventSource(undefined) + $.getJSON(metadataURL, function (data) { const license = new LicenseInfo(); license.artist = data.properties?.username; license.licenseShortName = "CC BY-SA 4.0"; license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; license.attributionRequired = true; - handleDescription(license); + source.setData(license); }) + return source } } \ No newline at end of file diff --git a/Logic/Web/Wikimedia.ts b/Logic/Web/Wikimedia.ts index bf68d22557..4668f35116 100644 --- a/Logic/Web/Wikimedia.ts +++ b/Logic/Web/Wikimedia.ts @@ -1,47 +1,28 @@ import * as $ from "jquery" +import ImageAttributionSource from "./ImageAttributionSource"; +import BaseUIElement from "../../UI/BaseUIElement"; +import Svg from "../../Svg"; +import {UIEventSource} from "../UIEventSource"; +import Link from "../../UI/Base/Link"; /** * This module provides endpoints for wikipedia/wikimedia and others */ -export class Wikimedia { +export class Wikimedia extends ImageAttributionSource { + + + public static readonly singleton = new Wikimedia(); + + private constructor() { + super(); + } - private static knownLicenses = {}; static ImageNameToUrl(filename: string, width: number = 500, height: number = 200): string { filename = encodeURIComponent(filename); return "https://commons.wikimedia.org/wiki/Special:FilePath/" + filename + "?width=" + width + "&height=" + height; } - static LicenseData(filename: string, handle: ((LicenseInfo) => void)): void { - if (filename in this.knownLicenses) { - return this.knownLicenses[filename]; - } - if (filename === "") { - return; - } - const url = "https://en.wikipedia.org/w/" + - "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + - "titles=" + filename + - "&format=json&origin=*"; - $.getJSON(url, function (data) { - const licenseInfo = new LicenseInfo(); - const license = data.query.pages[-1].imageinfo[0].extmetadata; - - licenseInfo.artist = license.Artist?.value; - licenseInfo.license = license.License?.value; - licenseInfo.copyrighted = license.Copyrighted?.value; - licenseInfo.attributionRequired = license.AttributionRequired?.value; - licenseInfo.usageTerms = license.UsageTerms?.value; - licenseInfo.licenseShortName = license.LicenseShortName?.value; - licenseInfo.credit = license.Credit?.value; - licenseInfo.description = license.ImageDescription?.value; - - Wikimedia.knownLicenses[filename] = licenseInfo; - handle(licenseInfo); - }); - - } - static GetCategoryFiles(categoryName: string, handleCategory: ((ImagesInCategory: ImagesInCategory) => void), alreadyLoaded = 0, continueParameter: { k: string, param: string } = undefined) { @@ -111,6 +92,71 @@ export class Wikimedia { }); } + private static ExtractFileName(url: string) { + if (!url.startsWith("http")) { + return url; + } + const path = new URL(url).pathname + return path.substring(path.lastIndexOf("/") + 1); + + } + + SourceIcon(backlink: string): BaseUIElement { + const img = Svg.wikimedia_commons_white_svg() + .SetStyle("width:2em;height: 2em"); + if (backlink === undefined) { + return img + } + + + return new Link(Svg.wikimedia_commons_white_img, + `https://commons.wikimedia.org/wiki/${backlink}`, true) + + + } + + PrepareUrl(value: string): string { + + if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) { + return value; + } + return Wikimedia.ImageNameToUrl(value, 500, 400) + .replace(/'/g, '%27'); + } + + protected DownloadAttribution(filename: string): UIEventSource { + + const source = new UIEventSource(undefined); + + filename = Wikimedia.ExtractFileName(filename) + + if (filename === "") { + return source; + } + + const url = "https://en.wikipedia.org/w/" + + "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + + "titles=" + filename + + "&format=json&origin=*"; + console.log("Getting attribution at ", url) + $.getJSON(url, function (data) { + const licenseInfo = new LicenseInfo(); + const license = data.query.pages[-1].imageinfo[0].extmetadata; + + licenseInfo.artist = license.Artist?.value; + licenseInfo.license = license.License?.value; + licenseInfo.copyrighted = license.Copyrighted?.value; + licenseInfo.attributionRequired = license.AttributionRequired?.value; + licenseInfo.usageTerms = license.UsageTerms?.value; + licenseInfo.licenseShortName = license.LicenseShortName?.value; + licenseInfo.credit = license.Credit?.value; + licenseInfo.description = license.ImageDescription?.value; + source.setData(licenseInfo); + }); + return source; + + } + } diff --git a/Svg.ts b/Svg.ts index 53889707c1..3f0ff50fe8 100644 --- a/Svg.ts +++ b/Svg.ts @@ -6,317 +6,317 @@ export default class Svg { public static SocialImageForeground = " image/svg+xml 010110010011010110010011ย  010110010011010110010011ย  " public static SocialImageForeground_img = Img.AsImageElement(Svg.SocialImageForeground) - public static SocialImageForeground_svg() { return new FixedUiElement(Svg.SocialImageForeground);} + public static SocialImageForeground_svg() { return new Img(Svg.SocialImageForeground, true);} public static SocialImageForeground_ui() { return new FixedUiElement(Svg.SocialImageForeground_img);} public static add = " image/svg+xml " public static add_img = Img.AsImageElement(Svg.add) - public static add_svg() { return new FixedUiElement(Svg.add);} + public static add_svg() { return new Img(Svg.add, true);} public static add_ui() { return new FixedUiElement(Svg.add_img);} public static addSmall = " image/svg+xml " public static addSmall_img = Img.AsImageElement(Svg.addSmall) - public static addSmall_svg() { return new FixedUiElement(Svg.addSmall);} + public static addSmall_svg() { return new Img(Svg.addSmall, true);} public static addSmall_ui() { return new FixedUiElement(Svg.addSmall_img);} public static ampersand = " image/svg+xml " public static ampersand_img = Img.AsImageElement(Svg.ampersand) - public static ampersand_svg() { return new FixedUiElement(Svg.ampersand);} + public static ampersand_svg() { return new Img(Svg.ampersand, true);} public static ampersand_ui() { return new FixedUiElement(Svg.ampersand_img);} public static arrow_left_smooth = " image/svg+xml " public static arrow_left_smooth_img = Img.AsImageElement(Svg.arrow_left_smooth) - public static arrow_left_smooth_svg() { return new FixedUiElement(Svg.arrow_left_smooth);} + public static arrow_left_smooth_svg() { return new Img(Svg.arrow_left_smooth, true);} public static arrow_left_smooth_ui() { return new FixedUiElement(Svg.arrow_left_smooth_img);} public static arrow_right_smooth = " image/svg+xml " public static arrow_right_smooth_img = Img.AsImageElement(Svg.arrow_right_smooth) - public static arrow_right_smooth_svg() { return new FixedUiElement(Svg.arrow_right_smooth);} + public static arrow_right_smooth_svg() { return new Img(Svg.arrow_right_smooth, true);} public static arrow_right_smooth_ui() { return new FixedUiElement(Svg.arrow_right_smooth_img);} public static back = " image/svg+xml " public static back_img = Img.AsImageElement(Svg.back) - public static back_svg() { return new FixedUiElement(Svg.back);} + public static back_svg() { return new Img(Svg.back, true);} public static back_ui() { return new FixedUiElement(Svg.back_img);} public static bug = " " public static bug_img = Img.AsImageElement(Svg.bug) - public static bug_svg() { return new FixedUiElement(Svg.bug);} + public static bug_svg() { return new Img(Svg.bug, true);} public static bug_ui() { return new FixedUiElement(Svg.bug_img);} public static camera_plus = " image/svg+xml " public static camera_plus_img = Img.AsImageElement(Svg.camera_plus) - public static camera_plus_svg() { return new FixedUiElement(Svg.camera_plus);} + public static camera_plus_svg() { return new Img(Svg.camera_plus, true);} public static camera_plus_ui() { return new FixedUiElement(Svg.camera_plus_img);} public static checkmark = "" public static checkmark_img = Img.AsImageElement(Svg.checkmark) - public static checkmark_svg() { return new FixedUiElement(Svg.checkmark);} + public static checkmark_svg() { return new Img(Svg.checkmark, true);} public static checkmark_ui() { return new FixedUiElement(Svg.checkmark_img);} public static circle = " " public static circle_img = Img.AsImageElement(Svg.circle) - public static circle_svg() { return new FixedUiElement(Svg.circle);} + public static circle_svg() { return new Img(Svg.circle, true);} public static circle_ui() { return new FixedUiElement(Svg.circle_img);} public static clock = " image/svg+xml " public static clock_img = Img.AsImageElement(Svg.clock) - public static clock_svg() { return new FixedUiElement(Svg.clock);} + public static clock_svg() { return new Img(Svg.clock, true);} public static clock_ui() { return new FixedUiElement(Svg.clock_img);} public static close = " image/svg+xml " public static close_img = Img.AsImageElement(Svg.close) - public static close_svg() { return new FixedUiElement(Svg.close);} + public static close_svg() { return new Img(Svg.close, true);} public static close_ui() { return new FixedUiElement(Svg.close_img);} public static compass = " image/svg+xml N S E W NW SW NE SE " public static compass_img = Img.AsImageElement(Svg.compass) - public static compass_svg() { return new FixedUiElement(Svg.compass);} + public static compass_svg() { return new Img(Svg.compass, true);} public static compass_ui() { return new FixedUiElement(Svg.compass_img);} public static cross_bottom_right = " image/svg+xml " public static cross_bottom_right_img = Img.AsImageElement(Svg.cross_bottom_right) - public static cross_bottom_right_svg() { return new FixedUiElement(Svg.cross_bottom_right);} + public static cross_bottom_right_svg() { return new Img(Svg.cross_bottom_right, true);} public static cross_bottom_right_ui() { return new FixedUiElement(Svg.cross_bottom_right_img);} public static crosshair_blue_center = " image/svg+xml " public static crosshair_blue_center_img = Img.AsImageElement(Svg.crosshair_blue_center) - public static crosshair_blue_center_svg() { return new FixedUiElement(Svg.crosshair_blue_center);} + public static crosshair_blue_center_svg() { return new Img(Svg.crosshair_blue_center, true);} public static crosshair_blue_center_ui() { return new FixedUiElement(Svg.crosshair_blue_center_img);} public static crosshair_blue = " image/svg+xml " public static crosshair_blue_img = Img.AsImageElement(Svg.crosshair_blue) - public static crosshair_blue_svg() { return new FixedUiElement(Svg.crosshair_blue);} + public static crosshair_blue_svg() { return new Img(Svg.crosshair_blue, true);} public static crosshair_blue_ui() { return new FixedUiElement(Svg.crosshair_blue_img);} public static crosshair = " image/svg+xml " public static crosshair_img = Img.AsImageElement(Svg.crosshair) - public static crosshair_svg() { return new FixedUiElement(Svg.crosshair);} + public static crosshair_svg() { return new Img(Svg.crosshair, true);} public static crosshair_ui() { return new FixedUiElement(Svg.crosshair_img);} public static delete_icon = " image/svg+xml " public static delete_icon_img = Img.AsImageElement(Svg.delete_icon) - public static delete_icon_svg() { return new FixedUiElement(Svg.delete_icon);} + public static delete_icon_svg() { return new Img(Svg.delete_icon, true);} public static delete_icon_ui() { return new FixedUiElement(Svg.delete_icon_img);} public static direction = " image/svg+xml " public static direction_img = Img.AsImageElement(Svg.direction) - public static direction_svg() { return new FixedUiElement(Svg.direction);} + public static direction_svg() { return new Img(Svg.direction, true);} public static direction_ui() { return new FixedUiElement(Svg.direction_img);} public static direction_gradient = " image/svg+xml " public static direction_gradient_img = Img.AsImageElement(Svg.direction_gradient) - public static direction_gradient_svg() { return new FixedUiElement(Svg.direction_gradient);} + public static direction_gradient_svg() { return new Img(Svg.direction_gradient, true);} public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);} public static down = " image/svg+xml " public static down_img = Img.AsImageElement(Svg.down) - public static down_svg() { return new FixedUiElement(Svg.down);} + public static down_svg() { return new Img(Svg.down, true);} public static down_ui() { return new FixedUiElement(Svg.down_img);} public static envelope = " image/svg+xml " public static envelope_img = Img.AsImageElement(Svg.envelope) - public static envelope_svg() { return new FixedUiElement(Svg.envelope);} + public static envelope_svg() { return new Img(Svg.envelope, true);} public static envelope_ui() { return new FixedUiElement(Svg.envelope_img);} public static floppy = " " public static floppy_img = Img.AsImageElement(Svg.floppy) - public static floppy_svg() { return new FixedUiElement(Svg.floppy);} + public static floppy_svg() { return new Img(Svg.floppy, true);} public static floppy_ui() { return new FixedUiElement(Svg.floppy_img);} public static gear = " " public static gear_img = Img.AsImageElement(Svg.gear) - public static gear_svg() { return new FixedUiElement(Svg.gear);} + public static gear_svg() { return new Img(Svg.gear, true);} public static gear_ui() { return new FixedUiElement(Svg.gear_img);} public static help = " " public static help_img = Img.AsImageElement(Svg.help) - public static help_svg() { return new FixedUiElement(Svg.help);} + public static help_svg() { return new Img(Svg.help, true);} public static help_ui() { return new FixedUiElement(Svg.help_img);} public static home = " " public static home_img = Img.AsImageElement(Svg.home) - public static home_svg() { return new FixedUiElement(Svg.home);} + public static home_svg() { return new Img(Svg.home, true);} public static home_ui() { return new FixedUiElement(Svg.home_img);} public static home_white_bg = " image/svg+xml " public static home_white_bg_img = Img.AsImageElement(Svg.home_white_bg) - public static home_white_bg_svg() { return new FixedUiElement(Svg.home_white_bg);} + public static home_white_bg_svg() { return new Img(Svg.home_white_bg, true);} public static home_white_bg_ui() { return new FixedUiElement(Svg.home_white_bg_img);} public static josm_logo = " JOSM Logotype 2019 image/svg+xml JOSM Logotype 2019 2019-08-05 Diamond00744 Public Domain " public static josm_logo_img = Img.AsImageElement(Svg.josm_logo) - public static josm_logo_svg() { return new FixedUiElement(Svg.josm_logo);} + public static josm_logo_svg() { return new Img(Svg.josm_logo, true);} public static josm_logo_ui() { return new FixedUiElement(Svg.josm_logo_img);} public static layers = " image/svg+xml " public static layers_img = Img.AsImageElement(Svg.layers) - public static layers_svg() { return new FixedUiElement(Svg.layers);} + public static layers_svg() { return new Img(Svg.layers, true);} public static layers_ui() { return new FixedUiElement(Svg.layers_img);} public static layersAdd = " image/svg+xml " public static layersAdd_img = Img.AsImageElement(Svg.layersAdd) - public static layersAdd_svg() { return new FixedUiElement(Svg.layersAdd);} + public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} public static logo = " image/svg+xml " public static logo_img = Img.AsImageElement(Svg.logo) - public static logo_svg() { return new FixedUiElement(Svg.logo);} + public static logo_svg() { return new Img(Svg.logo, true);} public static logo_ui() { return new FixedUiElement(Svg.logo_img);} public static logout = " image/svg+xml " public static logout_img = Img.AsImageElement(Svg.logout) - public static logout_svg() { return new FixedUiElement(Svg.logout);} + public static logout_svg() { return new Img(Svg.logout, true);} public static logout_ui() { return new FixedUiElement(Svg.logout_img);} public static mapcomplete_logo = " OpenStreetMap logo 2011 image/svg+xml OpenStreetMap logo 2011 Ken Vermette April 2011 OpenStreetMap.org Replacement logo for OpenStreetMap Foundation OSM openstreetmap logo http://wiki.openstreetmap.org/wiki/File:Public-images-osm_logo.svg 010110010011010110010011ย  010110010011010110010011ย  " public static mapcomplete_logo_img = Img.AsImageElement(Svg.mapcomplete_logo) - public static mapcomplete_logo_svg() { return new FixedUiElement(Svg.mapcomplete_logo);} + public static mapcomplete_logo_svg() { return new Img(Svg.mapcomplete_logo, true);} public static mapcomplete_logo_ui() { return new FixedUiElement(Svg.mapcomplete_logo_img);} public static mapillary = "" public static mapillary_img = Img.AsImageElement(Svg.mapillary) - public static mapillary_svg() { return new FixedUiElement(Svg.mapillary);} + public static mapillary_svg() { return new Img(Svg.mapillary, true);} public static mapillary_ui() { return new FixedUiElement(Svg.mapillary_img);} public static mapillary_black = " image/svg+xml " public static mapillary_black_img = Img.AsImageElement(Svg.mapillary_black) - public static mapillary_black_svg() { return new FixedUiElement(Svg.mapillary_black);} + public static mapillary_black_svg() { return new Img(Svg.mapillary_black, true);} public static mapillary_black_ui() { return new FixedUiElement(Svg.mapillary_black_img);} public static min = " image/svg+xml " public static min_img = Img.AsImageElement(Svg.min) - public static min_svg() { return new FixedUiElement(Svg.min);} + public static min_svg() { return new Img(Svg.min, true);} public static min_ui() { return new FixedUiElement(Svg.min_img);} public static no_checkmark = " " public static no_checkmark_img = Img.AsImageElement(Svg.no_checkmark) - public static no_checkmark_svg() { return new FixedUiElement(Svg.no_checkmark);} + public static no_checkmark_svg() { return new Img(Svg.no_checkmark, true);} public static no_checkmark_ui() { return new FixedUiElement(Svg.no_checkmark_img);} public static or = " image/svg+xml " public static or_img = Img.AsImageElement(Svg.or) - public static or_svg() { return new FixedUiElement(Svg.or);} + public static or_svg() { return new Img(Svg.or, true);} public static or_ui() { return new FixedUiElement(Svg.or_img);} public static osm_copyright = " image/svg+xml " public static osm_copyright_img = Img.AsImageElement(Svg.osm_copyright) - public static osm_copyright_svg() { return new FixedUiElement(Svg.osm_copyright);} + public static osm_copyright_svg() { return new Img(Svg.osm_copyright, true);} public static osm_copyright_ui() { return new FixedUiElement(Svg.osm_copyright_img);} public static osm_logo_us = "" public static osm_logo_us_img = Img.AsImageElement(Svg.osm_logo_us) - public static osm_logo_us_svg() { return new FixedUiElement(Svg.osm_logo_us);} + public static osm_logo_us_svg() { return new Img(Svg.osm_logo_us, true);} public static osm_logo_us_ui() { return new FixedUiElement(Svg.osm_logo_us_img);} public static osm_logo = " OpenStreetMap logo 2011 image/svg+xml OpenStreetMap logo 2011 Ken Vermette April 2011 OpenStreetMap.org Replacement logo for OpenStreetMap Foundation OSM openstreetmap logo http://wiki.openstreetmap.org/wiki/File:Public-images-osm_logo.svg 010110010011010110010011 010110010011010110010011 " public static osm_logo_img = Img.AsImageElement(Svg.osm_logo) - public static osm_logo_svg() { return new FixedUiElement(Svg.osm_logo);} + public static osm_logo_svg() { return new Img(Svg.osm_logo, true);} public static osm_logo_ui() { return new FixedUiElement(Svg.osm_logo_img);} public static pencil = " " public static pencil_img = Img.AsImageElement(Svg.pencil) - public static pencil_svg() { return new FixedUiElement(Svg.pencil);} + public static pencil_svg() { return new Img(Svg.pencil, true);} public static pencil_ui() { return new FixedUiElement(Svg.pencil_img);} public static phone = " image/svg+xml " public static phone_img = Img.AsImageElement(Svg.phone) - public static phone_svg() { return new FixedUiElement(Svg.phone);} + public static phone_svg() { return new Img(Svg.phone, true);} public static phone_ui() { return new FixedUiElement(Svg.phone_img);} public static pin = " image/svg+xml " public static pin_img = Img.AsImageElement(Svg.pin) - public static pin_svg() { return new FixedUiElement(Svg.pin);} + public static pin_svg() { return new Img(Svg.pin, true);} public static pin_ui() { return new FixedUiElement(Svg.pin_img);} public static plus = " image/svg+xml " public static plus_img = Img.AsImageElement(Svg.plus) - public static plus_svg() { return new FixedUiElement(Svg.plus);} + public static plus_svg() { return new Img(Svg.plus, true);} public static plus_ui() { return new FixedUiElement(Svg.plus_img);} public static pop_out = " Svg Vector Icons : http://www.onlinewebfonts.com/icon " public static pop_out_img = Img.AsImageElement(Svg.pop_out) - public static pop_out_svg() { return new FixedUiElement(Svg.pop_out);} + public static pop_out_svg() { return new Img(Svg.pop_out, true);} public static pop_out_ui() { return new FixedUiElement(Svg.pop_out_img);} public static reload = " " public static reload_img = Img.AsImageElement(Svg.reload) - public static reload_svg() { return new FixedUiElement(Svg.reload);} + public static reload_svg() { return new Img(Svg.reload, true);} public static reload_ui() { return new FixedUiElement(Svg.reload_img);} public static ring = " image/svg+xml " public static ring_img = Img.AsImageElement(Svg.ring) - public static ring_svg() { return new FixedUiElement(Svg.ring);} + public static ring_svg() { return new Img(Svg.ring, true);} public static ring_ui() { return new FixedUiElement(Svg.ring_img);} public static search = " " public static search_img = Img.AsImageElement(Svg.search) - public static search_svg() { return new FixedUiElement(Svg.search);} + public static search_svg() { return new Img(Svg.search, true);} public static search_ui() { return new FixedUiElement(Svg.search_img);} public static send_email = " image/svg+xml " public static send_email_img = Img.AsImageElement(Svg.send_email) - public static send_email_svg() { return new FixedUiElement(Svg.send_email);} + public static send_email_svg() { return new Img(Svg.send_email, true);} public static send_email_ui() { return new FixedUiElement(Svg.send_email_img);} public static share = " image/svg+xml " public static share_img = Img.AsImageElement(Svg.share) - public static share_svg() { return new FixedUiElement(Svg.share);} + public static share_svg() { return new Img(Svg.share, true);} public static share_ui() { return new FixedUiElement(Svg.share_img);} public static square = " image/svg+xml " public static square_img = Img.AsImageElement(Svg.square) - public static square_svg() { return new FixedUiElement(Svg.square);} + public static square_svg() { return new Img(Svg.square, true);} public static square_ui() { return new FixedUiElement(Svg.square_img);} public static star = " Created by potrace 1.15, written by Peter Selinger 2001-2017 " public static star_img = Img.AsImageElement(Svg.star) - public static star_svg() { return new FixedUiElement(Svg.star);} + public static star_svg() { return new Img(Svg.star, true);} public static star_ui() { return new FixedUiElement(Svg.star_img);} public static star_half = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static star_half_img = Img.AsImageElement(Svg.star_half) - public static star_half_svg() { return new FixedUiElement(Svg.star_half);} + public static star_half_svg() { return new Img(Svg.star_half, true);} public static star_half_ui() { return new FixedUiElement(Svg.star_half_img);} public static star_outline = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static star_outline_img = Img.AsImageElement(Svg.star_outline) - public static star_outline_svg() { return new FixedUiElement(Svg.star_outline);} + public static star_outline_svg() { return new Img(Svg.star_outline, true);} public static star_outline_ui() { return new FixedUiElement(Svg.star_outline_img);} public static star_outline_half = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static star_outline_half_img = Img.AsImageElement(Svg.star_outline_half) - public static star_outline_half_svg() { return new FixedUiElement(Svg.star_outline_half);} + public static star_outline_half_svg() { return new Img(Svg.star_outline_half, true);} public static star_outline_half_ui() { return new FixedUiElement(Svg.star_outline_half_img);} public static statistics = " Svg Vector Icons : http://www.onlinewebfonts.com/icon " public static statistics_img = Img.AsImageElement(Svg.statistics) - public static statistics_svg() { return new FixedUiElement(Svg.statistics);} + public static statistics_svg() { return new Img(Svg.statistics, true);} public static statistics_ui() { return new FixedUiElement(Svg.statistics_img);} public static translate = " " public static translate_img = Img.AsImageElement(Svg.translate) - public static translate_svg() { return new FixedUiElement(Svg.translate);} + public static translate_svg() { return new Img(Svg.translate, true);} public static translate_ui() { return new FixedUiElement(Svg.translate_img);} public static up = " " public static up_img = Img.AsImageElement(Svg.up) - public static up_svg() { return new FixedUiElement(Svg.up);} + public static up_svg() { return new Img(Svg.up, true);} public static up_ui() { return new FixedUiElement(Svg.up_img);} public static wikidata = " " public static wikidata_img = Img.AsImageElement(Svg.wikidata) - public static wikidata_svg() { return new FixedUiElement(Svg.wikidata);} + public static wikidata_svg() { return new Img(Svg.wikidata, true);} public static wikidata_ui() { return new FixedUiElement(Svg.wikidata_img);} public static wikimedia_commons_white = " Wikimedia Commons Logo " public static wikimedia_commons_white_img = Img.AsImageElement(Svg.wikimedia_commons_white) - public static wikimedia_commons_white_svg() { return new FixedUiElement(Svg.wikimedia_commons_white);} + public static wikimedia_commons_white_svg() { return new Img(Svg.wikimedia_commons_white, true);} public static wikimedia_commons_white_ui() { return new FixedUiElement(Svg.wikimedia_commons_white_img);} public static wikipedia = " Wikipedia logo version 2" public static wikipedia_img = Img.AsImageElement(Svg.wikipedia) - public static wikipedia_svg() { return new FixedUiElement(Svg.wikipedia);} + public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/Base/Img.ts b/UI/Base/Img.ts index 23c89fa33c..f2628bdb9f 100644 --- a/UI/Base/Img.ts +++ b/UI/Base/Img.ts @@ -3,10 +3,12 @@ import BaseUIElement from "../BaseUIElement"; export default class Img extends BaseUIElement { private _src: string; + private readonly _rawSvg: boolean; - constructor(src: string) { + constructor(src: string, rawSvg = false) { super(); this._src = src; + this._rawSvg = rawSvg; } static AsData(source: string) { @@ -21,6 +23,13 @@ export default class Img extends BaseUIElement { } protected InnerConstructElement(): HTMLElement { + + if (this._rawSvg) { + const e = document.createElement("div") + e.innerHTML = this._src + return e; + } + const el = document.createElement("img") el.src = this._src; el.onload = () => { diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index 9d87c903d0..f1530a1a1c 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -29,14 +29,14 @@ export class SubtleButton extends UIElement { } 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 mr-4") const image = new Combine([img]) .SetClass("flex-shrink-0"); if (linkTo == undefined) { return new Combine([ image, - message?.SetClass("blcok ml-4 overflow-ellipsis"), + message?.SetClass("block overflow-ellipsis"), ]).SetClass("flex group w-full"); } @@ -44,7 +44,7 @@ export class SubtleButton extends UIElement { return new Link( new Combine([ image, - message?.SetClass("block ml-4 overflow-ellipsis") + message?.SetClass("block overflow-ellipsis") ]).SetClass("flex group w-full"), linkTo.url, linkTo.newTab ?? false diff --git a/UI/BigComponents/Basemap.ts b/UI/BigComponents/Basemap.ts index 5db429e0f1..d2b7036587 100644 --- a/UI/BigComponents/Basemap.ts +++ b/UI/BigComponents/Basemap.ts @@ -34,11 +34,11 @@ export class Basemap { this.map.setMaxBounds( [[-100, -200], [100, 200]] ); + this.map.attributionControl.setPrefix( " | OpenStreetMap"); extraAttribution.AttachTo('leaflet-attribution') - const self = this; let previousLayer = currentLayer.data; diff --git a/UI/BigComponents/ThemeIntroductionPanel.ts b/UI/BigComponents/ThemeIntroductionPanel.ts index 7ea3701da9..f3b66e8803 100644 --- a/UI/BigComponents/ThemeIntroductionPanel.ts +++ b/UI/BigComponents/ThemeIntroductionPanel.ts @@ -20,7 +20,7 @@ export default class ThemeIntroductionPanel extends VariableUiElement { ; const toTheMap = new SubtleButton( - new FixedUiElement(""), + undefined, Translations.t.general.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center") ).onClick(() =>{ isShown.setData(false) diff --git a/UI/Image/AttributedImage.ts b/UI/Image/AttributedImage.ts new file mode 100644 index 0000000000..260c5306f8 --- /dev/null +++ b/UI/Image/AttributedImage.ts @@ -0,0 +1,19 @@ +import Combine from "../Base/Combine"; +import Attribution from "./Attribution"; +import Img from "../Base/Img"; +import ImageAttributionSource from "../../Logic/Web/ImageAttributionSource"; + + +export class AttributedImage extends Combine { + + constructor(urlSource: string, imgSource: ImageAttributionSource) { + urlSource = imgSource.PrepareUrl(urlSource) + super([ + new Img( urlSource), + new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()) + ]); + this.SetClass('block relative h-full'); + } + + +} \ No newline at end of file diff --git a/UI/Image/Attribution.ts b/UI/Image/Attribution.ts index 78b6171d17..514bc475a9 100644 --- a/UI/Image/Attribution.ts +++ b/UI/Image/Attribution.ts @@ -1,19 +1,33 @@ -import {UIElement} from "../UIElement"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {LicenseInfo} from "../../Logic/Web/Wikimedia"; -export default class Attribution extends Combine { +export default class Attribution extends VariableUiElement { - constructor(author: BaseUIElement | string, license: BaseUIElement | string, icon: BaseUIElement) { - super([ - icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em"), - new Combine([ - Translations.W(author).SetClass("block font-bold"), - Translations.W((license ?? "") === "undefined" ? "CC0" : (license ?? "")) - ]).SetClass("flex flex-col") - ]); - this.SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg"); + constructor(license: UIEventSource, icon: BaseUIElement) { + if (license === undefined) { + throw "No license source given in the attribution element" + } + super( + license.map((license : LicenseInfo) => { + + if (license?.artist === undefined) { + return undefined; + } + + return new Combine([ + icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), + + new Combine([ + Translations.W(license.artist).SetClass("block font-bold"), + Translations.W((license.license ?? "") === "" ? "CC0" : (license.license ?? "")) + ]).SetClass("flex flex-col") + ]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg") + + })); } } \ No newline at end of file diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 4a29d6a0b4..a28d89a2cf 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -2,12 +2,14 @@ import {SlideShow} from "./SlideShow"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; import DeleteImage from "./DeleteImage"; -import {WikimediaImage} from "./WikimediaImage"; -import {ImgurImage} from "./ImgurImage"; -import {MapillaryImage} from "./MapillaryImage"; +import {AttributedImage} from "./AttributedImage"; import BaseUIElement from "../BaseUIElement"; import Img from "../Base/Img"; import Toggle from "../Input/Toggle"; +import ImageAttributionSource from "../../Logic/Web/ImageAttributionSource"; +import {Wikimedia} from "../../Logic/Web/Wikimedia"; +import {Mapillary} from "../../Logic/Web/Mapillary"; +import {Imgur} from "../../Logic/Web/Imgur"; export class ImageCarousel extends Toggle { @@ -45,17 +47,20 @@ export class ImageCarousel extends Toggle { */ private static CreateImageElement(url: string): BaseUIElement { // @ts-ignore + let attrSource : ImageAttributionSource = undefined; if (url.startsWith("File:")) { - return new WikimediaImage(url); + attrSource = Wikimedia.singleton } else if (url.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) { - const commons = url.substr("https://commons.wikimedia.org/wiki/".length); - return new WikimediaImage(commons); + attrSource = Wikimedia.singleton; } else if (url.toLowerCase().startsWith("https://i.imgur.com/")) { - return new ImgurImage(url); + attrSource = Imgur.singleton } else if (url.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) { - return new MapillaryImage(url); + attrSource = Mapillary.singleton } else { return new Img(url); } + + return new AttributedImage(url, attrSource) + } } \ No newline at end of file diff --git a/UI/Image/ImgurImage.ts b/UI/Image/ImgurImage.ts deleted file mode 100644 index 93355df4e3..0000000000 --- a/UI/Image/ImgurImage.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LicenseInfo} from "../../Logic/Web/Wikimedia"; -import {Imgur} from "../../Logic/Web/Imgur"; -import Combine from "../Base/Combine"; -import Attribution from "./Attribution"; -import BaseUIElement from "../BaseUIElement"; -import Img from "../Base/Img"; -import {VariableUiElement} from "../Base/VariableUIElement"; - - -export class ImgurImage extends UIElement { - - - /*** - * Dictionary from url to alreayd known license info - */ - private static allLicenseInfos: any = {}; - private readonly _imageMeta: UIEventSource; - private readonly _imageLocation: string; - - constructor(source: string) { - super() - this._imageLocation = source; - if (ImgurImage.allLicenseInfos[source] !== undefined) { - this._imageMeta = ImgurImage.allLicenseInfos[source]; - } else { - this._imageMeta = new UIEventSource(null); - ImgurImage.allLicenseInfos[source] = this._imageMeta; - const self = this; - Imgur.getDescriptionOfImage(source, (license) => { - self._imageMeta.setData(license) - }) - } - } - - InnerRender(): BaseUIElement { - const image = new Img( this._imageLocation); - - return new Combine([ - image, - new VariableUiElement(this._imageMeta.map(meta => (meta === undefined || meta === null) ? undefined : new Attribution(meta.artist, meta.license, undefined))) - ]).SetClass('block relative h-full'); - - } - - -} \ No newline at end of file diff --git a/UI/Image/MapillaryImage.ts b/UI/Image/MapillaryImage.ts deleted file mode 100644 index 53ec504313..0000000000 --- a/UI/Image/MapillaryImage.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {UIElement} from "../UIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LicenseInfo} from "../../Logic/Web/Wikimedia"; -import {Mapillary} from "../../Logic/Web/Mapillary"; -import Svg from "../../Svg"; -import Combine from "../Base/Combine"; -import Attribution from "./Attribution"; -import Img from "../Base/Img"; -import BaseUIElement from "../BaseUIElement"; - - -export class MapillaryImage extends UIElement { - - /*** - * Dictionary from url to already known license info - */ - private static allLicenseInfos: any = {}; - private readonly _imageMeta: UIEventSource; - private readonly _imageLocation: string; - - constructor(source: string) { - super() - - if (source.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) { - source = source.substring("https://www.mapillary.com/map/im/".length); - } - - this._imageLocation = source; - if (MapillaryImage.allLicenseInfos[source] !== undefined) { - this._imageMeta = MapillaryImage.allLicenseInfos[source]; - } else { - this._imageMeta = new UIEventSource(null); - MapillaryImage.allLicenseInfos[source] = this._imageMeta; - const self = this; - Mapillary.getDescriptionOfImage(source, (license) => { - self._imageMeta.setData(license) - }) - } - - this.ListenTo(this._imageMeta); - - } - - InnerRender(): BaseUIElement { - const url = `https://images.mapillary.com/${this._imageLocation}/thumb-640.jpg?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`; - const image = new Img(url) - - const meta = this._imageMeta?.data; - if (!meta) { - return image; - } - - return new Combine([ - image, - new Attribution(meta.artist, meta.license, Svg.mapillary_svg()) - ]).SetClass("relative block h-full"); - - } - - -} \ No newline at end of file diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index eea8ea5668..fdc8920085 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -11,23 +11,27 @@ export class SlideShow extends BaseUIElement { constructor(embeddedElements: UIEventSource) { super() this.embeddedElements =embeddedElements; - } + this.SetStyle("scroll-snap-type: x mandatory; overflow-x: scroll") + } protected InnerConstructElement(): HTMLElement { const el = document.createElement("div") - el.style.overflowX = "auto" - el.style.width = "min-content" el.style.minWidth = "min-content" el.style.display = "flex" - + el.style.justifyContent = "center" this.embeddedElements.addCallbackAndRun(elements => { + + if(elements.length > 1){ + el.style.justifyContent = "unset" + } + while (el.firstChild) { el.removeChild(el.lastChild) } elements = Utils.NoNull(elements).map(el => new Combine([el]) .SetClass("block relative ml-1 bg-gray-200 m-1 rounded slideshow-item") - .SetStyle("min-width: 150px; width: max-content; height: var(--image-carousel-height);max-height: var(--image-carousel-height);") + .SetStyle("min-width: 150px; width: max-content; height: var(--image-carousel-height);max-height: var(--image-carousel-height);scroll-snap-align: start;") ) for (const element of elements ?? []) { diff --git a/UI/Image/WikimediaImage.ts b/UI/Image/WikimediaImage.ts deleted file mode 100644 index 85b4c50a49..0000000000 --- a/UI/Image/WikimediaImage.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {UIElement} from "../UIElement"; -import {LicenseInfo, Wikimedia} from "../../Logic/Web/Wikimedia"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Svg from "../../Svg"; -import Link from "../Base/Link"; -import Combine from "../Base/Combine"; -import Attribution from "./Attribution"; -import BaseUIElement from "../BaseUIElement"; -import Img from "../Base/Img"; - - -export class WikimediaImage extends UIElement { - - - static allLicenseInfos: any = {}; - private readonly _imageMeta: UIEventSource; - private readonly _imageLocation: string; - - constructor(source: string) { - super(undefined) - this._imageLocation = source; - if (WikimediaImage.allLicenseInfos[source] !== undefined) { - this._imageMeta = WikimediaImage.allLicenseInfos[source]; - } else { - this._imageMeta = new UIEventSource(new LicenseInfo()); - WikimediaImage.allLicenseInfos[source] = this._imageMeta; - const self = this; - Wikimedia.LicenseData(source, (info) => { - self._imageMeta.setData(info); - }) - } - - this.ListenTo(this._imageMeta); - - - } - - InnerRender(): BaseUIElement { - const url = Wikimedia.ImageNameToUrl(this._imageLocation, 500, 400) - .replace(/'/g, '%27'); - const image = new Img(url) - const meta = this._imageMeta?.data; - - if (!meta) { - return image; - } - new Link(Svg.wikimedia_commons_white_img, - `https://commons.wikimedia.org/wiki/${this._imageLocation}`, true) - .SetStyle("width:2em;height: 2em"); - - return new Combine([ - image, - new Attribution(meta.artist, meta.license, Svg.wikimedia_commons_white_svg()) - ]).SetClass("relative block h-full") - - } - - -} \ No newline at end of file diff --git a/index.css b/index.css index 7e22c57b99..ebf83ca4bb 100644 --- a/index.css +++ b/index.css @@ -62,7 +62,7 @@ --variable-title-height: 0px; /* Set by javascript */ --return-to-the-map-height: 2em; - --image-carousel-height: 400px; + --image-carousel-height: 350px; } html, body { @@ -148,10 +148,6 @@ li::marker { .border-attention-catch{ border: 5px solid var(--catch-detail-color);} -.slick-prev:before, .slick-next:before { - /*Slideshow workaround*/ - color:black !important; -} #topleft-tools svg { fill: var(--foreground-color) !important; @@ -360,6 +356,6 @@ li::marker { .slideshow-item img{ - height: 100%; + height: var(--image-carousel-height); width: unset; } \ No newline at end of file diff --git a/index.manifest b/index.manifest index 9b311e9008..1eb1aec3ce 100644 --- a/index.manifest +++ b/index.manifest @@ -1,8 +1,10 @@ { "name": "index", + "short_name": "MapComplete", "start_url": "index.html", "display": "standalone", "background_color": "#fff", + "description": "A thematic map viewer and editor based on OpenStreetMap", "orientation": "portrait-primary, landscape-primary", "icons": [ { diff --git a/scripts/generateIncludedImages.ts b/scripts/generateIncludedImages.ts index f240be7ef8..f812bb2c41 100644 --- a/scripts/generateIncludedImages.ts +++ b/scripts/generateIncludedImages.ts @@ -28,7 +28,7 @@ function genImages() { .replace(/[ -]/g, "_"); module += ` public static ${name} = "${svg}"\n` module += ` public static ${name}_img = Img.AsImageElement(Svg.${name})\n` - module += ` public static ${name}_svg() { return new FixedUiElement(Svg.${name});}\n` + module += ` public static ${name}_svg() { return new Img(Svg.${name}, true);}\n` module += ` public static ${name}_ui() { return new FixedUiElement(Svg.${name}_img);}\n\n` allNames.push(`"${path}": Svg.${name}`) } diff --git a/test.ts b/test.ts index 8dc3264023..4bf602cf73 100644 --- a/test.ts +++ b/test.ts @@ -8,7 +8,8 @@ import TagRenderingQuestion from "./UI/Popup/TagRenderingQuestion"; import {SlideShow} from "./UI/Image/SlideShow"; import {FixedUiElement} from "./UI/Base/FixedUiElement"; import Img from "./UI/Base/Img"; -import {ImgurImage} from "./UI/Image/ImgurImage"; +import {AttributedImage} from "./UI/Image/AttributedImage"; +import {Imgur} from "./Logic/Web/Imgur"; function TestSlideshow(){ @@ -16,7 +17,7 @@ function TestSlideshow(){ new FixedUiElement("A"), new FixedUiElement("qmsldkfjqmlsdkjfmqlskdjfmqlksdf").SetClass("text-xl"), new Img("https://i.imgur.com/8lIQ5Hv.jpg"), - new ImgurImage("https://i.imgur.com/y5XudzW.jpg"), + new AttributedImage("https://i.imgur.com/y5XudzW.jpg", new Imgur()), new Img("https://www.grunge.com/img/gallery/the-real-reason-your-cat-sleeps-so-much/intro-1601496900.webp") ]) new SlideShow(elems).AttachTo("maindiv") From 55f03c3de7654a95e29f153dc045e46a42c5a971 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 18 Jun 2021 01:50:03 +0200 Subject: [PATCH 26/30] Use a dropdown if there are a lot of options to choose from in tagrenderingquestion --- UI/Popup/TagRenderingQuestion.ts | 72 +++++++++++++++++++++++--------- UI/ShowDataLayer.ts | 7 +++- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 70da15be8e..2b6dd7bc46 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -23,6 +23,7 @@ import {Tag} from "../../Logic/Tags/Tag"; import {And} from "../../Logic/Tags/And"; import {TagUtils} from "../../Logic/Tags/TagUtils"; import BaseUIElement from "../BaseUIElement"; +import {DropDown} from "../Input/DropDown"; /** * Shows the question element. @@ -71,7 +72,7 @@ export default class TagRenderingQuestion extends UIElement { } - this._saveButton = new SaveButton(this._inputElement.GetValue(), + this._saveButton = new SaveButton(this._inputElement.GetValue(), State.state?.osmConnection) .onClick(save) @@ -94,9 +95,8 @@ export default class TagRenderingQuestion extends UIElement { } ) ).SetClass("block break-all") - - - + + } InnerRender() { @@ -111,24 +111,58 @@ export default class TagRenderingQuestion extends UIElement { } private GenerateInputElement(): InputElement { - const ff = this.GenerateFreeform(); const self = this; - let mappings = - (this._configuration.mappings ?? []).map(mapping => self.GenerateMappingElement(mapping)); - mappings = Utils.NoNull(mappings); + let inputEls: InputElement[]; - if (mappings.length == 0) { + const mappings = (this._configuration.mappings??[]) + .filter( mapping => { + if(mapping.hideInAnswer === true){ + return false; + } + if (typeof (mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matchesProperties(this._tags.data)) { + return false; + } + return true; + }) + + + let allIfNots: TagsFilter[] = Utils.NoNull(this._configuration.mappings?.map(m => m.ifnot) ?? [] ); + const ff = this.GenerateFreeform(); + const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 + + if (mappings.length < 8 || this._configuration.multiAnswer || hasImages) { + inputEls = (mappings ?? []).map(mapping => self.GenerateMappingElement(mapping, allIfNots)); + inputEls = Utils.NoNull(inputEls); + } else { + const dropdown: InputElement = new DropDown("", + mappings.map(mapping => { + return { + value: new And([mapping.if, ...allIfNots]), + shown: Translations.WT(mapping.then).Clone() + } + }) + ) + + if (ff == undefined) { + return dropdown; + } else { + inputEls = [dropdown] + } + } + + + if (inputEls.length == 0) { return ff; } if (ff) { - mappings.push(ff); + inputEls.push(ff); } if (this._configuration.multiAnswer) { - return this.GenerateMultiAnswer(mappings, ff, this._configuration.mappings.map(mp => mp.ifnot)) + return this.GenerateMultiAnswer(inputEls, ff, this._configuration.mappings.map(mp => mp.ifnot)) } else { - return new RadioButton(mappings, false) + return new RadioButton(inputEls, false) } } @@ -237,16 +271,16 @@ export default class TagRenderingQuestion extends UIElement { if: TagsFilter, then: Translation, hideInAnswer: boolean | TagsFilter - }): InputElement { - if (mapping.hideInAnswer === true) { - return undefined; - } - if (typeof (mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matchesProperties(this._tags.data)) { - return undefined; + }, ifNot?: TagsFilter[]): InputElement { + + let tagging = mapping.if; + if (ifNot.length > 0) { + tagging = new And([tagging, ...ifNot]) } + return new FixedInputElement( new SubstitutedTranslation(mapping.then, this._tags), - mapping.if, + tagging, (t0, t1) => t1.isEquivalent(t0)); } diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 128a904a48..509ca2f5e5 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -159,7 +159,12 @@ export default class ShowDataLayer { return; } if (selected.properties.id === feature.properties.id) { - leafletLayer.openPopup() + // A small sanity check to prevent infinite loops: + // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again + if(selected.geometry.type === feature.geometry.type){ + leafletLayer.openPopup() + } + } }) From c41690aab35e310acd62d163ea6ce579d65bfed4 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 18 Jun 2021 14:30:49 +0200 Subject: [PATCH 27/30] Formatting --- UI/BigComponents/Basemap.ts | 9 +++++---- UI/Input/DirectionInput.ts | 35 +++++++++++++++++------------------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/UI/BigComponents/Basemap.ts b/UI/BigComponents/Basemap.ts index d2b7036587..5c2844bd0f 100644 --- a/UI/BigComponents/Basemap.ts +++ b/UI/BigComponents/Basemap.ts @@ -12,13 +12,14 @@ export class Basemap { constructor(leafletElementId: string, location: UIEventSource, currentLayer: UIEventSource, - lastClickLocation: UIEventSource<{ lat: number, lon: number }>, - extraAttribution: BaseUIElement) { + lastClickLocation?: UIEventSource<{ lat: number, lon: number }>, + extraAttribution?: BaseUIElement) { this.map = L.map(leafletElementId, { center: [location.data.lat ?? 0, location.data.lon ?? 0], zoom: location.data.zoom ?? 2, layers: [currentLayer.data.layer], zoomControl: false, + attributionControl: extraAttribution !== undefined }); L.control.scale( @@ -70,12 +71,12 @@ export class Basemap { this.map.on("click", function (e) { // @ts-ignore - lastClickLocation.setData({lat: e.latlng.lat, lon: e.latlng.lng}) + lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}) }); this.map.on("contextmenu", function (e) { // @ts-ignore - lastClickLocation.setData({lat: e.latlng.lat, lon: e.latlng.lng}); + lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}); }); diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index f5b1b05ef7..4be40b99e3 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -10,20 +10,28 @@ import {FixedUiElement} from "../Base/FixedUiElement"; */ export default class DirectionInput extends InputElement { - private readonly value: UIEventSource; public readonly IsSelected: UIEventSource = new UIEventSource(false); + private readonly value: UIEventSource; constructor(value?: UIEventSource) { super(); this.value = value ?? new UIEventSource(undefined); } - + + GetValue(): UIEventSource { + return this.value; + } + + IsValid(str: string): boolean { + const t = Number(str); + return !isNaN(t) && t >= 0 && t <= 360; + } protected InnerConstructElement(): HTMLElement { - const element = new Combine([ + const element = new Combine([ new FixedUiElement("").SetClass("w-full h-full absolute top-0 left-O rounded-full"), Svg.direction_svg().SetStyle( `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`) @@ -40,18 +48,12 @@ export default class DirectionInput extends InputElement { cone.style.transform = `rotate(${rotation}deg)`; }) - + this.RegisterTriggers(element) - + return element; } - - GetValue(): UIEventSource { - return this.value; - } - - private RegisterTriggers(htmlElement: HTMLElement) { const self = this; @@ -83,19 +85,16 @@ export default class DirectionInput extends InputElement { } htmlElement.onmouseup = (ev) => { - isDown = false; ev.preventDefault(); + isDown = false; + ev.preventDefault(); } htmlElement.onmousemove = (ev: MouseEvent) => { if (isDown) { onPosChange(ev.clientX, ev.clientY); - } ev.preventDefault(); + } + ev.preventDefault(); } } - IsValid(str: string): boolean { - const t = Number(str); - return !isNaN(t) && t >= 0 && t <= 360; - } - } \ No newline at end of file From 87bf376b2e6b2fbf055b17fbc574887225118a55 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 18 Jun 2021 16:19:07 +0200 Subject: [PATCH 28/30] Link to legacy custom theme generator --- UI/BigComponents/MoreScreen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index d4632b6280..8976abb29a 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -84,7 +84,7 @@ export default class MoreScreen extends Combine { }); } return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), { - url: "./customGenerator.html", + url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html", newTab: false }); }) From bf7e6376c0beba409bd7147522ed76c55fd170da Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 19 Jun 2021 18:28:30 +0200 Subject: [PATCH 29/30] Styling tweaks, better metadata handling at data upload --- Logic/Osm/ChangesetHandler.ts | 134 ++++++++++++++++---------------- UI/Base/SubtleButton.ts | 2 +- UI/BigComponents/SimpleAddUI.ts | 3 +- UI/Input/Checkboxes.ts | 14 +++- UI/Input/RadioButton.ts | 40 ++++++---- 5 files changed, 108 insertions(+), 85 deletions(-) diff --git a/Logic/Osm/ChangesetHandler.ts b/Logic/Osm/ChangesetHandler.ts index 18ddaa9260..5280510ff6 100644 --- a/Logic/Osm/ChangesetHandler.ts +++ b/Logic/Osm/ChangesetHandler.ts @@ -10,12 +10,11 @@ import Constants from "../../Models/Constants"; export class ChangesetHandler { + public readonly currentChangeset: UIEventSource; private readonly _dryRun: boolean; private readonly userDetails: UIEventSource; private readonly auth: any; - public readonly currentChangeset: UIEventSource; - constructor(layoutName: string, dryRun: boolean, osmConnection: OsmConnection, auth) { this._dryRun = dryRun; this.userDetails = osmConnection.userDetails; @@ -27,14 +26,34 @@ export class ChangesetHandler { } } + private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) { + const nodes = response.getElementsByTagName("node"); + // @ts-ignore + for (const node of nodes) { + const oldId = parseInt(node.attributes.old_id.value); + const newId = parseInt(node.attributes.new_id.value); + if (oldId !== undefined && newId !== undefined && + !isNaN(oldId) && !isNaN(newId)) { + if (oldId == newId) { + continue; + } + console.log("Rewriting id: ", oldId, "-->", newId); + const element = allElements.getEventSourceById("node/" + oldId); + element.data.id = "node/" + newId; + allElements.addElementById("node/" + newId, element); + element.ping(); + + } + } + } public UploadChangeset( layout: LayoutConfig, allElements: ElementStorage, generateChangeXML: (csid: string) => string, - continuation: () => void) { - - if(this.userDetails.data.csCount == 0){ + continuation: () => void) { + + if (this.userDetails.data.csCount == 0) { // The user became a contributor! this.userDetails.data.csCount = 1; this.userDetails.ping(); @@ -51,7 +70,7 @@ export class ChangesetHandler { if (this.currentChangeset.data === undefined || this.currentChangeset.data === "") { // We have to open a new changeset - this.OpenChangeset(layout,(csId) => { + this.OpenChangeset(layout, (csId) => { this.currentChangeset.setData(csId); const changeset = generateChangeXML(csId); console.log(changeset); @@ -86,31 +105,61 @@ export class ChangesetHandler { } } + public CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { + }) { + if (changesetId === undefined) { + changesetId = this.currentChangeset.data; + } + if (changesetId === undefined) { + return; + } + console.log("closing changeset", changesetId); + this.currentChangeset.setData(""); + this.auth.xhr({ + method: 'PUT', + path: '/api/0.6/changeset/' + changesetId + '/close', + }, function (err, response) { + if (response == null) { + + console.log("err", err); + } + console.log("Closed changeset ", changesetId) + + if (continuation !== undefined) { + continuation(); + } + }); + } private OpenChangeset( - layout : LayoutConfig, + layout: LayoutConfig, continuation: (changesetId: string) => void) { const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; - let surveySource = ""; - if (State.state.currentGPSLocation.data !== undefined) { - surveySource = '' - } + let path = window.location.pathname; + path = path.substr(1, path.lastIndexOf("/")); + const metadata = [ + ["created_by", `MapComplete ${Constants.vNumber}`], + ["comment", `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`], + ["theme", layout.id], + ["language", Locale.language.data], + ["host", window.location.host], + ["path", path], + ["source", State.state.currentGPSLocation.data !== undefined ? "survey" : undefined], + ["imagery", State.state.backgroundLayer.data.id], + ["theme-creator", layout.maintainer] + ] + .filter(kv => (kv[1] ?? "") !== "") + .map(kv => ``) + .join("\n") this.auth.xhr({ method: 'PUT', path: '/api/0.6/changeset/create', options: {header: {'Content-Type': 'text/xml'}}, content: [``, - ``, - ``, - ``, - ``, - ``, - ``, - surveySource, - (layout.maintainer ?? "") !== "" ? `` : "", + metadata, ``].join("") }, function (err, response) { if (response === undefined) { @@ -147,52 +196,5 @@ export class ChangesetHandler { }); } - public CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { - }) { - if (changesetId === undefined) { - changesetId = this.currentChangeset.data; - } - if (changesetId === undefined) { - return; - } - console.log("closing changeset", changesetId); - this.currentChangeset.setData(""); - this.auth.xhr({ - method: 'PUT', - path: '/api/0.6/changeset/' + changesetId + '/close', - }, function (err, response) { - if (response == null) { - - console.log("err", err); - } - console.log("Closed changeset ", changesetId) - - if (continuation !== undefined) { - continuation(); - } - }); - } - - private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) { - const nodes = response.getElementsByTagName("node"); - // @ts-ignore - for (const node of nodes) { - const oldId = parseInt(node.attributes.old_id.value); - const newId = parseInt(node.attributes.new_id.value); - if (oldId !== undefined && newId !== undefined && - !isNaN(oldId) && !isNaN(newId)) { - if(oldId == newId){ - continue; - } - console.log("Rewriting id: ", oldId, "-->", newId); - const element = allElements.getEventSourceById("node/" + oldId); - element.data.id = "node/" + newId; - allElements.addElementById("node/" + newId, element); - element.ping(); - - } - } - } - } \ No newline at end of file diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index f1530a1a1c..a324505f77 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -25,7 +25,7 @@ export class SubtleButton extends UIElement { if ((imageUrl ?? "") === "") { img = undefined; } else if (typeof (imageUrl) === "string") { - img = new Img(imageUrl).SetClass("w-full") + img = new Img(imageUrl) } else { img = imageUrl; } diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 78730096f5..6b0cbf3c42 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -159,8 +159,7 @@ export default class SimpleAddUI extends Toggle { return new Toggle( Translations.t.general.presetInfo.Subs({ tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"), - - }), + }).SetStyle("word-break: break-all"), undefined, State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt) diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index e46227ecf4..3ae6b5fc6e 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -44,7 +44,7 @@ export default class CheckBoxes extends InputElement { input.id = "checkbox" + id input.type = "checkbox" - input.classList.add("p-1","cursor-pointer","ml-3","pl-3") + input.classList.add("p-1","cursor-pointer","m-3","pl-3","mr-0") const label = document.createElement("label") label.htmlFor = input.id @@ -52,7 +52,7 @@ export default class CheckBoxes extends InputElement { label.classList.add("block","w-full","p-2","cursor-pointer","bg-red") const wrapper = document.createElement("span") - wrapper.classList.add("flex","w-full","border", "border-gray-400") + wrapper.classList.add("flex","w-full","border", "border-gray-400","m-1") wrapper.appendChild(input) wrapper.appendChild(label) el.appendChild(wrapper) @@ -64,6 +64,16 @@ export default class CheckBoxes extends InputElement { if (selectedValues.indexOf(i) >= 0) { input.checked = true; } + + + if(input.checked){ + wrapper.classList.remove("border-gray-400") + wrapper.classList.add("border-black") + }else{ + wrapper.classList.add("border-gray-400") + wrapper.classList.remove("border-black") + } + }) input.onchange = () => { diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 0ed44402c9..fd5c006c2e 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -70,18 +70,7 @@ export class RadioButton extends InputElement { const form = document.createElement("form") const inputs = [] - - value.addCallbackAndRun( - selected => { - - let somethingChecked = false; - for (let i = 0; i < inputs.length; i++){ - let input = inputs[i]; - input.checked = !somethingChecked && elements[i].IsValid(selected); - somethingChecked = somethingChecked || input.checked - } - } - ) + const wrappers: HTMLElement[] = [] for (let i1 = 0; i1 < elements.length; i1++) { let element = elements[i1]; @@ -94,7 +83,7 @@ export class RadioButton extends InputElement { input.id = "radio" + groupId + "-" + i1; input.name = groupId; input.type = "radio" - input.classList.add("p-1","cursor-pointer","ml-2","pl-2","pr-0","m-0","ml-3") + input.classList.add("p-1","cursor-pointer","ml-2","pl-2","pr-0","m-3","mr-0") input.onchange = () => { if(input.checked){ @@ -114,12 +103,35 @@ export class RadioButton extends InputElement { const block = document.createElement("div") block.appendChild(input) block.appendChild(label) - block.classList.add("flex","w-full","border", "rounded-full", "border-gray-400") + block.classList.add("flex","w-full","border", "rounded-full", "border-gray-400","m-1") + wrappers.push(block) form.appendChild(block) } + value.addCallbackAndRun( + selected => { + + let somethingChecked = false; + for (let i = 0; i < inputs.length; i++){ + let input = inputs[i]; + input.checked = !somethingChecked && elements[i].IsValid(selected); + somethingChecked = somethingChecked || input.checked + + if(input.checked){ + wrappers[i].classList.remove("border-gray-400") + wrappers[i].classList.add("border-black") + }else{ + wrappers[i].classList.add("border-gray-400") + wrappers[i].classList.remove("border-black") + } + + } + } + ) + + this.SetClass("flex flex-col") return form; } From 106d9927aaa7027686188dee7d2d3e9f95d2bee8 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 19 Jun 2021 19:16:20 +0200 Subject: [PATCH 30/30] Fixed the personal layer --- Customizations/JSON/LayerConfig.ts | 3 +- Logic/UIEventSource.ts | 13 +- UI/BigComponents/PersonalLayersPanel.ts | 212 +++++++++++------------- 3 files changed, 110 insertions(+), 118 deletions(-) diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index fa615d7a63..10d8ddfe1b 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -12,7 +12,6 @@ import Combine from "../../UI/Base/Combine"; import {VariableUiElement} from "../../UI/Base/VariableUIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import {FixedUiElement} from "../../UI/Base/FixedUiElement"; -import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation"; import SourceConfig from "./SourceConfig"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {Tag} from "../../Logic/Tags/Tag"; @@ -290,7 +289,7 @@ export default class LayerConfig { } - public GenerateLeafletStyle(tags: UIEventSource, clickable: boolean): + public GenerateLeafletStyle(tags: UIEventSource, clickable: boolean, widthHeight= "100%"): { icon: { diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 63ed45d09e..7fa5a432d0 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -92,9 +92,16 @@ export class UIEventSource { } } - public map(f: ((T) => J), + /** + * Monoidal map: + * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' + * @param f: The transforming function + * @param extraSources: also trigger the update if one of these sources change + * @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData + */ + public map(f: ((t: T) => J), extraSources: UIEventSource[] = [], - g: ((J) => T) = undefined): UIEventSource { + g: ((j:J, t:T) => T) = undefined): UIEventSource { const self = this; const newSource = new UIEventSource( @@ -113,7 +120,7 @@ export class UIEventSource { if (g !== undefined) { newSource.addCallback((latest) => { - self.setData(g(latest)); + self.setData(g(latest, self.data)); }) } diff --git a/UI/BigComponents/PersonalLayersPanel.ts b/UI/BigComponents/PersonalLayersPanel.ts index 51438c7599..7c30d916a6 100644 --- a/UI/BigComponents/PersonalLayersPanel.ts +++ b/UI/BigComponents/PersonalLayersPanel.ts @@ -1,6 +1,3 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; import Svg from "../../Svg"; import State from "../../State"; @@ -8,130 +5,119 @@ import Combine from "../Base/Combine"; import Toggle from "../Input/Toggle"; import {SubtleButton} from "../Base/SubtleButton"; import Translations from "../i18n/Translations"; -import * as personal from "../../assets/themes/personalLayout/personalLayout.json" -import Locale from "../i18n/Locale"; import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import Img from "../Base/Img"; +import {UIEventSource} from "../../Logic/UIEventSource"; -export default class PersonalLayersPanel extends UIElement { - private checkboxes: BaseUIElement[] = []; +export default class PersonalLayersPanel extends VariableUiElement { constructor() { - super(State.state.favouriteLayers); - this.ListenTo(State.state.osmConnection.userDetails); - this.ListenTo(Locale.language); - this.UpdateView([]); - const self = this; - State.state.installedThemes.addCallback(extraThemes => { - self.UpdateView(extraThemes.map(layout => layout.layout)); - }) - } + super( + State.state.installedThemes.map(installedThemes => { + const t = Translations.t.favourite; + // Lets get all the layers + const allThemes = AllKnownLayouts.layoutsList.concat(installedThemes.map(layout => layout.layout)) + .filter(theme => !theme.hideFromOverview) - private UpdateView(extraThemes: LayoutConfig[]) { - this.checkboxes = []; - const favs = State.state.favouriteLayers.data ?? []; - const controls = new Map>(); - const allLayouts = AllKnownLayouts.layoutsList.concat(extraThemes); - for (const layout of allLayouts) { - if (layout.id === personal.id) { - continue; - } - - if(layout.hideFromOverview){ - continue; - } - - const header = - new Combine([ - ``, - "", - layout.title, - "
    ", - layout.shortDescription ?? "" - ]).SetClass("block p1 overflow-auto rounded") - .SetStyle("background: #eee;") - this.checkboxes.push(header); - - for (const layer of layout.layers) { - if(layer === undefined){ - console.warn("Undefined layer for ",layout.id) - continue; - } - if (typeof layer === "string") { - continue; - } - let icon :BaseUIElement = layer.GenerateLeafletStyle(new UIEventSource({id:"node/-1"}), false).icon.html - ?? Svg.checkmark_svg(); - let iconUnset =new Combine([icon]); - icon.SetClass("single-layer-selection-toggle") - iconUnset.SetClass("single-layer-selection-toggle") - - - let name = layer.name ?? layer.id; - if (name === undefined) { - continue; - } - const content = new Combine([ - "", - name, - " ", - layer.description !== undefined ? new Combine(["
    ", layer.description]) : "", - ]) - - - const cb = new Toggle( - new SubtleButton( - icon, - content), - new SubtleButton( - iconUnset.SetStyle("opacity:0.1"), - new Combine(["", - content, - "" - ])), - controls[layer.id] ?? (favs.indexOf(layer.id) >= 0) - ).ToggleOnClick(); - cb.SetClass("custom-layer-checkbox"); - controls[layer.id] = cb.isEnabled; - - cb.isEnabled.addCallback((isEnabled) => { - const favs = State.state.favouriteLayers; - if (isEnabled) { - if(favs.data.indexOf(layer.id)>= 0){ - return; // Already added + const allLayers = [] + { + const seenLayers = new Set() + for (const layers of allThemes.map(theme => theme.layers)) { + for (const layer of layers) { + if (seenLayers.has(layer.id)) { + continue + } + seenLayers.add(layer.id) + allLayers.push(layer) } - favs.data.push(layer.id); - } else { - favs.data.splice(favs.data.indexOf(layer.id), 1); } - favs.ping(); - }) + } - this.checkboxes.push(cb); + // Time to create a panel based on them! + const panel: BaseUIElement = new Combine(allLayers.map(PersonalLayersPanel.CreateLayerToggle)); - } - - } - - State.state.favouriteLayers.addCallback((layers) => { - for (const layerId of layers) { - controls[layerId]?.setData(true); - } - }); + return new Toggle( + new Combine([ + t.panelIntro.Clone(), + panel + ]).SetClass("flex flex-col"), + new SubtleButton( + Svg.osm_logo_ui(), + t.loginNeeded.Clone().SetClass("text-center") + ).onClick(() => State.state.osmConnection.AttemptLogin()), + State.state.osmConnection.isLoggedIn + ) + }) + ) } - InnerRender(): BaseUIElement { - const t = Translations.t.favourite; + /*** + * Creates a toggle for the given layer, which'll update State.state.favouriteLayers right away + * @param layer + * @constructor + * @private + */ + private static CreateLayerToggle(layer: LayerConfig): Toggle { + const iconUrl = layer.icon.GetRenderValue({id: "node/-1"}).txt + let icon :BaseUIElement =new Combine([ layer.GenerateLeafletStyle( + new UIEventSource({id: "node/-1"}), + false, + "2em" + ).icon.html]).SetClass("relative") + let iconUnset =new Combine([ layer.GenerateLeafletStyle( + new UIEventSource({id: "node/-1"}), + false, + "2em" + ).icon.html]).SetClass("relative") + + iconUnset.SetStyle("opacity:0.1") + + let name = layer.name ; + if (name === undefined) { + return undefined; + } + const content = new Combine([ + Translations.WT(name).Clone().SetClass("font-bold"), + Translations.WT(layer.description)?.Clone() + ]).SetClass("flex flex-col") + + const contentUnselected = new Combine([ + Translations.WT(name).Clone().SetClass("font-bold"), + Translations.WT(layer.description)?.Clone() + ]).SetClass("flex flex-col line-through") + return new Toggle( - new Combine([ - t.panelIntro, - ...this.checkboxes - ]), - t.loginNeeded, - State.state.osmConnection.isLoggedIn - - ) + new SubtleButton( + icon, + content ), + new SubtleButton( + iconUnset, + contentUnselected + ), + State.state.favouriteLayers.map(favLayers => { + return favLayers.indexOf(layer.id) >= 0 + }, [], (selected, current) => { + if (!selected && current.indexOf(layer.id) <= 0) { + // Not selected and not contained: nothing to change: we return current as is + return current; + } + if (selected && current.indexOf(layer.id) >= 0) { + // Selected and contained: this is fine! + return current; + } + const clone = [...current] + if (selected) { + clone.push(layer.id) + } else { + clone.splice(clone.indexOf(layer.id), 1) + } + return clone + }) + ).ToggleOnClick(); }