First steps for a decent custom theme generator

This commit is contained in:
Pieter Vander Vennet 2020-08-31 02:59:47 +02:00
parent a57b7d93fa
commit 2052976909
82 changed files with 1880 additions and 1311 deletions

View file

@ -17,6 +17,10 @@ export class SubtleButton extends UIElement{
}
InnerRender(): string {
if(this.message.IsEmpty()){
return "";
}
if(this.linkTo != undefined){
return new Combine([

View file

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

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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>`;
}
}

View 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, "&nbsp;");
}));
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();
}
}

View 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);
}
}

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
})
}
},