Way to much fixes and improvements

This commit is contained in:
Pieter Vander Vennet 2020-09-02 11:37:34 +02:00
parent e68d9d99a5
commit 5ed0bb431c
41 changed files with 1244 additions and 402 deletions

View file

@ -0,0 +1,48 @@
import {UIElement} from "../UI/UIElement";
import {SubtleButton} from "../UI/Base/SubtleButton";
import {VariableUiElement} from "../UI/Base/VariableUIElement";
import SingleSetting from "../UI/CustomGenerator/SingleSetting";
import Combine from "../UI/Base/Combine";
import {UIEventSource} from "../Logic/UIEventSource";
export default class HelpText extends UIElement {
private helpText: UIElement;
private returnButton: UIElement;
constructor(currentSetting: UIEventSource<SingleSetting<any>>) {
super();
this.returnButton = new SubtleButton("./assets/close.svg",
new VariableUiElement(
currentSetting.map(currentSetting => {
if (currentSetting === undefined) {
return "";
}
return "Return to general help";
}
)
))
.ListenTo(currentSetting)
.onClick(() => currentSetting.setData(undefined));
this.helpText = new VariableUiElement(currentSetting.map((setting: SingleSetting<any>) => {
if (setting === undefined) {
return "<h1>Welcome to the Custom Theme Builder</h1>" +
"Here, one can make their own custom mapcomplete themes.<br/>" +
"Fill out the fields to get a working mapcomplete theme. More information on the selected field will appear here when you click it";
}
return new Combine(["<h1>", setting._name, "</h1>", setting._description.Render()]).Render();
}))
}
InnerRender(): string {
return new Combine([this.helpText,
this.returnButton,
]).Render();
}
}

View file

@ -1,7 +1,7 @@
import {Layout} from "../Layout"; import {Layout} from "../Layout";
import {LayoutConfigJson} from "./LayoutConfigJson"; import {LayoutConfigJson} from "./LayoutConfigJson";
import {AndOrTagConfigJson} from "./TagConfigJson"; import {AndOrTagConfigJson} from "./TagConfigJson";
import {And, RegexTag, Tag, TagsFilter} from "../../Logic/Tags"; import {And, Or, RegexTag, Tag, TagsFilter} from "../../Logic/Tags";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {TagRenderingOptions} from "../TagRenderingOptions"; import {TagRenderingOptions} from "../TagRenderingOptions";
import Translation from "../../UI/i18n/Translation"; import Translation from "../../UI/i18n/Translation";
@ -81,9 +81,14 @@ export class FromJSON {
return json; return json;
} }
const tr = {}; const tr = {};
let keyCount = 0;
for (let key in json) { for (let key in json) {
keyCount ++;
tr[key] = json[key]; // I'm doing this wrong, I know tr[key] = json[key]; // I'm doing this wrong, I know
} }
if(keyCount == 0){
return undefined;
}
return new Translation(tr); return new Translation(tr);
} }
@ -133,10 +138,10 @@ export class FromJSON {
let template = FromJSON.Translation(json.render); let template = FromJSON.Translation(json.render);
let freeform = undefined; let freeform = undefined;
if (json.freeform) { if (json.freeform?.key) {
// Setup the freeform
if(json.render === undefined){ if (template === undefined) {
console.error("Freeform is defined, but render is not. This is not allowed.", json) console.error("Freeform.key is defined, but render is not. This is not allowed.", json)
throw "Freeform is defined, but render is not. This is not allowed." throw "Freeform is defined, but render is not. This is not allowed."
} }
@ -146,13 +151,14 @@ export class FromJSON {
key: json.freeform.key key: json.freeform.key
}; };
if (json.freeform.addExtraTags) { if (json.freeform.addExtraTags) {
freeform["extraTags"] = FromJSON.Tag(json.freeform.addExtraTags); freeform.extraTags = new And(json.freeform.addExtraTags.map(FromJSON.SimpleTag))
} }
} else if (json.render) { } else if (json.render) {
// Template (aka rendering) is defined, but freeform.key is not. We allow an input as string
freeform = { freeform = {
template: `$string$`, template: undefined, // Template to ask is undefined -> we block asking for this key
renderTemplate: template, renderTemplate: template,
key: "id" key: "id" // every object always has an id
} }
} }
@ -164,6 +170,10 @@ export class FromJSON {
}) })
); );
if(template === undefined && (mappings === undefined || mappings.length === 0)){
throw "Empty tagrendering detected: no mappings nor template given"
}
let rendering = new TagRenderingOptions({ let rendering = new TagRenderingOptions({
question: FromJSON.Translation(json.question), question: FromJSON.Translation(json.question),
@ -185,6 +195,9 @@ export class FromJSON {
} }
public static Tag(json: AndOrTagConfigJson | string): TagsFilter { public static Tag(json: AndOrTagConfigJson | string): TagsFilter {
if(json === undefined){
throw "Error while parsing a tag: nothing defined. Make sure all the tags are defined and at least one tag is present in a complex expression"
}
if (typeof (json) == "string") { if (typeof (json) == "string") {
const tag = json as string; const tag = json as string;
if (tag.indexOf("!~") >= 0) { if (tag.indexOf("!~") >= 0) {
@ -227,7 +240,7 @@ export class FromJSON {
return new And(json.and.map(FromJSON.Tag)); return new And(json.and.map(FromJSON.Tag));
} }
if (json.or !== undefined) { if (json.or !== undefined) {
return new And(json.or.map(FromJSON.Tag)); return new Or(json.or.map(FromJSON.Tag));
} }
} }
@ -270,7 +283,8 @@ export class FromJSON {
}) ?? []; }) ?? [];
function style(tags) { function style(tags) {
const iconSizeStr = iconSize.GetContent(tags).txt.split(","); const iconSizeStr =
iconSize.GetContent(tags).txt.split(",");
const iconwidth = Number(iconSizeStr[0]); const iconwidth = Number(iconSizeStr[0]);
const iconheight = Number(iconSizeStr[1]); const iconheight = Number(iconSizeStr[1]);
const iconmode = iconSizeStr[2]; const iconmode = iconSizeStr[2];

View file

@ -66,7 +66,7 @@ export interface LayerConfigJson {
* Wayhandling: should a way/area be displayed as: * Wayhandling: should a way/area be displayed as:
* 0) The way itself * 0) The way itself
* 1) The centerpoint and the way * 1) The centerpoint and the way
* 2) Only the centerpoint? * 2) Only the centerpoint
*/ */
wayHandling?: number; wayHandling?: number;

View file

@ -1,8 +1,5 @@
export interface AndOrTagConfigJson { export interface AndOrTagConfigJson {
and?: (string | AndOrTagConfigJson)[] and?: (string | AndOrTagConfigJson)[]
or?: (string | AndOrTagConfigJson)[] or?: (string | AndOrTagConfigJson)[]
} }

View file

@ -37,7 +37,7 @@ export interface TagRenderingConfigJson {
* If a value is added with the textfield, these extra tag is addded. * If a value is added with the textfield, these extra tag is addded.
* Usefull to add a 'fixme=freeform textfield used - to be checked' * Usefull to add a 'fixme=freeform textfield used - to be checked'
**/ **/
addExtraTags?: AndOrTagConfigJson | string; addExtraTags?: string[];
} }
/** /**

View file

@ -127,12 +127,13 @@ TagRendering extends UIElement implements TagDependantUIElement {
// Prepare the actual input element -> pick an appropriate implementation // Prepare the actual input element -> pick an appropriate implementation
this._questionElement = this.InputElementFor(options); this._questionElement = this.InputElementFor(options) ??
new FixedInputElement<TagsFilter>("<span class='alert'>No input possible</span>", new Tag("a","b"));
const save = () => { const save = () => {
const selection = self._questionElement.GetValue().data; const selection = self._questionElement.GetValue().data;
console.log("Tagrendering: saving tags ", selection); console.log("Tagrendering: saving tags ", selection);
if (selection) { if (selection) {
State.state.changes.addTag(tags.data.id, selection); State.state?.changes?.addTag(tags.data.id, selection);
} }
self._editMode.setData(false); self._editMode.setData(false);
} }
@ -143,7 +144,7 @@ TagRendering extends UIElement implements TagDependantUIElement {
if (tags === undefined) { if (tags === undefined) {
return Translations.t.general.noTagsSelected.SetClass("subtle").Render(); return Translations.t.general.noTagsSelected.SetClass("subtle").Render();
} }
const csCount = State.state.osmConnection.userDetails.data.csCount; const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000;
if (csCount < State.userJourney.tagsVisibleAt) { if (csCount < State.userJourney.tagsVisibleAt) {
return ""; return "";
} }
@ -154,7 +155,7 @@ TagRendering extends UIElement implements TagDependantUIElement {
return tags.asHumanString(true, true); return tags.asHumanString(true, true);
} }
) )
); ).ListenTo(self._questionElement);
const cancel = () => { const cancel = () => {
self._questionSkipped.setData(true); self._questionSkipped.setData(true);
@ -246,7 +247,7 @@ TagRendering extends UIElement implements TagDependantUIElement {
private InputForFreeForm(freeform): InputElement<TagsFilter> { private InputForFreeForm(freeform): InputElement<TagsFilter> {
if (freeform === undefined) { if (freeform?.template === undefined) {
return undefined; return undefined;
} }
@ -269,8 +270,14 @@ TagRendering extends UIElement implements TagDependantUIElement {
if (!isValid(string, this._source.data._country)) { if (!isValid(string, this._source.data._country)) {
return undefined; return undefined;
} }
const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); const tag = new Tag(freeform.key, formatter(string, this._source.data._country));
if (tag.value.length > 255) {
return undefined; // Toolong
}
if (freeform.extraTags === undefined) { if (freeform.extraTags === undefined) {
return tag; return tag;
} }
@ -340,7 +347,8 @@ TagRendering extends UIElement implements TagDependantUIElement {
if (this.IsKnown()) { if (this.IsKnown()) {
return false; return false;
} }
if (this._question === undefined) { if (this._question === undefined ||
(this._freeform?.template === undefined && (this._mapping?.length ?? 0) == 0)) {
// We don't ask this question in the first place // We don't ask this question in the first place
return false; return false;
} }
@ -390,15 +398,20 @@ TagRendering extends UIElement implements TagDependantUIElement {
InnerRender(): string { InnerRender(): string {
if (this.IsQuestioning() && !State.state?.osmConnection?.userDetails?.data?.loggedIn) { if (this.IsQuestioning()
&& (State.state !== undefined) // If State.state is undefined, we are testing/custom theme building -> show regular save
&& !State.state.osmConnection.userDetails.data.loggedIn) {
const question = const question =
this.ApplyTemplate(this._question).SetClass('question-text'); this.ApplyTemplate(this._question).SetClass('question-text');
return "<div class='question'>" + return "<div class='question'>" +
new Combine([ new Combine([
question, question.Render(),
"<br/>", "<br/>",
this._questionElement.Render(), this._questionElement.Render(),
"<span class='login-button-friendly'>" + this._friendlyLogin.Render() + "</span>", "<span class='login-button-friendly'>",
this._friendlyLogin,
"</span>",
]).Render() + "</div>"; ]).Render() + "</div>";
} }
@ -428,7 +441,8 @@ TagRendering extends UIElement implements TagDependantUIElement {
let editButton = ""; let editButton = "";
if (State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) { if (State.state === undefined || // state undefined -> we are custom testing
State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) {
editButton = this._editButton.Render(); editButton = this._editButton.Render();
} }
@ -438,6 +452,8 @@ TagRendering extends UIElement implements TagDependantUIElement {
"</span>"; "</span>";
} }
console.log("No rendering for",this)
return ""; return "";
} }

View file

@ -184,16 +184,18 @@ export class FilteredLayer {
idsFromOverpass.add(feature.properties.id); idsFromOverpass.add(feature.properties.id);
fusedFeatures.push(feature); fusedFeatures.push(feature);
} }
this._dataFromOverpass = fusedFeatures;
console.log("New elements are ", this._newElements)
for (const feature of this._newElements) { for (const feature of this._newElements) {
if (idsFromOverpass.has(feature.properties.id)) { if (!idsFromOverpass.has(feature.properties.id)) {
// This element is not yet uploaded or not yet visible in overpass // This element is not yet uploaded or not yet visible in overpass
// We include it in the layer // We include it in the layer
fusedFeatures.push(feature); fusedFeatures.push(feature);
console.log("Adding ", feature," to fusedFeatures")
} }
} }
this._dataFromOverpass = fusedFeatures;
// We use a new, fused dataset // We use a new, fused dataset
data = { data = {

View file

@ -4,6 +4,7 @@ import {FilteredLayer} from "./FilteredLayer";
import {Bounds} from "./Bounds"; import {Bounds} from "./Bounds";
import {Overpass} from "./Osm/Overpass"; import {Overpass} from "./Osm/Overpass";
import {State} from "../State"; import {State} from "../State";
import {LayerDefinition} from "../Customizations/LayerDefinition";
export class LayerUpdater { export class LayerUpdater {
@ -27,7 +28,7 @@ export class LayerUpdater {
const self = this; const self = this;
this.sufficentlyZoomed = State.state.locationControl.map(location => { this.sufficentlyZoomed = State.state.locationControl.map(location => {
let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => (layer as LayerDefinition).minzoom ?? 18));
return location.zoom >= minzoom; return location.zoom >= minzoom;
}, [state.layoutToUse] }, [state.layoutToUse]
); );
@ -49,6 +50,9 @@ export class LayerUpdater {
const filters: TagsFilter[] = []; const filters: TagsFilter[] = [];
state = state ?? State.state; state = state ?? State.state;
for (const layer of state.layoutToUse.data.layers) { for (const layer of state.layoutToUse.data.layers) {
if(typeof(layer) === "string"){
continue;
}
if (state.locationControl.data.zoom < layer.minzoom) { if (state.locationControl.data.zoom < layer.minzoom) {
console.log("Not loading layer ", layer.id, " as it needs at least ", layer.minzoom, "zoom") console.log("Not loading layer ", layer.id, " as it needs at least ", layer.minzoom, "zoom")
continue; continue;

View file

@ -36,9 +36,8 @@ export class UIEventSource<T>{
}); });
for (const possibleSource of possibleSources) { for (const possibleSource of possibleSources) {
possibleSource.addCallback(() => { possibleSource?.addCallback(() => {
sink.setData(source.data?.data); sink.setData(source.data?.data);
}) })
} }
@ -87,4 +86,24 @@ export class UIEventSource<T>{
return this; return this;
} }
public stabilized(millisToStabilize) : UIEventSource<T>{
const newSource = new UIEventSource<T>(this.data);
let currentCallback = 0;
this.addCallback(latestData => {
currentCallback++;
const thisCallback = currentCallback;
window.setTimeout(() => {
if(thisCallback === currentCallback){
newSource.setData(latestData);
}
}, millisToStabilize)
});
return newSource;
}
} }

View file

@ -2,14 +2,17 @@ import {UIElement} from "../UIElement";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
export default class Combine extends UIElement { export default class Combine extends UIElement {
private uiElements: (string | UIElement)[]; private readonly uiElements: (string | UIElement)[];
private className: string = undefined; private readonly className: string = undefined;
private clas: string = undefined;
constructor(uiElements: (string | UIElement)[], className: string = undefined) { constructor(uiElements: (string | UIElement)[], className: string = undefined) {
super(undefined); super(undefined);
this.dumbMode = false;
this.className = className; this.className = className;
this.uiElements = uiElements; this.uiElements = uiElements;
if (className) {
console.error("Deprecated used of className")
}
} }
InnerRender(): string { InnerRender(): string {

20
UI/Base/PageSplit.ts Normal file
View file

@ -0,0 +1,20 @@
import {UIElement} from "../UIElement";
export default class PageSplit extends UIElement{
private _left: UIElement;
private _right: UIElement;
private _leftPercentage: number;
constructor(left: UIElement, right:UIElement,
leftPercentage: number = 50) {
super();
this._left = left;
this._right = right;
this._leftPercentage = leftPercentage;
}
InnerRender(): string {
return `<span class="page-split" style="height: min-content"><span style="width:${this._leftPercentage}%">${this._left.Render()}</span><span style="width:${100-this._leftPercentage}">${this._right.Render()}</span></span>`;
}
}

View file

@ -4,9 +4,9 @@ import Combine from "./Combine";
export class SubtleButton extends UIElement{ export class SubtleButton extends UIElement{
private imageUrl: string; private readonly imageUrl: string;
private message: UIElement; private readonly message: UIElement;
private linkTo: { url: string, newTab?: boolean } = undefined; private readonly linkTo: { url: string, newTab?: boolean } = undefined;
constructor(imageUrl: string, message: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined) { constructor(imageUrl: string, message: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined) {
super(undefined); super(undefined);
@ -18,7 +18,7 @@ export class SubtleButton extends UIElement{
InnerRender(): string { InnerRender(): string {
if(this.message.IsEmpty()){ if(this.message !== null && this.message.IsEmpty()){
return ""; return "";
} }
@ -26,7 +26,7 @@ export class SubtleButton extends UIElement{
return new Combine([ return new Combine([
`<a class="subtle-button" href="${this.linkTo.url}" ${this.linkTo.newTab ? 'target="_blank"' : ""}>`, `<a class="subtle-button" href="${this.linkTo.url}" ${this.linkTo.newTab ? 'target="_blank"' : ""}>`,
this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "", this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
this.message, this.message ?? "",
'</a>' '</a>'
]).Render(); ]).Render();
} }
@ -34,7 +34,7 @@ export class SubtleButton extends UIElement{
return new Combine([ return new Combine([
'<span class="subtle-button">', '<span class="subtle-button">',
this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "", this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
this.message, this.message ?? "",
'</span>' '</span>'
]).Render(); ]).Render();
} }

View file

@ -7,8 +7,8 @@ export class TabbedComponent extends UIElement {
private headers: UIElement[] = []; private headers: UIElement[] = [];
private content: UIElement[] = []; private content: UIElement[] = [];
constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab : UIEventSource<number> = new UIEventSource<number>(0)) { constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) {
super(openedTab); super(typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0)));
const self = this; const self = this;
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
let element = elements[i]; let element = elements[i];

View file

@ -3,34 +3,30 @@ import {TabbedComponent} from "../Base/TabbedComponent";
import {SubtleButton} from "../Base/SubtleButton"; import {SubtleButton} from "../Base/SubtleButton";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
import LayerPanel from "./LayerPanel"; import LayerPanel from "./LayerPanel";
import SingleSetting from "./SingleSetting"; import SingleSetting from "./SingleSetting";
import Combine from "../Base/Combine";
import {GenerateEmpty} from "./GenerateEmpty";
import PageSplit from "../Base/PageSplit";
import {VariableUiElement} from "../Base/VariableUIElement";
import HelpText from "../../Customizations/HelpText";
import {MultiTagInput} from "../Input/MultiTagInput";
import {FromJSON} from "../../Customizations/JSON/FromJSON";
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
import {FixedUiElement} from "../Base/FixedUiElement";
import TagRenderingPanel from "./TagRenderingPanel";
export default class AllLayersPanel extends UIElement { export default class AllLayersPanel extends UIElement {
private panel: UIElement; private panel: UIElement;
private _config: UIEventSource<LayoutConfigJson>; private readonly _config: UIEventSource<LayoutConfigJson>;
private _currentlySelected: UIEventSource<SingleSetting<any>>; private readonly languages: UIEventSource<string[]>;
private languages: UIEventSource<string[]>;
private static createEmptyLayer(): LayerConfigJson { constructor(config: UIEventSource<LayoutConfigJson>,
return {
id: undefined,
name: undefined,
minzoom: 0,
overpassTags: undefined,
title: undefined,
description: {}
}
}
constructor(config: UIEventSource<LayoutConfigJson>, currentlySelected: UIEventSource<SingleSetting<any>>,
languages: UIEventSource<any>) { languages: UIEventSource<any>) {
super(undefined); super(undefined);
this._config = config; this._config = config;
this._currentlySelected = currentlySelected;
this.languages = languages; this.languages = languages;
this.createPanels(); this.createPanels();
@ -46,23 +42,85 @@ export default class AllLayersPanel extends UIElement {
const layers = this._config.data.layers; const layers = this._config.data.layers;
for (let i = 0; i < layers.length; i++) { for (let i = 0; i < layers.length; i++) {
const currentlySelected = new UIEventSource<(SingleSetting<any>)>(undefined);
const layer = new LayerPanel(this._config, this.languages, i, currentlySelected);
const helpText = new HelpText(currentlySelected);
const previewTagInput = new MultiTagInput();
previewTagInput.GetValue().setData(["id=123456"]);
const previewTagValue = previewTagInput.GetValue().map(tags => {
const properties = {};
for (const str of tags) {
const tag = FromJSON.SimpleTag(str);
if (tag !== undefined) {
properties[tag.key] = tag.value;
}
}
return properties;
});
const preview = new VariableUiElement(layer.selectedTagRendering.map(
(tagRenderingPanel: TagRenderingPanel) => {
if (tagRenderingPanel === undefined) {
return "No tag rendering selected at the moment";
}
let es = tagRenderingPanel.GetValue();
let tagRenderingConfig: TagRenderingConfigJson = es.data;
let rendering: UIElement;
try {
rendering = FromJSON.TagRendering(tagRenderingConfig)
.construct({tags: previewTagValue})
} catch (e) {
console.error("User defined tag rendering incorrect:", e);
rendering = new FixedUiElement(e).SetClass("alert");
}
return new Combine([
"<h3>",
tagRenderingPanel.options.title ?? "Extra tag rendering",
"</h3>",
tagRenderingPanel.options.description ?? "This tag rendering will appear in the popup",
"<br/>",
rendering]).Render();
},
[this._config]
)).ListenTo(layer.selectedTagRendering);
tabs.push({ tabs.push({
header: "<img src='./assets/bug.svg'>", header: "<img src='./assets/bug.svg'>",
content: new LayerPanel(this._config, this.languages, i, this._currentlySelected) content:
new PageSplit(
layer.SetClass("scrollable"),
new Combine([
helpText,
"</br>",
"<h2>Testing tags</h2>",
previewTagInput,
"<h2>Tag Rendering preview</h2>",
preview
]), 60
)
}); });
} }
tabs.push({ tabs.push({
header: "<img src='./assets/add.svg'>", header: "<img src='./assets/add.svg'>",
content: new SubtleButton( content: new Combine([
"<h2>Layer editor</h2>",
"In this tab page, you can add and edit the layers of the theme. Click the layers above or add a new layer to get started.",
new SubtleButton(
"./assets/add.svg", "./assets/add.svg",
"Add a new layer" "Add a new layer"
).onClick(() => { ).onClick(() => {
self._config.data.layers.push(AllLayersPanel.createEmptyLayer()) self._config.data.layers.push(GenerateEmpty.createEmptyLayer())
self._config.ping(); self._config.ping();
}) })])
}) })
this.panel = new TabbedComponent(tabs, new UIEventSource<number>(Math.max(0, layers.length-1))); this.panel = new TabbedComponent(tabs, new UIEventSource<number>(Math.max(0, layers.length - 1)));
this.Update(); this.Update();
} }

View file

@ -0,0 +1,67 @@
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
export class GenerateEmpty {
public static createEmptyLayer(): LayerConfigJson {
return {
id: undefined,
name: undefined,
minzoom: 0,
overpassTags: {and: [""]},
title: undefined,
description: {},
}
}
public static createEmptyLayout(): LayoutConfigJson {
return {
id: "",
title: {},
description: {},
language: [],
maintainer: "",
icon: "./assets/bug.svg",
version: "0",
startLat: 0,
startLon: 0,
startZoom: 1,
socialImage: "",
layers: []
}
}
public static createTestLayout(): LayoutConfigJson {
return {
id: "test",
title: {"en": "Test layout"},
description: {"en": "A layout for testing"},
language: ["en"],
maintainer: "Pieter Vander Vennet",
icon: "./assets/bug.svg",
version: "0",
startLat: 0,
startLon: 0,
startZoom: 1,
widenFactor: 0.05,
socialImage: "",
layers: [{
id: "testlayer",
name: "Testing layer",
minzoom: 15,
overpassTags: {and: ["highway=residential"]},
title: "Some Title",
description: {"en": "Some Description"},
icon: {render: {en: "./assets/pencil.svg"}},
width: {render: {en: "5"}},
tagRenderings: [{
render: {"en":"Test Rendering"}
}]
}]
}
}
public static createEmptyTagRendering(): TagRenderingConfigJson {
return {};
}
}

View file

@ -9,24 +9,37 @@ import {TextField} from "../Input/TextField";
import {InputElement} from "../Input/InputElement"; import {InputElement} from "../Input/InputElement";
import MultiLingualTextFields from "../Input/MultiLingualTextFields"; import MultiLingualTextFields from "../Input/MultiLingualTextFields";
import {CheckBox} from "../Input/CheckBox"; import {CheckBox} from "../Input/CheckBox";
import {MultiTagInput} from "../Input/MultiTagInput"; import {AndOrTagInput} from "../Input/AndOrTagInput";
import TagRenderingPanel from "./TagRenderingPanel";
import {GenerateEmpty} from "./GenerateEmpty";
import {DropDown} from "../Input/DropDown";
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
import {MultiInput} from "../Input/MultiInput";
import {Tag} from "../../Logic/Tags";
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
/** /**
* Shows the configuration for a single layer * Shows the configuration for a single layer
*/ */
export default class LayerPanel extends UIElement { export default class LayerPanel extends UIElement {
private _config: UIEventSource<LayoutConfigJson>; private readonly _config: UIEventSource<LayoutConfigJson>;
private settingsTable: UIElement; private readonly settingsTable: UIElement;
private readonly renderingOptions: UIElement;
private deleteButton: UIElement; private readonly deleteButton: UIElement;
public readonly selectedTagRendering: UIEventSource<TagRenderingPanel>
= new UIEventSource<TagRenderingPanel>(undefined);
private tagRenderings: UIElement;
constructor(config: UIEventSource<LayoutConfigJson>, constructor(config: UIEventSource<LayoutConfigJson>,
languages: UIEventSource<string[]>, languages: UIEventSource<string[]>,
index: number, index: number,
currentlySelected: UIEventSource<SingleSetting<any>>) { currentlySelected: UIEventSource<SingleSetting<any>>) {
super(undefined); super();
this._config = config; this._config = config;
this.renderingOptions = this.setupRenderOptions(config, languages, index, currentlySelected);
const actualDeleteButton = new SubtleButton( const actualDeleteButton = new SubtleButton(
"./assets/delete.svg", "./assets/delete.svg",
@ -70,17 +83,120 @@ export default class LayerPanel extends UIElement {
setting(TextField.StringInput(), "id", "Id", "An identifier for this layer<br/>This should be a simple, lowercase, human readable string that is used to identify the layer."), setting(TextField.StringInput(), "id", "Id", "An identifier for this layer<br/>This should be a simple, lowercase, human readable string that is used to identify the layer."),
setting(new MultiLingualTextFields(languages), "title", "Title", "The human-readable name of this layer<br/>Used in the layer control panel and the 'Personal theme'"), setting(new MultiLingualTextFields(languages), "title", "Title", "The human-readable name of this layer<br/>Used in the layer control panel and the 'Personal theme'"),
setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.<br/>Shown in the layer selections and in the personal theme"), setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.<br/>Shown in the layer selections and in the personal theme"),
setting(new MultiTagInput(), "overpassTags","Overpass query", setting(TextField.NumberInput("nat", n => n < 23), "minzoom", "Minimal zoom",
new Combine(["The tags to load from overpass. ", MultiTagInput.tagExplanation])) "The minimum zoomlevel needed to load and show this layer."),
setting(new DropDown("", [
{value: 0, shown: "Show ways and areas as ways and lines"},
{value: 1, shown: "Show both the ways/areas and the centerpoints"},
{value: 2, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling",
"Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"),
setting(new AndOrTagInput(), "overpassTags", "Overpass query",
"The tags of the objects to load from overpass"),
], ],
currentlySelected currentlySelected);
const self = this;
const tagRenderings = new MultiInput<TagRenderingConfigJson>("Add a tag rendering/question",
() => ({}),
() => {
const tagPanel = new TagRenderingPanel(languages, currentlySelected)
self.registerTagRendering(tagPanel);
return tagPanel;
});
tagRenderings.GetValue().addCallback(
tagRenderings => {
(config.data.layers[index] as LayerConfigJson).tagRenderings = tagRenderings;
config.ping();
}
) )
;
function loadTagRenderings() {
const values = (config.data.layers[index] as LayerConfigJson).tagRenderings;
const renderings: TagRenderingConfigJson[] = [];
for (const value of values) {
if (typeof (value) !== "string") {
renderings.push(value);
}
}
tagRenderings.GetValue().setData(renderings);
}
loadTagRenderings();
this.tagRenderings = tagRenderings;
}
private setupRenderOptions(config: UIEventSource<LayoutConfigJson>,
languages: UIEventSource<string[]>,
index: number,
currentlySelected: UIEventSource<SingleSetting<any>>): UIElement {
const iconSelect = new TagRenderingPanel(
languages, currentlySelected,
{
title: "Icon",
description: "A visual representation for this layer and for the points on the map.",
disableQuestions: true
});
const size = new TagRenderingPanel(languages, currentlySelected,
{
title: "Icon Size",
description: "The size of the icons on the map in pixels. Can vary based on the tagging",
disableQuestions: true
});
const color = new TagRenderingPanel(languages, currentlySelected,
{
title: "Way and area color",
description: "The color or a shown way or area. Can vary based on the tagging",
disableQuestions: true
});
const stroke = new TagRenderingPanel(languages, currentlySelected,
{
title: "Stroke width",
description: "The width of lines representing ways and the outline of areas. Can vary based on the tags",
disableQuestions: true
});
this.registerTagRendering(iconSelect);
this.registerTagRendering(size);
this.registerTagRendering(color);
this.registerTagRendering(stroke);
function setting(input: InputElement<any>, path, isIcon: boolean = false): SingleSetting<TagRenderingConfigJson> {
return new SingleSetting(config, input, ["layers", index, path], undefined, undefined)
}
return new SettingsTable([
setting(iconSelect, "icon"),
setting(size, "size"),
setting(color, "color"),
setting(stroke, "stroke")
], currentlySelected);
}
private registerTagRendering(
tagRenderingPanel: TagRenderingPanel) {
tagRenderingPanel.IsHovered().addCallback(isHovering => {
if (!isHovering) {
return;
}
this.selectedTagRendering.setData(tagRenderingPanel);
})
} }
InnerRender(): string { InnerRender(): string {
return new Combine([ return new Combine([
"<h2>General layer settings</h2>",
this.settingsTable, this.settingsTable,
"<h2>Map rendering options</h2>",
this.renderingOptions,
"<h2>Tag rendering and questions</h2>",
this.tagRenderings,
"<h2>Layer delete</h2>",
this.deleteButton this.deleteButton
]).Render(); ]).Render();
} }

View file

@ -0,0 +1,64 @@
import {InputElement} from "../Input/InputElement";
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import SettingsTable from "./SettingsTable";
import SingleSetting from "./SingleSetting";
import {AndOrTagInput} from "../Input/AndOrTagInput";
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
import {DropDown} from "../Input/DropDown";
export default class MappingInput extends InputElement<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }> {
private readonly _value: UIEventSource<{ if: AndOrTagConfigJson; then: any; hideInAnswer?: boolean }>;
private readonly _panel: UIElement;
constructor(languages: UIEventSource<any>, disableQuestions: boolean = false) {
super();
const currentSelected = new UIEventSource<SingleSetting<any>>(undefined);
this._value = new UIEventSource<{ if: AndOrTagConfigJson, then: any, hideInAnswer?: boolean }>({
if: undefined,
then: undefined
});
const self = this;
function setting(inputElement: InputElement<any>, path: string, name: string, description: string | UIElement) {
return new SingleSetting(self._value, inputElement, path, name, description);
}
const withQuestions = [setting(new DropDown("",
[{value: false, shown: "Can be used as answer"}, {value: true, shown: "Not an answer option"}]),
"hideInAnswer", "Answer option",
"Sometimes, multiple tags for the same meaning are used (e.g. <span class='literal-code'>access=yes</span> and <span class='literal-code'>access=public</span>)." +
"Use this toggle to disable an anwer. Alternatively an implied/assumed rendering can be used. In order to do this:" +
"use a single tag in the 'if' with <i>no</i> value defined, e.g. <span class='literal-code'>indoor=</span>. The mapping will then be shown as default until explicitly changed"
)];
this._panel = new SettingsTable([
setting(new AndOrTagInput(), "if", "If matches", "If this condition matches, the template <b>then</b> below will be used"),
setting(new MultiLingualTextFields(languages),
"then", "Then show", "If the condition above matches, this template <b>then</b> below will be shown to the user."),
...(disableQuestions ? [] : withQuestions)
], currentSelected).SetClass("bordered tag-mapping");
}
InnerRender(): string {
return this._panel.Render();
}
GetValue(): UIEventSource<{ if: AndOrTagConfigJson; then: any; hideInAnswer?: boolean }> {
return this._value;
}
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
IsValid(t: { if: AndOrTagConfigJson; then: any; hideInAnswer: boolean }): boolean {
return false;
}
}

View file

@ -1,27 +1,33 @@
import SingleSetting from "./SingleSetting"; import SingleSetting from "./SingleSetting";
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import {InputElement} from "../Input/InputElement";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import PageSplit from "../Base/PageSplit";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import {VariableUiElement} from "../Base/VariableUIElement";
export default class SettingsTable extends UIElement { export default class SettingsTable extends UIElement {
private _col1: UIElement[] = []; private _col1: UIElement[] = [];
private _col2: InputElement<any>[] = []; private _col2: UIElement[] = [];
public selectedSetting: UIEventSource<SingleSetting<any>>; public selectedSetting: UIEventSource<SingleSetting<any>>;
constructor(elements: SingleSetting<any>[], constructor(elements: (SingleSetting<any> | string)[],
currentSelectedSetting: UIEventSource<SingleSetting<any>>) { currentSelectedSetting: UIEventSource<SingleSetting<any>>) {
super(undefined); super(undefined);
const self = this; const self = this;
this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined); this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined);
for (const element of elements) { for (const element of elements) {
let title: UIElement = new FixedUiElement(element._name); if(typeof element === "string"){
this._col1.push(new FixedUiElement(element));
this._col2.push(null);
continue;
}
let title: UIElement = element._name === undefined ? null : new FixedUiElement(element._name);
this._col1.push(title); this._col1.push(title);
this._col2.push(element._value); this._col2.push(element._value);
element._value.SetStyle("display:block");
element._value.IsSelected.addCallback(isSelected => { element._value.IsSelected.addCallback(isSelected => {
if (isSelected) { if (isSelected) {
self.selectedSetting.setData(element); self.selectedSetting.setData(element);
@ -34,13 +40,19 @@ export default class SettingsTable extends UIElement {
} }
InnerRender(): string { InnerRender(): string {
let html = ""; let elements = [];
for (let i = 0; i < this._col1.length; i++) { for (let i = 0; i < this._col1.length; i++) {
html += `<tr><td>${this._col1[i].Render()}</td><td>${this._col2[i].Render()}</td></tr>` if(this._col1[i] !== null && this._col2[i] !== null){
elements.push(new PageSplit(this._col1[i], this._col2[i], 25));
}else if(this._col1[i] !== null){
elements.push(this._col1[i])
}else{
elements.push(this._col2[i])
}
} }
return `<table><tr>${html}</tr></table>`; return new Combine(elements).Render();
} }
} }

View file

@ -1,4 +1,3 @@
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import {InputElement} from "../Input/InputElement"; import {InputElement} from "../Input/InputElement";
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
@ -12,7 +11,7 @@ export default class SingleSetting<T> {
public _description: UIElement; public _description: UIElement;
public _options: { showIconPreview?: boolean }; public _options: { showIconPreview?: boolean };
constructor(config: UIEventSource<LayoutConfigJson>, constructor(config: UIEventSource<any>,
value: InputElement<T>, value: InputElement<T>,
path: string | (string | number)[], path: string | (string | number)[],
name: string, name: string,
@ -47,11 +46,17 @@ export default class SingleSetting<T> {
// We have to rewalk every time as parts might be new // We have to rewalk every time as parts might be new
let configPart = config.data; let configPart = config.data;
for (const pathPart of path) { for (const pathPart of path) {
configPart = configPart[pathPart]; let newConfigPart = configPart[pathPart];
if (configPart === undefined) { if (newConfigPart === undefined) {
console.warn("Lost the way for path ", path) console.warn("Lost the way for path ", path, " - creating entry")
return; if (typeof (pathPart) === "string") {
configPart[pathPart] = {};
} else {
configPart[pathPart] = [];
} }
newConfigPart = configPart[pathPart];
}
configPart = newConfigPart;
} }
configPart[lastPart] = value; configPart[lastPart] = value;
config.ping(); config.ping();
@ -66,7 +71,6 @@ export default class SingleSetting<T> {
} }
} }
const loadedValue = configPart[lastPart]; const loadedValue = configPart[lastPart];
if (loadedValue !== undefined) { if (loadedValue !== undefined) {
value.GetValue().setData(loadedValue); value.GetValue().setData(loadedValue);
} }
@ -81,4 +85,6 @@ export default class SingleSetting<T> {
} }
} }

View file

@ -0,0 +1,103 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {InputElement} from "../Input/InputElement";
import SingleSetting from "./SingleSetting";
import SettingsTable from "./SettingsTable";
import {TextField, ValidatedTextField} from "../Input/TextField";
import Combine from "../Base/Combine";
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
import {AndOrTagInput} from "../Input/AndOrTagInput";
import {MultiTagInput} from "../Input/MultiTagInput";
import {MultiInput} from "../Input/MultiInput";
import MappingInput from "./MappingInput";
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
export default class TagRenderingPanel extends InputElement<TagRenderingConfigJson> {
private intro: UIElement;
private settingsTable: UIElement;
public IsImage = false;
private readonly _value: UIEventSource<TagRenderingConfigJson>;
public options: { title?: string; description?: string; disableQuestions?: boolean; isImage?: boolean; };
constructor(languages: UIEventSource<string[]>,
currentlySelected: UIEventSource<SingleSetting<any>>,
options?: {
title?: string,
description?: string,
disableQuestions?: boolean,
isImage?: boolean
}) {
super();
this.SetClass("bordered");
this.SetClass("min-height");
this.options = options ?? {};
this.intro = new Combine(["<h3>", options?.title ?? "TagRendering", "</h3>", options?.description ?? ""])
this.IsImage = options?.isImage ?? false;
const value = new UIEventSource<TagRenderingConfigJson>({});
this._value = value;
function setting(input: InputElement<any>, id: string | string[], name: string, description: string | UIElement): SingleSetting<any> {
return new SingleSetting<any>(value, input, id, name, description);
}
const questionSettings = [
setting(new MultiLingualTextFields(languages), "question", "Question", "If the key or mapping doesn't match, this question is asked"),
setting(new AndOrTagInput(), "condition", "Condition",
"Only show this tag rendering if these tags matches. Optional field.<br/>Note that the Overpass-tags are already always included in this object"),
"<h3>Freeform key</h3>",
setting(TextField.KeyInput(), ["freeform", "key"], "Freeform key<br/>",
"If specified, the rendering will search if this key is present." +
"If it is, the rendering above will be used to display the element.<br/>" +
"The rendering will go into question mode if <ul><li>this key is not present</li><li>No single mapping matches</li><li>A question is given</li>"),
setting(ValidatedTextField.TypeDropdown(), ["freeform", "type"], "Freeform type",
"The type of this freeform text field, in order to validate"),
setting(new MultiTagInput(), ["freeform", "addExtraTags"], "Extra tags on freeform",
"When the freeform text field is used, the user might mean a predefined key. This field allows to add extra tags, e.g. <span class='literal-code'>fixme=User used a freeform field - to check</span>"),
];
const settings: (string | SingleSetting<any>)[] = [
setting(new MultiLingualTextFields(languages), "render", "Value to show", " Renders this value. Note that <span class='literal-code'>{key}</span>-parts are substituted by the corresponding values of the element. If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value."),
...(options?.disableQuestions ? [] : questionSettings),
"<h3>Mappings</h3>",
setting(new MultiInput<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }>("Add a mapping",
() => ({if: undefined, then: undefined}),
() => new MappingInput(languages, options?.disableQuestions ?? false)), "mappings",
"Mappings", "")
];
this.settingsTable = new SettingsTable(settings, currentlySelected);
}
InnerRender(): string {
return new Combine([
this.intro,
this.settingsTable]).Render();
}
GetValue(): UIEventSource<TagRenderingConfigJson> {
return this._value;
}
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
IsValid(t: TagRenderingConfigJson): boolean {
return false;
}
}

View file

@ -0,0 +1,15 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingPanel from "./TagRenderingPanel";
export default class TagRenderingPreview extends UIElement{
constructor(selectedTagRendering: UIEventSource<TagRenderingPanel>) {
super(selectedTagRendering);
}
InnerRender(): string {
return "";
}
}

View file

@ -3,88 +3,162 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton"; import {SubtleButton} from "../Base/SubtleButton";
import TagInput from "./TagInput"; import {CheckBox} from "./CheckBox";
import {FixedUiElement} from "../Base/FixedUiElement"; import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
import {MultiTagInput} from "./MultiTagInput";
import {FormatNumberOptions} from "libphonenumber-js";
export class AndOrTagInput extends InputElement<(string | AndOrTagInput)[]> { class AndOrConfig implements AndOrTagConfigJson {
public and: (string | AndOrTagConfigJson)[] = undefined;
public or: (string | AndOrTagConfigJson)[] = undefined;
}
private readonly _value: UIEventSource<string[]>; export class AndOrTagInput extends InputElement<AndOrTagConfigJson> {
private readonly _rawTags = new MultiTagInput();
private readonly _subAndOrs: AndOrTagInput[] = [];
private readonly _isAnd: UIEventSource<boolean> = new UIEventSource<boolean>(true);
private readonly _isAndButton;
private readonly _addBlock: UIElement;
private readonly _value: UIEventSource<AndOrConfig> = new UIEventSource<AndOrConfig>(undefined);
public bottomLeftButton: UIElement;
IsSelected: UIEventSource<boolean>; IsSelected: UIEventSource<boolean>;
private elements: UIElement[] = [];
private inputELements: (InputElement<string> | InputElement<AndOrTagInput>)[] = [];
private addTag: UIElement;
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) { constructor() {
super(undefined); super();
this._value = value;
this.addTag = new SubtleButton("./assets/addSmall.svg", "Add a tag")
.SetClass("small-button")
.onClick(() => {
this.IsSelected.setData(true);
value.data.push("");
value.ping();
});
const self = this; const self = this;
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements()); this._isAndButton = new CheckBox(
this.createElements(); new SubtleButton("./assets/ampersand.svg", null).SetClass("small-button"),
new SubtleButton("./assets/or.svg", null).SetClass("small-button"),
this._isAnd);
this._value.addCallback(tags => self.load(tags)); this._addBlock =
this.IsSelected = new UIEventSource<boolean>(false); new SubtleButton("./assets/addSmall.svg", "Add an and/or-expression")
} .SetClass("small-button")
.onClick(() => {self.createNewBlock()});
private load(tags: string[]) {
if (tags === undefined) {
return;
}
for (let i = 0; i < tags.length; i++) {
console.log("Setting tag ", i)
this.inputELements[i].GetValue().setData(tags[i]);
}
}
private UpdateIsSelected(){ this._isAnd.addCallback(() => self.UpdateValue());
this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b)) this._rawTags.GetValue().addCallback(() => {
} self.UpdateValue()
private createElements() {
this.inputELements = [];
this.elements = [];
for (let i = 0; i < this._value.data.length; i++) {
let tag = this._value.data[i];
const input = new TagInput(new UIEventSource<string>(tag));
input.GetValue().addCallback(tag => {
console.log("Writing ", tag)
this._value.data[i] = tag;
this._value.ping();
}
);
this.inputELements.push(input);
input.IsSelected.addCallback(() => this.UpdateIsSelected());
const deleteBtn = new FixedUiElement("<img src='./assets/delete.svg' style='max-width: 1.5em; margin-left: 5px;'>")
.onClick(() => {
this._value.data.splice(i, 1);
this._value.ping();
}); });
this.elements.push(new Combine([input, deleteBtn, "<br/>"]).SetClass("tag-input-row"))
this.IsSelected = this._rawTags.IsSelected;
this._value.addCallback(tags => self.loadFromValue(tags));
} }
private createNewBlock(){
const inputEl = new AndOrTagInput();
inputEl.GetValue().addCallback(() => this.UpdateValue());
const deleteButton = this.createDeleteButton(inputEl.id);
inputEl.bottomLeftButton = deleteButton;
this._subAndOrs.push(inputEl);
this.Update(); this.Update();
} }
InnerRender(): string { private createDeleteButton(elementId: string): UIElement {
return new Combine([...this.elements, this.addTag]).SetClass("bordered").Render(); const self = this;
return new SubtleButton("./assets/delete.svg", null).SetClass("small-button")
.onClick(() => {
for (let i = 0; i < self._subAndOrs.length; i++) {
if (self._subAndOrs[i].id === elementId) {
self._subAndOrs.splice(i, 1);
self.Update();
self.UpdateValue();
return;
}
}
});
} }
private loadFromValue(value: AndOrTagConfigJson) {
this._isAnd.setData(value.and !== undefined);
const tags = value.and ?? value.or;
const rawTags: string[] = [];
const subTags: AndOrTagConfigJson[] = [];
for (const tag of tags) {
IsValid(t: string[]): boolean { if (typeof (tag) === "string") {
return false; rawTags.push(tag);
} else {
subTags.push(tag);
}
} }
GetValue(): UIEventSource<string[]> { for (let i = 0; i < rawTags.length; i++) {
if (this._rawTags.GetValue().data[i] !== rawTags[i]) {
// For some reason, 'setData' isn't stable as the comparison between the lists fails
// Probably because we generate a new list object every timee
// So we compare again here and update only if we find a difference
this._rawTags.GetValue().setData(rawTags);
break;
}
}
while(this._subAndOrs.length < subTags.length){
this.createNewBlock();
}
for (let i = 0; i < subTags.length; i++){
let subTag = subTags[i];
this._subAndOrs[i].GetValue().setData(subTag);
}
}
private UpdateValue() {
const tags: (string | AndOrTagConfigJson)[] = [];
tags.push(...this._rawTags.GetValue().data);
for (const subAndOr of this._subAndOrs) {
const subAndOrData = subAndOr._value.data;
if (subAndOrData === undefined) {
continue;
}
console.log(subAndOrData);
tags.push(subAndOrData);
}
const tagConfig = new AndOrConfig();
if (this._isAnd.data) {
tagConfig.and = tags;
} else {
tagConfig.or = tags;
}
this._value.setData(tagConfig);
}
GetValue(): UIEventSource<AndOrTagConfigJson> {
return this._value; return this._value;
} }
InnerRender(): string {
const leftColumn = new Combine([
this._isAndButton,
"<br/>",
this.bottomLeftButton ?? ""
]);
const tags = new Combine([
this._rawTags,
...this._subAndOrs,
this._addBlock
]).Render();
return `<span class="bordered"><table><tr><td>${leftColumn.Render()}</td><td>${tags}</td></tr></table></span>`;
}
IsValid(t: AndOrTagConfigJson): boolean {
return true;
}
} }

89
UI/Input/MultiInput.ts Normal file
View file

@ -0,0 +1,89 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton";
import {FixedUiElement} from "../Base/FixedUiElement";
export class MultiInput<T> extends InputElement<T[]> {
private readonly _value: UIEventSource<T[]>;
IsSelected: UIEventSource<boolean>;
private elements: UIElement[] = [];
private inputELements: InputElement<T>[] = [];
private addTag: UIElement;
constructor(
addAElement: string,
newElement: (() => T),
createInput: (() => InputElement<T>),
value: UIEventSource<T[]> = new UIEventSource<T[]>([])) {
super(undefined);
this._value = value;
this.addTag = new SubtleButton("./assets/addSmall.svg", addAElement)
.SetClass("small-button")
.onClick(() => {
this.IsSelected.setData(true);
value.data.push(newElement());
value.ping();
});
const self = this;
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements(createInput));
this.createElements(createInput);
this._value.addCallback(tags => self.load(tags));
this.IsSelected = new UIEventSource<boolean>(false);
}
private load(tags: T[]) {
if (tags === undefined) {
return;
}
for (let i = 0; i < tags.length; i++) {
this.inputELements[i].GetValue().setData(tags[i]);
}
}
private UpdateIsSelected(){
this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b))
}
private createElements(createInput: (() => InputElement<T>)) {
this.inputELements.splice(0, this.inputELements.length);
this.elements = [];
const self = this;
for (let i = 0; i < this._value.data.length; i++) {
let tag = this._value.data[i];
const input = createInput();
input.GetValue().addCallback(tag => {
self._value.data[i] = tag;
self._value.ping();
}
);
this.inputELements.push(input);
input.IsSelected.addCallback(() => this.UpdateIsSelected());
const deleteBtn = new FixedUiElement("<img src='./assets/delete.svg' style='max-width: 1.5em; margin-left: 5px;'>")
.onClick(() => {
self._value.data.splice(i, 1);
self._value.ping();
});
this.elements.push(new Combine([input, deleteBtn, "<br/>"]).SetClass("tag-input-row"))
}
this.Update();
}
InnerRender(): string {
return new Combine([...this.elements, this.addTag]).Render();
}
IsValid(t: T[]): boolean {
return false;
}
GetValue(): UIEventSource<T[]> {
return this._value;
}
}

View file

@ -5,88 +5,17 @@ import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton"; import {SubtleButton} from "../Base/SubtleButton";
import TagInput from "./TagInput"; import TagInput from "./TagInput";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import {MultiInput} from "./MultiInput";
export class MultiTagInput extends InputElement<string[]> { export class MultiTagInput extends MultiInput<string> {
public static tagExplanation: UIElement =
new FixedUiElement("<h3>How to use the tag-element</h3>")
private readonly _value: UIEventSource<string[]>;
IsSelected: UIEventSource<boolean>;
private elements: UIElement[] = [];
private inputELements: InputElement<string>[] = [];
private addTag: UIElement;
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) { constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) {
super(undefined); super("Add a new tag",
this._value = value; () => "",
() => new TagInput(),
this.addTag = new SubtleButton("./assets/addSmall.svg", "Add a tag") value
.SetClass("small-button")
.onClick(() => {
this.IsSelected.setData(true);
value.data.push("");
value.ping();
});
const self = this;
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements());
this.createElements();
this._value.addCallback(tags => self.load(tags));
this.IsSelected = new UIEventSource<boolean>(false);
}
private load(tags: string[]) {
if (tags === undefined) {
return;
}
for (let i = 0; i < tags.length; i++) {
console.log("Setting tag ", i)
this.inputELements[i].GetValue().setData(tags[i]);
}
}
private UpdateIsSelected(){
this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b))
}
private createElements() {
this.inputELements = [];
this.elements = [];
for (let i = 0; i < this._value.data.length; i++) {
let tag = this._value.data[i];
const input = new TagInput(new UIEventSource<string>(tag));
input.GetValue().addCallback(tag => {
console.log("Writing ", tag)
this._value.data[i] = tag;
this._value.ping();
}
); );
this.inputELements.push(input);
input.IsSelected.addCallback(() => this.UpdateIsSelected());
const deleteBtn = new FixedUiElement("<img src='./assets/delete.svg' style='max-width: 1.5em; margin-left: 5px;'>")
.onClick(() => {
this._value.data.splice(i, 1);
this._value.ping();
});
this.elements.push(new Combine([input, deleteBtn, "<br/>"]).SetClass("tag-input-row"))
}
this.Update();
}
InnerRender(): string {
return new Combine([...this.elements, this.addTag]).SetClass("bordered").Render();
}
IsValid(t: string[]): boolean {
return false;
}
GetValue(): UIEventSource<string[]> {
return this._value;
} }
} }

View file

@ -2,6 +2,7 @@ import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
export class RadioButton<T> extends InputElement<T> { export class RadioButton<T> extends InputElement<T> {
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _selectedElementIndex: UIEventSource<number> private readonly _selectedElementIndex: UIEventSource<number>
= new UIEventSource<number>(null); = new UIEventSource<number>(null);
@ -26,16 +27,16 @@ export class RadioButton<T> extends InputElement<T> {
return elements[selectedIndex].GetValue() return elements[selectedIndex].GetValue()
} }
} }
), elements.map(e => e.GetValue())); ), elements.map(e => e?.GetValue()));
this.value.addCallback((t) => { this.value.addCallback((t) => {
self.ShowValue(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); self._selectedElementIndex.setData(i);
}); });
} }

View file

@ -18,16 +18,7 @@ export default class SingleTagInput extends InputElement<string> {
super(undefined); super(undefined);
this._value = value ?? new UIEventSource<string>(undefined); this._value = value ?? new UIEventSource<string>(undefined);
this.key = new TextField({ this.key = TextField.KeyInput();
placeholder: "key",
fromString: str => {
if (str?.match(/^[a-zA-Z][a-zA-Z0-9:]*$/)) {
return str;
}
return undefined
},
toString: str => str
});
this.value = new TextField<string>({ this.value = new TextField<string>({
placeholder: "value - if blank, matches if key is NOT present", placeholder: "value - if blank, matches if key is NOT present",
@ -95,7 +86,8 @@ export default class SingleTagInput extends InputElement<string> {
InnerRender(): string { InnerRender(): string {
return new Combine([ return new Combine([
this.key, this.operator, this.value this.key, this.operator, this.value
]).Render(); ]).SetStyle("display:flex")
.Render();
} }

View file

@ -4,8 +4,33 @@ import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import * as EmailValidator from "email-validator"; import * as EmailValidator from "email-validator";
import {parsePhoneNumberFromString} from "libphonenumber-js"; import {parsePhoneNumberFromString} from "libphonenumber-js";
import {DropDown} from "./DropDown";
export class ValidatedTextField { export class ValidatedTextField {
public static explanations = {
"string": "A basic, 255-char string",
"date": "A date",
"wikidata": "A wikidata identifier, e.g. Q42",
"int": "A number",
"nat": "A positive number",
"float": "A decimal",
"pfloat": "A positive decimal",
"email": "An email adress",
"url": "A url",
"phone": "A phone number"
}
public static TypeDropdown() : DropDown<string>{
const values : {value: string, shown: string}[] = [];
const expl = ValidatedTextField.explanations;
for(const key in expl){
values.push({value: key, shown: `${key} - ${expl[key]}`})
}
return new DropDown<string>("", values)
}
public static inputValidation = { public static inputValidation = {
"$": () => true, "$": () => true,
"string": () => true, "string": () => true,
@ -40,6 +65,19 @@ export class TextField<T> extends InputElement<T> {
}); });
} }
public static KeyInput(): TextField<string>{
return new TextField<string>({
placeholder: "key",
fromString: str => {
if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) {
return str;
}
return undefined
},
toString: str => str
});
}
public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{ public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{
const isValid = ValidatedTextField.inputValidation[type]; const isValid = ValidatedTextField.inputValidation[type];
extraValidation = extraValidation ?? (() => true) extraValidation = extraValidation ?? (() => true)

View file

@ -162,6 +162,7 @@ export class ShareScreen extends UIElement {
this._iframeCode = new VariableUiElement( this._iframeCode = new VariableUiElement(
url.map((url) => { url.map((url) => {
return `<span class='literal-code iframe-code-block'> return `<span class='literal-code iframe-code-block'>
&lt;iframe src="${url}" width="100%" height="100%" title="${layout.title.InnerRender()} with MapComplete"&gt;&lt;/iframe&gt
</span>` </span>`
}) })
); );

View file

@ -53,7 +53,7 @@ export class SimpleAddUI extends UIElement {
if (typeof (preset.icon) !== "string") { if (typeof (preset.icon) !== "string") {
const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"}); const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"});
icon = preset.icon.GetContent(tags); icon = preset.icon.GetContent(tags).txt;
} else { } else {
icon = preset.icon; icon = preset.icon;
} }
@ -193,7 +193,7 @@ export class SimpleAddUI extends UIElement {
return new Combine([header, Translations.t.general.add.stillLoading]).Render() return new Combine([header, Translations.t.general.add.stillLoading]).Render()
} }
return header.Render() + new Combine(this._addButtons, "add-popup-all-buttons").Render(); return header.Render() + new Combine(this._addButtons).SetClass("add-popup-all-buttons").Render();
} }

View file

@ -1,6 +1,6 @@
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
export abstract class UIElement extends UIEventSource<string>{ export abstract class UIElement extends UIEventSource<string> {
private static nextId: number = 0; private static nextId: number = 0;
@ -8,8 +8,12 @@ export abstract class UIElement extends UIEventSource<string>{
public readonly _source: UIEventSource<any>; public readonly _source: UIEventSource<any>;
private clss: string[] = [] private clss: string[] = []
private style: string;
private _hideIfEmpty = false; private _hideIfEmpty = false;
public dumbMode = false;
/** /**
* In the 'deploy'-step, some code needs to be run by ts-node. * In the 'deploy'-step, some code needs to be run by ts-node.
* However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed. * However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed.
@ -30,6 +34,7 @@ export abstract class UIElement extends UIEventSource<string>{
if (source === undefined) { if (source === undefined) {
return this; return this;
} }
this.dumbMode = false;
const self = this; const self = this;
source.addCallback(() => { source.addCallback(() => {
self.Update(); self.Update();
@ -40,24 +45,56 @@ export abstract class UIElement extends UIEventSource<string>{
private _onClick: () => void; private _onClick: () => void;
public onClick(f: (() => void)) { public onClick(f: (() => void)) {
this.dumbMode = false;
this._onClick = f; this._onClick = f;
this.SetClass("clickable") this.SetClass("clickable")
this.Update(); this.Update();
return this; return this;
} }
private _onHover: UIEventSource<boolean>;
public IsHovered(): UIEventSource<boolean> {
this.dumbMode = false;
if (this._onHover !== undefined) {
return this._onHover;
}
// Note: we just save it. 'Update' will register that an eventsource exist and install the necessary hooks
this._onHover = new UIEventSource<boolean>(false);
return this._onHover;
}
Update(): void { Update(): void {
if(UIElement.runningFromConsole){ if (UIElement.runningFromConsole) {
return; return;
} }
let element = document.getElementById(this.id); let element = document.getElementById(this.id);
if (element === undefined || element === null) { if (element === undefined || element === null) {
// The element is not painted // The element is not painted
if (this.dumbMode) {
// We update all the children anyway
for (const i in this) {
const child = this[i];
if (child instanceof UIElement) {
child.Update();
} else if (child instanceof Array) {
for (const ch of child) {
if (ch instanceof UIElement) {
ch.Update();
}
}
}
}
}
return; return;
} }
this.setData(this.InnerRender()); this.setData(this.InnerRender());
element.innerHTML = this.data; element.innerHTML = this.data;
if (this._hideIfEmpty) { if (this._hideIfEmpty) {
if (element.innerHTML === "") { if (element.innerHTML === "") {
element.parentElement.style.display = "none"; element.parentElement.style.display = "none";
@ -70,7 +107,7 @@ export abstract class UIElement extends UIEventSource<string>{
const self = this; const self = this;
element.onclick = (e) => { element.onclick = (e) => {
// @ts-ignore // @ts-ignore
if(e.consumed){ if (e.consumed) {
return; return;
} }
self._onClick(); self._onClick();
@ -81,6 +118,12 @@ export abstract class UIElement extends UIEventSource<string>{
element.style.cursor = "pointer"; element.style.cursor = "pointer";
} }
if (this._onHover !== undefined) {
const self = this;
element.addEventListener('mouseover', () => self._onHover.setData(true));
element.addEventListener('mouseout', () => self._onHover.setData(false));
}
this.InnerUpdate(element); this.InnerUpdate(element);
for (const i in this) { for (const i in this) {
@ -108,10 +151,18 @@ export abstract class UIElement extends UIEventSource<string>{
} }
Render(): string { Render(): string {
return `<span class='uielement ${this.clss.join(" ")}' id='${this.id}'>${this.InnerRender()}</span>` if (this.dumbMode) {
return this.InnerRender();
}
let style = "";
if (this.style !== undefined && this.style !== "") {
style = `style="${this.style}"`;
}
return `<span class='uielement ${this.clss.join(" ")}' ${style} id='${this.id}'>${this.InnerRender()}</span>`
} }
AttachTo(divId: string) { AttachTo(divId: string) {
this.dumbMode = false;
let element = document.getElementById(divId); let element = document.getElementById(divId);
if (element === null) { if (element === null) {
throw "SEVERE: could not attach UIElement to " + divId; throw "SEVERE: could not attach UIElement to " + divId;
@ -143,6 +194,7 @@ export abstract class UIElement extends UIEventSource<string>{
} }
public SetClass(clss: string): UIElement { public SetClass(clss: string): UIElement {
this.dumbMode = false;
if (this.clss.indexOf(clss) < 0) { if (this.clss.indexOf(clss) < 0) {
this.clss.push(clss); this.clss.push(clss);
} }
@ -150,14 +202,13 @@ export abstract class UIElement extends UIEventSource<string>{
return this; return this;
} }
public RemoveClass(clss: string): UIElement {
if (this.clss.indexOf(clss) >= 0) { public SetStyle(style: string): UIElement {
this.clss = this.clss.splice(this.clss.indexOf(clss), 1); this.dumbMode = false;
} this.style = style;
this.Update(); this.Update();
return this; return this;
} }
} }

View file

@ -52,8 +52,8 @@ export default class Translation extends UIElement {
for (const i in this.translations) { for (const i in this.translations) {
return this.translations[i]; // Return a random language return this.translations[i]; // Return a random language
} }
console.log("Missing language ",Locale.language.data,"for",this.translations) console.error("Missing language ",Locale.language.data,"for",this.translations)
return "Missing translation" return undefined;
} }
InnerRender(): string { InnerRender(): string {

53
assets/ampersand.svg Normal file
View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="275.9444"
height="243.66881"
version="1.1"
id="svg6"
sodipodi:docname="Ampersand.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="1013"
id="namedview8"
showgrid="false"
inkscape:zoom="0.5503876"
inkscape:cx="319.5"
inkscape:cy="120"
inkscape:window-x="1560"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M 69.184621,88.05971 C 65.398038,84.28878 45.405425,62.369149 47.716835,38.654524 49.990823,15.323837 73.884556,1.2473955 95.97427,0.11693352 c 20.36977,-1.042443 43.85918,4.70805898 53.3103,24.39507048 10.11956,21.079395 -1.28925,45.999521 -18.03685,58.640336 -5.82684,4.398004 -7.18682,4.599329 -15.78717,8.35864 -12.3926,5.41695 -24.869636,10.70587 -37.591472,15.28724 -26.286247,9.46617 -46.329939,30.90918 -45.609377,60.10938 0.656673,26.61116 24.371436,47.43668 49.951101,51.46486 27.220348,4.28654 49.202778,-0.15657 67.923898,-21.02736 8.04442,-8.96814 24.45293,-23.68334 32.63281,-32.53125 14.48284,-15.66562 21.97669,-28.32038 27.29668,-49.45345 3.60407,-14.31675 -20.5185,-11.01811 -16.28105,-23.06216 25.44722,-2.93304 51.02915,-3.7848 76.5625,-5.66406 4.00323,11.84618 -9.36778,8.3653 -26.72951,23.04671 -19.60573,16.579 -28.72934,30.72561 -45.60418,49.85029 l -11.89837,13.48472 c -8.00837,9.07609 -21.15724,23.50336 -29.33044,32.43076 -17.4629,19.07433 -33.57017,30.64012 -59.50887,35.61559 -27.730664,5.31919 -60.623141,2.30496 -80.151308,-20.4437 C -3.6264102,196.44728 -7.0848351,156.57316 15.462826,132.33729 30.171306,116.52755 38.031184,108.84767 57.724466,100.76314 73.147466,94.43165 97.05575,88.100173 109.29677,82.829346 136.69178,71.033402 137.40896,42.147541 124.50818,21.048935 113.44184,2.9504655 80.908653,4.4216525 74.904904,25.669377 69.689417,44.127381 77.089538,56.651269 88.37226,69.60789 l 96.21768,110.49249 c 11.83509,14.20823 29.6542,37.45695 49.80585,41.07969 13.32763,2.39596 30.53611,-3.5713 39.88214,-12.02915 4.97541,9.00928 -2.24528,16.35839 -7.83854,22.01449 -18.29468,18.50022 -49.85481,14.73994 -70.12946,-0.0122 -12.30082,-8.95026 -20.35382,-15.29947 -31.35277,-27.32693 z"
id="path2"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

54
assets/or.svg Normal file
View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="275.9444"
height="243.66881"
version="1.1"
id="svg6"
sodipodi:docname="or.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1001"
id="namedview8"
showgrid="false"
inkscape:zoom="1.5567312"
inkscape:cx="116.77734"
inkscape:cy="95.251996"
inkscape:window-x="1560"
inkscape:window-y="1060"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
style="fill:none;stroke:#000000;stroke-width:27.45802498;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 136.18279,27.932469 V 214.66155"
id="path812"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -93,7 +93,7 @@
"condition": "indoor=yes", "condition": "indoor=yes",
"freeform": { "freeform": {
"key": "access", "key": "access",
"addExtraTags": "fixme=Freeform field used for access - doublecheck the value" "addExtraTags": ["fixme=Freeform field used for access - doublecheck the value"]
}, },
"mappings": [ "mappings": [
{ {

View file

@ -76,7 +76,7 @@
}, },
"freeform": { "freeform": {
"key": "artwork_type", "key": "artwork_type",
"addExtraTags": "fixme=Artowrk type was added with the freeform, might need another check" "addExtraTags": ["fixme=Artowrk type was added with the freeform, might need another check"]
}, },
"mappings": [ "mappings": [
{ {

View file

@ -56,7 +56,9 @@
"render": "Access is {access}", "render": "Access is {access}",
"freeform": { "freeform": {
"key": "access", "key": "access",
"addExtraTags": "fixme=the tag access was filled out by the user and might need refinement" "addExtraTags": [
"fixme=the tag access was filled out by the user and might need refinement"
]
}, },
"mappings": [ "mappings": [
{ {

View file

@ -5,32 +5,6 @@
<title>Custom Theme Generator for Mapcomplete</title> <title>Custom Theme Generator for Mapcomplete</title>
<style type="text/css"> <style type="text/css">
#left {
position: absolute;
width: 50vw;
height: 100vh;
left: 0;
top: 0;
overflow-y: auto;
}
#right {
position: absolute;
width: 50vw;
height: 35vh;
right: 0;
top: 0;
overflow-y: auto;
}
#bottomright {
position: absolute;
width: 50vw;
height: 65vh;
right: 0;
bottom: 0;
overflow-y: auto;
}
.icon-preview { .icon-preview {
max-width: 2em; max-width: 2em;
@ -52,27 +26,72 @@
display:block; display:block;
padding: 0.5em; padding: 0.5em;
border-radius: 0.5em; border-radius: 0.5em;
box-sizing: border-box;
} }
.tag-input-row { .tag-input-row {
display: block ruby; display: block ruby;
box-sizing: border-box; box-sizing: border-box;
margin-right: 2em; margin-right: 2em;
width: calc(100% - 3em);
padding-right: 0.5em;
height: min-content;
}
.min-height {
display: block;
height: min-content;
}
.main-tabs{
height: 100vh;
}
.tab-content {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
margin-left: 0.5em;
}
.main-tabs > .tabs-header-bar {
background: #eee;
}
.scrollable {
display: block;
overflow-y: scroll;
height: calc(100vh - 9em - 10px);
}
.main-tabs > .tab-content {
display: block;
height: 100%;
padding-bottom: 0 ;
padding-right: 0;
}
.main-tabs > .tab-content > span{
display: block;
height: 100%;
} }
body { body {
height: 100%; height: 100%;
} }
#maindiv {
height: calc(100% - 6em);
}
</style> </style>
</head> </head>
<body> <body>
<div id="left"> <div id="maindiv">
'left' not attached 'maindiv' not attached
</div> </div>
<div id="right">'right' not attached</div>
<div id="bottomright">'bottomright' not attached</div>
<script src="./customGenerator.ts"></script> <script src="./customGenerator.ts"></script>
</body> </body>
</html> </html>

View file

@ -1,68 +1,51 @@
import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
import {UIEventSource} from "./Logic/UIEventSource"; import {UIEventSource} from "./Logic/UIEventSource";
import SingleSetting from "./UI/CustomGenerator/SingleSetting"; import SingleSetting from "./UI/CustomGenerator/SingleSetting";
import Combine from "./UI/Base/Combine"; import Combine from "./UI/Base/Combine";
import {VariableUiElement} from "./UI/Base/VariableUIElement"; import {VariableUiElement} from "./UI/Base/VariableUIElement";
import GeneralSettings from "./UI/CustomGenerator/GeneralSettings"; import GeneralSettings from "./UI/CustomGenerator/GeneralSettings";
import {SubtleButton} from "./UI/Base/SubtleButton";
import {TabbedComponent} from "./UI/Base/TabbedComponent"; import {TabbedComponent} from "./UI/Base/TabbedComponent";
import AllLayersPanel from "./UI/CustomGenerator/AllLayersPanel"; import AllLayersPanel from "./UI/CustomGenerator/AllLayersPanel";
import {ShareScreen} from "./UI/ShareScreen";
import {FromJSON} from "./Customizations/JSON/FromJSON";
import SharePanel from "./UI/CustomGenerator/SharePanel"; import SharePanel from "./UI/CustomGenerator/SharePanel";
import {GenerateEmpty} from "./UI/CustomGenerator/GenerateEmpty";
import PageSplit from "./UI/Base/PageSplit";
import HelpText from "./Customizations/HelpText";
import {TagRendering} from "./Customizations/TagRendering";
const empty: LayoutConfigJson = { const es = new UIEventSource(GenerateEmpty.createTestLayout());
id: "",
title: {},
description: {},
language: [],
maintainer: "",
icon: "./assets/bug.svg",
version: "0",
startLat: 0,
startLon: 0,
startZoom: 1,
socialImage: "",
layers: [],
}
const test: LayoutConfigJson = {
id: "test",
title: {"en": "Test layout"},
description: {"en": "A layout for testing"},
language: ["en"],
maintainer: "Pieter Vander Vennet",
icon: "./assets/bug.svg",
version: "0",
startLat: 0,
startLon: 0,
startZoom: 1,
widenFactor: 0.05,
socialImage: "",
layers: [],
}
const es = new UIEventSource(test);
const encoded = es.map(config => btoa(JSON.stringify(config))); const encoded = es.map(config => btoa(JSON.stringify(config)));
const testUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}&test=true#${encoded}`)
const liveUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}#${encoded}`) const liveUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}#${encoded}`)
const iframe = liveUrl.map(url => `<iframe src='${url}' width='100%' height='99%' style="box-sizing: border-box" title='Theme Preview'></iframe>`); const iframe = liveUrl.map(url => `<iframe src='${url}' width='100%' height='99%' style="box-sizing: border-box" title='Theme Preview'></iframe>`);
TagRendering.injectFunction();
const currentSetting = new UIEventSource<SingleSetting<any>>(undefined) const currentSetting = new UIEventSource<SingleSetting<any>>(undefined)
const generalSettings = new GeneralSettings(es, currentSetting); const generalSettings = new GeneralSettings(es, currentSetting);
const languages = generalSettings.languages; const languages = generalSettings.languages;
// The preview
const preview = new Combine([
new VariableUiElement(iframe.stabilized(2500))
]).SetClass("preview")
new TabbedComponent([ new TabbedComponent([
{ {
header: "<img src='./assets/gear.svg'>", header: "<img src='./assets/gear.svg'>",
content: generalSettings content:
new PageSplit(
generalSettings.SetStyle("width: 50vw;"),
new Combine([
new HelpText(currentSetting).SetStyle("height:calc(100% - 65vh); width: 100%; display:block; overflow-y: auto"),
preview.SetStyle("height:65vh; width:100%; display:block")
]).SetStyle("position:relative; width: 50%;")
)
}, },
{ {
header: "<img src='./assets/layers.svg'>", header: "<img src='./assets/layers.svg'>",
content: new AllLayersPanel(es, currentSetting, languages) content: new AllLayersPanel(es, languages)
}, },
{ {
header: "<img src='./assets/floppy.svg'>", header: "<img src='./assets/floppy.svg'>",
@ -77,44 +60,6 @@ new TabbedComponent([
header: "<img src='./assets/share.svg'>", header: "<img src='./assets/share.svg'>",
content: new SharePanel(es, liveUrl) content: new SharePanel(es, liveUrl)
} }
]).AttachTo("left"); ], 1).SetClass("main-tabs")
.AttachTo("maindiv");
const returnButton = new SubtleButton("./assets/close.svg",
new VariableUiElement(
currentSetting.map(currentSetting => {
if (currentSetting === undefined) {
return "";
}
return "Return to general help";
}
)
))
.ListenTo(currentSetting)
.onClick(() => currentSetting.setData(undefined));
const helpText = new VariableUiElement(currentSetting.map((setting: SingleSetting<any>) => {
if (setting === undefined) {
return "<h1>Welcome to the Custom Theme Builder</h1>" +
"Here, one can make their own custom mapcomplete themes.<br/>" +
"Fill out the fields to get a working mapcomplete theme. More information on the selected field will appear here when you click it";
}
return new Combine(["<h1>", setting._name, "</h1>", setting._description.Render()]).Render();
}))
new Combine([helpText,
returnButton,
]).AttachTo("right");
// The preview
new Combine([
new VariableUiElement(iframe)
]).AttachTo("bottomright");

View file

@ -109,16 +109,21 @@
padding-bottom: 0.15em; padding-bottom: 0.15em;
} }
.clickable { .clickable {
pointer-events: all; pointer-events: all;
} }
.page-split {
display: flex;
height: 100%;
}
.activate-osm-authentication { .activate-osm-authentication {
cursor: pointer; cursor: pointer;
color: blue; color: blue;
text-decoration: underline; text-decoration: underline;
} }
/**************** USER BADGE ****************/ /**************** USER BADGE ****************/
@ -1224,11 +1229,10 @@
.tab-content { .tab-content {
padding: 1em;
z-index: 5002; z-index: 5002;
background-color: white; background-color: white;
position: relative; position: relative;
padding: 1em;
} }
.tab-single-header { .tab-single-header {

View file

@ -1,8 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Small tests</title> <title>Small tests</title>
<link href="index.css" rel="stylesheet"/> <link href="index.css" rel="stylesheet"/>
<style>
.tag-input-row {
display: block ruby;
box-sizing: border-box;
margin-right: 2em;
width: 100%;
}
.bordered {
border: 1px solid black;
display: block;
padding: 0.5em;
border-radius: 0.5em;
}
</style>
</head> </head>
<body> <body>
<div id="maindiv">'maindiv' not attached</div> <div id="maindiv">'maindiv' not attached</div>

22
test.ts
View file

@ -1,9 +1,19 @@
import TagInput from "./UI/Input/TagInput"; import TagRenderingPanel from "./UI/CustomGenerator/TagRenderingPanel";
import {UIEventSource} from "./Logic/UIEventSource"; import {UIEventSource} from "./Logic/UIEventSource";
import {TextField} from "./UI/Input/TextField";
import {VariableUiElement} from "./UI/Base/VariableUIElement"; import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {MultiTagInput} from "./UI/Input/MultiTagInput"; import SettingsTable from "./UI/CustomGenerator/SettingsTable";
import SingleSetting from "./UI/CustomGenerator/SingleSetting";
import {MultiInput} from "./UI/Input/MultiInput";
const input = new MultiTagInput(new UIEventSource<string[]>(["key~value|0"]));
input.GetValue().addCallback(console.log); const config = new UIEventSource({})
input.AttachTo("maindiv"); const languages = new UIEventSource(["en","nl"]);
new VariableUiElement(input.GetValue().map(tags => tags.join(" & "))).AttachTo("extradiv") new MultiInput(
() => "Add a tag rendering",
() => new TagRenderingPanel(
)
)