forked from MapComplete/MapComplete
First steps for a decent custom theme generator
This commit is contained in:
parent
a57b7d93fa
commit
2052976909
82 changed files with 1880 additions and 1311 deletions
73
UI/CustomGenerator/AllLayersPanel.ts
Normal file
73
UI/CustomGenerator/AllLayersPanel.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
|
||||
import LayerPanel from "./LayerPanel";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
|
||||
export default class AllLayersPanel extends UIElement {
|
||||
|
||||
|
||||
private panel: UIElement;
|
||||
private _config: UIEventSource<LayoutConfigJson>;
|
||||
private _currentlySelected: UIEventSource<SingleSetting<any>>;
|
||||
private languages: UIEventSource<string[]>;
|
||||
|
||||
private static createEmptyLayer(): LayerConfigJson {
|
||||
return {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
minzoom: 0,
|
||||
overpassTags: undefined,
|
||||
title: undefined,
|
||||
description: {}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>, currentlySelected: UIEventSource<SingleSetting<any>>,
|
||||
languages: UIEventSource<any>) {
|
||||
super(undefined);
|
||||
this._config = config;
|
||||
this._currentlySelected = currentlySelected;
|
||||
this.languages = languages;
|
||||
|
||||
this.createPanels();
|
||||
const self = this;
|
||||
config.map<number>(config => config.layers.length).addCallback(() => self.createPanels());
|
||||
|
||||
}
|
||||
|
||||
|
||||
private createPanels() {
|
||||
const self = this;
|
||||
const tabs = [];
|
||||
|
||||
const layers = this._config.data.layers;
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
tabs.push({
|
||||
header: "<img src='./assets/bug.svg'>",
|
||||
content: new LayerPanel(this._config, this.languages, i, this._currentlySelected)
|
||||
});
|
||||
}
|
||||
tabs.push({
|
||||
header: "<img src='./assets/add.svg'>",
|
||||
content: new SubtleButton(
|
||||
"./assets/add.svg",
|
||||
"Add a new layer"
|
||||
).onClick(() => {
|
||||
self._config.data.layers.push(AllLayersPanel.createEmptyLayer())
|
||||
self._config.ping();
|
||||
})
|
||||
})
|
||||
|
||||
this.panel = new TabbedComponent(tabs, new UIEventSource<number>(Math.max(0, layers.length-1)));
|
||||
this.Update();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.panel.Render();
|
||||
}
|
||||
|
||||
}
|
0
UI/CustomGenerator/CustomGeneratorPanel.ts
Normal file
0
UI/CustomGenerator/CustomGeneratorPanel.ts
Normal file
84
UI/CustomGenerator/GeneralSettings.ts
Normal file
84
UI/CustomGenerator/GeneralSettings.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import Combine from "../Base/Combine";
|
||||
import SettingsTable from "./SettingsTable";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
||||
|
||||
|
||||
export default class GeneralSettingsPanel extends UIElement {
|
||||
private panel: Combine;
|
||||
|
||||
public languages : UIEventSource<string[]>;
|
||||
|
||||
constructor(configuration: UIEventSource<LayoutConfigJson>, currentSetting: UIEventSource<SingleSetting<any>>) {
|
||||
super(undefined);
|
||||
|
||||
|
||||
const languagesField = new TextField<string[]>(
|
||||
{
|
||||
fromString: str => str?.split(";")?.map(str => str.trim().toLowerCase()),
|
||||
toString: languages => languages.join(";"),
|
||||
}
|
||||
);
|
||||
this.languages = languagesField.GetValue();
|
||||
|
||||
const version = TextField.StringInput();
|
||||
const current_datetime = new Date();
|
||||
let formatted_date = current_datetime.getFullYear() + "-" + (current_datetime.getMonth() + 1) + "-" + current_datetime.getDate() + " " + current_datetime.getHours() + ":" + current_datetime.getMinutes() + ":" + current_datetime.getSeconds()
|
||||
version.GetValue().setData(formatted_date);
|
||||
|
||||
|
||||
const locationRemark = "<br/>Note that, as soon as an URL-parameter sets the location or a location is known due to a previous visit, that the theme-set location is ignored"
|
||||
|
||||
const settingsTable = new SettingsTable(
|
||||
[
|
||||
new SingleSetting(configuration, TextField.StringInput(), "id",
|
||||
"Identifier", "The identifier of this theme. This should be a lowercase, unique string"),
|
||||
new SingleSetting(configuration, version, "version", "Version",
|
||||
"A version to indicate the theme version. Ideal is the date you created or updated the theme"),
|
||||
new SingleSetting(configuration, languagesField, "language",
|
||||
"Supported languages", "Which languages do you want to support in this theme? Type the two letter code representing your language, seperated by <span class='literal-code'>;</span>. For example:<span class='literal-code'>en;nl</span> "),
|
||||
new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "title",
|
||||
"Title", "The title as shown in the welcome message, in the browser title bar, in the more screen, ..."),
|
||||
new SingleSetting(configuration, new MultiLingualTextFields(this.languages, true),
|
||||
"description", "Description", "The description is shown in the welcomemessage. It is a small text welcoming users"),
|
||||
new SingleSetting(configuration, TextField.StringInput(), "icon",
|
||||
"Icon", "A visual representation for your theme; used as logo in the welcomeMessage. If your theme is official, used as favicon and webapp logo",
|
||||
{
|
||||
showIconPreview: true
|
||||
}),
|
||||
|
||||
new SingleSetting(configuration, TextField.NumberInput("nat", n => n < 23), "startZoom","Initial zoom level",
|
||||
"When a user first loads MapComplete, this zoomlevel is shown."+locationRemark),
|
||||
new SingleSetting(configuration, TextField.NumberInput("float", n => (n < 90 && n > -90)), "startLat","Initial latitude",
|
||||
"When a user first loads MapComplete, this latitude is shown as location."+locationRemark),
|
||||
new SingleSetting(configuration, TextField.NumberInput("float", n => (n < 180 && n > -180)), "startLon","Initial longitude",
|
||||
"When a user first loads MapComplete, this longitude is shown as location."+locationRemark),
|
||||
|
||||
new SingleSetting(configuration, TextField.NumberInput("pfloat", n => (n < 0.5 )), "widenFactor","Query widening",
|
||||
"When a query is run, the data within bounds of the visible map is loaded.\n" +
|
||||
"However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data.\n" +
|
||||
"For this, the bounds are widened in order to make a small pan still within bounds of the loaded data.\n" +
|
||||
"IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries)"),
|
||||
|
||||
new SingleSetting(configuration, TextField.StringInput(), "socialImage",
|
||||
"og:image (aka Social Image)", "<span class='alert'>Only works on incorporated themes</span>" +
|
||||
"The Social Image is set as og:image for the HTML-site and helps social networks to show a preview", {showIconPreview: true})
|
||||
], currentSetting);
|
||||
|
||||
this.panel = new Combine([
|
||||
"<h3>General theme settings</h3>",
|
||||
settingsTable
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return this.panel.Render();
|
||||
}
|
||||
|
||||
|
||||
}
|
87
UI/CustomGenerator/LayerPanel.ts
Normal file
87
UI/CustomGenerator/LayerPanel.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import SettingsTable from "./SettingsTable";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Combine from "../Base/Combine";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
||||
import {CheckBox} from "../Input/CheckBox";
|
||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
||||
|
||||
/**
|
||||
* Shows the configuration for a single layer
|
||||
*/
|
||||
export default class LayerPanel extends UIElement {
|
||||
private _config: UIEventSource<LayoutConfigJson>;
|
||||
|
||||
private settingsTable: UIElement;
|
||||
|
||||
private deleteButton: UIElement;
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
||||
languages: UIEventSource<string[]>,
|
||||
index: number,
|
||||
currentlySelected: UIEventSource<SingleSetting<any>>) {
|
||||
super(undefined);
|
||||
this._config = config;
|
||||
|
||||
const actualDeleteButton = new SubtleButton(
|
||||
"./assets/delete.svg",
|
||||
"Yes, delete this layer"
|
||||
).onClick(() => {
|
||||
config.data.layers.splice(index, 1);
|
||||
config.ping();
|
||||
});
|
||||
|
||||
this.deleteButton = new CheckBox(
|
||||
new Combine(
|
||||
[
|
||||
"<h3>Confirm layer deletion</h3>",
|
||||
new SubtleButton(
|
||||
"./assets/close.svg",
|
||||
"No, don't delete"
|
||||
),
|
||||
"<span class='alert'>Deleting a layer can not be undone!</span>",
|
||||
actualDeleteButton
|
||||
]
|
||||
),
|
||||
new SubtleButton(
|
||||
"./assets/delete.svg",
|
||||
"Remove this layer"
|
||||
)
|
||||
)
|
||||
|
||||
function setting(input: InputElement<any>, path: string | string[], name: string, description: string | UIElement): SingleSetting<any> {
|
||||
let pathPre = ["layers", index];
|
||||
if (typeof (path) === "string") {
|
||||
pathPre.push(path);
|
||||
} else {
|
||||
pathPre = pathPre.concat(path);
|
||||
}
|
||||
|
||||
return new SingleSetting<any>(config, input, pathPre, name, description);
|
||||
}
|
||||
|
||||
|
||||
this.settingsTable = new SettingsTable([
|
||||
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, 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",
|
||||
new Combine(["The tags to load from overpass. ", MultiTagInput.tagExplanation]))
|
||||
],
|
||||
currentlySelected
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([
|
||||
this.settingsTable,
|
||||
this.deleteButton
|
||||
]).Render();
|
||||
}
|
||||
}
|
46
UI/CustomGenerator/SettingsTable.ts
Normal file
46
UI/CustomGenerator/SettingsTable.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import SingleSetting from "./SingleSetting";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
export default class SettingsTable extends UIElement {
|
||||
|
||||
private _col1: UIElement[] = [];
|
||||
private _col2: InputElement<any>[] = [];
|
||||
|
||||
public selectedSetting: UIEventSource<SingleSetting<any>>;
|
||||
|
||||
constructor(elements: SingleSetting<any>[],
|
||||
currentSelectedSetting: UIEventSource<SingleSetting<any>>) {
|
||||
super(undefined);
|
||||
const self = this;
|
||||
this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined);
|
||||
for (const element of elements) {
|
||||
let title: UIElement = new FixedUiElement(element._name);
|
||||
this._col1.push(title);
|
||||
this._col2.push(element._value);
|
||||
element._value.IsSelected.addCallback(isSelected => {
|
||||
if (isSelected) {
|
||||
self.selectedSetting.setData(element);
|
||||
} else if (self.selectedSetting.data === element) {
|
||||
self.selectedSetting.setData(undefined);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let html = "";
|
||||
|
||||
for (let i = 0; i < this._col1.length; i++) {
|
||||
html += `<tr><td>${this._col1[i].Render()}</td><td>${this._col2[i].Render()}</td></tr>`
|
||||
}
|
||||
|
||||
return `<table><tr>${html}</tr></table>`;
|
||||
}
|
||||
|
||||
}
|
39
UI/CustomGenerator/SharePanel.ts
Normal file
39
UI/CustomGenerator/SharePanel.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import Combine from "../Base/Combine";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
export default class SharePanel extends UIElement {
|
||||
private _config: UIEventSource<LayoutConfigJson>;
|
||||
|
||||
private _panel: UIElement;
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>, liveUrl: UIEventSource<string>) {
|
||||
super(undefined);
|
||||
this._config = config;
|
||||
|
||||
const json = new VariableUiElement(config.map(config => {
|
||||
return JSON.stringify(config, null, 2)
|
||||
.replace(/\n/g, "<br/>")
|
||||
.replace(/ /g, " ");
|
||||
}));
|
||||
|
||||
|
||||
this._panel = new Combine([
|
||||
"<h2>share</h2>",
|
||||
"Share the following link with friends:<br/>",
|
||||
new VariableUiElement(liveUrl.map(url => `<a href='${url}' target="_blank">${url}</a>`)),
|
||||
"<h3>Json</h3>",
|
||||
"The json configuration is included for debugging purposes",
|
||||
"<div class='literal-code json'>",
|
||||
json,
|
||||
"</div>"
|
||||
]);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._panel.Render();
|
||||
}
|
||||
|
||||
}
|
84
UI/CustomGenerator/SingleSetting.ts
Normal file
84
UI/CustomGenerator/SingleSetting.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Combine from "../Base/Combine";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
export default class SingleSetting<T> {
|
||||
public _value: InputElement<T>;
|
||||
public _name: string;
|
||||
public _description: UIElement;
|
||||
public _options: { showIconPreview?: boolean };
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
||||
value: InputElement<T>,
|
||||
path: string | (string | number)[],
|
||||
name: string,
|
||||
description: string | UIElement,
|
||||
options?: {
|
||||
showIconPreview?: boolean
|
||||
}
|
||||
) {
|
||||
this._value = value;
|
||||
this._name = name;
|
||||
this._description = Translations.W(description);
|
||||
|
||||
this._options = options ?? {};
|
||||
if (this._options.showIconPreview) {
|
||||
this._description = new Combine([
|
||||
this._description,
|
||||
"<h3>Icon preview</h3>",
|
||||
new VariableUiElement(this._value.GetValue().map(url => `<img src='${url}' class="image-large-preview">`))
|
||||
]);
|
||||
}
|
||||
|
||||
if(typeof (path) === "string"){
|
||||
path = [path];
|
||||
}
|
||||
const lastPart = path[path.length - 1];
|
||||
path.splice(path.length - 1, 1);
|
||||
|
||||
function assignValue(value) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
// We have to rewalk every time as parts might be new
|
||||
let configPart = config.data;
|
||||
for (const pathPart of path) {
|
||||
configPart = configPart[pathPart];
|
||||
if (configPart === undefined) {
|
||||
console.warn("Lost the way for path ", path)
|
||||
return;
|
||||
}
|
||||
}
|
||||
configPart[lastPart] = value;
|
||||
config.ping();
|
||||
}
|
||||
|
||||
function loadValue() {
|
||||
let configPart = config.data;
|
||||
for (const pathPart of path) {
|
||||
configPart = configPart[pathPart];
|
||||
if (configPart === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const loadedValue = configPart[lastPart];
|
||||
|
||||
if (loadedValue !== undefined) {
|
||||
value.GetValue().setData(loadedValue);
|
||||
}
|
||||
}
|
||||
loadValue();
|
||||
config.addCallback(() => loadValue());
|
||||
|
||||
value.GetValue().addCallback(assignValue);
|
||||
assignValue(this._value.GetValue().data);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue