Butchering the UI framework
This commit is contained in:
parent
8d404b1ba9
commit
6415e195d1
90 changed files with 1012 additions and 3101 deletions
|
@ -1,164 +0,0 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import CheckBox from "./CheckBox";
|
||||
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
||||
import {MultiTagInput} from "./MultiTagInput";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
class AndOrConfig implements AndOrTagConfigJson {
|
||||
public and: (string | AndOrTagConfigJson)[] = undefined;
|
||||
public or: (string | AndOrTagConfigJson)[] = undefined;
|
||||
}
|
||||
|
||||
|
||||
export default class AndOrTagInput extends InputElement<AndOrTagConfigJson> {
|
||||
|
||||
private readonly _rawTags = new MultiTagInput();
|
||||
private readonly _subAndOrs: AndOrTagInput[] = [];
|
||||
private readonly _isAnd: UIEventSource<boolean> = new UIEventSource<boolean>(true);
|
||||
private readonly _isAndButton;
|
||||
private readonly _addBlock: UIElement;
|
||||
private readonly _value: UIEventSource<AndOrConfig> = new UIEventSource<AndOrConfig>(undefined);
|
||||
|
||||
public bottomLeftButton: UIElement;
|
||||
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const self = this;
|
||||
this._isAndButton = new CheckBox(
|
||||
new SubtleButton(Svg.ampersand_ui(), null).SetClass("small-button"),
|
||||
new SubtleButton(Svg.or_ui(), null).SetClass("small-button"),
|
||||
this._isAnd);
|
||||
|
||||
|
||||
this._addBlock =
|
||||
new SubtleButton(Svg.addSmall_ui(), "Add an and/or-expression")
|
||||
.SetClass("small-button")
|
||||
.onClick(() => {self.createNewBlock()});
|
||||
|
||||
|
||||
this._isAnd.addCallback(() => self.UpdateValue());
|
||||
this._rawTags.GetValue().addCallback(() => {
|
||||
self.UpdateValue()
|
||||
});
|
||||
|
||||
this.IsSelected = this._rawTags.IsSelected;
|
||||
|
||||
this._value.addCallback(tags => self.loadFromValue(tags));
|
||||
|
||||
}
|
||||
|
||||
private createNewBlock(){
|
||||
const inputEl = new AndOrTagInput();
|
||||
inputEl.GetValue().addCallback(() => this.UpdateValue());
|
||||
const deleteButton = this.createDeleteButton(inputEl.id);
|
||||
inputEl.bottomLeftButton = deleteButton;
|
||||
this._subAndOrs.push(inputEl);
|
||||
this.Update();
|
||||
}
|
||||
|
||||
private createDeleteButton(elementId: string): UIElement {
|
||||
const self = this;
|
||||
return new SubtleButton(Svg.delete_icon_ui(), null).SetClass("small-button")
|
||||
.onClick(() => {
|
||||
for (let i = 0; i < self._subAndOrs.length; i++) {
|
||||
if (self._subAndOrs[i].id === elementId) {
|
||||
self._subAndOrs.splice(i, 1);
|
||||
self.Update();
|
||||
self.UpdateValue();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private loadFromValue(value: AndOrTagConfigJson) {
|
||||
this._isAnd.setData(value.and !== undefined);
|
||||
const tags = value.and ?? value.or;
|
||||
const rawTags: string[] = [];
|
||||
const subTags: AndOrTagConfigJson[] = [];
|
||||
for (const tag of tags) {
|
||||
|
||||
if (typeof (tag) === "string") {
|
||||
rawTags.push(tag);
|
||||
} else {
|
||||
subTags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < rawTags.length; i++) {
|
||||
if (this._rawTags.GetValue().data[i] !== rawTags[i]) {
|
||||
// For some reason, 'setData' isn't stable as the comparison between the lists fails
|
||||
// Probably because we generate a new list object every timee
|
||||
// So we compare again here and update only if we find a difference
|
||||
this._rawTags.GetValue().setData(rawTags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while(this._subAndOrs.length < subTags.length){
|
||||
this.createNewBlock();
|
||||
}
|
||||
|
||||
for (let i = 0; i < subTags.length; i++){
|
||||
let subTag = subTags[i];
|
||||
this._subAndOrs[i].GetValue().setData(subTag);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private UpdateValue() {
|
||||
const tags: (string | AndOrTagConfigJson)[] = [];
|
||||
tags.push(...this._rawTags.GetValue().data);
|
||||
|
||||
for (const subAndOr of this._subAndOrs) {
|
||||
const subAndOrData = subAndOr._value.data;
|
||||
if (subAndOrData === undefined) {
|
||||
continue;
|
||||
}
|
||||
console.log(subAndOrData);
|
||||
tags.push(subAndOrData);
|
||||
}
|
||||
|
||||
const tagConfig = new AndOrConfig();
|
||||
|
||||
if (this._isAnd.data) {
|
||||
tagConfig.and = tags;
|
||||
} else {
|
||||
tagConfig.or = tags;
|
||||
}
|
||||
this._value.setData(tagConfig);
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<AndOrTagConfigJson> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const leftColumn = new Combine([
|
||||
this._isAndButton,
|
||||
"<br/>",
|
||||
this.bottomLeftButton ?? ""
|
||||
]);
|
||||
const tags = new Combine([
|
||||
this._rawTags,
|
||||
...this._subAndOrs,
|
||||
this._addBlock
|
||||
]).Render();
|
||||
return `<span class="bordered"><table><tr><td>${leftColumn.Render()}</td><td>${tags}</td></tr></table></span>`;
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: AndOrTagConfigJson): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class CheckBox extends UIElement{
|
||||
public readonly isEnabled: UIEventSource<boolean>;
|
||||
private readonly _showEnabled: UIElement;
|
||||
private readonly _showDisabled: UIElement;
|
||||
|
||||
constructor(showEnabled: string | UIElement, showDisabled: string | UIElement, data: UIEventSource<boolean> | boolean = false) {
|
||||
super(undefined);
|
||||
this.isEnabled =
|
||||
data instanceof UIEventSource ? data : new UIEventSource(data ?? false);
|
||||
this.ListenTo(this.isEnabled);
|
||||
this._showEnabled = Translations.W(showEnabled);
|
||||
this._showDisabled =Translations.W(showDisabled);
|
||||
const self = this;
|
||||
this.onClick(() => {
|
||||
self.isEnabled.setData(!self.isEnabled.data);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
if (this.isEnabled.data) {
|
||||
return Translations.W(this._showEnabled).Render();
|
||||
} else {
|
||||
return Translations.W(this._showDisabled).Render();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -16,7 +16,6 @@ export default class CheckBoxes extends InputElement<number[]> {
|
|||
constructor(elements: UIElement[]) {
|
||||
super(undefined);
|
||||
this._elements = Utils.NoNull(elements);
|
||||
this.dumbMode = false;
|
||||
|
||||
this.value = new UIEventSource<number[]>([])
|
||||
this.ListenTo(this.value);
|
||||
|
@ -51,7 +50,6 @@ export default class CheckBoxes extends InputElement<number[]> {
|
|||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
|
||||
for (let i = 0; i < this._elements.length; i++) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class ColorPicker extends InputElement<string> {
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import {UIElement} from "../UIElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class CombinedInputElement<T> extends InputElement<T> {
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._combined.ConstructElement();
|
||||
}
|
||||
private readonly _a: InputElement<T>;
|
||||
private readonly _b: UIElement;
|
||||
private readonly _combined: UIElement;
|
||||
private readonly _b: BaseUIElement;
|
||||
private readonly _combined: BaseUIElement;
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
|
||||
constructor(a: InputElement<T>, b: InputElement<T>) {
|
||||
super();
|
||||
this._a = a;
|
||||
|
@ -23,11 +25,6 @@ export default class CombinedInputElement<T> extends InputElement<T> {
|
|||
return this._a.GetValue();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._combined.Render();
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this._a.IsValid(t);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ export default class DirectionInput extends InputElement<string> {
|
|||
|
||||
constructor(value?: UIEventSource<string>) {
|
||||
super();
|
||||
this.dumbMode = false;
|
||||
this.value = value ?? new UIEventSource<string>(undefined);
|
||||
|
||||
this.value.addCallbackAndRun(rotation => {
|
||||
|
@ -48,7 +47,6 @@ export default class DirectionInput extends InputElement<string> {
|
|||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
|
||||
function onPosChange(x: number, y: number) {
|
||||
|
|
|
@ -1,50 +1,81 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class DropDown<T> extends InputElement<T> {
|
||||
|
||||
private readonly _label: UIElement;
|
||||
private readonly _values: { value: T; shown: UIElement }[];
|
||||
private static _nextDropdownId = 0;
|
||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
private readonly _element: HTMLElement;
|
||||
|
||||
private readonly _value: UIEventSource<T>;
|
||||
private readonly _values: { value: T; shown: string | BaseUIElement }[];
|
||||
|
||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _label_class: string;
|
||||
private readonly _select_class: string;
|
||||
private _form_style: string;
|
||||
|
||||
constructor(label: string | UIElement,
|
||||
values: { value: T, shown: string | UIElement }[],
|
||||
constructor(label: string | BaseUIElement,
|
||||
values: { value: T, shown: string | BaseUIElement }[],
|
||||
value: UIEventSource<T> = undefined,
|
||||
label_class: string = "",
|
||||
select_class: string = "",
|
||||
form_style: string = "flex") {
|
||||
super(undefined);
|
||||
this._form_style = form_style;
|
||||
this._value = value ?? new UIEventSource<T>(undefined);
|
||||
this._label = Translations.W(label);
|
||||
this._label_class = label_class || '';
|
||||
this._select_class = select_class || '';
|
||||
this._values = values.map(v => {
|
||||
return {
|
||||
value: v.value,
|
||||
shown: Translations.W(v.shown)
|
||||
options?: {
|
||||
select_class?: string
|
||||
}
|
||||
}
|
||||
);
|
||||
for (const v of this._values) {
|
||||
this.ListenTo(v.shown._source);
|
||||
) {
|
||||
super();
|
||||
this._values = values;
|
||||
if (values.length <= 1) {
|
||||
return;
|
||||
}
|
||||
this.ListenTo(this._value);
|
||||
|
||||
this.onClick(() => {}) // by registering a click, the click event is consumed and doesn't bubble furter to other elements, e.g. checkboxes
|
||||
const id = DropDown._nextDropdownId;
|
||||
DropDown._nextDropdownId++;
|
||||
|
||||
|
||||
const el = document.createElement("form")
|
||||
this._element = el;
|
||||
el.id = "dropdown" + id;
|
||||
|
||||
{
|
||||
const labelEl = Translations.W(label).ConstructElement()
|
||||
const labelHtml = document.createElement("label")
|
||||
labelHtml.appendChild(labelEl)
|
||||
labelHtml.htmlFor = el.id;
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
const select = document.createElement("select")
|
||||
select.classList.add(...(options?.select_class?.split(" ") ?? []))
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
|
||||
const option = document.createElement("option")
|
||||
option.value = "" + i
|
||||
option.appendChild(Translations.W(values[i].shown).ConstructElement())
|
||||
}
|
||||
|
||||
select.onchange = (() => {
|
||||
var index = select.selectedIndex;
|
||||
value.setData(values[index].value);
|
||||
});
|
||||
|
||||
value.addCallbackAndRun(selected => {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i].value;
|
||||
if (value === selected) {
|
||||
select.selectedIndex = i;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.onClick(() => {
|
||||
}) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
for (const value of this._values) {
|
||||
if (value.value === t) {
|
||||
|
@ -54,44 +85,8 @@ export class DropDown<T> extends InputElement<T> {
|
|||
return false
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
if(this._values.length <=1){
|
||||
return "";
|
||||
}
|
||||
|
||||
let options = "";
|
||||
for (let i = 0; i < this._values.length; i++) {
|
||||
options += "<option value='" + i + "'>" + this._values[i].shown.InnerRender() + "</option>"
|
||||
}
|
||||
|
||||
return `<form class="${this._form_style}">` +
|
||||
`<label class='${this._label_class}' for='dropdown-${this.id}'>${this._label.Render()}</label>` +
|
||||
`<select class='${this._select_class}' name='dropdown-${this.id}' id='dropdown-${this.id}'>` +
|
||||
options +
|
||||
`</select>` +
|
||||
`</form>`;
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
protected InnerUpdate(element) {
|
||||
var e = document.getElementById("dropdown-" + this.id);
|
||||
if(e === null){
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
e.onchange = (() => {
|
||||
// @ts-ignore
|
||||
var index = parseInt(e.selectedIndex);
|
||||
self._value.setData(self._values[index].value);
|
||||
});
|
||||
|
||||
var t = this._value.data;
|
||||
for (let i = 0; i < this._values.length ; i++) {
|
||||
const value = this._values[i].value;
|
||||
if (value === t) {
|
||||
// @ts-ignore
|
||||
e.selectedIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export abstract class InputElement<T> extends UIElement{
|
||||
export abstract class InputElement<T> extends BaseUIElement{
|
||||
|
||||
abstract GetValue() : UIEventSource<T>;
|
||||
abstract IsSelected: UIEventSource<boolean>;
|
||||
|
|
|
@ -3,13 +3,12 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
|
||||
|
||||
export default class InputElementMap<T, X> extends InputElement<X> {
|
||||
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
private readonly _inputElement: InputElement<T>;
|
||||
private isSame: (x0: X, x1: X) => boolean;
|
||||
private readonly fromX: (x: X) => T;
|
||||
private readonly toX: (t: T) => X;
|
||||
private readonly _value: UIEventSource<X>;
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
|
||||
constructor(inputElement: InputElement<T>,
|
||||
isSame: (x0: X, x1: X) => boolean,
|
||||
|
@ -41,19 +40,19 @@ export default class InputElementMap<T, X> extends InputElement<X> {
|
|||
return this._value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._inputElement.InnerRender();
|
||||
}
|
||||
|
||||
IsValid(x: X): boolean {
|
||||
if(x === undefined){
|
||||
if (x === undefined) {
|
||||
return false;
|
||||
}
|
||||
const t = this.fromX(x);
|
||||
if(t === undefined){
|
||||
if (t === undefined) {
|
||||
return false;
|
||||
}
|
||||
return this._inputElement.IsValid(t);
|
||||
}
|
||||
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._inputElement.ConstructElement();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
export class MultiInput<T> extends InputElement<T[]> {
|
||||
|
||||
private readonly _value: UIEventSource<T[]>;
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
private elements: UIElement[] = [];
|
||||
private inputElements: InputElement<T>[] = [];
|
||||
private addTag: UIElement;
|
||||
private _options: { allowMovement?: boolean };
|
||||
|
||||
constructor(
|
||||
addAElement: string,
|
||||
newElement: (() => T),
|
||||
createInput: (() => InputElement<T>),
|
||||
value: UIEventSource<T[]> = undefined,
|
||||
options?: {
|
||||
allowMovement?: boolean
|
||||
}) {
|
||||
super(undefined);
|
||||
this._value = value ?? new UIEventSource<T[]>([]);
|
||||
value = this._value;
|
||||
this.ListenTo(value.map((latest : T[]) => latest.length));
|
||||
this._options = options ?? {};
|
||||
|
||||
this.addTag = new SubtleButton(Svg.addSmall_ui(), addAElement)
|
||||
.SetClass("small-button")
|
||||
.onClick(() => {
|
||||
this.IsSelected.setData(true);
|
||||
value.data.push(newElement());
|
||||
value.ping();
|
||||
});
|
||||
const self = this;
|
||||
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements(createInput));
|
||||
this.createElements(createInput);
|
||||
|
||||
this._value.addCallback(tags => self.load(tags));
|
||||
this.IsSelected = new UIEventSource<boolean>(false);
|
||||
}
|
||||
|
||||
private load(tags: T[]) {
|
||||
if (tags === undefined) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
this.inputElements[i].GetValue().setData(tags[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private UpdateIsSelected(){
|
||||
this.IsSelected.setData(this.inputElements.map(input => input.IsSelected.data).reduce((a,b) => a && b))
|
||||
}
|
||||
|
||||
private createElements(createInput: (() => InputElement<T>)) {
|
||||
this.inputElements.splice(0, this.inputElements.length);
|
||||
this.elements = [];
|
||||
const self = this;
|
||||
for (let i = 0; i < this._value.data.length; i++) {
|
||||
const input = createInput();
|
||||
input.GetValue().addCallback(tag => {
|
||||
self._value.data[i] = tag;
|
||||
self._value.ping();
|
||||
}
|
||||
);
|
||||
this.inputElements.push(input);
|
||||
input.IsSelected.addCallback(() => this.UpdateIsSelected());
|
||||
|
||||
const moveUpBtn = Svg.up_ui()
|
||||
.SetClass('small-image').onClick(() => {
|
||||
const v = self._value.data[i];
|
||||
self._value.data[i] = self._value.data[i - 1];
|
||||
self._value.data[i - 1] = v;
|
||||
self._value.ping();
|
||||
});
|
||||
|
||||
const moveDownBtn =
|
||||
Svg.down_ui()
|
||||
.SetClass('small-image') .onClick(() => {
|
||||
const v = self._value.data[i];
|
||||
self._value.data[i] = self._value.data[i + 1];
|
||||
self._value.data[i + 1] = v;
|
||||
self._value.ping();
|
||||
});
|
||||
|
||||
const controls = [];
|
||||
if (i > 0 && this._options.allowMovement) {
|
||||
controls.push(moveUpBtn);
|
||||
}
|
||||
|
||||
if (i + 1 < this._value.data.length && this._options.allowMovement) {
|
||||
controls.push(moveDownBtn);
|
||||
}
|
||||
|
||||
|
||||
const deleteBtn =
|
||||
Svg.delete_icon_ui().SetClass('small-image')
|
||||
.onClick(() => {
|
||||
self._value.data.splice(i, 1);
|
||||
self._value.ping();
|
||||
});
|
||||
controls.push(deleteBtn);
|
||||
this.elements.push(new Combine([input.SetStyle("width: calc(100% - 2em - 5px)"), new Combine(controls).SetStyle("display:flex;flex-direction:column;width:min-content;")]).SetClass("tag-input-row"))
|
||||
}
|
||||
|
||||
this.Update();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([...this.elements, this.addTag]).Render();
|
||||
}
|
||||
|
||||
IsValid(t: T[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T[]> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
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> = new Map<string, TextField>();
|
||||
private readonly _value: UIEventSource<any>;
|
||||
public readonly 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({});
|
||||
this._value.addCallbackAndRun(latestData => {
|
||||
if (typeof (latestData) === "string") {
|
||||
console.warn("Refusing string for multilingual input", latestData);
|
||||
self._value.setData({});
|
||||
}
|
||||
})
|
||||
|
||||
const self = this;
|
||||
|
||||
function setup(languages: string[]) {
|
||||
if (languages === undefined) {
|
||||
return;
|
||||
}
|
||||
const newFields = new Map<string, TextField>();
|
||||
for (const language of languages) {
|
||||
if (language.length != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let oldField = self._fields.get(language);
|
||||
if (oldField === undefined) {
|
||||
oldField = new TextField({textArea: 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import TagInput from "./SingleTagInput";
|
||||
import {MultiInput} from "./MultiInput";
|
||||
|
||||
export class MultiTagInput extends MultiInput<string> {
|
||||
|
||||
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) {
|
||||
super("Add a new tag",
|
||||
() => "",
|
||||
() => new TagInput(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export class NumberField extends InputElement<number> {
|
||||
private readonly value: UIEventSource<number>;
|
||||
public readonly enterPressed = new UIEventSource<string>(undefined);
|
||||
private readonly _placeholder: UIElement;
|
||||
private options?: {
|
||||
placeholder?: string | UIElement,
|
||||
value?: UIEventSource<number>,
|
||||
isValid?: ((i: number) => boolean),
|
||||
min?: number,
|
||||
max?: number
|
||||
};
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _isValid: (i:number) => boolean;
|
||||
|
||||
constructor(options?: {
|
||||
placeholder?: string | UIElement,
|
||||
value?: UIEventSource<number>,
|
||||
isValid?: ((i:number) => boolean),
|
||||
min?: number,
|
||||
max?:number
|
||||
}) {
|
||||
super(undefined);
|
||||
this.options = options;
|
||||
const self = this;
|
||||
this.value = new UIEventSource<number>(undefined);
|
||||
this.value = options?.value ?? new UIEventSource<number>(undefined);
|
||||
|
||||
this._isValid = options.isValid ?? ((i) => true);
|
||||
|
||||
this._placeholder = Translations.W(options.placeholder ?? "");
|
||||
this.ListenTo(this._placeholder._source);
|
||||
|
||||
this.onClick(() => {
|
||||
self.IsSelected.setData(true)
|
||||
});
|
||||
this.value.addCallback((t) => {
|
||||
const field = document.getElementById("txt-"+this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
field.className = self.IsValid(t) ? "" : "invalid";
|
||||
|
||||
if (t === undefined || t === null) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
field.value = t;
|
||||
});
|
||||
this.dumbMode = false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<number> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
const placeholder = this._placeholder.InnerRender().replace("'", "'");
|
||||
|
||||
let min = "";
|
||||
if(this.options.min){
|
||||
min = `min='${this.options.min}'`;
|
||||
}
|
||||
|
||||
let max = "";
|
||||
if(this.options.min){
|
||||
max = `max='${this.options.max}'`;
|
||||
}
|
||||
|
||||
return `<span id="${this.id}"><form onSubmit='return false' class='form-text-field'>` +
|
||||
`<input type='number' ${min} ${max} placeholder='${placeholder}' id='txt-${this.id}'>` +
|
||||
`</form></span>`;
|
||||
}
|
||||
|
||||
InnerUpdate() {
|
||||
const field = document.getElementById("txt-" + this.id);
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
|
||||
// How much characters are on the right, not including spaces?
|
||||
// @ts-ignore
|
||||
const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length;
|
||||
// @ts-ignore
|
||||
let val: number = Number(field.value);
|
||||
if (!self.IsValid(val)) {
|
||||
self.value.setData(undefined);
|
||||
} else {
|
||||
self.value.setData(val);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (this.value.data !== undefined && this.value.data !== null) {
|
||||
// @ts-ignore
|
||||
field.value = this.value.data;
|
||||
}
|
||||
|
||||
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
|
||||
self.enterPressed.setData(field.value);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
IsValid(t: number): boolean {
|
||||
if (t === undefined || t === null) {
|
||||
return false
|
||||
}
|
||||
return this._isValid(t);
|
||||
}
|
||||
|
||||
}
|
|
@ -5,48 +5,37 @@ export default class SimpleDatePicker extends InputElement<string> {
|
|||
|
||||
private readonly value: UIEventSource<string>
|
||||
|
||||
private readonly _element: HTMLElement;
|
||||
|
||||
constructor(
|
||||
value?: UIEventSource<string>
|
||||
) {
|
||||
super();
|
||||
this.value = value ?? new UIEventSource<string>(undefined);
|
||||
const self = this;
|
||||
|
||||
const el = document.createElement("input")
|
||||
this._element = el;
|
||||
el.type = "date"
|
||||
el.oninput = () => {
|
||||
// Already in YYYY-MM-DD value!
|
||||
self.value.setData(el.value);
|
||||
}
|
||||
|
||||
|
||||
this.value.addCallbackAndRun(v => {
|
||||
if(v === undefined){
|
||||
return;
|
||||
}
|
||||
self.SetValue(v);
|
||||
el.value = v;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return `<span id="${this.id}"><input type='date' id='date-${this.id}'></span>`;
|
||||
}
|
||||
|
||||
private SetValue(date: string){
|
||||
const field = document.getElementById("date-" + this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
field.value = date;
|
||||
}
|
||||
|
||||
protected InnerUpdate() {
|
||||
const field = document.getElementById("date-" + this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
// Already in YYYY-MM-DD value!
|
||||
// @ts-ignore
|
||||
self.value.setData(field.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element
|
||||
}
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.value;
|
||||
}
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
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";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FromJSON} from "../../Customizations/JSON/FromJSON";
|
||||
import ValidatedTextField from "./ValidatedTextField";
|
||||
|
||||
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>
|
||||
private readonly helpMessage: UIElement;
|
||||
|
||||
constructor(value: UIEventSource<string> = undefined) {
|
||||
super(undefined);
|
||||
this._value = value ?? new UIEventSource<string>("");
|
||||
this.helpMessage = new VariableUiElement(this._value.map(tagDef => {
|
||||
try {
|
||||
FromJSON.Tag(tagDef, "");
|
||||
return "";
|
||||
} catch (e) {
|
||||
return `<br/><span class='alert'>${e}</span>`
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
this.key = ValidatedTextField.KeyInput();
|
||||
|
||||
this.value = new TextField({
|
||||
placeholder: "value - if blank, matches if key is NOT present",
|
||||
value: new UIEventSource<string>("")
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
this.helpMessage
|
||||
]).Render();
|
||||
}
|
||||
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,99 +1,85 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class TextField extends InputElement<string> {
|
||||
private readonly value: UIEventSource<string>;
|
||||
public readonly enterPressed = new UIEventSource<string>(undefined);
|
||||
private readonly _placeholder: UIElement;
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _htmlType: string;
|
||||
private readonly _inputMode : string;
|
||||
private readonly _textAreaRows: number;
|
||||
|
||||
private readonly _isValid: (string,country) => boolean;
|
||||
private _label: UIElement;
|
||||
|
||||
private _element: HTMLElement;
|
||||
private readonly _isValid: (s: string, country?: () => string) => boolean;
|
||||
|
||||
constructor(options?: {
|
||||
placeholder?: string | UIElement,
|
||||
placeholder?: string | BaseUIElement,
|
||||
value?: UIEventSource<string>,
|
||||
textArea?: boolean,
|
||||
htmlType?: string,
|
||||
inputMode?: string,
|
||||
label?: UIElement,
|
||||
label?: BaseUIElement,
|
||||
textAreaRows?: number,
|
||||
isValid?: ((s: string, country?: () => string) => boolean)
|
||||
}) {
|
||||
super(undefined);
|
||||
super();
|
||||
const self = this;
|
||||
this.value = new UIEventSource<string>("");
|
||||
options = options ?? {};
|
||||
this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text");
|
||||
this.value = options?.value ?? new UIEventSource<string>(undefined);
|
||||
|
||||
this._label = options.label;
|
||||
this._textAreaRows = options.textAreaRows;
|
||||
this._isValid = options.isValid ?? ((str, country) => true);
|
||||
|
||||
this._placeholder = Translations.W(options.placeholder ?? "");
|
||||
this._inputMode = options.inputMode;
|
||||
this.ListenTo(this._placeholder._source);
|
||||
|
||||
this._isValid = options.isValid ?? (_ => true);
|
||||
|
||||
this.onClick(() => {
|
||||
self.IsSelected.setData(true)
|
||||
});
|
||||
this.value.addCallback((t) => {
|
||||
const field = document.getElementById("txt-"+this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
field.className = self.IsValid(t) ? "" : "invalid";
|
||||
|
||||
if (t === undefined || t === null) {
|
||||
|
||||
|
||||
const placeholder = Translations.W(options. placeholder ?? "").ConstructElement().innerText.replace("'", "'");
|
||||
|
||||
this.SetClass("form-text-field")
|
||||
let inputEl : HTMLElement
|
||||
if(options.htmlType === "area"){
|
||||
const el = document.createElement("textarea")
|
||||
el.placeholder = placeholder
|
||||
el.rows = options.textAreaRows
|
||||
el.cols = 50
|
||||
el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box"
|
||||
inputEl = el;
|
||||
}else{
|
||||
const el = document.createElement("input")
|
||||
el.type = options.htmlType
|
||||
el.inputMode = options.inputMode
|
||||
el.placeholder = placeholder
|
||||
inputEl = el
|
||||
}
|
||||
|
||||
const form = document.createElement("form")
|
||||
form.onsubmit = () => false;
|
||||
|
||||
if(options.label){
|
||||
form.appendChild(options.label.ConstructElement())
|
||||
}
|
||||
|
||||
this._element = form;
|
||||
|
||||
const field = inputEl;
|
||||
|
||||
|
||||
this.value.addCallbackAndRun(value => {
|
||||
if (!(value !== undefined && value !== null)) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
field.value = t;
|
||||
});
|
||||
this.dumbMode = false;
|
||||
}
|
||||
field.value = value;
|
||||
if(self.IsValid(value)){
|
||||
self.RemoveClass("invalid")
|
||||
}else{
|
||||
self.SetClass("invalid")
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.value;
|
||||
}
|
||||
})
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
const placeholder = this._placeholder.InnerRender().replace("'", "'");
|
||||
if (this._htmlType === "area") {
|
||||
return `<span id="${this.id}"><textarea id="txt-${this.id}" placeholder='${placeholder}' class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea></span>`
|
||||
}
|
||||
|
||||
let label = "";
|
||||
if (this._label != undefined) {
|
||||
label = this._label.Render();
|
||||
}
|
||||
let inputMode = ""
|
||||
if(this._inputMode !== undefined){
|
||||
inputMode = `inputmode="${this._inputMode}" `
|
||||
}
|
||||
return new Combine([
|
||||
`<span id="${this.id}">`,
|
||||
`<form onSubmit='return false' class='form-text-field'>`,
|
||||
label,
|
||||
`<input type='${this._htmlType}' ${inputMode} placeholder='${placeholder}' id='txt-${this.id}'/>`,
|
||||
`</form>`,
|
||||
`</span>`
|
||||
]).Render();
|
||||
}
|
||||
|
||||
InnerUpdate() {
|
||||
const field = document.getElementById("txt-" + this.id);
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
|
||||
|
||||
// How much characters are on the right, not including spaces?
|
||||
// @ts-ignore
|
||||
const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length;
|
||||
|
@ -107,11 +93,11 @@ export class TextField extends InputElement<string> {
|
|||
// Setting the value might cause the value to be set again. We keep the distance _to the end_ stable, as phone number formatting might cause the start to change
|
||||
// See https://github.com/pietervdvn/MapComplete/issues/103
|
||||
// We reread the field value - it might have changed!
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
val = field.value;
|
||||
let newCursorPos = val.length - endDistance;
|
||||
while(newCursorPos >= 0 &&
|
||||
while(newCursorPos >= 0 &&
|
||||
// We count the number of _actual_ characters (non-space characters) on the right of the new value
|
||||
// This count should become bigger then the end distance
|
||||
val.substr(newCursorPos).replace(/ /g, '').length < endDistance
|
||||
|
@ -119,14 +105,10 @@ export class TextField extends InputElement<string> {
|
|||
newCursorPos --;
|
||||
}
|
||||
// @ts-ignore
|
||||
self.SetCursorPosition(newCursorPos);
|
||||
TextField.SetCursorPosition(newCursorPos);
|
||||
};
|
||||
|
||||
if (this.value.data !== undefined && this.value.data !== null) {
|
||||
// @ts-ignore
|
||||
field.value = this.value.data;
|
||||
}
|
||||
|
||||
|
||||
field.addEventListener("focusin", () => self.IsSelected.setData(true));
|
||||
field.addEventListener("focusout", () => self.IsSelected.setData(false));
|
||||
|
||||
|
@ -136,22 +118,31 @@ export class TextField extends InputElement<string> {
|
|||
// @ts-ignore
|
||||
self.enterPressed.setData(field.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public SetCursorPosition(i: number) {
|
||||
const field = document.getElementById('txt-' + this.id);
|
||||
if(field === undefined || field === null){
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
private static SetCursorPosition(textfield: HTMLElement, i: number) {
|
||||
if(textfield === undefined || textfield === null){
|
||||
return;
|
||||
}
|
||||
if (i === -1) {
|
||||
// @ts-ignore
|
||||
i = field.value.length;
|
||||
i = textfield.value.length;
|
||||
}
|
||||
field.focus();
|
||||
textfield.focus();
|
||||
// @ts-ignore
|
||||
field.setSelectionRange(i, i);
|
||||
textfield.setSelectionRange(i, i);
|
||||
|
||||
}
|
||||
|
||||
|
|
22
UI/Input/Toggle.ts
Normal file
22
UI/Input/Toggle.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
/**
|
||||
* The 'Toggle' is a UIElement showing either one of two elements, depending on the state.
|
||||
* It can be used to implement e.g. checkboxes or collapsible elements
|
||||
*/
|
||||
export default class Toggle extends VariableUiElement{
|
||||
|
||||
public readonly isEnabled: UIEventSource<boolean>;
|
||||
|
||||
constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, data: UIEventSource<boolean> = new UIEventSource<boolean>(false)) {
|
||||
super(
|
||||
data.map(isEnabled => isEnabled ? showEnabled : showDisabled)
|
||||
);
|
||||
this.onClick(() => {
|
||||
data.setData(!data.data);
|
||||
})
|
||||
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue