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
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue