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 {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import {UIElement} from "../../UI/UIElement";
|
||||
import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation";
|
||||
import SourceConfig from "./SourceConfig";
|
||||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import SubstitutingTag from "../../Logic/Tags/SubstitutingTag";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
|
||||
export default class LayerConfig {
|
||||
|
||||
|
@ -294,7 +294,7 @@ export default class LayerConfig {
|
|||
{
|
||||
icon:
|
||||
{
|
||||
html: UIElement,
|
||||
html: BaseUIElement,
|
||||
iconSize: [number, number],
|
||||
iconAnchor: [number, number],
|
||||
popupAnchor: [number, number],
|
||||
|
@ -361,7 +361,7 @@ export default class LayerConfig {
|
|||
const iconUrlStatic = render(this.icon);
|
||||
const self = this;
|
||||
const mappedHtml = tags.map(tgs => {
|
||||
function genHtmlFromString(sourcePart: string): UIElement {
|
||||
function genHtmlFromString(sourcePart: string): BaseUIElement {
|
||||
if (sourcePart.indexOf("html:") == 0) {
|
||||
// We use § as a replacement for ;
|
||||
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`;
|
||||
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_]*):([^;]*)/)
|
||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||
html = new Combine([
|
||||
|
@ -387,7 +387,7 @@ export default class LayerConfig {
|
|||
const iconUrl = render(self.icon);
|
||||
const rotation = render(self.rotation, "0deg");
|
||||
|
||||
let htmlParts: UIElement[] = [];
|
||||
let htmlParts: BaseUIElement[] = [];
|
||||
let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != ""));
|
||||
for (const sourcePart of sourceParts) {
|
||||
htmlParts.push(genHtmlFromString(sourcePart))
|
||||
|
@ -399,7 +399,7 @@ export default class LayerConfig {
|
|||
continue;
|
||||
}
|
||||
if (iconOverlay.badge) {
|
||||
const badgeParts: UIElement[] = [];
|
||||
const badgeParts: BaseUIElement[] = [];
|
||||
const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != "");
|
||||
|
||||
for (const badgePartStr of partDefs) {
|
||||
|
@ -437,7 +437,7 @@ export default class LayerConfig {
|
|||
} catch (e) {
|
||||
console.error(e, tgs)
|
||||
}
|
||||
return new Combine(htmlParts).Render();
|
||||
return new Combine(htmlParts);
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 State from "./State";
|
||||
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
||||
|
@ -272,7 +272,7 @@ export class InitUiElements {
|
|||
|
||||
// ?-Button on Desktop, opens panel with close-X.
|
||||
const help = new MapControlButton(Svg.help_svg());
|
||||
new CheckBox(
|
||||
new Toggle(
|
||||
fullOptions
|
||||
.SetClass("welcomeMessage")
|
||||
.onClick(() => {/*Catch the click*/
|
||||
|
@ -307,7 +307,7 @@ export class InitUiElements {
|
|||
)
|
||||
|
||||
;
|
||||
const copyrightButton = new CheckBox(
|
||||
const copyrightButton = new Toggle(
|
||||
copyrightNotice,
|
||||
new MapControlButton(Svg.osm_copyright_svg()),
|
||||
copyrightNotice.isShown
|
||||
|
@ -316,13 +316,13 @@ export class InitUiElements {
|
|||
const layerControlPanel = new LayerControlPanel(
|
||||
State.state.layerControlIsOpened)
|
||||
.SetClass("block p-1 rounded-full");
|
||||
const layerControlButton = new CheckBox(
|
||||
const layerControlButton = new Toggle(
|
||||
layerControlPanel,
|
||||
new MapControlButton(Svg.layers_svg()),
|
||||
State.state.layerControlIsOpened
|
||||
)
|
||||
|
||||
const layerControl = new CheckBox(
|
||||
const layerControl = new Toggle(
|
||||
layerControlButton,
|
||||
"",
|
||||
State.state.featureSwitchLayers
|
||||
|
|
|
@ -183,7 +183,6 @@ export default class GeoLocationHandler extends UIElement {
|
|||
self.StartGeolocating(false);
|
||||
}
|
||||
|
||||
this.HideOnEmpty(true);
|
||||
}
|
||||
|
||||
private locate() {
|
||||
|
|
|
@ -21,7 +21,6 @@ class TitleElement extends UIElement {
|
|||
this._allElementsStorage = allElementsStorage;
|
||||
this.ListenTo(Locale.language);
|
||||
this.ListenTo(this._selectedFeature)
|
||||
this.dumbMode = false;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
@ -63,7 +62,7 @@ export default class TitleHandler {
|
|||
selectedFeature.addCallbackAndRun(_ => {
|
||||
const title = new TitleElement(layoutToUse, selectedFeature, allElementsStorage)
|
||||
const d = document.createElement('div');
|
||||
d.innerHTML = title.InnerRender();
|
||||
d.innerHTML = title.InnerRenderAsString();
|
||||
// We pass everything into a div to strip out images etc...
|
||||
document.title = (d.textContent || d.innerText);
|
||||
})
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import {Utils} from "../../Utils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class Combine extends UIElement {
|
||||
private readonly uiElements: UIElement[];
|
||||
export default class Combine extends BaseUIElement {
|
||||
private readonly uiElements: BaseUIElement[];
|
||||
|
||||
constructor(uiElements: (string | UIElement)[]) {
|
||||
constructor(uiElements: (string | BaseUIElement)[]) {
|
||||
super();
|
||||
this.uiElements = Utils.NoNull(uiElements)
|
||||
.map(el => {
|
||||
|
@ -15,18 +15,21 @@ export default class Combine extends UIElement {
|
|||
return el;
|
||||
});
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("span")
|
||||
|
||||
InnerRender(): string {
|
||||
return this.uiElements.map(ui => {
|
||||
if(ui === undefined || ui === null){
|
||||
return "";
|
||||
for (const subEl of this.uiElements) {
|
||||
if(subEl === undefined || subEl === null){
|
||||
continue;
|
||||
}
|
||||
if(ui.Render === undefined){
|
||||
console.error("Not a UI-element", ui);
|
||||
return "";
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if(subHtml !== undefined){
|
||||
el.appendChild(subHtml)
|
||||
}
|
||||
return ui.Render();
|
||||
}).join("");
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -12,11 +12,11 @@ export default class FeatureSwitched extends UIElement{
|
|||
this._swtch = swtch;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender(): UIElement | string {
|
||||
if(this._swtch.data){
|
||||
return this._upstream.Render();
|
||||
}
|
||||
return "";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,7 @@ export class FixedUiElement extends UIElement {
|
|||
super(undefined);
|
||||
this._html = html ?? "";
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return this._html;
|
||||
}
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
import Constants from "../../Models/Constants";
|
||||
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){
|
||||
if(Utils.runningFromConsole){
|
||||
return source;
|
||||
}
|
||||
return `data:image/svg+xml;base64,${(btoa(source))}`;
|
||||
}
|
||||
static AsData(source: string) {
|
||||
if (Utils.runningFromConsole) {
|
||||
return source;
|
||||
}
|
||||
return `data:image/svg+xml;base64,${(btoa(source))}`;
|
||||
}
|
||||
|
||||
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)}">`;
|
||||
}
|
||||
|
||||
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 BaseUIElement from "../BaseUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
|
||||
export default class Link extends UIElement {
|
||||
private readonly _embeddedShow: UIElement;
|
||||
private readonly _target: string;
|
||||
private readonly _newTab: string;
|
||||
export default class Link extends BaseUIElement {
|
||||
private readonly _element: HTMLElement;
|
||||
|
||||
constructor(embeddedShow: UIElement | string, target: string, newTab: boolean = false) {
|
||||
constructor(embeddedShow: BaseUIElement | string, target: string | UIEventSource<string>, newTab: boolean = false) {
|
||||
super();
|
||||
this._embeddedShow = Translations.W(embeddedShow);
|
||||
this._target = target;
|
||||
this._newTab = "";
|
||||
if (newTab) {
|
||||
this._newTab = "target='_blank'"
|
||||
const _embeddedShow = Translations.W(embeddedShow);
|
||||
|
||||
|
||||
const el = document.createElement("a")
|
||||
|
||||
if(typeof target === "string"){
|
||||
el.href = target
|
||||
}else{
|
||||
target.addCallbackAndRun(target => {
|
||||
el.target = target;
|
||||
})
|
||||
}
|
||||
if (newTab) {
|
||||
el.target = "_blank"
|
||||
}
|
||||
el.appendChild(_embeddedShow.ConstructElement())
|
||||
this._element = el
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return `<a href="${this._target}" ${this._newTab}>${this._embeddedShow.Render()}</a>`;
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
|
||||
return this._element;
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,13 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
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 {
|
||||
private static readonly empty = new FixedUiElement("");
|
||||
|
@ -40,8 +46,8 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
})
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._component.Render();
|
||||
InnerRender(): UIElement {
|
||||
return this._component;
|
||||
}
|
||||
|
||||
Activate(): void {
|
||||
|
|
|
@ -1,55 +1,51 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
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 {
|
||||
|
||||
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));
|
||||
|
||||
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);
|
||||
if (message !== null) {
|
||||
message.dumbMode = false;
|
||||
}
|
||||
let img;
|
||||
if ((imageUrl ?? "") === "") {
|
||||
img = new FixedUiElement("");
|
||||
img = undefined;
|
||||
} else if (typeof (imageUrl) === "string") {
|
||||
img = new FixedUiElement(`<img style="width: 100%;" src="${imageUrl}" alt="">`);
|
||||
img = new Img(imageUrl).SetClass("w-full")
|
||||
} else {
|
||||
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])
|
||||
.SetClass("flex-shrink-0");
|
||||
|
||||
|
||||
if (message !== null && message.IsEmpty()) {
|
||||
// Message == null: special case to force empty text
|
||||
return [];
|
||||
}
|
||||
|
||||
if (linkTo != undefined) {
|
||||
|
||||
if (linkTo == undefined) {
|
||||
return [
|
||||
`<a class='flex group' href="${linkTo.url}" ${linkTo.newTab ? 'target="_blank"' : ""}>`,
|
||||
image,
|
||||
`<div class='ml-4 overflow-ellipsis'>`,
|
||||
message,
|
||||
`</div>`,
|
||||
`</a>`
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
return [
|
||||
image,
|
||||
message,
|
||||
new Link(
|
||||
new Combine([
|
||||
image,
|
||||
message?.SetClass("block ml-4 overflow-ellipsis")
|
||||
]).SetClass("flex group"),
|
||||
linkTo.url,
|
||||
linkTo.newTab ?? false
|
||||
)
|
||||
];
|
||||
|
||||
}
|
||||
|
|
|
@ -1,39 +1,41 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "./Combine";
|
||||
|
||||
export class TabbedComponent extends UIElement {
|
||||
|
||||
private headers: UIElement[] = [];
|
||||
private readonly header: UIElement;
|
||||
private content: UIElement[] = [];
|
||||
|
||||
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)));
|
||||
const self = this;
|
||||
const tabs: UIElement[] = []
|
||||
|
||||
for (let i = 0; i < elements.length; 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)
|
||||
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()) {
|
||||
headerBar += `<div class=\'tab-single-header ${i == this._source.data ? 'tab-active' : 'tab-non-active'}\'>` +
|
||||
header.Render() + "</div>"
|
||||
const tab = header.SetClass("block tab-single-header")
|
||||
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];
|
||||
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 BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class VariableUiElement extends UIElement {
|
||||
private _html: UIEventSource<string>;
|
||||
export class VariableUiElement extends BaseUIElement {
|
||||
|
||||
constructor(html: UIEventSource<string>) {
|
||||
super(html);
|
||||
this._html = html;
|
||||
private _element : HTMLElement;
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
if(contents === undefined){
|
||||
return
|
||||
}
|
||||
if(typeof contents === "string"){
|
||||
el.innerHTML = contents
|
||||
}else{
|
||||
el.appendChild(contents.ConstructElement())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._html.data;
|
||||
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(", ")
|
||||
|
||||
if (hiddenCount == 0) {
|
||||
|
||||
|
||||
return Translations.t.general.attribution.mapContributionsBy.Subs({
|
||||
contributors: contribs
|
||||
}).InnerRender()
|
||||
})
|
||||
} else {
|
||||
return Translations.t.general.attribution.mapContributionsByAndHidden.Subs({
|
||||
contributors: contribs,
|
||||
hiddenCount: hiddenCount
|
||||
}).InnerRender();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,10 +4,11 @@ import Translations from "../i18n/Translations";
|
|||
import State from "../../State";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class BackgroundSelector extends UIElement {
|
||||
|
||||
private _dropdown: UIElement;
|
||||
private _dropdown: BaseUIElement;
|
||||
private readonly _availableLayers: UIEventSource<BaseLayer[]>;
|
||||
|
||||
constructor() {
|
||||
|
@ -31,8 +32,8 @@ export default class BackgroundSelector extends UIElement {
|
|||
this._dropdown = new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.backgroundLayer);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._dropdown.Render();
|
||||
InnerRender(): BaseUIElement {
|
||||
return this._dropdown;
|
||||
}
|
||||
|
||||
}
|
|
@ -80,8 +80,8 @@ export default class FullWelcomePaneWithTabs extends UIElement {
|
|||
.ListenTo(userDetails);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._component.Render();
|
||||
InnerRender(): UIElement {
|
||||
return this._component;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import State from "../../State";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
@ -65,7 +65,7 @@ export default class LayerSelection extends UIElement {
|
|||
}))
|
||||
const style = "display:flex;align-items:center;"
|
||||
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])
|
||||
.SetStyle(styleWhole),
|
||||
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 LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
|
@ -11,87 +10,93 @@ import * as personal from "../../assets/themes/personalLayout/personalLayout.jso
|
|||
import Constants from "../../Models/Constants";
|
||||
import LanguagePicker from "../LanguagePicker";
|
||||
import IndexText from "./IndexText";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class MoreScreen extends UIElement {
|
||||
private readonly _onMainScreen: boolean;
|
||||
|
||||
private _component: UIElement;
|
||||
export default class MoreScreen extends Combine {
|
||||
|
||||
|
||||
constructor(onMainScreen: boolean = false) {
|
||||
super(State.state.locationControl);
|
||||
this._onMainScreen = onMainScreen;
|
||||
this.ListenTo(State.state.osmConnection.userDetails);
|
||||
this.ListenTo(State.state.installedThemes);
|
||||
super(MoreScreen.Init(onMainScreen, State.state));
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
private static Init(onMainScreen: boolean, state: State): BaseUIElement [] {
|
||||
const tr = Translations.t.general.morescreen;
|
||||
|
||||
const els: UIElement[] = []
|
||||
|
||||
const themeButtons: UIElement[] = []
|
||||
|
||||
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) {
|
||||
let intro: BaseUIElement = tr.intro;
|
||||
let themeButtonStyle = ""
|
||||
let themeListStyle = ""
|
||||
if (onMainScreen) {
|
||||
intro = new Combine([
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
|
||||
.SetClass("absolute top-2 right-3"),
|
||||
new IndexText()
|
||||
])
|
||||
themeButtons.map(e => e?.SetClass("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")
|
||||
themeButtonStyle = "h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden"
|
||||
themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
|
||||
}
|
||||
|
||||
|
||||
|
||||
this._component = new Combine([
|
||||
return[
|
||||
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")
|
||||
]);
|
||||
return this._component.Render();
|
||||
];
|
||||
}
|
||||
|
||||
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 createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined) {
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -100,17 +105,14 @@ export default class MoreScreen extends UIElement {
|
|||
return undefined;
|
||||
}
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const currentLocation = State.state.locationControl.data;
|
||||
const currentLocation = State.state.locationControl;
|
||||
|
||||
let path = window.location.pathname;
|
||||
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
|
||||
path = path.substr(0, path.lastIndexOf("/"));
|
||||
|
@ -119,19 +121,23 @@ export default class MoreScreen extends UIElement {
|
|||
path = "."
|
||||
}
|
||||
|
||||
const params = `z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}`
|
||||
let linkText =
|
||||
`${path}/${layout.id.toLowerCase()}.html?${params}`
|
||||
|
||||
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
|
||||
let linkSuffix = ""
|
||||
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) {
|
||||
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);
|
||||
return new SubtleButton(layout.icon,
|
||||
new Combine([
|
||||
|
@ -144,4 +150,5 @@ export default class MoreScreen extends UIElement {
|
|||
]), {url: linkText, newTab: false});
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -5,7 +5,7 @@ import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
|||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
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(
|
||||
icon,
|
||||
content),
|
||||
|
|
|
@ -19,7 +19,6 @@ export default class ShareButton extends UIElement{
|
|||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self= this;
|
||||
htmlElement.addEventListener('click', () => {
|
||||
if (navigator.share) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {VerticalCombine} from "../Base/VerticalCombine";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
|
@ -9,7 +8,7 @@ import {SubtleButton} from "../Base/SubtleButton";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import State from "../../State";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
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;");
|
||||
}
|
||||
|
||||
const includeLocation = new CheckBox(
|
||||
const includeLocation = new Toggle(
|
||||
new Combine([check(), tr.fsIncludeCurrentLocation]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLocation]),
|
||||
true
|
||||
|
@ -75,7 +74,7 @@ export default class ShareScreen extends UIElement {
|
|||
const currentBackground = new VariableUiElement(currentLayer.map(layer => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render();
|
||||
}));
|
||||
const includeCurrentBackground = new CheckBox(
|
||||
const includeCurrentBackground = new Toggle(
|
||||
new Combine([check(), currentBackground]),
|
||||
new Combine([nocheck(), currentBackground]),
|
||||
true
|
||||
|
@ -90,7 +89,7 @@ export default class ShareScreen extends UIElement {
|
|||
}, [currentLayer]));
|
||||
|
||||
|
||||
const includeLayerChoices = new CheckBox(
|
||||
const includeLayerChoices = new Toggle(
|
||||
new Combine([check(), tr.fsIncludeCurrentLayers]),
|
||||
new Combine([nocheck(), tr.fsIncludeCurrentLayers]),
|
||||
true
|
||||
|
@ -120,7 +119,7 @@ export default class ShareScreen extends UIElement {
|
|||
|
||||
for (const swtch of switches) {
|
||||
|
||||
const checkbox = new CheckBox(
|
||||
const checkbox = new Toggle(
|
||||
new Combine([check(), Translations.W(swtch.human)]),
|
||||
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 host = window.location.host;
|
||||
|
@ -216,8 +215,8 @@ export default class ShareScreen extends UIElement {
|
|||
).onClick(async () => {
|
||||
|
||||
const shareData = {
|
||||
title: Translations.W(layout.id)?.InnerRender() ?? "",
|
||||
text: Translations.W(layout.description)?.InnerRender() ?? "",
|
||||
title: Translations.W(layout.title)?.InnerRenderAsString() ?? "",
|
||||
text: Translations.W(layout.description)?.InnerRenderAsString() ?? "",
|
||||
url: self._link.data,
|
||||
}
|
||||
|
||||
|
@ -251,11 +250,11 @@ export default class ShareScreen extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender(): UIElement {
|
||||
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
|
||||
return new VerticalCombine([
|
||||
return new Combine([
|
||||
this._editLayout,
|
||||
tr.intro,
|
||||
this._link,
|
||||
|
@ -264,7 +263,7 @@ export default class ShareScreen extends UIElement {
|
|||
tr.embedIntro,
|
||||
this._options,
|
||||
this._iframeCode,
|
||||
]).Render()
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
}
|
|
@ -7,6 +7,7 @@ import Translations from "../i18n/Translations";
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class ThemeIntroductionPanel extends UIElement {
|
||||
private languagePicker: UIElement;
|
||||
|
@ -44,7 +45,7 @@ export default class ThemeIntroductionPanel extends UIElement {
|
|||
this.SetClass("link-underline")
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender(): BaseUIElement {
|
||||
const layout : LayoutConfig = this._layout.data;
|
||||
return new Combine([
|
||||
layout.description,
|
||||
|
@ -54,7 +55,7 @@ export default class ThemeIntroductionPanel extends UIElement {
|
|||
"<br/>",
|
||||
this.languagePicker,
|
||||
...layout.CustomCodeSnippets()
|
||||
]).Render()
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -64,10 +64,10 @@ export default class UserBadge extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender(): UIElement {
|
||||
const user = this._userDetails.data;
|
||||
if (!user.loggedIn) {
|
||||
return this._loginButton.Render();
|
||||
return this._loginButton;
|
||||
}
|
||||
|
||||
const linkStyle = "flex items-baseline"
|
||||
|
@ -138,7 +138,7 @@ export default class UserBadge extends UIElement {
|
|||
return new Combine([
|
||||
userIcon,
|
||||
usertext,
|
||||
]).Render()
|
||||
])
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -13,34 +13,37 @@ export default class CenterMessageBox extends UIElement {
|
|||
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 != "") {
|
||||
return {innerHtml: State.state.centerMessage.data, done: false};
|
||||
}
|
||||
const lu = State.state.layerUpdater;
|
||||
if (lu.timeout.data > 0) {
|
||||
return {
|
||||
innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.timeout.data}).Render(),
|
||||
innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.timeout.data}),
|
||||
done: false
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
return {innerHtml: Translations.t.centerMessage.zoomIn.Render(), done: false};
|
||||
return {innerHtml: Translations.t.centerMessage.zoomIn, done: false};
|
||||
} 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;
|
||||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
if(htmlElement.parentElement === null){
|
||||
return;
|
||||
}
|
||||
const pstyle = htmlElement.parentElement.style;
|
||||
if (State.state.centerMessage.data != "") {
|
||||
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 {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Translations from "../i18n/Translations";
|
||||
import CheckBox from "../Input/CheckBox";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Combine from "../Base/Combine";
|
||||
import State from "../../State";
|
||||
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;");
|
||||
this.deleteDialog = new CheckBox(
|
||||
this.deleteDialog = new Toggle(
|
||||
new Combine([
|
||||
deleteButton,
|
||||
cancelButton
|
||||
|
@ -40,17 +40,17 @@ export default class DeleteImage extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender() {
|
||||
if(! State.state?.featureSwitchUserbadge?.data){
|
||||
return "";
|
||||
}
|
||||
|
||||
const value = this.tags.data[this.key];
|
||||
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;
|
||||
});
|
||||
|
||||
this.slideshow = new SlideShow(uiElements).HideOnEmpty(true);
|
||||
this.slideshow = new SlideShow(uiElements);
|
||||
this.SetClass("block w-full");
|
||||
this.slideshow.SetClass("w-full");
|
||||
}
|
||||
|
|
|
@ -9,9 +9,10 @@ import {DropDown} from "../Input/DropDown";
|
|||
import Translations from "../i18n/Translations";
|
||||
import Svg from "../../Svg";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class ImageUploadFlow extends UIElement {
|
||||
private readonly _licensePicker: UIElement;
|
||||
private readonly _licensePicker: BaseUIElement;
|
||||
private readonly _tags: UIEventSource<any>;
|
||||
private readonly _selectedLicence: UIEventSource<string>;
|
||||
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 4.0", shown: Translations.t.image.ccb}
|
||||
],
|
||||
State.state.osmConnection.GetPreference("pictures-license"),
|
||||
"","",
|
||||
"flex flex-col sm:flex-row"
|
||||
);
|
||||
State.state.osmConnection.GetPreference("pictures-license")
|
||||
).SetClass("flex flex-col sm:flex-row");
|
||||
licensePicker.SetStyle("float:left");
|
||||
|
||||
const t = Translations.t.image;
|
||||
|
@ -186,8 +185,6 @@ export class ImageUploadFlow extends UIElement {
|
|||
}
|
||||
|
||||
InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
|
||||
this._licensePicker.Update()
|
||||
const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement
|
||||
const selector = document.getElementById('fileselector-' + this.id)
|
||||
|
|
|
@ -35,7 +35,6 @@ export class SlideShow extends UIElement {
|
|||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
require("slick-carousel")
|
||||
if(this._embeddedElements.data.length == 0){
|
||||
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[]) {
|
||||
super(undefined);
|
||||
this._elements = Utils.NoNull(elements);
|
||||
this.dumbMode = false;
|
||||
|
||||
this.value = new UIEventSource<number[]>([])
|
||||
this.ListenTo(this.value);
|
||||
|
@ -51,7 +50,6 @@ export default class CheckBoxes extends InputElement<number[]> {
|
|||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
|
||||
for (let i = 0; i < this._elements.length; i++) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class ColorPicker extends InputElement<string> {
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import {UIElement} from "../UIElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class CombinedInputElement<T> extends InputElement<T> {
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._combined.ConstructElement();
|
||||
}
|
||||
private readonly _a: InputElement<T>;
|
||||
private readonly _b: UIElement;
|
||||
private readonly _combined: UIElement;
|
||||
private readonly _b: BaseUIElement;
|
||||
private readonly _combined: BaseUIElement;
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
|
||||
constructor(a: InputElement<T>, b: InputElement<T>) {
|
||||
super();
|
||||
this._a = a;
|
||||
|
@ -23,11 +25,6 @@ export default class CombinedInputElement<T> extends InputElement<T> {
|
|||
return this._a.GetValue();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._combined.Render();
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this._a.IsValid(t);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ export default class DirectionInput extends InputElement<string> {
|
|||
|
||||
constructor(value?: UIEventSource<string>) {
|
||||
super();
|
||||
this.dumbMode = false;
|
||||
this.value = value ?? new UIEventSource<string>(undefined);
|
||||
|
||||
this.value.addCallbackAndRun(rotation => {
|
||||
|
@ -48,7 +47,6 @@ export default class DirectionInput extends InputElement<string> {
|
|||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
const self = this;
|
||||
|
||||
function onPosChange(x: number, y: number) {
|
||||
|
|
|
@ -1,50 +1,81 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class DropDown<T> extends InputElement<T> {
|
||||
|
||||
private readonly _label: UIElement;
|
||||
private readonly _values: { value: T; shown: UIElement }[];
|
||||
private static _nextDropdownId = 0;
|
||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
private readonly _element: HTMLElement;
|
||||
|
||||
private readonly _value: UIEventSource<T>;
|
||||
private readonly _values: { value: T; shown: string | BaseUIElement }[];
|
||||
|
||||
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _label_class: string;
|
||||
private readonly _select_class: string;
|
||||
private _form_style: string;
|
||||
|
||||
constructor(label: string | UIElement,
|
||||
values: { value: T, shown: string | UIElement }[],
|
||||
constructor(label: string | BaseUIElement,
|
||||
values: { value: T, shown: string | BaseUIElement }[],
|
||||
value: UIEventSource<T> = undefined,
|
||||
label_class: string = "",
|
||||
select_class: string = "",
|
||||
form_style: string = "flex") {
|
||||
super(undefined);
|
||||
this._form_style = form_style;
|
||||
this._value = value ?? new UIEventSource<T>(undefined);
|
||||
this._label = Translations.W(label);
|
||||
this._label_class = label_class || '';
|
||||
this._select_class = select_class || '';
|
||||
this._values = values.map(v => {
|
||||
return {
|
||||
value: v.value,
|
||||
shown: Translations.W(v.shown)
|
||||
options?: {
|
||||
select_class?: string
|
||||
}
|
||||
}
|
||||
);
|
||||
for (const v of this._values) {
|
||||
this.ListenTo(v.shown._source);
|
||||
) {
|
||||
super();
|
||||
this._values = values;
|
||||
if (values.length <= 1) {
|
||||
return;
|
||||
}
|
||||
this.ListenTo(this._value);
|
||||
|
||||
this.onClick(() => {}) // by registering a click, the click event is consumed and doesn't bubble furter to other elements, e.g. checkboxes
|
||||
const id = DropDown._nextDropdownId;
|
||||
DropDown._nextDropdownId++;
|
||||
|
||||
|
||||
const el = document.createElement("form")
|
||||
this._element = el;
|
||||
el.id = "dropdown" + id;
|
||||
|
||||
{
|
||||
const labelEl = Translations.W(label).ConstructElement()
|
||||
const labelHtml = document.createElement("label")
|
||||
labelHtml.appendChild(labelEl)
|
||||
labelHtml.htmlFor = el.id;
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
const select = document.createElement("select")
|
||||
select.classList.add(...(options?.select_class?.split(" ") ?? []))
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
|
||||
const option = document.createElement("option")
|
||||
option.value = "" + i
|
||||
option.appendChild(Translations.W(values[i].shown).ConstructElement())
|
||||
}
|
||||
|
||||
select.onchange = (() => {
|
||||
var index = select.selectedIndex;
|
||||
value.setData(values[index].value);
|
||||
});
|
||||
|
||||
value.addCallbackAndRun(selected => {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i].value;
|
||||
if (value === selected) {
|
||||
select.selectedIndex = i;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.onClick(() => {
|
||||
}) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
for (const value of this._values) {
|
||||
if (value.value === t) {
|
||||
|
@ -54,44 +85,8 @@ export class DropDown<T> extends InputElement<T> {
|
|||
return false
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
if(this._values.length <=1){
|
||||
return "";
|
||||
}
|
||||
|
||||
let options = "";
|
||||
for (let i = 0; i < this._values.length; i++) {
|
||||
options += "<option value='" + i + "'>" + this._values[i].shown.InnerRender() + "</option>"
|
||||
}
|
||||
|
||||
return `<form class="${this._form_style}">` +
|
||||
`<label class='${this._label_class}' for='dropdown-${this.id}'>${this._label.Render()}</label>` +
|
||||
`<select class='${this._select_class}' name='dropdown-${this.id}' id='dropdown-${this.id}'>` +
|
||||
options +
|
||||
`</select>` +
|
||||
`</form>`;
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
protected InnerUpdate(element) {
|
||||
var e = document.getElementById("dropdown-" + this.id);
|
||||
if(e === null){
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
e.onchange = (() => {
|
||||
// @ts-ignore
|
||||
var index = parseInt(e.selectedIndex);
|
||||
self._value.setData(self._values[index].value);
|
||||
});
|
||||
|
||||
var t = this._value.data;
|
||||
for (let i = 0; i < this._values.length ; i++) {
|
||||
const value = this._values[i].value;
|
||||
if (value === t) {
|
||||
// @ts-ignore
|
||||
e.selectedIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export abstract class InputElement<T> extends UIElement{
|
||||
export abstract class InputElement<T> extends BaseUIElement{
|
||||
|
||||
abstract GetValue() : UIEventSource<T>;
|
||||
abstract IsSelected: UIEventSource<boolean>;
|
||||
|
|
|
@ -3,13 +3,12 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
|
||||
|
||||
export default class InputElementMap<T, X> extends InputElement<X> {
|
||||
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
private readonly _inputElement: InputElement<T>;
|
||||
private isSame: (x0: X, x1: X) => boolean;
|
||||
private readonly fromX: (x: X) => T;
|
||||
private readonly toX: (t: T) => X;
|
||||
private readonly _value: UIEventSource<X>;
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
|
||||
constructor(inputElement: InputElement<T>,
|
||||
isSame: (x0: X, x1: X) => boolean,
|
||||
|
@ -41,19 +40,19 @@ export default class InputElementMap<T, X> extends InputElement<X> {
|
|||
return this._value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._inputElement.InnerRender();
|
||||
}
|
||||
|
||||
IsValid(x: X): boolean {
|
||||
if(x === undefined){
|
||||
if (x === undefined) {
|
||||
return false;
|
||||
}
|
||||
const t = this.fromX(x);
|
||||
if(t === undefined){
|
||||
if (t === undefined) {
|
||||
return false;
|
||||
}
|
||||
return this._inputElement.IsValid(t);
|
||||
}
|
||||
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._inputElement.ConstructElement();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
export class MultiInput<T> extends InputElement<T[]> {
|
||||
|
||||
private readonly _value: UIEventSource<T[]>;
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
private elements: UIElement[] = [];
|
||||
private inputElements: InputElement<T>[] = [];
|
||||
private addTag: UIElement;
|
||||
private _options: { allowMovement?: boolean };
|
||||
|
||||
constructor(
|
||||
addAElement: string,
|
||||
newElement: (() => T),
|
||||
createInput: (() => InputElement<T>),
|
||||
value: UIEventSource<T[]> = undefined,
|
||||
options?: {
|
||||
allowMovement?: boolean
|
||||
}) {
|
||||
super(undefined);
|
||||
this._value = value ?? new UIEventSource<T[]>([]);
|
||||
value = this._value;
|
||||
this.ListenTo(value.map((latest : T[]) => latest.length));
|
||||
this._options = options ?? {};
|
||||
|
||||
this.addTag = new SubtleButton(Svg.addSmall_ui(), addAElement)
|
||||
.SetClass("small-button")
|
||||
.onClick(() => {
|
||||
this.IsSelected.setData(true);
|
||||
value.data.push(newElement());
|
||||
value.ping();
|
||||
});
|
||||
const self = this;
|
||||
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements(createInput));
|
||||
this.createElements(createInput);
|
||||
|
||||
this._value.addCallback(tags => self.load(tags));
|
||||
this.IsSelected = new UIEventSource<boolean>(false);
|
||||
}
|
||||
|
||||
private load(tags: T[]) {
|
||||
if (tags === undefined) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
this.inputElements[i].GetValue().setData(tags[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private UpdateIsSelected(){
|
||||
this.IsSelected.setData(this.inputElements.map(input => input.IsSelected.data).reduce((a,b) => a && b))
|
||||
}
|
||||
|
||||
private createElements(createInput: (() => InputElement<T>)) {
|
||||
this.inputElements.splice(0, this.inputElements.length);
|
||||
this.elements = [];
|
||||
const self = this;
|
||||
for (let i = 0; i < this._value.data.length; i++) {
|
||||
const input = createInput();
|
||||
input.GetValue().addCallback(tag => {
|
||||
self._value.data[i] = tag;
|
||||
self._value.ping();
|
||||
}
|
||||
);
|
||||
this.inputElements.push(input);
|
||||
input.IsSelected.addCallback(() => this.UpdateIsSelected());
|
||||
|
||||
const moveUpBtn = Svg.up_ui()
|
||||
.SetClass('small-image').onClick(() => {
|
||||
const v = self._value.data[i];
|
||||
self._value.data[i] = self._value.data[i - 1];
|
||||
self._value.data[i - 1] = v;
|
||||
self._value.ping();
|
||||
});
|
||||
|
||||
const moveDownBtn =
|
||||
Svg.down_ui()
|
||||
.SetClass('small-image') .onClick(() => {
|
||||
const v = self._value.data[i];
|
||||
self._value.data[i] = self._value.data[i + 1];
|
||||
self._value.data[i + 1] = v;
|
||||
self._value.ping();
|
||||
});
|
||||
|
||||
const controls = [];
|
||||
if (i > 0 && this._options.allowMovement) {
|
||||
controls.push(moveUpBtn);
|
||||
}
|
||||
|
||||
if (i + 1 < this._value.data.length && this._options.allowMovement) {
|
||||
controls.push(moveDownBtn);
|
||||
}
|
||||
|
||||
|
||||
const deleteBtn =
|
||||
Svg.delete_icon_ui().SetClass('small-image')
|
||||
.onClick(() => {
|
||||
self._value.data.splice(i, 1);
|
||||
self._value.ping();
|
||||
});
|
||||
controls.push(deleteBtn);
|
||||
this.elements.push(new Combine([input.SetStyle("width: calc(100% - 2em - 5px)"), new Combine(controls).SetStyle("display:flex;flex-direction:column;width:min-content;")]).SetClass("tag-input-row"))
|
||||
}
|
||||
|
||||
this.Update();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([...this.elements, this.addTag]).Render();
|
||||
}
|
||||
|
||||
IsValid(t: T[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T[]> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {TextField} from "./TextField";
|
||||
|
||||
export default class MultiLingualTextFields extends InputElement<any> {
|
||||
private _fields: Map<string, TextField> = new Map<string, TextField>();
|
||||
private readonly _value: UIEventSource<any>;
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
constructor(languages: UIEventSource<string[]>,
|
||||
textArea: boolean = false,
|
||||
value: UIEventSource<Map<string, UIEventSource<string>>> = undefined) {
|
||||
super(undefined);
|
||||
this._value = value ?? new UIEventSource({});
|
||||
this._value.addCallbackAndRun(latestData => {
|
||||
if (typeof (latestData) === "string") {
|
||||
console.warn("Refusing string for multilingual input", latestData);
|
||||
self._value.setData({});
|
||||
}
|
||||
})
|
||||
|
||||
const self = this;
|
||||
|
||||
function setup(languages: string[]) {
|
||||
if (languages === undefined) {
|
||||
return;
|
||||
}
|
||||
const newFields = new Map<string, TextField>();
|
||||
for (const language of languages) {
|
||||
if (language.length != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let oldField = self._fields.get(language);
|
||||
if (oldField === undefined) {
|
||||
oldField = new TextField({textArea: textArea});
|
||||
oldField.GetValue().addCallback(str => {
|
||||
self._value.data[language] = str;
|
||||
self._value.ping();
|
||||
});
|
||||
oldField.GetValue().setData(self._value.data[language]);
|
||||
|
||||
oldField.IsSelected.addCallback(() => {
|
||||
let selected = false;
|
||||
self._fields.forEach(value => {selected = selected || value.IsSelected.data});
|
||||
self.IsSelected.setData(selected);
|
||||
})
|
||||
|
||||
}
|
||||
newFields.set(language, oldField);
|
||||
}
|
||||
self._fields = newFields;
|
||||
self.Update();
|
||||
|
||||
|
||||
}
|
||||
|
||||
setup(languages.data);
|
||||
languages.addCallback(setup);
|
||||
|
||||
|
||||
function load(latest: any){
|
||||
if(latest === undefined){
|
||||
return;
|
||||
}
|
||||
for (const lang in latest) {
|
||||
self._fields.get(lang)?.GetValue().setData(latest[lang]);
|
||||
}
|
||||
}
|
||||
this._value.addCallback(load);
|
||||
load(this._value.data);
|
||||
}
|
||||
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
super.InnerUpdate(htmlElement);
|
||||
this._fields.forEach(value => value.Update());
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<Map<string, UIEventSource<string>>> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let html = "";
|
||||
this._fields.forEach((field, lang) => {
|
||||
html += `<tr><td>${lang}</td><td>${field.Render()}</td></tr>`
|
||||
})
|
||||
if(html === ""){
|
||||
return "Please define one or more languages"
|
||||
}
|
||||
|
||||
return `<table>${html}</table>`;
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import TagInput from "./SingleTagInput";
|
||||
import {MultiInput} from "./MultiInput";
|
||||
|
||||
export class MultiTagInput extends MultiInput<string> {
|
||||
|
||||
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) {
|
||||
super("Add a new tag",
|
||||
() => "",
|
||||
() => new TagInput(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export class NumberField extends InputElement<number> {
|
||||
private readonly value: UIEventSource<number>;
|
||||
public readonly enterPressed = new UIEventSource<string>(undefined);
|
||||
private readonly _placeholder: UIElement;
|
||||
private options?: {
|
||||
placeholder?: string | UIElement,
|
||||
value?: UIEventSource<number>,
|
||||
isValid?: ((i: number) => boolean),
|
||||
min?: number,
|
||||
max?: number
|
||||
};
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _isValid: (i:number) => boolean;
|
||||
|
||||
constructor(options?: {
|
||||
placeholder?: string | UIElement,
|
||||
value?: UIEventSource<number>,
|
||||
isValid?: ((i:number) => boolean),
|
||||
min?: number,
|
||||
max?:number
|
||||
}) {
|
||||
super(undefined);
|
||||
this.options = options;
|
||||
const self = this;
|
||||
this.value = new UIEventSource<number>(undefined);
|
||||
this.value = options?.value ?? new UIEventSource<number>(undefined);
|
||||
|
||||
this._isValid = options.isValid ?? ((i) => true);
|
||||
|
||||
this._placeholder = Translations.W(options.placeholder ?? "");
|
||||
this.ListenTo(this._placeholder._source);
|
||||
|
||||
this.onClick(() => {
|
||||
self.IsSelected.setData(true)
|
||||
});
|
||||
this.value.addCallback((t) => {
|
||||
const field = document.getElementById("txt-"+this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
field.className = self.IsValid(t) ? "" : "invalid";
|
||||
|
||||
if (t === undefined || t === null) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
field.value = t;
|
||||
});
|
||||
this.dumbMode = false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<number> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
const placeholder = this._placeholder.InnerRender().replace("'", "'");
|
||||
|
||||
let min = "";
|
||||
if(this.options.min){
|
||||
min = `min='${this.options.min}'`;
|
||||
}
|
||||
|
||||
let max = "";
|
||||
if(this.options.min){
|
||||
max = `max='${this.options.max}'`;
|
||||
}
|
||||
|
||||
return `<span id="${this.id}"><form onSubmit='return false' class='form-text-field'>` +
|
||||
`<input type='number' ${min} ${max} placeholder='${placeholder}' id='txt-${this.id}'>` +
|
||||
`</form></span>`;
|
||||
}
|
||||
|
||||
InnerUpdate() {
|
||||
const field = document.getElementById("txt-" + this.id);
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
|
||||
// How much characters are on the right, not including spaces?
|
||||
// @ts-ignore
|
||||
const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length;
|
||||
// @ts-ignore
|
||||
let val: number = Number(field.value);
|
||||
if (!self.IsValid(val)) {
|
||||
self.value.setData(undefined);
|
||||
} else {
|
||||
self.value.setData(val);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (this.value.data !== undefined && this.value.data !== null) {
|
||||
// @ts-ignore
|
||||
field.value = this.value.data;
|
||||
}
|
||||
|
||||
field.addEventListener("focusin", () => self.IsSelected.setData(true));
|
||||
field.addEventListener("focusout", () => self.IsSelected.setData(false));
|
||||
|
||||
|
||||
field.addEventListener("keyup", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
// @ts-ignore
|
||||
self.enterPressed.setData(field.value);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
IsValid(t: number): boolean {
|
||||
if (t === undefined || t === null) {
|
||||
return false
|
||||
}
|
||||
return this._isValid(t);
|
||||
}
|
||||
|
||||
}
|
|
@ -5,48 +5,37 @@ export default class SimpleDatePicker extends InputElement<string> {
|
|||
|
||||
private readonly value: UIEventSource<string>
|
||||
|
||||
private readonly _element: HTMLElement;
|
||||
|
||||
constructor(
|
||||
value?: UIEventSource<string>
|
||||
) {
|
||||
super();
|
||||
this.value = value ?? new UIEventSource<string>(undefined);
|
||||
const self = this;
|
||||
|
||||
const el = document.createElement("input")
|
||||
this._element = el;
|
||||
el.type = "date"
|
||||
el.oninput = () => {
|
||||
// Already in YYYY-MM-DD value!
|
||||
self.value.setData(el.value);
|
||||
}
|
||||
|
||||
|
||||
this.value.addCallbackAndRun(v => {
|
||||
if(v === undefined){
|
||||
return;
|
||||
}
|
||||
self.SetValue(v);
|
||||
el.value = v;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return `<span id="${this.id}"><input type='date' id='date-${this.id}'></span>`;
|
||||
}
|
||||
|
||||
private SetValue(date: string){
|
||||
const field = document.getElementById("date-" + this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
field.value = date;
|
||||
}
|
||||
|
||||
protected InnerUpdate() {
|
||||
const field = document.getElementById("date-" + this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
// Already in YYYY-MM-DD value!
|
||||
// @ts-ignore
|
||||
self.value.setData(field.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element
|
||||
}
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.value;
|
||||
}
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {DropDown} from "./DropDown";
|
||||
import {TextField} from "./TextField";
|
||||
import Combine from "../Base/Combine";
|
||||
import {Utils} from "../../Utils";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FromJSON} from "../../Customizations/JSON/FromJSON";
|
||||
import ValidatedTextField from "./ValidatedTextField";
|
||||
|
||||
export default class SingleTagInput extends InputElement<string> {
|
||||
|
||||
private readonly _value: UIEventSource<string>;
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
|
||||
private key: InputElement<string>;
|
||||
private value: InputElement<string>;
|
||||
private operator: DropDown<string>
|
||||
private readonly helpMessage: UIElement;
|
||||
|
||||
constructor(value: UIEventSource<string> = undefined) {
|
||||
super(undefined);
|
||||
this._value = value ?? new UIEventSource<string>("");
|
||||
this.helpMessage = new VariableUiElement(this._value.map(tagDef => {
|
||||
try {
|
||||
FromJSON.Tag(tagDef, "");
|
||||
return "";
|
||||
} catch (e) {
|
||||
return `<br/><span class='alert'>${e}</span>`
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
this.key = ValidatedTextField.KeyInput();
|
||||
|
||||
this.value = new TextField({
|
||||
placeholder: "value - if blank, matches if key is NOT present",
|
||||
value: new UIEventSource<string>("")
|
||||
}
|
||||
);
|
||||
|
||||
this.operator = new DropDown<string>("", [
|
||||
{value: "=", shown: "="},
|
||||
{value: "~", shown: "~"},
|
||||
{value: "!~", shown: "!~"}
|
||||
]);
|
||||
this.operator.GetValue().setData("=");
|
||||
|
||||
const self = this;
|
||||
|
||||
function updateValue() {
|
||||
if (self.key.GetValue().data === undefined ||
|
||||
self.value.GetValue().data === undefined ||
|
||||
self.operator.GetValue().data === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
self._value.setData(self.key.GetValue().data + self.operator.GetValue().data + self.value.GetValue().data);
|
||||
}
|
||||
|
||||
this.key.GetValue().addCallback(() => updateValue());
|
||||
this.operator.GetValue().addCallback(() => updateValue());
|
||||
this.value.GetValue().addCallback(() => updateValue());
|
||||
|
||||
|
||||
function loadValue(value: string) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
let parts: string[];
|
||||
if (value.indexOf("=") >= 0) {
|
||||
parts = Utils.SplitFirst(value, "=");
|
||||
self.operator.GetValue().setData("=");
|
||||
} else if (value.indexOf("!~") > 0) {
|
||||
parts = Utils.SplitFirst(value, "!~");
|
||||
self.operator.GetValue().setData("!~");
|
||||
|
||||
} else if (value.indexOf("~") > 0) {
|
||||
parts = Utils.SplitFirst(value, "~");
|
||||
self.operator.GetValue().setData("~");
|
||||
} else {
|
||||
console.warn("Invalid value for tag: ", value)
|
||||
return;
|
||||
}
|
||||
self.key.GetValue().setData(parts[0]);
|
||||
self.value.GetValue().setData(parts[1]);
|
||||
}
|
||||
|
||||
self._value.addCallback(loadValue);
|
||||
loadValue(self._value.data);
|
||||
this.IsSelected = this.key.IsSelected.map(
|
||||
isSelected => isSelected || this.value.IsSelected.data, [this.value.IsSelected]
|
||||
)
|
||||
}
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([
|
||||
this.key, this.operator, this.value,
|
||||
this.helpMessage
|
||||
]).Render();
|
||||
}
|
||||
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,99 +1,85 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {InputElement} from "./InputElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class TextField extends InputElement<string> {
|
||||
private readonly value: UIEventSource<string>;
|
||||
public readonly enterPressed = new UIEventSource<string>(undefined);
|
||||
private readonly _placeholder: UIElement;
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _htmlType: string;
|
||||
private readonly _inputMode : string;
|
||||
private readonly _textAreaRows: number;
|
||||
|
||||
private readonly _isValid: (string,country) => boolean;
|
||||
private _label: UIElement;
|
||||
|
||||
private _element: HTMLElement;
|
||||
private readonly _isValid: (s: string, country?: () => string) => boolean;
|
||||
|
||||
constructor(options?: {
|
||||
placeholder?: string | UIElement,
|
||||
placeholder?: string | BaseUIElement,
|
||||
value?: UIEventSource<string>,
|
||||
textArea?: boolean,
|
||||
htmlType?: string,
|
||||
inputMode?: string,
|
||||
label?: UIElement,
|
||||
label?: BaseUIElement,
|
||||
textAreaRows?: number,
|
||||
isValid?: ((s: string, country?: () => string) => boolean)
|
||||
}) {
|
||||
super(undefined);
|
||||
super();
|
||||
const self = this;
|
||||
this.value = new UIEventSource<string>("");
|
||||
options = options ?? {};
|
||||
this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text");
|
||||
this.value = options?.value ?? new UIEventSource<string>(undefined);
|
||||
|
||||
this._label = options.label;
|
||||
this._textAreaRows = options.textAreaRows;
|
||||
this._isValid = options.isValid ?? ((str, country) => true);
|
||||
|
||||
this._placeholder = Translations.W(options.placeholder ?? "");
|
||||
this._inputMode = options.inputMode;
|
||||
this.ListenTo(this._placeholder._source);
|
||||
|
||||
this._isValid = options.isValid ?? (_ => true);
|
||||
|
||||
this.onClick(() => {
|
||||
self.IsSelected.setData(true)
|
||||
});
|
||||
this.value.addCallback((t) => {
|
||||
const field = document.getElementById("txt-"+this.id);
|
||||
if (field === undefined || field === null) {
|
||||
return;
|
||||
}
|
||||
field.className = self.IsValid(t) ? "" : "invalid";
|
||||
|
||||
if (t === undefined || t === null) {
|
||||
|
||||
|
||||
const placeholder = Translations.W(options. placeholder ?? "").ConstructElement().innerText.replace("'", "'");
|
||||
|
||||
this.SetClass("form-text-field")
|
||||
let inputEl : HTMLElement
|
||||
if(options.htmlType === "area"){
|
||||
const el = document.createElement("textarea")
|
||||
el.placeholder = placeholder
|
||||
el.rows = options.textAreaRows
|
||||
el.cols = 50
|
||||
el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box"
|
||||
inputEl = el;
|
||||
}else{
|
||||
const el = document.createElement("input")
|
||||
el.type = options.htmlType
|
||||
el.inputMode = options.inputMode
|
||||
el.placeholder = placeholder
|
||||
inputEl = el
|
||||
}
|
||||
|
||||
const form = document.createElement("form")
|
||||
form.onsubmit = () => false;
|
||||
|
||||
if(options.label){
|
||||
form.appendChild(options.label.ConstructElement())
|
||||
}
|
||||
|
||||
this._element = form;
|
||||
|
||||
const field = inputEl;
|
||||
|
||||
|
||||
this.value.addCallbackAndRun(value => {
|
||||
if (!(value !== undefined && value !== null)) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
field.value = t;
|
||||
});
|
||||
this.dumbMode = false;
|
||||
}
|
||||
field.value = value;
|
||||
if(self.IsValid(value)){
|
||||
self.RemoveClass("invalid")
|
||||
}else{
|
||||
self.SetClass("invalid")
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.value;
|
||||
}
|
||||
})
|
||||
|
||||
InnerRender(): string {
|
||||
|
||||
const placeholder = this._placeholder.InnerRender().replace("'", "'");
|
||||
if (this._htmlType === "area") {
|
||||
return `<span id="${this.id}"><textarea id="txt-${this.id}" placeholder='${placeholder}' class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea></span>`
|
||||
}
|
||||
|
||||
let label = "";
|
||||
if (this._label != undefined) {
|
||||
label = this._label.Render();
|
||||
}
|
||||
let inputMode = ""
|
||||
if(this._inputMode !== undefined){
|
||||
inputMode = `inputmode="${this._inputMode}" `
|
||||
}
|
||||
return new Combine([
|
||||
`<span id="${this.id}">`,
|
||||
`<form onSubmit='return false' class='form-text-field'>`,
|
||||
label,
|
||||
`<input type='${this._htmlType}' ${inputMode} placeholder='${placeholder}' id='txt-${this.id}'/>`,
|
||||
`</form>`,
|
||||
`</span>`
|
||||
]).Render();
|
||||
}
|
||||
|
||||
InnerUpdate() {
|
||||
const field = document.getElementById("txt-" + this.id);
|
||||
const self = this;
|
||||
field.oninput = () => {
|
||||
|
||||
|
||||
// How much characters are on the right, not including spaces?
|
||||
// @ts-ignore
|
||||
const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length;
|
||||
|
@ -107,11 +93,11 @@ export class TextField extends InputElement<string> {
|
|||
// Setting the value might cause the value to be set again. We keep the distance _to the end_ stable, as phone number formatting might cause the start to change
|
||||
// See https://github.com/pietervdvn/MapComplete/issues/103
|
||||
// We reread the field value - it might have changed!
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
val = field.value;
|
||||
let newCursorPos = val.length - endDistance;
|
||||
while(newCursorPos >= 0 &&
|
||||
while(newCursorPos >= 0 &&
|
||||
// We count the number of _actual_ characters (non-space characters) on the right of the new value
|
||||
// This count should become bigger then the end distance
|
||||
val.substr(newCursorPos).replace(/ /g, '').length < endDistance
|
||||
|
@ -119,14 +105,10 @@ export class TextField extends InputElement<string> {
|
|||
newCursorPos --;
|
||||
}
|
||||
// @ts-ignore
|
||||
self.SetCursorPosition(newCursorPos);
|
||||
TextField.SetCursorPosition(newCursorPos);
|
||||
};
|
||||
|
||||
if (this.value.data !== undefined && this.value.data !== null) {
|
||||
// @ts-ignore
|
||||
field.value = this.value.data;
|
||||
}
|
||||
|
||||
|
||||
field.addEventListener("focusin", () => self.IsSelected.setData(true));
|
||||
field.addEventListener("focusout", () => self.IsSelected.setData(false));
|
||||
|
||||
|
@ -136,22 +118,31 @@ export class TextField extends InputElement<string> {
|
|||
// @ts-ignore
|
||||
self.enterPressed.setData(field.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public SetCursorPosition(i: number) {
|
||||
const field = document.getElementById('txt-' + this.id);
|
||||
if(field === undefined || field === null){
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
private static SetCursorPosition(textfield: HTMLElement, i: number) {
|
||||
if(textfield === undefined || textfield === null){
|
||||
return;
|
||||
}
|
||||
if (i === -1) {
|
||||
// @ts-ignore
|
||||
i = field.value.length;
|
||||
i = textfield.value.length;
|
||||
}
|
||||
field.focus();
|
||||
textfield.focus();
|
||||
// @ts-ignore
|
||||
field.setSelectionRange(i, i);
|
||||
textfield.setSelectionRange(i, i);
|
||||
|
||||
}
|
||||
|
||||
|
|
22
UI/Input/Toggle.ts
Normal file
22
UI/Input/Toggle.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
/**
|
||||
* The 'Toggle' is a UIElement showing either one of two elements, depending on the state.
|
||||
* It can be used to implement e.g. checkboxes or collapsible elements
|
||||
*/
|
||||
export default class Toggle extends VariableUiElement{
|
||||
|
||||
public readonly isEnabled: UIEventSource<boolean>;
|
||||
|
||||
constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, data: UIEventSource<boolean> = new UIEventSource<boolean>(false)) {
|
||||
super(
|
||||
data.map(isEnabled => isEnabled ? showEnabled : showDisabled)
|
||||
);
|
||||
this.onClick(() => {
|
||||
data.setData(!data.data);
|
||||
})
|
||||
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
import {UIElement} from "./UIElement";
|
||||
import {DropDown} from "./Input/DropDown";
|
||||
import Locale from "./i18n/Locale";
|
||||
import Svg from "../Svg";
|
||||
import Img from "./Base/Img";
|
||||
|
||||
export default class LanguagePicker {
|
||||
|
||||
|
@ -18,7 +16,7 @@ export default class LanguagePicker {
|
|||
return new DropDown(label, languages.map(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);");
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._contents.Render();
|
||||
InnerRender() {
|
||||
return this._contents;
|
||||
}
|
||||
|
||||
}
|
|
@ -9,21 +9,28 @@ import Constants from "../../Models/Constants";
|
|||
import opening_hours from "opening_hours";
|
||||
|
||||
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;
|
||||
|
||||
constructor(tags: UIEventSource<any>, key: string) {
|
||||
super(tags);
|
||||
this._key = key;
|
||||
this.ListenTo(UIEventSource.Chronic(60*1000)); // Automatically reload every minute
|
||||
this.ListenTo(UIEventSource.Chronic(60 * 1000)); // Automatically reload every minute
|
||||
this.ListenTo(UIEventSource.Chronic(500, () => {
|
||||
return tags.data._country === undefined;
|
||||
}));
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static GetRanges(oh: any, from: Date, to: Date): ({
|
||||
isOpen: boolean,
|
||||
isSpecial: boolean,
|
||||
|
@ -38,7 +45,7 @@ export default class OpeningHoursVisualization extends UIElement {
|
|||
const start = new Date(from);
|
||||
// We go one day more into the past, in order to force rendering of holidays in the start of the period
|
||||
start.setDate(from.getDate() - 1);
|
||||
|
||||
|
||||
const iterator = oh.getIterator(start);
|
||||
|
||||
let prevValue = undefined;
|
||||
|
@ -63,8 +70,8 @@ export default class OpeningHoursVisualization extends UIElement {
|
|||
// simply closed, nothing special here
|
||||
continue;
|
||||
}
|
||||
|
||||
if(value.startDate < from){
|
||||
|
||||
if (value.startDate < from) {
|
||||
continue;
|
||||
}
|
||||
// Get day: sunday is 0, monday is 1. We move everything so that monday == 0
|
||||
|
@ -80,8 +87,190 @@ export default class OpeningHoursVisualization extends UIElement {
|
|||
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,
|
||||
isSpecial: boolean,
|
||||
comment: string,
|
||||
|
@ -131,194 +320,4 @@ export default class OpeningHoursVisualization extends UIElement {
|
|||
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 {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
@ -14,21 +13,20 @@ import {InputElement} from "../Input/InputElement";
|
|||
import PublicHolidayInput from "./PublicHolidayInput";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {Utils} from "../../Utils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
|
||||
export default class OpeningHoursInput extends InputElement<string> {
|
||||
|
||||
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _value: UIEventSource<string>;
|
||||
|
||||
private readonly _ohPicker: UIElement;
|
||||
private readonly _leftoverWarning: UIElement;
|
||||
private readonly _phSelector: UIElement;
|
||||
private readonly _element: BaseUIElement;
|
||||
|
||||
constructor(value: UIEventSource<string> = new UIEventSource<string>("")) {
|
||||
super();
|
||||
|
||||
|
||||
|
||||
const leftoverRules = value.map<string[]>(str => {
|
||||
if (str === undefined) {
|
||||
return []
|
||||
|
@ -61,11 +59,11 @@ export default class OpeningHoursInput extends InputElement<string> {
|
|||
}
|
||||
return "";
|
||||
})
|
||||
this._phSelector = new PublicHolidayInput(ph);
|
||||
const phSelector = new PublicHolidayInput(ph);
|
||||
|
||||
function update() {
|
||||
const regular = OH.ToString(rulesFromOhPicker.data);
|
||||
const rules : string[] = [
|
||||
const rules: string[] = [
|
||||
regular,
|
||||
...leftoverRules.data,
|
||||
ph.data
|
||||
|
@ -76,39 +74,35 @@ export default class OpeningHoursInput extends InputElement<string> {
|
|||
rulesFromOhPicker.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) {
|
||||
return "";
|
||||
}
|
||||
return new Combine([
|
||||
Translations.t.general.opening_hours.not_all_rules_parsed,
|
||||
new FixedUiElement(leftovers.map(r => `${r}<br/>`).join("")).SetClass("subtle")
|
||||
]).Render();
|
||||
new FixedUiElement(leftovers.map(r => `${r}<br/>`).join("")).SetClass("subtle")
|
||||
]);
|
||||
|
||||
}))
|
||||
|
||||
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> {
|
||||
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 {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine from "../Base/Combine";
|
|||
import OpeningHoursPickerTable from "./OpeningHoursPickerTable";
|
||||
import {OH, OpeningHour} from "./OpeningHours";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
||||
private readonly _ohs: UIEventSource<OpeningHour[]>;
|
||||
|
@ -12,7 +13,7 @@ export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
|||
|
||||
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[]>([])) {
|
||||
super();
|
||||
|
@ -49,8 +50,12 @@ export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
|||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this._backgroundTable.Render();
|
||||
InnerRender(): BaseUIElement {
|
||||
return this._backgroundTable;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._backgroundTable.ConstructElement();
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<OpeningHour[]> {
|
||||
|
|
|
@ -8,12 +8,13 @@ import {Utils} from "../../Utils";
|
|||
import {OpeningHour} from "./OpeningHours";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> {
|
||||
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.tuesday,
|
||||
|
@ -28,8 +29,8 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]>
|
|||
private readonly source: UIEventSource<OpeningHour[]>;
|
||||
|
||||
|
||||
constructor(weekdays: UIEventSource<UIElement[]>, source?: UIEventSource<OpeningHour[]>) {
|
||||
super(weekdays);
|
||||
constructor(weekdays: UIEventSource<BaseUIElement[]>, source?: UIEventSource<OpeningHour[]>) {
|
||||
super();
|
||||
this.weekdays = weekdays;
|
||||
this.source = source ?? new UIEventSource<OpeningHour[]>([]);
|
||||
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;
|
||||
if (oh === undefined) {
|
||||
return "";
|
||||
return undefined;
|
||||
}
|
||||
const height = this.getHeight();
|
||||
|
||||
|
@ -62,7 +62,6 @@ export default class OpeningHoursRange extends UIElement {
|
|||
|
||||
return new Combine(content)
|
||||
.SetClass("oh-timerange-inner")
|
||||
.Render();
|
||||
}
|
||||
|
||||
private getHeight(): number {
|
||||
|
|
|
@ -143,7 +143,7 @@ export default class PublicHolidayInput extends InputElement<string> {
|
|||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender(): UIElement {
|
||||
const mode = this._mode.data;
|
||||
if (mode === " ") {
|
||||
return new Combine([this._dropdown,
|
||||
|
@ -154,9 +154,9 @@ export default class PublicHolidayInput extends InputElement<string> {
|
|||
" ",
|
||||
Translations.t.general.opening_hours.openTill,
|
||||
" ",
|
||||
this._endHour]).Render();
|
||||
this._endHour]);
|
||||
}
|
||||
return this._dropdown.Render();
|
||||
return this._dropdown;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
|
|
|
@ -39,7 +39,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
const titleIcons = new Combine(
|
||||
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
|
||||
"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")
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ export default class QuestionBox extends UIElement {
|
|||
this.SetClass("block mb-8")
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender() {
|
||||
const allQuestions : UIElement[] = []
|
||||
for (let i = 0; i < this._tagRenderingQuestions.length; 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())
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender() {
|
||||
if(this._userDetails != undefined && !this._userDetails.data.loggedIn){
|
||||
return this._friendlyLogin.Render();
|
||||
return this._friendlyLogin;
|
||||
}
|
||||
let inactive_class = ''
|
||||
if (this._value.data === false || (this._value.data ?? "") === "") {
|
||||
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;");
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender(): string | UIElement{
|
||||
if (this._configuration.condition !== undefined) {
|
||||
if (!this._configuration.condition.matchesProperties(this._tags.data)) {
|
||||
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);
|
||||
if (tr !== undefined) {
|
||||
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 "";
|
||||
|
|
|
@ -94,7 +94,7 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
).SetClass("block")
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender() {
|
||||
return new Combine([
|
||||
this._question,
|
||||
this._inputElement,
|
||||
|
@ -103,7 +103,6 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
this._appliedTags]
|
||||
)
|
||||
.SetClass("question")
|
||||
.Render()
|
||||
}
|
||||
|
||||
private GenerateInputElement(): InputElement<TagsFilter> {
|
||||
|
|
|
@ -26,7 +26,7 @@ export default class ReviewElement extends UIElement {
|
|||
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender(): UIElement {
|
||||
|
||||
const elements = [];
|
||||
const revs = this._reviews.data;
|
||||
|
@ -56,7 +56,7 @@ export default class ReviewElement extends UIElement {
|
|||
|
||||
.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;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender(): UIElement {
|
||||
|
||||
if(!this.userDetails.data.loggedIn){
|
||||
return Translations.t.reviews.plz_login.Render();
|
||||
return Translations.t.reviews.plz_login;
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
|
@ -103,7 +103,6 @@ export default class ReviewForm extends InputElement<Review> {
|
|||
Translations.t.reviews.tos.SetClass("subtle")
|
||||
])
|
||||
.SetClass("review-form")
|
||||
.Render();
|
||||
}
|
||||
|
||||
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'/>" : ""
|
||||
]).SetClass("flex w-max")
|
||||
}
|
||||
InnerRender(): string {
|
||||
InnerRender(): UIElement {
|
||||
const d = this._review.date;
|
||||
let review = this._review;
|
||||
const el= new Combine(
|
||||
|
@ -51,7 +51,7 @@ export default class SingleReview extends UIElement{
|
|||
if(review.made_by_user.data){
|
||||
el.SetClass("border-attention-catch")
|
||||
}
|
||||
return el.Render();
|
||||
return el;
|
||||
}
|
||||
|
||||
}
|
|
@ -6,11 +6,8 @@ import Combine from "./Base/Combine";
|
|||
import State from "../State";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import SpecialVisualizations from "./SpecialVisualizations";
|
||||
import {Utils} from "../Utils";
|
||||
|
||||
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 translation: Translation;
|
||||
private content: UIElement[];
|
||||
|
@ -37,39 +34,24 @@ export class SubstitutedTranslation extends UIElement {
|
|||
public static construct(
|
||||
translation: Translation,
|
||||
tags: UIEventSource<any>): SubstitutedTranslation {
|
||||
|
||||
/* 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;
|
||||
return new SubstitutedTranslation(translation, tags);
|
||||
}
|
||||
|
||||
public static SubstituteKeys(txt: string, tags: any) {
|
||||
for (const key in tags) {
|
||||
if(!tags.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
txt = txt.replace(new RegExp("{" + key + "}", "g"), tags[key])
|
||||
}
|
||||
return txt;
|
||||
}
|
||||
|
||||
private static GenerateMap() {
|
||||
return new Map<UIEventSource<any>, SubstitutedTranslation>()
|
||||
}
|
||||
|
||||
private static GenerateSubCache() {
|
||||
return new Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>>();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
InnerRender() {
|
||||
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[] {
|
||||
|
@ -118,11 +100,11 @@ export class SubstitutedTranslation extends UIElement {
|
|||
}
|
||||
|
||||
// Let's to a small sanity check to help the theme designers:
|
||||
if(template.search(/{[^}]+\([^}]*\)}/) >= 0){
|
||||
if (template.search(/{[^}]+\([^}]*\)}/) >= 0) {
|
||||
// Hmm, we might have found an invalid rendering name
|
||||
console.warn("Found a suspicious special rendering value in: ", template, " did you mean one of: ", SpecialVisualizations.specialVisualizations.map(sp => sp.funcName+"()").join(", "))
|
||||
console.warn("Found a suspicious special rendering value in: ", template, " did you mean one of: ", SpecialVisualizations.specialVisualizations.map(sp => sp.funcName + "()").join(", "))
|
||||
}
|
||||
|
||||
|
||||
// IF we end up here, no changes have to be made - except to remove any resting {}
|
||||
return [new FixedUiElement(template.replace(/{.*}/g, ""))];
|
||||
}
|
||||
|
|
222
UI/UIElement.ts
222
UI/UIElement.ts
|
@ -1,25 +1,19 @@
|
|||
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;
|
||||
public readonly id: string;
|
||||
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 _onClick: () => void;
|
||||
private _onHover: UIEventSource<boolean>;
|
||||
|
||||
protected constructor(source: UIEventSource<any> = undefined) {
|
||||
super("");
|
||||
this.id = "ui-element-" + UIElement.nextId;
|
||||
super()
|
||||
this.id = `ui-${this.constructor.name}-${UIElement.nextId}`;
|
||||
this._source = source;
|
||||
UIElement.nextId++;
|
||||
this.dumbMode = true;
|
||||
this.ListenTo(source);
|
||||
}
|
||||
|
||||
|
@ -27,183 +21,97 @@ export abstract class UIElement extends UIEventSource<string> {
|
|||
if (source === undefined) {
|
||||
return this;
|
||||
}
|
||||
this.dumbMode = false;
|
||||
const self = this;
|
||||
source.addCallback(() => {
|
||||
self.lastInnerRender = undefined;
|
||||
self.Update();
|
||||
if(self._constructedHtmlElement !== undefined){
|
||||
self.UpdateElement(self._constructedHtmlElement);
|
||||
}
|
||||
|
||||
})
|
||||
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 {
|
||||
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 {
|
||||
this.lastInnerRender = this.InnerRender();
|
||||
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>`
|
||||
return "Don't use Render!"
|
||||
}
|
||||
|
||||
AttachTo(divId: string) {
|
||||
this.dumbMode = false;
|
||||
let element = document.getElementById(divId);
|
||||
if (element === null) {
|
||||
throw "SEVERE: could not attach UIElement to " + divId;
|
||||
}
|
||||
element.innerHTML = this.Render();
|
||||
this.Update();
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract InnerRender(): string;
|
||||
public InnerRenderAsString(): string {
|
||||
let rendered = this.InnerRender();
|
||||
if (typeof rendered !== "string") {
|
||||
let html = rendered.ConstructElement()
|
||||
return html.innerHTML
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
public IsEmpty(): boolean {
|
||||
return this.InnerRender() === "";
|
||||
return this.InnerRender() === undefined || this.InnerRender() === "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Adds all the relevant classes, space seperated
|
||||
* @param clss
|
||||
* @constructor
|
||||
* Should be overridden for specific HTML functionality
|
||||
*/
|
||||
public SetClass(clss: string) {
|
||||
this.dumbMode = false;
|
||||
const all = clss.split(" ");
|
||||
let recordedChange = false;
|
||||
for (const c of all) {
|
||||
if (this.clss.has(clss)) {
|
||||
continue;
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
// Uses the old fashioned way to construct an element using 'InnerRender'
|
||||
const innerRender = this.InnerRender();
|
||||
if (innerRender === undefined || innerRender === "") {
|
||||
return undefined;
|
||||
}
|
||||
const el = document.createElement("span")
|
||||
if (typeof innerRender === "string") {
|
||||
el.innerHTML = innerRender
|
||||
} else {
|
||||
const subElement = innerRender.ConstructElement();
|
||||
if (subElement === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
this.clss.add(c);
|
||||
recordedChange = true;
|
||||
el.appendChild(subElement)
|
||||
}
|
||||
if (recordedChange) {
|
||||
this.Update();
|
||||
}
|
||||
return this;
|
||||
return el;
|
||||
}
|
||||
|
||||
public RemoveClass(clss: string): UIElement {
|
||||
if (this.clss.has(clss)) {
|
||||
this.clss.delete(clss);
|
||||
this.Update();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
protected UpdateElement(el: HTMLElement) : void{
|
||||
const innerRender = this.InnerRender();
|
||||
|
||||
public SetStyle(style: string): UIElement {
|
||||
this.dumbMode = false;
|
||||
this.style = style;
|
||||
this.Update();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Called after the HTML has been replaced. Can be used for css tricks
|
||||
protected InnerUpdate(htmlElement: HTMLElement) {
|
||||
}
|
||||
|
||||
private UpdateAllChildren() {
|
||||
for (const i in this) {
|
||||
const child = this[i];
|
||||
if (child instanceof UIElement) {
|
||||
child.Update();
|
||||
} else if (child instanceof Array) {
|
||||
for (const ch of child) {
|
||||
if (ch instanceof UIElement) {
|
||||
ch.Update();
|
||||
}
|
||||
}
|
||||
if (typeof innerRender === "string") {
|
||||
if(el.innerHTML !== innerRender){
|
||||
el.innerHTML = innerRender
|
||||
}
|
||||
} else {
|
||||
const subElement = innerRender.ConstructElement();
|
||||
if(el.children.length === 1 && el.children[0] === subElement){
|
||||
return; // Nothing changed
|
||||
}
|
||||
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.firstChild);
|
||||
}
|
||||
|
||||
if (subElement === undefined) {
|
||||
return;
|
||||
}
|
||||
el.appendChild(subElement)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated The method should not be used
|
||||
*/
|
||||
protected abstract InnerRender(): string | BaseUIElement;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import Locale from "./Locale";
|
||||
import {Utils} from "../../Utils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class Translation extends UIElement {
|
||||
export class Translation extends BaseUIElement {
|
||||
|
||||
public static forcedLanguage = undefined;
|
||||
|
||||
public readonly translations: object
|
||||
return
|
||||
allIcons;
|
||||
|
||||
constructor(translations: object, context?: string) {
|
||||
super(Locale.language)
|
||||
super()
|
||||
if (translations === undefined) {
|
||||
throw `Translation without content (${context})`
|
||||
}
|
||||
let count = 0;
|
||||
for (const translationsKey in translations) {
|
||||
if(!translations.hasOwnProperty(translationsKey)){
|
||||
if (!translations.hasOwnProperty(translationsKey)) {
|
||||
continue
|
||||
}
|
||||
count++;
|
||||
|
@ -46,15 +44,29 @@ export class Translation extends UIElement {
|
|||
return en;
|
||||
}
|
||||
for (const i in this.translations) {
|
||||
if (!this.translations.hasOwnProperty(i)) {
|
||||
continue;
|
||||
}
|
||||
return this.translations[i]; // Return a random language
|
||||
}
|
||||
console.error("Missing language ", Locale.language.data, "for", this.translations)
|
||||
return "";
|
||||
}
|
||||
|
||||
InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("span")
|
||||
Locale.language.addCallbackAndRun(_ => {
|
||||
el.innerHTML = this.txt
|
||||
})
|
||||
return el;
|
||||
}
|
||||
|
||||
public SupportedLanguages(): string[] {
|
||||
const langs = []
|
||||
for (const translationsKey in this.translations) {
|
||||
if (!this.translations.hasOwnProperty(translationsKey)) {
|
||||
continue;
|
||||
}
|
||||
if (translationsKey === "#") {
|
||||
continue;
|
||||
}
|
||||
|
@ -66,9 +78,15 @@ export class Translation extends UIElement {
|
|||
public Subs(text: any): Translation {
|
||||
const newTranslations = {};
|
||||
for (const lang in this.translations) {
|
||||
if (!this.translations.hasOwnProperty(lang)) {
|
||||
continue;
|
||||
}
|
||||
let template: string = this.translations[lang];
|
||||
for (const k in text) {
|
||||
const combined = [];
|
||||
if (!text.hasOwnProperty(k)) {
|
||||
continue
|
||||
}
|
||||
const combined: (string)[] = [];
|
||||
const parts = template.split("{" + k + "}");
|
||||
const el: string | UIElement = text[k];
|
||||
if (el === undefined) {
|
||||
|
@ -85,12 +103,12 @@ export class Translation extends UIElement {
|
|||
// @ts-ignore
|
||||
const date: Date = el;
|
||||
rtext = date.toLocaleString();
|
||||
} else if (el.InnerRender === undefined) {
|
||||
} else if (el.InnerRenderAsString === undefined) {
|
||||
console.error("InnerREnder is not defined", el);
|
||||
throw "Hmmm, el.InnerRender is not defined?"
|
||||
} else {
|
||||
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++) {
|
||||
|
@ -98,7 +116,7 @@ export class Translation extends UIElement {
|
|||
combined.push(rtext)
|
||||
}
|
||||
combined.push(parts[parts.length - 1]);
|
||||
template = new Combine(combined).InnerRender();
|
||||
template = combined.join("")
|
||||
}
|
||||
newTranslations[lang] = template;
|
||||
}
|
||||
|
@ -107,16 +125,11 @@ export class Translation extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return this.txt
|
||||
}
|
||||
|
||||
public replace(a: string, b: string) {
|
||||
if (a.startsWith("{") && a.endsWith("}")) {
|
||||
a = a.substr(1, a.length - 2);
|
||||
}
|
||||
const result = this.Subs({[a]: b});
|
||||
return result;
|
||||
return this.Subs({[a]: b});
|
||||
}
|
||||
|
||||
public Clone() {
|
||||
|
@ -127,6 +140,9 @@ export class Translation extends UIElement {
|
|||
|
||||
const tr = {};
|
||||
for (const lng in this.translations) {
|
||||
if (!this.translations.hasOwnProperty(lng)) {
|
||||
continue
|
||||
}
|
||||
let txt = this.translations[lng];
|
||||
txt = txt.replace(/\..*/, "");
|
||||
txt = Utils.EllipsesAfter(txt, 255);
|
||||
|
@ -139,6 +155,9 @@ export class Translation extends UIElement {
|
|||
public ExtractImages(isIcon = false): string[] {
|
||||
const allIcons: string[] = []
|
||||
for (const key in this.translations) {
|
||||
if (!this.translations.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
const render = this.translations[key]
|
||||
|
||||
if (isIcon) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import {UIElement} from "../UIElement";
|
|||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import AllTranslationAssets from "../../AllTranslationAssets";
|
||||
import {Translation} from "./Translation";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class Translations {
|
||||
|
||||
|
@ -10,7 +11,7 @@ export default class Translations {
|
|||
}
|
||||
|
||||
static t = AllTranslationAssets.t;
|
||||
public static W(s: string | UIElement): UIElement {
|
||||
public static W(s: string | BaseUIElement): BaseUIElement {
|
||||
if (typeof (s) === "string") {
|
||||
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')
|
||||
|
||||
function WriteFile(filename, html: UIElement) : void {
|
||||
const md = new TurndownService().turndown(html.InnerRender());
|
||||
const md = new TurndownService().turndown(html.InnerRenderAsString());
|
||||
writeFileSync(filename, md);
|
||||
}
|
||||
|
||||
|
|
|
@ -88,8 +88,8 @@ async function createManifest(layout: LayoutConfig) {
|
|||
console.log(icon)
|
||||
throw "Icon is not an svg for " + layout.id
|
||||
}
|
||||
const ogTitle = Translations.W(layout.title).InnerRender();
|
||||
const ogDescr = Translations.W(layout.description ?? "").InnerRender();
|
||||
const ogTitle = Translations.W(layout.title).InnerRenderAsString();
|
||||
const ogDescr = Translations.W(layout.description ?? "").InnerRenderAsString();
|
||||
|
||||
return {
|
||||
name: name,
|
||||
|
@ -109,8 +109,8 @@ async function createLandingPage(layout: LayoutConfig, manifest) {
|
|||
|
||||
Locale.language.setData(layout.language[0]);
|
||||
|
||||
const ogTitle = Translations.W(layout.title)?.InnerRender();
|
||||
const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRender();
|
||||
const ogTitle = Translations.W(layout.title)?.InnerRenderAsString();
|
||||
const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRenderAsString();
|
||||
const ogImage = layout.socialImage;
|
||||
|
||||
let customCss = "";
|
||||
|
|
|
@ -20,7 +20,7 @@ function generateWikiEntry(layout: LayoutConfig) {
|
|||
|region= Worldwide
|
||||
|lang= ${languages}
|
||||
|descr= A MapComplete theme: ${Translations.W(layout.description)
|
||||
.InnerRender()
|
||||
.InnerRenderAsString()
|
||||
.replace("<a href='", "[[")
|
||||
.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("Ook een {name}", tr.GetRenderValue({"name": "xyz"})?.txt);
|
||||
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"}));
|
||||
|
||||
})],
|
||||
|
@ -196,7 +196,7 @@ export default class TagSpec extends T{
|
|||
const uiEl = new EditableTagRendering(new UIEventSource<any>(
|
||||
{leisure: "park", "access": "no"}), constr
|
||||
);
|
||||
const rendered = uiEl.InnerRender();
|
||||
const rendered = uiEl.InnerRenderAsString();
|
||||
equal(true, rendered.indexOf("Niet toegankelijk") > 0)
|
||||
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class TagQuestionSpec extends T {
|
|||
}, undefined, "Testing tag"
|
||||
);
|
||||
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("<input type='text'", html);
|
||||
}],
|
||||
|
@ -53,7 +53,7 @@ export default class TagQuestionSpec extends T {
|
|||
}, undefined, "Testing tag"
|
||||
);
|
||||
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("This bookcase has no name", html);
|
||||
T.assertContains("<input type='text'", html);
|
||||
|
|
Loading…
Reference in a new issue