Way to much fixes and improvements
This commit is contained in:
parent
e68d9d99a5
commit
5ed0bb431c
41 changed files with 1244 additions and 402 deletions
48
Customizations/HelpText.ts
Normal file
48
Customizations/HelpText.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import {UIElement} from "../UI/UIElement";
|
||||
import {SubtleButton} from "../UI/Base/SubtleButton";
|
||||
import {VariableUiElement} from "../UI/Base/VariableUIElement";
|
||||
import SingleSetting from "../UI/CustomGenerator/SingleSetting";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
|
||||
export default class HelpText extends UIElement {
|
||||
|
||||
private helpText: UIElement;
|
||||
private returnButton: UIElement;
|
||||
|
||||
constructor(currentSetting: UIEventSource<SingleSetting<any>>) {
|
||||
super();
|
||||
this.returnButton = new SubtleButton("./assets/close.svg",
|
||||
new VariableUiElement(
|
||||
currentSetting.map(currentSetting => {
|
||||
if (currentSetting === undefined) {
|
||||
return "";
|
||||
}
|
||||
return "Return to general help";
|
||||
}
|
||||
)
|
||||
))
|
||||
.ListenTo(currentSetting)
|
||||
.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";
|
||||
}
|
||||
|
||||
return new Combine(["<h1>", setting._name, "</h1>", setting._description.Render()]).Render();
|
||||
}))
|
||||
|
||||
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([this.helpText,
|
||||
this.returnButton,
|
||||
]).Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import {Layout} from "../Layout";
|
||||
import {LayoutConfigJson} from "./LayoutConfigJson";
|
||||
import {AndOrTagConfigJson} from "./TagConfigJson";
|
||||
import {And, RegexTag, Tag, TagsFilter} from "../../Logic/Tags";
|
||||
import {And, Or, RegexTag, Tag, TagsFilter} from "../../Logic/Tags";
|
||||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import {TagRenderingOptions} from "../TagRenderingOptions";
|
||||
import Translation from "../../UI/i18n/Translation";
|
||||
|
@ -81,9 +81,14 @@ export class FromJSON {
|
|||
return json;
|
||||
}
|
||||
const tr = {};
|
||||
let keyCount = 0;
|
||||
for (let key in json) {
|
||||
keyCount ++;
|
||||
tr[key] = json[key]; // I'm doing this wrong, I know
|
||||
}
|
||||
if(keyCount == 0){
|
||||
return undefined;
|
||||
}
|
||||
return new Translation(tr);
|
||||
}
|
||||
|
||||
|
@ -92,7 +97,7 @@ export class FromJSON {
|
|||
}
|
||||
|
||||
public static TagRenderingWithDefault(json: TagRenderingConfigJson | string, propertyName, defaultValue: string): TagDependantUIElementConstructor {
|
||||
if (json === undefined) {
|
||||
if (json === undefined) {
|
||||
if(defaultValue !== undefined){
|
||||
console.warn(`Using default value ${defaultValue} for ${propertyName}`)
|
||||
return FromJSON.TagRendering(defaultValue);
|
||||
|
@ -133,10 +138,10 @@ export class FromJSON {
|
|||
let template = FromJSON.Translation(json.render);
|
||||
|
||||
let freeform = undefined;
|
||||
if (json.freeform) {
|
||||
|
||||
if(json.render === undefined){
|
||||
console.error("Freeform is defined, but render is not. This is not allowed.", json)
|
||||
if (json.freeform?.key) {
|
||||
// Setup the freeform
|
||||
if (template === undefined) {
|
||||
console.error("Freeform.key is defined, but render is not. This is not allowed.", json)
|
||||
throw "Freeform is defined, but render is not. This is not allowed."
|
||||
}
|
||||
|
||||
|
@ -146,13 +151,14 @@ export class FromJSON {
|
|||
key: json.freeform.key
|
||||
};
|
||||
if (json.freeform.addExtraTags) {
|
||||
freeform["extraTags"] = FromJSON.Tag(json.freeform.addExtraTags);
|
||||
freeform.extraTags = new And(json.freeform.addExtraTags.map(FromJSON.SimpleTag))
|
||||
}
|
||||
} else if (json.render) {
|
||||
// Template (aka rendering) is defined, but freeform.key is not. We allow an input as string
|
||||
freeform = {
|
||||
template: `$string$`,
|
||||
template: undefined, // Template to ask is undefined -> we block asking for this key
|
||||
renderTemplate: template,
|
||||
key: "id"
|
||||
key: "id" // every object always has an id
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,6 +170,10 @@ export class FromJSON {
|
|||
})
|
||||
);
|
||||
|
||||
if(template === undefined && (mappings === undefined || mappings.length === 0)){
|
||||
throw "Empty tagrendering detected: no mappings nor template given"
|
||||
}
|
||||
|
||||
|
||||
let rendering = new TagRenderingOptions({
|
||||
question: FromJSON.Translation(json.question),
|
||||
|
@ -185,6 +195,9 @@ export class FromJSON {
|
|||
}
|
||||
|
||||
public static Tag(json: AndOrTagConfigJson | string): TagsFilter {
|
||||
if(json === undefined){
|
||||
throw "Error while parsing a tag: nothing defined. Make sure all the tags are defined and at least one tag is present in a complex expression"
|
||||
}
|
||||
if (typeof (json) == "string") {
|
||||
const tag = json as string;
|
||||
if (tag.indexOf("!~") >= 0) {
|
||||
|
@ -227,7 +240,7 @@ export class FromJSON {
|
|||
return new And(json.and.map(FromJSON.Tag));
|
||||
}
|
||||
if (json.or !== undefined) {
|
||||
return new And(json.or.map(FromJSON.Tag));
|
||||
return new Or(json.or.map(FromJSON.Tag));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,7 +283,8 @@ export class FromJSON {
|
|||
}) ?? [];
|
||||
|
||||
function style(tags) {
|
||||
const iconSizeStr = iconSize.GetContent(tags).txt.split(",");
|
||||
const iconSizeStr =
|
||||
iconSize.GetContent(tags).txt.split(",");
|
||||
const iconwidth = Number(iconSizeStr[0]);
|
||||
const iconheight = Number(iconSizeStr[1]);
|
||||
const iconmode = iconSizeStr[2];
|
||||
|
|
|
@ -66,7 +66,7 @@ export interface LayerConfigJson {
|
|||
* Wayhandling: should a way/area be displayed as:
|
||||
* 0) The way itself
|
||||
* 1) The centerpoint and the way
|
||||
* 2) Only the centerpoint?
|
||||
* 2) Only the centerpoint
|
||||
*/
|
||||
wayHandling?: number;
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
|
||||
export interface AndOrTagConfigJson {
|
||||
|
||||
and?: (string | AndOrTagConfigJson)[]
|
||||
or?: (string | AndOrTagConfigJson)[]
|
||||
|
||||
|
||||
}
|
|
@ -37,7 +37,7 @@ export interface TagRenderingConfigJson {
|
|||
* If a value is added with the textfield, these extra tag is addded.
|
||||
* Usefull to add a 'fixme=freeform textfield used - to be checked'
|
||||
**/
|
||||
addExtraTags?: AndOrTagConfigJson | string;
|
||||
addExtraTags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -127,12 +127,13 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
|
||||
// Prepare the actual input element -> pick an appropriate implementation
|
||||
|
||||
this._questionElement = this.InputElementFor(options);
|
||||
this._questionElement = this.InputElementFor(options) ??
|
||||
new FixedInputElement<TagsFilter>("<span class='alert'>No input possible</span>", new Tag("a","b"));
|
||||
const save = () => {
|
||||
const selection = self._questionElement.GetValue().data;
|
||||
console.log("Tagrendering: saving tags ", selection);
|
||||
if (selection) {
|
||||
State.state.changes.addTag(tags.data.id, selection);
|
||||
State.state?.changes?.addTag(tags.data.id, selection);
|
||||
}
|
||||
self._editMode.setData(false);
|
||||
}
|
||||
|
@ -143,7 +144,7 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
if (tags === undefined) {
|
||||
return Translations.t.general.noTagsSelected.SetClass("subtle").Render();
|
||||
}
|
||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
||||
const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000;
|
||||
if (csCount < State.userJourney.tagsVisibleAt) {
|
||||
return "";
|
||||
}
|
||||
|
@ -154,7 +155,7 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
return tags.asHumanString(true, true);
|
||||
}
|
||||
)
|
||||
);
|
||||
).ListenTo(self._questionElement);
|
||||
|
||||
const cancel = () => {
|
||||
self._questionSkipped.setData(true);
|
||||
|
@ -246,7 +247,7 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
|
||||
|
||||
private InputForFreeForm(freeform): InputElement<TagsFilter> {
|
||||
if (freeform === undefined) {
|
||||
if (freeform?.template === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -269,8 +270,14 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
if (!isValid(string, this._source.data._country)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const tag = new Tag(freeform.key, formatter(string, this._source.data._country));
|
||||
|
||||
if (tag.value.length > 255) {
|
||||
return undefined; // Toolong
|
||||
}
|
||||
|
||||
if (freeform.extraTags === undefined) {
|
||||
return tag;
|
||||
}
|
||||
|
@ -340,7 +347,8 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
if (this.IsKnown()) {
|
||||
return false;
|
||||
}
|
||||
if (this._question === undefined) {
|
||||
if (this._question === undefined ||
|
||||
(this._freeform?.template === undefined && (this._mapping?.length ?? 0) == 0)) {
|
||||
// We don't ask this question in the first place
|
||||
return false;
|
||||
}
|
||||
|
@ -390,15 +398,20 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
|
||||
InnerRender(): string {
|
||||
|
||||
if (this.IsQuestioning() && !State.state?.osmConnection?.userDetails?.data?.loggedIn) {
|
||||
if (this.IsQuestioning()
|
||||
&& (State.state !== undefined) // If State.state is undefined, we are testing/custom theme building -> show regular save
|
||||
&& !State.state.osmConnection.userDetails.data.loggedIn) {
|
||||
|
||||
const question =
|
||||
this.ApplyTemplate(this._question).SetClass('question-text');
|
||||
return "<div class='question'>" +
|
||||
new Combine([
|
||||
question,
|
||||
question.Render(),
|
||||
"<br/>",
|
||||
this._questionElement.Render(),
|
||||
"<span class='login-button-friendly'>" + this._friendlyLogin.Render() + "</span>",
|
||||
"<span class='login-button-friendly'>",
|
||||
this._friendlyLogin,
|
||||
"</span>",
|
||||
]).Render() + "</div>";
|
||||
}
|
||||
|
||||
|
@ -428,7 +441,8 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
|
||||
|
||||
let editButton = "";
|
||||
if (State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) {
|
||||
if (State.state === undefined || // state undefined -> we are custom testing
|
||||
State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) {
|
||||
editButton = this._editButton.Render();
|
||||
}
|
||||
|
||||
|
@ -438,6 +452,8 @@ TagRendering extends UIElement implements TagDependantUIElement {
|
|||
"</span>";
|
||||
}
|
||||
|
||||
console.log("No rendering for",this)
|
||||
|
||||
return "";
|
||||
|
||||
}
|
||||
|
|
|
@ -184,16 +184,18 @@ export class FilteredLayer {
|
|||
idsFromOverpass.add(feature.properties.id);
|
||||
fusedFeatures.push(feature);
|
||||
}
|
||||
this._dataFromOverpass = fusedFeatures;
|
||||
|
||||
console.log("New elements are ", this._newElements)
|
||||
for (const feature of this._newElements) {
|
||||
if (idsFromOverpass.has(feature.properties.id)) {
|
||||
if (!idsFromOverpass.has(feature.properties.id)) {
|
||||
// This element is not yet uploaded or not yet visible in overpass
|
||||
// We include it in the layer
|
||||
fusedFeatures.push(feature);
|
||||
console.log("Adding ", feature," to fusedFeatures")
|
||||
}
|
||||
}
|
||||
|
||||
this._dataFromOverpass = fusedFeatures;
|
||||
|
||||
// We use a new, fused dataset
|
||||
data = {
|
||||
|
|
|
@ -4,6 +4,7 @@ import {FilteredLayer} from "./FilteredLayer";
|
|||
import {Bounds} from "./Bounds";
|
||||
import {Overpass} from "./Osm/Overpass";
|
||||
import {State} from "../State";
|
||||
import {LayerDefinition} from "../Customizations/LayerDefinition";
|
||||
|
||||
export class LayerUpdater {
|
||||
|
||||
|
@ -27,7 +28,7 @@ export class LayerUpdater {
|
|||
const self = this;
|
||||
|
||||
this.sufficentlyZoomed = State.state.locationControl.map(location => {
|
||||
let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
|
||||
let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => (layer as LayerDefinition).minzoom ?? 18));
|
||||
return location.zoom >= minzoom;
|
||||
}, [state.layoutToUse]
|
||||
);
|
||||
|
@ -49,6 +50,9 @@ export class LayerUpdater {
|
|||
const filters: TagsFilter[] = [];
|
||||
state = state ?? State.state;
|
||||
for (const layer of state.layoutToUse.data.layers) {
|
||||
if(typeof(layer) === "string"){
|
||||
continue;
|
||||
}
|
||||
if (state.locationControl.data.zoom < layer.minzoom) {
|
||||
console.log("Not loading layer ", layer.id, " as it needs at least ", layer.minzoom, "zoom")
|
||||
continue;
|
||||
|
|
|
@ -36,9 +36,8 @@ export class UIEventSource<T>{
|
|||
});
|
||||
|
||||
for (const possibleSource of possibleSources) {
|
||||
possibleSource.addCallback(() => {
|
||||
possibleSource?.addCallback(() => {
|
||||
sink.setData(source.data?.data);
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -87,4 +86,24 @@ export class UIEventSource<T>{
|
|||
return this;
|
||||
}
|
||||
|
||||
public stabilized(millisToStabilize) : UIEventSource<T>{
|
||||
|
||||
const newSource = new UIEventSource<T>(this.data);
|
||||
|
||||
let currentCallback = 0;
|
||||
this.addCallback(latestData => {
|
||||
currentCallback++;
|
||||
const thisCallback = currentCallback;
|
||||
window.setTimeout(() => {
|
||||
if(thisCallback === currentCallback){
|
||||
newSource.setData(latestData);
|
||||
}
|
||||
}, millisToStabilize)
|
||||
});
|
||||
|
||||
return newSource;
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -2,14 +2,17 @@ import {UIElement} from "../UIElement";
|
|||
import Translations from "../i18n/Translations";
|
||||
|
||||
export default class Combine extends UIElement {
|
||||
private uiElements: (string | UIElement)[];
|
||||
private className: string = undefined;
|
||||
private clas: string = undefined;
|
||||
private readonly uiElements: (string | UIElement)[];
|
||||
private readonly className: string = undefined;
|
||||
|
||||
constructor(uiElements: (string | UIElement)[], className: string = undefined) {
|
||||
super(undefined);
|
||||
this.dumbMode = false;
|
||||
this.className = className;
|
||||
this.uiElements = uiElements;
|
||||
if (className) {
|
||||
console.error("Deprecated used of className")
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
|
20
UI/Base/PageSplit.ts
Normal file
20
UI/Base/PageSplit.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
|
||||
export default class PageSplit extends UIElement{
|
||||
private _left: UIElement;
|
||||
private _right: UIElement;
|
||||
private _leftPercentage: number;
|
||||
|
||||
constructor(left: UIElement, right:UIElement,
|
||||
leftPercentage: number = 50) {
|
||||
super();
|
||||
this._left = left;
|
||||
this._right = right;
|
||||
this._leftPercentage = leftPercentage;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return `<span class="page-split" style="height: min-content"><span style="width:${this._leftPercentage}%">${this._left.Render()}</span><span style="width:${100-this._leftPercentage}">${this._right.Render()}</span></span>`;
|
||||
}
|
||||
|
||||
}
|
|
@ -4,9 +4,9 @@ import Combine from "./Combine";
|
|||
|
||||
|
||||
export class SubtleButton extends UIElement{
|
||||
private imageUrl: string;
|
||||
private message: UIElement;
|
||||
private linkTo: { url: string, newTab?: boolean } = undefined;
|
||||
private readonly imageUrl: string;
|
||||
private readonly message: UIElement;
|
||||
private readonly linkTo: { url: string, newTab?: boolean } = undefined;
|
||||
|
||||
constructor(imageUrl: string, message: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined) {
|
||||
super(undefined);
|
||||
|
@ -18,7 +18,7 @@ export class SubtleButton extends UIElement{
|
|||
|
||||
InnerRender(): string {
|
||||
|
||||
if(this.message.IsEmpty()){
|
||||
if(this.message !== null && this.message.IsEmpty()){
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ export class SubtleButton extends UIElement{
|
|||
return new Combine([
|
||||
`<a class="subtle-button" href="${this.linkTo.url}" ${this.linkTo.newTab ? 'target="_blank"' : ""}>`,
|
||||
this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
|
||||
this.message,
|
||||
this.message ?? "",
|
||||
'</a>'
|
||||
]).Render();
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export class SubtleButton extends UIElement{
|
|||
return new Combine([
|
||||
'<span class="subtle-button">',
|
||||
this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
|
||||
this.message,
|
||||
this.message ?? "",
|
||||
'</span>'
|
||||
]).Render();
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ export class TabbedComponent extends UIElement {
|
|||
private headers: UIElement[] = [];
|
||||
private content: UIElement[] = [];
|
||||
|
||||
constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab : UIEventSource<number> = new UIEventSource<number>(0)) {
|
||||
super(openedTab);
|
||||
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;
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let element = elements[i];
|
||||
|
|
|
@ -3,34 +3,30 @@ import {TabbedComponent} from "../Base/TabbedComponent";
|
|||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
|
||||
import LayerPanel from "./LayerPanel";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import Combine from "../Base/Combine";
|
||||
import {GenerateEmpty} from "./GenerateEmpty";
|
||||
import PageSplit from "../Base/PageSplit";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import HelpText from "../../Customizations/HelpText";
|
||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
||||
import {FromJSON} from "../../Customizations/JSON/FromJSON";
|
||||
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import TagRenderingPanel from "./TagRenderingPanel";
|
||||
|
||||
export default class AllLayersPanel extends UIElement {
|
||||
|
||||
|
||||
private panel: UIElement;
|
||||
private _config: UIEventSource<LayoutConfigJson>;
|
||||
private _currentlySelected: UIEventSource<SingleSetting<any>>;
|
||||
private languages: UIEventSource<string[]>;
|
||||
private readonly _config: UIEventSource<LayoutConfigJson>;
|
||||
private readonly languages: UIEventSource<string[]>;
|
||||
|
||||
private static createEmptyLayer(): LayerConfigJson {
|
||||
return {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
minzoom: 0,
|
||||
overpassTags: undefined,
|
||||
title: undefined,
|
||||
description: {}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>, currentlySelected: UIEventSource<SingleSetting<any>>,
|
||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
||||
languages: UIEventSource<any>) {
|
||||
super(undefined);
|
||||
this._config = config;
|
||||
this._currentlySelected = currentlySelected;
|
||||
this.languages = languages;
|
||||
|
||||
this.createPanels();
|
||||
|
@ -46,23 +42,85 @@ export default class AllLayersPanel extends UIElement {
|
|||
|
||||
const layers = this._config.data.layers;
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const currentlySelected = new UIEventSource<(SingleSetting<any>)>(undefined);
|
||||
const layer = new LayerPanel(this._config, this.languages, i, currentlySelected);
|
||||
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 VariableUiElement(layer.selectedTagRendering.map(
|
||||
(tagRenderingPanel: TagRenderingPanel) => {
|
||||
if (tagRenderingPanel === undefined) {
|
||||
return "No tag rendering selected at the moment";
|
||||
}
|
||||
|
||||
let es = tagRenderingPanel.GetValue();
|
||||
let tagRenderingConfig: TagRenderingConfigJson = es.data;
|
||||
|
||||
let rendering: UIElement;
|
||||
try {
|
||||
rendering = FromJSON.TagRendering(tagRenderingConfig)
|
||||
.construct({tags: previewTagValue})
|
||||
} 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/>",
|
||||
rendering]).Render();
|
||||
|
||||
},
|
||||
[this._config]
|
||||
)).ListenTo(layer.selectedTagRendering);
|
||||
|
||||
tabs.push({
|
||||
header: "<img src='./assets/bug.svg'>",
|
||||
content: new LayerPanel(this._config, this.languages, i, this._currentlySelected)
|
||||
content:
|
||||
new PageSplit(
|
||||
layer.SetClass("scrollable"),
|
||||
new Combine([
|
||||
helpText,
|
||||
"</br>",
|
||||
"<h2>Testing tags</h2>",
|
||||
previewTagInput,
|
||||
"<h2>Tag Rendering preview</h2>",
|
||||
preview
|
||||
|
||||
]), 60
|
||||
)
|
||||
});
|
||||
}
|
||||
tabs.push({
|
||||
header: "<img src='./assets/add.svg'>",
|
||||
content: new SubtleButton(
|
||||
"./assets/add.svg",
|
||||
"Add a new layer"
|
||||
).onClick(() => {
|
||||
self._config.data.layers.push(AllLayersPanel.createEmptyLayer())
|
||||
self._config.ping();
|
||||
})
|
||||
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(
|
||||
"./assets/add.svg",
|
||||
"Add a new layer"
|
||||
).onClick(() => {
|
||||
self._config.data.layers.push(GenerateEmpty.createEmptyLayer())
|
||||
self._config.ping();
|
||||
})])
|
||||
})
|
||||
|
||||
this.panel = new TabbedComponent(tabs, new UIEventSource<number>(Math.max(0, layers.length-1)));
|
||||
this.panel = new TabbedComponent(tabs, new UIEventSource<number>(Math.max(0, layers.length - 1)));
|
||||
this.Update();
|
||||
}
|
||||
|
||||
|
|
67
UI/CustomGenerator/GenerateEmpty.ts
Normal file
67
UI/CustomGenerator/GenerateEmpty.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
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: undefined,
|
||||
name: undefined,
|
||||
minzoom: 0,
|
||||
overpassTags: {and: [""]},
|
||||
title: undefined,
|
||||
description: {},
|
||||
}
|
||||
}
|
||||
|
||||
public static createEmptyLayout(): LayoutConfigJson {
|
||||
return {
|
||||
id: "",
|
||||
title: {},
|
||||
description: {},
|
||||
language: [],
|
||||
maintainer: "",
|
||||
icon: "./assets/bug.svg",
|
||||
version: "0",
|
||||
startLat: 0,
|
||||
startLon: 0,
|
||||
startZoom: 1,
|
||||
socialImage: "",
|
||||
layers: []
|
||||
}
|
||||
}
|
||||
|
||||
public static createTestLayout(): LayoutConfigJson {
|
||||
return {
|
||||
id: "test",
|
||||
title: {"en": "Test layout"},
|
||||
description: {"en": "A layout for testing"},
|
||||
language: ["en"],
|
||||
maintainer: "Pieter Vander Vennet",
|
||||
icon: "./assets/bug.svg",
|
||||
version: "0",
|
||||
startLat: 0,
|
||||
startLon: 0,
|
||||
startZoom: 1,
|
||||
widenFactor: 0.05,
|
||||
socialImage: "",
|
||||
layers: [{
|
||||
id: "testlayer",
|
||||
name: "Testing layer",
|
||||
minzoom: 15,
|
||||
overpassTags: {and: ["highway=residential"]},
|
||||
title: "Some Title",
|
||||
description: {"en": "Some Description"},
|
||||
icon: {render: {en: "./assets/pencil.svg"}},
|
||||
width: {render: {en: "5"}},
|
||||
tagRenderings: [{
|
||||
render: {"en":"Test Rendering"}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
public static createEmptyTagRendering(): TagRenderingConfigJson {
|
||||
return {};
|
||||
}
|
||||
}
|
|
@ -9,24 +9,37 @@ import {TextField} from "../Input/TextField";
|
|||
import {InputElement} from "../Input/InputElement";
|
||||
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
|
||||
import {CheckBox} from "../Input/CheckBox";
|
||||
import {MultiTagInput} from "../Input/MultiTagInput";
|
||||
import {AndOrTagInput} from "../Input/AndOrTagInput";
|
||||
import TagRenderingPanel from "./TagRenderingPanel";
|
||||
import {GenerateEmpty} from "./GenerateEmpty";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConfigJson";
|
||||
import {MultiInput} from "../Input/MultiInput";
|
||||
import {Tag} from "../../Logic/Tags";
|
||||
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
|
||||
|
||||
/**
|
||||
* Shows the configuration for a single layer
|
||||
*/
|
||||
export default class LayerPanel extends UIElement {
|
||||
private _config: UIEventSource<LayoutConfigJson>;
|
||||
private readonly _config: UIEventSource<LayoutConfigJson>;
|
||||
|
||||
private settingsTable: UIElement;
|
||||
private readonly settingsTable: UIElement;
|
||||
private readonly renderingOptions: UIElement;
|
||||
|
||||
private deleteButton: UIElement;
|
||||
private readonly deleteButton: UIElement;
|
||||
|
||||
public readonly selectedTagRendering: UIEventSource<TagRenderingPanel>
|
||||
= new UIEventSource<TagRenderingPanel>(undefined);
|
||||
private tagRenderings: UIElement;
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
||||
languages: UIEventSource<string[]>,
|
||||
index: number,
|
||||
currentlySelected: UIEventSource<SingleSetting<any>>) {
|
||||
super(undefined);
|
||||
super();
|
||||
this._config = config;
|
||||
this.renderingOptions = this.setupRenderOptions(config, languages, index, currentlySelected);
|
||||
|
||||
const actualDeleteButton = new SubtleButton(
|
||||
"./assets/delete.svg",
|
||||
|
@ -70,17 +83,120 @@ export default class LayerPanel extends UIElement {
|
|||
setting(TextField.StringInput(), "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), "title", "Title", "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(new MultiTagInput(), "overpassTags","Overpass query",
|
||||
new Combine(["The tags to load from overpass. ", MultiTagInput.tagExplanation]))
|
||||
setting(TextField.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: 1, shown: "Show both the ways/areas and the centerpoints"},
|
||||
{value: 2, 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
|
||||
currentlySelected);
|
||||
const self = this;
|
||||
|
||||
const tagRenderings = new MultiInput<TagRenderingConfigJson>("Add a tag rendering/question",
|
||||
() => ({}),
|
||||
() => {
|
||||
const tagPanel = new TagRenderingPanel(languages, currentlySelected)
|
||||
self.registerTagRendering(tagPanel);
|
||||
return tagPanel;
|
||||
});
|
||||
tagRenderings.GetValue().addCallback(
|
||||
tagRenderings => {
|
||||
(config.data.layers[index] as LayerConfigJson).tagRenderings = tagRenderings;
|
||||
config.ping();
|
||||
}
|
||||
)
|
||||
;
|
||||
|
||||
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>>): UIElement {
|
||||
const iconSelect = new TagRenderingPanel(
|
||||
languages, currentlySelected,
|
||||
{
|
||||
title: "Icon",
|
||||
description: "A visual representation for this layer and for the points on the map.",
|
||||
disableQuestions: true
|
||||
});
|
||||
const size = new TagRenderingPanel(languages, currentlySelected,
|
||||
{
|
||||
title: "Icon Size",
|
||||
description: "The size of the icons on the map in pixels. Can vary based on the tagging",
|
||||
disableQuestions: true
|
||||
});
|
||||
const color = new TagRenderingPanel(languages, currentlySelected,
|
||||
{
|
||||
title: "Way and area color",
|
||||
description: "The color or a shown way or area. Can vary based on the tagging",
|
||||
disableQuestions: true
|
||||
});
|
||||
const stroke = new TagRenderingPanel(languages, currentlySelected,
|
||||
{
|
||||
title: "Stroke width",
|
||||
description: "The width of lines representing ways and the outline of areas. Can vary based on the tags",
|
||||
disableQuestions: 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, "size"),
|
||||
setting(color, "color"),
|
||||
setting(stroke, "stroke")
|
||||
], 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>Map rendering options</h2>",
|
||||
this.renderingOptions,
|
||||
"<h2>Tag rendering and questions</h2>",
|
||||
this.tagRenderings,
|
||||
"<h2>Layer delete</h2>",
|
||||
this.deleteButton
|
||||
]).Render();
|
||||
}
|
||||
|
|
64
UI/CustomGenerator/MappingInput.ts
Normal file
64
UI/CustomGenerator/MappingInput.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
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,27 +1,33 @@
|
|||
import SingleSetting from "./SingleSetting";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import PageSplit from "../Base/PageSplit";
|
||||
import Combine from "../Base/Combine";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
|
||||
export default class SettingsTable extends UIElement {
|
||||
|
||||
private _col1: UIElement[] = [];
|
||||
private _col2: InputElement<any>[] = [];
|
||||
private _col2: UIElement[] = [];
|
||||
|
||||
public selectedSetting: UIEventSource<SingleSetting<any>>;
|
||||
|
||||
constructor(elements: 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) {
|
||||
let title: UIElement = new FixedUiElement(element._name);
|
||||
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);
|
||||
|
@ -34,13 +40,19 @@ export default class SettingsTable extends UIElement {
|
|||
}
|
||||
|
||||
InnerRender(): string {
|
||||
let html = "";
|
||||
let elements = [];
|
||||
|
||||
for (let i = 0; i < this._col1.length; i++) {
|
||||
html += `<tr><td>${this._col1[i].Render()}</td><td>${this._col2[i].Render()}</td></tr>`
|
||||
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 `<table><tr>${html}</tr></table>`;
|
||||
return new Combine(elements).Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {UIElement} from "../UIElement";
|
||||
|
@ -12,7 +11,7 @@ export default class SingleSetting<T> {
|
|||
public _description: UIElement;
|
||||
public _options: { showIconPreview?: boolean };
|
||||
|
||||
constructor(config: UIEventSource<LayoutConfigJson>,
|
||||
constructor(config: UIEventSource<any>,
|
||||
value: InputElement<T>,
|
||||
path: string | (string | number)[],
|
||||
name: string,
|
||||
|
@ -47,11 +46,17 @@ export default class SingleSetting<T> {
|
|||
// We have to rewalk every time as parts might be new
|
||||
let configPart = config.data;
|
||||
for (const pathPart of path) {
|
||||
configPart = configPart[pathPart];
|
||||
if (configPart === undefined) {
|
||||
console.warn("Lost the way for path ", path)
|
||||
return;
|
||||
let newConfigPart = configPart[pathPart];
|
||||
if (newConfigPart === undefined) {
|
||||
console.warn("Lost the way for path ", path, " - creating entry")
|
||||
if (typeof (pathPart) === "string") {
|
||||
configPart[pathPart] = {};
|
||||
} else {
|
||||
configPart[pathPart] = [];
|
||||
}
|
||||
newConfigPart = configPart[pathPart];
|
||||
}
|
||||
configPart = newConfigPart;
|
||||
}
|
||||
configPart[lastPart] = value;
|
||||
config.ping();
|
||||
|
@ -66,7 +71,6 @@ export default class SingleSetting<T> {
|
|||
}
|
||||
}
|
||||
const loadedValue = configPart[lastPart];
|
||||
|
||||
if (loadedValue !== undefined) {
|
||||
value.GetValue().setData(loadedValue);
|
||||
}
|
||||
|
@ -81,4 +85,6 @@ export default class SingleSetting<T> {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
103
UI/CustomGenerator/TagRenderingPanel.ts
Normal file
103
UI/CustomGenerator/TagRenderingPanel.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import SingleSetting from "./SingleSetting";
|
||||
import SettingsTable from "./SettingsTable";
|
||||
import {TextField, ValidatedTextField} 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";
|
||||
|
||||
export default class TagRenderingPanel extends InputElement<TagRenderingConfigJson> {
|
||||
|
||||
private intro: UIElement;
|
||||
private settingsTable: UIElement;
|
||||
|
||||
public IsImage = false;
|
||||
private readonly _value: UIEventSource<TagRenderingConfigJson>;
|
||||
public options: { title?: string; description?: string; disableQuestions?: boolean; isImage?: boolean; };
|
||||
|
||||
constructor(languages: UIEventSource<string[]>,
|
||||
currentlySelected: UIEventSource<SingleSetting<any>>,
|
||||
options?: {
|
||||
title?: string,
|
||||
description?: string,
|
||||
disableQuestions?: boolean,
|
||||
isImage?: boolean
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.SetClass("bordered");
|
||||
this.SetClass("min-height");
|
||||
|
||||
this.options = options ?? {};
|
||||
|
||||
this.intro = new Combine(["<h3>", options?.title ?? "TagRendering", "</h3>", options?.description ?? ""])
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
const questionSettings = [
|
||||
|
||||
setting(new MultiLingualTextFields(languages), "question", "Question", "If the key or mapping doesn't match, this question is asked"),
|
||||
|
||||
setting(new AndOrTagInput(), "condition", "Condition",
|
||||
"Only show this tag rendering if these tags matches. Optional field.<br/>Note that the Overpass-tags are already always included in this object"),
|
||||
|
||||
"<h3>Freeform key</h3>",
|
||||
setting(TextField.KeyInput(), ["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(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."),
|
||||
...(options?.disableQuestions ? [] : questionSettings),
|
||||
|
||||
"<h3>Mappings</h3>",
|
||||
setting(new MultiInput<{ if: AndOrTagConfigJson, then: (string | any), hideInAnswer?: boolean }>("Add a mapping",
|
||||
() => ({if: undefined, then: undefined}),
|
||||
() => new MappingInput(languages, options?.disableQuestions ?? false)), "mappings",
|
||||
"Mappings", "")
|
||||
|
||||
];
|
||||
|
||||
this.settingsTable = new SettingsTable(settings, currentlySelected);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([
|
||||
this.intro,
|
||||
this.settingsTable]).Render();
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<TagRenderingConfigJson> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
IsValid(t: TagRenderingConfigJson): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
15
UI/CustomGenerator/TagRenderingPreview.ts
Normal file
15
UI/CustomGenerator/TagRenderingPreview.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import TagRenderingPanel from "./TagRenderingPanel";
|
||||
|
||||
export default class TagRenderingPreview extends UIElement{
|
||||
|
||||
constructor(selectedTagRendering: UIEventSource<TagRenderingPanel>) {
|
||||
super(selectedTagRendering);
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
|
@ -3,88 +3,162 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import TagInput from "./TagInput";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {CheckBox} from "./CheckBox";
|
||||
import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
||||
import {MultiTagInput} from "./MultiTagInput";
|
||||
import {FormatNumberOptions} from "libphonenumber-js";
|
||||
|
||||
export class AndOrTagInput extends InputElement<(string | AndOrTagInput)[]> {
|
||||
class AndOrConfig implements AndOrTagConfigJson {
|
||||
public and: (string | AndOrTagConfigJson)[] = undefined;
|
||||
public or: (string | AndOrTagConfigJson)[] = undefined;
|
||||
}
|
||||
|
||||
|
||||
private readonly _value: UIEventSource<string[]>;
|
||||
export class AndOrTagInput extends InputElement<AndOrTagConfigJson> {
|
||||
|
||||
private readonly _rawTags = new MultiTagInput();
|
||||
private readonly _subAndOrs: AndOrTagInput[] = [];
|
||||
private readonly _isAnd: UIEventSource<boolean> = new UIEventSource<boolean>(true);
|
||||
private readonly _isAndButton;
|
||||
private readonly _addBlock: UIElement;
|
||||
private readonly _value: UIEventSource<AndOrConfig> = new UIEventSource<AndOrConfig>(undefined);
|
||||
|
||||
public bottomLeftButton: UIElement;
|
||||
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
private elements: UIElement[] = [];
|
||||
private inputELements: (InputElement<string> | InputElement<AndOrTagInput>)[] = [];
|
||||
private addTag: UIElement;
|
||||
|
||||
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) {
|
||||
super(undefined);
|
||||
this._value = value;
|
||||
|
||||
this.addTag = new SubtleButton("./assets/addSmall.svg", "Add a tag")
|
||||
.SetClass("small-button")
|
||||
.onClick(() => {
|
||||
this.IsSelected.setData(true);
|
||||
value.data.push("");
|
||||
value.ping();
|
||||
});
|
||||
constructor() {
|
||||
super();
|
||||
const self = this;
|
||||
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements());
|
||||
this.createElements();
|
||||
this._isAndButton = new CheckBox(
|
||||
new SubtleButton("./assets/ampersand.svg", null).SetClass("small-button"),
|
||||
new SubtleButton("./assets/or.svg", null).SetClass("small-button"),
|
||||
this._isAnd);
|
||||
|
||||
|
||||
this._value.addCallback(tags => self.load(tags));
|
||||
this.IsSelected = new UIEventSource<boolean>(false);
|
||||
this._addBlock =
|
||||
new SubtleButton("./assets/addSmall.svg", "Add an and/or-expression")
|
||||
.SetClass("small-button")
|
||||
.onClick(() => {self.createNewBlock()});
|
||||
|
||||
|
||||
this._isAnd.addCallback(() => self.UpdateValue());
|
||||
this._rawTags.GetValue().addCallback(() => {
|
||||
self.UpdateValue()
|
||||
});
|
||||
|
||||
this.IsSelected = this._rawTags.IsSelected;
|
||||
|
||||
this._value.addCallback(tags => self.loadFromValue(tags));
|
||||
|
||||
}
|
||||
|
||||
private load(tags: string[]) {
|
||||
if (tags === undefined) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
console.log("Setting tag ", i)
|
||||
this.inputELements[i].GetValue().setData(tags[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private UpdateIsSelected(){
|
||||
this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b))
|
||||
}
|
||||
|
||||
private createElements() {
|
||||
this.inputELements = [];
|
||||
this.elements = [];
|
||||
for (let i = 0; i < this._value.data.length; i++) {
|
||||
let tag = this._value.data[i];
|
||||
const input = new TagInput(new UIEventSource<string>(tag));
|
||||
input.GetValue().addCallback(tag => {
|
||||
console.log("Writing ", tag)
|
||||
this._value.data[i] = tag;
|
||||
this._value.ping();
|
||||
}
|
||||
);
|
||||
this.inputELements.push(input);
|
||||
input.IsSelected.addCallback(() => this.UpdateIsSelected());
|
||||
const deleteBtn = new FixedUiElement("<img src='./assets/delete.svg' style='max-width: 1.5em; margin-left: 5px;'>")
|
||||
.onClick(() => {
|
||||
this._value.data.splice(i, 1);
|
||||
this._value.ping();
|
||||
});
|
||||
this.elements.push(new Combine([input, deleteBtn, "<br/>"]).SetClass("tag-input-row"))
|
||||
}
|
||||
|
||||
private createNewBlock(){
|
||||
const inputEl = new AndOrTagInput();
|
||||
inputEl.GetValue().addCallback(() => this.UpdateValue());
|
||||
const deleteButton = this.createDeleteButton(inputEl.id);
|
||||
inputEl.bottomLeftButton = deleteButton;
|
||||
this._subAndOrs.push(inputEl);
|
||||
this.Update();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([...this.elements, this.addTag]).SetClass("bordered").Render();
|
||||
private createDeleteButton(elementId: string): UIElement {
|
||||
const self = this;
|
||||
return new SubtleButton("./assets/delete.svg", null).SetClass("small-button")
|
||||
.onClick(() => {
|
||||
for (let i = 0; i < self._subAndOrs.length; i++) {
|
||||
if (self._subAndOrs[i].id === elementId) {
|
||||
self._subAndOrs.splice(i, 1);
|
||||
self.Update();
|
||||
self.UpdateValue();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private loadFromValue(value: AndOrTagConfigJson) {
|
||||
this._isAnd.setData(value.and !== undefined);
|
||||
const tags = value.and ?? value.or;
|
||||
const rawTags: string[] = [];
|
||||
const subTags: AndOrTagConfigJson[] = [];
|
||||
for (const tag of tags) {
|
||||
|
||||
if (typeof (tag) === "string") {
|
||||
rawTags.push(tag);
|
||||
} else {
|
||||
subTags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < rawTags.length; i++) {
|
||||
if (this._rawTags.GetValue().data[i] !== rawTags[i]) {
|
||||
// For some reason, 'setData' isn't stable as the comparison between the lists fails
|
||||
// Probably because we generate a new list object every timee
|
||||
// So we compare again here and update only if we find a difference
|
||||
this._rawTags.GetValue().setData(rawTags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while(this._subAndOrs.length < subTags.length){
|
||||
this.createNewBlock();
|
||||
}
|
||||
|
||||
for (let i = 0; i < subTags.length; i++){
|
||||
let subTag = subTags[i];
|
||||
this._subAndOrs[i].GetValue().setData(subTag);
|
||||
|
||||
}
|
||||
|
||||
IsValid(t: string[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string[]> {
|
||||
private UpdateValue() {
|
||||
const tags: (string | AndOrTagConfigJson)[] = [];
|
||||
tags.push(...this._rawTags.GetValue().data);
|
||||
|
||||
for (const subAndOr of this._subAndOrs) {
|
||||
const subAndOrData = subAndOr._value.data;
|
||||
if (subAndOrData === undefined) {
|
||||
continue;
|
||||
}
|
||||
console.log(subAndOrData);
|
||||
tags.push(subAndOrData);
|
||||
}
|
||||
|
||||
const tagConfig = new AndOrConfig();
|
||||
|
||||
if (this._isAnd.data) {
|
||||
tagConfig.and = tags;
|
||||
} else {
|
||||
tagConfig.or = tags;
|
||||
}
|
||||
this._value.setData(tagConfig);
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<AndOrTagConfigJson> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
const leftColumn = new Combine([
|
||||
this._isAndButton,
|
||||
"<br/>",
|
||||
this.bottomLeftButton ?? ""
|
||||
]);
|
||||
const tags = new Combine([
|
||||
this._rawTags,
|
||||
...this._subAndOrs,
|
||||
this._addBlock
|
||||
]).Render();
|
||||
return `<span class="bordered"><table><tr><td>${leftColumn.Render()}</td><td>${tags}</td></tr></table></span>`;
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: AndOrTagConfigJson): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
89
UI/Input/MultiInput.ts
Normal file
89
UI/Input/MultiInput.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export class MultiInput<T> extends InputElement<T[]> {
|
||||
|
||||
private readonly _value: UIEventSource<T[]>;
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
private elements: UIElement[] = [];
|
||||
private inputELements: InputElement<T>[] = [];
|
||||
private addTag: UIElement;
|
||||
|
||||
constructor(
|
||||
addAElement: string,
|
||||
newElement: (() => T),
|
||||
createInput: (() => InputElement<T>),
|
||||
value: UIEventSource<T[]> = new UIEventSource<T[]>([])) {
|
||||
super(undefined);
|
||||
this._value = value;
|
||||
|
||||
this.addTag = new SubtleButton("./assets/addSmall.svg", addAElement)
|
||||
.SetClass("small-button")
|
||||
.onClick(() => {
|
||||
this.IsSelected.setData(true);
|
||||
value.data.push(newElement());
|
||||
value.ping();
|
||||
});
|
||||
const self = this;
|
||||
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements(createInput));
|
||||
this.createElements(createInput);
|
||||
|
||||
this._value.addCallback(tags => self.load(tags));
|
||||
this.IsSelected = new UIEventSource<boolean>(false);
|
||||
}
|
||||
|
||||
private load(tags: T[]) {
|
||||
if (tags === undefined) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
this.inputELements[i].GetValue().setData(tags[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private UpdateIsSelected(){
|
||||
this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b))
|
||||
}
|
||||
|
||||
private createElements(createInput: (() => InputElement<T>)) {
|
||||
this.inputELements.splice(0, this.inputELements.length);
|
||||
this.elements = [];
|
||||
const self = this;
|
||||
for (let i = 0; i < this._value.data.length; i++) {
|
||||
let tag = this._value.data[i];
|
||||
const input = createInput();
|
||||
input.GetValue().addCallback(tag => {
|
||||
self._value.data[i] = tag;
|
||||
self._value.ping();
|
||||
}
|
||||
);
|
||||
this.inputELements.push(input);
|
||||
input.IsSelected.addCallback(() => this.UpdateIsSelected());
|
||||
const deleteBtn = new FixedUiElement("<img src='./assets/delete.svg' style='max-width: 1.5em; margin-left: 5px;'>")
|
||||
.onClick(() => {
|
||||
self._value.data.splice(i, 1);
|
||||
self._value.ping();
|
||||
});
|
||||
this.elements.push(new Combine([input, deleteBtn, "<br/>"]).SetClass("tag-input-row"))
|
||||
}
|
||||
|
||||
this.Update();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([...this.elements, this.addTag]).Render();
|
||||
}
|
||||
|
||||
IsValid(t: T[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T[]> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
}
|
|
@ -5,88 +5,17 @@ import Combine from "../Base/Combine";
|
|||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import TagInput from "./TagInput";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {MultiInput} from "./MultiInput";
|
||||
|
||||
export class MultiTagInput extends InputElement<string[]> {
|
||||
export class MultiTagInput extends MultiInput<string> {
|
||||
|
||||
public static tagExplanation: UIElement =
|
||||
new FixedUiElement("<h3>How to use the tag-element</h3>")
|
||||
|
||||
private readonly _value: UIEventSource<string[]>;
|
||||
IsSelected: UIEventSource<boolean>;
|
||||
private elements: UIElement[] = [];
|
||||
private inputELements: InputElement<string>[] = [];
|
||||
private addTag: UIElement;
|
||||
|
||||
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) {
|
||||
super(undefined);
|
||||
this._value = value;
|
||||
|
||||
this.addTag = new SubtleButton("./assets/addSmall.svg", "Add a tag")
|
||||
.SetClass("small-button")
|
||||
.onClick(() => {
|
||||
this.IsSelected.setData(true);
|
||||
value.data.push("");
|
||||
value.ping();
|
||||
});
|
||||
const self = this;
|
||||
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements());
|
||||
this.createElements();
|
||||
|
||||
|
||||
this._value.addCallback(tags => self.load(tags));
|
||||
this.IsSelected = new UIEventSource<boolean>(false);
|
||||
}
|
||||
|
||||
private load(tags: string[]) {
|
||||
if (tags === undefined) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
console.log("Setting tag ", i)
|
||||
this.inputELements[i].GetValue().setData(tags[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private UpdateIsSelected(){
|
||||
this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b))
|
||||
}
|
||||
|
||||
private createElements() {
|
||||
this.inputELements = [];
|
||||
this.elements = [];
|
||||
for (let i = 0; i < this._value.data.length; i++) {
|
||||
let tag = this._value.data[i];
|
||||
const input = new TagInput(new UIEventSource<string>(tag));
|
||||
input.GetValue().addCallback(tag => {
|
||||
console.log("Writing ", tag)
|
||||
this._value.data[i] = tag;
|
||||
this._value.ping();
|
||||
}
|
||||
);
|
||||
this.inputELements.push(input);
|
||||
input.IsSelected.addCallback(() => this.UpdateIsSelected());
|
||||
const deleteBtn = new FixedUiElement("<img src='./assets/delete.svg' style='max-width: 1.5em; margin-left: 5px;'>")
|
||||
.onClick(() => {
|
||||
this._value.data.splice(i, 1);
|
||||
this._value.ping();
|
||||
});
|
||||
this.elements.push(new Combine([input, deleteBtn, "<br/>"]).SetClass("tag-input-row"))
|
||||
}
|
||||
|
||||
this.Update();
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
return new Combine([...this.elements, this.addTag]).SetClass("bordered").Render();
|
||||
}
|
||||
|
||||
|
||||
IsValid(t: string[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string[]> {
|
||||
return this._value;
|
||||
super("Add a new tag",
|
||||
() => "",
|
||||
() => new TagInput(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@ import {InputElement} from "./InputElement";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export class RadioButton<T> extends InputElement<T> {
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
private readonly _selectedElementIndex: UIEventSource<number>
|
||||
= new UIEventSource<number>(null);
|
||||
|
@ -26,16 +27,16 @@ export class RadioButton<T> extends InputElement<T> {
|
|||
return elements[selectedIndex].GetValue()
|
||||
}
|
||||
}
|
||||
), elements.map(e => e.GetValue()));
|
||||
), elements.map(e => e?.GetValue()));
|
||||
|
||||
this.value.addCallback((t) => {
|
||||
self.ShowValue(t);
|
||||
self?.ShowValue(t);
|
||||
})
|
||||
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
// If an element is clicked, the radio button corresponding with it should be selected as well
|
||||
elements[i].onClick(() => {
|
||||
elements[i]?.onClick(() => {
|
||||
self._selectedElementIndex.setData(i);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,16 +18,7 @@ export default class SingleTagInput extends InputElement<string> {
|
|||
super(undefined);
|
||||
this._value = value ?? new UIEventSource<string>(undefined);
|
||||
|
||||
this.key = new TextField({
|
||||
placeholder: "key",
|
||||
fromString: str => {
|
||||
if (str?.match(/^[a-zA-Z][a-zA-Z0-9:]*$/)) {
|
||||
return str;
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
toString: str => str
|
||||
});
|
||||
this.key = TextField.KeyInput();
|
||||
|
||||
this.value = new TextField<string>({
|
||||
placeholder: "value - if blank, matches if key is NOT present",
|
||||
|
@ -95,7 +86,8 @@ export default class SingleTagInput extends InputElement<string> {
|
|||
InnerRender(): string {
|
||||
return new Combine([
|
||||
this.key, this.operator, this.value
|
||||
]).Render();
|
||||
]).SetStyle("display:flex")
|
||||
.Render();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,8 +4,33 @@ import Translations from "../i18n/Translations";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import * as EmailValidator from "email-validator";
|
||||
import {parsePhoneNumberFromString} from "libphonenumber-js";
|
||||
import {DropDown} from "./DropDown";
|
||||
|
||||
export class ValidatedTextField {
|
||||
|
||||
public static explanations = {
|
||||
"string": "A basic, 255-char string",
|
||||
"date": "A date",
|
||||
"wikidata": "A wikidata identifier, e.g. Q42",
|
||||
"int": "A number",
|
||||
"nat": "A positive number",
|
||||
"float": "A decimal",
|
||||
"pfloat": "A positive decimal",
|
||||
"email": "An email adress",
|
||||
"url": "A url",
|
||||
"phone": "A phone number"
|
||||
}
|
||||
|
||||
public static TypeDropdown() : DropDown<string>{
|
||||
const values : {value: string, shown: string}[] = [];
|
||||
const expl = ValidatedTextField.explanations;
|
||||
for(const key in expl){
|
||||
values.push({value: key, shown: `${key} - ${expl[key]}`})
|
||||
}
|
||||
return new DropDown<string>("", values)
|
||||
}
|
||||
|
||||
|
||||
public static inputValidation = {
|
||||
"$": () => true,
|
||||
"string": () => true,
|
||||
|
@ -40,6 +65,19 @@ export class TextField<T> extends InputElement<T> {
|
|||
});
|
||||
}
|
||||
|
||||
public static KeyInput(): TextField<string>{
|
||||
return new TextField<string>({
|
||||
placeholder: "key",
|
||||
fromString: str => {
|
||||
if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) {
|
||||
return str;
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
toString: str => str
|
||||
});
|
||||
}
|
||||
|
||||
public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{
|
||||
const isValid = ValidatedTextField.inputValidation[type];
|
||||
extraValidation = extraValidation ?? (() => true)
|
||||
|
|
|
@ -162,6 +162,7 @@ export class ShareScreen extends UIElement {
|
|||
this._iframeCode = new VariableUiElement(
|
||||
url.map((url) => {
|
||||
return `<span class='literal-code iframe-code-block'>
|
||||
<iframe src="${url}" width="100%" height="100%" title="${layout.title.InnerRender()} with MapComplete"></iframe>
|
||||
</span>`
|
||||
})
|
||||
);
|
||||
|
|
|
@ -53,7 +53,7 @@ export class SimpleAddUI extends UIElement {
|
|||
|
||||
if (typeof (preset.icon) !== "string") {
|
||||
const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"});
|
||||
icon = preset.icon.GetContent(tags);
|
||||
icon = preset.icon.GetContent(tags).txt;
|
||||
} else {
|
||||
icon = preset.icon;
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ export class SimpleAddUI extends UIElement {
|
|||
return new Combine([header, Translations.t.general.add.stillLoading]).Render()
|
||||
}
|
||||
|
||||
return header.Render() + new Combine(this._addButtons, "add-popup-all-buttons").Render();
|
||||
return header.Render() + new Combine(this._addButtons).SetClass("add-popup-all-buttons").Render();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
|
||||
export abstract class UIElement extends UIEventSource<string>{
|
||||
export abstract class UIElement extends UIEventSource<string> {
|
||||
|
||||
private static nextId: number = 0;
|
||||
|
||||
|
@ -8,8 +8,12 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
public readonly _source: UIEventSource<any>;
|
||||
private clss: string[] = []
|
||||
|
||||
private style: string;
|
||||
|
||||
private _hideIfEmpty = false;
|
||||
|
||||
public dumbMode = false;
|
||||
|
||||
/**
|
||||
* In the 'deploy'-step, some code needs to be run by ts-node.
|
||||
* However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed.
|
||||
|
@ -30,6 +34,7 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
if (source === undefined) {
|
||||
return this;
|
||||
}
|
||||
this.dumbMode = false;
|
||||
const self = this;
|
||||
source.addCallback(() => {
|
||||
self.Update();
|
||||
|
@ -40,24 +45,56 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
private _onClick: () => void;
|
||||
|
||||
public onClick(f: (() => void)) {
|
||||
this.dumbMode = false;
|
||||
this._onClick = f;
|
||||
this.SetClass("clickable")
|
||||
this.Update();
|
||||
return this;
|
||||
}
|
||||
|
||||
private _onHover: UIEventSource<boolean>;
|
||||
|
||||
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(UIElement.runningFromConsole){
|
||||
if (UIElement.runningFromConsole) {
|
||||
return;
|
||||
}
|
||||
|
||||
let element = document.getElementById(this.id);
|
||||
if (element === undefined || element === null) {
|
||||
// The element is not painted
|
||||
|
||||
if (this.dumbMode) {
|
||||
// We update all the children anyway
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
this.setData(this.InnerRender());
|
||||
element.innerHTML = this.data;
|
||||
|
||||
if (this._hideIfEmpty) {
|
||||
if (element.innerHTML === "") {
|
||||
element.parentElement.style.display = "none";
|
||||
|
@ -70,7 +107,7 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
const self = this;
|
||||
element.onclick = (e) => {
|
||||
// @ts-ignore
|
||||
if(e.consumed){
|
||||
if (e.consumed) {
|
||||
return;
|
||||
}
|
||||
self._onClick();
|
||||
|
@ -81,6 +118,12 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
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);
|
||||
|
||||
for (const i in this) {
|
||||
|
@ -108,10 +151,18 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
}
|
||||
|
||||
Render(): string {
|
||||
return `<span class='uielement ${this.clss.join(" ")}' id='${this.id}'>${this.InnerRender()}</span>`
|
||||
if (this.dumbMode) {
|
||||
return this.InnerRender();
|
||||
}
|
||||
let style = "";
|
||||
if (this.style !== undefined && this.style !== "") {
|
||||
style = `style="${this.style}"`;
|
||||
}
|
||||
return `<span class='uielement ${this.clss.join(" ")}' ${style} id='${this.id}'>${this.InnerRender()}</span>`
|
||||
}
|
||||
|
||||
AttachTo(divId: string) {
|
||||
this.dumbMode = false;
|
||||
let element = document.getElementById(divId);
|
||||
if (element === null) {
|
||||
throw "SEVERE: could not attach UIElement to " + divId;
|
||||
|
@ -143,6 +194,7 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
}
|
||||
|
||||
public SetClass(clss: string): UIElement {
|
||||
this.dumbMode = false;
|
||||
if (this.clss.indexOf(clss) < 0) {
|
||||
this.clss.push(clss);
|
||||
}
|
||||
|
@ -150,14 +202,13 @@ export abstract class UIElement extends UIEventSource<string>{
|
|||
return this;
|
||||
}
|
||||
|
||||
public RemoveClass(clss: string): UIElement {
|
||||
if (this.clss.indexOf(clss) >= 0) {
|
||||
this.clss = this.clss.splice(this.clss.indexOf(clss), 1);
|
||||
}
|
||||
|
||||
public SetStyle(style: string): UIElement {
|
||||
this.dumbMode = false;
|
||||
this.style = style;
|
||||
this.Update();
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -52,8 +52,8 @@ export default class Translation extends UIElement {
|
|||
for (const i in this.translations) {
|
||||
return this.translations[i]; // Return a random language
|
||||
}
|
||||
console.log("Missing language ",Locale.language.data,"for",this.translations)
|
||||
return "Missing translation"
|
||||
console.error("Missing language ",Locale.language.data,"for",this.translations)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
|
53
assets/ampersand.svg
Normal file
53
assets/ampersand.svg
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="275.9444"
|
||||
height="243.66881"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="Ampersand.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="1013"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5503876"
|
||||
inkscape:cx="319.5"
|
||||
inkscape:cy="120"
|
||||
inkscape:window-x="1560"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M 69.184621,88.05971 C 65.398038,84.28878 45.405425,62.369149 47.716835,38.654524 49.990823,15.323837 73.884556,1.2473955 95.97427,0.11693352 c 20.36977,-1.042443 43.85918,4.70805898 53.3103,24.39507048 10.11956,21.079395 -1.28925,45.999521 -18.03685,58.640336 -5.82684,4.398004 -7.18682,4.599329 -15.78717,8.35864 -12.3926,5.41695 -24.869636,10.70587 -37.591472,15.28724 -26.286247,9.46617 -46.329939,30.90918 -45.609377,60.10938 0.656673,26.61116 24.371436,47.43668 49.951101,51.46486 27.220348,4.28654 49.202778,-0.15657 67.923898,-21.02736 8.04442,-8.96814 24.45293,-23.68334 32.63281,-32.53125 14.48284,-15.66562 21.97669,-28.32038 27.29668,-49.45345 3.60407,-14.31675 -20.5185,-11.01811 -16.28105,-23.06216 25.44722,-2.93304 51.02915,-3.7848 76.5625,-5.66406 4.00323,11.84618 -9.36778,8.3653 -26.72951,23.04671 -19.60573,16.579 -28.72934,30.72561 -45.60418,49.85029 l -11.89837,13.48472 c -8.00837,9.07609 -21.15724,23.50336 -29.33044,32.43076 -17.4629,19.07433 -33.57017,30.64012 -59.50887,35.61559 -27.730664,5.31919 -60.623141,2.30496 -80.151308,-20.4437 C -3.6264102,196.44728 -7.0848351,156.57316 15.462826,132.33729 30.171306,116.52755 38.031184,108.84767 57.724466,100.76314 73.147466,94.43165 97.05575,88.100173 109.29677,82.829346 136.69178,71.033402 137.40896,42.147541 124.50818,21.048935 113.44184,2.9504655 80.908653,4.4216525 74.904904,25.669377 69.689417,44.127381 77.089538,56.651269 88.37226,69.60789 l 96.21768,110.49249 c 11.83509,14.20823 29.6542,37.45695 49.80585,41.07969 13.32763,2.39596 30.53611,-3.5713 39.88214,-12.02915 4.97541,9.00928 -2.24528,16.35839 -7.83854,22.01449 -18.29468,18.50022 -49.85481,14.73994 -70.12946,-0.0122 -12.30082,-8.95026 -20.35382,-15.29947 -31.35277,-27.32693 z"
|
||||
id="path2"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
54
assets/or.svg
Normal file
54
assets/or.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="275.9444"
|
||||
height="243.66881"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="or.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1001"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.5567312"
|
||||
inkscape:cx="116.77734"
|
||||
inkscape:cy="95.251996"
|
||||
inkscape:window-x="1560"
|
||||
inkscape:window-y="1060"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:27.45802498;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 136.18279,27.932469 V 214.66155"
|
||||
id="path812"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -93,7 +93,7 @@
|
|||
"condition": "indoor=yes",
|
||||
"freeform": {
|
||||
"key": "access",
|
||||
"addExtraTags": "fixme=Freeform field used for access - doublecheck the value"
|
||||
"addExtraTags": ["fixme=Freeform field used for access - doublecheck the value"]
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
},
|
||||
"freeform": {
|
||||
"key": "artwork_type",
|
||||
"addExtraTags": "fixme=Artowrk type was added with the freeform, might need another check"
|
||||
"addExtraTags": ["fixme=Artowrk type was added with the freeform, might need another check"]
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
|
|
|
@ -56,7 +56,9 @@
|
|||
"render": "Access is {access}",
|
||||
"freeform": {
|
||||
"key": "access",
|
||||
"addExtraTags": "fixme=the tag access was filled out by the user and might need refinement"
|
||||
"addExtraTags": [
|
||||
"fixme=the tag access was filled out by the user and might need refinement"
|
||||
]
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
|
|
|
@ -5,32 +5,6 @@
|
|||
<title>Custom Theme Generator for Mapcomplete</title>
|
||||
|
||||
<style type="text/css">
|
||||
#left {
|
||||
position: absolute;
|
||||
width: 50vw;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#right {
|
||||
position: absolute;
|
||||
width: 50vw;
|
||||
height: 35vh;
|
||||
right: 0;
|
||||
top: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#bottomright {
|
||||
position: absolute;
|
||||
width: 50vw;
|
||||
height: 65vh;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
max-width: 2em;
|
||||
|
@ -52,27 +26,72 @@
|
|||
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="left">
|
||||
'left' not attached
|
||||
<div id="maindiv">
|
||||
'maindiv' not attached
|
||||
</div>
|
||||
<div id="right">'right' not attached</div>
|
||||
<div id="bottomright">'bottomright' not attached</div>
|
||||
<script src="./customGenerator.ts"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,68 +1,51 @@
|
|||
import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
|
||||
import {UIEventSource} from "./Logic/UIEventSource";
|
||||
import SingleSetting from "./UI/CustomGenerator/SingleSetting";
|
||||
import Combine from "./UI/Base/Combine";
|
||||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||
import GeneralSettings from "./UI/CustomGenerator/GeneralSettings";
|
||||
import {SubtleButton} from "./UI/Base/SubtleButton";
|
||||
import {TabbedComponent} from "./UI/Base/TabbedComponent";
|
||||
import AllLayersPanel from "./UI/CustomGenerator/AllLayersPanel";
|
||||
import {ShareScreen} from "./UI/ShareScreen";
|
||||
import {FromJSON} from "./Customizations/JSON/FromJSON";
|
||||
import SharePanel from "./UI/CustomGenerator/SharePanel";
|
||||
import {GenerateEmpty} from "./UI/CustomGenerator/GenerateEmpty";
|
||||
import PageSplit from "./UI/Base/PageSplit";
|
||||
import HelpText from "./Customizations/HelpText";
|
||||
import {TagRendering} from "./Customizations/TagRendering";
|
||||
|
||||
|
||||
const empty: LayoutConfigJson = {
|
||||
id: "",
|
||||
title: {},
|
||||
description: {},
|
||||
language: [],
|
||||
maintainer: "",
|
||||
icon: "./assets/bug.svg",
|
||||
version: "0",
|
||||
startLat: 0,
|
||||
startLon: 0,
|
||||
startZoom: 1,
|
||||
socialImage: "",
|
||||
layers: [],
|
||||
}
|
||||
|
||||
const test: LayoutConfigJson = {
|
||||
id: "test",
|
||||
title: {"en": "Test layout"},
|
||||
description: {"en": "A layout for testing"},
|
||||
language: ["en"],
|
||||
maintainer: "Pieter Vander Vennet",
|
||||
icon: "./assets/bug.svg",
|
||||
version: "0",
|
||||
startLat: 0,
|
||||
startLon: 0,
|
||||
startZoom: 1,
|
||||
widenFactor: 0.05,
|
||||
socialImage: "",
|
||||
layers: [],
|
||||
}
|
||||
|
||||
|
||||
const es = new UIEventSource(test);
|
||||
const es = new UIEventSource(GenerateEmpty.createTestLayout());
|
||||
const encoded = es.map(config => btoa(JSON.stringify(config)));
|
||||
const testUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}&test=true#${encoded}`)
|
||||
const liveUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}#${encoded}`)
|
||||
const iframe = liveUrl.map(url => `<iframe src='${url}' width='100%' height='99%' style="box-sizing: border-box" title='Theme Preview'></iframe>`);
|
||||
|
||||
TagRendering.injectFunction();
|
||||
|
||||
const currentSetting = new UIEventSource<SingleSetting<any>>(undefined)
|
||||
|
||||
const generalSettings = new GeneralSettings(es, currentSetting);
|
||||
const languages = generalSettings.languages;
|
||||
|
||||
|
||||
// The preview
|
||||
const preview = new Combine([
|
||||
new VariableUiElement(iframe.stabilized(2500))
|
||||
]).SetClass("preview")
|
||||
|
||||
|
||||
new TabbedComponent([
|
||||
{
|
||||
header: "<img src='./assets/gear.svg'>",
|
||||
content: generalSettings
|
||||
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: "<img src='./assets/layers.svg'>",
|
||||
content: new AllLayersPanel(es, currentSetting, languages)
|
||||
content: new AllLayersPanel(es, languages)
|
||||
},
|
||||
{
|
||||
header: "<img src='./assets/floppy.svg'>",
|
||||
|
@ -77,44 +60,6 @@ new TabbedComponent([
|
|||
header: "<img src='./assets/share.svg'>",
|
||||
content: new SharePanel(es, liveUrl)
|
||||
}
|
||||
]).AttachTo("left");
|
||||
|
||||
|
||||
const returnButton = new SubtleButton("./assets/close.svg",
|
||||
new VariableUiElement(
|
||||
currentSetting.map(currentSetting => {
|
||||
if (currentSetting === undefined) {
|
||||
return "";
|
||||
}
|
||||
return "Return to general help";
|
||||
}
|
||||
)
|
||||
))
|
||||
.ListenTo(currentSetting)
|
||||
.onClick(() => currentSetting.setData(undefined));
|
||||
|
||||
|
||||
const 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";
|
||||
}
|
||||
|
||||
return new Combine(["<h1>", setting._name, "</h1>", setting._description.Render()]).Render();
|
||||
}))
|
||||
|
||||
|
||||
new Combine([helpText,
|
||||
returnButton,
|
||||
]).AttachTo("right");
|
||||
|
||||
// The preview
|
||||
new Combine([
|
||||
new VariableUiElement(iframe)
|
||||
]).AttachTo("bottomright");
|
||||
|
||||
|
||||
|
||||
|
||||
], 1).SetClass("main-tabs")
|
||||
.AttachTo("maindiv");
|
||||
|
||||
|
|
24
index.css
24
index.css
|
@ -109,16 +109,21 @@
|
|||
padding-bottom: 0.15em;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
pointer-events: all;
|
||||
}
|
||||
.clickable {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.page-split {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.activate-osm-authentication {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.activate-osm-authentication {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/**************** USER BADGE ****************/
|
||||
|
@ -1224,11 +1229,10 @@
|
|||
|
||||
|
||||
.tab-content {
|
||||
padding: 1em;
|
||||
z-index: 5002;
|
||||
background-color: white;
|
||||
position: relative;
|
||||
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.tab-single-header {
|
||||
|
|
16
test.html
16
test.html
|
@ -1,8 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Small tests</title>
|
||||
<link href="index.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.tag-input-row {
|
||||
display: block ruby;
|
||||
box-sizing: border-box;
|
||||
margin-right: 2em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bordered {
|
||||
border: 1px solid black;
|
||||
display: block;
|
||||
padding: 0.5em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="maindiv">'maindiv' not attached</div>
|
||||
|
|
22
test.ts
22
test.ts
|
@ -1,9 +1,19 @@
|
|||
import TagInput from "./UI/Input/TagInput";
|
||||
import TagRenderingPanel from "./UI/CustomGenerator/TagRenderingPanel";
|
||||
import {UIEventSource} from "./Logic/UIEventSource";
|
||||
import {TextField} from "./UI/Input/TextField";
|
||||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||
import {MultiTagInput} from "./UI/Input/MultiTagInput";
|
||||
import SettingsTable from "./UI/CustomGenerator/SettingsTable";
|
||||
import SingleSetting from "./UI/CustomGenerator/SingleSetting";
|
||||
import {MultiInput} from "./UI/Input/MultiInput";
|
||||
|
||||
const input = new MultiTagInput(new UIEventSource<string[]>(["key~value|0"]));
|
||||
input.GetValue().addCallback(console.log);
|
||||
input.AttachTo("maindiv");
|
||||
new VariableUiElement(input.GetValue().map(tags => tags.join(" & "))).AttachTo("extradiv")
|
||||
|
||||
const config = new UIEventSource({})
|
||||
const languages = new UIEventSource(["en","nl"]);
|
||||
new MultiInput(
|
||||
() => "Add a tag rendering",
|
||||
() => new TagRenderingPanel(
|
||||
|
||||
)
|
||||
|
||||
|
||||
)
|
Loading…
Reference in a new issue