From 9cc721abad38681ed1ab5d8f682e7efabb9a8f82 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 14 Jun 2021 02:39:23 +0200 Subject: [PATCH] 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 be2adc52d..fa615d7a6 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 157bc2d1b..80e51c603 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 8fd6f88be..7858dba2a 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 b9b7866d9..d76ef1206 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 064da814c..b4d630070 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 f140f2fd2..004cefa2a 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 a350603c7..e7ec48ba9 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 c89927f3f..a65b80e6f 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 000000000..d7b45a399 --- /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 ec737381a..c87e12158 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 ca38f64d9..8d8285873 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 456f9139c..840814530 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 754d82b84..63f513488 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 08a8f68a5..7810c7b10 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 d6a27e49d..2bf2f800e 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 70a9ab3c5..7f32802f5 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 c6248681e..78730096f 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 df6de5e2e..656b9160d 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 bc2424939..de0f2b593 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 3ead32abf..ac0828f75 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 f3a4eb74f..ca9fd6fba 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 0019910a5..db89a4fd5 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 14025b905..64ffe59a2 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 89d7aa6db..415b30da5 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 1b716208c..800036151 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 cdf197c77..292b53021 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 ce2ac3423..fb0ecc32f 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 bb15b6dca..70560ad7e 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 e63a7f8e3..bd43e127f 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 e228d70d3..83b5f6752 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 ab13e4b40..508d9c026 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 9b41a260d..b8ae81e7e 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 5c73f84ed..a87f67a47 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 c6b0dabbb..10c8df5ee 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 287f85dc3..b92fc6e2b 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 0c1938a79..b4d07702c 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 f0415ad00..b094986df 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() {