Butchering the UI framework
This commit is contained in:
parent
8d404b1ba9
commit
6415e195d1
90 changed files with 1012 additions and 3101 deletions
|
@ -12,12 +12,12 @@ import Combine from "../../UI/Base/Combine";
|
||||||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||||
import {UIElement} from "../../UI/UIElement";
|
|
||||||
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
||||||
import SourceConfig from "./SourceConfig";
|
import SourceConfig from "./SourceConfig";
|
||||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
import {Tag} from "../../Logic/Tags/Tag";
|
||||||
import SubstitutingTag from "../../Logic/Tags/SubstitutingTag";
|
import SubstitutingTag from "../../Logic/Tags/SubstitutingTag";
|
||||||
|
import BaseUIElement from "../../UI/BaseUIElement";
|
||||||
|
|
||||||
export default class LayerConfig {
|
export default class LayerConfig {
|
||||||
|
|
||||||
|
@ -294,7 +294,7 @@ export default class LayerConfig {
|
||||||
{
|
{
|
||||||
icon:
|
icon:
|
||||||
{
|
{
|
||||||
html: UIElement,
|
html: BaseUIElement,
|
||||||
iconSize: [number, number],
|
iconSize: [number, number],
|
||||||
iconAnchor: [number, number],
|
iconAnchor: [number, number],
|
||||||
popupAnchor: [number, number],
|
popupAnchor: [number, number],
|
||||||
|
@ -361,7 +361,7 @@ export default class LayerConfig {
|
||||||
const iconUrlStatic = render(this.icon);
|
const iconUrlStatic = render(this.icon);
|
||||||
const self = this;
|
const self = this;
|
||||||
const mappedHtml = tags.map(tgs => {
|
const mappedHtml = tags.map(tgs => {
|
||||||
function genHtmlFromString(sourcePart: string): UIElement {
|
function genHtmlFromString(sourcePart: string): BaseUIElement {
|
||||||
if (sourcePart.indexOf("html:") == 0) {
|
if (sourcePart.indexOf("html:") == 0) {
|
||||||
// We use § as a replacement for ;
|
// We use § as a replacement for ;
|
||||||
const html = sourcePart.substring("html:".length)
|
const html = sourcePart.substring("html:".length)
|
||||||
|
@ -370,7 +370,7 @@ export default class LayerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
|
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
|
||||||
let html: UIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`);
|
let html: BaseUIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`);
|
||||||
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/)
|
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/)
|
||||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||||
html = new Combine([
|
html = new Combine([
|
||||||
|
@ -387,7 +387,7 @@ export default class LayerConfig {
|
||||||
const iconUrl = render(self.icon);
|
const iconUrl = render(self.icon);
|
||||||
const rotation = render(self.rotation, "0deg");
|
const rotation = render(self.rotation, "0deg");
|
||||||
|
|
||||||
let htmlParts: UIElement[] = [];
|
let htmlParts: BaseUIElement[] = [];
|
||||||
let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != ""));
|
let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != ""));
|
||||||
for (const sourcePart of sourceParts) {
|
for (const sourcePart of sourceParts) {
|
||||||
htmlParts.push(genHtmlFromString(sourcePart))
|
htmlParts.push(genHtmlFromString(sourcePart))
|
||||||
|
@ -399,7 +399,7 @@ export default class LayerConfig {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (iconOverlay.badge) {
|
if (iconOverlay.badge) {
|
||||||
const badgeParts: UIElement[] = [];
|
const badgeParts: BaseUIElement[] = [];
|
||||||
const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != "");
|
const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != "");
|
||||||
|
|
||||||
for (const badgePartStr of partDefs) {
|
for (const badgePartStr of partDefs) {
|
||||||
|
@ -437,7 +437,7 @@ export default class LayerConfig {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e, tgs)
|
console.error(e, tgs)
|
||||||
}
|
}
|
||||||
return new Combine(htmlParts).Render();
|
return new Combine(htmlParts);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {FixedUiElement} from "./UI/Base/FixedUiElement";
|
import {FixedUiElement} from "./UI/Base/FixedUiElement";
|
||||||
import CheckBox from "./UI/Input/CheckBox";
|
import Toggle from "./UI/Input/Toggle";
|
||||||
import {Basemap} from "./UI/BigComponents/Basemap";
|
import {Basemap} from "./UI/BigComponents/Basemap";
|
||||||
import State from "./State";
|
import State from "./State";
|
||||||
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
||||||
|
@ -272,7 +272,7 @@ export class InitUiElements {
|
||||||
|
|
||||||
// ?-Button on Desktop, opens panel with close-X.
|
// ?-Button on Desktop, opens panel with close-X.
|
||||||
const help = new MapControlButton(Svg.help_svg());
|
const help = new MapControlButton(Svg.help_svg());
|
||||||
new CheckBox(
|
new Toggle(
|
||||||
fullOptions
|
fullOptions
|
||||||
.SetClass("welcomeMessage")
|
.SetClass("welcomeMessage")
|
||||||
.onClick(() => {/*Catch the click*/
|
.onClick(() => {/*Catch the click*/
|
||||||
|
@ -307,7 +307,7 @@ export class InitUiElements {
|
||||||
)
|
)
|
||||||
|
|
||||||
;
|
;
|
||||||
const copyrightButton = new CheckBox(
|
const copyrightButton = new Toggle(
|
||||||
copyrightNotice,
|
copyrightNotice,
|
||||||
new MapControlButton(Svg.osm_copyright_svg()),
|
new MapControlButton(Svg.osm_copyright_svg()),
|
||||||
copyrightNotice.isShown
|
copyrightNotice.isShown
|
||||||
|
@ -316,13 +316,13 @@ export class InitUiElements {
|
||||||
const layerControlPanel = new LayerControlPanel(
|
const layerControlPanel = new LayerControlPanel(
|
||||||
State.state.layerControlIsOpened)
|
State.state.layerControlIsOpened)
|
||||||
.SetClass("block p-1 rounded-full");
|
.SetClass("block p-1 rounded-full");
|
||||||
const layerControlButton = new CheckBox(
|
const layerControlButton = new Toggle(
|
||||||
layerControlPanel,
|
layerControlPanel,
|
||||||
new MapControlButton(Svg.layers_svg()),
|
new MapControlButton(Svg.layers_svg()),
|
||||||
State.state.layerControlIsOpened
|
State.state.layerControlIsOpened
|
||||||
)
|
)
|
||||||
|
|
||||||
const layerControl = new CheckBox(
|
const layerControl = new Toggle(
|
||||||
layerControlButton,
|
layerControlButton,
|
||||||
"",
|
"",
|
||||||
State.state.featureSwitchLayers
|
State.state.featureSwitchLayers
|
||||||
|
|
|
@ -183,7 +183,6 @@ export default class GeoLocationHandler extends UIElement {
|
||||||
self.StartGeolocating(false);
|
self.StartGeolocating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.HideOnEmpty(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private locate() {
|
private locate() {
|
||||||
|
|
|
@ -21,7 +21,6 @@ class TitleElement extends UIElement {
|
||||||
this._allElementsStorage = allElementsStorage;
|
this._allElementsStorage = allElementsStorage;
|
||||||
this.ListenTo(Locale.language);
|
this.ListenTo(Locale.language);
|
||||||
this.ListenTo(this._selectedFeature)
|
this.ListenTo(this._selectedFeature)
|
||||||
this.dumbMode = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): string {
|
||||||
|
@ -63,7 +62,7 @@ export default class TitleHandler {
|
||||||
selectedFeature.addCallbackAndRun(_ => {
|
selectedFeature.addCallbackAndRun(_ => {
|
||||||
const title = new TitleElement(layoutToUse, selectedFeature, allElementsStorage)
|
const title = new TitleElement(layoutToUse, selectedFeature, allElementsStorage)
|
||||||
const d = document.createElement('div');
|
const d = document.createElement('div');
|
||||||
d.innerHTML = title.InnerRender();
|
d.innerHTML = title.InnerRenderAsString();
|
||||||
// We pass everything into a div to strip out images etc...
|
// We pass everything into a div to strip out images etc...
|
||||||
document.title = (d.textContent || d.innerText);
|
document.title = (d.textContent || d.innerText);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {FixedUiElement} from "./FixedUiElement";
|
import {FixedUiElement} from "./FixedUiElement";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class Combine extends UIElement {
|
export default class Combine extends BaseUIElement {
|
||||||
private readonly uiElements: UIElement[];
|
private readonly uiElements: BaseUIElement[];
|
||||||
|
|
||||||
constructor(uiElements: (string | UIElement)[]) {
|
constructor(uiElements: (string | BaseUIElement)[]) {
|
||||||
super();
|
super();
|
||||||
this.uiElements = Utils.NoNull(uiElements)
|
this.uiElements = Utils.NoNull(uiElements)
|
||||||
.map(el => {
|
.map(el => {
|
||||||
|
@ -16,17 +16,20 @@ export default class Combine extends UIElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
return this.uiElements.map(ui => {
|
const el = document.createElement("span")
|
||||||
if(ui === undefined || ui === null){
|
|
||||||
return "";
|
for (const subEl of this.uiElements) {
|
||||||
|
if(subEl === undefined || subEl === null){
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if(ui.Render === undefined){
|
const subHtml = subEl.ConstructElement()
|
||||||
console.error("Not a UI-element", ui);
|
if(subHtml !== undefined){
|
||||||
return "";
|
el.appendChild(subHtml)
|
||||||
}
|
}
|
||||||
return ui.Render();
|
}
|
||||||
}).join("");
|
|
||||||
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -12,11 +12,11 @@ export default class FeatureSwitched extends UIElement{
|
||||||
this._swtch = swtch;
|
this._swtch = swtch;
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement | string {
|
||||||
if(this._swtch.data){
|
if(this._swtch.data){
|
||||||
return this._upstream.Render();
|
return this._upstream.Render();
|
||||||
}
|
}
|
||||||
return "";
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,9 +1,13 @@
|
||||||
import Constants from "../../Models/Constants";
|
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class Img {
|
export default class Img extends BaseUIElement {
|
||||||
|
private _src: string;
|
||||||
|
|
||||||
public static runningFromConsole = false;
|
constructor(src: string) {
|
||||||
|
super();
|
||||||
|
this._src = src;
|
||||||
|
}
|
||||||
|
|
||||||
static AsData(source: string) {
|
static AsData(source: string) {
|
||||||
if (Utils.runningFromConsole) {
|
if (Utils.runningFromConsole) {
|
||||||
|
@ -15,5 +19,11 @@ export default class Img {
|
||||||
static AsImageElement(source: string, css_class: string = "", style = ""): string {
|
static AsImageElement(source: string, css_class: string = "", style = ""): string {
|
||||||
return `<img class="${css_class}" style="${style}" alt="" src="${Img.AsData(source)}">`;
|
return `<img class="${css_class}" style="${style}" alt="" src="${Img.AsData(source)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
const el = document.createElement("img")
|
||||||
|
el.src = this._src;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,35 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
|
||||||
|
|
||||||
export default class Link extends UIElement {
|
export default class Link extends BaseUIElement {
|
||||||
private readonly _embeddedShow: UIElement;
|
private readonly _element: HTMLElement;
|
||||||
private readonly _target: string;
|
|
||||||
private readonly _newTab: string;
|
|
||||||
|
|
||||||
constructor(embeddedShow: UIElement | string, target: string, newTab: boolean = false) {
|
constructor(embeddedShow: BaseUIElement | string, target: string | UIEventSource<string>, newTab: boolean = false) {
|
||||||
super();
|
super();
|
||||||
this._embeddedShow = Translations.W(embeddedShow);
|
const _embeddedShow = Translations.W(embeddedShow);
|
||||||
this._target = target;
|
|
||||||
this._newTab = "";
|
|
||||||
|
const el = document.createElement("a")
|
||||||
|
|
||||||
|
if(typeof target === "string"){
|
||||||
|
el.href = target
|
||||||
|
}else{
|
||||||
|
target.addCallbackAndRun(target => {
|
||||||
|
el.target = target;
|
||||||
|
})
|
||||||
|
}
|
||||||
if (newTab) {
|
if (newTab) {
|
||||||
this._newTab = "target='_blank'"
|
el.target = "_blank"
|
||||||
}
|
}
|
||||||
|
el.appendChild(_embeddedShow.ConstructElement())
|
||||||
|
this._element = el
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
return `<a href="${this._target}" ${this._newTab}>${this._embeddedShow.Render()}</a>`;
|
|
||||||
|
return this._element;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,7 +7,13 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Hash from "../../Logic/Web/Hash";
|
import Hash from "../../Logic/Web/Hash";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps some contents into a panel that scrolls the content _under_ the title
|
*
|
||||||
|
* The scrollableFullScreen is a bit of a peculiar component:
|
||||||
|
* - It shows a title and some contents, constructed from the respective functions passed into the constructor
|
||||||
|
* - When the element is 'activated', one clone of title+contents is attached to the fullscreen
|
||||||
|
* - The element itself will - upon rendering - also show the title and contents (allthough it'll be a different clone)
|
||||||
|
*
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export default class ScrollableFullScreen extends UIElement {
|
export default class ScrollableFullScreen extends UIElement {
|
||||||
private static readonly empty = new FixedUiElement("");
|
private static readonly empty = new FixedUiElement("");
|
||||||
|
@ -40,8 +46,8 @@ export default class ScrollableFullScreen extends UIElement {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
return this._component.Render();
|
return this._component;
|
||||||
}
|
}
|
||||||
|
|
||||||
Activate(): void {
|
Activate(): void {
|
||||||
|
|
|
@ -1,55 +1,51 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Combine from "./Combine";
|
import Combine from "./Combine";
|
||||||
import {FixedUiElement} from "./FixedUiElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import Link from "./Link";
|
||||||
|
import Img from "./Img";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
|
||||||
|
|
||||||
export class SubtleButton extends Combine {
|
export class SubtleButton extends Combine {
|
||||||
|
|
||||||
constructor(imageUrl: string | UIElement, message: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined) {
|
constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, linkTo: { url: string | UIEventSource<string>, newTab?: boolean } = undefined) {
|
||||||
super(SubtleButton.generateContent(imageUrl, message, linkTo));
|
super(SubtleButton.generateContent(imageUrl, message, linkTo));
|
||||||
|
|
||||||
this.SetClass("block flex p-3 my-2 bg-blue-100 rounded-lg hover:shadow-xl hover:bg-blue-200 link-no-underline")
|
this.SetClass("block flex p-3 my-2 bg-blue-100 rounded-lg hover:shadow-xl hover:bg-blue-200 link-no-underline")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static generateContent(imageUrl: string | UIElement, messageT: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined): (UIElement | string)[] {
|
private static generateContent(imageUrl: string | BaseUIElement, messageT: string | BaseUIElement, linkTo: { url: string | UIEventSource<string>, newTab?: boolean } = undefined): (BaseUIElement )[] {
|
||||||
const message = Translations.W(messageT);
|
const message = Translations.W(messageT);
|
||||||
if (message !== null) {
|
|
||||||
message.dumbMode = false;
|
|
||||||
}
|
|
||||||
let img;
|
let img;
|
||||||
if ((imageUrl ?? "") === "") {
|
if ((imageUrl ?? "") === "") {
|
||||||
img = new FixedUiElement("");
|
img = undefined;
|
||||||
} else if (typeof (imageUrl) === "string") {
|
} else if (typeof (imageUrl) === "string") {
|
||||||
img = new FixedUiElement(`<img style="width: 100%;" src="${imageUrl}" alt="">`);
|
img = new Img(imageUrl).SetClass("w-full")
|
||||||
} else {
|
} else {
|
||||||
img = imageUrl;
|
img = imageUrl;
|
||||||
}
|
}
|
||||||
img.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0")
|
img?.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0")
|
||||||
const image = new Combine([img])
|
const image = new Combine([img])
|
||||||
.SetClass("flex-shrink-0");
|
.SetClass("flex-shrink-0");
|
||||||
|
|
||||||
|
if (linkTo == undefined) {
|
||||||
if (message !== null && message.IsEmpty()) {
|
|
||||||
// Message == null: special case to force empty text
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkTo != undefined) {
|
|
||||||
return [
|
return [
|
||||||
`<a class='flex group' href="${linkTo.url}" ${linkTo.newTab ? 'target="_blank"' : ""}>`,
|
|
||||||
image,
|
image,
|
||||||
`<div class='ml-4 overflow-ellipsis'>`,
|
|
||||||
message,
|
message,
|
||||||
`</div>`,
|
|
||||||
`</a>`
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
new Link(
|
||||||
|
new Combine([
|
||||||
image,
|
image,
|
||||||
message,
|
message?.SetClass("block ml-4 overflow-ellipsis")
|
||||||
|
]).SetClass("flex group"),
|
||||||
|
linkTo.url,
|
||||||
|
linkTo.newTab ?? false
|
||||||
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,41 @@
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import Combine from "./Combine";
|
||||||
|
|
||||||
export class TabbedComponent extends UIElement {
|
export class TabbedComponent extends UIElement {
|
||||||
|
|
||||||
private headers: UIElement[] = [];
|
private readonly header: UIElement;
|
||||||
private content: UIElement[] = [];
|
private content: UIElement[] = [];
|
||||||
|
|
||||||
constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) {
|
constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) {
|
||||||
super(typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0)));
|
super(typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0)));
|
||||||
const self = this;
|
const self = this;
|
||||||
|
const tabs: UIElement[] = []
|
||||||
|
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
let element = elements[i];
|
let element = elements[i];
|
||||||
this.headers.push(Translations.W(element.header).onClick(() => self._source.setData(i)));
|
const header = Translations.W(element.header).onClick(() => self._source.setData(i))
|
||||||
const content = Translations.W(element.content)
|
const content = Translations.W(element.content)
|
||||||
this.content.push(content);
|
this.content.push(content);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
let headerBar = "";
|
|
||||||
for (let i = 0; i < this.headers.length; i++) {
|
|
||||||
let header = this.headers[i];
|
|
||||||
|
|
||||||
if (!this.content[i].IsEmpty()) {
|
if (!this.content[i].IsEmpty()) {
|
||||||
headerBar += `<div class=\'tab-single-header ${i == this._source.data ? 'tab-active' : 'tab-non-active'}\'>` +
|
const tab = header.SetClass("block tab-single-header")
|
||||||
header.Render() + "</div>"
|
tabs.push(tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.header = new Combine(tabs).SetClass("block tabs-header-bar")
|
||||||
|
|
||||||
headerBar = "<div class='tabs-header-bar'>" + headerBar + "</div>"
|
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerRender(): UIElement {
|
||||||
|
|
||||||
const content = this.content[this._source.data];
|
const content = this.content[this._source.data];
|
||||||
return headerBar + "<div class='tab-content'>" + (content?.Render() ?? "") + "</div>";
|
return new Combine([
|
||||||
|
this.header,
|
||||||
|
content.SetClass("tab-content"),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,16 +1,35 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class VariableUiElement extends UIElement {
|
export class VariableUiElement extends BaseUIElement {
|
||||||
private _html: UIEventSource<string>;
|
|
||||||
|
|
||||||
constructor(html: UIEventSource<string>) {
|
private _element : HTMLElement;
|
||||||
super(html);
|
|
||||||
this._html = html;
|
constructor(contents: UIEventSource<string | BaseUIElement>) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._element = document.createElement("span")
|
||||||
|
const el = this._element
|
||||||
|
contents.addCallbackAndRun(contents => {
|
||||||
|
while(el.firstChild){
|
||||||
|
el.removeChild(
|
||||||
|
el.lastChild
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
if(contents === undefined){
|
||||||
return this._html.data;
|
return
|
||||||
|
}
|
||||||
|
if(typeof contents === "string"){
|
||||||
|
el.innerHTML = contents
|
||||||
|
}else{
|
||||||
|
el.appendChild(contents.ConstructElement())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._element;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
|
|
||||||
export class VerticalCombine extends UIElement {
|
|
||||||
private readonly _elements: UIElement[];
|
|
||||||
|
|
||||||
constructor(elements: UIElement[]) {
|
|
||||||
super(undefined);
|
|
||||||
this._elements = elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
let html = "";
|
|
||||||
for (const element of this._elements) {
|
|
||||||
if (element !== undefined && !element.IsEmpty()) {
|
|
||||||
html += "<div>" + element.Render() + "</div>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
154
UI/BaseUIElement.ts
Normal file
154
UI/BaseUIElement.ts
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import {Utils} from "../Utils";
|
||||||
|
import {UIEventSource} from "../Logic/UIEventSource";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A thin wrapper around a html element, which allows to generate a HTML-element.
|
||||||
|
*
|
||||||
|
* Assumes a read-only configuration, so it has no 'ListenTo'
|
||||||
|
*/
|
||||||
|
export default abstract class BaseUIElement {
|
||||||
|
|
||||||
|
private clss: Set<string> = new Set<string>();
|
||||||
|
private style: string;
|
||||||
|
private _onClick: () => void;
|
||||||
|
private _onHover: UIEventSource<boolean>;
|
||||||
|
|
||||||
|
protected _constructedHtmlElement: HTMLElement;
|
||||||
|
|
||||||
|
|
||||||
|
protected abstract InnerConstructElement(): HTMLElement;
|
||||||
|
|
||||||
|
public onClick(f: (() => void)) {
|
||||||
|
this._onClick = f;
|
||||||
|
this.SetClass("clickable")
|
||||||
|
if(this._constructedHtmlElement !== undefined){
|
||||||
|
this._constructedHtmlElement.onclick = f;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IsHovered(): UIEventSource<boolean> {
|
||||||
|
if (this._onHover !== undefined) {
|
||||||
|
return this._onHover;
|
||||||
|
}
|
||||||
|
// Note: we just save it. 'Update' will register that an eventsource exist and install the necessary hooks
|
||||||
|
this._onHover = new UIEventSource<boolean>(false);
|
||||||
|
return this._onHover;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AttachTo(divId: string) {
|
||||||
|
let element = document.getElementById(divId);
|
||||||
|
if (element === null) {
|
||||||
|
throw "SEVERE: could not attach UIElement to " + divId;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (element.firstChild) {
|
||||||
|
//The list is LIVE so it will re-index each call
|
||||||
|
element.removeChild(element.firstChild);
|
||||||
|
}
|
||||||
|
const el = this.ConstructElement();
|
||||||
|
if(el !== undefined){
|
||||||
|
element.appendChild(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds all the relevant classes, space seperated
|
||||||
|
* @param clss
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public SetClass(clss: string) {
|
||||||
|
const all = clss.split(" ").map(clsName => clsName.trim());
|
||||||
|
let recordedChange = false;
|
||||||
|
for (const c of all) {
|
||||||
|
if (this.clss.has(clss)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.clss.add(c);
|
||||||
|
recordedChange = true;
|
||||||
|
}
|
||||||
|
if (recordedChange) {
|
||||||
|
this._constructedHtmlElement?.classList.add(...Array.from(this.clss));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoveClass(clss: string): BaseUIElement {
|
||||||
|
if (this.clss.has(clss)) {
|
||||||
|
this.clss.delete(clss);
|
||||||
|
this._constructedHtmlElement?.classList.remove(clss)
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetStyle(style: string): BaseUIElement {
|
||||||
|
this.style = style;
|
||||||
|
if(this._constructedHtmlElement !== undefined){
|
||||||
|
this._constructedHtmlElement.style.cssText = style;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The same as 'Render', but creates a HTML element instead of the HTML representation
|
||||||
|
*/
|
||||||
|
public ConstructElement(): HTMLElement {
|
||||||
|
if (Utils.runningFromConsole) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._constructedHtmlElement !== undefined) {
|
||||||
|
return this._constructedHtmlElement
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const el = this.InnerConstructElement();
|
||||||
|
|
||||||
|
if(el === undefined){
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._constructedHtmlElement = el;
|
||||||
|
const style = this.style
|
||||||
|
if (style !== undefined && style !== "") {
|
||||||
|
el.style.cssText = style
|
||||||
|
}
|
||||||
|
if (this.clss.size > 0) {
|
||||||
|
try{
|
||||||
|
el.classList.add(...Array.from(this.clss))
|
||||||
|
}catch(e){
|
||||||
|
console.error("Invalid class name detected in:", Array.from(this.clss).join(" "),"\nErr msg is ",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._onClick !== undefined) {
|
||||||
|
const self = this;
|
||||||
|
el.onclick = (e) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (e.consumed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self._onClick();
|
||||||
|
// @ts-ignore
|
||||||
|
e.consumed = true;
|
||||||
|
}
|
||||||
|
el.style.pointerEvents = "all";
|
||||||
|
el.style.cursor = "pointer";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._onHover !== undefined) {
|
||||||
|
const self = this;
|
||||||
|
el.addEventListener('mouseover', () => self._onHover.setData(true));
|
||||||
|
el.addEventListener('mouseout', () => self._onHover.setData(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._onHover !== undefined) {
|
||||||
|
const self = this;
|
||||||
|
el.addEventListener('mouseover', () => self._onHover.setData(true));
|
||||||
|
el.addEventListener('mouseout', () => self._onHover.setData(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,16 +44,14 @@ export default class AttributionPanel extends Combine {
|
||||||
const contribs = links.join(", ")
|
const contribs = links.join(", ")
|
||||||
|
|
||||||
if (hiddenCount == 0) {
|
if (hiddenCount == 0) {
|
||||||
|
|
||||||
|
|
||||||
return Translations.t.general.attribution.mapContributionsBy.Subs({
|
return Translations.t.general.attribution.mapContributionsBy.Subs({
|
||||||
contributors: contribs
|
contributors: contribs
|
||||||
}).InnerRender()
|
})
|
||||||
} else {
|
} else {
|
||||||
return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({
|
return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({
|
||||||
contributors: contribs,
|
contributors: contribs,
|
||||||
hiddenCount: hiddenCount
|
hiddenCount: hiddenCount
|
||||||
}).InnerRender();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,11 @@ import Translations from "../i18n/Translations";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class BackgroundSelector extends UIElement {
|
export default class BackgroundSelector extends UIElement {
|
||||||
|
|
||||||
private _dropdown: UIElement;
|
private _dropdown: BaseUIElement;
|
||||||
private readonly _availableLayers: UIEventSource<BaseLayer[]>;
|
private readonly _availableLayers: UIEventSource<BaseLayer[]>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -31,8 +32,8 @@ export default class BackgroundSelector extends UIElement {
|
||||||
this._dropdown = new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer);
|
this._dropdown = new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): BaseUIElement {
|
||||||
return this._dropdown.Render();
|
return this._dropdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -80,8 +80,8 @@ export default class FullWelcomePaneWithTabs extends UIElement {
|
||||||
.ListenTo(userDetails);
|
.ListenTo(userDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
return this._component.Render();
|
return this._component;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import CheckBox from "../Input/CheckBox";
|
import Toggle from "../Input/Toggle";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
|
@ -65,7 +65,7 @@ export default class LayerSelection extends UIElement {
|
||||||
}))
|
}))
|
||||||
const style = "display:flex;align-items:center;"
|
const style = "display:flex;align-items:center;"
|
||||||
const styleWhole = "display:flex; flex-wrap: wrap"
|
const styleWhole = "display:flex; flex-wrap: wrap"
|
||||||
this._checkboxes.push(new CheckBox(
|
this._checkboxes.push(new Toggle(
|
||||||
new Combine([new Combine([icon, name]).SetStyle(style), zoomStatus])
|
new Combine([new Combine([icon, name]).SetStyle(style), zoomStatus])
|
||||||
.SetStyle(styleWhole),
|
.SetStyle(styleWhole),
|
||||||
new Combine([new Combine([iconUnselected, "<del>", name, "</del>"]).SetStyle(style), zoomStatus])
|
new Combine([new Combine([iconUnselected, "<del>", name, "</del>"]).SetStyle(style), zoomStatus])
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||||
|
@ -11,87 +10,93 @@ import * as personal from "../../assets/themes/personalLayout/personalLayout.jso
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import LanguagePicker from "../LanguagePicker";
|
import LanguagePicker from "../LanguagePicker";
|
||||||
import IndexText from "./IndexText";
|
import IndexText from "./IndexText";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class MoreScreen extends UIElement {
|
export default class MoreScreen extends Combine {
|
||||||
private readonly _onMainScreen: boolean;
|
|
||||||
|
|
||||||
private _component: UIElement;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(onMainScreen: boolean = false) {
|
constructor(onMainScreen: boolean = false) {
|
||||||
super(State.state.locationControl);
|
super(MoreScreen.Init(onMainScreen, State.state));
|
||||||
this._onMainScreen = onMainScreen;
|
|
||||||
this.ListenTo(State.state.osmConnection.userDetails);
|
|
||||||
this.ListenTo(State.state.installedThemes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
private static Init(onMainScreen: boolean, state: State): BaseUIElement [] {
|
||||||
|
|
||||||
const tr = Translations.t.general.morescreen;
|
const tr = Translations.t.general.morescreen;
|
||||||
|
let intro: BaseUIElement = tr.intro;
|
||||||
const els: UIElement[] = []
|
let themeButtonStyle = ""
|
||||||
|
let themeListStyle = ""
|
||||||
const themeButtons: UIElement[] = []
|
if (onMainScreen) {
|
||||||
|
|
||||||
for (const layout of AllKnownLayouts.layoutsList) {
|
|
||||||
if (layout.id === personal.id) {
|
|
||||||
if (State.state.osmConnection.userDetails.data.csCount < Constants.userJourney.personalLayoutUnlock) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
themeButtons.push(this.createLinkButton(layout));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
els.push(new VariableUiElement(
|
|
||||||
State.state.osmConnection.userDetails.map(userDetails => {
|
|
||||||
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
|
|
||||||
return new SubtleButton(null, tr.requestATheme, {url:"https://github.com/pietervdvn/MapComplete/issues", newTab: true}).Render();
|
|
||||||
}
|
|
||||||
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme, {
|
|
||||||
url: "./customGenerator.html",
|
|
||||||
newTab: false
|
|
||||||
}).Render();
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
els.push(new Combine(themeButtons))
|
|
||||||
|
|
||||||
|
|
||||||
const customThemesNames = State.state.installedThemes.data ?? [];
|
|
||||||
|
|
||||||
if (customThemesNames.length > 0) {
|
|
||||||
els.push(Translations.t.general.customThemeIntro)
|
|
||||||
|
|
||||||
for (const installed of State.state.installedThemes.data) {
|
|
||||||
els.push(this.createLinkButton(installed.layout, installed.definition));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let intro: UIElement = tr.intro;
|
|
||||||
const themeButtonsElement = new Combine(els)
|
|
||||||
|
|
||||||
if (this._onMainScreen) {
|
|
||||||
intro = new Combine([
|
intro = new Combine([
|
||||||
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
||||||
.SetClass("absolute top-2 right-3"),
|
.SetClass("absolute top-2 right-3"),
|
||||||
new IndexText()
|
new IndexText()
|
||||||
])
|
])
|
||||||
themeButtons.map(e => e?.SetClass("h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden"))
|
themeButtonStyle = "h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden"
|
||||||
themeButtonsElement.SetClass("md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4")
|
themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return[
|
||||||
|
|
||||||
this._component = new Combine([
|
|
||||||
intro,
|
intro,
|
||||||
themeButtonsElement,
|
MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle),
|
||||||
|
MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle),
|
||||||
tr.streetcomplete.SetClass("block text-base mx-10 my-3 mb-10")
|
tr.streetcomplete.SetClass("block text-base mx-10 my-3 mb-10")
|
||||||
]);
|
];
|
||||||
return this._component.Render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined) {
|
private static createUnofficialThemeList(buttonClass: string): BaseUIElement{
|
||||||
|
const customThemes = State.state.installedThemes.data ?? [];
|
||||||
|
const els : BaseUIElement[] = []
|
||||||
|
if (customThemes.length > 0) {
|
||||||
|
els.push(Translations.t.general.customThemeIntro)
|
||||||
|
|
||||||
|
const customThemesElement = new Combine(
|
||||||
|
customThemes.map(theme => MoreScreen.createLinkButton(theme.layout, theme.definition)?.SetClass(buttonClass))
|
||||||
|
)
|
||||||
|
els.push(customThemesElement)
|
||||||
|
}
|
||||||
|
return new Combine(els)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createOfficialThemesList(state: State, buttonClass: string): BaseUIElement {
|
||||||
|
let officialThemes = AllKnownLayouts.layoutsList
|
||||||
|
if (State.state.osmConnection.userDetails.data.csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||||
|
officialThemes = officialThemes.filter(theme => theme.id !== personal.id)
|
||||||
|
}
|
||||||
|
let buttons = officialThemes.map((layout) => MoreScreen.createLinkButton(layout)?.SetClass(buttonClass))
|
||||||
|
|
||||||
|
let customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
|
||||||
|
buttons.splice(0, 0, customGeneratorLink);
|
||||||
|
|
||||||
|
return new Combine(buttons)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
|
||||||
|
* */
|
||||||
|
private static createCustomGeneratorButton(state: State): VariableUiElement {
|
||||||
|
const tr = Translations.t.general.morescreen;
|
||||||
|
return new VariableUiElement(
|
||||||
|
state.osmConnection.userDetails.map(userDetails => {
|
||||||
|
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
|
||||||
|
return new SubtleButton(null, tr.requestATheme, {
|
||||||
|
url: "https://github.com/pietervdvn/MapComplete/issues",
|
||||||
|
newTab: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme, {
|
||||||
|
url: "./customGenerator.html",
|
||||||
|
newTab: false
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a button linking to the given theme
|
||||||
|
* @param layout
|
||||||
|
* @param customThemeDefinition
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private static createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined): BaseUIElement {
|
||||||
if (layout === undefined) {
|
if (layout === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -100,17 +105,14 @@ export default class MoreScreen extends UIElement {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (layout.hideFromOverview) {
|
if (layout.hideFromOverview) {
|
||||||
const pref = State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled");
|
|
||||||
this.ListenTo(pref);
|
|
||||||
if (pref.data !== "true") {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (layout.id === State.state.layoutToUse.data?.id) {
|
if (layout.id === State.state.layoutToUse.data?.id) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLocation = State.state.locationControl.data;
|
const currentLocation = State.state.locationControl;
|
||||||
|
|
||||||
let path = window.location.pathname;
|
let path = window.location.pathname;
|
||||||
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
|
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
|
||||||
path = path.substr(0, path.lastIndexOf("/"));
|
path = path.substr(0, path.lastIndexOf("/"));
|
||||||
|
@ -119,19 +121,23 @@ export default class MoreScreen extends UIElement {
|
||||||
path = "."
|
path = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = `z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}`
|
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
|
||||||
let linkText =
|
let linkSuffix = ""
|
||||||
`${path}/${layout.id.toLowerCase()}.html?${params}`
|
|
||||||
|
|
||||||
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
||||||
linkText = `${path}/index.html?layout=${layout.id}&${params}`
|
linkPrefix = `${path}/index.html?layout=${layout.id}&`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customThemeDefinition) {
|
if (customThemeDefinition) {
|
||||||
linkText = `${path}/index.html?userlayout=${layout.id}&${params}#${customThemeDefinition}`
|
linkPrefix = `${path}/index.html?userlayout=${layout.id}&`
|
||||||
|
linkSuffix = `#${customThemeDefinition}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const linkText = currentLocation.map(currentLocation =>
|
||||||
|
`${linkPrefix}z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}${linkSuffix}`)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let description = Translations.W(layout.shortDescription);
|
let description = Translations.W(layout.shortDescription);
|
||||||
return new SubtleButton(layout.icon,
|
return new SubtleButton(layout.icon,
|
||||||
new Combine([
|
new Combine([
|
||||||
|
@ -144,4 +150,5 @@ export default class MoreScreen extends UIElement {
|
||||||
]), {url: linkText, newTab: false});
|
]), {url: linkText, newTab: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import CheckBox from "../Input/CheckBox";
|
import Toggle from "../Input/Toggle";
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
|
@ -79,7 +79,7 @@ export default class PersonalLayersPanel extends UIElement {
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
const cb = new CheckBox(
|
const cb = new Toggle(
|
||||||
new SubtleButton(
|
new SubtleButton(
|
||||||
icon,
|
icon,
|
||||||
content),
|
content),
|
||||||
|
|
|
@ -19,7 +19,6 @@ export default class ShareButton extends UIElement{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||||
super.InnerUpdate(htmlElement);
|
|
||||||
const self= this;
|
const self= this;
|
||||||
htmlElement.addEventListener('click', () => {
|
htmlElement.addEventListener('click', () => {
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {VerticalCombine} from "../Base/VerticalCombine";
|
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import {Translation} from "../i18n/Translation";
|
import {Translation} from "../i18n/Translation";
|
||||||
|
@ -9,7 +8,7 @@ import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import CheckBox from "../Input/CheckBox";
|
import Toggle from "../Input/Toggle";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
|
@ -40,7 +39,7 @@ export default class ShareScreen extends UIElement {
|
||||||
return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;");
|
return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;");
|
||||||
}
|
}
|
||||||
|
|
||||||
const includeLocation = new CheckBox(
|
const includeLocation = new Toggle(
|
||||||
new Combine([check(), tr.fsIncludeCurrentLocation]),
|
new Combine([check(), tr.fsIncludeCurrentLocation]),
|
||||||
new Combine([nocheck(), tr.fsIncludeCurrentLocation]),
|
new Combine([nocheck(), tr.fsIncludeCurrentLocation]),
|
||||||
true
|
true
|
||||||
|
@ -75,7 +74,7 @@ export default class ShareScreen extends UIElement {
|
||||||
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
||||||
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render();
|
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render();
|
||||||
}));
|
}));
|
||||||
const includeCurrentBackground = new CheckBox(
|
const includeCurrentBackground = new Toggle(
|
||||||
new Combine([check(), currentBackground]),
|
new Combine([check(), currentBackground]),
|
||||||
new Combine([nocheck(), currentBackground]),
|
new Combine([nocheck(), currentBackground]),
|
||||||
true
|
true
|
||||||
|
@ -90,7 +89,7 @@ export default class ShareScreen extends UIElement {
|
||||||
}, [currentLayer]));
|
}, [currentLayer]));
|
||||||
|
|
||||||
|
|
||||||
const includeLayerChoices = new CheckBox(
|
const includeLayerChoices = new Toggle(
|
||||||
new Combine([check(), tr.fsIncludeCurrentLayers]),
|
new Combine([check(), tr.fsIncludeCurrentLayers]),
|
||||||
new Combine([nocheck(), tr.fsIncludeCurrentLayers]),
|
new Combine([nocheck(), tr.fsIncludeCurrentLayers]),
|
||||||
true
|
true
|
||||||
|
@ -120,7 +119,7 @@ export default class ShareScreen extends UIElement {
|
||||||
|
|
||||||
for (const swtch of switches) {
|
for (const swtch of switches) {
|
||||||
|
|
||||||
const checkbox = new CheckBox(
|
const checkbox = new Toggle(
|
||||||
new Combine([check(), Translations.W(swtch.human)]),
|
new Combine([check(), Translations.W(swtch.human)]),
|
||||||
new Combine([nocheck(), Translations.W(swtch.human)]), !swtch.reverse
|
new Combine([nocheck(), Translations.W(swtch.human)]), !swtch.reverse
|
||||||
);
|
);
|
||||||
|
@ -143,7 +142,7 @@ export default class ShareScreen extends UIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this._options = new VerticalCombine(optionCheckboxes)
|
this._options = new Combine(optionCheckboxes).SetClass("flex flex-col")
|
||||||
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
|
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
|
||||||
|
|
||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
|
@ -216,8 +215,8 @@ export default class ShareScreen extends UIElement {
|
||||||
).onClick(async () => {
|
).onClick(async () => {
|
||||||
|
|
||||||
const shareData = {
|
const shareData = {
|
||||||
title: Translations.W(layout.id)?.InnerRender() ?? "",
|
title: Translations.W(layout.title)?.InnerRenderAsString() ?? "",
|
||||||
text: Translations.W(layout.description)?.InnerRender() ?? "",
|
text: Translations.W(layout.description)?.InnerRenderAsString() ?? "",
|
||||||
url: self._link.data,
|
url: self._link.data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,11 +250,11 @@ export default class ShareScreen extends UIElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
|
|
||||||
const tr = Translations.t.general.sharescreen;
|
const tr = Translations.t.general.sharescreen;
|
||||||
|
|
||||||
return new VerticalCombine([
|
return new Combine([
|
||||||
this._editLayout,
|
this._editLayout,
|
||||||
tr.intro,
|
tr.intro,
|
||||||
this._link,
|
this._link,
|
||||||
|
@ -264,7 +263,7 @@ export default class ShareScreen extends UIElement {
|
||||||
tr.embedIntro,
|
tr.embedIntro,
|
||||||
this._options,
|
this._options,
|
||||||
this._iframeCode,
|
this._iframeCode,
|
||||||
]).Render()
|
]).SetClass("flex flex-col")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,6 +7,7 @@ import Translations from "../i18n/Translations";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class ThemeIntroductionPanel extends UIElement {
|
export default class ThemeIntroductionPanel extends UIElement {
|
||||||
private languagePicker: UIElement;
|
private languagePicker: UIElement;
|
||||||
|
@ -44,7 +45,7 @@ export default class ThemeIntroductionPanel extends UIElement {
|
||||||
this.SetClass("link-underline")
|
this.SetClass("link-underline")
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): BaseUIElement {
|
||||||
const layout : LayoutConfig = this._layout.data;
|
const layout : LayoutConfig = this._layout.data;
|
||||||
return new Combine([
|
return new Combine([
|
||||||
layout.description,
|
layout.description,
|
||||||
|
@ -54,7 +55,7 @@ export default class ThemeIntroductionPanel extends UIElement {
|
||||||
"<br/>",
|
"<br/>",
|
||||||
this.languagePicker,
|
this.languagePicker,
|
||||||
...layout.CustomCodeSnippets()
|
...layout.CustomCodeSnippets()
|
||||||
]).Render()
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -64,10 +64,10 @@ export default class UserBadge extends UIElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
const user = this._userDetails.data;
|
const user = this._userDetails.data;
|
||||||
if (!user.loggedIn) {
|
if (!user.loggedIn) {
|
||||||
return this._loginButton.Render();
|
return this._loginButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkStyle = "flex items-baseline"
|
const linkStyle = "flex items-baseline"
|
||||||
|
@ -138,7 +138,7 @@ export default class UserBadge extends UIElement {
|
||||||
return new Combine([
|
return new Combine([
|
||||||
userIcon,
|
userIcon,
|
||||||
usertext,
|
usertext,
|
||||||
]).Render()
|
])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,34 +13,37 @@ export default class CenterMessageBox extends UIElement {
|
||||||
this.ListenTo(State.state.layerUpdater.sufficientlyZoomed);
|
this.ListenTo(State.state.layerUpdater.sufficientlyZoomed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static prep(): { innerHtml: string, done: boolean } {
|
private static prep(): { innerHtml: string | UIElement, done: boolean } {
|
||||||
if (State.state.centerMessage.data != "") {
|
if (State.state.centerMessage.data != "") {
|
||||||
return {innerHtml: State.state.centerMessage.data, done: false};
|
return {innerHtml: State.state.centerMessage.data, done: false};
|
||||||
}
|
}
|
||||||
const lu = State.state.layerUpdater;
|
const lu = State.state.layerUpdater;
|
||||||
if (lu.timeout.data > 0) {
|
if (lu.timeout.data > 0) {
|
||||||
return {
|
return {
|
||||||
innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.timeout.data}).Render(),
|
innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.timeout.data}),
|
||||||
done: false
|
done: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lu.runningQuery.data) {
|
if (lu.runningQuery.data) {
|
||||||
return {innerHtml: Translations.t.centerMessage.loadingData.Render(), done: false};
|
return {innerHtml: Translations.t.centerMessage.loadingData, done: false};
|
||||||
|
|
||||||
}
|
}
|
||||||
if (!lu.sufficientlyZoomed.data) {
|
if (!lu.sufficientlyZoomed.data) {
|
||||||
return {innerHtml: Translations.t.centerMessage.zoomIn.Render(), done: false};
|
return {innerHtml: Translations.t.centerMessage.zoomIn, done: false};
|
||||||
} else {
|
} else {
|
||||||
return {innerHtml: Translations.t.centerMessage.ready.Render(), done: true};
|
return {innerHtml: Translations.t.centerMessage.ready, done: true};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): string | UIElement {
|
||||||
return CenterMessageBox.prep().innerHtml;
|
return CenterMessageBox.prep().innerHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerUpdate(htmlElement: HTMLElement) {
|
InnerUpdate(htmlElement: HTMLElement) {
|
||||||
|
if(htmlElement.parentElement === null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pstyle = htmlElement.parentElement.style;
|
const pstyle = htmlElement.parentElement.style;
|
||||||
if (State.state.centerMessage.data != "") {
|
if (State.state.centerMessage.data != "") {
|
||||||
pstyle.opacity = "1";
|
pstyle.opacity = "1";
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import {GenerateEmpty} from "./GenerateEmpty";
|
|
||||||
import LayerPanelWithPreview from "./LayerPanelWithPreview";
|
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
|
||||||
import {MultiInput} from "../Input/MultiInput";
|
|
||||||
import TagRenderingPanel from "./TagRenderingPanel";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
|
||||||
import {DropDown} from "../Input/DropDown";
|
|
||||||
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
|
||||||
import Svg from "../../Svg";
|
|
||||||
|
|
||||||
export default class AllLayersPanel extends UIElement {
|
|
||||||
|
|
||||||
|
|
||||||
private panel: UIElement;
|
|
||||||
private readonly _config: UIEventSource<LayoutConfigJson>;
|
|
||||||
private readonly languages: UIEventSource<string[]>;
|
|
||||||
private readonly userDetails: UserDetails;
|
|
||||||
private readonly currentlySelected: UIEventSource<SingleSetting<any>>;
|
|
||||||
|
|
||||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
|
||||||
languages: UIEventSource<any>, userDetails: UserDetails) {
|
|
||||||
super(undefined);
|
|
||||||
this.userDetails = userDetails;
|
|
||||||
this._config = config;
|
|
||||||
this.languages = languages;
|
|
||||||
|
|
||||||
this.createPanels(userDetails);
|
|
||||||
const self = this;
|
|
||||||
this.dumbMode = false;
|
|
||||||
config.map<number>(config => config.layers.length).addCallback(() => self.createPanels(userDetails));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private createPanels(userDetails: UserDetails) {
|
|
||||||
const self = this;
|
|
||||||
const tabs = [];
|
|
||||||
|
|
||||||
|
|
||||||
const roamingTags = new MultiInput("Add a tagrendering",
|
|
||||||
() => GenerateEmpty.createEmptyTagRendering(),
|
|
||||||
() => {
|
|
||||||
return new TagRenderingPanel(self.languages, self.currentlySelected, self.userDetails)
|
|
||||||
|
|
||||||
}, undefined, {allowMovement: true});
|
|
||||||
new SingleSetting(this._config, roamingTags, "roamingRenderings", "Roaming Renderings", "These tagrenderings are shown everywhere");
|
|
||||||
|
|
||||||
|
|
||||||
const backgroundLayers = AvailableBaseLayers.layerOverview.map(baselayer => ({shown:
|
|
||||||
baselayer.name, value: baselayer.id}));
|
|
||||||
const dropDown = new DropDown("Choose the default background layer",
|
|
||||||
[{value: "osm",shown:"OpenStreetMap <b>(default)</b>"}, ...backgroundLayers])
|
|
||||||
new SingleSetting(self._config, dropDown, "defaultBackgroundId", "Default background layer",
|
|
||||||
"Selects the background layer that is used by default. If this layer is not available at the given point, OSM-Carto will be ued");
|
|
||||||
|
|
||||||
const layers = this._config.data.layers;
|
|
||||||
for (let i = 0; i < layers.length; i++) {
|
|
||||||
tabs.push({
|
|
||||||
header: new VariableUiElement(this._config.map((config: LayoutConfigJson) => {
|
|
||||||
const layer = config.layers[i];
|
|
||||||
if (typeof layer !== "string") {
|
|
||||||
try {
|
|
||||||
const iconTagRendering = new TagRenderingConfig(layer["icon"], undefined, "icon")
|
|
||||||
const icon = iconTagRendering.GetRenderValue({"id": "node/-1"}).txt;
|
|
||||||
return `<img src='${icon}'>`
|
|
||||||
} catch (e) {
|
|
||||||
return Svg.bug_img
|
|
||||||
// Nothing to do here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Svg.help_img;
|
|
||||||
})),
|
|
||||||
content: new LayerPanelWithPreview(this._config, this.languages, i, userDetails)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
tabs.push({
|
|
||||||
header: Svg.layersAdd_img,
|
|
||||||
content: new Combine([
|
|
||||||
"<h2>Layer editor</h2>",
|
|
||||||
"In this tab page, you can add and edit the layers of the theme. Click the layers above or add a new layer to get started.",
|
|
||||||
new SubtleButton(
|
|
||||||
Svg.layersAdd_ui(),
|
|
||||||
"Add a new layer"
|
|
||||||
).onClick(() => {
|
|
||||||
self._config.data.layers.push(GenerateEmpty.createEmptyLayer())
|
|
||||||
self._config.ping();
|
|
||||||
}),
|
|
||||||
"<h2>Default background layer</h2>",
|
|
||||||
dropDown,
|
|
||||||
"<h2>TagRenderings for every layer</h2>",
|
|
||||||
"Define tag renderings and questions here that should be shown on every layer of the theme.",
|
|
||||||
roamingTags
|
|
||||||
]
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
this.panel = new TabbedComponent(tabs, new UIEventSource<number>(Math.max(0, layers.length - 1)));
|
|
||||||
this.Update();
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this.panel.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import GeneralSettings from "./GeneralSettings";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
|
||||||
import PageSplit from "../Base/PageSplit";
|
|
||||||
import AllLayersPanel from "./AllLayersPanel";
|
|
||||||
import SharePanel from "./SharePanel";
|
|
||||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import SavePanel from "./SavePanel";
|
|
||||||
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
|
|
||||||
import HelpText from "./HelpText";
|
|
||||||
import Svg from "../../Svg";
|
|
||||||
import Constants from "../../Models/Constants";
|
|
||||||
import LZString from "lz-string";
|
|
||||||
import {Utils} from "../../Utils";
|
|
||||||
|
|
||||||
export default class CustomGeneratorPanel extends UIElement {
|
|
||||||
private mainPanel: UIElement;
|
|
||||||
private loginButton: UIElement;
|
|
||||||
|
|
||||||
private readonly connection: OsmConnection;
|
|
||||||
|
|
||||||
constructor(connection: OsmConnection, layout: LayoutConfigJson) {
|
|
||||||
super(connection.userDetails);
|
|
||||||
this.connection = connection;
|
|
||||||
this.SetClass("main-tabs");
|
|
||||||
this.loginButton = new SubtleButton("", "Login to create a custom theme").onClick(() => connection.AttemptLogin())
|
|
||||||
const self = this;
|
|
||||||
self.mainPanel = new FixedUiElement("Attempting to log in...");
|
|
||||||
connection.OnLoggedIn(userDetails => {
|
|
||||||
self.InitMainPanel(layout, userDetails, connection);
|
|
||||||
self.Update();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private InitMainPanel(layout: LayoutConfigJson, userDetails: UserDetails, connection: OsmConnection) {
|
|
||||||
const es = new UIEventSource(layout);
|
|
||||||
const encoded = es.map(config => LZString.compressToBase64(Utils.MinifyJSON(JSON.stringify(config, null, 0))));
|
|
||||||
encoded.addCallback(encoded => LocalStorageSource.Get("last-custom-theme"))
|
|
||||||
const liveUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}#${encoded}`)
|
|
||||||
const testUrl = encoded.map(encoded => `./index.html?test=true&userlayout=${es.data.id}#${encoded}`)
|
|
||||||
const iframe = testUrl.map(url => `<iframe src='${url}' width='100%' height='99%' style="box-sizing: border-box" title='Theme Preview'></iframe>`);
|
|
||||||
const currentSetting = new UIEventSource<SingleSetting<any>>(undefined)
|
|
||||||
const generalSettings = new GeneralSettings(es, currentSetting);
|
|
||||||
const languages = generalSettings.languages;
|
|
||||||
|
|
||||||
const chronic = UIEventSource.Chronic(120 * 1000)
|
|
||||||
.map(date => {
|
|
||||||
if (es.data.id == undefined) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
if (es.data.id === "") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const pref = connection.GetLongPreference("installed-theme-" + es.data.id);
|
|
||||||
pref.setData(encoded.data);
|
|
||||||
return date;
|
|
||||||
});
|
|
||||||
|
|
||||||
const preview = new Combine([
|
|
||||||
new VariableUiElement(iframe)
|
|
||||||
]).SetClass("preview")
|
|
||||||
this.mainPanel = new TabbedComponent([
|
|
||||||
{
|
|
||||||
header: Svg.gear_img,
|
|
||||||
content:
|
|
||||||
new PageSplit(
|
|
||||||
generalSettings.SetStyle("width: 50vw;"),
|
|
||||||
new Combine([
|
|
||||||
new HelpText(currentSetting).SetStyle("height:calc(100% - 65vh); width: 100%; display:block; overflow-y: auto"),
|
|
||||||
preview.SetStyle("height:65vh; width:100%; display:block")
|
|
||||||
]).SetStyle("position:relative; width: 50%;")
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: Svg.layers_img,
|
|
||||||
content: new AllLayersPanel(es, languages, userDetails)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: Svg.floppy_img,
|
|
||||||
content: new SavePanel(this.connection, es, chronic)
|
|
||||||
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header:Svg.share_img,
|
|
||||||
content: new SharePanel(es, liveUrl, userDetails)
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
const ud = this.connection.userDetails.data;
|
|
||||||
if (!ud.loggedIn) {
|
|
||||||
return new Combine([
|
|
||||||
"<h3>Not Logged in</h3>",
|
|
||||||
"You need to be logged in in order to create a custom theme",
|
|
||||||
this.loginButton
|
|
||||||
]).Render();
|
|
||||||
}
|
|
||||||
const journey = Constants.userJourney;
|
|
||||||
if (ud.csCount <= journey.themeGeneratorReadOnlyUnlock) {
|
|
||||||
return new Combine([
|
|
||||||
"<h3>Too little experience</h3>",
|
|
||||||
`<p>Creating your own (readonly) themes can only be done if you have more then <b>${journey.themeGeneratorReadOnlyUnlock}</b> changesets made</p>`,
|
|
||||||
`<p>Making a theme including survey options can be done at <b>${journey.themeGeneratorFullUnlock}</b> changesets</p>`
|
|
||||||
]).Render();
|
|
||||||
}
|
|
||||||
return this.mainPanel.Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import SettingsTable from "./SettingsTable";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import {TextField} from "../Input/TextField";
|
|
||||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
|
||||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
|
||||||
|
|
||||||
|
|
||||||
export default class GeneralSettingsPanel extends UIElement {
|
|
||||||
private panel: Combine;
|
|
||||||
|
|
||||||
public languages : UIEventSource<string[]>;
|
|
||||||
|
|
||||||
constructor(configuration: UIEventSource<LayoutConfigJson>, currentSetting: UIEventSource<SingleSetting<any>>) {
|
|
||||||
super(undefined);
|
|
||||||
|
|
||||||
|
|
||||||
const languagesField =
|
|
||||||
ValidatedTextField.Mapped(
|
|
||||||
str => {
|
|
||||||
console.log("Language from str", str);
|
|
||||||
return str?.split(";")?.map(str => str.trim().toLowerCase());
|
|
||||||
},
|
|
||||||
languages => languages.join(";"));
|
|
||||||
this.languages = languagesField.GetValue();
|
|
||||||
|
|
||||||
const version = new TextField();
|
|
||||||
const current_datetime = new Date();
|
|
||||||
let formatted_date = current_datetime.getFullYear() + "-" + (current_datetime.getMonth() + 1) + "-" + current_datetime.getDate() + " " + current_datetime.getHours() + ":" + current_datetime.getMinutes() + ":" + current_datetime.getSeconds()
|
|
||||||
version.GetValue().setData(formatted_date);
|
|
||||||
|
|
||||||
|
|
||||||
const locationRemark = "<br/>Note that, as soon as an URL-parameter sets the location or a location is known due to a previous visit, that the theme-set location is ignored"
|
|
||||||
|
|
||||||
const settingsTable = new SettingsTable(
|
|
||||||
[
|
|
||||||
new SingleSetting(configuration, new TextField({placeholder:"id"}), "id",
|
|
||||||
"Identifier", "The identifier of this theme. This should be a lowercase, unique string"),
|
|
||||||
new SingleSetting(configuration, version, "version", "Version",
|
|
||||||
"A version to indicate the theme version. Ideal is the date you created or updated the theme"),
|
|
||||||
new SingleSetting(configuration, languagesField, "language",
|
|
||||||
"Supported languages", "Which languages do you want to support in this theme? Type the two letter code representing your language, seperated by <span class='literal-code'>;</span>. For example:<span class='literal-code'>en;nl</span> "),
|
|
||||||
new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "title",
|
|
||||||
"Title", "The title as shown in the welcome message, in the browser title bar, in the more screen, ..."),
|
|
||||||
new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "shortDescription","Short description",
|
|
||||||
"The short description is shown as subtext in the social preview and on the 'more screen'-buttons. It should be at most one sentence of around ~25words"),
|
|
||||||
new SingleSetting(configuration, new MultiLingualTextFields(this.languages, true),
|
|
||||||
"description", "Description", "The description is shown in the welcome-message when opening MapComplete. It is a small text welcoming users"),
|
|
||||||
new SingleSetting(configuration, new TextField({placeholder: "URL to icon"}), "icon",
|
|
||||||
"Icon", "A visual representation for your theme; used as logo in the welcomeMessage. If your theme is official, used as favicon and webapp logo",
|
|
||||||
{
|
|
||||||
showIconPreview: true
|
|
||||||
}),
|
|
||||||
|
|
||||||
new SingleSetting(configuration, ValidatedTextField.NumberInput("nat", n => n < 23), "startZoom","Initial zoom level",
|
|
||||||
"When a user first loads MapComplete, this zoomlevel is shown."+locationRemark),
|
|
||||||
new SingleSetting(configuration, ValidatedTextField.NumberInput("float", n => (n < 90 && n > -90)), "startLat","Initial latitude",
|
|
||||||
"When a user first loads MapComplete, this latitude is shown as location."+locationRemark),
|
|
||||||
new SingleSetting(configuration, ValidatedTextField.NumberInput("float", n => (n < 180 && n > -180)), "startLon","Initial longitude",
|
|
||||||
"When a user first loads MapComplete, this longitude is shown as location."+locationRemark),
|
|
||||||
|
|
||||||
new SingleSetting(configuration, ValidatedTextField.NumberInput("pfloat", n => (n < 0.5 )), "widenFactor","Query widening",
|
|
||||||
"When a query is run, the data within bounds of the visible map is loaded.\n" +
|
|
||||||
"However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data.\n" +
|
|
||||||
"For this, the bounds are widened in order to make a small pan still within bounds of the loaded data.\n" +
|
|
||||||
"IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries)"),
|
|
||||||
|
|
||||||
new SingleSetting(configuration, new TextField({placeholder: "URL to social image"}), "socialImage",
|
|
||||||
"og:image (aka Social Image)", "<span class='alert'>Only works on incorporated themes</span>" +
|
|
||||||
"The Social Image is set as og:image for the HTML-site and helps social networks to show a preview", {showIconPreview: true})
|
|
||||||
], currentSetting);
|
|
||||||
|
|
||||||
this.panel = new Combine([
|
|
||||||
"<h3>General theme settings</h3>",
|
|
||||||
settingsTable
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this.panel.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
|
|
||||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
|
||||||
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
|
|
||||||
|
|
||||||
export class GenerateEmpty {
|
|
||||||
public static createEmptyLayer(): LayerConfigJson {
|
|
||||||
return {
|
|
||||||
id: "yourlayer",
|
|
||||||
name: {},
|
|
||||||
minzoom: 12,
|
|
||||||
overpassTags: {and: [""]},
|
|
||||||
title: {},
|
|
||||||
description: {},
|
|
||||||
tagRenderings: [],
|
|
||||||
hideUnderlayingFeaturesMinPercentage: 0,
|
|
||||||
icon: {
|
|
||||||
render: "./assets/svg/bug.svg"
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
render: "8"
|
|
||||||
},
|
|
||||||
iconSize: {
|
|
||||||
render: "40,40,center"
|
|
||||||
},
|
|
||||||
color:{
|
|
||||||
render: "#00f"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createEmptyLayout(): LayoutConfigJson {
|
|
||||||
return {
|
|
||||||
id: "id",
|
|
||||||
title: {},
|
|
||||||
shortDescription: {},
|
|
||||||
description: {},
|
|
||||||
language: [],
|
|
||||||
maintainer: "",
|
|
||||||
icon: "./assets/svg/bug.svg",
|
|
||||||
version: "0",
|
|
||||||
startLat: 0,
|
|
||||||
startLon: 0,
|
|
||||||
startZoom: 1,
|
|
||||||
widenFactor: 0.05,
|
|
||||||
socialImage: "",
|
|
||||||
|
|
||||||
layers: [
|
|
||||||
GenerateEmpty.createEmptyLayer()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createTestLayout(): LayoutConfigJson {
|
|
||||||
return {
|
|
||||||
id: "test",
|
|
||||||
title: {"en": "Test layout"},
|
|
||||||
shortDescription: {},
|
|
||||||
description: {"en": "A layout for testing"},
|
|
||||||
language: ["en"],
|
|
||||||
maintainer: "Pieter Vander Vennet",
|
|
||||||
icon: "./assets/svg/bug.svg",
|
|
||||||
version: "0",
|
|
||||||
startLat: 0,
|
|
||||||
startLon: 0,
|
|
||||||
startZoom: 1,
|
|
||||||
widenFactor: 0.05,
|
|
||||||
socialImage: "",
|
|
||||||
layers: [{
|
|
||||||
id: "testlayer",
|
|
||||||
name: {en:"Testing layer"},
|
|
||||||
minzoom: 15,
|
|
||||||
overpassTags: {and: ["highway=residential"]},
|
|
||||||
title: {},
|
|
||||||
description: {"en": "Some Description"},
|
|
||||||
icon: {render: {en: "./assets/svg/pencil.svg"}},
|
|
||||||
width: {render: {en: "5"}},
|
|
||||||
tagRenderings: [{
|
|
||||||
render: {"en":"Test Rendering"}
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createEmptyTagRendering(): TagRenderingConfigJson {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import Svg from "../../Svg";
|
|
||||||
|
|
||||||
export default class HelpText extends UIElement {
|
|
||||||
|
|
||||||
private helpText: UIElement;
|
|
||||||
private returnButton: UIElement;
|
|
||||||
|
|
||||||
constructor(currentSetting: UIEventSource<SingleSetting<any>>) {
|
|
||||||
super();
|
|
||||||
this.returnButton = new SubtleButton(Svg.close_ui(),
|
|
||||||
new VariableUiElement(
|
|
||||||
currentSetting.map(currentSetting => {
|
|
||||||
if (currentSetting === undefined) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return "Return to general help";
|
|
||||||
}
|
|
||||||
)
|
|
||||||
))
|
|
||||||
.ListenTo(currentSetting)
|
|
||||||
.SetClass("small-button")
|
|
||||||
.onClick(() => currentSetting.setData(undefined));
|
|
||||||
|
|
||||||
|
|
||||||
this.helpText = new VariableUiElement(currentSetting.map((setting: SingleSetting<any>) => {
|
|
||||||
if (setting === undefined) {
|
|
||||||
return "<h1>Welcome to the Custom Theme Builder</h1>" +
|
|
||||||
"Here, one can make their own custom mapcomplete themes.<br/>" +
|
|
||||||
"Fill out the fields to get a working mapcomplete theme. More information on the selected field will appear here when you click it.<br/>" +
|
|
||||||
"Want to see how the quests are doing in number of visits? All the stats are open on <a href='https://pietervdvn.goatcounter.com' target='_blank'>goatcounter</a>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Combine(["<h1>", setting._name, "</h1>", setting._description.Render()]).Render();
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return new Combine([this.helpText,
|
|
||||||
this.returnButton,
|
|
||||||
]).Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,251 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
|
||||||
import SettingsTable from "./SettingsTable";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import {TextField} from "../Input/TextField";
|
|
||||||
import {InputElement} from "../Input/InputElement";
|
|
||||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
|
||||||
import CheckBox from "../Input/CheckBox";
|
|
||||||
import AndOrTagInput from "../Input/AndOrTagInput";
|
|
||||||
import TagRenderingPanel from "./TagRenderingPanel";
|
|
||||||
import {DropDown} from "../Input/DropDown";
|
|
||||||
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
|
|
||||||
import {MultiInput} from "../Input/MultiInput";
|
|
||||||
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
|
|
||||||
import PresetInputPanel from "./PresetInputPanel";
|
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
|
||||||
import Svg from "../../Svg";
|
|
||||||
import Constants from "../../Models/Constants";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the configuration for a single layer
|
|
||||||
*/
|
|
||||||
export default class LayerPanel extends UIElement {
|
|
||||||
private readonly _config: UIEventSource<LayoutConfigJson>;
|
|
||||||
|
|
||||||
private readonly settingsTable: UIElement;
|
|
||||||
private readonly mapRendering: UIElement;
|
|
||||||
|
|
||||||
private readonly deleteButton: UIElement;
|
|
||||||
|
|
||||||
public readonly titleRendering: UIElement;
|
|
||||||
|
|
||||||
public readonly selectedTagRendering: UIEventSource<TagRenderingPanel>
|
|
||||||
= new UIEventSource<TagRenderingPanel>(undefined);
|
|
||||||
private tagRenderings: UIElement;
|
|
||||||
private presetsPanel: UIElement;
|
|
||||||
|
|
||||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
|
||||||
languages: UIEventSource<string[]>,
|
|
||||||
index: number,
|
|
||||||
currentlySelected: UIEventSource<SingleSetting<any>>,
|
|
||||||
userDetails: UserDetails) {
|
|
||||||
super();
|
|
||||||
this._config = config;
|
|
||||||
this.mapRendering = this.setupRenderOptions(config, languages, index, currentlySelected, userDetails);
|
|
||||||
|
|
||||||
const actualDeleteButton = new SubtleButton(
|
|
||||||
Svg.delete_icon_ui(),
|
|
||||||
"Yes, delete this layer"
|
|
||||||
).onClick(() => {
|
|
||||||
config.data.layers.splice(index, 1);
|
|
||||||
config.ping();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.deleteButton = new CheckBox(
|
|
||||||
new Combine(
|
|
||||||
[
|
|
||||||
"<h3>Confirm layer deletion</h3>",
|
|
||||||
new SubtleButton(
|
|
||||||
Svg.close_ui(),
|
|
||||||
"No, don't delete"
|
|
||||||
),
|
|
||||||
"<span class='alert'>Deleting a layer can not be undone!</span>",
|
|
||||||
actualDeleteButton
|
|
||||||
]
|
|
||||||
),
|
|
||||||
new SubtleButton(
|
|
||||||
Svg.delete_icon_ui(),
|
|
||||||
"Remove this layer"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
function setting(input: InputElement<any>, path: string | string[], name: string, description: string | UIElement): SingleSetting<any> {
|
|
||||||
let pathPre = ["layers", index];
|
|
||||||
if (typeof (path) === "string") {
|
|
||||||
pathPre.push(path);
|
|
||||||
} else {
|
|
||||||
pathPre = pathPre.concat(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SingleSetting<any>(config, input, pathPre, name, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.settingsTable = new SettingsTable([
|
|
||||||
setting(new TextField({placeholder:"Layer id"}), "id", "Id", "An identifier for this layer<br/>This should be a simple, lowercase, human readable string that is used to identify the layer."),
|
|
||||||
setting(new MultiLingualTextFields(languages), "name", "Name", "The human-readable name of this layer<br/>Used in the layer control panel and the 'Personal theme'"),
|
|
||||||
setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.<br/>Shown in the layer selections and in the personal theme"),
|
|
||||||
setting(ValidatedTextField.NumberInput("nat", n => n < 23), "minzoom", "Minimal zoom",
|
|
||||||
"The minimum zoomlevel needed to load and show this layer."),
|
|
||||||
setting(new DropDown("", [
|
|
||||||
{value: 0, shown: "Show ways and areas as ways and lines"},
|
|
||||||
{value: 2, shown: "Show both the ways/areas and the centerpoints"},
|
|
||||||
{value: 1, shown: "Show everything as centerpoint"}]), "wayHandling", "Way handling",
|
|
||||||
"Describes how ways and areas are represented on the map: areas can be represented as the area itself, or it can be converted into the centerpoint"),
|
|
||||||
setting(new AndOrTagInput(), ["overpassTags"], "Overpass query",
|
|
||||||
"The tags of the objects to load from overpass"),
|
|
||||||
|
|
||||||
],
|
|
||||||
currentlySelected);
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const popupTitleRendering = new TagRenderingPanel(languages, currentlySelected, userDetails, {
|
|
||||||
title: "Popup title",
|
|
||||||
description: "This is the rendering shown as title in the popup for this element",
|
|
||||||
disableQuestions: true
|
|
||||||
});
|
|
||||||
|
|
||||||
new SingleSetting(config, popupTitleRendering, ["layers", index, "title"], "Popup title", "This is the rendering shown as title in the popup");
|
|
||||||
this.titleRendering = popupTitleRendering;
|
|
||||||
this.registerTagRendering(popupTitleRendering);
|
|
||||||
|
|
||||||
|
|
||||||
const renderings = config.map(config => {
|
|
||||||
const layer = config.layers[index] as LayerConfigJson;
|
|
||||||
// @ts-ignore
|
|
||||||
const renderings : TagRenderingConfigJson[] = layer.tagRenderings ;
|
|
||||||
return renderings;
|
|
||||||
});
|
|
||||||
const tagRenderings = new MultiInput<TagRenderingConfigJson>("Add a tag rendering/question",
|
|
||||||
() => ({}),
|
|
||||||
() => {
|
|
||||||
const tagPanel = new TagRenderingPanel(languages, currentlySelected, userDetails)
|
|
||||||
self.registerTagRendering(tagPanel);
|
|
||||||
return tagPanel;
|
|
||||||
}, renderings,
|
|
||||||
{allowMovement: true});
|
|
||||||
|
|
||||||
tagRenderings.GetValue().addCallback(
|
|
||||||
tagRenderings => {
|
|
||||||
(config.data.layers[index] as LayerConfigJson).tagRenderings = tagRenderings;
|
|
||||||
config.ping();
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (userDetails.csCount >= Constants.userJourney.themeGeneratorFullUnlock) {
|
|
||||||
|
|
||||||
const presetPanel = new MultiInput("Add a preset",
|
|
||||||
() => ({tags: [], title: {}}),
|
|
||||||
() => new PresetInputPanel(currentlySelected, languages),
|
|
||||||
undefined, {allowMovement: true});
|
|
||||||
new SingleSetting(config, presetPanel, ["layers", index, "presets"], "Presets", "")
|
|
||||||
this.presetsPanel = presetPanel;
|
|
||||||
} else {
|
|
||||||
this.presetsPanel = new FixedUiElement(`Creating a custom theme which also edits OSM is only unlocked after ${Constants.userJourney.themeGeneratorFullUnlock} changesets`).SetClass("alert");
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTagRenderings() {
|
|
||||||
const values = (config.data.layers[index] as LayerConfigJson).tagRenderings;
|
|
||||||
const renderings: TagRenderingConfigJson[] = [];
|
|
||||||
for (const value of values) {
|
|
||||||
if (typeof (value) !== "string") {
|
|
||||||
renderings.push(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
tagRenderings.GetValue().setData(renderings);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTagRenderings();
|
|
||||||
|
|
||||||
this.tagRenderings = tagRenderings;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupRenderOptions(config: UIEventSource<LayoutConfigJson>,
|
|
||||||
languages: UIEventSource<string[]>,
|
|
||||||
index: number,
|
|
||||||
currentlySelected: UIEventSource<SingleSetting<any>>,
|
|
||||||
userDetails: UserDetails
|
|
||||||
): UIElement {
|
|
||||||
const iconSelect = new TagRenderingPanel(
|
|
||||||
languages, currentlySelected, userDetails,
|
|
||||||
{
|
|
||||||
title: "Icon",
|
|
||||||
description: "A visual representation for this layer and for the points on the map.",
|
|
||||||
disableQuestions: true,
|
|
||||||
noLanguage: true
|
|
||||||
});
|
|
||||||
const size = new TagRenderingPanel(languages, currentlySelected, userDetails,
|
|
||||||
{
|
|
||||||
title: "Icon Size",
|
|
||||||
description: "The size of the icons on the map in pixels. Can vary based on the tagging",
|
|
||||||
disableQuestions: true,
|
|
||||||
noLanguage: true
|
|
||||||
});
|
|
||||||
const color = new TagRenderingPanel(languages, currentlySelected, userDetails,
|
|
||||||
{
|
|
||||||
title: "Way and area color",
|
|
||||||
description: "The color or a shown way or area. Can vary based on the tagging",
|
|
||||||
disableQuestions: true,
|
|
||||||
noLanguage: true
|
|
||||||
});
|
|
||||||
const stroke = new TagRenderingPanel(languages, currentlySelected, userDetails,
|
|
||||||
{
|
|
||||||
title: "Stroke width",
|
|
||||||
description: "The width of lines representing ways and the outline of areas. Can vary based on the tags",
|
|
||||||
disableQuestions: true,
|
|
||||||
noLanguage: true
|
|
||||||
});
|
|
||||||
this.registerTagRendering(iconSelect);
|
|
||||||
this.registerTagRendering(size);
|
|
||||||
this.registerTagRendering(color);
|
|
||||||
this.registerTagRendering(stroke);
|
|
||||||
|
|
||||||
function setting(input: InputElement<any>, path, isIcon: boolean = false): SingleSetting<TagRenderingConfigJson> {
|
|
||||||
return new SingleSetting(config, input, ["layers", index, path], undefined, undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SettingsTable([
|
|
||||||
setting(iconSelect, "icon"),
|
|
||||||
setting(size, "iconSize"),
|
|
||||||
setting(color, "color"),
|
|
||||||
setting(stroke, "width")
|
|
||||||
], currentlySelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerTagRendering(
|
|
||||||
tagRenderingPanel: TagRenderingPanel) {
|
|
||||||
|
|
||||||
tagRenderingPanel.IsHovered().addCallback(isHovering => {
|
|
||||||
if (!isHovering) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.selectedTagRendering.setData(tagRenderingPanel);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return new Combine([
|
|
||||||
"<h2>General layer settings</h2>",
|
|
||||||
this.settingsTable,
|
|
||||||
"<h2>Popup contents</h2>",
|
|
||||||
this.titleRendering,
|
|
||||||
this.tagRenderings,
|
|
||||||
"<h2>Presets</h2>",
|
|
||||||
"Does this theme support adding a new point?<br/>If this should be the case, add a preset. Make sure that the preset tags do match the overpass-tags, otherwise it might seem like the newly added points dissapear ",
|
|
||||||
this.presetsPanel,
|
|
||||||
"<h2>Map rendering options</h2>",
|
|
||||||
this.mapRendering,
|
|
||||||
"<h2>Layer delete</h2>",
|
|
||||||
this.deleteButton
|
|
||||||
]).Render();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import LayerPanel from "./LayerPanel";
|
|
||||||
import HelpText from "./HelpText";
|
|
||||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
|
||||||
import {FromJSON} from "../../Customizations/JSON/FromJSON";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import PageSplit from "../Base/PageSplit";
|
|
||||||
import TagRenderingPreview from "./TagRenderingPreview";
|
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
|
||||||
|
|
||||||
|
|
||||||
export default class LayerPanelWithPreview extends UIElement{
|
|
||||||
private panel: UIElement;
|
|
||||||
constructor(config: UIEventSource<any>, languages: UIEventSource<string[]>, index: number, userDetails: UserDetails) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
const currentlySelected = new UIEventSource<(SingleSetting<any>)>(undefined);
|
|
||||||
const layer = new LayerPanel(config, languages, index, currentlySelected, userDetails);
|
|
||||||
const helpText = new HelpText(currentlySelected);
|
|
||||||
|
|
||||||
const previewTagInput = new MultiTagInput();
|
|
||||||
previewTagInput.GetValue().setData(["id=123456"]);
|
|
||||||
|
|
||||||
const previewTagValue = previewTagInput.GetValue().map(tags => {
|
|
||||||
const properties = {};
|
|
||||||
for (const str of tags) {
|
|
||||||
const tag = FromJSON.SimpleTag(str);
|
|
||||||
if (tag !== undefined) {
|
|
||||||
properties[tag.key] = tag.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return properties;
|
|
||||||
});
|
|
||||||
|
|
||||||
const preview = new TagRenderingPreview(layer.selectedTagRendering, previewTagValue);
|
|
||||||
|
|
||||||
this.panel = new PageSplit(
|
|
||||||
layer.SetClass("scrollable"),
|
|
||||||
new Combine([
|
|
||||||
helpText,
|
|
||||||
"</br>",
|
|
||||||
"<h2>Testing tags</h2>",
|
|
||||||
previewTagInput,
|
|
||||||
"<h2>Tag Rendering preview</h2>",
|
|
||||||
preview
|
|
||||||
|
|
||||||
]), 60
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this.panel.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
import {InputElement} from "../Input/InputElement";
|
|
||||||
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import SettingsTable from "./SettingsTable";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import AndOrTagInput from "../Input/AndOrTagInput";
|
|
||||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
|
||||||
import {DropDown} from "../Input/DropDown";
|
|
||||||
|
|
||||||
export default class MappingInput extends InputElement<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }> {
|
|
||||||
|
|
||||||
private readonly _value: UIEventSource<{ if: AndOrTagConfigJson; then: any; hideInAnswer?: boolean }>;
|
|
||||||
private readonly _panel: UIElement;
|
|
||||||
|
|
||||||
constructor(languages: UIEventSource<any>, disableQuestions: boolean = false) {
|
|
||||||
super();
|
|
||||||
const currentSelected = new UIEventSource<SingleSetting<any>>(undefined);
|
|
||||||
this._value = new UIEventSource<{ if: AndOrTagConfigJson, then: any, hideInAnswer?: boolean }>({
|
|
||||||
if: undefined,
|
|
||||||
then: undefined
|
|
||||||
});
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
function setting(inputElement: InputElement<any>, path: string, name: string, description: string | UIElement) {
|
|
||||||
return new SingleSetting(self._value, inputElement, path, name, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
const withQuestions = [setting(new DropDown("",
|
|
||||||
[{value: false, shown: "Can be used as answer"}, {value: true, shown: "Not an answer option"}]),
|
|
||||||
"hideInAnswer", "Answer option",
|
|
||||||
"Sometimes, multiple tags for the same meaning are used (e.g. <span class='literal-code'>access=yes</span> and <span class='literal-code'>access=public</span>)." +
|
|
||||||
"Use this toggle to disable an anwer. Alternatively an implied/assumed rendering can be used. In order to do this:" +
|
|
||||||
"use a single tag in the 'if' with <i>no</i> value defined, e.g. <span class='literal-code'>indoor=</span>. The mapping will then be shown as default until explicitly changed"
|
|
||||||
)];
|
|
||||||
|
|
||||||
this._panel = new SettingsTable([
|
|
||||||
setting(new AndOrTagInput(), "if", "If matches", "If this condition matches, the template <b>then</b> below will be used"),
|
|
||||||
setting(new MultiLingualTextFields(languages),
|
|
||||||
"then", "Then show", "If the condition above matches, this template <b>then</b> below will be shown to the user."),
|
|
||||||
...(disableQuestions ? [] : withQuestions)
|
|
||||||
|
|
||||||
], currentSelected).SetClass("bordered tag-mapping");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this._panel.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
GetValue(): UIEventSource<{ if: AndOrTagConfigJson; then: any; hideInAnswer?: boolean }> {
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
|
||||||
|
|
||||||
IsValid(t: { if: AndOrTagConfigJson; then: any; hideInAnswer: boolean }): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import {InputElement} from "../Input/InputElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
|
||||||
import SettingsTable from "./SettingsTable";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
|
|
||||||
export default class PresetInputPanel extends InputElement<{
|
|
||||||
title: string | any,
|
|
||||||
tags: string[],
|
|
||||||
description?: string | any
|
|
||||||
}> {
|
|
||||||
private readonly _value: UIEventSource<{
|
|
||||||
title: string | any,
|
|
||||||
tags: string[],
|
|
||||||
description?: string | any
|
|
||||||
}>;
|
|
||||||
private readonly panel: UIElement;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(currentlySelected: UIEventSource<SingleSetting<any>>, languages: UIEventSource<string[]>) {
|
|
||||||
super();
|
|
||||||
this._value = new UIEventSource({tags: [], title: {}});
|
|
||||||
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
function s(input: InputElement<any>, path: string, name: string, description: string){
|
|
||||||
return new SingleSetting(self._value, input, path, name, description)
|
|
||||||
}
|
|
||||||
this.panel = new SettingsTable([
|
|
||||||
s(new MultiTagInput(), "tags","Preset tags","These tags will be applied on the newly created point"),
|
|
||||||
s(new MultiLingualTextFields(languages), "title","Preset title","This little text is shown in bold on the 'create new point'-button" ),
|
|
||||||
s(new MultiLingualTextFields(languages), "description","Description", "This text is shown in the button as description when creating a new point")
|
|
||||||
], currentlySelected).SetStyle("display: block; border: 1px solid black; border-radius: 1em;padding: 1em;");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return new Combine([this.panel]).Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
GetValue(): UIEventSource<{
|
|
||||||
title: string | any,
|
|
||||||
tags: string[],
|
|
||||||
description?: string | any
|
|
||||||
}> {
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
|
||||||
|
|
||||||
IsValid(t: any): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import {TextField} from "../Input/TextField";
|
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
|
||||||
import Svg from "../../Svg";
|
|
||||||
|
|
||||||
export default class SavePanel extends UIElement {
|
|
||||||
private json: UIElement;
|
|
||||||
private lastSaveEl: UIElement;
|
|
||||||
private loadFromJson: UIElement;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
connection: OsmConnection,
|
|
||||||
config: UIEventSource<LayoutConfigJson>,
|
|
||||||
chronic: UIEventSource<Date>) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
|
|
||||||
this.lastSaveEl = new VariableUiElement(chronic
|
|
||||||
.map(date => {
|
|
||||||
if (date === undefined) {
|
|
||||||
return new FixedUiElement("Your theme will be saved automatically within two minutes... Click here to force saving").SetClass("alert").Render()
|
|
||||||
}
|
|
||||||
return "Your theme was last saved at " + date.toISOString()
|
|
||||||
})).onClick(() => chronic.setData(new Date()));
|
|
||||||
|
|
||||||
const jsonStr = config.map(config =>
|
|
||||||
JSON.stringify(config, null, 2));
|
|
||||||
|
|
||||||
|
|
||||||
const jsonTextField = new TextField({
|
|
||||||
placeholder: "JSON Config",
|
|
||||||
value: jsonStr,
|
|
||||||
textArea: true,
|
|
||||||
textAreaRows: 20
|
|
||||||
});
|
|
||||||
this.json = jsonTextField;
|
|
||||||
this.loadFromJson = new SubtleButton(Svg.reload_ui(), "<b>Load the JSON file below</b>")
|
|
||||||
.onClick(() => {
|
|
||||||
try{
|
|
||||||
const json = jsonTextField.GetValue().data;
|
|
||||||
const parsed : LayoutConfigJson = JSON.parse(json);
|
|
||||||
config.setData(parsed);
|
|
||||||
}catch(e){
|
|
||||||
alert("Invalid JSON: "+e)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return new Combine([
|
|
||||||
"<h3>Save your theme</h3>",
|
|
||||||
this.lastSaveEl,
|
|
||||||
"<h3>JSON configuration</h3>",
|
|
||||||
"The url hash is actually no more then a BASE64-encoding of the below JSON. This json contains the full configuration of the theme.<br/>" +
|
|
||||||
"This configuration is mainly useful for debugging",
|
|
||||||
"<br/>",
|
|
||||||
this.loadFromJson,
|
|
||||||
this.json
|
|
||||||
]).SetClass("scrollable")
|
|
||||||
.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import PageSplit from "../Base/PageSplit";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
|
|
||||||
export default class SettingsTable extends UIElement {
|
|
||||||
|
|
||||||
private _col1: UIElement[] = [];
|
|
||||||
private _col2: UIElement[] = [];
|
|
||||||
|
|
||||||
public selectedSetting: UIEventSource<SingleSetting<any>>;
|
|
||||||
|
|
||||||
constructor(elements: (SingleSetting<any> | string)[],
|
|
||||||
currentSelectedSetting?: UIEventSource<SingleSetting<any>>) {
|
|
||||||
super(undefined);
|
|
||||||
const self = this;
|
|
||||||
this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined);
|
|
||||||
for (const element of elements) {
|
|
||||||
if(typeof element === "string"){
|
|
||||||
this._col1.push(new FixedUiElement(element));
|
|
||||||
this._col2.push(null);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let title: UIElement = element._name === undefined ? null : new FixedUiElement(element._name);
|
|
||||||
this._col1.push(title);
|
|
||||||
this._col2.push(element._value);
|
|
||||||
element._value.SetStyle("display:block");
|
|
||||||
element._value.IsSelected.addCallback(isSelected => {
|
|
||||||
if (isSelected) {
|
|
||||||
self.selectedSetting.setData(element);
|
|
||||||
} else if (self.selectedSetting.data === element) {
|
|
||||||
self.selectedSetting.setData(undefined);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
let elements = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < this._col1.length; i++) {
|
|
||||||
if(this._col1[i] !== null && this._col2[i] !== null){
|
|
||||||
elements.push(new PageSplit(this._col1[i], this._col2[i], 25));
|
|
||||||
}else if(this._col1[i] !== null){
|
|
||||||
elements.push(this._col1[i])
|
|
||||||
}else{
|
|
||||||
elements.push(this._col2[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Combine(elements).Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
|
||||||
|
|
||||||
export default class SharePanel extends UIElement {
|
|
||||||
private _config: UIEventSource<LayoutConfigJson>;
|
|
||||||
|
|
||||||
private _panel: UIElement;
|
|
||||||
|
|
||||||
constructor(config: UIEventSource<LayoutConfigJson>, liveUrl: UIEventSource<string>, userDetails: UserDetails) {
|
|
||||||
super(undefined);
|
|
||||||
this._config = config;
|
|
||||||
|
|
||||||
this._panel = new Combine([
|
|
||||||
"<h2>Share</h2>",
|
|
||||||
"Share the following link with friends:<br/>",
|
|
||||||
new VariableUiElement(liveUrl.map(url => `<a href='${url}' target="_blank">${url}</a>`)),
|
|
||||||
"<h2>Publish on some website</h2>",
|
|
||||||
|
|
||||||
"It is possible to load a JSON-file from the wide internet, but you'll need some (public CORS-enabled) server.",
|
|
||||||
`Put the raw json online, and use ${window.location.host}?userlayout=https://<your-url-here>.json`,
|
|
||||||
"Please note: it used to be possible to load from the wiki - this is not possible anymore due to technical reasons.",
|
|
||||||
"</div>"
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this._panel.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {InputElement} from "../Input/InputElement";
|
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import Translations from "../i18n/Translations";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
|
|
||||||
export default class SingleSetting<T> {
|
|
||||||
public _value: InputElement<T>;
|
|
||||||
public _name: string;
|
|
||||||
public _description: UIElement;
|
|
||||||
public _options: { showIconPreview?: boolean };
|
|
||||||
|
|
||||||
constructor(config: UIEventSource<any>,
|
|
||||||
value: InputElement<T>,
|
|
||||||
path: string | (string | number)[],
|
|
||||||
name: string,
|
|
||||||
description: string | UIElement,
|
|
||||||
options?: {
|
|
||||||
showIconPreview?: boolean
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
this._value = value;
|
|
||||||
this._name = name;
|
|
||||||
this._description = Translations.W(description);
|
|
||||||
|
|
||||||
this._options = options ?? {};
|
|
||||||
if (this._options.showIconPreview) {
|
|
||||||
this._description = new Combine([
|
|
||||||
this._description,
|
|
||||||
"<h3>Icon preview</h3>",
|
|
||||||
new VariableUiElement(this._value.GetValue().map(url => `<img src='${url}' class="image-large-preview">`))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(typeof (path) === "string"){
|
|
||||||
path = [path];
|
|
||||||
}
|
|
||||||
const lastPart = path[path.length - 1];
|
|
||||||
path.splice(path.length - 1, 1);
|
|
||||||
|
|
||||||
function assignValue(value) {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We have to rewalk every time as parts might be new
|
|
||||||
let configPart = config.data;
|
|
||||||
for (const pathPart of path) {
|
|
||||||
let newConfigPart = configPart[pathPart];
|
|
||||||
if (newConfigPart === undefined) {
|
|
||||||
if (typeof (pathPart) === "string") {
|
|
||||||
configPart[pathPart] = {};
|
|
||||||
} else {
|
|
||||||
configPart[pathPart] = [];
|
|
||||||
}
|
|
||||||
newConfigPart = configPart[pathPart];
|
|
||||||
}
|
|
||||||
configPart = newConfigPart;
|
|
||||||
}
|
|
||||||
configPart[lastPart] = value;
|
|
||||||
config.ping();
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadValue() {
|
|
||||||
let configPart = config.data;
|
|
||||||
for (const pathPart of path) {
|
|
||||||
configPart = configPart[pathPart];
|
|
||||||
if (configPart === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const loadedValue = configPart[lastPart];
|
|
||||||
if (loadedValue !== undefined) {
|
|
||||||
value.GetValue().setData(loadedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadValue();
|
|
||||||
config.addCallback(() => loadValue());
|
|
||||||
|
|
||||||
value.GetValue().addCallback(assignValue);
|
|
||||||
assignValue(this._value.GetValue().data);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {InputElement} from "../Input/InputElement";
|
|
||||||
import SingleSetting from "./SingleSetting";
|
|
||||||
import SettingsTable from "./SettingsTable";
|
|
||||||
import {TextField} from "../Input/TextField";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
|
||||||
import AndOrTagInput from "../Input/AndOrTagInput";
|
|
||||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
|
||||||
import {MultiInput} from "../Input/MultiInput";
|
|
||||||
import MappingInput from "./MappingInput";
|
|
||||||
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
|
||||||
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
|
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
|
||||||
import SpecialVisualizations from "../SpecialVisualizations";
|
|
||||||
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
|
||||||
import Constants from "../../Models/Constants";
|
|
||||||
|
|
||||||
export default class TagRenderingPanel extends InputElement<TagRenderingConfigJson> {
|
|
||||||
|
|
||||||
public IsImage = false;
|
|
||||||
public options: { title?: string; description?: string; disableQuestions?: boolean; isImage?: boolean; };
|
|
||||||
public readonly validText: UIElement;
|
|
||||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
|
||||||
private intro: UIElement;
|
|
||||||
private settingsTable: UIElement;
|
|
||||||
private readonly _value: UIEventSource<TagRenderingConfigJson>;
|
|
||||||
|
|
||||||
constructor(languages: UIEventSource<string[]>,
|
|
||||||
currentlySelected: UIEventSource<SingleSetting<any>>,
|
|
||||||
userDetails: UserDetails,
|
|
||||||
options?: {
|
|
||||||
title?: string,
|
|
||||||
description?: string,
|
|
||||||
disableQuestions?: boolean,
|
|
||||||
isImage?: boolean,
|
|
||||||
noLanguage?: boolean
|
|
||||||
}) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.SetClass("bordered");
|
|
||||||
this.SetClass("min-height");
|
|
||||||
|
|
||||||
this.options = options ?? {};
|
|
||||||
const questionsNotUnlocked = userDetails.csCount < Constants.userJourney.themeGeneratorFullUnlock;
|
|
||||||
this.options.disableQuestions =
|
|
||||||
(this.options.disableQuestions ?? false) ||
|
|
||||||
questionsNotUnlocked;
|
|
||||||
|
|
||||||
this.intro = new Combine(["<h3>", options?.title ?? "TagRendering", "</h3>",
|
|
||||||
options?.description ?? "A tagrendering converts OSM-tags into a value on screen. Fill out the field 'render' with the text that should appear. Note that `{key}` will be replaced with the corresponding `value`, if present.<br/>For specific known tags (e.g. if `foo=bar`, make a mapping). "])
|
|
||||||
this.IsImage = options?.isImage ?? false;
|
|
||||||
|
|
||||||
const value = new UIEventSource<TagRenderingConfigJson>({});
|
|
||||||
this._value = value;
|
|
||||||
|
|
||||||
function setting(input: InputElement<any>, id: string | string[], name: string, description: string | UIElement): SingleSetting<any> {
|
|
||||||
return new SingleSetting<any>(value, input, id, name, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._value.addCallback(value => {
|
|
||||||
let doPing = false;
|
|
||||||
if (value?.freeform?.key == "") {
|
|
||||||
value.freeform = undefined;
|
|
||||||
doPing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value?.render == "") {
|
|
||||||
value.render = undefined;
|
|
||||||
doPing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doPing) {
|
|
||||||
this._value.ping();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const questionSettings = [
|
|
||||||
|
|
||||||
|
|
||||||
setting(options?.noLanguage ? new TextField({placeholder: "question"}) : new MultiLingualTextFields(languages)
|
|
||||||
, "question", "Question", "If the key or mapping doesn't match, this question is asked"),
|
|
||||||
|
|
||||||
"<h3>Freeform key</h3>",
|
|
||||||
setting(ValidatedTextField.KeyInput(true), ["freeform", "key"], "Freeform key<br/>",
|
|
||||||
"If specified, the rendering will search if this key is present." +
|
|
||||||
"If it is, the rendering above will be used to display the element.<br/>" +
|
|
||||||
"The rendering will go into question mode if <ul><li>this key is not present</li><li>No single mapping matches</li><li>A question is given</li>"),
|
|
||||||
|
|
||||||
setting(ValidatedTextField.TypeDropdown(), ["freeform", "type"], "Freeform type",
|
|
||||||
"The type of this freeform text field, in order to validate"),
|
|
||||||
setting(new MultiTagInput(), ["freeform", "addExtraTags"], "Extra tags on freeform",
|
|
||||||
"When the freeform text field is used, the user might mean a predefined key. This field allows to add extra tags, e.g. <span class='literal-code'>fixme=User used a freeform field - to check</span>"),
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
const settings: (string | SingleSetting<any>)[] = [
|
|
||||||
setting(
|
|
||||||
options?.noLanguage ? new TextField({placeholder: "Rendering"}) :
|
|
||||||
new MultiLingualTextFields(languages), "render", "Value to show",
|
|
||||||
"Renders this value. Note that <span class='literal-code'>{key}</span>-parts are substituted by the corresponding values of the element. If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value." +
|
|
||||||
"<br/><br/>" +
|
|
||||||
"Furhtermore, some special functions are supported:" + SpecialVisualizations.HelpMessage.Render()),
|
|
||||||
|
|
||||||
questionsNotUnlocked ? `You need at least ${Constants.userJourney.themeGeneratorFullUnlock} changesets to unlock the 'question'-field and to use your theme to edit OSM data` : "",
|
|
||||||
...(options?.disableQuestions ? [] : questionSettings),
|
|
||||||
|
|
||||||
"<h3>Mappings</h3>",
|
|
||||||
setting(new MultiInput<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }>("Add a mapping",
|
|
||||||
() => ({if: {and: []}, then: {}}),
|
|
||||||
() => new MappingInput(languages, options?.disableQuestions ?? false),
|
|
||||||
undefined, {allowMovement: true}), "mappings",
|
|
||||||
"If a tag matches, then show the first respective text", ""),
|
|
||||||
|
|
||||||
"<h3>Condition</h3>",
|
|
||||||
setting(new AndOrTagInput(), "condition", "Only show this tagrendering if the following condition applies",
|
|
||||||
"Only show this tag rendering if these tags matches. Optional field.<br/>Note that the Overpass-tags are already always included in this object"),
|
|
||||||
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
this.settingsTable = new SettingsTable(settings, currentlySelected);
|
|
||||||
|
|
||||||
|
|
||||||
this.validText = new VariableUiElement(value.map((json: TagRenderingConfigJson) => {
|
|
||||||
try {
|
|
||||||
new TagRenderingConfig(json, undefined, options?.title ?? "");
|
|
||||||
return "";
|
|
||||||
} catch (e) {
|
|
||||||
return "<span class='alert'>" + e + "</span>"
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return new Combine([
|
|
||||||
this.intro,
|
|
||||||
this.settingsTable,
|
|
||||||
this.validText]).Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
GetValue(): UIEventSource<TagRenderingConfigJson> {
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsValid(t: TagRenderingConfigJson): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import TagRenderingPanel from "./TagRenderingPanel";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
|
||||||
import EditableTagRendering from "../Popup/EditableTagRendering";
|
|
||||||
|
|
||||||
export default class TagRenderingPreview extends UIElement {
|
|
||||||
|
|
||||||
private readonly previewTagValue: UIEventSource<any>;
|
|
||||||
private selectedTagRendering: UIEventSource<TagRenderingPanel>;
|
|
||||||
private panel: UIElement;
|
|
||||||
|
|
||||||
constructor(selectedTagRendering: UIEventSource<TagRenderingPanel>,
|
|
||||||
previewTagValue: UIEventSource<any>) {
|
|
||||||
super(selectedTagRendering);
|
|
||||||
this.selectedTagRendering = selectedTagRendering;
|
|
||||||
this.previewTagValue = previewTagValue;
|
|
||||||
this.panel = this.GetPanel(undefined);
|
|
||||||
const self = this;
|
|
||||||
this.selectedTagRendering.addCallback(trp => {
|
|
||||||
self.panel = self.GetPanel(trp);
|
|
||||||
self.Update();
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private GetPanel(tagRenderingPanel: TagRenderingPanel): UIElement {
|
|
||||||
if (tagRenderingPanel === undefined) {
|
|
||||||
return new FixedUiElement("No tag rendering selected at the moment. Hover over a tag rendering to see what it looks like");
|
|
||||||
}
|
|
||||||
|
|
||||||
let es = tagRenderingPanel.GetValue();
|
|
||||||
|
|
||||||
let rendering: UIElement;
|
|
||||||
const self = this;
|
|
||||||
try {
|
|
||||||
rendering =
|
|
||||||
new VariableUiElement(es.map(tagRenderingConfig => {
|
|
||||||
try {
|
|
||||||
const tr = new EditableTagRendering(self.previewTagValue, new TagRenderingConfig(tagRenderingConfig, undefined,"preview"));
|
|
||||||
return tr.Render();
|
|
||||||
} catch (e) {
|
|
||||||
return new Combine(["Could not show this tagrendering:", e.message]).Render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("User defined tag rendering incorrect:", e);
|
|
||||||
rendering = new FixedUiElement(e).SetClass("alert");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Combine([
|
|
||||||
"<h3>",
|
|
||||||
tagRenderingPanel.options.title ?? "Extra tag rendering",
|
|
||||||
"</h3>",
|
|
||||||
tagRenderingPanel.options.description ?? "This tag rendering will appear in the popup",
|
|
||||||
"<br/><br/>",
|
|
||||||
rendering]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this.panel.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import CheckBox from "../Input/CheckBox";
|
import Toggle from "../Input/Toggle";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
|
@ -30,7 +30,7 @@ export default class DeleteImage extends UIElement {
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelButton = Translations.t.general.cancel.SetClass("bg-white pl-4 pr-4").SetStyle( "border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;");
|
const cancelButton = Translations.t.general.cancel.SetClass("bg-white pl-4 pr-4").SetStyle( "border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;");
|
||||||
this.deleteDialog = new CheckBox(
|
this.deleteDialog = new Toggle(
|
||||||
new Combine([
|
new Combine([
|
||||||
deleteButton,
|
deleteButton,
|
||||||
cancelButton
|
cancelButton
|
||||||
|
@ -40,17 +40,17 @@ export default class DeleteImage extends UIElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender() {
|
||||||
if(! State.state?.featureSwitchUserbadge?.data){
|
if(! State.state?.featureSwitchUserbadge?.data){
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = this.tags.data[this.key];
|
const value = this.tags.data[this.key];
|
||||||
if (value === undefined || value === "") {
|
if (value === undefined || value === "") {
|
||||||
return this.isDeletedBadge.Render();
|
return this.isDeletedBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.deleteDialog.Render();
|
return this.deleteDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -31,7 +31,7 @@ export class ImageCarousel extends UIElement{
|
||||||
return uiElements;
|
return uiElements;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.slideshow = new SlideShow(uiElements).HideOnEmpty(true);
|
this.slideshow = new SlideShow(uiElements);
|
||||||
this.SetClass("block w-full");
|
this.SetClass("block w-full");
|
||||||
this.slideshow.SetClass("w-full");
|
this.slideshow.SetClass("w-full");
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,10 @@ import {DropDown} from "../Input/DropDown";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
import {Tag} from "../../Logic/Tags/Tag";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class ImageUploadFlow extends UIElement {
|
export class ImageUploadFlow extends UIElement {
|
||||||
private readonly _licensePicker: UIElement;
|
private readonly _licensePicker: BaseUIElement;
|
||||||
private readonly _tags: UIEventSource<any>;
|
private readonly _tags: UIEventSource<any>;
|
||||||
private readonly _selectedLicence: UIEventSource<string>;
|
private readonly _selectedLicence: UIEventSource<string>;
|
||||||
private readonly _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
|
private readonly _isUploading: UIEventSource<number> = new UIEventSource<number>(0)
|
||||||
|
@ -35,10 +36,8 @@ export class ImageUploadFlow extends UIElement {
|
||||||
{value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs},
|
{value: "CC-BY-SA 4.0", shown: Translations.t.image.ccbs},
|
||||||
{value: "CC-BY 4.0", shown: Translations.t.image.ccb}
|
{value: "CC-BY 4.0", shown: Translations.t.image.ccb}
|
||||||
],
|
],
|
||||||
State.state.osmConnection.GetPreference("pictures-license"),
|
State.state.osmConnection.GetPreference("pictures-license")
|
||||||
"","",
|
).SetClass("flex flex-col sm:flex-row");
|
||||||
"flex flex-col sm:flex-row"
|
|
||||||
);
|
|
||||||
licensePicker.SetStyle("float:left");
|
licensePicker.SetStyle("float:left");
|
||||||
|
|
||||||
const t = Translations.t.image;
|
const t = Translations.t.image;
|
||||||
|
@ -186,8 +185,6 @@ export class ImageUploadFlow extends UIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerUpdate(htmlElement: HTMLElement) {
|
InnerUpdate(htmlElement: HTMLElement) {
|
||||||
super.InnerUpdate(htmlElement);
|
|
||||||
|
|
||||||
this._licensePicker.Update()
|
this._licensePicker.Update()
|
||||||
const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement
|
const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement
|
||||||
const selector = document.getElementById('fileselector-' + this.id)
|
const selector = document.getElementById('fileselector-' + this.id)
|
||||||
|
|
|
@ -35,7 +35,6 @@ export class SlideShow extends UIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||||
super.InnerUpdate(htmlElement);
|
|
||||||
require("slick-carousel")
|
require("slick-carousel")
|
||||||
if(this._embeddedElements.data.length == 0){
|
if(this._embeddedElements.data.length == 0){
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -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[]) {
|
constructor(elements: UIElement[]) {
|
||||||
super(undefined);
|
super(undefined);
|
||||||
this._elements = Utils.NoNull(elements);
|
this._elements = Utils.NoNull(elements);
|
||||||
this.dumbMode = false;
|
|
||||||
|
|
||||||
this.value = new UIEventSource<number[]>([])
|
this.value = new UIEventSource<number[]>([])
|
||||||
this.ListenTo(this.value);
|
this.ListenTo(this.value);
|
||||||
|
@ -51,7 +50,6 @@ export default class CheckBoxes extends InputElement<number[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||||
super.InnerUpdate(htmlElement);
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
for (let i = 0; i < this._elements.length; i++) {
|
for (let i = 0; i < this._elements.length; i++) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {InputElement} from "./InputElement";
|
import {InputElement} from "./InputElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {Utils} from "../../Utils";
|
|
||||||
|
|
||||||
export default class ColorPicker extends InputElement<string> {
|
export default class ColorPicker extends InputElement<string> {
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import {InputElement} from "./InputElement";
|
import {InputElement} from "./InputElement";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {UIElement} from "../UIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class CombinedInputElement<T> extends InputElement<T> {
|
export default class CombinedInputElement<T> extends InputElement<T> {
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._combined.ConstructElement();
|
||||||
|
}
|
||||||
private readonly _a: InputElement<T>;
|
private readonly _a: InputElement<T>;
|
||||||
private readonly _b: UIElement;
|
private readonly _b: BaseUIElement;
|
||||||
private readonly _combined: UIElement;
|
private readonly _combined: BaseUIElement;
|
||||||
public readonly IsSelected: UIEventSource<boolean>;
|
public readonly IsSelected: UIEventSource<boolean>;
|
||||||
|
|
||||||
constructor(a: InputElement<T>, b: InputElement<T>) {
|
constructor(a: InputElement<T>, b: InputElement<T>) {
|
||||||
super();
|
super();
|
||||||
this._a = a;
|
this._a = a;
|
||||||
|
@ -23,11 +25,6 @@ export default class CombinedInputElement<T> extends InputElement<T> {
|
||||||
return this._a.GetValue();
|
return this._a.GetValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this._combined.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
IsValid(t: T): boolean {
|
IsValid(t: T): boolean {
|
||||||
return this._a.IsValid(t);
|
return this._a.IsValid(t);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ export default class DirectionInput extends InputElement<string> {
|
||||||
|
|
||||||
constructor(value?: UIEventSource<string>) {
|
constructor(value?: UIEventSource<string>) {
|
||||||
super();
|
super();
|
||||||
this.dumbMode = false;
|
|
||||||
this.value = value ?? new UIEventSource<string>(undefined);
|
this.value = value ?? new UIEventSource<string>(undefined);
|
||||||
|
|
||||||
this.value.addCallbackAndRun(rotation => {
|
this.value.addCallbackAndRun(rotation => {
|
||||||
|
@ -48,7 +47,6 @@ export default class DirectionInput extends InputElement<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||||
super.InnerUpdate(htmlElement);
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
function onPosChange(x: number, y: number) {
|
function onPosChange(x: number, y: number) {
|
||||||
|
|
|
@ -1,50 +1,81 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {InputElement} from "./InputElement";
|
import {InputElement} from "./InputElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class DropDown<T> extends InputElement<T> {
|
export class DropDown<T> extends InputElement<T> {
|
||||||
|
|
||||||
private readonly _label: UIElement;
|
private static _nextDropdownId = 0;
|
||||||
private readonly _values: { value: T; shown: UIElement }[];
|
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
|
||||||
|
private readonly _element: HTMLElement;
|
||||||
|
|
||||||
private readonly _value: UIEventSource<T>;
|
private readonly _value: UIEventSource<T>;
|
||||||
|
private readonly _values: { value: T; shown: string | BaseUIElement }[];
|
||||||
|
|
||||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
constructor(label: string | BaseUIElement,
|
||||||
private readonly _label_class: string;
|
values: { value: T, shown: string | BaseUIElement }[],
|
||||||
private readonly _select_class: string;
|
|
||||||
private _form_style: string;
|
|
||||||
|
|
||||||
constructor(label: string | UIElement,
|
|
||||||
values: { value: T, shown: string | UIElement }[],
|
|
||||||
value: UIEventSource<T> = undefined,
|
value: UIEventSource<T> = undefined,
|
||||||
label_class: string = "",
|
options?: {
|
||||||
select_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)
|
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this._values = values;
|
||||||
|
if (values.length <= 1) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
for (const v of this._values) {
|
|
||||||
this.ListenTo(v.shown._source);
|
|
||||||
}
|
|
||||||
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> {
|
GetValue(): UIEventSource<T> {
|
||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsValid(t: T): boolean {
|
IsValid(t: T): boolean {
|
||||||
for (const value of this._values) {
|
for (const value of this._values) {
|
||||||
if (value.value === t) {
|
if (value.value === t) {
|
||||||
|
@ -54,44 +85,8 @@ export class DropDown<T> extends InputElement<T> {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
InnerRender(): string {
|
return this._element;
|
||||||
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 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 {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 GetValue() : UIEventSource<T>;
|
||||||
abstract IsSelected: UIEventSource<boolean>;
|
abstract IsSelected: UIEventSource<boolean>;
|
||||||
|
|
|
@ -3,13 +3,12 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
|
||||||
|
|
||||||
export default class InputElementMap<T, X> extends InputElement<X> {
|
export default class InputElementMap<T, X> extends InputElement<X> {
|
||||||
|
public readonly IsSelected: UIEventSource<boolean>;
|
||||||
private readonly _inputElement: InputElement<T>;
|
private readonly _inputElement: InputElement<T>;
|
||||||
private isSame: (x0: X, x1: X) => boolean;
|
private isSame: (x0: X, x1: X) => boolean;
|
||||||
private readonly fromX: (x: X) => T;
|
private readonly fromX: (x: X) => T;
|
||||||
private readonly toX: (t: T) => X;
|
private readonly toX: (t: T) => X;
|
||||||
private readonly _value: UIEventSource<X>;
|
private readonly _value: UIEventSource<X>;
|
||||||
public readonly IsSelected: UIEventSource<boolean>;
|
|
||||||
|
|
||||||
constructor(inputElement: InputElement<T>,
|
constructor(inputElement: InputElement<T>,
|
||||||
isSame: (x0: X, x1: X) => boolean,
|
isSame: (x0: X, x1: X) => boolean,
|
||||||
|
@ -41,10 +40,6 @@ export default class InputElementMap<T, X> extends InputElement<X> {
|
||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this._inputElement.InnerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
IsValid(x: X): boolean {
|
IsValid(x: X): boolean {
|
||||||
if (x === undefined) {
|
if (x === undefined) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -56,4 +51,8 @@ export default class InputElementMap<T, X> extends InputElement<X> {
|
||||||
return this._inputElement.IsValid(t);
|
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 value: UIEventSource<string>
|
||||||
|
|
||||||
|
private readonly _element: HTMLElement;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
value?: UIEventSource<string>
|
value?: UIEventSource<string>
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.value = value ?? new UIEventSource<string>(undefined);
|
this.value = value ?? new UIEventSource<string>(undefined);
|
||||||
const self = this;
|
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 => {
|
this.value.addCallbackAndRun(v => {
|
||||||
if(v === undefined){
|
if(v === undefined){
|
||||||
return;
|
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> {
|
GetValue(): UIEventSource<string> {
|
||||||
return this.value;
|
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,97 +1,83 @@
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {InputElement} from "./InputElement";
|
import {InputElement} from "./InputElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Combine from "../Base/Combine";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class TextField extends InputElement<string> {
|
export class TextField extends InputElement<string> {
|
||||||
private readonly value: UIEventSource<string>;
|
private readonly value: UIEventSource<string>;
|
||||||
public readonly enterPressed = new UIEventSource<string>(undefined);
|
public readonly enterPressed = new UIEventSource<string>(undefined);
|
||||||
private readonly _placeholder: UIElement;
|
|
||||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
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 _element: HTMLElement;
|
||||||
private _label: UIElement;
|
private readonly _isValid: (s: string, country?: () => string) => boolean;
|
||||||
|
|
||||||
constructor(options?: {
|
constructor(options?: {
|
||||||
placeholder?: string | UIElement,
|
placeholder?: string | BaseUIElement,
|
||||||
value?: UIEventSource<string>,
|
value?: UIEventSource<string>,
|
||||||
textArea?: boolean,
|
textArea?: boolean,
|
||||||
htmlType?: string,
|
htmlType?: string,
|
||||||
inputMode?: string,
|
inputMode?: string,
|
||||||
label?: UIElement,
|
label?: BaseUIElement,
|
||||||
textAreaRows?: number,
|
textAreaRows?: number,
|
||||||
isValid?: ((s: string, country?: () => string) => boolean)
|
isValid?: ((s: string, country?: () => string) => boolean)
|
||||||
}) {
|
}) {
|
||||||
super(undefined);
|
super();
|
||||||
const self = this;
|
const self = this;
|
||||||
this.value = new UIEventSource<string>("");
|
|
||||||
options = options ?? {};
|
options = options ?? {};
|
||||||
this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text");
|
|
||||||
this.value = options?.value ?? new UIEventSource<string>(undefined);
|
this.value = options?.value ?? new UIEventSource<string>(undefined);
|
||||||
|
this._isValid = options.isValid ?? (_ => true);
|
||||||
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.onClick(() => {
|
this.onClick(() => {
|
||||||
self.IsSelected.setData(true)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
field.value = t;
|
field.value = value;
|
||||||
});
|
if(self.IsValid(value)){
|
||||||
this.dumbMode = false;
|
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 = () => {
|
field.oninput = () => {
|
||||||
|
|
||||||
// How much characters are on the right, not including spaces?
|
// How much characters are on the right, not including spaces?
|
||||||
|
@ -119,13 +105,9 @@ export class TextField extends InputElement<string> {
|
||||||
newCursorPos --;
|
newCursorPos --;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @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("focusin", () => self.IsSelected.setData(true));
|
||||||
field.addEventListener("focusout", () => self.IsSelected.setData(false));
|
field.addEventListener("focusout", () => self.IsSelected.setData(false));
|
||||||
|
@ -138,20 +120,29 @@ export class TextField extends InputElement<string> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetCursorPosition(i: number) {
|
GetValue(): UIEventSource<string> {
|
||||||
const field = document.getElementById('txt-' + this.id);
|
return this.value;
|
||||||
if(field === undefined || field === null){
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SetCursorPosition(textfield: HTMLElement, i: number) {
|
||||||
|
if(textfield === undefined || textfield === null){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (i === -1) {
|
if (i === -1) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
i = field.value.length;
|
i = textfield.value.length;
|
||||||
}
|
}
|
||||||
field.focus();
|
textfield.focus();
|
||||||
// @ts-ignore
|
// @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);
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
import {UIElement} from "./UIElement";
|
import {UIElement} from "./UIElement";
|
||||||
import {DropDown} from "./Input/DropDown";
|
import {DropDown} from "./Input/DropDown";
|
||||||
import Locale from "./i18n/Locale";
|
import Locale from "./i18n/Locale";
|
||||||
import Svg from "../Svg";
|
|
||||||
import Img from "./Base/Img";
|
|
||||||
|
|
||||||
export default class LanguagePicker {
|
export default class LanguagePicker {
|
||||||
|
|
||||||
|
@ -18,7 +16,7 @@ export default class LanguagePicker {
|
||||||
return new DropDown(label, languages.map(lang => {
|
return new DropDown(label, languages.map(lang => {
|
||||||
return {value: lang, shown: lang}
|
return {value: lang, shown: lang}
|
||||||
}
|
}
|
||||||
), Locale.language, '', 'bg-indigo-100 p-1 rounded hover:bg-indigo-200');
|
), Locale.language, { select_class: 'bg-indigo-100 p-1 rounded hover:bg-indigo-200'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,8 @@ export default class MapControlButton extends UIElement {
|
||||||
this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);");
|
this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);");
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender() {
|
||||||
return this._contents.Render();
|
return this._contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -9,6 +9,15 @@ import Constants from "../../Models/Constants";
|
||||||
import opening_hours from "opening_hours";
|
import opening_hours from "opening_hours";
|
||||||
|
|
||||||
export default class OpeningHoursVisualization extends UIElement {
|
export default class OpeningHoursVisualization extends UIElement {
|
||||||
|
private static readonly weekdays = [
|
||||||
|
Translations.t.general.weekdays.abbreviations.monday,
|
||||||
|
Translations.t.general.weekdays.abbreviations.tuesday,
|
||||||
|
Translations.t.general.weekdays.abbreviations.wednesday,
|
||||||
|
Translations.t.general.weekdays.abbreviations.thursday,
|
||||||
|
Translations.t.general.weekdays.abbreviations.friday,
|
||||||
|
Translations.t.general.weekdays.abbreviations.saturday,
|
||||||
|
Translations.t.general.weekdays.abbreviations.sunday,
|
||||||
|
]
|
||||||
private readonly _key: string;
|
private readonly _key: string;
|
||||||
|
|
||||||
constructor(tags: UIEventSource<any>, key: string) {
|
constructor(tags: UIEventSource<any>, key: string) {
|
||||||
|
@ -22,8 +31,6 @@ export default class OpeningHoursVisualization extends UIElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static GetRanges(oh: any, from: Date, to: Date): ({
|
private static GetRanges(oh: any, from: Date, to: Date): ({
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
isSpecial: boolean,
|
isSpecial: boolean,
|
||||||
|
@ -80,8 +87,190 @@ export default class OpeningHoursVisualization extends UIElement {
|
||||||
return new Date(d.setDate(diff));
|
return new Date(d.setDate(diff));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InnerRender(): string | UIElement {
|
||||||
|
|
||||||
private allChangeMoments(ranges: {
|
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const lastMonday = OpeningHoursVisualization.getMonday(today);
|
||||||
|
const nextSunday = new Date(lastMonday);
|
||||||
|
nextSunday.setDate(nextSunday.getDate() + 7);
|
||||||
|
|
||||||
|
const tags = this._source.data;
|
||||||
|
if (tags._country === undefined) {
|
||||||
|
return "Loading country information...";
|
||||||
|
}
|
||||||
|
let oh = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// noinspection JSPotentiallyInvalidConstructorUsage
|
||||||
|
oh = new opening_hours(tags[this._key], {
|
||||||
|
lat: tags._lat,
|
||||||
|
lon: tags._lon,
|
||||||
|
address: {
|
||||||
|
country_code: tags._country
|
||||||
|
}
|
||||||
|
}, {tag_key: this._key});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return new Combine([Translations.t.general.opening_hours.error_loading,
|
||||||
|
State.state?.osmConnection?.userDetails?.data?.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked ?
|
||||||
|
`<span class='subtle'>${e}</span>`
|
||||||
|
: ""
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oh.getState() && !oh.getUnknown()) {
|
||||||
|
// POI is currently closed
|
||||||
|
const nextChange: Date = oh.getNextChange();
|
||||||
|
if (
|
||||||
|
// Shop isn't gonna open anymore in this timerange
|
||||||
|
nextSunday < nextChange
|
||||||
|
// And we are already in the weekend to show next week
|
||||||
|
&& (today.getDay() == 0 || today.getDay() == 6)
|
||||||
|
) {
|
||||||
|
// We mover further along
|
||||||
|
lastMonday.setDate(lastMonday.getDate() + 7);
|
||||||
|
nextSunday.setDate(nextSunday.getDate() + 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ranges[0] are all ranges for monday
|
||||||
|
const ranges = OpeningHoursVisualization.GetRanges(oh, lastMonday, nextSunday);
|
||||||
|
if (ranges.map(r => r.length).reduce((a, b) => a + b, 0) == 0) {
|
||||||
|
// Closed!
|
||||||
|
const opensAtDate = oh.getNextChange();
|
||||||
|
if (opensAtDate === undefined) {
|
||||||
|
const comm = oh.getComment() ?? oh.getUnknown();
|
||||||
|
if (!!comm) {
|
||||||
|
return new FixedUiElement(comm).SetClass("ohviz-closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oh.getState()) {
|
||||||
|
return Translations.t.general.opening_hours.open_24_7.SetClass("ohviz-closed")
|
||||||
|
}
|
||||||
|
return Translations.t.general.opening_hours.closed_permanently.SetClass("ohviz-closed")
|
||||||
|
}
|
||||||
|
const moment = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}`
|
||||||
|
return Translations.t.general.opening_hours.closed_until.Subs({date: moment}).SetClass("ohviz-closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWeekstable = oh.isWeekStable();
|
||||||
|
|
||||||
|
let [changeHours, changeHourText] = OpeningHoursVisualization.allChangeMoments(ranges);
|
||||||
|
|
||||||
|
// By default, we always show the range between 8 - 19h, in order to give a stable impression
|
||||||
|
// Ofc, a bigger range is used if needed
|
||||||
|
const earliestOpen = Math.min(8 * 60 * 60, ...changeHours);
|
||||||
|
let latestclose = Math.max(...changeHours);
|
||||||
|
// We always make sure there is 30m of leeway in order to give enough room for the closing entry
|
||||||
|
latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60)
|
||||||
|
|
||||||
|
|
||||||
|
const rows: UIElement[] = [];
|
||||||
|
const availableArea = latestclose - earliestOpen;
|
||||||
|
// @ts-ignore
|
||||||
|
const now = (100 * (((new Date() - today) / 1000) - earliestOpen)) / availableArea;
|
||||||
|
|
||||||
|
|
||||||
|
let header: UIElement[] = [];
|
||||||
|
|
||||||
|
if (now >= 0 && now <= 100) {
|
||||||
|
header.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now"))
|
||||||
|
}
|
||||||
|
for (const changeMoment of changeHours) {
|
||||||
|
const offset = 100 * (changeMoment - earliestOpen) / availableArea;
|
||||||
|
if (offset < 0 || offset > 100) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line");
|
||||||
|
header.push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < changeHours.length; i++) {
|
||||||
|
let changeMoment = changeHours[i];
|
||||||
|
const offset = 100 * (changeMoment - earliestOpen) / availableArea;
|
||||||
|
if (offset < 0 || offset > 100) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const el = new FixedUiElement(
|
||||||
|
`<div style='margin-top: ${i % 2 == 0 ? '1.5em;' : "1%"}'>${changeHourText[i]}</div>`
|
||||||
|
)
|
||||||
|
.SetStyle(`left:${offset}%`)
|
||||||
|
.SetClass("ohviz-time-indication");
|
||||||
|
header.push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(new Combine([`<td width="5%"> </td>`,
|
||||||
|
`<td style="position:relative;height:2.5em;">`,
|
||||||
|
new Combine(header), `</td>`]));
|
||||||
|
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const dayRanges = ranges[i];
|
||||||
|
const isToday = (new Date().getDay() + 6) % 7 === i;
|
||||||
|
let weekday = OpeningHoursVisualization.weekdays[i];
|
||||||
|
|
||||||
|
let dateToShow = ""
|
||||||
|
if (!isWeekstable) {
|
||||||
|
const day = new Date(lastMonday)
|
||||||
|
day.setDate(day.getDate() + i);
|
||||||
|
dateToShow = "" + day.getDate() + "/" + (day.getMonth() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let innerContent: (string | UIElement)[] = [];
|
||||||
|
|
||||||
|
// Add the lines
|
||||||
|
for (const changeMoment of changeHours) {
|
||||||
|
const offset = 100 * (changeMoment - earliestOpen) / availableArea;
|
||||||
|
innerContent.push(new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the actual ranges
|
||||||
|
for (const range of dayRanges) {
|
||||||
|
if (!range.isOpen && !range.isSpecial) {
|
||||||
|
innerContent.push(
|
||||||
|
new FixedUiElement(range.comment ?? dateToShow).SetClass("ohviz-day-off"))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startOfDay: Date = new Date(range.startDate);
|
||||||
|
startOfDay.setHours(0, 0, 0, 0);
|
||||||
|
// @ts-ignore
|
||||||
|
const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen;
|
||||||
|
// @ts-ignore
|
||||||
|
const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen);
|
||||||
|
const startPercentage = (100 * startpoint / availableArea);
|
||||||
|
innerContent.push(
|
||||||
|
new FixedUiElement(range.comment ?? dateToShow).SetStyle(`left:${startPercentage}%; width:${width}%`).SetClass("ohviz-range"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add line for 'now'
|
||||||
|
if (now >= 0 && now <= 100) {
|
||||||
|
innerContent.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now"))
|
||||||
|
}
|
||||||
|
|
||||||
|
let clss = ""
|
||||||
|
if (isToday) {
|
||||||
|
clss = "ohviz-today"
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(new Combine(
|
||||||
|
[`<td class="ohviz-weekday ${clss}">${weekday}</td>`,
|
||||||
|
`<td style="position:relative;" class="${clss}">`,
|
||||||
|
...innerContent,
|
||||||
|
`</td>`]))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return new Combine([
|
||||||
|
"<table class='ohviz' style='width:100%; word-break: normal; word-wrap: normal'>",
|
||||||
|
...rows.map(el => "<tr>" + el.Render() + "</tr>"),
|
||||||
|
"</table>"
|
||||||
|
]).SetClass("ohviz-container");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static allChangeMoments(ranges: {
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
isSpecial: boolean,
|
isSpecial: boolean,
|
||||||
comment: string,
|
comment: string,
|
||||||
|
@ -131,194 +320,4 @@ export default class OpeningHoursVisualization extends UIElement {
|
||||||
return [changeHours, changeHourText]
|
return [changeHours, changeHourText]
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly weekdays = [
|
|
||||||
Translations.t.general.weekdays.abbreviations.monday,
|
|
||||||
Translations.t.general.weekdays.abbreviations.tuesday,
|
|
||||||
Translations.t.general.weekdays.abbreviations.wednesday,
|
|
||||||
Translations.t.general.weekdays.abbreviations.thursday,
|
|
||||||
Translations.t.general.weekdays.abbreviations.friday,
|
|
||||||
Translations.t.general.weekdays.abbreviations.saturday,
|
|
||||||
Translations.t.general.weekdays.abbreviations.sunday,
|
|
||||||
]
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
const lastMonday = OpeningHoursVisualization.getMonday(today);
|
|
||||||
const nextSunday = new Date(lastMonday);
|
|
||||||
nextSunday.setDate(nextSunday.getDate() + 7);
|
|
||||||
|
|
||||||
const tags = this._source.data;
|
|
||||||
if (tags._country === undefined) {
|
|
||||||
return "Loading country information...";
|
|
||||||
}
|
|
||||||
let oh = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
oh = new opening_hours(tags[this._key], {
|
|
||||||
lat: tags._lat,
|
|
||||||
lon: tags._lon,
|
|
||||||
address: {
|
|
||||||
country_code: tags._country
|
|
||||||
}
|
|
||||||
}, {tag_key: this._key});
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
const msg = new Combine([Translations.t.general.opening_hours.error_loading,
|
|
||||||
State.state?.osmConnection?.userDetails?.data?.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked ?
|
|
||||||
`<span class='subtle'>${e}</span>`
|
|
||||||
: ""
|
|
||||||
]);
|
|
||||||
return msg.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oh.getState() && !oh.getUnknown()) {
|
|
||||||
// POI is currently closed
|
|
||||||
const nextChange: Date = oh.getNextChange();
|
|
||||||
if (
|
|
||||||
// Shop isn't gonna open anymore in this timerange
|
|
||||||
nextSunday < nextChange
|
|
||||||
// And we are already in the weekend to show next week
|
|
||||||
&& (today.getDay() == 0 || today.getDay() == 6)
|
|
||||||
) {
|
|
||||||
// We mover further along
|
|
||||||
lastMonday.setDate(lastMonday.getDate() + 7);
|
|
||||||
nextSunday.setDate(nextSunday.getDate() + 7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ranges[0] are all ranges for monday
|
|
||||||
const ranges = OpeningHoursVisualization.GetRanges(oh, lastMonday, nextSunday);
|
|
||||||
if (ranges.map(r => r.length).reduce((a, b) => a + b, 0) == 0) {
|
|
||||||
// Closed!
|
|
||||||
const opensAtDate = oh.getNextChange();
|
|
||||||
if(opensAtDate === undefined){
|
|
||||||
const comm = oh.getComment() ?? oh.getUnknown();
|
|
||||||
if(!!comm){
|
|
||||||
return new FixedUiElement(comm).SetClass("ohviz-closed").Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(oh.getState()){
|
|
||||||
return Translations.t.general.opening_hours.open_24_7.SetClass("ohviz-closed").Render()
|
|
||||||
}
|
|
||||||
return Translations.t.general.opening_hours.closed_permanently.SetClass("ohviz-closed").Render()
|
|
||||||
}
|
|
||||||
const moment = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}`
|
|
||||||
return Translations.t.general.opening_hours.closed_until.Subs({date: moment}).SetClass("ohviz-closed").Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isWeekstable = oh.isWeekStable();
|
|
||||||
|
|
||||||
let [changeHours, changeHourText] = this.allChangeMoments(ranges);
|
|
||||||
|
|
||||||
// By default, we always show the range between 8 - 19h, in order to give a stable impression
|
|
||||||
// Ofc, a bigger range is used if needed
|
|
||||||
const earliestOpen = Math.min(8 * 60 * 60, ...changeHours);
|
|
||||||
let latestclose = Math.max(...changeHours);
|
|
||||||
// We always make sure there is 30m of leeway in order to give enough room for the closing entry
|
|
||||||
latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60)
|
|
||||||
|
|
||||||
|
|
||||||
const rows: UIElement[] = [];
|
|
||||||
const availableArea = latestclose - earliestOpen;
|
|
||||||
// @ts-ignore
|
|
||||||
const now = (100 * (((new Date() - today) / 1000) - earliestOpen)) / availableArea;
|
|
||||||
|
|
||||||
|
|
||||||
let header = "";
|
|
||||||
|
|
||||||
if (now >= 0 && now <= 100) {
|
|
||||||
header += new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now").Render()
|
|
||||||
}
|
|
||||||
for (const changeMoment of changeHours) {
|
|
||||||
const offset = 100 * (changeMoment - earliestOpen) / availableArea;
|
|
||||||
if (offset < 0 || offset > 100) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line").Render();
|
|
||||||
header += el;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < changeHours.length; i++) {
|
|
||||||
let changeMoment = changeHours[i];
|
|
||||||
const offset = 100 * (changeMoment - earliestOpen) / availableArea;
|
|
||||||
if (offset < 0 || offset > 100) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const el = new FixedUiElement(
|
|
||||||
`<div style='margin-top: ${i % 2 == 0 ? '1.5em;' : "1%"}'>${changeHourText[i]}</div>`
|
|
||||||
)
|
|
||||||
.SetStyle(`left:${offset}%`)
|
|
||||||
.SetClass("ohviz-time-indication").Render();
|
|
||||||
header += el;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push(new Combine([`<td width="5%"> </td>`,
|
|
||||||
`<td style="position:relative;height:2.5em;">${header}</td>`]));
|
|
||||||
|
|
||||||
for (let i = 0; i < 7; i++) {
|
|
||||||
const dayRanges = ranges[i];
|
|
||||||
const isToday = (new Date().getDay() + 6) % 7 === i;
|
|
||||||
let weekday = OpeningHoursVisualization.weekdays[i].Render();
|
|
||||||
|
|
||||||
let dateToShow = ""
|
|
||||||
if (!isWeekstable) {
|
|
||||||
const day = new Date(lastMonday)
|
|
||||||
day.setDate(day.getDate() + i);
|
|
||||||
dateToShow = "" + day.getDate() + "/" + (day.getMonth() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let innerContent: string[] = [];
|
|
||||||
|
|
||||||
// Add the lines
|
|
||||||
for (const changeMoment of changeHours) {
|
|
||||||
const offset = 100 * (changeMoment - earliestOpen) / availableArea;
|
|
||||||
innerContent.push(new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line").Render())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the actual ranges
|
|
||||||
for (const range of dayRanges) {
|
|
||||||
if (!range.isOpen && !range.isSpecial) {
|
|
||||||
innerContent.push(
|
|
||||||
new FixedUiElement(range.comment ?? dateToShow).SetClass("ohviz-day-off").Render())
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startOfDay: Date = new Date(range.startDate);
|
|
||||||
startOfDay.setHours(0, 0, 0, 0);
|
|
||||||
// @ts-ignore
|
|
||||||
const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen;
|
|
||||||
// @ts-ignore
|
|
||||||
const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen);
|
|
||||||
const startPercentage = (100 * startpoint / availableArea);
|
|
||||||
innerContent.push(
|
|
||||||
new FixedUiElement(range.comment ?? dateToShow).SetStyle(`left:${startPercentage}%; width:${width}%`).SetClass("ohviz-range").Render())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add line for 'now'
|
|
||||||
if (now >= 0 && now <= 100) {
|
|
||||||
innerContent.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now").Render())
|
|
||||||
}
|
|
||||||
|
|
||||||
let clss = ""
|
|
||||||
if (isToday) {
|
|
||||||
clss = "ohviz-today"
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push(new Combine(
|
|
||||||
[`<td class="ohviz-weekday ${clss}">${weekday}</td>`,
|
|
||||||
`<td style="position:relative;" class="${clss}">${innerContent.join("")}</td>`]))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return new Combine([
|
|
||||||
"<table class='ohviz' style='width:100%; word-break: normal; word-wrap: normal'>",
|
|
||||||
rows.map(el => "<tr>" + el.Render() + "</tr>").join(""),
|
|
||||||
"</table>"
|
|
||||||
]).SetClass("ohviz-container").Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
import OpeningHoursPicker from "./OpeningHoursPicker";
|
import OpeningHoursPicker from "./OpeningHoursPicker";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {UIElement} from "../UIElement";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
@ -14,16 +13,15 @@ import {InputElement} from "../Input/InputElement";
|
||||||
import PublicHolidayInput from "./PublicHolidayInput";
|
import PublicHolidayInput from "./PublicHolidayInput";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
|
|
||||||
export default class OpeningHoursInput extends InputElement<string> {
|
export default class OpeningHoursInput extends InputElement<string> {
|
||||||
|
|
||||||
|
|
||||||
|
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
private readonly _value: UIEventSource<string>;
|
private readonly _value: UIEventSource<string>;
|
||||||
|
private readonly _element: BaseUIElement;
|
||||||
private readonly _ohPicker: UIElement;
|
|
||||||
private readonly _leftoverWarning: UIElement;
|
|
||||||
private readonly _phSelector: UIElement;
|
|
||||||
|
|
||||||
constructor(value: UIEventSource<string> = new UIEventSource<string>("")) {
|
constructor(value: UIEventSource<string> = new UIEventSource<string>("")) {
|
||||||
super();
|
super();
|
||||||
|
@ -61,7 +59,7 @@ export default class OpeningHoursInput extends InputElement<string> {
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
})
|
})
|
||||||
this._phSelector = new PublicHolidayInput(ph);
|
const phSelector = new PublicHolidayInput(ph);
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const regular = OH.ToString(rulesFromOhPicker.data);
|
const regular = OH.ToString(rulesFromOhPicker.data);
|
||||||
|
@ -76,7 +74,7 @@ export default class OpeningHoursInput extends InputElement<string> {
|
||||||
rulesFromOhPicker.addCallback(update);
|
rulesFromOhPicker.addCallback(update);
|
||||||
ph.addCallback(update);
|
ph.addCallback(update);
|
||||||
|
|
||||||
this._leftoverWarning = new VariableUiElement(leftoverRules.map((leftovers: string[]) => {
|
const leftoverWarning = new VariableUiElement(leftoverRules.map((leftovers: string[]) => {
|
||||||
|
|
||||||
if (leftovers.length == 0) {
|
if (leftovers.length == 0) {
|
||||||
return "";
|
return "";
|
||||||
|
@ -84,31 +82,27 @@ export default class OpeningHoursInput extends InputElement<string> {
|
||||||
return new Combine([
|
return new Combine([
|
||||||
Translations.t.general.opening_hours.not_all_rules_parsed,
|
Translations.t.general.opening_hours.not_all_rules_parsed,
|
||||||
new FixedUiElement(leftovers.map(r => `${r}<br/>`).join("")).SetClass("subtle")
|
new FixedUiElement(leftovers.map(r => `${r}<br/>`).join("")).SetClass("subtle")
|
||||||
]).Render();
|
]);
|
||||||
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
this._ohPicker = new OpeningHoursPicker(rulesFromOhPicker);
|
const ohPicker = new OpeningHoursPicker(rulesFromOhPicker);
|
||||||
|
|
||||||
|
|
||||||
|
this._element = new Combine([
|
||||||
|
leftoverWarning,
|
||||||
|
ohPicker,
|
||||||
|
phSelector
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._element.ConstructElement()
|
||||||
|
}
|
||||||
|
|
||||||
GetValue(): UIEventSource<string> {
|
GetValue(): UIEventSource<string> {
|
||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return new Combine([
|
|
||||||
this._leftoverWarning,
|
|
||||||
this._ohPicker,
|
|
||||||
this._phSelector
|
|
||||||
]).Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
|
||||||
|
|
||||||
IsValid(t: string): boolean {
|
IsValid(t: string): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Combine from "../Base/Combine";
|
||||||
import OpeningHoursPickerTable from "./OpeningHoursPickerTable";
|
import OpeningHoursPickerTable from "./OpeningHoursPickerTable";
|
||||||
import {OH, OpeningHour} from "./OpeningHours";
|
import {OH, OpeningHour} from "./OpeningHours";
|
||||||
import {InputElement} from "../Input/InputElement";
|
import {InputElement} from "../Input/InputElement";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
||||||
private readonly _ohs: UIEventSource<OpeningHour[]>;
|
private readonly _ohs: UIEventSource<OpeningHour[]>;
|
||||||
|
@ -12,7 +13,7 @@ export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
||||||
|
|
||||||
private readonly _backgroundTable: OpeningHoursPickerTable;
|
private readonly _backgroundTable: OpeningHoursPickerTable;
|
||||||
|
|
||||||
private readonly _weekdays: UIEventSource<UIElement[]> = new UIEventSource<UIElement[]>([]);
|
private readonly _weekdays: UIEventSource<BaseUIElement[]> = new UIEventSource<BaseUIElement[]>([]);
|
||||||
|
|
||||||
constructor(ohs: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([])) {
|
constructor(ohs: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([])) {
|
||||||
super();
|
super();
|
||||||
|
@ -49,8 +50,12 @@ export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): BaseUIElement {
|
||||||
return this._backgroundTable.Render();
|
return this._backgroundTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._backgroundTable.ConstructElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
GetValue(): UIEventSource<OpeningHour[]> {
|
GetValue(): UIEventSource<OpeningHour[]> {
|
||||||
|
|
|
@ -8,12 +8,13 @@ import {Utils} from "../../Utils";
|
||||||
import {OpeningHour} from "./OpeningHours";
|
import {OpeningHour} from "./OpeningHours";
|
||||||
import {InputElement} from "../Input/InputElement";
|
import {InputElement} from "../Input/InputElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> {
|
export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> {
|
||||||
public readonly IsSelected: UIEventSource<boolean>;
|
public readonly IsSelected: UIEventSource<boolean>;
|
||||||
private readonly weekdays: UIEventSource<UIElement[]>;
|
private readonly weekdays: UIEventSource<BaseUIElement[]>;
|
||||||
|
|
||||||
public static readonly days: UIElement[] =
|
public static readonly days: BaseUIElement[] =
|
||||||
[
|
[
|
||||||
Translations.t.general.weekdays.abbreviations.monday,
|
Translations.t.general.weekdays.abbreviations.monday,
|
||||||
Translations.t.general.weekdays.abbreviations.tuesday,
|
Translations.t.general.weekdays.abbreviations.tuesday,
|
||||||
|
@ -28,8 +29,8 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]>
|
||||||
private readonly source: UIEventSource<OpeningHour[]>;
|
private readonly source: UIEventSource<OpeningHour[]>;
|
||||||
|
|
||||||
|
|
||||||
constructor(weekdays: UIEventSource<UIElement[]>, source?: UIEventSource<OpeningHour[]>) {
|
constructor(weekdays: UIEventSource<BaseUIElement[]>, source?: UIEventSource<OpeningHour[]>) {
|
||||||
super(weekdays);
|
super();
|
||||||
this.weekdays = weekdays;
|
this.weekdays = weekdays;
|
||||||
this.source = source ?? new UIEventSource<OpeningHour[]>([]);
|
this.source = source ?? new UIEventSource<OpeningHour[]>([]);
|
||||||
this.IsSelected = new UIEventSource<boolean>(false);
|
this.IsSelected = new UIEventSource<boolean>(false);
|
||||||
|
|
|
@ -48,10 +48,10 @@ export default class OpeningHoursRange extends UIElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
const oh = this._oh.data;
|
const oh = this._oh.data;
|
||||||
if (oh === undefined) {
|
if (oh === undefined) {
|
||||||
return "";
|
return undefined;
|
||||||
}
|
}
|
||||||
const height = this.getHeight();
|
const height = this.getHeight();
|
||||||
|
|
||||||
|
@ -62,7 +62,6 @@ export default class OpeningHoursRange extends UIElement {
|
||||||
|
|
||||||
return new Combine(content)
|
return new Combine(content)
|
||||||
.SetClass("oh-timerange-inner")
|
.SetClass("oh-timerange-inner")
|
||||||
.Render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHeight(): number {
|
private getHeight(): number {
|
||||||
|
|
|
@ -143,7 +143,7 @@ export default class PublicHolidayInput extends InputElement<string> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
const mode = this._mode.data;
|
const mode = this._mode.data;
|
||||||
if (mode === " ") {
|
if (mode === " ") {
|
||||||
return new Combine([this._dropdown,
|
return new Combine([this._dropdown,
|
||||||
|
@ -154,9 +154,9 @@ export default class PublicHolidayInput extends InputElement<string> {
|
||||||
" ",
|
" ",
|
||||||
Translations.t.general.opening_hours.openTill,
|
Translations.t.general.opening_hours.openTill,
|
||||||
" ",
|
" ",
|
||||||
this._endHour]).Render();
|
this._endHour]);
|
||||||
}
|
}
|
||||||
return this._dropdown.Render();
|
return this._dropdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
GetValue(): UIEventSource<string> {
|
GetValue(): UIEventSource<string> {
|
||||||
|
|
|
@ -39,7 +39,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||||
const titleIcons = new Combine(
|
const titleIcons = new Combine(
|
||||||
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
|
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
|
||||||
"block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem !important;")
|
"block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem !important;")
|
||||||
.HideOnEmpty(true)
|
|
||||||
))
|
))
|
||||||
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default class QuestionBox extends UIElement {
|
||||||
this.SetClass("block mb-8")
|
this.SetClass("block mb-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender() {
|
||||||
const allQuestions : UIElement[] = []
|
const allQuestions : UIElement[] = []
|
||||||
for (let i = 0; i < this._tagRenderingQuestions.length; i++) {
|
for (let i = 0; i < this._tagRenderingQuestions.length; i++) {
|
||||||
let tagRendering = this._tagRenderings[i];
|
let tagRendering = this._tagRenderings[i];
|
||||||
|
@ -72,7 +72,7 @@ export default class QuestionBox extends UIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return new Combine(allQuestions).Render();
|
return new Combine(allQuestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -21,15 +21,15 @@ export class SaveButton extends UIElement {
|
||||||
.onClick(() => osmConnection?.AttemptLogin())
|
.onClick(() => osmConnection?.AttemptLogin())
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender() {
|
||||||
if(this._userDetails != undefined && !this._userDetails.data.loggedIn){
|
if(this._userDetails != undefined && !this._userDetails.data.loggedIn){
|
||||||
return this._friendlyLogin.Render();
|
return this._friendlyLogin;
|
||||||
}
|
}
|
||||||
let inactive_class = ''
|
let inactive_class = ''
|
||||||
if (this._value.data === false || (this._value.data ?? "") === "") {
|
if (this._value.data === false || (this._value.data ?? "") === "") {
|
||||||
inactive_class = "btn-disabled";
|
inactive_class = "btn-disabled";
|
||||||
}
|
}
|
||||||
return Translations.t.general.save.Clone().SetClass(`btn ${inactive_class}`).Render();
|
return Translations.t.general.save.Clone().SetClass(`btn ${inactive_class}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -30,7 +30,7 @@ export default class TagRenderingAnswer extends UIElement {
|
||||||
this.SetStyle("word-wrap: anywhere;");
|
this.SetStyle("word-wrap: anywhere;");
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): string | UIElement{
|
||||||
if (this._configuration.condition !== undefined) {
|
if (this._configuration.condition !== undefined) {
|
||||||
if (!this._configuration.condition.matchesProperties(this._tags.data)) {
|
if (!this._configuration.condition.matchesProperties(this._tags.data)) {
|
||||||
return "";
|
return "";
|
||||||
|
@ -80,14 +80,14 @@ export default class TagRenderingAnswer extends UIElement {
|
||||||
])
|
])
|
||||||
|
|
||||||
}
|
}
|
||||||
return this._content.SetClass(this._contentClass).SetStyle(this._contentStyle).Render();
|
return this._content.SetClass(this._contentClass).SetStyle(this._contentStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tr = this._configuration.GetRenderValue(tags);
|
const tr = this._configuration.GetRenderValue(tags);
|
||||||
if (tr !== undefined) {
|
if (tr !== undefined) {
|
||||||
this._content = SubstitutedTranslation.construct(tr, this._tags);
|
this._content = SubstitutedTranslation.construct(tr, this._tags);
|
||||||
return this._content.SetClass(this._contentClass).SetStyle(this._contentStyle).Render();
|
return this._content.SetClass(this._contentClass).SetStyle(this._contentStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
|
|
|
@ -94,7 +94,7 @@ export default class TagRenderingQuestion extends UIElement {
|
||||||
).SetClass("block")
|
).SetClass("block")
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender() {
|
||||||
return new Combine([
|
return new Combine([
|
||||||
this._question,
|
this._question,
|
||||||
this._inputElement,
|
this._inputElement,
|
||||||
|
@ -103,7 +103,6 @@ export default class TagRenderingQuestion extends UIElement {
|
||||||
this._appliedTags]
|
this._appliedTags]
|
||||||
)
|
)
|
||||||
.SetClass("question")
|
.SetClass("question")
|
||||||
.Render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private GenerateInputElement(): InputElement<TagsFilter> {
|
private GenerateInputElement(): InputElement<TagsFilter> {
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default class ReviewElement extends UIElement {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
|
|
||||||
const elements = [];
|
const elements = [];
|
||||||
const revs = this._reviews.data;
|
const revs = this._reviews.data;
|
||||||
|
@ -56,7 +56,7 @@ export default class ReviewElement extends UIElement {
|
||||||
|
|
||||||
.SetClass("review-attribution"))
|
.SetClass("review-attribution"))
|
||||||
|
|
||||||
return new Combine(elements).SetClass("block").Render();
|
return new Combine(elements).SetClass("block");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -86,10 +86,10 @@ export default class ReviewForm extends InputElement<Review> {
|
||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
|
|
||||||
if(!this.userDetails.data.loggedIn){
|
if(!this.userDetails.data.loggedIn){
|
||||||
return Translations.t.reviews.plz_login.Render();
|
return Translations.t.reviews.plz_login;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Combine([
|
return new Combine([
|
||||||
|
@ -103,7 +103,6 @@ export default class ReviewForm extends InputElement<Review> {
|
||||||
Translations.t.reviews.tos.SetClass("subtle")
|
Translations.t.reviews.tos.SetClass("subtle")
|
||||||
])
|
])
|
||||||
.SetClass("review-form")
|
.SetClass("review-form")
|
||||||
.Render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default class SingleReview extends UIElement{
|
||||||
scoreTen % 2 == 1 ? "<img src='./assets/svg/star_half.svg' class='h-8 md:h-12'/>" : ""
|
scoreTen % 2 == 1 ? "<img src='./assets/svg/star_half.svg' class='h-8 md:h-12'/>" : ""
|
||||||
]).SetClass("flex w-max")
|
]).SetClass("flex w-max")
|
||||||
}
|
}
|
||||||
InnerRender(): string {
|
InnerRender(): UIElement {
|
||||||
const d = this._review.date;
|
const d = this._review.date;
|
||||||
let review = this._review;
|
let review = this._review;
|
||||||
const el= new Combine(
|
const el= new Combine(
|
||||||
|
@ -51,7 +51,7 @@ export default class SingleReview extends UIElement{
|
||||||
if(review.made_by_user.data){
|
if(review.made_by_user.data){
|
||||||
el.SetClass("border-attention-catch")
|
el.SetClass("border-attention-catch")
|
||||||
}
|
}
|
||||||
return el.Render();
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -6,11 +6,8 @@ import Combine from "./Base/Combine";
|
||||||
import State from "../State";
|
import State from "../State";
|
||||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||||
import SpecialVisualizations from "./SpecialVisualizations";
|
import SpecialVisualizations from "./SpecialVisualizations";
|
||||||
import {Utils} from "../Utils";
|
|
||||||
|
|
||||||
export class SubstitutedTranslation extends UIElement {
|
export class SubstitutedTranslation extends UIElement {
|
||||||
private static cachedTranslations:
|
|
||||||
Map<string, Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>>> = new Map<string, Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>>>();
|
|
||||||
private readonly tags: UIEventSource<any>;
|
private readonly tags: UIEventSource<any>;
|
||||||
private readonly translation: Translation;
|
private readonly translation: Translation;
|
||||||
private content: UIElement[];
|
private content: UIElement[];
|
||||||
|
@ -37,39 +34,24 @@ export class SubstitutedTranslation extends UIElement {
|
||||||
public static construct(
|
public static construct(
|
||||||
translation: Translation,
|
translation: Translation,
|
||||||
tags: UIEventSource<any>): SubstitutedTranslation {
|
tags: UIEventSource<any>): SubstitutedTranslation {
|
||||||
|
return new SubstitutedTranslation(translation, tags);
|
||||||
/* let cachedTranslations = Utils.getOrSetDefault(SubstitutedTranslation.cachedTranslations, SubstitutedTranslation.GenerateSubCache);
|
|
||||||
const innerMap = Utils.getOrSetDefault(cachedTranslations, translation, SubstitutedTranslation.GenerateMap);
|
|
||||||
|
|
||||||
const cachedTranslation = innerMap.get(tags);
|
|
||||||
if (cachedTranslation !== undefined) {
|
|
||||||
return cachedTranslation;
|
|
||||||
}*/
|
|
||||||
const st = new SubstitutedTranslation(translation, tags);
|
|
||||||
// innerMap.set(tags, st);
|
|
||||||
return st;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SubstituteKeys(txt: string, tags: any) {
|
public static SubstituteKeys(txt: string, tags: any) {
|
||||||
for (const key in tags) {
|
for (const key in tags) {
|
||||||
|
if(!tags.hasOwnProperty(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
txt = txt.replace(new RegExp("{" + key + "}", "g"), tags[key])
|
txt = txt.replace(new RegExp("{" + key + "}", "g"), tags[key])
|
||||||
}
|
}
|
||||||
return txt;
|
return txt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GenerateMap() {
|
InnerRender() {
|
||||||
return new Map<UIEventSource<any>, SubstitutedTranslation>()
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GenerateSubCache() {
|
|
||||||
return new Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
if (this.content.length == 1) {
|
if (this.content.length == 1) {
|
||||||
return this.content[0].Render();
|
return this.content[0];
|
||||||
}
|
}
|
||||||
return new Combine(this.content).Render();
|
return new Combine(this.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CreateContent(): UIElement[] {
|
private CreateContent(): UIElement[] {
|
||||||
|
|
210
UI/UIElement.ts
210
UI/UIElement.ts
|
@ -1,25 +1,19 @@
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import {UIEventSource} from "../Logic/UIEventSource";
|
||||||
import {Utils} from "../Utils";
|
import BaseUIElement from "./BaseUIElement";
|
||||||
|
|
||||||
export abstract class UIElement extends UIEventSource<string> {
|
export abstract class UIElement extends BaseUIElement{
|
||||||
|
|
||||||
private static nextId: number = 0;
|
private static nextId: number = 0;
|
||||||
public readonly id: string;
|
public readonly id: string;
|
||||||
public readonly _source: UIEventSource<any>;
|
public readonly _source: UIEventSource<any>;
|
||||||
public dumbMode = false;
|
|
||||||
private clss: Set<string> = new Set<string>();
|
|
||||||
private style: string;
|
|
||||||
private _hideIfEmpty = false;
|
|
||||||
private lastInnerRender: string;
|
private lastInnerRender: string;
|
||||||
private _onClick: () => void;
|
|
||||||
private _onHover: UIEventSource<boolean>;
|
|
||||||
|
|
||||||
protected constructor(source: UIEventSource<any> = undefined) {
|
protected constructor(source: UIEventSource<any> = undefined) {
|
||||||
super("");
|
super()
|
||||||
this.id = "ui-element-" + UIElement.nextId;
|
this.id = `ui-${this.constructor.name}-${UIElement.nextId}`;
|
||||||
this._source = source;
|
this._source = source;
|
||||||
UIElement.nextId++;
|
UIElement.nextId++;
|
||||||
this.dumbMode = true;
|
|
||||||
this.ListenTo(source);
|
this.ListenTo(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,183 +21,97 @@ export abstract class UIElement extends UIEventSource<string> {
|
||||||
if (source === undefined) {
|
if (source === undefined) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
this.dumbMode = false;
|
|
||||||
const self = this;
|
const self = this;
|
||||||
source.addCallback(() => {
|
source.addCallback(() => {
|
||||||
self.lastInnerRender = undefined;
|
self.lastInnerRender = undefined;
|
||||||
self.Update();
|
if(self._constructedHtmlElement !== undefined){
|
||||||
|
self.UpdateElement(self._constructedHtmlElement);
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onClick(f: (() => void)) {
|
|
||||||
this.dumbMode = false;
|
|
||||||
this._onClick = f;
|
|
||||||
this.SetClass("clickable")
|
|
||||||
this.Update();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IsHovered(): UIEventSource<boolean> {
|
|
||||||
this.dumbMode = false;
|
|
||||||
if (this._onHover !== undefined) {
|
|
||||||
return this._onHover;
|
|
||||||
}
|
|
||||||
// Note: we just save it. 'Update' will register that an eventsource exist and install the necessary hooks
|
|
||||||
this._onHover = new UIEventSource<boolean>(false);
|
|
||||||
return this._onHover;
|
|
||||||
}
|
|
||||||
|
|
||||||
Update(): void {
|
Update(): void {
|
||||||
if (Utils.runningFromConsole) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let element = document.getElementById(this.id);
|
|
||||||
if (element === undefined || element === null) {
|
|
||||||
// The element is not painted or, in the case of 'dumbmode' this UI-element is not explicitely present
|
|
||||||
if (this.dumbMode) {
|
|
||||||
// We update all the children anyway
|
|
||||||
this.UpdateAllChildren();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newRender = this.InnerRender();
|
|
||||||
if (newRender !== this.lastInnerRender) {
|
|
||||||
this.lastInnerRender = newRender;
|
|
||||||
this.setData(this.InnerRender());
|
|
||||||
element.innerHTML = this.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._hideIfEmpty) {
|
|
||||||
if (element.innerHTML === "") {
|
|
||||||
element.parentElement.style.display = "none";
|
|
||||||
} else {
|
|
||||||
element.parentElement.style.display = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._onClick !== undefined) {
|
|
||||||
const self = this;
|
|
||||||
element.onclick = (e) => {
|
|
||||||
// @ts-ignore
|
|
||||||
if (e.consumed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self._onClick();
|
|
||||||
// @ts-ignore
|
|
||||||
e.consumed = true;
|
|
||||||
}
|
|
||||||
element.style.pointerEvents = "all";
|
|
||||||
element.style.cursor = "pointer";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._onHover !== undefined) {
|
|
||||||
const self = this;
|
|
||||||
element.addEventListener('mouseover', () => self._onHover.setData(true));
|
|
||||||
element.addEventListener('mouseout', () => self._onHover.setData(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.InnerUpdate(element);
|
|
||||||
this.UpdateAllChildren();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
HideOnEmpty(hide: boolean): UIElement {
|
|
||||||
this._hideIfEmpty = hide;
|
|
||||||
this.Update();
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Render(): string {
|
Render(): string {
|
||||||
this.lastInnerRender = this.InnerRender();
|
return "Don't use Render!"
|
||||||
if (this.dumbMode) {
|
|
||||||
return this.lastInnerRender;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let style = "";
|
|
||||||
if (this.style !== undefined && this.style !== "") {
|
|
||||||
style = `style="${this.style}" `;
|
|
||||||
}
|
|
||||||
let clss = "";
|
|
||||||
if (this.clss.size > 0) {
|
|
||||||
clss = `class='${Array.from(this.clss).join(" ")}' `;
|
|
||||||
}
|
|
||||||
return `<span ${clss}${style}id='${this.id}' gen="${this.constructor.name}">${this.lastInnerRender}</span>`
|
|
||||||
}
|
|
||||||
|
|
||||||
AttachTo(divId: string) {
|
public InnerRenderAsString(): string {
|
||||||
this.dumbMode = false;
|
let rendered = this.InnerRender();
|
||||||
let element = document.getElementById(divId);
|
if (typeof rendered !== "string") {
|
||||||
if (element === null) {
|
let html = rendered.ConstructElement()
|
||||||
throw "SEVERE: could not attach UIElement to " + divId;
|
return html.innerHTML
|
||||||
}
|
}
|
||||||
element.innerHTML = this.Render();
|
return rendered
|
||||||
this.Update();
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract InnerRender(): string;
|
|
||||||
|
|
||||||
public IsEmpty(): boolean {
|
public IsEmpty(): boolean {
|
||||||
return this.InnerRender() === "";
|
return this.InnerRender() === undefined || this.InnerRender() === "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds all the relevant classes, space seperated
|
* Should be overridden for specific HTML functionality
|
||||||
* @param clss
|
|
||||||
* @constructor
|
|
||||||
*/
|
*/
|
||||||
public SetClass(clss: string) {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
this.dumbMode = false;
|
// Uses the old fashioned way to construct an element using 'InnerRender'
|
||||||
const all = clss.split(" ");
|
const innerRender = this.InnerRender();
|
||||||
let recordedChange = false;
|
if (innerRender === undefined || innerRender === "") {
|
||||||
for (const c of all) {
|
return undefined;
|
||||||
if (this.clss.has(clss)) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
this.clss.add(c);
|
const el = document.createElement("span")
|
||||||
recordedChange = true;
|
if (typeof innerRender === "string") {
|
||||||
|
el.innerHTML = innerRender
|
||||||
|
} else {
|
||||||
|
const subElement = innerRender.ConstructElement();
|
||||||
|
if (subElement === undefined) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
if (recordedChange) {
|
el.appendChild(subElement)
|
||||||
this.Update();
|
|
||||||
}
|
}
|
||||||
return this;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RemoveClass(clss: string): UIElement {
|
protected UpdateElement(el: HTMLElement) : void{
|
||||||
if (this.clss.has(clss)) {
|
const innerRender = this.InnerRender();
|
||||||
this.clss.delete(clss);
|
|
||||||
this.Update();
|
if (typeof innerRender === "string") {
|
||||||
|
if(el.innerHTML !== innerRender){
|
||||||
|
el.innerHTML = innerRender
|
||||||
}
|
}
|
||||||
return this;
|
} else {
|
||||||
|
const subElement = innerRender.ConstructElement();
|
||||||
|
if(el.children.length === 1 && el.children[0] === subElement){
|
||||||
|
return; // Nothing changed
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetStyle(style: string): UIElement {
|
while (el.firstChild) {
|
||||||
this.dumbMode = false;
|
el.removeChild(el.firstChild);
|
||||||
this.style = style;
|
|
||||||
this.Update();
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called after the HTML has been replaced. Can be used for css tricks
|
if (subElement === undefined) {
|
||||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
return;
|
||||||
|
}
|
||||||
|
el.appendChild(subElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
private UpdateAllChildren() {
|
}
|
||||||
for (const i in this) {
|
|
||||||
const child = this[i];
|
|
||||||
if (child instanceof UIElement) {
|
|
||||||
child.Update();
|
/**
|
||||||
} else if (child instanceof Array) {
|
* @deprecated The method should not be used
|
||||||
for (const ch of child) {
|
*/
|
||||||
if (ch instanceof UIElement) {
|
protected abstract InnerRender(): string | BaseUIElement;
|
||||||
ch.Update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import Locale from "./Locale";
|
import Locale from "./Locale";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export class Translation extends UIElement {
|
export class Translation extends BaseUIElement {
|
||||||
|
|
||||||
public static forcedLanguage = undefined;
|
public static forcedLanguage = undefined;
|
||||||
|
|
||||||
public readonly translations: object
|
public readonly translations: object
|
||||||
return
|
|
||||||
allIcons;
|
|
||||||
|
|
||||||
constructor(translations: object, context?: string) {
|
constructor(translations: object, context?: string) {
|
||||||
super(Locale.language)
|
super()
|
||||||
if (translations === undefined) {
|
if (translations === undefined) {
|
||||||
throw `Translation without content (${context})`
|
throw `Translation without content (${context})`
|
||||||
}
|
}
|
||||||
|
@ -46,15 +44,29 @@ export class Translation extends UIElement {
|
||||||
return en;
|
return en;
|
||||||
}
|
}
|
||||||
for (const i in this.translations) {
|
for (const i in this.translations) {
|
||||||
|
if (!this.translations.hasOwnProperty(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
return this.translations[i]; // Return a random language
|
return this.translations[i]; // Return a random language
|
||||||
}
|
}
|
||||||
console.error("Missing language ", Locale.language.data, "for", this.translations)
|
console.error("Missing language ", Locale.language.data, "for", this.translations)
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InnerConstructElement(): HTMLElement {
|
||||||
|
const el = document.createElement("span")
|
||||||
|
Locale.language.addCallbackAndRun(_ => {
|
||||||
|
el.innerHTML = this.txt
|
||||||
|
})
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
public SupportedLanguages(): string[] {
|
public SupportedLanguages(): string[] {
|
||||||
const langs = []
|
const langs = []
|
||||||
for (const translationsKey in this.translations) {
|
for (const translationsKey in this.translations) {
|
||||||
|
if (!this.translations.hasOwnProperty(translationsKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (translationsKey === "#") {
|
if (translationsKey === "#") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -66,9 +78,15 @@ export class Translation extends UIElement {
|
||||||
public Subs(text: any): Translation {
|
public Subs(text: any): Translation {
|
||||||
const newTranslations = {};
|
const newTranslations = {};
|
||||||
for (const lang in this.translations) {
|
for (const lang in this.translations) {
|
||||||
|
if (!this.translations.hasOwnProperty(lang)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let template: string = this.translations[lang];
|
let template: string = this.translations[lang];
|
||||||
for (const k in text) {
|
for (const k in text) {
|
||||||
const combined = [];
|
if (!text.hasOwnProperty(k)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const combined: (string)[] = [];
|
||||||
const parts = template.split("{" + k + "}");
|
const parts = template.split("{" + k + "}");
|
||||||
const el: string | UIElement = text[k];
|
const el: string | UIElement = text[k];
|
||||||
if (el === undefined) {
|
if (el === undefined) {
|
||||||
|
@ -85,12 +103,12 @@ export class Translation extends UIElement {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const date: Date = el;
|
const date: Date = el;
|
||||||
rtext = date.toLocaleString();
|
rtext = date.toLocaleString();
|
||||||
} else if (el.InnerRender === undefined) {
|
} else if (el.InnerRenderAsString === undefined) {
|
||||||
console.error("InnerREnder is not defined", el);
|
console.error("InnerREnder is not defined", el);
|
||||||
throw "Hmmm, el.InnerRender is not defined?"
|
throw "Hmmm, el.InnerRender is not defined?"
|
||||||
} else {
|
} else {
|
||||||
Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
|
Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
|
||||||
rtext = el.InnerRender();
|
rtext = el.InnerRenderAsString();
|
||||||
|
|
||||||
}
|
}
|
||||||
for (let i = 0; i < parts.length - 1; i++) {
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
@ -98,7 +116,7 @@ export class Translation extends UIElement {
|
||||||
combined.push(rtext)
|
combined.push(rtext)
|
||||||
}
|
}
|
||||||
combined.push(parts[parts.length - 1]);
|
combined.push(parts[parts.length - 1]);
|
||||||
template = new Combine(combined).InnerRender();
|
template = combined.join("")
|
||||||
}
|
}
|
||||||
newTranslations[lang] = template;
|
newTranslations[lang] = template;
|
||||||
}
|
}
|
||||||
|
@ -107,16 +125,11 @@ export class Translation extends UIElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this.txt
|
|
||||||
}
|
|
||||||
|
|
||||||
public replace(a: string, b: string) {
|
public replace(a: string, b: string) {
|
||||||
if (a.startsWith("{") && a.endsWith("}")) {
|
if (a.startsWith("{") && a.endsWith("}")) {
|
||||||
a = a.substr(1, a.length - 2);
|
a = a.substr(1, a.length - 2);
|
||||||
}
|
}
|
||||||
const result = this.Subs({[a]: b});
|
return this.Subs({[a]: b});
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Clone() {
|
public Clone() {
|
||||||
|
@ -127,6 +140,9 @@ export class Translation extends UIElement {
|
||||||
|
|
||||||
const tr = {};
|
const tr = {};
|
||||||
for (const lng in this.translations) {
|
for (const lng in this.translations) {
|
||||||
|
if (!this.translations.hasOwnProperty(lng)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
let txt = this.translations[lng];
|
let txt = this.translations[lng];
|
||||||
txt = txt.replace(/\..*/, "");
|
txt = txt.replace(/\..*/, "");
|
||||||
txt = Utils.EllipsesAfter(txt, 255);
|
txt = Utils.EllipsesAfter(txt, 255);
|
||||||
|
@ -139,6 +155,9 @@ export class Translation extends UIElement {
|
||||||
public ExtractImages(isIcon = false): string[] {
|
public ExtractImages(isIcon = false): string[] {
|
||||||
const allIcons: string[] = []
|
const allIcons: string[] = []
|
||||||
for (const key in this.translations) {
|
for (const key in this.translations) {
|
||||||
|
if (!this.translations.hasOwnProperty(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const render = this.translations[key]
|
const render = this.translations[key]
|
||||||
|
|
||||||
if (isIcon) {
|
if (isIcon) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {UIElement} from "../UIElement";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import AllTranslationAssets from "../../AllTranslationAssets";
|
import AllTranslationAssets from "../../AllTranslationAssets";
|
||||||
import {Translation} from "./Translation";
|
import {Translation} from "./Translation";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class Translations {
|
export default class Translations {
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@ export default class Translations {
|
||||||
}
|
}
|
||||||
|
|
||||||
static t = AllTranslationAssets.t;
|
static t = AllTranslationAssets.t;
|
||||||
public static W(s: string | UIElement): UIElement {
|
public static W(s: string | BaseUIElement): BaseUIElement {
|
||||||
if (typeof (s) === "string") {
|
if (typeof (s) === "string") {
|
||||||
return new FixedUiElement(s);
|
return new FixedUiElement(s);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link href="index.css" rel="stylesheet"/>
|
|
||||||
<link href="css/tabbedComponent.css" rel="stylesheet"/>
|
|
||||||
<title>Custom Theme Generator for Mapcomplete</title>
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
img {
|
|
||||||
min-width: 35px;
|
|
||||||
min-height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input{
|
|
||||||
border: 0.5px solid #939393;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-preview {
|
|
||||||
max-width: 2em;
|
|
||||||
max-height: 2em ;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-large-preview {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 30vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json{
|
|
||||||
width:100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bordered {
|
|
||||||
border: 1px solid black;
|
|
||||||
display:block;
|
|
||||||
padding: 0.5em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-input-row {
|
|
||||||
display: block ruby;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-right: 2em;
|
|
||||||
width: calc(100% - 3em);
|
|
||||||
padding-right: 0.5em;
|
|
||||||
height: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.min-height {
|
|
||||||
display: block;
|
|
||||||
height: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-tabs{
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-tabs > .tabs-header-bar {
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.scrollable {
|
|
||||||
display: block;
|
|
||||||
overflow-y: scroll;
|
|
||||||
height: calc(100vh - 9em - 10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-tabs > .tab-content {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 0 ;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-tabs > .tab-content > span{
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#maindiv {
|
|
||||||
height: calc(100% - 6em);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="maindiv">
|
|
||||||
Loading the MapComplete custom theme builder...<br/>
|
|
||||||
If this message persists, make sure javascript is enabled and no script blocker is blocking this.
|
|
||||||
</div>
|
|
||||||
<script src="./customGenerator.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,32 +0,0 @@
|
||||||
import {UIEventSource} from "./Logic/UIEventSource";
|
|
||||||
import {GenerateEmpty} from "./UI/CustomGenerator/GenerateEmpty";
|
|
||||||
import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
|
|
||||||
import {OsmConnection} from "./Logic/Osm/OsmConnection";
|
|
||||||
import CustomGeneratorPanel from "./UI/CustomGenerator/CustomGeneratorPanel";
|
|
||||||
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
|
||||||
import {Utils} from "./Utils";
|
|
||||||
import LZString from "lz-string";
|
|
||||||
|
|
||||||
let layout = GenerateEmpty.createEmptyLayout();
|
|
||||||
if (window.location.hash.length > 10) {
|
|
||||||
const hash = window.location.hash.substr(1)
|
|
||||||
try{
|
|
||||||
layout = JSON.parse(atob(hash)) as LayoutConfigJson;
|
|
||||||
}catch(e){
|
|
||||||
console.log("Initial load of theme failed, attempt nr 2 with decompression", e)
|
|
||||||
layout = JSON.parse( Utils.UnMinify(LZString.decompressFromBase64(hash)))
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
const hash = LocalStorageSource.Get("last-custom-theme").data
|
|
||||||
if (hash !== undefined) {
|
|
||||||
console.log("Using theme from local storage")
|
|
||||||
layout = JSON.parse(atob(hash)) as LayoutConfigJson;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const connection = new OsmConnection(false, new UIEventSource<string>(undefined), "customGenerator", false);
|
|
||||||
|
|
||||||
new CustomGeneratorPanel(connection, layout)
|
|
||||||
.AttachTo("maindiv");
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import ValidatedTextField from "../UI/Input/ValidatedTextField";
|
||||||
const TurndownService = require('turndown')
|
const TurndownService = require('turndown')
|
||||||
|
|
||||||
function WriteFile(filename, html: UIElement) : void {
|
function WriteFile(filename, html: UIElement) : void {
|
||||||
const md = new TurndownService().turndown(html.InnerRender());
|
const md = new TurndownService().turndown(html.InnerRenderAsString());
|
||||||
writeFileSync(filename, md);
|
writeFileSync(filename, md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,8 +88,8 @@ async function createManifest(layout: LayoutConfig) {
|
||||||
console.log(icon)
|
console.log(icon)
|
||||||
throw "Icon is not an svg for " + layout.id
|
throw "Icon is not an svg for " + layout.id
|
||||||
}
|
}
|
||||||
const ogTitle = Translations.W(layout.title).InnerRender();
|
const ogTitle = Translations.W(layout.title).InnerRenderAsString();
|
||||||
const ogDescr = Translations.W(layout.description ?? "").InnerRender();
|
const ogDescr = Translations.W(layout.description ?? "").InnerRenderAsString();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: name,
|
name: name,
|
||||||
|
@ -109,8 +109,8 @@ async function createLandingPage(layout: LayoutConfig, manifest) {
|
||||||
|
|
||||||
Locale.language.setData(layout.language[0]);
|
Locale.language.setData(layout.language[0]);
|
||||||
|
|
||||||
const ogTitle = Translations.W(layout.title)?.InnerRender();
|
const ogTitle = Translations.W(layout.title)?.InnerRenderAsString();
|
||||||
const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRender();
|
const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRenderAsString();
|
||||||
const ogImage = layout.socialImage;
|
const ogImage = layout.socialImage;
|
||||||
|
|
||||||
let customCss = "";
|
let customCss = "";
|
||||||
|
|
|
@ -20,7 +20,7 @@ function generateWikiEntry(layout: LayoutConfig) {
|
||||||
|region= Worldwide
|
|region= Worldwide
|
||||||
|lang= ${languages}
|
|lang= ${languages}
|
||||||
|descr= A MapComplete theme: ${Translations.W(layout.description)
|
|descr= A MapComplete theme: ${Translations.W(layout.description)
|
||||||
.InnerRender()
|
.InnerRenderAsString()
|
||||||
.replace("<a href='", "[[")
|
.replace("<a href='", "[[")
|
||||||
.replace(/'>.*<\/a>/, "]]")
|
.replace(/'>.*<\/a>/, "]]")
|
||||||
}
|
}
|
||||||
|
|
16
test.ts
16
test.ts
|
@ -1,3 +1,15 @@
|
||||||
import ValidatedTextField from "./UI/Input/ValidatedTextField";
|
import {Translation} from "./UI/i18n/Translation";
|
||||||
|
import Locale from "./UI/i18n/Locale";
|
||||||
|
import Combine from "./UI/Base/Combine";
|
||||||
|
|
||||||
ValidatedTextField.InputForType("phone").AttachTo("maindiv")
|
|
||||||
|
new Combine(["Some language:",new Translation({en:"English",nl:"Nederlands",fr:"Françcais"})]).AttachTo("maindiv")
|
||||||
|
|
||||||
|
Locale.language.setData("nl")
|
||||||
|
window.setTimeout(() => {
|
||||||
|
Locale.language.setData("en")
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
Locale.language.setData("fr")
|
||||||
|
}, 5000)
|
|
@ -145,7 +145,7 @@ export default class TagSpec extends T{
|
||||||
equal("Has no name", tr.GetRenderValue({"noname": "yes"})?.txt);
|
equal("Has no name", tr.GetRenderValue({"noname": "yes"})?.txt);
|
||||||
equal("Ook een {name}", tr.GetRenderValue({"name": "xyz"})?.txt);
|
equal("Ook een {name}", tr.GetRenderValue({"name": "xyz"})?.txt);
|
||||||
equal("Ook een xyz", SubstitutedTranslation.construct(tr.GetRenderValue({"name": "xyz"}),
|
equal("Ook een xyz", SubstitutedTranslation.construct(tr.GetRenderValue({"name": "xyz"}),
|
||||||
new UIEventSource<any>({"name": "xyz"})).InnerRender());
|
new UIEventSource<any>({"name": "xyz"})).InnerRenderAsString());
|
||||||
equal(undefined, tr.GetRenderValue({"foo": "bar"}));
|
equal(undefined, tr.GetRenderValue({"foo": "bar"}));
|
||||||
|
|
||||||
})],
|
})],
|
||||||
|
@ -196,7 +196,7 @@ export default class TagSpec extends T{
|
||||||
const uiEl = new EditableTagRendering(new UIEventSource<any>(
|
const uiEl = new EditableTagRendering(new UIEventSource<any>(
|
||||||
{leisure: "park", "access": "no"}), constr
|
{leisure: "park", "access": "no"}), constr
|
||||||
);
|
);
|
||||||
const rendered = uiEl.InnerRender();
|
const rendered = uiEl.InnerRenderAsString();
|
||||||
equal(true, rendered.indexOf("Niet toegankelijk") > 0)
|
equal(true, rendered.indexOf("Niet toegankelijk") > 0)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default class TagQuestionSpec extends T {
|
||||||
}, undefined, "Testing tag"
|
}, undefined, "Testing tag"
|
||||||
);
|
);
|
||||||
const questionElement = new TagRenderingQuestion(tags, config);
|
const questionElement = new TagRenderingQuestion(tags, config);
|
||||||
const html = questionElement.InnerRender();
|
const html = questionElement.InnerRenderAsString();
|
||||||
T.assertContains("What is the name of this bookcase?", html);
|
T.assertContains("What is the name of this bookcase?", html);
|
||||||
T.assertContains("<input type='text'", html);
|
T.assertContains("<input type='text'", html);
|
||||||
}],
|
}],
|
||||||
|
@ -53,7 +53,7 @@ export default class TagQuestionSpec extends T {
|
||||||
}, undefined, "Testing tag"
|
}, undefined, "Testing tag"
|
||||||
);
|
);
|
||||||
const questionElement = new TagRenderingQuestion(tags, config);
|
const questionElement = new TagRenderingQuestion(tags, config);
|
||||||
const html = questionElement.InnerRender();
|
const html = questionElement.InnerRenderAsString();
|
||||||
T.assertContains("What is the name of this bookcase?", html);
|
T.assertContains("What is the name of this bookcase?", html);
|
||||||
T.assertContains("This bookcase has no name", html);
|
T.assertContains("This bookcase has no name", html);
|
||||||
T.assertContains("<input type='text'", html);
|
T.assertContains("<input type='text'", html);
|
||||||
|
|
Loading…
Reference in a new issue