More refactoring and fixes

This commit is contained in:
pietervdvn 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) {
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(",");

View file

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

View file

@ -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,

View file

@ -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]
);

View file

@ -47,13 +47,13 @@ export default class StrayClickHandler {
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.bindPopup(popup);
self._lastMarker.on("click", () => {
uiToShow.AttachTo("strayclick")
uiToShow.Activate();
uiToShow.Update();
});
});

View file

@ -65,7 +65,14 @@ export class OsmConnection {
this.userDetails = new UIEventSource<UserDetails>(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();
}
});
}

View file

@ -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,

View file

@ -1,4 +1,3 @@
import {UIElement} from "../UIElement";
import BaseUIElement from "../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) {
return new Combine([
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
} 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())
}
})
}

View file

@ -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();

View file

@ -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)
)

View file

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

View file

@ -91,7 +91,7 @@ export default class PersonalLayersPanel extends UIElement {
"</del>"
])),
controls[layer.id] ?? (favs.indexOf(layer.id) >= 0)
);
).ToggleOnClick();
cb.SetClass("custom-layer-checkbox");
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([nocheck(), tr.fsIncludeCurrentLocation.Clone()]),
new UIEventSource<boolean>(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<boolean>(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<boolean>(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<boolean>(!swtch.reverse)
);
).ToggleOnClick();
optionCheckboxes.push(checkbox);
optionParts.push(checkbox.isEnabled.map((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
*/
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<boolean>
}
}>
= new UIEventSource(undefined);
interface PresetInfo {
description: string | Translation,
name: string | BaseUIElement,
icon: BaseUIElement,
tags: Tag[],
layerToAddTo: {
layerDef: LayerConfig,
isDisplayed: UIEventSource<boolean>
}
}
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<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.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(["<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)
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<PresetInfo>): 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([
"<b>",
Translations.t.general.add.confirmButton.Subs({category: preset.name}),
"</b>"])).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 = `<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
])
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<PresetInfo>): 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<any>(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<any>(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([
"<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();
}
)
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)
}
}

View file

@ -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()
}

View file

@ -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<T> extends InputElement<T> {
private readonly rendering: UIElement;
private readonly value: UIEventSource<T>;
public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(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<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(() => {
self.IsSelected.setData(true)
selected.setData(true)
})
}
protected InnerConstructElement(): HTMLElement {
return undefined;
}
GetValue(): UIEventSource<T> {
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))
}
}

View file

@ -3,24 +3,19 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {Utils} from "../../Utils";
export class RadioButton<T> extends InputElement<T> {
private static _nextId = 0;
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _selectedElementIndex: UIEventSource<number>
= new UIEventSource<number>(null);
private readonly value: UIEventSource<T>;
private readonly _elements: InputElement<T>[]
private readonly _selectFirstAsDefault: boolean;
private _elements: InputElement<T>[];
private readonly _element: HTMLElement;
constructor(elements: InputElement<T>[],
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<number> = new UIEventSource<number>(null);
const value =
UIEventSource.flatten(selectedElementIndex.map(
(selectedIndex) => {
if (selectedIndex !== undefined && selectedIndex !== null) {
return elements[selectedIndex].GetValue()
@ -28,26 +23,63 @@ export class RadioButton<T> extends InputElement<T> {
}
), 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<T> extends InputElement<T> {
}
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 =
`<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>`;
protected InnerConstructElement(): HTMLElement {
return this._element;
}
/*
public ShowValue(t: T): boolean {
if (t === undefined) {
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)
);
this.isEnabled = isEnabled
}
public ToggleOnClick(): Toggle{
const self = this;
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 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<any>;
private readonly _configuration: TagRenderingConfig;
private _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _editButton: UIElement;
private _question: UIElement;
private _answer: UIElement;
export default class EditableTagRendering extends Toggle {
constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig) {
super(tags);
this._tags = tags;
this._configuration = configuration;
this.ListenTo(this._editMode);
this.ListenTo(State.state?.osmConnection?.userDetails)
const editMode = new UIEventSource<boolean>(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
)
}
}

View file

@ -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<any>,
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>,
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<any>,
layerConfig: LayerConfig): UIElement {
layerConfig: LayerConfig): BaseUIElement {
let questionBox: UIElement = undefined;
if (State.state.featureSwitchUserbadge.data) {

View file

@ -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<any>, 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<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 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<any>;
private _configuration: TagRenderingConfig;
private _content: BaseUIElement;
private readonly _contentClass: string;
private _contentStyle: string;
export default class TagRenderingAnswer extends VariableUiElement {
constructor(tags: UIEventSource<any>, 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<any>, 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(["<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 _configuration: TagRenderingConfig;
private _saveButton: UIElement;
private _saveButton: BaseUIElement;
private _inputElement: InputElement<TagsFilter>;
private _cancelButton: UIElement;
private _cancelButton: BaseUIElement;
private _appliedTags: BaseUIElement;
private _question: UIElement;
private _question: BaseUIElement;
constructor(tags: UIEventSource<any>,
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));
}

View file

@ -102,8 +102,8 @@ export default class ReviewForm extends InputElement<Review> {
.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()
}

View file

@ -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("<div style='height: 50vh'>Rendering</div>");
popup.setContent(`<div style='height: 50vh' id='popup-${feature.properties.id}'>Rendering</div>`);
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);

View file

@ -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<any>;
private readonly translation: Translation;
private content: BaseUIElement[];
export class SubstitutedTranslation extends VariableUiElement {
private constructor(
public constructor(
translation: Translation,
tags: UIEventSource<any>) {
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<any>): 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<any>): 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);

View file

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

View file

@ -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++) {

View file

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

View file

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

View file

@ -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%;
}
/**************************************/

View file

@ -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": "<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",
"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",

View file

@ -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<any>({"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<any>(
{leisure: "park", "access": "no"}), constr
);
const rendered = uiEl.InnerRenderAsString();
const rendered = uiEl.ConstructElement().innerHTML;
equal(true, rendered.indexOf("Niet toegankelijk") > 0)
}

View file

@ -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() {