From 3943100e5475df275846086088a8c6cae544426f Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 12 Jun 2021 02:58:32 +0200 Subject: [PATCH] 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 e8386d7c0..8fd6f88be 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 90d6cec31..64545ee90 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 b6f890614..16459bac8 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 e74fb1c97..f078ec624 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 25053f497..0e27ece69 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 254a1b042..89364807b 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 7641a4801..000000000 --- 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 e27afb3a1..c89927f3f 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 372ee7e03..23c89fa33 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 ea7d30c69..000000000 --- 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 d63a62011..10d9f2508 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 39f5f1bff..dd715b4df 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 36e46b4f5..000000000 --- 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 42636d431..ec737381a 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 260317eb0..0244b0e7b 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 aecf6695c..456f9139c 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 630ee5747..bad254726 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 6b3b830ab..191320f99 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 a55ddcab9..8a92e359d 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 9384671b9..5db429e0f 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 b521d0709..29b53f56f 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 d92fb5b0a..08a8f68a5 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 aee0e00b4..f3a9c799b 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 6713f9699..d6a27e49d 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 275828dfe..825ba2c96 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 0ad95828c..474abe7cd 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 9f1a109a1..c6248681e 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 61f7c2347..f3a907320 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 6248140e7..26214661a 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 e1f15e498..d136ea1d1 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 8f0745697..78b6171d1 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 9182406d2..1cdae491a 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 aa378beb8..025d8e550 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 4e376e7ef..4a187ed8b 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 a3ce32a65..2071d7480 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 f4fbdb20e..f3a4eb74f 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 e7c80e419..686a62b12 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 877cd8553..6276885b1 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 6ac2b3324..e060d0e6a 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 d01382c70..89d7aa6db 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 dc880d304..1b716208c 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 44b1cb3c1..cdf197c77 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 2eb7437f4..bb15b6dca 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 e817a8518..e228d70d3 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 b515cf501..72ae5e59c 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 cdc550874..c6b0dabbb 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 bb3bbb633..9330ff148 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 47cf7ebb0..e787b7649 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 47d533a44..287f85dc3 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 7aae201d5..b243a09fb 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 ae14051b8..000000000 --- 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