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
|
@ -17,6 +17,10 @@ export class SubtleButton extends UIElement{
|
|||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
if(this.message.IsEmpty()){
|
||||
return "";
|
||||
}
|
||||
|
||||
if(this.linkTo != undefined){
|
||||
return new Combine([
|
||||
|
|
|
@ -15,13 +15,9 @@ export class TabbedComponent extends UIElement {
|
|||
this.headers.push(Translations.W(element.header).onClick(() => self._source.setData(i)));
|
||||
this.content.push(Translations.W(element.content));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let html = "";
|
||||
|
||||
let headerBar = "";
|
||||
for (let i = 0; i < this.headers.length; i++) {
|
||||
let header = this.headers[i];
|
||||
|
@ -36,10 +32,11 @@ export class TabbedComponent extends UIElement {
|
|||
headerBar = "<div class='tabs-header-bar'>" + headerBar + "</div>"
|
||||
|
||||
const content = this.content[this._source.data];
|
||||
return headerBar + "<div class='tab-content'>" + content.Render() + "</div>";
|
||||
return headerBar + "<div class='tab-content'>" + (content?.Render() ?? "") + "</div>";
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
|
||||
super.InnerUpdate(htmlElement);
|
||||
this.content[this._source.data].Update();
|
||||
}
|
||||
|
|
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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
30
UI/Img.ts
30
UI/Img.ts
File diff suppressed because one or more lines are too long
|
@ -8,7 +8,9 @@ export class DropDown<T> extends InputElement<T> {
|
|||
private readonly _label: UIElement;
|
||||
private readonly _values: { value: T; shown: UIElement }[];
|
||||
|
||||
private readonly _value : UIEventSource<T>;
|
||||
private readonly _value: UIEventSource<T>;
|
||||
|
||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
constructor(label: string | UIElement,
|
||||
values: { value: T, shown: string | UIElement }[],
|
||||
|
@ -17,8 +19,8 @@ export class DropDown<T> extends InputElement<T> {
|
|||
this._value = value ?? new UIEventSource<T>(undefined);
|
||||
this._label = Translations.W(label);
|
||||
this._values = values.map(v => {
|
||||
return {
|
||||
value: v.value,
|
||||
return {
|
||||
value: v.value,
|
||||
shown: Translations.W(v.shown)
|
||||
}
|
||||
}
|
||||
|
@ -36,14 +38,6 @@ export class DropDown<T> extends InputElement<T> {
|
|||
GetValue(): UIEventSource<T> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
ShowValue(t: T): boolean {
|
||||
if (!this.IsValid(t)) {
|
||||
return false;
|
||||
}
|
||||
this._value.setData(t);
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
for (const value of this._values) {
|
||||
if (value.value === t) {
|
||||
|
|
|
@ -4,8 +4,9 @@ import {FixedUiElement} from "../Base/FixedUiElement";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export class FixedInputElement<T> extends InputElement<T> {
|
||||
private rendering: UIElement;
|
||||
private value: UIEventSource<T>;
|
||||
private readonly rendering: UIElement;
|
||||
private readonly value: UIEventSource<T>;
|
||||
public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
constructor(rendering: UIElement | string, value: T) {
|
||||
super(undefined);
|
||||
|
@ -16,11 +17,6 @@ export class FixedInputElement<T> extends InputElement<T> {
|
|||
GetValue(): UIEventSource<T> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
ShowValue(t: T): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.rendering.Render();
|
||||
}
|
||||
|
@ -28,7 +24,14 @@ export class FixedInputElement<T> extends InputElement<T> {
|
|||
IsValid(t: T): boolean {
|
||||
return t == this.value.data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
htmlElement.addEventListener("mouseenter", () => self.IsSelected.setData(true));
|
||||
htmlElement.addEventListener("mouseout", () => self.IsSelected.setData(false))
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export abstract class InputElement<T> extends UIElement{
|
||||
|
||||
abstract GetValue() : UIEventSource<T>;
|
||||
|
||||
abstract IsSelected: UIEventSource<boolean>;
|
||||
abstract IsValid(t: T) : boolean;
|
||||
|
||||
}
|
||||
|
|
93
UI/Input/MultiLingualTextFields.ts
Normal file
93
UI/Input/MultiLingualTextFields.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {TextField} from "./TextField";
|
||||
|
||||
export default class MultiLingualTextFields extends InputElement<any> {
|
||||
private _fields: Map<string, TextField<string>> = new Map<string, TextField<string>>();
|
||||
private _value: UIEventSource<any>;
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
constructor(languages: UIEventSource<string[]>,
|
||||
textArea: boolean = false,
|
||||
value: UIEventSource<Map<string, UIEventSource<string>>> = undefined) {
|
||||
super(undefined);
|
||||
this._value = value ?? new UIEventSource({});
|
||||
const self = this;
|
||||
|
||||
function setup(languages: string[]) {
|
||||
if(languages === undefined){
|
||||
return;
|
||||
}
|
||||
const newFields = new Map<string, TextField<string>>();
|
||||
for (const language of languages) {
|
||||
if(language.length != 2){
|
||||
continue;
|
||||
}
|
||||
|
||||
let oldField = self._fields.get(language);
|
||||
if (oldField === undefined) {
|
||||
oldField = TextField.StringInput(textArea);
|
||||
oldField.GetValue().addCallback(str => {
|
||||
self._value.data[language] = str;
|
||||
self._value.ping();
|
||||
});
|
||||
oldField.GetValue().setData(self._value.data[language]);
|
||||
|
||||
oldField.IsSelected.addCallback(() => {
|
||||
let selected = false;
|
||||
self._fields.forEach(value => {selected = selected || value.IsSelected.data});
|
||||
self.IsSelected.setData(selected);
|
||||
})
|
||||
|
||||
}
|
||||
newFields.set(language, oldField);
|
||||
}
|
||||
self._fields = newFields;
|
||||
self.Update();
|
||||
|
||||
|
||||
}
|
||||
|
||||
setup(languages.data);
|
||||
languages.addCallback(setup);
|
||||
|
||||
|
||||
function load(latest: any){
|
||||
if(latest === undefined){
|
||||
return;
|
||||
}
|
||||
for (const lang in latest) {
|
||||
self._fields.get(lang)?.GetValue().setData(latest[lang]);
|
||||
}
|
||||
}
|
||||
this._value.addCallback(load);
|
||||
load(this._value.data);
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
this._fields.forEach(value => value.Update());
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<Map<string, UIEventSource<string>>> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let html = "";
|
||||
this._fields.forEach((field, lang) => {
|
||||
html += `<tr><td>${lang}</td><td>${field.Render()}</td></tr>`
|
||||
})
|
||||
if(html === ""){
|
||||
return "Please define one or more languages"
|
||||
}
|
||||
|
||||
return `<table>${html}</table>`;
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
92
UI/Input/MultiTagInput.ts
Normal file
92
UI/Input/MultiTagInput.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import TagInput from "./TagInput";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export class MultiTagInput extends InputElement<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[]>([])) {
|
||||
super(undefined);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
107
UI/Input/TagInput.ts
Normal file
107
UI/Input/TagInput.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {DropDown} from "./DropDown";
|
||||
import {TextField} from "./TextField";
|
||||
import Combine from "../Base/Combine";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class SingleTagInput extends InputElement<string> {
|
||||
|
||||
private readonly _value: UIEventSource<string>;
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
|
||||
private key: InputElement<string>;
|
||||
private value: InputElement<string>;
|
||||
private operator: DropDown<string>
|
||||
|
||||
constructor(value: UIEventSource<string> = undefined) {
|
||||
super(undefined);
|
||||
this._value = value ?? new UIEventSource<string>(undefined);
|
||||
|
||||
this.key = new TextField({
|
||||
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>({
|
||||
placeholder: "value - if blank, matches if key is NOT present",
|
||||
fromString: str => str,
|
||||
toString: str => str
|
||||
}
|
||||
);
|
||||
this.operator = new DropDown<string>("", [
|
||||
{value: "=", shown: "="},
|
||||
{value: "~", shown: "~"},
|
||||
{value: "!~", shown: "!~"}
|
||||
]);
|
||||
this.operator.GetValue().setData("=");
|
||||
|
||||
const self = this;
|
||||
|
||||
function updateValue() {
|
||||
if (self.key.GetValue().data === undefined ||
|
||||
self.value.GetValue().data === undefined ||
|
||||
self.operator.GetValue().data === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
self._value.setData(self.key.GetValue().data + self.operator.GetValue().data + self.value.GetValue().data);
|
||||
}
|
||||
|
||||
this.key.GetValue().addCallback(() => updateValue());
|
||||
this.operator.GetValue().addCallback(() => updateValue());
|
||||
this.value.GetValue().addCallback(() => updateValue());
|
||||
|
||||
|
||||
function loadValue(value: string) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
let parts: string[];
|
||||
if (value.indexOf("=") >= 0) {
|
||||
parts = Utils.SplitFirst(value, "=");
|
||||
self.operator.GetValue().setData("=");
|
||||
} else if (value.indexOf("!~") > 0) {
|
||||
parts = Utils.SplitFirst(value, "!~");
|
||||
self.operator.GetValue().setData("!~");
|
||||
|
||||
} else if (value.indexOf("~") > 0) {
|
||||
parts = Utils.SplitFirst(value, "~");
|
||||
self.operator.GetValue().setData("~");
|
||||
} else {
|
||||
console.warn("Invalid value for tag: ", value)
|
||||
return;
|
||||
}
|
||||
self.key.GetValue().setData(parts[0]);
|
||||
self.value.GetValue().setData(parts[1]);
|
||||
}
|
||||
|
||||
self._value.addCallback(loadValue);
|
||||
loadValue(self._value.data);
|
||||
this.IsSelected = this.key.IsSelected.map(
|
||||
isSelected => isSelected || this.value.IsSelected.data, [this.value.IsSelected]
|
||||
)
|
||||
}
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([
|
||||
this.key, this.operator, this.value
|
||||
]).Render();
|
||||
}
|
||||
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@ export class ValidatedTextField {
|
|||
"int": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))},
|
||||
"nat": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0},
|
||||
"float": (str) => !isNaN(Number(str)),
|
||||
"pfloat": (str) => !isNaN(Number(str)) && Number(str) > 0,
|
||||
"pfloat": (str) => !isNaN(Number(str)) && Number(str) >= 0,
|
||||
"email": (str) => EmailValidator.validate(str),
|
||||
"url": (str) => str,
|
||||
"phone": (str, country) => {
|
||||
|
@ -32,6 +32,33 @@ export class ValidatedTextField {
|
|||
|
||||
export class TextField<T> extends InputElement<T> {
|
||||
|
||||
public static StringInput(textArea: boolean = false): TextField<string> {
|
||||
return new TextField<string>({
|
||||
toString: str => str,
|
||||
fromString: str => str,
|
||||
textArea: textArea
|
||||
});
|
||||
}
|
||||
|
||||
public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{
|
||||
const isValid = ValidatedTextField.inputValidation[type];
|
||||
extraValidation = extraValidation ?? (() => true)
|
||||
return new TextField({
|
||||
fromString: str => {
|
||||
if(!isValid(str)){
|
||||
return undefined;
|
||||
}
|
||||
const n = Number(str);
|
||||
if(!extraValidation(n)){
|
||||
return undefined;
|
||||
}
|
||||
return n;
|
||||
},
|
||||
toString: num => ""+num,
|
||||
placeholder: type
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private readonly value: UIEventSource<string>;
|
||||
private readonly mappedValue: UIEventSource<T>;
|
||||
|
@ -40,7 +67,8 @@ export class TextField<T> extends InputElement<T> {
|
|||
private readonly _fromString?: (string: string) => T;
|
||||
private readonly _toString: (t: T) => string;
|
||||
private readonly startValidated: boolean;
|
||||
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _isArea: boolean;
|
||||
|
||||
constructor(options: {
|
||||
/**
|
||||
|
@ -61,11 +89,12 @@ export class TextField<T> extends InputElement<T> {
|
|||
fromString: (string: string) => T,
|
||||
value?: UIEventSource<T>,
|
||||
startValidated?: boolean,
|
||||
textArea?: boolean
|
||||
}) {
|
||||
super(undefined);
|
||||
const self = this;
|
||||
this.value = new UIEventSource<string>("");
|
||||
|
||||
this._isArea = options.textArea ?? false;
|
||||
this.mappedValue = options?.value ?? new UIEventSource<T>(undefined);
|
||||
this.mappedValue.addCallback(() => self.InnerUpdate());
|
||||
|
||||
|
@ -98,6 +127,11 @@ export class TextField<T> extends InputElement<T> {
|
|||
return this.mappedValue;
|
||||
}
|
||||
InnerRender(): string {
|
||||
|
||||
if(this._isArea){
|
||||
return `<textarea id="text-${this.id}" class="form-text-field" rows="4" cols="50" style="max-width: 100%;box-sizing: border-box"></textarea>`
|
||||
}
|
||||
|
||||
return `<form onSubmit='return false' class='form-text-field'>` +
|
||||
`<input type='text' placeholder='${this._placeholder.InnerRender()}' id='text-${this.id}'>` +
|
||||
`</form>`;
|
||||
|
@ -112,7 +146,7 @@ export class TextField<T> extends InputElement<T> {
|
|||
this.mappedValue.addCallback((data) => {
|
||||
field.className = data !== undefined ? "valid" : "invalid";
|
||||
});
|
||||
|
||||
|
||||
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";
|
||||
|
||||
const self = this;
|
||||
|
@ -121,6 +155,9 @@ export class TextField<T> extends InputElement<T> {
|
|||
self.value.setData(field.value);
|
||||
};
|
||||
|
||||
field.addEventListener("focusin", () => self.IsSelected.setData(true));
|
||||
field.addEventListener("focusout", () => self.IsSelected.setData(false));
|
||||
|
||||
field.addEventListener("keyup", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
// @ts-ignore
|
||||
|
|
|
@ -23,6 +23,10 @@ export class MoreScreen extends UIElement {
|
|||
if (layout === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if(layout.id === undefined){
|
||||
console.error("ID is undefined for layout",layout);
|
||||
return undefined;
|
||||
}
|
||||
if (layout.hideFromOverview) {
|
||||
if (State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled").data !== "true") {
|
||||
return undefined;
|
||||
|
|
|
@ -17,7 +17,7 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
*/
|
||||
public static runningFromConsole = false;
|
||||
|
||||
protected constructor(source: UIEventSource<any>) {
|
||||
protected constructor(source: UIEventSource<any> = undefined) {
|
||||
super("");
|
||||
this.id = "ui-element-" + UIElement.nextId;
|
||||
this._source = source;
|
||||
|
@ -146,6 +146,15 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
if (this.clss.indexOf(clss) < 0) {
|
||||
this.clss.push(clss);
|
||||
}
|
||||
this.Update();
|
||||
return this;
|
||||
}
|
||||
|
||||
public RemoveClass(clss: string): UIElement {
|
||||
if (this.clss.indexOf(clss) >= 0) {
|
||||
this.clss = this.clss.splice(this.clss.indexOf(clss), 1);
|
||||
}
|
||||
this.Update();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {State} from "../State";
|
|||
import {Utils} from "../Utils";
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import {SubtleButton} from "./Base/SubtleButton";
|
||||
import {InitUiElements} from "../InitUiElements";
|
||||
|
||||
/**
|
||||
* Handles and updates the user badge
|
||||
|
@ -23,7 +24,7 @@ export class UserBadge extends UIElement {
|
|||
constructor() {
|
||||
super(State.state.osmConnection.userDetails);
|
||||
this._userDetails = State.state.osmConnection.userDetails;
|
||||
this._languagePicker = Utils.CreateLanguagePicker();
|
||||
this._languagePicker = InitUiElements.CreateLanguagePicker();
|
||||
this._loginButton = Translations.t.general.loginWithOpenStreetMap
|
||||
.Clone()
|
||||
.SetClass("userbadge-login")
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {UIElement} from "../UI/UIElement";
|
||||
import {UIElement} from "./UIElement";
|
||||
import Locale from "../UI/i18n/Locale";
|
||||
import {State} from "../State";
|
||||
import {Layout} from "../Customizations/Layout";
|
||||
import Translations from "./i18n/Translations";
|
||||
import {Utils} from "../Utils";
|
||||
import Combine from "./Base/Combine";
|
||||
import {InitUiElements} from "../InitUiElements";
|
||||
|
||||
|
||||
export class WelcomeMessage extends UIElement {
|
||||
|
@ -19,7 +19,7 @@ export class WelcomeMessage extends UIElement {
|
|||
constructor() {
|
||||
super(State.state.osmConnection.userDetails);
|
||||
this.ListenTo(Locale.language);
|
||||
this.languagePicker = Utils.CreateLanguagePicker(Translations.t.general.pickLanguage);
|
||||
this.languagePicker = InitUiElements.CreateLanguagePicker(Translations.t.general.pickLanguage);
|
||||
|
||||
function fromLayout(f: (layout: Layout) => (string | UIElement)): UIElement {
|
||||
return Translations.W(f(State.state.layoutToUse.data));
|
||||
|
|
|
@ -760,14 +760,6 @@ export default class Translations {
|
|||
fr: "{name} (vend des vélos)",
|
||||
gl: "{name} (vende bicicletas)"
|
||||
}),
|
||||
},
|
||||
drinking_water: {
|
||||
title: new T({
|
||||
en: 'Drinking water',
|
||||
nl: "Drinkbaar water",
|
||||
fr: "Eau potable",
|
||||
gl: "Auga potábel"
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue