forked from MapComplete/MapComplete
Way to much fixes and improvements
This commit is contained in:
parent
e68d9d99a5
commit
5ed0bb431c
41 changed files with 1244 additions and 402 deletions
|
@ -3,88 +3,162 @@ 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";
|
||||
import {CheckBox} from "./CheckBox";
|
||||
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
||||
import {MultiTagInput} from "./MultiTagInput";
|
||||
import {FormatNumberOptions} from "libphonenumber-js";
|
||||
|
||||
export class AndOrTagInput extends InputElement<(string | AndOrTagInput)[]> {
|
||||
class AndOrConfig implements AndOrTagConfigJson {
|
||||
public and: (string | AndOrTagConfigJson)[] = undefined;
|
||||
public or: (string | AndOrTagConfigJson)[] = undefined;
|
||||
}
|
||||
|
||||
|
||||
private readonly _value: UIEventSource<string[]>;
|
||||
export 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>;
|
||||
private elements: UIElement[] = [];
|
||||
private inputELements: (InputElement<string> | InputElement<AndOrTagInput>)[] = [];
|
||||
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();
|
||||
});
|
||||
constructor() {
|
||||
super();
|
||||
const self = this;
|
||||
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements());
|
||||
this.createElements();
|
||||
this._isAndButton = new CheckBox(
|
||||
new SubtleButton("./assets/ampersand.svg", null).SetClass("small-button"),
|
||||
new SubtleButton("./assets/or.svg", null).SetClass("small-button"),
|
||||
this._isAnd);
|
||||
|
||||
|
||||
this._value.addCallback(tags => self.load(tags));
|
||||
this.IsSelected = new UIEventSource<boolean>(false);
|
||||
}
|
||||
this._addBlock =
|
||||
new SubtleButton("./assets/addSmall.svg", "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 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"))
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([...this.elements, this.addTag]).SetClass("bordered").Render();
|
||||
private createDeleteButton(elementId: string): UIElement {
|
||||
const self = this;
|
||||
return new SubtleButton("./assets/delete.svg", 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);
|
||||
|
||||
}
|
||||
|
||||
IsValid(t: string[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string[]> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
89
UI/Input/MultiInput.ts
Normal file
89
UI/Input/MultiInput.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
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;
|
||||
|
||||
constructor(
|
||||
addAElement: string,
|
||||
newElement: (() => T),
|
||||
createInput: (() => InputElement<T>),
|
||||
value: UIEventSource<T[]> = new UIEventSource<T[]>([])) {
|
||||
super(undefined);
|
||||
this._value = value;
|
||||
|
||||
this.addTag = new SubtleButton("./assets/addSmall.svg", 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++) {
|
||||
let tag = this._value.data[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 deleteBtn = new FixedUiElement("<img src='./assets/delete.svg' style='max-width: 1.5em; margin-left: 5px;'>")
|
||||
.onClick(() => {
|
||||
self._value.data.splice(i, 1);
|
||||
self._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]).Render();
|
||||
}
|
||||
|
||||
IsValid(t: T[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T[]> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
}
|
|
@ -5,88 +5,17 @@ import Combine from "../Base/Combine";
|
|||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import TagInput from "./TagInput";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {MultiInput} from "./MultiInput";
|
||||
|
||||
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;
|
||||
export class MultiTagInput extends MultiInput<string> {
|
||||
|
||||
|
||||
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;
|
||||
super("Add a new tag",
|
||||
() => "",
|
||||
() => new TagInput(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@ import {InputElement} from "./InputElement";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export class RadioButton<T> extends InputElement<T> {
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
private readonly _selectedElementIndex: UIEventSource<number>
|
||||
= new UIEventSource<number>(null);
|
||||
|
@ -26,16 +27,16 @@ export class RadioButton<T> extends InputElement<T> {
|
|||
return elements[selectedIndex].GetValue()
|
||||
}
|
||||
}
|
||||
), elements.map(e => e.GetValue()));
|
||||
), elements.map(e => e?.GetValue()));
|
||||
|
||||
this.value.addCallback((t) => {
|
||||
self.ShowValue(t);
|
||||
self?.ShowValue(t);
|
||||
})
|
||||
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
// If an element is clicked, the radio button corresponding with it should be selected as well
|
||||
elements[i].onClick(() => {
|
||||
elements[i]?.onClick(() => {
|
||||
self._selectedElementIndex.setData(i);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,16 +18,7 @@ export default class SingleTagInput extends InputElement<string> {
|
|||
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.key = TextField.KeyInput();
|
||||
|
||||
this.value = new TextField<string>({
|
||||
placeholder: "value - if blank, matches if key is NOT present",
|
||||
|
@ -95,7 +86,8 @@ export default class SingleTagInput extends InputElement<string> {
|
|||
InnerRender(): string {
|
||||
return new Combine([
|
||||
this.key, this.operator, this.value
|
||||
]).Render();
|
||||
]).SetStyle("display:flex")
|
||||
.Render();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,8 +4,33 @@ import Translations from "../i18n/Translations";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import * as EmailValidator from "email-validator";
|
||||
import {parsePhoneNumberFromString} from "libphonenumber-js";
|
||||
import {DropDown} from "./DropDown";
|
||||
|
||||
export class ValidatedTextField {
|
||||
|
||||
public static explanations = {
|
||||
"string": "A basic, 255-char string",
|
||||
"date": "A date",
|
||||
"wikidata": "A wikidata identifier, e.g. Q42",
|
||||
"int": "A number",
|
||||
"nat": "A positive number",
|
||||
"float": "A decimal",
|
||||
"pfloat": "A positive decimal",
|
||||
"email": "An email adress",
|
||||
"url": "A url",
|
||||
"phone": "A phone number"
|
||||
}
|
||||
|
||||
public static TypeDropdown() : DropDown<string>{
|
||||
const values : {value: string, shown: string}[] = [];
|
||||
const expl = ValidatedTextField.explanations;
|
||||
for(const key in expl){
|
||||
values.push({value: key, shown: `${key} - ${expl[key]}`})
|
||||
}
|
||||
return new DropDown<string>("", values)
|
||||
}
|
||||
|
||||
|
||||
public static inputValidation = {
|
||||
"$": () => true,
|
||||
"string": () => true,
|
||||
|
@ -40,6 +65,19 @@ export class TextField<T> extends InputElement<T> {
|
|||
});
|
||||
}
|
||||
|
||||
public static KeyInput(): TextField<string>{
|
||||
return new TextField<string>({
|
||||
placeholder: "key",
|
||||
fromString: str => {
|
||||
if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) {
|
||||
return str;
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
toString: str => str
|
||||
});
|
||||
}
|
||||
|
||||
public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{
|
||||
const isValid = ValidatedTextField.inputValidation[type];
|
||||
extraValidation = extraValidation ?? (() => true)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue