More refactoring and fixes

This commit is contained in:
Pieter Vander Vennet 2021-06-14 02:39:23 +02:00
parent d7004cd3dc
commit 9cc721abad
37 changed files with 519 additions and 632 deletions

View file

@ -325,7 +325,7 @@ export default class LayerConfig {
function render(tr: TagRenderingConfig, deflt?: string) { function render(tr: TagRenderingConfig, deflt?: string) {
const str = (tr?.GetRenderValue(tags.data)?.txt ?? deflt); 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(","); const iconSize = render(this.iconSize, "40,40,center").split(",");

View file

@ -7,6 +7,8 @@ import {Utils} from "../../Utils";
import {TagUtils} from "../../Logic/Tags/TagUtils"; import {TagUtils} from "../../Logic/Tags/TagUtils";
import {And} from "../../Logic/Tags/And"; import {And} from "../../Logic/Tags/And";
import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {UIElement} from "../../UI/UIElement";
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
/*** /***
* The parsed version of TagRenderingConfigJSON * The parsed version of TagRenderingConfigJSON
@ -240,6 +242,46 @@ export default class TagRenderingConfig {
return this.question === null && this.condition === null; 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) * Gets the correct rendering value (or undefined if not known)
* @constructor * @constructor

View file

@ -269,11 +269,10 @@ export class InitUiElements {
// ?-Button on Desktop, opens panel with close-X. // ?-Button on Desktop, opens panel with close-X.
const help = new MapControlButton(Svg.help_svg()); const help = new MapControlButton(Svg.help_svg());
help.onClick(() => isOpened.setData(true))
new Toggle( new Toggle(
fullOptions fullOptions
.SetClass("welcomeMessage") .SetClass("welcomeMessage"),
.onClick(() => {/*Catch the click*/
}),
help help
, isOpened , isOpened
).AttachTo("messagesbox"); ).AttachTo("messagesbox");
@ -308,7 +307,8 @@ export class InitUiElements {
copyrightNotice, copyrightNotice,
new MapControlButton(Svg.osm_copyright_svg()), new MapControlButton(Svg.osm_copyright_svg()),
copyrightNotice.isShown copyrightNotice.isShown
).SetClass("p-0.5") ).ToggleOnClick()
.SetClass("p-0.5")
const layerControlPanel = new LayerControlPanel( const layerControlPanel = new LayerControlPanel(
State.state.layerControlIsOpened) State.state.layerControlIsOpened)
@ -317,7 +317,7 @@ export class InitUiElements {
layerControlPanel, layerControlPanel,
new MapControlButton(Svg.layers_svg()), new MapControlButton(Svg.layers_svg()),
State.state.layerControlIsOpened State.state.layerControlIsOpened
) ).ToggleOnClick()
const layerControl = new Toggle( const layerControl = new Toggle(
layerControlButton, layerControlButton,

View file

@ -53,7 +53,6 @@ export default class OverpassFeatureSource implements FeatureSource {
return false; return false;
} }
let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
console.debug("overpass source: minzoom is ", minzoom)
return location.zoom >= minzoom; return location.zoom >= minzoom;
}, [layoutToUse] }, [layoutToUse]
); );

View file

@ -47,13 +47,13 @@ export default class StrayClickHandler {
popupAnchor: [0, -45] popupAnchor: [0, -45]
}) })
}); });
const popup = L.popup().setContent(uiToShow.Render()); const popup = L.popup().setContent("<div id='strayclick'></div>");
self._lastMarker.addTo(leafletMap.data); self._lastMarker.addTo(leafletMap.data);
self._lastMarker.bindPopup(popup); self._lastMarker.bindPopup(popup);
self._lastMarker.on("click", () => { self._lastMarker.on("click", () => {
uiToShow.AttachTo("strayclick")
uiToShow.Activate(); uiToShow.Activate();
uiToShow.Update();
}); });
}); });

View file

@ -65,7 +65,14 @@ export class OsmConnection {
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(), "userDetails"); this.userDetails = new UIEventSource<UserDetails>(new UserDetails(), "userDetails");
this.userDetails.data.dryRun = dryRun; 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._dryRun = dryRun;
this.updateAuthObject(); this.updateAuthObject();
@ -217,14 +224,15 @@ export class OsmConnection {
}); });
} }
private CheckForMessagesContinuously() { private CheckForMessagesContinuously(){
const self = this; const self =this;
window.setTimeout(() => { UIEventSource.Chronic(5 * 60 * 1000).addCallback(_ => {
if (self.userDetails.data.loggedIn) { if (self.isLoggedIn .data) {
console.log("Checking for messages") console.log("Checking for messages")
this.AttemptLogin(); self.AttemptLogin();
} }
}, 5 * 60 * 1000); });
} }

View file

@ -6,7 +6,6 @@ export default class Constants {
// The user journey states thresholds when a new feature gets unlocked // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {
addNewPointsUnlock: 0,
moreScreenUnlock: 1, moreScreenUnlock: 1,
personalLayoutUnlock: 15, personalLayoutUnlock: 15,
historyLinkVisible: 20, historyLinkVisible: 20,

View file

@ -1,4 +1,3 @@
import {UIElement} from "../UIElement";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
export class FixedUiElement extends BaseUIElement { export class FixedUiElement extends BaseUIElement {

34
UI/Base/List.ts Normal file
View file

@ -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;
}
}

View file

@ -35,8 +35,8 @@ export class SubtleButton extends UIElement {
if (linkTo == undefined) { if (linkTo == undefined) {
return new Combine([ return new Combine([
image, image,
message, message?.SetClass("blcok ml-4 overflow-ellipsis"),
]); ]).SetClass("flex group");
} }

View file

@ -24,10 +24,17 @@ export class VariableUiElement extends BaseUIElement {
el.innerHTML = contents el.innerHTML = contents
} else if (contents instanceof Array) { } else if (contents instanceof Array) {
for (const content of contents) { 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())
} }
}) })
} }

View file

@ -104,6 +104,9 @@ export default abstract class BaseUIElement {
return this._constructedHtmlElement return this._constructedHtmlElement
} }
if(this.InnerConstructElement === undefined){
throw "ERROR! This is not a correct baseUIElement: "+this.constructor.name
}
const el = this.InnerConstructElement(); const el = this.InnerConstructElement();

View file

@ -65,8 +65,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
); );
return new Toggle( return new Toggle(
new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab),
new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab),
new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab),
userDetails.map(userdetails => userDetails.map(userdetails =>
userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock)
) )

View file

@ -56,7 +56,7 @@ export default class LayerSelection extends Combine {
.SetStyle(styleWhole), .SetStyle(styleWhole),
new Combine([new Combine([iconUnselected, "<del>", name, "</del>"]).SetStyle(style), zoomStatus]) new Combine([new Combine([iconUnselected, "<del>", name, "</del>"]).SetStyle(style), zoomStatus])
.SetStyle(styleWhole), .SetStyle(styleWhole),
layer.isDisplayed) layer.isDisplayed).ToggleOnClick()
.SetStyle("margin:0.3em;") .SetStyle("margin:0.3em;")
); );
} }

View file

@ -91,7 +91,7 @@ export default class PersonalLayersPanel extends UIElement {
"</del>" "</del>"
])), ])),
controls[layer.id] ?? (favs.indexOf(layer.id) >= 0) controls[layer.id] ?? (favs.indexOf(layer.id) >= 0)
); ).ToggleOnClick();
cb.SetClass("custom-layer-checkbox"); cb.SetClass("custom-layer-checkbox");
controls[layer.id] = cb.isEnabled; controls[layer.id] = cb.isEnabled;

View file

@ -36,7 +36,7 @@ export default class ShareScreen extends Combine {
new Combine([check(), tr.fsIncludeCurrentLocation.Clone()]), new Combine([check(), tr.fsIncludeCurrentLocation.Clone()]),
new Combine([nocheck(), tr.fsIncludeCurrentLocation.Clone()]), new Combine([nocheck(), tr.fsIncludeCurrentLocation.Clone()]),
new UIEventSource<boolean>(true) new UIEventSource<boolean>(true)
) ).ToggleOnClick()
optionCheckboxes.push(includeLocation); optionCheckboxes.push(includeLocation);
const currentLocation = State.state?.locationControl; const currentLocation = State.state?.locationControl;
@ -71,7 +71,7 @@ export default class ShareScreen extends Combine {
new Combine([check(), currentBackground]), new Combine([check(), currentBackground]),
new Combine([nocheck(), currentBackground]), new Combine([nocheck(), currentBackground]),
new UIEventSource<boolean>(true) new UIEventSource<boolean>(true)
) ).ToggleOnClick()
optionCheckboxes.push(includeCurrentBackground); optionCheckboxes.push(includeCurrentBackground);
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => { optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
if (includeBG) { if (includeBG) {
@ -86,7 +86,7 @@ export default class ShareScreen extends Combine {
new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]), new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]),
new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]), new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]),
new UIEventSource<boolean>(true) new UIEventSource<boolean>(true)
) ).ToggleOnClick()
optionCheckboxes.push(includeLayerChoices); optionCheckboxes.push(includeLayerChoices);
optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => { 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([check(), Translations.W(swtch.human.Clone())]),
new Combine([nocheck(), Translations.W(swtch.human.Clone())]), new Combine([nocheck(), Translations.W(swtch.human.Clone())]),
new UIEventSource<boolean>(!swtch.reverse) new UIEventSource<boolean>(!swtch.reverse)
); ).ToggleOnClick();
optionCheckboxes.push(checkbox); optionCheckboxes.push(checkbox);
optionParts.push(checkbox.isEnabled.map((isEn) => { optionParts.push(checkbox.isEnabled.map((isEn) => {
if (isEn) { if (isEn) {

View file

@ -1,9 +1,7 @@
/** /**
* Asks to add a feature at the last clicked location, at least if zoom is sufficient * 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 {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import Svg from "../../Svg"; import Svg from "../../Svg";
import {SubtleButton} from "../Base/SubtleButton"; import {SubtleButton} from "../Base/SubtleButton";
import State from "../../State"; import State from "../../State";
@ -14,118 +12,163 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig";
import {Tag} from "../../Logic/Tags/Tag"; import {Tag} from "../../Logic/Tags/Tag";
import {TagUtils} from "../../Logic/Tags/TagUtils"; import {TagUtils} from "../../Logic/Tags/TagUtils";
import BaseUIElement from "../BaseUIElement"; 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<{ interface PresetInfo {
description: string | BaseUIElement, description: string | Translation,
name: string | BaseUIElement, name: string | BaseUIElement,
icon: BaseUIElement, icon: BaseUIElement,
tags: Tag[], tags: Tag[],
layerToAddTo: { layerToAddTo: {
layerDef: LayerConfig, layerDef: LayerConfig,
isDisplayed: UIEventSource<boolean> isDisplayed: UIEventSource<boolean>
} }
}> }
= new UIEventSource(undefined);
private _component:BaseUIElement; export default class SimpleAddUI extends Toggle {
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<boolean>) { constructor(isShown: UIEventSource<boolean>) {
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<PresetInfo>(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.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(() => { private static CreateConfirmButton(preset: PresetInfo,
State.state.layerControlIsOpened.setData(true); 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 const openLayerOrConfirm = new Toggle(
isShown.addCallback(isShown => confirmButton,
{ openLayerControl,
if(!isShown){ preset.layerToAddTo.isDisplayed
self._confirmPreset.setData(undefined) )
} const tagInfo = SimpleAddUI.CreateTagInfoFor(preset);
})
// If the click location changes, we reset the dialog as well
State.state.LastClickLocation.addCallback(() => {
self._confirmPreset.setData(undefined)
})
this._component = this.CreateContent();
}
InnerRender(): BaseUIElement { const cancelButton = new SubtleButton(Svg.close_ui(),
return this._component; 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 { private static CreateTagInfoFor(preset: PresetInfo, optionallyLinkToWiki = true) {
const userDetails = State.state.osmConnection.userDetails; const csCount = State.state.osmConnection.userDetails.data.csCount;
if (userDetails === undefined) { return new Toggle(
return undefined; 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) { undefined,
return new Combine([ State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt)
Translations.t.general.readYourMessages.Clone().SetClass("alert"), );
this.goToInboxButton
]);
}
if (userDetails.data.csCount < Constants.userJourney.addNewPointsUnlock) {
return new Combine(["<span class='alert'>",
Translations.t.general.fewChangesBefore,
"</span>"]);
}
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)
} }
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
private CreateContent(): BaseUIElement { const presetButtons = SimpleAddUI.CreatePresetButtons(selectedPreset)
const confirmPanel = this.CreateConfirmPanel();
if (confirmPanel !== undefined) {
return confirmPanel;
}
let intro: BaseUIElement = Translations.t.general.add.intro; let intro: BaseUIElement = Translations.t.general.add.intro;
let testMode: BaseUIElement = undefined; let testMode: BaseUIElement = undefined;
@ -133,113 +176,58 @@ export default class SimpleAddUI extends UIElement {
testMode = Translations.t.general.testing.Clone().SetClass("alert") testMode = Translations.t.general.testing.Clone().SetClass("alert")
} }
let presets = this.CreatePresetsPanel(); return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col")
return new Combine([intro, testMode, presets])
} }
private CreateConfirmPanel(): BaseUIElement { private static CreatePresetSelectButton(preset: PresetInfo){
const preset = this._confirmPreset.data;
if (preset === undefined) {
return undefined;
}
const confirmButton = new SubtleButton(preset.icon, const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false);
return new SubtleButton(
preset.icon,
new Combine([ new Combine([
"<b>", Translations.t.general.add.addNew.Subs({
Translations.t.general.add.confirmButton.Subs({category: preset.name}), category: preset.name
"</b>"])).SetClass("break-words"); }).SetClass("font-bold"),
confirmButton.onClick( Translations.WT(preset.description)?.FirstSentence(),
this.CreatePoint(preset.tags) tagInfo?.SetClass("subtle")
); ]).SetClass("flex flex-col")
)
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 = `<br/>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 ? "<span class='alert'>TESTING - changes won't be saved</span>" : "",
confirmButton,
this.cancelButton,
preset.description,
tagInfo
])
} }
private CreatePresetButtons() { /*
* Generates the list with all the buttons.*/
private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
const allButtons = []; const allButtons = [];
const self = this;
for (const layer of State.state.filteredLayers.data) { for (const layer of State.state.filteredLayers.data) {
if(layer.isDisplayed.data === false && State.state.featureSwitchLayers){
continue;
}
const presets = layer.layerDef.presets; const presets = layer.layerDef.presets;
for (const preset of presets) { for (const preset of presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html.SetClass("simple-add-ui-icon");
const csCount = State.state.osmConnection.userDetails.data.csCount; const tags = TagUtils.KVtoProperties(preset.tags ?? []);
let tagInfo = undefined; let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
if (csCount > Constants.userJourney.tagsVisibleAt) { .SetClass("w-12 h-12 block relative");
const presets = preset.tags.map(t => new Combine([t.asHumanString(false, true), " "]).SetClass("subtle break-words")) const presetInfo: PresetInfo = {
tagInfo = new Combine(presets) tags: preset.tags,
layerToAddTo: layer,
name: preset.title,
description: preset.description,
icon: icon
} }
const button: UIElement =
new SubtleButton( const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
icon, button.onClick(() => {
new Combine([ selectedPreset.setData(presetInfo)
"<b>", })
preset.title,
"</b>",
preset.description !== undefined ? new Combine(["<br/>", preset.description.FirstSentence()]) : "",
"<br/>",
tagInfo
])
).onClick(
() => {
self._confirmPreset.setData({
tags: preset.tags,
layerToAddTo: layer,
name: preset.title,
description: preset.description,
icon: icon
});
self.Update();
}
)
allButtons.push(button); 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)
}
} }

View file

@ -37,7 +37,7 @@ export default class DeleteImage extends UIElement {
cancelButton cancelButton
]).SetClass("flex flex-col background-black"), ]).SetClass("flex flex-col background-black"),
Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;") Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;")
) ).ToggleOnClick()
} }

View file

@ -1,44 +1,46 @@
import {InputElement} from "./InputElement"; import {InputElement} from "./InputElement";
import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import BaseUIElement from "../BaseUIElement";
export class FixedInputElement<T> extends InputElement<T> { export class FixedInputElement<T> extends InputElement<T> {
private readonly rendering: UIElement;
private readonly value: UIEventSource<T>; private readonly value: UIEventSource<T>;
public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _comparator: (t0: T, t1: T) => boolean; private readonly _comparator: (t0: T, t1: T) => boolean;
constructor(rendering: UIElement | string, private readonly _el : HTMLElement;
constructor(rendering: BaseUIElement | string,
value: T, value: T,
comparator: ((t0: T, t1: T) => boolean ) = undefined) { comparator: ((t0: T, t1: T) => boolean ) = undefined) {
super(undefined); super();
this._comparator = comparator ?? ((t0, t1) => t0 == t1); this._comparator = comparator ?? ((t0, t1) => t0 == t1);
this.value = new UIEventSource<T>(value); this.value = new UIEventSource<T>(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(() => { this.onClick(() => {
self.IsSelected.setData(true) selected.setData(true)
}) })
} }
protected InnerConstructElement(): HTMLElement {
return undefined;
}
GetValue(): UIEventSource<T> { GetValue(): UIEventSource<T> {
return this.value; return this.value;
} }
InnerRender(): string {
return this.rendering.Render();
}
IsValid(t: T): boolean { IsValid(t: T): boolean {
return this._comparator(t, this.value.data); return this._comparator(t, this.value.data);
} }
protected InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
const self = this;
htmlElement.addEventListener("mouseout", () => self.IsSelected.setData(false))
}
} }

View file

@ -3,24 +3,19 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
export class RadioButton<T> extends InputElement<T> { export class RadioButton<T> extends InputElement<T> {
private static _nextId = 0;
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _selectedElementIndex: UIEventSource<number>
= new UIEventSource<number>(null);
private readonly value: UIEventSource<T>; private readonly value: UIEventSource<T>;
private readonly _elements: InputElement<T>[] private _elements: InputElement<T>[];
private readonly _selectFirstAsDefault: boolean; private readonly _element: HTMLElement;
constructor(elements: InputElement<T>[], constructor(elements: InputElement<T>[],
selectFirstAsDefault = true) { selectFirstAsDefault = true) {
super(undefined); super()
this._elements = Utils.NoNull(elements); elements = Utils.NoNull(elements);
this._selectFirstAsDefault = selectFirstAsDefault; const selectedElementIndex: UIEventSource<number> = new UIEventSource<number>(null);
const self = this; const value =
UIEventSource.flatten(selectedElementIndex.map(
this.value =
UIEventSource.flatten(this._selectedElementIndex.map(
(selectedIndex) => { (selectedIndex) => {
if (selectedIndex !== undefined && selectedIndex !== null) { if (selectedIndex !== undefined && selectedIndex !== null) {
return elements[selectedIndex].GetValue() return elements[selectedIndex].GetValue()
@ -28,26 +23,63 @@ export class RadioButton<T> extends InputElement<T> {
} }
), elements.map(e => e?.GetValue())); ), 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++) { for (let i = 0; i < elements.length; i++) {
// If an element is clicked, the radio button corresponding with it should be selected as well // If an element is clicked, the radio button corresponding with it should be selected as well
elements[i]?.onClick(() => { elements[i]?.onClick(() => {
self._selectedElementIndex.setData(i); selectedElementIndex.setData(i);
}); });
elements[i].IsSelected.addCallback(isSelected => { elements[i].IsSelected.addCallback(isSelected => {
if (isSelected) { if (isSelected) {
self._selectedElementIndex.setData(i); selectedElementIndex.setData(i);
} }
}) })
elements[i].GetValue().addCallback(() => { 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<T> extends InputElement<T> {
} }
private IdFor(i) { protected InnerConstructElement(): HTMLElement {
return 'radio-' + this.id + '-' + i; return this._element;
}
InnerRender(): string {
let body = "";
for (let i = 0; i < this._elements.length; i++){
const el = this._elements[i];
const htmlElement =
`<label for="${this.IdFor(i)}" class="question-option-with-border">` +
`<input type="radio" id="${this.IdFor(i)}" name="radiogroup-${this.id}">` +
el.Render() +
`</label>`;
body += htmlElement;
}
return `<form id='${this.id}-form'>${body}</form>`;
} }
/*
public ShowValue(t: T): boolean { public ShowValue(t: T): boolean {
if (t === undefined) { if (t === undefined) {
return false; return false;
@ -104,48 +122,7 @@ export class RadioButton<T> extends InputElement<T> {
} }
} }
} }*/
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();
}
}
}
};
} }

View file

@ -15,9 +15,13 @@ export default class Toggle extends VariableUiElement{
isEnabled.map(isEnabled => isEnabled ? showEnabled : showDisabled) isEnabled.map(isEnabled => isEnabled ? showEnabled : showDisabled)
); );
this.isEnabled = isEnabled this.isEnabled = isEnabled
}
public ToggleOnClick(): Toggle{
const self = this;
this.onClick(() => { this.onClick(() => {
isEnabled.setData(!isEnabled.data); self. isEnabled.setData(!self.isEnabled.data);
}) })
return this;
} }
} }

View file

@ -1,4 +1,3 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import TagRenderingQuestion from "./TagRenderingQuestion"; import TagRenderingQuestion from "./TagRenderingQuestion";
@ -7,80 +6,65 @@ import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer"; import TagRenderingAnswer from "./TagRenderingAnswer";
import State from "../../State"; import State from "../../State";
import Svg from "../../Svg"; import Svg from "../../Svg";
import Toggle from "../Input/Toggle";
import BaseUIElement from "../BaseUIElement";
export default class EditableTagRendering extends UIElement { export default class EditableTagRendering extends Toggle {
private readonly _tags: UIEventSource<any>;
private readonly _configuration: TagRenderingConfig;
private _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _editButton: UIElement;
private _question: UIElement;
private _answer: UIElement;
constructor(tags: UIEventSource<any>, constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig) { configuration: TagRenderingConfig) {
super(tags);
this._tags = tags;
this._configuration = configuration;
this.ListenTo(this._editMode); const editMode = new UIEventSource<boolean>(false);
this.ListenTo(State.state?.osmConnection?.userDetails)
this._answer = new TagRenderingAnswer(tags, configuration); const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration)
this._answer.SetClass("w-full") let rendering = answer;
this._question = this.GenerateQuestion();
this.dumbMode = false;
if (this._configuration.question !== undefined) { if (configuration.question !== undefined && State.state?.featureSwitchUserbadge?.data) {
if (State.state?.featureSwitchUserbadge?.data) { // We have a question and editing is enabled
// 2.3em total width const editButton =
const self = this; 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")
this._editButton = .onClick(() => {
Svg.pencil_svg().SetClass("edit-button") editMode.setData(true);
.onClick(() => { });
self._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, const answerWithEditButton = new Combine([answer,
(State.state?.osmConnection?.userDetails?.data?.loggedIn ?? true) ? this._editButton : undefined new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)]).SetClass("w-full")
]).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();
}
private GenerateQuestion() {
const self = this;
if (this._configuration.question !== undefined) {
// And at last, set up the skip button
const cancelbutton = const cancelbutton =
Translations.t.general.cancel.Clone() Translations.t.general.cancel.Clone()
.SetClass("btn btn-secondary mr-3") .SetClass("btn btn-secondary mr-3")
.onClick(() => { .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) 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
)
} }
} }

View file

@ -11,10 +11,11 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import {Tag} from "../../Logic/Tags/Tag"; import {Tag} from "../../Logic/Tags/Tag";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import BaseUIElement from "../BaseUIElement";
export default class FeatureInfoBox extends ScrollableFullScreen { export default class FeatureInfoBox extends ScrollableFullScreen {
private constructor( public constructor(
tags: UIEventSource<any>, tags: UIEventSource<any>,
layerConfig: LayerConfig layerConfig: LayerConfig
) { ) {
@ -28,12 +29,8 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
} }
static construct(tags: UIEventSource<any>, layer: LayerConfig): FeatureInfoBox {
return new FeatureInfoBox(tags, layer)
}
private static GenerateTitleBar(tags: UIEventSource<any>, private static GenerateTitleBar(tags: UIEventSource<any>,
layerConfig: LayerConfig): UIElement { layerConfig: LayerConfig): BaseUIElement {
const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI", undefined)) 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"); .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2");
const titleIcons = new Combine( const titleIcons = new Combine(
@ -48,7 +45,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
} }
private static GenerateContent(tags: UIEventSource<any>, private static GenerateContent(tags: UIEventSource<any>,
layerConfig: LayerConfig): UIElement { layerConfig: LayerConfig): BaseUIElement {
let questionBox: UIElement = undefined; let questionBox: UIElement = undefined;
if (State.state.featureSwitchUserbadge.data) { if (State.state.featureSwitchUserbadge.data) {

View file

@ -1,17 +1,11 @@
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle"; import Toggle from "../Input/Toggle";
export class SaveButton extends UIElement { export class SaveButton extends Toggle {
private readonly _element: BaseUIElement;
constructor(value: UIEventSource<any>, osmConnection: OsmConnection) { constructor(value: UIEventSource<any>, osmConnection: OsmConnection) {
super(value);
if (value === undefined) { if (value === undefined) {
throw "No event source for savebutton, something is wrong" throw "No event source for savebutton, something is wrong"
} }
@ -31,7 +25,7 @@ export class SaveButton extends UIElement {
saveDisabled, saveDisabled,
isSaveable isSaveable
) )
this._element = new Toggle( super(
save save
, pleaseLogin, , pleaseLogin,
osmConnection?.userDetails?.map(userDetails => userDetails.loggedIn) ?? new UIEventSource<any>(false) osmConnection?.userDetails?.map(userDetails => userDetails.loggedIn) ?? new UIEventSource<any>(false)
@ -39,9 +33,4 @@ export class SaveButton extends UIElement {
} }
InnerRender(): BaseUIElement {
return this._element
}
} }

View file

@ -1,98 +1,43 @@
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import {UIElement} from "../UIElement";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import Combine from "../Base/Combine";
import {SubstitutedTranslation} from "../SubstitutedTranslation"; import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {Translation} from "../i18n/Translation";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import BaseUIElement from "../BaseUIElement"; 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 * Displays the correct value for a known tagrendering
*/ */
export default class TagRenderingAnswer extends UIElement { export default class TagRenderingAnswer extends VariableUiElement {
private readonly _tags: UIEventSource<any>;
private _configuration: TagRenderingConfig;
private _content: BaseUIElement;
private readonly _contentClass: string;
private _contentStyle: string;
constructor(tags: UIEventSource<any>, configuration: TagRenderingConfig, contentClasses: string = "", contentStyle: string = "") { constructor(tagsSource: UIEventSource<any>, configuration: TagRenderingConfig, contentClasses: string = "", contentStyle: string = "") {
super();
this._tags = tags;
this.ListenTo(tags)
this._configuration = configuration;
this._contentClass = contentClasses;
this._contentStyle = contentStyle;
if (configuration === undefined) { if (configuration === undefined) {
throw "Trying to generate a tagRenderingAnswer without configuration..." 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.SetClass("flex items-center flex-row text-lg link-underline")
this.SetStyle("word-wrap: anywhere;"); 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(["<ul>",
...valuesToRender.map(tr => new Combine(["<li>", tr, "</li>"])) ,
"</ul>"
])
}
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 "";
}
} }

View file

@ -32,23 +32,23 @@ export default class TagRenderingQuestion extends UIElement {
private readonly _tags: UIEventSource<any>; private readonly _tags: UIEventSource<any>;
private _configuration: TagRenderingConfig; private _configuration: TagRenderingConfig;
private _saveButton: UIElement; private _saveButton: BaseUIElement;
private _inputElement: InputElement<TagsFilter>; private _inputElement: InputElement<TagsFilter>;
private _cancelButton: UIElement; private _cancelButton: BaseUIElement;
private _appliedTags: BaseUIElement; private _appliedTags: BaseUIElement;
private _question: UIElement; private _question: BaseUIElement;
constructor(tags: UIEventSource<any>, constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig, configuration: TagRenderingConfig,
afterSave?: () => void, afterSave?: () => void,
cancelButton?: UIElement cancelButton?: BaseUIElement
) { ) {
super(tags); super(tags);
this._tags = tags; this._tags = tags;
this._configuration = configuration; this._configuration = configuration;
this._cancelButton = cancelButton; this._cancelButton = cancelButton;
this._question = SubstitutedTranslation.construct(this._configuration.question, tags) this._question = new SubstitutedTranslation(this._configuration.question, tags)
.SetClass("question-text"); .SetClass("question-text");
if (configuration === undefined) { if (configuration === undefined) {
throw "A question is needed for a question visualization" throw "A question is needed for a question visualization"
@ -242,7 +242,7 @@ export default class TagRenderingQuestion extends UIElement {
return undefined; return undefined;
} }
return new FixedInputElement( return new FixedInputElement(
SubstitutedTranslation.construct(mapping.then, this._tags), new SubstitutedTranslation(mapping.then, this._tags),
mapping.if, mapping.if,
(t0, t1) => t1.isEquivalent(t0)); (t0, t1) => t1.isEquivalent(t0));
} }

View file

@ -102,8 +102,8 @@ export default class ReviewForm extends InputElement<Review> {
.SetClass("review-form") .SetClass("review-form")
return new Toggle(form, Translations.t.reviews.plz_login, return new Toggle(form, Translations.t.reviews.plz_login.Clone(),
this.userDetails.map(userdetails => userdetails.loggedIn)) this.userDetails.map(userdetails => userdetails.loggedIn)).ToggleOnClick()
.ConstructElement() .ConstructElement()
} }

View file

@ -87,10 +87,10 @@ export default class ShowDataLayer {
} }
marker.openPopup(); marker.openPopup();
const popup = marker.getPopup();
const tags = State.state.allElements.getEventSourceById(selected.properties.id); const tags = State.state.allElements.getEventSourceById(selected.properties.id);
const layer: LayerConfig = this._layerDict[selected._matching_layer_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 => { infoBox.isShown.addCallback(isShown => {
if (!isShown) { if (!isShown) {
@ -98,9 +98,8 @@ export default class ShowDataLayer {
} }
}); });
popup.setContent(infoBox.Render()); infoBox.AttachTo(`popup-${selected.properties.id}`)
infoBox.Activate(); infoBox.Activate();
infoBox.Update();
}) })
} }
@ -156,11 +155,13 @@ export default class ShowDataLayer {
}, leafletLayer); }, leafletLayer);
// By setting 50vh, leaflet will attempt to fit the entire screen and move the feature down // By setting 50vh, leaflet will attempt to fit the entire screen and move the feature down
popup.setContent("<div style='height: 50vh'>Rendering</div>"); popup.setContent(`<div style='height: 50vh' id='popup-${feature.properties.id}'>Rendering</div>`);
leafletLayer.bindPopup(popup); leafletLayer.bindPopup(popup);
leafletLayer.on("popupopen", () => { leafletLayer.on("popupopen", () => {
State.state.selectedElement.setData(feature) 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); this._popups.set(feature, leafletLayer);

View file

@ -1,71 +1,37 @@
import {UIElement} from "./UIElement";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import {Translation} from "./i18n/Translation"; import {Translation} from "./i18n/Translation";
import Locale from "./i18n/Locale"; import Locale from "./i18n/Locale";
import Combine from "./Base/Combine";
import State from "../State"; import State from "../State";
import {FixedUiElement} from "./Base/FixedUiElement"; import {FixedUiElement} from "./Base/FixedUiElement";
import SpecialVisualizations from "./SpecialVisualizations"; import SpecialVisualizations from "./SpecialVisualizations";
import BaseUIElement from "./BaseUIElement"; import BaseUIElement from "./BaseUIElement";
import {Utils} from "../Utils";
import {VariableUiElement} from "./Base/VariableUIElement";
import Combine from "./Base/Combine";
export class SubstitutedTranslation extends UIElement { export class SubstitutedTranslation extends VariableUiElement {
private readonly tags: UIEventSource<any>;
private readonly translation: Translation;
private content: BaseUIElement[];
private constructor( public constructor(
translation: Translation, translation: Translation,
tags: UIEventSource<any>) { tags: UIEventSource<any>) {
super(tags); super(
this.translation = translation; tags.map(tags => {
this.tags = tags; const txt = Utils.SubstituteKeys(translation.txt, tags)
const self = this; if (txt === undefined) {
tags.addCallbackAndRun(() => { return "no tags subs tr"
self.content = self.CreateContent(); }
self.Update(); const contents = SubstitutedTranslation.EvaluateSpecialComponents(txt, tags)
}); console.log("Substr has contents", contents)
return new Combine(contents)
Locale.language.addCallback(() => { }, [Locale.language])
self.content = self.CreateContent(); )
self.Update();
});
this.SetClass("w-full") this.SetClass("w-full")
} }
public static construct(
translation: Translation,
tags: UIEventSource<any>): SubstitutedTranslation {
return new SubstitutedTranslation(translation, tags);
}
public static SubstituteKeys(txt: string, tags: any) { private static EvaluateSpecialComponents(template: string, tags: UIEventSource<any>): BaseUIElement[] {
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[] {
for (const knownSpecial of SpecialVisualizations.specialVisualizations) { for (const knownSpecial of SpecialVisualizations.specialVisualizations) {
@ -74,9 +40,9 @@ export class SubstitutedTranslation extends UIElement {
if (matched != null) { if (matched != null) {
// We found a special component that should be brought to live // 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 argument = matched[2].trim();
const partAfter = this.EvaluateSpecialComponents(matched[3]); const partAfter = SubstitutedTranslation.EvaluateSpecialComponents(matched[3], tags);
try { try {
const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
if (argument.length > 0) { 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] return [...partBefore, element, ...partAfter]
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View file

@ -21,7 +21,7 @@ export abstract class UIElement extends BaseUIElement{
if (source === undefined) { if (source === undefined) {
return this; return this;
} }
console.trace("Got a listenTo in ", this.constructor.name) //console.trace("Got a listenTo in ", this.constructor.name)
const self = this; const self = this;
source.addCallback(() => { source.addCallback(() => {
self.lastInnerRender = undefined; self.lastInnerRender = undefined;

View file

@ -32,10 +32,14 @@ export class Translation extends BaseUIElement {
} }
get txt(): string { get txt(): string {
return this.textFor(Translation.forcedLanguage ?? Locale.language.data)
}
public textFor(language: string): string{
if (this.translations["*"]) { if (this.translations["*"]) {
return this.translations["*"]; return this.translations["*"];
} }
const txt = this.translations[Translation.forcedLanguage ?? Locale.language.data]; const txt = this.translations[language];
if (txt !== undefined) { if (txt !== undefined) {
return txt; return txt;
} }
@ -52,7 +56,7 @@ export class Translation extends BaseUIElement {
console.error("Missing language ", Locale.language.data, "for", this.translations) console.error("Missing language ", Locale.language.data, "for", this.translations)
return ""; return "";
} }
InnerConstructElement(): HTMLElement { InnerConstructElement(): HTMLElement {
const el = document.createElement("span") const el = document.createElement("span")
Locale.language.addCallbackAndRun(_ => { Locale.language.addCallbackAndRun(_ => {
@ -106,12 +110,12 @@ export class Translation extends BaseUIElement {
// @ts-ignore // @ts-ignore
const date: Date = el; const date: Date = el;
rtext = date.toLocaleString(); rtext = date.toLocaleString();
} else if (el.InnerRenderAsString === undefined) { } else if (el.ConstructElement() === undefined) {
console.error("InnerREnder is not defined", el); console.error("InnerREnder is not defined", el);
throw "Hmmm, el.InnerRender is not defined?" throw "Hmmm, el.InnerRender is not defined?"
} else { } else {
Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day 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++) { for (let i = 0; i < parts.length - 1; i++) {

View file

@ -149,6 +149,16 @@ export class Utils {
return [a.substr(0, index), a.substr(index + sep.length)]; 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 // Date will be undefined on failure
public static LoadCustomCss(location: string) { public static LoadCustomCss(location: string) {
const head = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];
@ -251,6 +261,10 @@ export class Utils {
public static UnMinify(minified: string): string { public static UnMinify(minified: string): string {
if(minified === undefined || minified === null){
return undefined;
}
const parts = minified.split("|"); const parts = minified.split("|");
let result = parts.shift(); let result = parts.shift();
const keys = Utils.knownKeys.concat(Utils.extraKeys); const keys = Utils.knownKeys.concat(Utils.extraKeys);

View file

@ -68,57 +68,4 @@ input:checked + label .question-option-with-border {
width: 100%; 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;
}

View file

@ -222,23 +222,6 @@ li::marker {
max-width: 2em !important; 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 ****************/ /**************** GENERIC ****************/
@ -292,14 +275,10 @@ li::marker {
} }
.link-underline .subtle a { .link-underline .subtle a {
color: var(--foreground-color);
text-decoration: underline 1px #7193bb88; text-decoration: underline 1px #7193bb88;
color: #7193bb; color: #7193bb;
} }
.bold {
font-weight: bold;
}
.thanks { .thanks {
background-color: #43d904; background-color: #43d904;
@ -318,11 +297,6 @@ li::marker {
pointer-events: none !important; pointer-events: none !important;
} }
.page-split {
display: flex;
height: 100%;
}
/**************************************/ /**************************************/

View file

@ -54,7 +54,7 @@
"zoomInFurther": "Zoom in further to add a point.", "zoomInFurther": "Zoom in further to add a point.",
"stillLoading": "The data is still loading. Please wait a bit before you add a new point.", "stillLoading": "The data is still loading. Please wait a bit before you add a new point.",
"confirmIntro": "<h3>Add a {title} here?</h3>The point you create here will be <b>visible for everyone</b>. Please, only add things on to the map if they truly exist. A lot of applications use this data.", "confirmIntro": "<h3>Add a {title} here?</h3>The point you create here will be <b>visible for everyone</b>. Please, only add things on to the map if they truly exist. A lot of applications use this data.",
"confirmButton": "Add a {category} here.<br/><div class='alert'>Your addition is visible for everyone</div>", "warnVisibleForEveryone": "Your addition will be visible for everyone",
"openLayerControl": "Open the layer control box", "openLayerControl": "Open the layer control box",
"layerNotEnabled": "The layer {layer} is not enabled. Enable this layer to add a point" "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" "createYourOwnTheme": "Create your own MapComplete theme from scratch"
}, },
"readYourMessages": "Please, read all your OpenStreetMap-messages before adding a new point.", "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.", "fewChangesBefore": "Please, answer a few questions of existing points before adding a new point.",
"goToInbox": "Open inbox", "goToInbox": "Open inbox",
"getStartedLogin": "Login with OpenStreetMap to get started", "getStartedLogin": "Login with OpenStreetMap to get started",

View file

@ -144,8 +144,6 @@ export default class TagSpec extends T{
equal(undefined, tr.GetRenderValue({"foo": "bar"})); equal(undefined, tr.GetRenderValue({"foo": "bar"}));
equal("Has no name", tr.GetRenderValue({"noname": "yes"})?.txt); equal("Has no name", tr.GetRenderValue({"noname": "yes"})?.txt);
equal("Ook een {name}", tr.GetRenderValue({"name": "xyz"})?.txt); equal("Ook een {name}", tr.GetRenderValue({"name": "xyz"})?.txt);
equal("Ook een xyz", SubstitutedTranslation.construct(tr.GetRenderValue({"name": "xyz"}),
new UIEventSource<any>({"name": "xyz"})).InnerRenderAsString());
equal(undefined, tr.GetRenderValue({"foo": "bar"})); equal(undefined, tr.GetRenderValue({"foo": "bar"}));
})], })],
@ -196,7 +194,7 @@ export default class TagSpec extends T{
const uiEl = new EditableTagRendering(new UIEventSource<any>( const uiEl = new EditableTagRendering(new UIEventSource<any>(
{leisure: "park", "access": "no"}), constr {leisure: "park", "access": "no"}), constr
); );
const rendered = uiEl.InnerRenderAsString(); const rendered = uiEl.ConstructElement().innerHTML;
equal(true, rendered.indexOf("Niet toegankelijk") > 0) equal(true, rendered.indexOf("Niet toegankelijk") > 0)
} }

View file

@ -5,7 +5,6 @@ Utils.runningFromConsole = true;
import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion"; import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig";
import EditableTagRendering from "../UI/Popup/EditableTagRendering";
export default class TagQuestionSpec extends T { export default class TagQuestionSpec extends T {
constructor() { constructor() {