forked from MapComplete/MapComplete
		
	Way to much fixes and improvements
This commit is contained in:
		
							parent
							
								
									e68d9d99a5
								
							
						
					
					
						commit
						5ed0bb431c
					
				
					 41 changed files with 1244 additions and 402 deletions
				
			
		
							
								
								
									
										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 {Layout} from "../Layout"; | ||||||
| import {LayoutConfigJson} from "./LayoutConfigJson"; | import {LayoutConfigJson} from "./LayoutConfigJson"; | ||||||
| import {AndOrTagConfigJson} from "./TagConfigJson"; | 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 {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||||
| import {TagRenderingOptions} from "../TagRenderingOptions"; | import {TagRenderingOptions} from "../TagRenderingOptions"; | ||||||
| import Translation from "../../UI/i18n/Translation"; | import Translation from "../../UI/i18n/Translation"; | ||||||
|  | @ -81,9 +81,14 @@ export class FromJSON { | ||||||
|             return json; |             return json; | ||||||
|         } |         } | ||||||
|         const tr = {}; |         const tr = {}; | ||||||
|  |         let keyCount = 0; | ||||||
|         for (let key in json) { |         for (let key in json) { | ||||||
|  |             keyCount ++; | ||||||
|             tr[key] = json[key]; // I'm doing this wrong, I know
 |             tr[key] = json[key]; // I'm doing this wrong, I know
 | ||||||
|         } |         } | ||||||
|  |         if(keyCount == 0){ | ||||||
|  |             return undefined; | ||||||
|  |         } | ||||||
|         return new Translation(tr); |         return new Translation(tr); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -133,10 +138,10 @@ export class FromJSON { | ||||||
|         let template = FromJSON.Translation(json.render); |         let template = FromJSON.Translation(json.render); | ||||||
| 
 | 
 | ||||||
|         let freeform = undefined; |         let freeform = undefined; | ||||||
|         if (json.freeform) { |         if (json.freeform?.key) { | ||||||
| 
 |             // Setup the freeform
 | ||||||
|             if(json.render === undefined){ |             if (template === undefined) { | ||||||
|                 console.error("Freeform is defined, but render is not. This is not allowed.", json) |                 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." |                 throw "Freeform is defined, but render is not. This is not allowed." | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -146,13 +151,14 @@ export class FromJSON { | ||||||
|                 key: json.freeform.key |                 key: json.freeform.key | ||||||
|             }; |             }; | ||||||
|             if (json.freeform.addExtraTags) { |             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) { |         } else if (json.render) { | ||||||
|  |             // Template (aka rendering) is defined, but freeform.key is not. We allow an input as string
 | ||||||
|             freeform = { |             freeform = { | ||||||
|                 template: `$string$`, |                 template: undefined, // Template to ask is undefined -> we block asking for this key
 | ||||||
|                 renderTemplate: template, |                 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({ |         let rendering = new TagRenderingOptions({ | ||||||
|             question: FromJSON.Translation(json.question), |             question: FromJSON.Translation(json.question), | ||||||
|  | @ -185,6 +195,9 @@ export class FromJSON { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static Tag(json: AndOrTagConfigJson | string): TagsFilter { |     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") { |         if (typeof (json) == "string") { | ||||||
|             const tag = json as string; |             const tag = json as string; | ||||||
|             if (tag.indexOf("!~") >= 0) { |             if (tag.indexOf("!~") >= 0) { | ||||||
|  | @ -227,7 +240,7 @@ export class FromJSON { | ||||||
|             return new And(json.and.map(FromJSON.Tag)); |             return new And(json.and.map(FromJSON.Tag)); | ||||||
|         } |         } | ||||||
|         if (json.or !== undefined) { |         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) { |         function style(tags) { | ||||||
|             const iconSizeStr = iconSize.GetContent(tags).txt.split(","); |             const iconSizeStr = | ||||||
|  |                 iconSize.GetContent(tags).txt.split(","); | ||||||
|             const iconwidth = Number(iconSizeStr[0]); |             const iconwidth = Number(iconSizeStr[0]); | ||||||
|             const iconheight = Number(iconSizeStr[1]); |             const iconheight = Number(iconSizeStr[1]); | ||||||
|             const iconmode = iconSizeStr[2]; |             const iconmode = iconSizeStr[2]; | ||||||
|  |  | ||||||
|  | @ -66,7 +66,7 @@ export interface LayerConfigJson { | ||||||
|      * Wayhandling: should a way/area be displayed as: |      * Wayhandling: should a way/area be displayed as: | ||||||
|      * 0) The way itself |      * 0) The way itself | ||||||
|      * 1) The centerpoint and the way |      * 1) The centerpoint and the way | ||||||
|      * 2) Only the centerpoint? |      * 2) Only the centerpoint | ||||||
|      */ |      */ | ||||||
|     wayHandling?: number; |     wayHandling?: number; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,5 @@ | ||||||
| 
 | 
 | ||||||
| export interface AndOrTagConfigJson { | export interface AndOrTagConfigJson { | ||||||
|    |  | ||||||
|     and?: (string | AndOrTagConfigJson)[] |     and?: (string | AndOrTagConfigJson)[] | ||||||
|     or?: (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. |          * If a value is added with the textfield, these extra tag is addded. | ||||||
|          * Usefull to add a 'fixme=freeform textfield used - to be checked' |          * 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
 |         // 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 save = () => { | ||||||
|             const selection = self._questionElement.GetValue().data; |             const selection = self._questionElement.GetValue().data; | ||||||
|             console.log("Tagrendering: saving tags ", selection); |             console.log("Tagrendering: saving tags ", selection); | ||||||
|             if (selection) { |             if (selection) { | ||||||
|                 State.state.changes.addTag(tags.data.id, selection); |                 State.state?.changes?.addTag(tags.data.id, selection); | ||||||
|             } |             } | ||||||
|             self._editMode.setData(false); |             self._editMode.setData(false); | ||||||
|         } |         } | ||||||
|  | @ -143,7 +144,7 @@ TagRendering extends UIElement implements TagDependantUIElement { | ||||||
|                     if (tags === undefined) { |                     if (tags === undefined) { | ||||||
|                         return Translations.t.general.noTagsSelected.SetClass("subtle").Render(); |                         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) { |                     if (csCount < State.userJourney.tagsVisibleAt) { | ||||||
|                         return ""; |                         return ""; | ||||||
|                     } |                     } | ||||||
|  | @ -154,7 +155,7 @@ TagRendering extends UIElement implements TagDependantUIElement { | ||||||
|                     return tags.asHumanString(true, true); |                     return tags.asHumanString(true, true); | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         ); |         ).ListenTo(self._questionElement); | ||||||
| 
 | 
 | ||||||
|         const cancel = () => { |         const cancel = () => { | ||||||
|             self._questionSkipped.setData(true); |             self._questionSkipped.setData(true); | ||||||
|  | @ -246,7 +247,7 @@ TagRendering extends UIElement implements TagDependantUIElement { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private InputForFreeForm(freeform): InputElement<TagsFilter> { |     private InputForFreeForm(freeform): InputElement<TagsFilter> { | ||||||
|         if (freeform === undefined) { |         if (freeform?.template === undefined) { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -269,8 +270,14 @@ TagRendering extends UIElement implements TagDependantUIElement { | ||||||
|                 if (!isValid(string, this._source.data._country)) { |                 if (!isValid(string, this._source.data._country)) { | ||||||
|                     return undefined; |                     return undefined; | ||||||
|                 } |                 } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|                 const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); |                 const tag = new Tag(freeform.key, formatter(string, this._source.data._country)); | ||||||
| 
 | 
 | ||||||
|  |                 if (tag.value.length > 255) { | ||||||
|  |                     return undefined; // Toolong
 | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|                 if (freeform.extraTags === undefined) { |                 if (freeform.extraTags === undefined) { | ||||||
|                     return tag; |                     return tag; | ||||||
|                 } |                 } | ||||||
|  | @ -340,7 +347,8 @@ TagRendering extends UIElement implements TagDependantUIElement { | ||||||
|         if (this.IsKnown()) { |         if (this.IsKnown()) { | ||||||
|             return false; |             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
 |             // We don't ask this question in the first place
 | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  | @ -390,15 +398,20 @@ TagRendering extends UIElement implements TagDependantUIElement { | ||||||
| 
 | 
 | ||||||
|     InnerRender(): string { |     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 = |             const question = | ||||||
|                 this.ApplyTemplate(this._question).SetClass('question-text'); |                 this.ApplyTemplate(this._question).SetClass('question-text'); | ||||||
|             return "<div class='question'>" + |             return "<div class='question'>" + | ||||||
|                 new Combine([ |                 new Combine([ | ||||||
|                     question, |                     question.Render(), | ||||||
|                     "<br/>", |                     "<br/>", | ||||||
|                     this._questionElement.Render(), |                     this._questionElement.Render(), | ||||||
|                     "<span class='login-button-friendly'>" + this._friendlyLogin.Render() + "</span>", |                     "<span class='login-button-friendly'>", | ||||||
|  |                     this._friendlyLogin, | ||||||
|  |                     "</span>", | ||||||
|                 ]).Render() + "</div>"; |                 ]).Render() + "</div>"; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -428,7 +441,8 @@ TagRendering extends UIElement implements TagDependantUIElement { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|             let editButton = ""; |             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(); |                 editButton = this._editButton.Render(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -438,6 +452,8 @@ TagRendering extends UIElement implements TagDependantUIElement { | ||||||
|                 "</span>"; |                 "</span>"; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         console.log("No rendering for",this) | ||||||
|  |          | ||||||
|         return ""; |         return ""; | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -184,16 +184,18 @@ export class FilteredLayer { | ||||||
|             idsFromOverpass.add(feature.properties.id); |             idsFromOverpass.add(feature.properties.id); | ||||||
|             fusedFeatures.push(feature); |             fusedFeatures.push(feature); | ||||||
|         } |         } | ||||||
|  |         this._dataFromOverpass = fusedFeatures; | ||||||
| 
 | 
 | ||||||
|  |         console.log("New elements are ", this._newElements) | ||||||
|         for (const feature of 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
 |                 // This element is not yet uploaded or not yet visible in overpass
 | ||||||
|                 // We include it in the layer
 |                 // We include it in the layer
 | ||||||
|                 fusedFeatures.push(feature); |                 fusedFeatures.push(feature); | ||||||
|  |                 console.log("Adding ", feature," to fusedFeatures") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         this._dataFromOverpass = fusedFeatures; |  | ||||||
| 
 | 
 | ||||||
|         // We use a new, fused dataset
 |         // We use a new, fused dataset
 | ||||||
|         data = { |         data = { | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import {FilteredLayer} from "./FilteredLayer"; | ||||||
| import {Bounds} from "./Bounds"; | import {Bounds} from "./Bounds"; | ||||||
| import {Overpass} from "./Osm/Overpass"; | import {Overpass} from "./Osm/Overpass"; | ||||||
| import {State} from "../State"; | import {State} from "../State"; | ||||||
|  | import {LayerDefinition} from "../Customizations/LayerDefinition"; | ||||||
| 
 | 
 | ||||||
| export class LayerUpdater { | export class LayerUpdater { | ||||||
| 
 | 
 | ||||||
|  | @ -27,7 +28,7 @@ export class LayerUpdater { | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         this.sufficentlyZoomed = State.state.locationControl.map(location => { |         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; |                 return location.zoom >= minzoom; | ||||||
|             }, [state.layoutToUse] |             }, [state.layoutToUse] | ||||||
|         ); |         ); | ||||||
|  | @ -49,6 +50,9 @@ export class LayerUpdater { | ||||||
|         const filters: TagsFilter[] = []; |         const filters: TagsFilter[] = []; | ||||||
|         state = state ?? State.state; |         state = state ?? State.state; | ||||||
|         for (const layer of state.layoutToUse.data.layers) { |         for (const layer of state.layoutToUse.data.layers) { | ||||||
|  |             if(typeof(layer) === "string"){ | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|             if (state.locationControl.data.zoom < layer.minzoom) { |             if (state.locationControl.data.zoom < layer.minzoom) { | ||||||
|                 console.log("Not loading layer ", layer.id, " as it needs at least ", layer.minzoom, "zoom") |                 console.log("Not loading layer ", layer.id, " as it needs at least ", layer.minzoom, "zoom") | ||||||
|                 continue; |                 continue; | ||||||
|  |  | ||||||
|  | @ -36,9 +36,8 @@ export class UIEventSource<T>{ | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         for (const possibleSource of possibleSources) { |         for (const possibleSource of possibleSources) { | ||||||
|             possibleSource.addCallback(() => { |             possibleSource?.addCallback(() => { | ||||||
|                 sink.setData(source.data?.data); |                 sink.setData(source.data?.data); | ||||||
|                  |  | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
|          |          | ||||||
|  | @ -87,4 +86,24 @@ export class UIEventSource<T>{ | ||||||
|         return this; |         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"; | import Translations from "../i18n/Translations"; | ||||||
| 
 | 
 | ||||||
| export default class Combine extends UIElement { | export default class Combine extends UIElement { | ||||||
|     private uiElements: (string | UIElement)[]; |     private readonly uiElements: (string | UIElement)[]; | ||||||
|     private className: string = undefined; |     private readonly className: string = undefined; | ||||||
|     private clas: string = undefined; |  | ||||||
| 
 | 
 | ||||||
|     constructor(uiElements: (string | UIElement)[], className: string = undefined) { |     constructor(uiElements: (string | UIElement)[], className: string = undefined) { | ||||||
|         super(undefined); |         super(undefined); | ||||||
|  |         this.dumbMode = false; | ||||||
|         this.className = className; |         this.className = className; | ||||||
|         this.uiElements = uiElements; |         this.uiElements = uiElements; | ||||||
|  |         if (className) { | ||||||
|  |             console.error("Deprecated used of className") | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     InnerRender(): string { |     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{ | export class SubtleButton extends UIElement{ | ||||||
|     private imageUrl: string; |     private readonly imageUrl: string; | ||||||
|     private message: UIElement; |     private readonly message: UIElement; | ||||||
|     private linkTo: { url: string, newTab?: boolean } = undefined; |     private readonly linkTo: { url: string, newTab?: boolean } = undefined; | ||||||
| 
 | 
 | ||||||
|     constructor(imageUrl: string, message: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined) { |     constructor(imageUrl: string, message: string | UIElement, linkTo: { url: string, newTab?: boolean } = undefined) { | ||||||
|         super(undefined); |         super(undefined); | ||||||
|  | @ -18,7 +18,7 @@ export class SubtleButton extends UIElement{ | ||||||
| 
 | 
 | ||||||
|     InnerRender(): string { |     InnerRender(): string { | ||||||
|          |          | ||||||
|         if(this.message.IsEmpty()){ |         if(this.message !== null && this.message.IsEmpty()){ | ||||||
|             return ""; |             return ""; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -26,7 +26,7 @@ export class SubtleButton extends UIElement{ | ||||||
|             return new Combine([ |             return new Combine([ | ||||||
|                 `<a class="subtle-button" href="${this.linkTo.url}" ${this.linkTo.newTab ? 'target="_blank"' : ""}>`, |                 `<a class="subtle-button" href="${this.linkTo.url}" ${this.linkTo.newTab ? 'target="_blank"' : ""}>`, | ||||||
|                 this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "", |                 this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "", | ||||||
|                 this.message, |                 this.message ?? "", | ||||||
|                 '</a>' |                 '</a>' | ||||||
|             ]).Render(); |             ]).Render(); | ||||||
|         } |         } | ||||||
|  | @ -34,7 +34,7 @@ export class SubtleButton extends UIElement{ | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             '<span class="subtle-button">', |             '<span class="subtle-button">', | ||||||
|             this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "", |             this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "", | ||||||
|             this.message, |             this.message ?? "", | ||||||
|             '</span>' |             '</span>' | ||||||
|         ]).Render(); |         ]).Render(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -7,8 +7,8 @@ export class TabbedComponent extends UIElement { | ||||||
|     private headers: UIElement[] = []; |     private headers: UIElement[] = []; | ||||||
|     private content: UIElement[] = []; |     private content: UIElement[] = []; | ||||||
| 
 | 
 | ||||||
|     constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab : UIEventSource<number> = new UIEventSource<number>(0)) { |     constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) { | ||||||
|         super(openedTab); |         super(typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0))); | ||||||
|         const self = this; |         const self = this; | ||||||
|         for (let i = 0; i < elements.length; i++) { |         for (let i = 0; i < elements.length; i++) { | ||||||
|             let element = elements[i]; |             let element = elements[i]; | ||||||
|  |  | ||||||
|  | @ -3,34 +3,30 @@ import {TabbedComponent} from "../Base/TabbedComponent"; | ||||||
| import {SubtleButton} from "../Base/SubtleButton"; | import {SubtleButton} from "../Base/SubtleButton"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; | import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson"; | ||||||
| import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson"; |  | ||||||
| import LayerPanel from "./LayerPanel"; | import LayerPanel from "./LayerPanel"; | ||||||
| import SingleSetting from "./SingleSetting"; | 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 { | export default class AllLayersPanel extends UIElement { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private panel: UIElement; |     private panel: UIElement; | ||||||
|     private _config: UIEventSource<LayoutConfigJson>; |     private readonly _config: UIEventSource<LayoutConfigJson>; | ||||||
|     private _currentlySelected: UIEventSource<SingleSetting<any>>; |     private readonly languages: UIEventSource<string[]>; | ||||||
|     private languages: UIEventSource<string[]>; |  | ||||||
| 
 | 
 | ||||||
|     private static createEmptyLayer(): LayerConfigJson { |     constructor(config: UIEventSource<LayoutConfigJson>, | ||||||
|         return { |  | ||||||
|             id: undefined, |  | ||||||
|             name: undefined, |  | ||||||
|             minzoom: 0, |  | ||||||
|             overpassTags: undefined, |  | ||||||
|             title: undefined, |  | ||||||
|             description: {} |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     constructor(config: UIEventSource<LayoutConfigJson>, currentlySelected: UIEventSource<SingleSetting<any>>, |  | ||||||
|                 languages: UIEventSource<any>) { |                 languages: UIEventSource<any>) { | ||||||
|         super(undefined); |         super(undefined); | ||||||
|         this._config = config; |         this._config = config; | ||||||
|         this._currentlySelected = currentlySelected; |  | ||||||
|         this.languages = languages; |         this.languages = languages; | ||||||
| 
 | 
 | ||||||
|         this.createPanels(); |         this.createPanels(); | ||||||
|  | @ -46,20 +42,82 @@ export default class AllLayersPanel extends UIElement { | ||||||
| 
 | 
 | ||||||
|         const layers = this._config.data.layers; |         const layers = this._config.data.layers; | ||||||
|         for (let i = 0; i < layers.length; i++) { |         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({ |             tabs.push({ | ||||||
|                 header: "<img src='./assets/bug.svg'>", |                 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({ |         tabs.push({ | ||||||
|             header: "<img src='./assets/add.svg'>", |             header: "<img src='./assets/add.svg'>", | ||||||
|             content: new SubtleButton( |             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", |                     "./assets/add.svg", | ||||||
|                     "Add a new layer" |                     "Add a new layer" | ||||||
|                 ).onClick(() => { |                 ).onClick(() => { | ||||||
|                 self._config.data.layers.push(AllLayersPanel.createEmptyLayer()) |                     self._config.data.layers.push(GenerateEmpty.createEmptyLayer()) | ||||||
|                     self._config.ping(); |                     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))); | ||||||
|  |  | ||||||
							
								
								
									
										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 {InputElement} from "../Input/InputElement"; | ||||||
| import MultiLingualTextFields from "../Input/MultiLingualTextFields"; | import MultiLingualTextFields from "../Input/MultiLingualTextFields"; | ||||||
| import {CheckBox} from "../Input/CheckBox"; | 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 |  * Shows the configuration for a single layer | ||||||
|  */ |  */ | ||||||
| export default class LayerPanel extends UIElement { | 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>, |     constructor(config: UIEventSource<LayoutConfigJson>, | ||||||
|                 languages: UIEventSource<string[]>, |                 languages: UIEventSource<string[]>, | ||||||
|                 index: number, |                 index: number, | ||||||
|                 currentlySelected: UIEventSource<SingleSetting<any>>) { |                 currentlySelected: UIEventSource<SingleSetting<any>>) { | ||||||
|         super(undefined); |         super(); | ||||||
|         this._config = config; |         this._config = config; | ||||||
|  |         this.renderingOptions = this.setupRenderOptions(config, languages, index, currentlySelected); | ||||||
| 
 | 
 | ||||||
|         const actualDeleteButton = new SubtleButton( |         const actualDeleteButton = new SubtleButton( | ||||||
|             "./assets/delete.svg", |             "./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(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), "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 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", |                 setting(TextField.NumberInput("nat", n => n < 23), "minzoom", "Minimal zoom", | ||||||
|                     new Combine(["The tags to load from overpass. ", MultiTagInput.tagExplanation])) |                     "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 { |     InnerRender(): string { | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|  |             "<h2>General layer settings</h2>", | ||||||
|             this.settingsTable, |             this.settingsTable, | ||||||
|  |             "<h2>Map rendering options</h2>", | ||||||
|  |             this.renderingOptions, | ||||||
|  |             "<h2>Tag rendering and questions</h2>", | ||||||
|  |             this.tagRenderings, | ||||||
|  |             "<h2>Layer delete</h2>", | ||||||
|             this.deleteButton |             this.deleteButton | ||||||
|         ]).Render(); |         ]).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 SingleSetting from "./SingleSetting"; | ||||||
| import {UIElement} from "../UIElement"; | import {UIElement} from "../UIElement"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| import {InputElement} from "../Input/InputElement"; |  | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import PageSplit from "../Base/PageSplit"; | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; |  | ||||||
| 
 | 
 | ||||||
| export default class SettingsTable extends UIElement { | export default class SettingsTable extends UIElement { | ||||||
| 
 | 
 | ||||||
|     private _col1: UIElement[] = []; |     private _col1: UIElement[] = []; | ||||||
|     private _col2: InputElement<any>[] = []; |     private _col2: UIElement[] = []; | ||||||
| 
 | 
 | ||||||
|     public selectedSetting: UIEventSource<SingleSetting<any>>; |     public selectedSetting: UIEventSource<SingleSetting<any>>; | ||||||
| 
 | 
 | ||||||
|     constructor(elements: SingleSetting<any>[], |     constructor(elements: (SingleSetting<any> | string)[], | ||||||
|                 currentSelectedSetting: UIEventSource<SingleSetting<any>>) { |                 currentSelectedSetting: UIEventSource<SingleSetting<any>>) { | ||||||
|         super(undefined); |         super(undefined); | ||||||
|         const self = this; |         const self = this; | ||||||
|         this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined); |         this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined); | ||||||
|         for (const element of elements) { |         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._col1.push(title); | ||||||
|             this._col2.push(element._value); |             this._col2.push(element._value); | ||||||
|  |             element._value.SetStyle("display:block"); | ||||||
|             element._value.IsSelected.addCallback(isSelected => { |             element._value.IsSelected.addCallback(isSelected => { | ||||||
|                 if (isSelected) { |                 if (isSelected) { | ||||||
|                     self.selectedSetting.setData(element); |                     self.selectedSetting.setData(element); | ||||||
|  | @ -34,13 +40,19 @@ export default class SettingsTable extends UIElement { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     InnerRender(): string { |     InnerRender(): string { | ||||||
|         let html = ""; |         let elements = []; | ||||||
| 
 | 
 | ||||||
|         for (let i = 0; i < this._col1.length; i++) { |         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 {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import {InputElement} from "../Input/InputElement"; | import {InputElement} from "../Input/InputElement"; | ||||||
| import {UIElement} from "../UIElement"; | import {UIElement} from "../UIElement"; | ||||||
|  | @ -12,7 +11,7 @@ export default class SingleSetting<T> { | ||||||
|     public _description: UIElement; |     public _description: UIElement; | ||||||
|     public _options: { showIconPreview?: boolean }; |     public _options: { showIconPreview?: boolean }; | ||||||
| 
 | 
 | ||||||
|     constructor(config: UIEventSource<LayoutConfigJson>, |     constructor(config: UIEventSource<any>, | ||||||
|                 value: InputElement<T>, |                 value: InputElement<T>, | ||||||
|                 path: string | (string | number)[], |                 path: string | (string | number)[], | ||||||
|                 name: string, |                 name: string, | ||||||
|  | @ -47,11 +46,17 @@ export default class SingleSetting<T> { | ||||||
|             // We have to rewalk every time as parts might be new
 |             // We have to rewalk every time as parts might be new
 | ||||||
|             let configPart = config.data; |             let configPart = config.data; | ||||||
|             for (const pathPart of path) { |             for (const pathPart of path) { | ||||||
|                 configPart = configPart[pathPart]; |                 let newConfigPart = configPart[pathPart]; | ||||||
|                 if (configPart === undefined) { |                 if (newConfigPart === undefined) { | ||||||
|                     console.warn("Lost the way for path ", path) |                     console.warn("Lost the way for path ", path, " - creating entry") | ||||||
|                     return; |                     if (typeof (pathPart) === "string") { | ||||||
|  |                         configPart[pathPart] = {}; | ||||||
|  |                     } else { | ||||||
|  |                         configPart[pathPart] = []; | ||||||
|                     } |                     } | ||||||
|  |                     newConfigPart = configPart[pathPart]; | ||||||
|  |                 } | ||||||
|  |                 configPart = newConfigPart; | ||||||
|             } |             } | ||||||
|             configPart[lastPart] = value; |             configPart[lastPart] = value; | ||||||
|             config.ping(); |             config.ping(); | ||||||
|  | @ -66,7 +71,6 @@ export default class SingleSetting<T> { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             const loadedValue = configPart[lastPart]; |             const loadedValue = configPart[lastPart]; | ||||||
| 
 |  | ||||||
|             if (loadedValue !== undefined) { |             if (loadedValue !== undefined) { | ||||||
|                 value.GetValue().setData(loadedValue); |                 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 {UIElement} from "../UIElement"; | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| import {SubtleButton} from "../Base/SubtleButton"; | import {SubtleButton} from "../Base/SubtleButton"; | ||||||
| import TagInput from "./TagInput"; | import {CheckBox} from "./CheckBox"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | 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>; |     IsSelected: UIEventSource<boolean>; | ||||||
|     private elements: UIElement[] = []; |  | ||||||
|     private inputELements: (InputElement<string> | InputElement<AndOrTagInput>)[] = []; |  | ||||||
|     private addTag: UIElement; |  | ||||||
| 
 | 
 | ||||||
|     constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) { |     constructor() { | ||||||
|         super(undefined); |         super(); | ||||||
|         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; |         const self = this; | ||||||
|         value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements()); |         this._isAndButton = new CheckBox( | ||||||
|         this.createElements(); |             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._addBlock = | ||||||
|         this.IsSelected = new UIEventSource<boolean>(false); |             new SubtleButton("./assets/addSmall.svg", "Add an and/or-expression") | ||||||
|     } |                 .SetClass("small-button") | ||||||
|  |                 .onClick(() => {self.createNewBlock()}); | ||||||
| 
 | 
 | ||||||
|     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._isAnd.addCallback(() => self.UpdateValue()); | ||||||
|         this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b)) |         this._rawTags.GetValue().addCallback(() => { | ||||||
|     } |             self.UpdateValue() | ||||||
| 
 |  | ||||||
|     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.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(); |         this.Update(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     InnerRender(): string { |     private createDeleteButton(elementId: string): UIElement { | ||||||
|         return new Combine([...this.elements, this.addTag]).SetClass("bordered").Render(); |         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) { | ||||||
| 
 | 
 | ||||||
|     IsValid(t: string[]): boolean { |             if (typeof (tag) === "string") { | ||||||
|         return false; |                 rawTags.push(tag); | ||||||
|  |             } else { | ||||||
|  |                 subTags.push(tag); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     GetValue(): UIEventSource<string[]> { |         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; |         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 {SubtleButton} from "../Base/SubtleButton"; | ||||||
| import TagInput from "./TagInput"; | import TagInput from "./TagInput"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | 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[]>([])) { |     constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) { | ||||||
|         super(undefined); |         super("Add a new tag", | ||||||
|         this._value = value; |             () => "", | ||||||
| 
 |             () => new TagInput(), | ||||||
|         this.addTag = new SubtleButton("./assets/addSmall.svg", "Add a tag") |             value | ||||||
|             .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; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -2,6 +2,7 @@ import {InputElement} from "./InputElement"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| 
 | 
 | ||||||
| export class RadioButton<T> extends InputElement<T> { | export class RadioButton<T> extends InputElement<T> { | ||||||
|  |     IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||||
| 
 | 
 | ||||||
|     private readonly _selectedElementIndex: UIEventSource<number> |     private readonly _selectedElementIndex: UIEventSource<number> | ||||||
|         = new UIEventSource<number>(null); |         = new UIEventSource<number>(null); | ||||||
|  | @ -26,16 +27,16 @@ export class RadioButton<T> extends InputElement<T> { | ||||||
|                         return elements[selectedIndex].GetValue() |                         return elements[selectedIndex].GetValue() | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             ), elements.map(e => e.GetValue())); |             ), elements.map(e => e?.GetValue())); | ||||||
| 
 | 
 | ||||||
|         this.value.addCallback((t) => { |         this.value.addCallback((t) => { | ||||||
|             self.ShowValue(t); |             self?.ShowValue(t); | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         for (let i = 0; i < elements.length; i++) { |         for (let i = 0; i < elements.length; i++) { | ||||||
|             // If an element is clicked, the radio button corresponding with it should be selected as well
 |             // 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); |                 self._selectedElementIndex.setData(i); | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -18,16 +18,7 @@ export default class SingleTagInput extends InputElement<string> { | ||||||
|         super(undefined); |         super(undefined); | ||||||
|         this._value = value ?? new UIEventSource<string>(undefined); |         this._value = value ?? new UIEventSource<string>(undefined); | ||||||
|         |         | ||||||
|         this.key = new TextField({ |         this.key = TextField.KeyInput(); | ||||||
|             placeholder: "key", |  | ||||||
|             fromString: str => { |  | ||||||
|                 if (str?.match(/^[a-zA-Z][a-zA-Z0-9:]*$/)) { |  | ||||||
|                     return str; |  | ||||||
|                 } |  | ||||||
|                 return undefined |  | ||||||
|             }, |  | ||||||
|             toString: str => str |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         this.value = new TextField<string>({ |         this.value = new TextField<string>({ | ||||||
|                 placeholder: "value - if blank, matches if key is NOT present", |                 placeholder: "value - if blank, matches if key is NOT present", | ||||||
|  | @ -95,7 +86,8 @@ export default class SingleTagInput extends InputElement<string> { | ||||||
|     InnerRender(): string { |     InnerRender(): string { | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             this.key, this.operator, this.value |             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 {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import * as EmailValidator from "email-validator"; | import * as EmailValidator from "email-validator"; | ||||||
| import {parsePhoneNumberFromString} from "libphonenumber-js"; | import {parsePhoneNumberFromString} from "libphonenumber-js"; | ||||||
|  | import {DropDown} from "./DropDown"; | ||||||
| 
 | 
 | ||||||
| export class ValidatedTextField { | 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 = { |     public static inputValidation = { | ||||||
|         "$": () => true, |         "$": () => true, | ||||||
|         "string": () => 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>{ |     public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{ | ||||||
|         const isValid = ValidatedTextField.inputValidation[type]; |         const isValid = ValidatedTextField.inputValidation[type]; | ||||||
|         extraValidation = extraValidation ?? (() => true) |         extraValidation = extraValidation ?? (() => true) | ||||||
|  |  | ||||||
|  | @ -162,6 +162,7 @@ export class ShareScreen extends UIElement { | ||||||
|         this._iframeCode = new VariableUiElement( |         this._iframeCode = new VariableUiElement( | ||||||
|             url.map((url) => { |             url.map((url) => { | ||||||
|                 return `<span class='literal-code iframe-code-block'>
 |                 return `<span class='literal-code iframe-code-block'>
 | ||||||
|  |                          <iframe src="${url}" width="100%" height="100%" title="${layout.title.InnerRender()} with MapComplete"></iframe>  | ||||||
|                     </span>` |                     </span>` | ||||||
|             }) |             }) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  | @ -53,7 +53,7 @@ export class SimpleAddUI extends UIElement { | ||||||
| 
 | 
 | ||||||
|                     if (typeof (preset.icon) !== "string") { |                     if (typeof (preset.icon) !== "string") { | ||||||
|                         const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"}); |                         const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"}); | ||||||
|                         icon = preset.icon.GetContent(tags); |                         icon = preset.icon.GetContent(tags).txt; | ||||||
|                     } else { |                     } else { | ||||||
|                         icon = preset.icon; |                         icon = preset.icon; | ||||||
|                     } |                     } | ||||||
|  | @ -193,7 +193,7 @@ export class SimpleAddUI extends UIElement { | ||||||
|             return new Combine([header, Translations.t.general.add.stillLoading]).Render() |             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(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,8 +8,12 @@ export abstract class UIElement extends UIEventSource<string>{ | ||||||
|     public readonly _source: UIEventSource<any>; |     public readonly _source: UIEventSource<any>; | ||||||
|     private clss: string[] = [] |     private clss: string[] = [] | ||||||
| 
 | 
 | ||||||
|  |     private style: string; | ||||||
|  | 
 | ||||||
|     private _hideIfEmpty = false; |     private _hideIfEmpty = false; | ||||||
| 
 | 
 | ||||||
|  |     public dumbMode = false; | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * In the 'deploy'-step, some code needs to be run by ts-node. |      * 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. |      * 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) { |         if (source === undefined) { | ||||||
|             return this; |             return this; | ||||||
|         } |         } | ||||||
|  |         this.dumbMode = false; | ||||||
|         const self = this; |         const self = this; | ||||||
|         source.addCallback(() => { |         source.addCallback(() => { | ||||||
|             self.Update(); |             self.Update(); | ||||||
|  | @ -40,12 +45,25 @@ export abstract class UIElement extends UIEventSource<string>{ | ||||||
|     private _onClick: () => void; |     private _onClick: () => void; | ||||||
| 
 | 
 | ||||||
|     public onClick(f: (() => void)) { |     public onClick(f: (() => void)) { | ||||||
|  |         this.dumbMode = false; | ||||||
|         this._onClick = f; |         this._onClick = f; | ||||||
|         this.SetClass("clickable") |         this.SetClass("clickable") | ||||||
|         this.Update(); |         this.Update(); | ||||||
|         return this; |         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 { |     Update(): void { | ||||||
|         if (UIElement.runningFromConsole) { |         if (UIElement.runningFromConsole) { | ||||||
|             return; |             return; | ||||||
|  | @ -54,10 +72,29 @@ export abstract class UIElement extends UIEventSource<string>{ | ||||||
|         let element = document.getElementById(this.id); |         let element = document.getElementById(this.id); | ||||||
|         if (element === undefined || element === null) { |         if (element === undefined || element === null) { | ||||||
|             // The element is not painted
 |             // 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; |             return; | ||||||
|         } |         } | ||||||
|         this.setData(this.InnerRender()); |         this.setData(this.InnerRender()); | ||||||
|         element.innerHTML = this.data; |         element.innerHTML = this.data; | ||||||
|  | 
 | ||||||
|         if (this._hideIfEmpty) { |         if (this._hideIfEmpty) { | ||||||
|             if (element.innerHTML === "") { |             if (element.innerHTML === "") { | ||||||
|                 element.parentElement.style.display = "none"; |                 element.parentElement.style.display = "none"; | ||||||
|  | @ -81,6 +118,12 @@ export abstract class UIElement extends UIEventSource<string>{ | ||||||
|             element.style.cursor = "pointer"; |             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.InnerUpdate(element); | ||||||
| 
 | 
 | ||||||
|         for (const i in this) { |         for (const i in this) { | ||||||
|  | @ -108,10 +151,18 @@ export abstract class UIElement extends UIEventSource<string>{ | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|     Render(): 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) { |     AttachTo(divId: string) { | ||||||
|  |         this.dumbMode = false; | ||||||
|         let element = document.getElementById(divId); |         let element = document.getElementById(divId); | ||||||
|         if (element === null) { |         if (element === null) { | ||||||
|             throw "SEVERE: could not attach UIElement to " + divId; |             throw "SEVERE: could not attach UIElement to " + divId; | ||||||
|  | @ -143,6 +194,7 @@ export abstract class UIElement extends UIEventSource<string>{ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public SetClass(clss: string): UIElement { |     public SetClass(clss: string): UIElement { | ||||||
|  |         this.dumbMode = false; | ||||||
|         if (this.clss.indexOf(clss) < 0) { |         if (this.clss.indexOf(clss) < 0) { | ||||||
|             this.clss.push(clss); |             this.clss.push(clss); | ||||||
|         } |         } | ||||||
|  | @ -150,14 +202,13 @@ export abstract class UIElement extends UIEventSource<string>{ | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public RemoveClass(clss: string): UIElement { | 
 | ||||||
|         if (this.clss.indexOf(clss) >= 0) { |     public SetStyle(style: string): UIElement { | ||||||
|             this.clss = this.clss.splice(this.clss.indexOf(clss), 1); |         this.dumbMode = false; | ||||||
|         } |         this.style = style; | ||||||
|         this.Update(); |         this.Update(); | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -52,8 +52,8 @@ export default class Translation extends UIElement { | ||||||
|         for (const i in this.translations) { |         for (const i in this.translations) { | ||||||
|             return this.translations[i]; // Return a random language
 |             return this.translations[i]; // Return a random language
 | ||||||
|         } |         } | ||||||
|         console.log("Missing language ",Locale.language.data,"for",this.translations) |         console.error("Missing language ",Locale.language.data,"for",this.translations) | ||||||
|         return "Missing translation" |         return undefined; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     InnerRender(): string { |     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", |           "condition": "indoor=yes", | ||||||
|           "freeform": { |           "freeform": { | ||||||
|             "key": "access", |             "key": "access", | ||||||
|             "addExtraTags": "fixme=Freeform field used for access - doublecheck the value" |             "addExtraTags": ["fixme=Freeform field used for access - doublecheck the value"] | ||||||
|           }, |           }, | ||||||
|           "mappings": [ |           "mappings": [ | ||||||
|             { |             { | ||||||
|  |  | ||||||
|  | @ -76,7 +76,7 @@ | ||||||
|           }, |           }, | ||||||
|           "freeform": { |           "freeform": { | ||||||
|             "key": "artwork_type", |             "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": [ |           "mappings": [ | ||||||
|             { |             { | ||||||
|  |  | ||||||
|  | @ -56,7 +56,9 @@ | ||||||
|           "render": "Access is {access}", |           "render": "Access is {access}", | ||||||
|           "freeform": { |           "freeform": { | ||||||
|             "key": "access", |             "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": [ |           "mappings": [ | ||||||
|             { |             { | ||||||
|  |  | ||||||
|  | @ -5,32 +5,6 @@ | ||||||
|     <title>Custom Theme Generator for Mapcomplete</title> |     <title>Custom Theme Generator for Mapcomplete</title> | ||||||
| 
 | 
 | ||||||
|     <style type="text/css"> |     <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 { |         .icon-preview { | ||||||
|             max-width: 2em; |             max-width: 2em; | ||||||
|  | @ -52,27 +26,72 @@ | ||||||
|             display:block; |             display:block; | ||||||
|             padding: 0.5em; |             padding: 0.5em; | ||||||
|             border-radius: 0.5em; |             border-radius: 0.5em; | ||||||
|  |             box-sizing: border-box; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         .tag-input-row { |         .tag-input-row { | ||||||
|             display: block ruby; |             display: block ruby; | ||||||
|             box-sizing: border-box; |             box-sizing: border-box; | ||||||
|             margin-right: 2em; |             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 { |         body { | ||||||
|             height: 100%; |             height: 100%; | ||||||
|         } |         } | ||||||
|  |          | ||||||
|  |         #maindiv { | ||||||
|  |             height: calc(100% - 6em); | ||||||
|  |         } | ||||||
|     </style> |     </style> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
| <div id="left"> | <div id="maindiv"> | ||||||
|     'left' not attached |     'maindiv' not attached | ||||||
| </div> | </div> | ||||||
| <div id="right">'right' not attached</div> |  | ||||||
| <div id="bottomright">'bottomright' not attached</div> |  | ||||||
| <script src="./customGenerator.ts"></script> | <script src="./customGenerator.ts"></script> | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  | @ -1,68 +1,51 @@ | ||||||
| import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; |  | ||||||
| import {UIEventSource} from "./Logic/UIEventSource"; | import {UIEventSource} from "./Logic/UIEventSource"; | ||||||
| import SingleSetting from "./UI/CustomGenerator/SingleSetting"; | import SingleSetting from "./UI/CustomGenerator/SingleSetting"; | ||||||
| import Combine from "./UI/Base/Combine"; | import Combine from "./UI/Base/Combine"; | ||||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||||
| import GeneralSettings from "./UI/CustomGenerator/GeneralSettings"; | import GeneralSettings from "./UI/CustomGenerator/GeneralSettings"; | ||||||
| import {SubtleButton} from "./UI/Base/SubtleButton"; |  | ||||||
| import {TabbedComponent} from "./UI/Base/TabbedComponent"; | import {TabbedComponent} from "./UI/Base/TabbedComponent"; | ||||||
| import AllLayersPanel from "./UI/CustomGenerator/AllLayersPanel"; | import AllLayersPanel from "./UI/CustomGenerator/AllLayersPanel"; | ||||||
| import {ShareScreen} from "./UI/ShareScreen"; |  | ||||||
| import {FromJSON} from "./Customizations/JSON/FromJSON"; |  | ||||||
| import SharePanel from "./UI/CustomGenerator/SharePanel"; | 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 = { | const es = new UIEventSource(GenerateEmpty.createTestLayout()); | ||||||
|     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 encoded = es.map(config => btoa(JSON.stringify(config))); | 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 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>`); | 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 currentSetting = new UIEventSource<SingleSetting<any>>(undefined) | ||||||
| 
 | 
 | ||||||
| const generalSettings = new GeneralSettings(es, currentSetting); | const generalSettings = new GeneralSettings(es, currentSetting); | ||||||
| const languages = generalSettings.languages; | const languages = generalSettings.languages; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | // The preview
 | ||||||
|  | const preview = new Combine([ | ||||||
|  |     new VariableUiElement(iframe.stabilized(2500)) | ||||||
|  | ]).SetClass("preview") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| new TabbedComponent([ | new TabbedComponent([ | ||||||
|     { |     { | ||||||
|         header: "<img src='./assets/gear.svg'>", |         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'>", |         header: "<img src='./assets/layers.svg'>", | ||||||
|         content: new AllLayersPanel(es, currentSetting, languages) |         content: new AllLayersPanel(es, languages) | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|         header: "<img src='./assets/floppy.svg'>", |         header: "<img src='./assets/floppy.svg'>", | ||||||
|  | @ -77,44 +60,6 @@ new TabbedComponent([ | ||||||
|         header: "<img src='./assets/share.svg'>", |         header: "<img src='./assets/share.svg'>", | ||||||
|         content: new SharePanel(es, liveUrl) |         content: new SharePanel(es, liveUrl) | ||||||
|     } |     } | ||||||
| ]).AttachTo("left"); | ], 1).SetClass("main-tabs") | ||||||
| 
 |     .AttachTo("maindiv"); | ||||||
| 
 |  | ||||||
| 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"); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -113,6 +113,11 @@ | ||||||
|         pointer-events: all; |         pointer-events: all; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .page-split { | ||||||
|  |         display: flex; | ||||||
|  |         height: 100%; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     .activate-osm-authentication { |     .activate-osm-authentication { | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|  | @ -1224,11 +1229,10 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| .tab-content { | .tab-content { | ||||||
|     padding: 1em; |  | ||||||
|     z-index: 5002; |     z-index: 5002; | ||||||
|     background-color: white; |     background-color: white; | ||||||
|     position: relative; |     position: relative; | ||||||
| 
 |     padding: 1em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .tab-single-header { | .tab-single-header { | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								test.html
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								test.html
									
										
									
									
									
								
							|  | @ -1,8 +1,24 @@ | ||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html> | <html> | ||||||
|  | 
 | ||||||
| <head> | <head> | ||||||
|     <title>Small tests</title> |     <title>Small tests</title> | ||||||
|     <link href="index.css" rel="stylesheet"/> |     <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> | </head> | ||||||
| <body> | <body> | ||||||
| <div id="maindiv">'maindiv' not attached</div> | <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 {UIEventSource} from "./Logic/UIEventSource"; | ||||||
|  | import {TextField} from "./UI/Input/TextField"; | ||||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | 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); | const config = new UIEventSource({}) | ||||||
| input.AttachTo("maindiv"); | const languages = new UIEventSource(["en","nl"]); | ||||||
| new VariableUiElement(input.GetValue().map(tags => tags.join(" & "))).AttachTo("extradiv") | new MultiInput( | ||||||
|  |     () => "Add a tag rendering", | ||||||
|  |     () => new TagRenderingPanel( | ||||||
|  |          | ||||||
|  |     ) | ||||||
|  |      | ||||||
|  |      | ||||||
|  | ) | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue