Butchering the UI framework

This commit is contained in:
Pieter Vander Vennet 2021-06-10 01:36:20 +02:00
parent 8d404b1ba9
commit 6415e195d1
90 changed files with 1012 additions and 3101 deletions

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {Utils} from "../../Utils";
export default class ColorPicker extends InputElement<string> {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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("'", "&#39");
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);
}
}

View file

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

View file

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

View file

@ -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("'", "&#39");
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("'", "&#39");
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
View 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);
})
}
}