forked from MapComplete/MapComplete
		
	Chore: formatting
This commit is contained in:
		
							parent
							
								
									6c3c67af56
								
							
						
					
					
						commit
						286578bfc7
					
				
					 58 changed files with 2199 additions and 1915 deletions
				
			
		|  | @ -24,7 +24,8 @@ | |||
|     unknown: t.loginFailedUnreachableMode, | ||||
|     readonly: t.loginFailedReadonlyMode, | ||||
|   } | ||||
|   const apiState = state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online") | ||||
|   const apiState = | ||||
|     state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online") | ||||
| </script> | ||||
| 
 | ||||
| {#if $badge} | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ | |||
|   <slot name="image" slot="image" /> | ||||
|   <div class="flex w-full items-center justify-between" slot="message"> | ||||
|     <slot /> | ||||
|     <ChevronRightIcon class={clss?.indexOf("small") >= 0?  "h-4 w-4 shrink-0": "h-12 w-12 shrink-0" }/> | ||||
|     <ChevronRightIcon | ||||
|       class={clss?.indexOf("small") >= 0 ? "h-4 w-4 shrink-0" : "h-12 w-12 shrink-0"} | ||||
|     /> | ||||
|   </div> | ||||
| </SubtleButton> | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ | |||
|    * Thin wrapper around 'TabGroup' which binds the state | ||||
|    */ | ||||
| 
 | ||||
|   import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"; | ||||
|   import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import { twJoin } from "tailwind-merge"; | ||||
|   import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui" | ||||
|   import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { twJoin } from "tailwind-merge" | ||||
| 
 | ||||
|   /** | ||||
|    * If a condition is given for a certain tab, it will only be shown if this condition is true. | ||||
|  | @ -43,49 +43,63 @@ | |||
|     <div class="interactive sticky top-0 flex items-center justify-between"> | ||||
|       <TabList class="flex flex-wrap"> | ||||
|         {#if $$slots.title0} | ||||
|           <Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition0 && "hidden")}> | ||||
|           <Tab | ||||
|             class={({ selected }) => twJoin("tab", selected && "primary", !$condition0 && "hidden")} | ||||
|           > | ||||
|             <div bind:this={tabElements[0]} class="flex"> | ||||
|               <slot name="title0">Tab 0</slot> | ||||
|             </div> | ||||
|           </Tab> | ||||
|         {/if} | ||||
|         {#if $$slots.title1} | ||||
|           <Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition1 && "hidden")}> | ||||
|           <Tab | ||||
|             class={({ selected }) => twJoin("tab", selected && "primary", !$condition1 && "hidden")} | ||||
|           > | ||||
|             <div bind:this={tabElements[1]} class="flex"> | ||||
|               <slot name="title1" /> | ||||
|             </div> | ||||
|           </Tab> | ||||
|         {/if} | ||||
|         {#if $$slots.title2} | ||||
|           <Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition2 && "hidden")}> | ||||
|           <Tab | ||||
|             class={({ selected }) => twJoin("tab", selected && "primary", !$condition2 && "hidden")} | ||||
|           > | ||||
|             <div bind:this={tabElements[2]} class="flex"> | ||||
|               <slot name="title2" /> | ||||
|             </div> | ||||
|           </Tab> | ||||
|         {/if} | ||||
|         {#if $$slots.title3} | ||||
|           <Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition3 && "hidden")}> | ||||
|           <Tab | ||||
|             class={({ selected }) => twJoin("tab", selected && "primary", !$condition3 && "hidden")} | ||||
|           > | ||||
|             <div bind:this={tabElements[3]} class="flex"> | ||||
|               <slot name="title3" /> | ||||
|             </div> | ||||
|           </Tab> | ||||
|         {/if} | ||||
|         {#if $$slots.title4} | ||||
|           <Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition4 && "hidden")}> | ||||
|           <Tab | ||||
|             class={({ selected }) => twJoin("tab", selected && "primary", !$condition4 && "hidden")} | ||||
|           > | ||||
|             <div bind:this={tabElements[4]} class="flex"> | ||||
|               <slot name="title4" /> | ||||
|             </div> | ||||
|           </Tab> | ||||
|         {/if} | ||||
|         {#if $$slots.title5} | ||||
|           <Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition5 && "hidden")}> | ||||
|           <Tab | ||||
|             class={({ selected }) => twJoin("tab", selected && "primary", !$condition5 && "hidden")} | ||||
|           > | ||||
|             <div bind:this={tabElements[5]} class="flex"> | ||||
|               <slot name="title5" /> | ||||
|             </div> | ||||
|           </Tab> | ||||
|         {/if} | ||||
|         {#if $$slots.title6} | ||||
|           <Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition6 && "hidden")}> | ||||
|           <Tab | ||||
|             class={({ selected }) => twJoin("tab", selected && "primary", !$condition6 && "hidden")} | ||||
|           > | ||||
|             <div bind:this={tabElements[6]} class="flex"> | ||||
|               <slot name="title6" /> | ||||
|             </div> | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ | |||
|     <Tr t={Translations.t.general.returnToTheMap} /> | ||||
|   </button> | ||||
| {:else} | ||||
|   <div class="flex flex-col gap-y-2 overflow-y-auto p-1 px-2 h-full"> | ||||
|   <div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-2"> | ||||
|     {#each layer.tagRenderings as config (config.id)} | ||||
|       {#if (config.condition?.matchesProperties($tags) ?? true) && config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true)} | ||||
|         {#if config.IsKnown($tags)} | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ | |||
|       mapExtent: state.mapProperties.bounds.data, | ||||
|       width: maindiv.offsetWidth, | ||||
|       height: maindiv.offsetHeight, | ||||
|       noSelfIntersectingLines: true | ||||
|       noSelfIntersectingLines: true, | ||||
|     }) | ||||
|   } | ||||
| </script> | ||||
|  | @ -84,7 +84,7 @@ | |||
|     helperText={t.downloadAsSvgLinesOnlyHelper} | ||||
|     construct={() => offerSvg(true)} | ||||
|   /> | ||||
|    | ||||
| 
 | ||||
|   <DownloadButton | ||||
|     {state} | ||||
|     {metaIsIncluded} | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
|    */ | ||||
| 
 | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import { ImmutableStore, Store } from "../../Logic/UIEventSource"; | ||||
|   import { ImmutableStore, Store } from "../../Logic/UIEventSource" | ||||
|   import type { OsmTags } from "../../Models/OsmFeature" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| <script lang="ts"> | ||||
|     import {UIEventSource} from "../../../Logic/UIEventSource"; | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||
| 
 | ||||
|     /** | ||||
|      * Simply shows the image | ||||
|      */ | ||||
|     export let value: UIEventSource<undefined | string> | ||||
|   /** | ||||
|    * Simply shows the image | ||||
|    */ | ||||
|   export let value: UIEventSource<undefined | string> | ||||
| </script> | ||||
| 
 | ||||
| <img src={$value}/> | ||||
| <img src={$value} /> | ||||
|  |  | |||
|  | @ -1,31 +1,39 @@ | |||
| <script lang="ts">/** | ||||
|  * Input helper to create a tag. The tag is JSON-encoded | ||||
|  */ | ||||
| import { UIEventSource } from "../../../Logic/UIEventSource"; | ||||
| import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte"; | ||||
| import { TagUtils } from "../../../Logic/Tags/TagUtils"; | ||||
| import nmd from "nano-markdown" | ||||
| import FromHtml from "../../Base/FromHtml.svelte"; | ||||
| export let value: UIEventSource<undefined | string>; | ||||
| export let args: string[] = []; | ||||
| let uploadableOnly: boolean = args[0] === "uploadableOnly"; | ||||
| export let overpassSupportNeeded: boolean; | ||||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Input helper to create a tag. The tag is JSON-encoded | ||||
|    */ | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte" | ||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils" | ||||
|   import nmd from "nano-markdown" | ||||
|   import FromHtml from "../../Base/FromHtml.svelte" | ||||
|   export let value: UIEventSource<undefined | string> | ||||
|   export let args: string[] = [] | ||||
|   let uploadableOnly: boolean = args[0] === "uploadableOnly" | ||||
|   export let overpassSupportNeeded: boolean | ||||
| 
 | ||||
| /** | ||||
|  * Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries) | ||||
|  */ | ||||
| export let silent: boolean = false; | ||||
| let mode: string = "="; | ||||
| let dropdownFocussed = new UIEventSource(false); | ||||
| let documentation = TagUtils.modeDocumentation[mode]; | ||||
| $: documentation = TagUtils.modeDocumentation[mode]; | ||||
|   /** | ||||
|    * Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries) | ||||
|    */ | ||||
|   export let silent: boolean = false | ||||
|   let mode: string = "=" | ||||
|   let dropdownFocussed = new UIEventSource(false) | ||||
|   let documentation = TagUtils.modeDocumentation[mode] | ||||
|   $: documentation = TagUtils.modeDocumentation[mode] | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <BasicTagInput bind:mode={mode} {dropdownFocussed} {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} on:submit /> | ||||
| <BasicTagInput | ||||
|   bind:mode | ||||
|   {dropdownFocussed} | ||||
|   {overpassSupportNeeded} | ||||
|   {silent} | ||||
|   tag={value} | ||||
|   {uploadableOnly} | ||||
|   on:submit | ||||
| /> | ||||
| {#if $dropdownFocussed} | ||||
|   <div class="border border-dashed border-black p-2 m-2"> | ||||
|   <b>{documentation.name}</b> | ||||
|     <FromHtml src={nmd(documentation.docs)}/> | ||||
|   <div class="m-2 border border-dashed border-black p-2"> | ||||
|     <b>{documentation.name}</b> | ||||
|     <FromHtml src={nmd(documentation.docs)} /> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,22 +1,21 @@ | |||
| <script lang="ts">/** | ||||
|  * Input helper to create a tag. The tag is JSON-encoded | ||||
|  */ | ||||
| import { UIEventSource } from "../../../Logic/UIEventSource"; | ||||
| import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson"; | ||||
| import FullTagInput from "../../Studio/TagInput/FullTagInput.svelte"; | ||||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Input helper to create a tag. The tag is JSON-encoded | ||||
|    */ | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson" | ||||
|   import FullTagInput from "../../Studio/TagInput/FullTagInput.svelte" | ||||
| 
 | ||||
| export let value: UIEventSource<TagConfigJson>; | ||||
| export let uploadableOnly: boolean; | ||||
| export let overpassSupportNeeded: boolean; | ||||
|   export let value: UIEventSource<TagConfigJson> | ||||
|   export let uploadableOnly: boolean | ||||
|   export let overpassSupportNeeded: boolean | ||||
| 
 | ||||
| /** | ||||
|  * Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries) | ||||
|  */ | ||||
| export let silent: boolean = false; | ||||
| 
 | ||||
| let tag: UIEventSource<string | TagConfigJson> = value | ||||
|   /** | ||||
|    * Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries) | ||||
|    */ | ||||
|   export let silent: boolean = false | ||||
| 
 | ||||
|   let tag: UIEventSource<string | TagConfigJson> = value | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit/> | ||||
| <FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit /> | ||||
|  |  | |||
|  | @ -1,55 +1,59 @@ | |||
| <script lang="ts"> | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import LanguageUtils from "../../../Utils/LanguageUtils" | ||||
|   import { createEventDispatcher, onDestroy } from "svelte" | ||||
|   import ValidatedInput from "../ValidatedInput.svelte" | ||||
| 
 | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource"; | ||||
|   import LanguageUtils from "../../../Utils/LanguageUtils"; | ||||
|   import { createEventDispatcher, onDestroy } from "svelte"; | ||||
|   import ValidatedInput from "../ValidatedInput.svelte"; | ||||
|   export let value: UIEventSource<Record<string, string>> = new UIEventSource< | ||||
|     Record<string, string> | ||||
|   >({}) | ||||
| 
 | ||||
|   export let value: UIEventSource<Record<string, string>> = new UIEventSource<Record<string, string>>({}); | ||||
|    | ||||
|   export let args: string[] = [] | ||||
|    | ||||
| 
 | ||||
|   let prefix = args[0] ?? "" | ||||
|   let postfix = args[1] ?? "" | ||||
| 
 | ||||
|   let translations: UIEventSource<Record<string, string>> = value | ||||
| 
 | ||||
|   const allLanguages: string[] = LanguageUtils.usedLanguagesSorted; | ||||
|   let currentLang = new UIEventSource("en"); | ||||
|   const currentVal = new UIEventSource<string>(""); | ||||
|   const allLanguages: string[] = LanguageUtils.usedLanguagesSorted | ||||
|   let currentLang = new UIEventSource("en") | ||||
|   const currentVal = new UIEventSource<string>("") | ||||
|   let dispatch = createEventDispatcher<{ submit }>() | ||||
| 
 | ||||
|   function update() { | ||||
|     const v = currentVal.data; | ||||
|     const l = currentLang.data; | ||||
|     if(translations.data === "" || translations.data === undefined){ | ||||
|     const v = currentVal.data | ||||
|     const l = currentLang.data | ||||
|     if (translations.data === "" || translations.data === undefined) { | ||||
|       translations.data = {} | ||||
|     } | ||||
|     if (translations.data[l] === v) { | ||||
|       return; | ||||
|       return | ||||
|     } | ||||
|     translations.data[l] = v; | ||||
|     translations.ping(); | ||||
|     translations.data[l] = v | ||||
|     translations.ping() | ||||
|   } | ||||
| 
 | ||||
|   onDestroy(currentLang.addCallbackAndRunD(currentLang => { | ||||
|     console.log("Applying current lang:", currentLang); | ||||
|     if(!translations.data){ | ||||
|       translations.data = {} | ||||
|     } | ||||
|     translations.data[currentLang] = translations.data[currentLang] ?? ""; | ||||
|     currentVal.setData(translations.data[currentLang]); | ||||
|   })); | ||||
| 
 | ||||
| 
 | ||||
|   onDestroy(currentVal.addCallbackAndRunD(v => { | ||||
|     update(); | ||||
|   })); | ||||
|   onDestroy( | ||||
|     currentLang.addCallbackAndRunD((currentLang) => { | ||||
|       console.log("Applying current lang:", currentLang) | ||||
|       if (!translations.data) { | ||||
|         translations.data = {} | ||||
|       } | ||||
|       translations.data[currentLang] = translations.data[currentLang] ?? "" | ||||
|       currentVal.setData(translations.data[currentLang]) | ||||
|     }) | ||||
|   ) | ||||
| 
 | ||||
|   onDestroy( | ||||
|     currentVal.addCallbackAndRunD((v) => { | ||||
|       update() | ||||
|     }) | ||||
|   ) | ||||
| </script> | ||||
| <div class="flex font-bold space-x-1 m-1 mt-2 interactive"> | ||||
| 
 | ||||
| <div class="interactive m-1 mt-2 flex space-x-1 font-bold"> | ||||
|   <span> | ||||
|   {prefix} | ||||
|     {prefix} | ||||
|   </span> | ||||
|   <select bind:value={$currentLang}> | ||||
|     {#each allLanguages as language} | ||||
|  | @ -58,8 +62,13 @@ | |||
|       </option> | ||||
|     {/each} | ||||
|   </select> | ||||
|   <ValidatedInput type="string" cls="w-full" value={currentVal} on:submit={() => dispatch("submit")} /> | ||||
|   <ValidatedInput | ||||
|     type="string" | ||||
|     cls="w-full" | ||||
|     value={currentVal} | ||||
|     on:submit={() => dispatch("submit")} | ||||
|   /> | ||||
|   <span> | ||||
|   {postfix} | ||||
|     {postfix} | ||||
|   </span> | ||||
| </div> | ||||
|  |  | |||
|  | @ -4,50 +4,49 @@ | |||
|    * Note that all values are stringified | ||||
|    */ | ||||
| 
 | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import type { ValidatorType } from "./Validators"; | ||||
|   import InputHelpers from "./InputHelpers"; | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import { createEventDispatcher } from "svelte"; | ||||
|   import ImageHelper from "./Helpers/ImageHelper.svelte"; | ||||
|   import TranslationInput from "./Helpers/TranslationInput.svelte"; | ||||
|   import TagInput from "./Helpers/TagInput.svelte"; | ||||
|   import SimpleTagInput from "./Helpers/SimpleTagInput.svelte"; | ||||
|   import DirectionInput from "./Helpers/DirectionInput.svelte"; | ||||
|   import DateInput from "./Helpers/DateInput.svelte"; | ||||
|   import ColorInput from "./Helpers/ColorInput.svelte"; | ||||
|   import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { ValidatorType } from "./Validators" | ||||
|   import InputHelpers from "./InputHelpers" | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte" | ||||
|   import type { Feature } from "geojson" | ||||
|   import { createEventDispatcher } from "svelte" | ||||
|   import ImageHelper from "./Helpers/ImageHelper.svelte" | ||||
|   import TranslationInput from "./Helpers/TranslationInput.svelte" | ||||
|   import TagInput from "./Helpers/TagInput.svelte" | ||||
|   import SimpleTagInput from "./Helpers/SimpleTagInput.svelte" | ||||
|   import DirectionInput from "./Helpers/DirectionInput.svelte" | ||||
|   import DateInput from "./Helpers/DateInput.svelte" | ||||
|   import ColorInput from "./Helpers/ColorInput.svelte" | ||||
|   import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte" | ||||
| 
 | ||||
|   export let type: ValidatorType; | ||||
|   export let value: UIEventSource<string | object>; | ||||
|   export let type: ValidatorType | ||||
|   export let value: UIEventSource<string | object> | ||||
| 
 | ||||
|   export let feature: Feature; | ||||
|   export let args: (string | number | boolean)[] = undefined; | ||||
|   export let feature: Feature | ||||
|   export let args: (string | number | boolean)[] = undefined | ||||
| 
 | ||||
|   let properties = { feature, args: args ?? [] }; | ||||
|   let properties = { feature, args: args ?? [] } | ||||
|   let dispatch = createEventDispatcher<{ | ||||
|     selected | ||||
|   }>(); | ||||
| 
 | ||||
|   }>() | ||||
| </script> | ||||
| 
 | ||||
| {#if type === "translation" } | ||||
| {#if type === "translation"} | ||||
|   <TranslationInput {value} on:submit {args} /> | ||||
| {:else if type === "direction"} | ||||
|   <DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} /> | ||||
| {:else if type === "date"} | ||||
|   <DateInput { value } /> | ||||
|   <DateInput {value} /> | ||||
| {:else if type === "color"} | ||||
|   <ColorInput { value } /> | ||||
|   <ColorInput {value} /> | ||||
| {:else if type === "image"} | ||||
|   <ImageHelper { value } /> | ||||
|   <ImageHelper {value} /> | ||||
| {:else if type === "tag"} | ||||
|   <TagInput { value } on:submit /> | ||||
|   <TagInput {value} on:submit /> | ||||
| {:else if type === "simple_tag"} | ||||
|   <SimpleTagInput { value } {args} on:submit /> | ||||
|   <SimpleTagInput {value} {args} on:submit /> | ||||
| {:else if type === "opening_hours"} | ||||
|   <OpeningHoursInput { value } /> | ||||
|   <OpeningHoursInput {value} /> | ||||
| {:else if type === "wikidata"} | ||||
|   <ToSvelte construct={() => InputHelpers.constructWikidataHelper(value, properties)} /> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,145 +1,156 @@ | |||
| <script lang="ts"> | ||||
|     import {UIEventSource} from "../../Logic/UIEventSource" | ||||
|     import type {ValidatorType} from "./Validators" | ||||
|     import Validators from "./Validators" | ||||
|     import {ExclamationIcon} from "@rgossiaux/svelte-heroicons/solid" | ||||
|     import {Translation} from "../i18n/Translation" | ||||
|     import {createEventDispatcher, onDestroy} from "svelte" | ||||
|     import {Validator} from "./Validator" | ||||
|     import {Unit} from "../../Models/Unit" | ||||
|     import UnitInput from "../Popup/UnitInput.svelte" | ||||
|     import {Utils} from "../../Utils"; | ||||
|     import { twMerge } from "tailwind-merge"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { ValidatorType } from "./Validators" | ||||
|   import Validators from "./Validators" | ||||
|   import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { Translation } from "../i18n/Translation" | ||||
|   import { createEventDispatcher, onDestroy } from "svelte" | ||||
|   import { Validator } from "./Validator" | ||||
|   import { Unit } from "../../Models/Unit" | ||||
|   import UnitInput from "../Popup/UnitInput.svelte" | ||||
|   import { Utils } from "../../Utils" | ||||
|   import { twMerge } from "tailwind-merge" | ||||
| 
 | ||||
|   export let type: ValidatorType | ||||
|   export let feedback: UIEventSource<Translation> | undefined = undefined | ||||
|     export let cls : string = undefined | ||||
|   export let cls: string = undefined | ||||
|   export let getCountry: () => string | undefined | ||||
|   export let placeholder: string | Translation | undefined | ||||
|   export let unit: Unit = undefined | ||||
|     export let value: UIEventSource<string> | ||||
|     /** | ||||
|      * Internal state bound to the input element. | ||||
|      * | ||||
|      * This is only copied to 'value' when appropriate so that no invalid values leak outside; | ||||
|      * Additionally, the unit is added when copying | ||||
|      */ | ||||
|     let _value = new UIEventSource(value.data ?? "") | ||||
|   export let value: UIEventSource<string> | ||||
|   /** | ||||
|    * Internal state bound to the input element. | ||||
|    * | ||||
|    * This is only copied to 'value' when appropriate so that no invalid values leak outside; | ||||
|    * Additionally, the unit is added when copying | ||||
|    */ | ||||
|   let _value = new UIEventSource(value.data ?? "") | ||||
| 
 | ||||
|     let validator: Validator = Validators.get(type ?? "string") | ||||
|     if(validator === undefined){ | ||||
|         console.warn("Didn't find a validator for type", type) | ||||
|     } | ||||
|     let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined) | ||||
|     let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type | ||||
|   let validator: Validator = Validators.get(type ?? "string") | ||||
|   if (validator === undefined) { | ||||
|     console.warn("Didn't find a validator for type", type) | ||||
|   } | ||||
|   let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined) | ||||
|   let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type | ||||
| 
 | ||||
|     function initValueAndDenom() { | ||||
|         if (unit && value.data) { | ||||
|             const [v, denom] = unit?.findDenomination(value.data, getCountry) | ||||
|             if (denom) { | ||||
|                 _value.setData(v) | ||||
|                 selectedUnit.setData(denom.canonical) | ||||
|             } else { | ||||
|                 _value.setData(value.data ?? "") | ||||
|             } | ||||
|         } else { | ||||
|             _value.setData(value.data ?? "") | ||||
|         } | ||||
|   function initValueAndDenom() { | ||||
|     if (unit && value.data) { | ||||
|       const [v, denom] = unit?.findDenomination(value.data, getCountry) | ||||
|       if (denom) { | ||||
|         _value.setData(v) | ||||
|         selectedUnit.setData(denom.canonical) | ||||
|       } else { | ||||
|         _value.setData(value.data ?? "") | ||||
|       } | ||||
|     } else { | ||||
|       _value.setData(value.data ?? "") | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   initValueAndDenom() | ||||
| 
 | ||||
|   $: { | ||||
|     // The type changed -> reset some values | ||||
|     validator = Validators.get(type ?? "string") | ||||
| 
 | ||||
|     _placeholder = placeholder ?? validator?.getPlaceholder() ?? type | ||||
|     feedback?.setData(validator?.getFeedback(_value.data, getCountry)) | ||||
| 
 | ||||
|     initValueAndDenom() | ||||
|   } | ||||
| 
 | ||||
|     $: { | ||||
|         // The type changed -> reset some values | ||||
|         validator = Validators.get(type ?? "string") | ||||
|          | ||||
|         _placeholder = placeholder ?? validator?.getPlaceholder() ?? type | ||||
|         feedback?.setData(validator?.getFeedback(_value.data, getCountry)) | ||||
| 
 | ||||
|         initValueAndDenom() | ||||
|   function setValues() { | ||||
|     // Update the value stores | ||||
|     const v = _value.data | ||||
|     if (!validator?.isValid(v, getCountry) || v === "") { | ||||
|       feedback?.setData(validator?.getFeedback(v, getCountry)) | ||||
|       value.setData("") | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     function setValues() { | ||||
|         // Update the value stores | ||||
|         const v = _value.data | ||||
|         if (!validator?.isValid(v, getCountry) || v === "") { | ||||
|             feedback?.setData(validator?.getFeedback(v, getCountry)) | ||||
|             value.setData("") | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if (unit !== undefined && isNaN(Number(v))) { | ||||
|             value.setData(undefined) | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         feedback?.setData(undefined) | ||||
|         if(selectedUnit.data){ | ||||
|             value.setData(v + selectedUnit.data) | ||||
|              | ||||
|         }else{ | ||||
|             value.setData(v) | ||||
|         } | ||||
|     } | ||||
|          | ||||
| 
 | ||||
|     onDestroy(_value.addCallbackAndRun((_) => setValues())) | ||||
|     onDestroy(value.addCallbackAndRunD(fromUpstream => { | ||||
|         if(_value.data !== fromUpstream && fromUpstream !== ""){ | ||||
|             _value.setData(fromUpstream) | ||||
|         } | ||||
|     })) | ||||
|     onDestroy(selectedUnit.addCallback((_) => setValues())) | ||||
|     if (validator === undefined) { | ||||
|         throw "Not a valid type (no validator found) for type '" + type+"'; did you perhaps mean one of: "+Utils.sortedByLevenshteinDistance(type, Validators.AllValidators.map(v => v.name), v => v).slice(0, 5).join(", ") | ||||
|     if (unit !== undefined && isNaN(Number(v))) { | ||||
|       value.setData(undefined) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true) | ||||
| 
 | ||||
|     let htmlElem: HTMLInputElement | ||||
| 
 | ||||
|     let dispatch = createEventDispatcher<{ selected, submit }>() | ||||
|     $: { | ||||
|         if (htmlElem !== undefined) { | ||||
|             htmlElem.onfocus = () => dispatch("selected") | ||||
|         } | ||||
|     feedback?.setData(undefined) | ||||
|     if (selectedUnit.data) { | ||||
|       value.setData(v + selectedUnit.data) | ||||
|     } else { | ||||
|       value.setData(v) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|     /** | ||||
|      * Dispatches the submit, but only if the value is valid | ||||
|      */ | ||||
|     function sendSubmit(){ | ||||
|         if(feedback?.data){ | ||||
|             console.log("Not sending a submit as there is feedback") | ||||
|         } | ||||
|         dispatch("submit")  | ||||
|   onDestroy(_value.addCallbackAndRun((_) => setValues())) | ||||
|   onDestroy( | ||||
|     value.addCallbackAndRunD((fromUpstream) => { | ||||
|       if (_value.data !== fromUpstream && fromUpstream !== "") { | ||||
|         _value.setData(fromUpstream) | ||||
|       } | ||||
|     }) | ||||
|   ) | ||||
|   onDestroy(selectedUnit.addCallback((_) => setValues())) | ||||
|   if (validator === undefined) { | ||||
|     throw ( | ||||
|       "Not a valid type (no validator found) for type '" + | ||||
|       type + | ||||
|       "'; did you perhaps mean one of: " + | ||||
|       Utils.sortedByLevenshteinDistance( | ||||
|         type, | ||||
|         Validators.AllValidators.map((v) => v.name), | ||||
|         (v) => v | ||||
|       ) | ||||
|         .slice(0, 5) | ||||
|         .join(", ") | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true) | ||||
| 
 | ||||
|   let htmlElem: HTMLInputElement | ||||
| 
 | ||||
|   let dispatch = createEventDispatcher<{ selected; submit }>() | ||||
|   $: { | ||||
|     if (htmlElem !== undefined) { | ||||
|       htmlElem.onfocus = () => dispatch("selected") | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Dispatches the submit, but only if the value is valid | ||||
|    */ | ||||
|   function sendSubmit() { | ||||
|     if (feedback?.data) { | ||||
|       console.log("Not sending a submit as there is feedback") | ||||
|     } | ||||
|     dispatch("submit") | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| {#if validator?.textArea} | ||||
|     <form on:submit|preventDefault={() => sendSubmit()}> | ||||
|      | ||||
|   <textarea | ||||
|           class="w-full" | ||||
|           bind:value={$_value} | ||||
|           inputmode={validator?.inputmode ?? "text"} | ||||
|           placeholder={_placeholder}></textarea> | ||||
|     </form> | ||||
|   <form on:submit|preventDefault={() => sendSubmit()}> | ||||
|     <textarea | ||||
|       class="w-full" | ||||
|       bind:value={$_value} | ||||
|       inputmode={validator?.inputmode ?? "text"} | ||||
|       placeholder={_placeholder} | ||||
|     /> | ||||
|   </form> | ||||
| {:else} | ||||
|     <form class={twMerge("inline-flex",cls )} on:submit|preventDefault={() => sendSubmit()}> | ||||
|         <input | ||||
|                 bind:this={htmlElem} | ||||
|                 bind:value={$_value} | ||||
|                 class="w-full" | ||||
|                 inputmode={validator?.inputmode ?? "text"} | ||||
|                 placeholder={_placeholder} | ||||
|         /> | ||||
|         {#if !$isValid} | ||||
|             <ExclamationIcon class="-ml-6 h-6 w-6"/> | ||||
|         {/if} | ||||
|   <form class={twMerge("inline-flex", cls)} on:submit|preventDefault={() => sendSubmit()}> | ||||
|     <input | ||||
|       bind:this={htmlElem} | ||||
|       bind:value={$_value} | ||||
|       class="w-full" | ||||
|       inputmode={validator?.inputmode ?? "text"} | ||||
|       placeholder={_placeholder} | ||||
|     /> | ||||
|     {#if !$isValid} | ||||
|       <ExclamationIcon class="-ml-6 h-6 w-6" /> | ||||
|     {/if} | ||||
| 
 | ||||
|         {#if unit !== undefined} | ||||
|             <UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value}/> | ||||
|         {/if} | ||||
|     </form> | ||||
|     {#if unit !== undefined} | ||||
|       <UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} /> | ||||
|     {/if} | ||||
|   </form> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import { Translation } from "../../i18n/Translation" | |||
| import Translations from "../../i18n/Translations" | ||||
| 
 | ||||
| export default class TagKeyValidator extends Validator { | ||||
| 
 | ||||
|     public readonly isMeta = true | ||||
|     constructor() { | ||||
|         super("key", "Validates a key, mostly that no weird characters are used") | ||||
|  |  | |||
|  | @ -8,7 +8,6 @@ import SimpleTagValidator from "./SimpleTagValidator" | |||
|  * Checks that the input conforms a JSON-encoded tag expression or a simpleTag`key=value`, | ||||
|  */ | ||||
| export default class TagValidator extends Validator { | ||||
| 
 | ||||
|     public readonly isMeta = true | ||||
|     constructor() { | ||||
|         super("tag", "A simple tag of the format `key=value` OR a tagExpression") | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| import { Validator } from "../Validator" | ||||
| 
 | ||||
| export default class TranslationValidator extends Validator { | ||||
| 
 | ||||
|     public readonly isMeta = true | ||||
|     constructor() { | ||||
|         super("translation", "Makes sure the the string is of format `Record<string, string>` ") | ||||
|  |  | |||
|  | @ -3,8 +3,9 @@ import { Validator } from "../Validator" | |||
| export default class UrlValidator extends Validator { | ||||
|     constructor(name?: string, explanation?: string) { | ||||
|         super( | ||||
|             name ??"url", | ||||
|              explanation?? "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed", | ||||
|             name ?? "url", | ||||
|             explanation ?? | ||||
|                 "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed", | ||||
|             "url" | ||||
|         ) | ||||
|     } | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ | |||
|         </td> | ||||
|         <td> | ||||
|           <b>{contributor.nrOfImages}</b> | ||||
|            total images | ||||
|           total images | ||||
|         </td> | ||||
|       </tr> | ||||
|     {/each} | ||||
|  |  | |||
|  | @ -1,41 +1,40 @@ | |||
| <script lang="ts"> | ||||
|   import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"; | ||||
|   import { Store } from "../../Logic/UIEventSource"; | ||||
|   import Pin from "../../assets/svg/Pin.svelte"; | ||||
|   import Square from "../../assets/svg/Square.svelte"; | ||||
|   import Circle from "../../assets/svg/Circle.svelte"; | ||||
|   import Checkmark from "../../assets/svg/Checkmark.svelte"; | ||||
|   import Clock from "../../assets/svg/Clock.svelte"; | ||||
|   import Close from "../../assets/svg/Close.svelte"; | ||||
|   import Crosshair from "../../assets/svg/Crosshair.svelte"; | ||||
|   import Help from "../../assets/svg/Help.svelte"; | ||||
|   import Home from "../../assets/svg/Home.svelte"; | ||||
|   import Invalid from "../../assets/svg/Invalid.svelte"; | ||||
|   import Location from "../../assets/svg/Location.svelte"; | ||||
|   import Location_empty from "../../assets/svg/Location_empty.svelte"; | ||||
|   import Location_locked from "../../assets/svg/Location_locked.svelte"; | ||||
|   import Note from "../../assets/svg/Note.svelte"; | ||||
|   import Resolved from "../../assets/svg/Resolved.svelte"; | ||||
|   import Ring from "../../assets/svg/Ring.svelte"; | ||||
|   import Scissors from "../../assets/svg/Scissors.svelte"; | ||||
|   import Teardrop from "../../assets/svg/Teardrop.svelte"; | ||||
|   import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"; | ||||
|   import Triangle from "../../assets/svg/Triangle.svelte"; | ||||
|   import Icon from "./Icon.svelte"; | ||||
|   import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig" | ||||
|   import { Store } from "../../Logic/UIEventSource" | ||||
|   import Pin from "../../assets/svg/Pin.svelte" | ||||
|   import Square from "../../assets/svg/Square.svelte" | ||||
|   import Circle from "../../assets/svg/Circle.svelte" | ||||
|   import Checkmark from "../../assets/svg/Checkmark.svelte" | ||||
|   import Clock from "../../assets/svg/Clock.svelte" | ||||
|   import Close from "../../assets/svg/Close.svelte" | ||||
|   import Crosshair from "../../assets/svg/Crosshair.svelte" | ||||
|   import Help from "../../assets/svg/Help.svelte" | ||||
|   import Home from "../../assets/svg/Home.svelte" | ||||
|   import Invalid from "../../assets/svg/Invalid.svelte" | ||||
|   import Location from "../../assets/svg/Location.svelte" | ||||
|   import Location_empty from "../../assets/svg/Location_empty.svelte" | ||||
|   import Location_locked from "../../assets/svg/Location_locked.svelte" | ||||
|   import Note from "../../assets/svg/Note.svelte" | ||||
|   import Resolved from "../../assets/svg/Resolved.svelte" | ||||
|   import Ring from "../../assets/svg/Ring.svelte" | ||||
|   import Scissors from "../../assets/svg/Scissors.svelte" | ||||
|   import Teardrop from "../../assets/svg/Teardrop.svelte" | ||||
|   import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte" | ||||
|   import Triangle from "../../assets/svg/Triangle.svelte" | ||||
|   import Icon from "./Icon.svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * Renders a single icon. | ||||
|    * | ||||
|    * Icons -placed on top of each other- form a 'Marker' together | ||||
|    */ | ||||
|   export let icon: IconConfig; | ||||
|   export let tags: Store<Record<string, string>>; | ||||
| 
 | ||||
|   let iconItem = icon.icon?.GetRenderValue(tags)?.txt; | ||||
|   $: iconItem = icon.icon?.GetRenderValue($tags)?.txt; | ||||
|   let color = icon.color?.GetRenderValue(tags)?.txt ?? "#000000"; | ||||
|   $: color = icon.color?.GetRenderValue($tags)?.txt ?? "#000000"; | ||||
|   export let icon: IconConfig | ||||
|   export let tags: Store<Record<string, string>> | ||||
| 
 | ||||
|   let iconItem = icon.icon?.GetRenderValue(tags)?.txt | ||||
|   $: iconItem = icon.icon?.GetRenderValue($tags)?.txt | ||||
|   let color = icon.color?.GetRenderValue(tags)?.txt ?? "#000000" | ||||
|   $: color = icon.color?.GetRenderValue($tags)?.txt ?? "#000000" | ||||
| </script> | ||||
| 
 | ||||
| <Icon icon={iconItem} {color}/> | ||||
| <Icon icon={iconItem} {color} /> | ||||
|  |  | |||
|  | @ -1,19 +1,18 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import PointRenderingConfig, { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"; | ||||
|   import { Store } from "../../Logic/UIEventSource"; | ||||
|   import DynamicIcon from "./DynamicIcon.svelte"; | ||||
|   import PointRenderingConfig, { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig" | ||||
|   import { Store } from "../../Logic/UIEventSource" | ||||
|   import DynamicIcon from "./DynamicIcon.svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * Renders a 'marker', which consists of multiple 'icons' | ||||
|    */ | ||||
|   export let config: PointRenderingConfig; | ||||
|   let icons: IconConfig[] = config.marker; | ||||
|   export let tags: Store<Record<string, string>>; | ||||
| 
 | ||||
|   export let config: PointRenderingConfig | ||||
|   let icons: IconConfig[] = config.marker | ||||
|   export let tags: Store<Record<string, string>> | ||||
| </script> | ||||
| 
 | ||||
| {#if config !== undefined} | ||||
|   <div class="relative w-full h-full"> | ||||
|   <div class="relative h-full w-full"> | ||||
|     {#each icons as icon} | ||||
|       <DynamicIcon {icon} {tags} /> | ||||
|     {/each} | ||||
|  |  | |||
|  | @ -1,24 +1,24 @@ | |||
| <script lang="ts"> | ||||
|   import Pin from "../../assets/svg/Pin.svelte"; | ||||
|   import Square from "../../assets/svg/Square.svelte"; | ||||
|   import Circle from "../../assets/svg/Circle.svelte"; | ||||
|   import Checkmark from "../../assets/svg/Checkmark.svelte"; | ||||
|   import Clock from "../../assets/svg/Clock.svelte"; | ||||
|   import Close from "../../assets/svg/Close.svelte"; | ||||
|   import Crosshair from "../../assets/svg/Crosshair.svelte"; | ||||
|   import Help from "../../assets/svg/Help.svelte"; | ||||
|   import Home from "../../assets/svg/Home.svelte"; | ||||
|   import Invalid from "../../assets/svg/Invalid.svelte"; | ||||
|   import Location from "../../assets/svg/Location.svelte"; | ||||
|   import Location_empty from "../../assets/svg/Location_empty.svelte"; | ||||
|   import Location_locked from "../../assets/svg/Location_locked.svelte"; | ||||
|   import Note from "../../assets/svg/Note.svelte"; | ||||
|   import Resolved from "../../assets/svg/Resolved.svelte"; | ||||
|   import Ring from "../../assets/svg/Ring.svelte"; | ||||
|   import Scissors from "../../assets/svg/Scissors.svelte"; | ||||
|   import Teardrop from "../../assets/svg/Teardrop.svelte"; | ||||
|   import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"; | ||||
|   import Triangle from "../../assets/svg/Triangle.svelte"; | ||||
|   import Pin from "../../assets/svg/Pin.svelte" | ||||
|   import Square from "../../assets/svg/Square.svelte" | ||||
|   import Circle from "../../assets/svg/Circle.svelte" | ||||
|   import Checkmark from "../../assets/svg/Checkmark.svelte" | ||||
|   import Clock from "../../assets/svg/Clock.svelte" | ||||
|   import Close from "../../assets/svg/Close.svelte" | ||||
|   import Crosshair from "../../assets/svg/Crosshair.svelte" | ||||
|   import Help from "../../assets/svg/Help.svelte" | ||||
|   import Home from "../../assets/svg/Home.svelte" | ||||
|   import Invalid from "../../assets/svg/Invalid.svelte" | ||||
|   import Location from "../../assets/svg/Location.svelte" | ||||
|   import Location_empty from "../../assets/svg/Location_empty.svelte" | ||||
|   import Location_locked from "../../assets/svg/Location_locked.svelte" | ||||
|   import Note from "../../assets/svg/Note.svelte" | ||||
|   import Resolved from "../../assets/svg/Resolved.svelte" | ||||
|   import Ring from "../../assets/svg/Ring.svelte" | ||||
|   import Scissors from "../../assets/svg/Scissors.svelte" | ||||
|   import Teardrop from "../../assets/svg/Teardrop.svelte" | ||||
|   import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte" | ||||
|   import Triangle from "../../assets/svg/Triangle.svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * Renders a single icon. | ||||
|  | @ -26,13 +26,12 @@ | |||
|    * Icons -placed on top of each other- form a 'Marker' together | ||||
|    */ | ||||
| 
 | ||||
|   export let icon: string | undefined; | ||||
|   export let color: string | undefined; | ||||
| 
 | ||||
|   export let icon: string | undefined | ||||
|   export let color: string | undefined | ||||
| </script> | ||||
| 
 | ||||
| {#if icon} | ||||
|   <div class="absolute top-0 left-0 w-full h-full"> | ||||
|   <div class="absolute top-0 left-0 h-full w-full"> | ||||
|     {#if icon === "pin"} | ||||
|       <Pin {color} /> | ||||
|     {:else if icon === "square"} | ||||
|  | @ -74,7 +73,7 @@ | |||
|     {:else if icon === "triangle"} | ||||
|       <Triangle {color} /> | ||||
|     {:else} | ||||
|       <img class="w-full h-full" src={icon} /> | ||||
|       <img class="h-full w-full" src={icon} /> | ||||
|     {/if} | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,15 +1,14 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import Icon from "./Icon.svelte"; | ||||
|   import Icon from "./Icon.svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * Renders a 'marker', which consists of multiple 'icons' | ||||
|    */ | ||||
| export  let icons: { icon: string, color: string }[]  | ||||
| 
 | ||||
|   export let icons: { icon: string; color: string }[] | ||||
| </script> | ||||
| 
 | ||||
| {#if icons !== undefined && icons.length > 0} | ||||
|   <div class="relative w-full h-full"> | ||||
|   <div class="relative h-full w-full"> | ||||
|     {#each icons as icon} | ||||
|       <Icon icon={icon.icon} color={icon.color} /> | ||||
|     {/each} | ||||
|  |  | |||
|  | @ -423,10 +423,7 @@ class LineRenderingLayer { | |||
| } | ||||
| 
 | ||||
| export default class ShowDataLayer { | ||||
|     private static rangeLayer = new LayerConfig( | ||||
|         <any>range_layer, | ||||
|         "ShowDataLayer.ts:range.json" | ||||
|     ) | ||||
|     private static rangeLayer = new LayerConfig(<any>range_layer, "ShowDataLayer.ts:range.json") | ||||
|     private readonly _options: ShowDataLayerOptions & { | ||||
|         layer: LayerConfig | ||||
|         drawMarkers?: true | boolean | ||||
|  |  | |||
|  | @ -3,109 +3,109 @@ | |||
|    * This component ties together all the steps that are needed to create a new point. | ||||
|    * There are many subcomponents which help with that | ||||
|    */ | ||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization"; | ||||
|   import PresetList from "./PresetList.svelte"; | ||||
|   import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"; | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
|   import Tr from "../../Base/Tr.svelte"; | ||||
|   import SubtleButton from "../../Base/SubtleButton.svelte"; | ||||
|   import FromHtml from "../../Base/FromHtml.svelte"; | ||||
|   import Translations from "../../i18n/Translations.js"; | ||||
|   import TagHint from "../TagHint.svelte"; | ||||
|   import { And } from "../../../Logic/Tags/And.js"; | ||||
|   import LoginToggle from "../../Base/LoginToggle.svelte"; | ||||
|   import Constants from "../../../Models/Constants.js"; | ||||
|   import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
|   import { Store, UIEventSource } from "../../../Logic/UIEventSource"; | ||||
|   import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import LoginButton from "../../Base/LoginButton.svelte"; | ||||
|   import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"; | ||||
|   import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||
|   import { OsmWay } from "../../../Logic/Osm/OsmObject"; | ||||
|   import { Tag } from "../../../Logic/Tags/Tag"; | ||||
|   import type { WayId } from "../../../Models/OsmFeature"; | ||||
|   import Loading from "../../Base/Loading.svelte"; | ||||
|   import type { GlobalFilter } from "../../../Models/GlobalFilter"; | ||||
|   import { onDestroy } from "svelte"; | ||||
|   import NextButton from "../../Base/NextButton.svelte"; | ||||
|   import BackButton from "../../Base/BackButton.svelte"; | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte"; | ||||
|   import Svg from "../../../Svg"; | ||||
|   import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"; | ||||
|   import { twJoin } from "tailwind-merge"; | ||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization" | ||||
|   import PresetList from "./PresetList.svelte" | ||||
|   import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig" | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
|   import Tr from "../../Base/Tr.svelte" | ||||
|   import SubtleButton from "../../Base/SubtleButton.svelte" | ||||
|   import FromHtml from "../../Base/FromHtml.svelte" | ||||
|   import Translations from "../../i18n/Translations.js" | ||||
|   import TagHint from "../TagHint.svelte" | ||||
|   import { And } from "../../../Logic/Tags/And.js" | ||||
|   import LoginToggle from "../../Base/LoginToggle.svelte" | ||||
|   import Constants from "../../../Models/Constants.js" | ||||
|   import FilteredLayer from "../../../Models/FilteredLayer" | ||||
|   import { Store, UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import LoginButton from "../../Base/LoginButton.svelte" | ||||
|   import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte" | ||||
|   import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction" | ||||
|   import { OsmWay } from "../../../Logic/Osm/OsmObject" | ||||
|   import { Tag } from "../../../Logic/Tags/Tag" | ||||
|   import type { WayId } from "../../../Models/OsmFeature" | ||||
|   import Loading from "../../Base/Loading.svelte" | ||||
|   import type { GlobalFilter } from "../../../Models/GlobalFilter" | ||||
|   import { onDestroy } from "svelte" | ||||
|   import NextButton from "../../Base/NextButton.svelte" | ||||
|   import BackButton from "../../Base/BackButton.svelte" | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte" | ||||
|   import Svg from "../../../Svg" | ||||
|   import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte" | ||||
|   import { twJoin } from "tailwind-merge" | ||||
| 
 | ||||
|   export let coordinate: { lon: number; lat: number }; | ||||
|   export let state: SpecialVisualizationState; | ||||
|   export let coordinate: { lon: number; lat: number } | ||||
|   export let state: SpecialVisualizationState | ||||
| 
 | ||||
|   let selectedPreset: { | ||||
|     preset: PresetConfig | ||||
|     layer: LayerConfig | ||||
|     icon: string | ||||
|     tags: Record<string, string> | ||||
|   } = undefined; | ||||
|   let checkedOfGlobalFilters: number = 0; | ||||
|   let confirmedCategory = false; | ||||
|   } = undefined | ||||
|   let checkedOfGlobalFilters: number = 0 | ||||
|   let confirmedCategory = false | ||||
|   $: if (selectedPreset === undefined) { | ||||
|     confirmedCategory = false; | ||||
|     creating = false; | ||||
|     checkedOfGlobalFilters = 0; | ||||
|     confirmedCategory = false | ||||
|     creating = false | ||||
|     checkedOfGlobalFilters = 0 | ||||
|   } | ||||
| 
 | ||||
|   let flayer: FilteredLayer = undefined; | ||||
|   let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined; | ||||
|   let layerHasFilters: Store<boolean> | undefined = undefined; | ||||
|   let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters; | ||||
|   let _globalFilter: GlobalFilter[] = []; | ||||
|   let flayer: FilteredLayer = undefined | ||||
|   let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined | ||||
|   let layerHasFilters: Store<boolean> | undefined = undefined | ||||
|   let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters | ||||
|   let _globalFilter: GlobalFilter[] = [] | ||||
|   onDestroy( | ||||
|     globalFilter.addCallbackAndRun((globalFilter) => { | ||||
|       console.log("Global filters are", globalFilter); | ||||
|       _globalFilter = globalFilter ?? []; | ||||
|       console.log("Global filters are", globalFilter) | ||||
|       _globalFilter = globalFilter ?? [] | ||||
|     }) | ||||
|   ); | ||||
|   ) | ||||
|   $: { | ||||
|     flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id); | ||||
|     layerIsDisplayed = flayer?.isDisplayed; | ||||
|     layerHasFilters = flayer?.hasFilter; | ||||
|     flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id) | ||||
|     layerIsDisplayed = flayer?.isDisplayed | ||||
|     layerHasFilters = flayer?.hasFilter | ||||
|   } | ||||
|   const t = Translations.t.general.add; | ||||
|   const t = Translations.t.general.add | ||||
| 
 | ||||
|   const zoom = state.mapProperties.zoom; | ||||
|   const zoom = state.mapProperties.zoom | ||||
| 
 | ||||
|   const isLoading = state.dataIsLoading; | ||||
|   let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined); | ||||
|   let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined); | ||||
|   const isLoading = state.dataIsLoading | ||||
|   let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined) | ||||
|   let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined) | ||||
| 
 | ||||
|   // Small helper variable: if the map is tapped, we should let the 'Next'-button grab some attention as users have to click _that_ to continue, not the map | ||||
|   let preciseInputIsTapped = false; | ||||
|   let preciseInputIsTapped = false | ||||
| 
 | ||||
|   let creating = false; | ||||
|   let creating = false | ||||
| 
 | ||||
|   /** | ||||
|    * Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters. | ||||
|    * Will delete the lastclick-location | ||||
|    */ | ||||
|   function abort() { | ||||
|     state.selectedElement.setData(undefined); | ||||
|     state.selectedElement.setData(undefined) | ||||
|     // When aborted, we force the contributors to place the pin _again_ | ||||
|     // This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map | ||||
|     state.lastClickObject.features.setData([]); | ||||
|     preciseInputIsTapped = false; | ||||
|     state.lastClickObject.features.setData([]) | ||||
|     preciseInputIsTapped = false | ||||
|   } | ||||
| 
 | ||||
|   async function confirm() { | ||||
|     creating = true; | ||||
|     const location: { lon: number; lat: number } = preciseCoordinate.data; | ||||
|     const snapTo: WayId | undefined = <WayId>snappedToObject.data; | ||||
|     creating = true | ||||
|     const location: { lon: number; lat: number } = preciseCoordinate.data | ||||
|     const snapTo: WayId | undefined = <WayId>snappedToObject.data | ||||
|     const tags: Tag[] = selectedPreset.preset.tags.concat( | ||||
|       ..._globalFilter.map((f) => f?.onNewPoint?.tags ?? []) | ||||
|     ); | ||||
|     console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags); | ||||
|     ) | ||||
|     console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags) | ||||
| 
 | ||||
|     let snapToWay: undefined | OsmWay = undefined; | ||||
|     let snapToWay: undefined | OsmWay = undefined | ||||
|     if (snapTo !== undefined && snapTo !== null) { | ||||
|       const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0); | ||||
|       const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0) | ||||
|       if (downloaded !== "deleted") { | ||||
|         snapToWay = downloaded; | ||||
|         snapToWay = downloaded | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | @ -113,44 +113,44 @@ | |||
|       theme: state.layout?.id ?? "unkown", | ||||
|       changeType: "create", | ||||
|       snapOnto: snapToWay, | ||||
|       reusePointWithinMeters: 1 | ||||
|     }); | ||||
|     await state.changes.applyAction(newElementAction); | ||||
|     state.newFeatures.features.ping(); | ||||
|       reusePointWithinMeters: 1, | ||||
|     }) | ||||
|     await state.changes.applyAction(newElementAction) | ||||
|     state.newFeatures.features.ping() | ||||
|     // The 'changes' should have created a new point, which added this into the 'featureProperties' | ||||
|     const newId = newElementAction.newElementId; | ||||
|     console.log("Applied pending changes, fetching store for", newId); | ||||
|     const tagsStore = state.featureProperties.getStore(newId); | ||||
|     const newId = newElementAction.newElementId | ||||
|     console.log("Applied pending changes, fetching store for", newId) | ||||
|     const tagsStore = state.featureProperties.getStore(newId) | ||||
|     if (!tagsStore) { | ||||
|       console.error("Bug: no tagsStore found for", newId); | ||||
|       console.error("Bug: no tagsStore found for", newId) | ||||
|     } | ||||
|     { | ||||
|       // Set some metainfo | ||||
|       const properties = tagsStore.data; | ||||
|       const properties = tagsStore.data | ||||
|       if (snapTo) { | ||||
|         // metatags (starting with underscore) are not uploaded, so we can safely mark this | ||||
|         delete properties["_referencing_ways"]; | ||||
|         properties["_referencing_ways"] = `["${snapTo}"]`; | ||||
|         delete properties["_referencing_ways"] | ||||
|         properties["_referencing_ways"] = `["${snapTo}"]` | ||||
|       } | ||||
|       properties["_backend"] = state.osmConnection.Backend(); | ||||
|       properties["_last_edit:timestamp"] = new Date().toISOString(); | ||||
|       const userdetails = state.osmConnection.userDetails.data; | ||||
|       properties["_last_edit:contributor"] = userdetails.name; | ||||
|       properties["_last_edit:uid"] = "" + userdetails.uid; | ||||
|       tagsStore.ping(); | ||||
|       properties["_backend"] = state.osmConnection.Backend() | ||||
|       properties["_last_edit:timestamp"] = new Date().toISOString() | ||||
|       const userdetails = state.osmConnection.userDetails.data | ||||
|       properties["_last_edit:contributor"] = userdetails.name | ||||
|       properties["_last_edit:uid"] = "" + userdetails.uid | ||||
|       tagsStore.ping() | ||||
|     } | ||||
|     const feature = state.indexedFeatures.featuresById.data.get(newId); | ||||
|     console.log("Selecting feature", feature, "and opening their popup"); | ||||
|     abort(); | ||||
|     state.selectedLayer.setData(selectedPreset.layer); | ||||
|     state.selectedElement.setData(feature); | ||||
|     tagsStore.ping(); | ||||
|     const feature = state.indexedFeatures.featuresById.data.get(newId) | ||||
|     console.log("Selecting feature", feature, "and opening their popup") | ||||
|     abort() | ||||
|     state.selectedLayer.setData(selectedPreset.layer) | ||||
|     state.selectedElement.setData(feature) | ||||
|     tagsStore.ping() | ||||
|   } | ||||
| 
 | ||||
|   function confirmSync() { | ||||
|     confirm() | ||||
|       .then((_) => console.debug("New point successfully handled")) | ||||
|       .catch((e) => console.error("Handling the new point went wrong due to", e)); | ||||
|       .catch((e) => console.error("Handling the new point went wrong due to", e)) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
|  | @ -163,7 +163,6 @@ | |||
|     <Tr slot="message" t={Translations.t.general.add.pleaseLogin} /> | ||||
|   </LoginButton> | ||||
|   <div class="h-full w-full"> | ||||
| 
 | ||||
|     {#if $zoom < Constants.minZoomLevelToAddNewPoint} | ||||
|       <div class="alert"> | ||||
|         <Tr t={Translations.t.general.add.zoomInFurther} /> | ||||
|  | @ -179,8 +178,8 @@ | |||
|       <PresetList | ||||
|         {state} | ||||
|         on:select={(event) => { | ||||
|         selectedPreset = event.detail | ||||
|       }} | ||||
|           selectedPreset = event.detail | ||||
|         }} | ||||
|       /> | ||||
|     {:else if !$layerIsDisplayed} | ||||
|       <!-- Check that the layer is enabled, so that we don't add a duplicate --> | ||||
|  | @ -195,9 +194,9 @@ | |||
|         <button | ||||
|           class="flex w-full gap-x-1" | ||||
|           on:click={() => { | ||||
|           abort() | ||||
|           state.guistate.openFilterView(selectedPreset.layer) | ||||
|         }} | ||||
|             abort() | ||||
|             state.guistate.openFilterView(selectedPreset.layer) | ||||
|           }} | ||||
|         > | ||||
|           <ToSvelte construct={Svg.layers_svg().SetClass("w-12")} /> | ||||
|           <Tr t={Translations.t.general.add.openLayerControl} /> | ||||
|  | @ -206,12 +205,14 @@ | |||
|         <button | ||||
|           class="primary flex w-full gap-x-1" | ||||
|           on:click={() => { | ||||
|           layerIsDisplayed.setData(true) | ||||
|           abort() | ||||
|         }} | ||||
|             layerIsDisplayed.setData(true) | ||||
|             abort() | ||||
|           }} | ||||
|         > | ||||
|           <EyeIcon class="w-12" /> | ||||
|           <Tr t={Translations.t.general.add.enableLayer.Subs({ name: selectedPreset.layer.name })} /> | ||||
|           <Tr | ||||
|             t={Translations.t.general.add.enableLayer.Subs({ name: selectedPreset.layer.name })} | ||||
|           /> | ||||
|         </button> | ||||
|       </div> | ||||
|     {:else if $layerHasFilters} | ||||
|  | @ -224,9 +225,9 @@ | |||
|         <button | ||||
|           class="primary flex w-full gap-x-1" | ||||
|           on:click={() => { | ||||
|           abort() | ||||
|           state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters() | ||||
|         }} | ||||
|             abort() | ||||
|             state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters() | ||||
|           }} | ||||
|         > | ||||
|           <EyeOffIcon class="w-12" /> | ||||
|           <Tr t={Translations.t.general.add.disableFilters} /> | ||||
|  | @ -234,9 +235,9 @@ | |||
|         <button | ||||
|           class="flex w-full gap-x-1" | ||||
|           on:click={() => { | ||||
|           abort() | ||||
|           state.guistate.openFilterView(selectedPreset.layer) | ||||
|         }} | ||||
|             abort() | ||||
|             state.guistate.openFilterView(selectedPreset.layer) | ||||
|           }} | ||||
|         > | ||||
|           <ToSvelte construct={Svg.layers_svg().SetClass("w-12")} /> | ||||
|           <Tr t={Translations.t.general.add.openLayerControl} /> | ||||
|  | @ -265,10 +266,10 @@ | |||
|           {/if} | ||||
|         </h3> | ||||
|         <span class="flex flex-wrap items-stretch"> | ||||
|         {#each selectedPreset.preset.exampleImages as src} | ||||
|           <img {src} class="m-1 h-64 w-auto rounded-lg" /> | ||||
|         {/each} | ||||
|       </span> | ||||
|           {#each selectedPreset.preset.exampleImages as src} | ||||
|             <img {src} class="m-1 h-64 w-auto rounded-lg" /> | ||||
|           {/each} | ||||
|         </span> | ||||
|       {/if} | ||||
|       <TagHint | ||||
|         embedIn={(tags) => t.presetInfo.Subs({ tags })} | ||||
|  | @ -295,8 +296,8 @@ | |||
|       <Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12" /> | ||||
|       <SubtleButton | ||||
|         on:click={() => { | ||||
|         checkedOfGlobalFilters = checkedOfGlobalFilters + 1 | ||||
|       }} | ||||
|           checkedOfGlobalFilters = checkedOfGlobalFilters + 1 | ||||
|         }} | ||||
|       > | ||||
|         <img | ||||
|           slot="image" | ||||
|  | @ -306,27 +307,27 @@ | |||
|         <Tr | ||||
|           slot="message" | ||||
|           t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({ | ||||
|           preset: selectedPreset.preset, | ||||
|         })} | ||||
|             preset: selectedPreset.preset, | ||||
|           })} | ||||
|         /> | ||||
|       </SubtleButton> | ||||
|       <SubtleButton | ||||
|         on:click={() => { | ||||
|         globalFilter.setData([]) | ||||
|         abort() | ||||
|       }} | ||||
|           globalFilter.setData([]) | ||||
|           abort() | ||||
|         }} | ||||
|       > | ||||
|         <img slot="image" src="./assets/svg/close.svg" class="h-8 w-8" /> | ||||
|         <Tr slot="message" t={Translations.t.general.cancel} /> | ||||
|       </SubtleButton> | ||||
|     {:else if !creating} | ||||
|       <div class="flex flex-col h-full"> | ||||
|         <div class="relative min-h-20 h-full w-full p-1 "> | ||||
|       <div class="flex h-full flex-col"> | ||||
|         <div class="min-h-20 relative h-full w-full p-1"> | ||||
|           <div class="h-full w-full overflow-hidden rounded-xl"> | ||||
|             <NewPointLocationInput | ||||
|               on:click={() => { | ||||
|             preciseInputIsTapped = true | ||||
|           }} | ||||
|                 preciseInputIsTapped = true | ||||
|               }} | ||||
|               value={preciseCoordinate} | ||||
|               snappedTo={snappedToObject} | ||||
|               {state} | ||||
|  | @ -338,9 +339,9 @@ | |||
| 
 | ||||
|           <div | ||||
|             class={twJoin( | ||||
|           !preciseInputIsTapped && "hidden", | ||||
|           "absolute top-0 flex w-full justify-center p-12" | ||||
|         )} | ||||
|               !preciseInputIsTapped && "hidden", | ||||
|               "absolute top-0 flex w-full justify-center p-12" | ||||
|             )} | ||||
|           > | ||||
|             <!-- This is an _extra_ button that appears when the map is tapped - see usertest 2023-01-07 --> | ||||
|             <NextButton on:click={confirmSync} clss="primary w-fit"> | ||||
|  |  | |||
|  | @ -11,9 +11,9 @@ | |||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
|   import FromHtml from "../../Base/FromHtml.svelte" | ||||
|   import NextButton from "../../Base/NextButton.svelte" | ||||
|   import { UIElement } from "../../UIElement"; | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte"; | ||||
|   import BaseUIElement from "../../BaseUIElement"; | ||||
|   import { UIElement } from "../../UIElement" | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte" | ||||
|   import BaseUIElement from "../../BaseUIElement" | ||||
| 
 | ||||
|   /** | ||||
|    * This component lists all the presets and allows the user to select one | ||||
|  | @ -86,7 +86,7 @@ | |||
| 
 | ||||
|   {#each presets as preset} | ||||
|     <NextButton on:click={() => dispatch("select", preset)}> | ||||
|         <ToSvelte slot="image" construct={() => preset.icon} /> | ||||
|       <ToSvelte slot="image" construct={() => preset.icon} /> | ||||
|       <div class="flex flex-col"> | ||||
|         <b class="w-fit"> | ||||
|           <Tr t={preset.text} /> | ||||
|  |  | |||
|  | @ -8,11 +8,11 @@ | |||
|   import Toggle from "../Input/Toggle" | ||||
|   import Lazy from "../Base/Lazy" | ||||
|   import BaseUIElement from "../BaseUIElement" | ||||
|   import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
|   import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| 
 | ||||
|   //Svelte props | ||||
|   export let tags: UIEventSource<any> | ||||
|   export let state: {layoutToUse: LayoutConfig} = undefined | ||||
|   export let state: { layoutToUse: LayoutConfig } = undefined | ||||
| 
 | ||||
|   const calculatedTags = [].concat( | ||||
|     ...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? []) | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|   import InputHelper from "../../InputElement/InputHelper.svelte" | ||||
|   import type { Feature } from "geojson" | ||||
|   import { Unit } from "../../../Models/Unit" | ||||
|   import InputHelpers from "../../InputElement/InputHelpers"; | ||||
|   import InputHelpers from "../../InputElement/InputHelpers" | ||||
| 
 | ||||
|   export let value: UIEventSource<string> | ||||
|   export let config: TagRenderingConfig | ||||
|  | @ -25,8 +25,8 @@ | |||
|     inline = config.freeform?.inline | ||||
|   } | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{selected}>() | ||||
|   export let feedback: UIEventSource<Translation>  | ||||
|   const dispatch = createEventDispatcher<{ selected }>() | ||||
|   export let feedback: UIEventSource<Translation> | ||||
|   onDestroy( | ||||
|     value.addCallbackD(() => { | ||||
|       dispatch("selected") | ||||
|  | @ -65,5 +65,11 @@ | |||
|     /> | ||||
|   {/if} | ||||
| 
 | ||||
|   <InputHelper args={config.freeform.helperArgs} {feature} type={config.freeform.type} {value} on:submit /> | ||||
|   <InputHelper | ||||
|     args={config.freeform.helperArgs} | ||||
|     {feature} | ||||
|     type={config.freeform.type} | ||||
|     {value} | ||||
|     on:submit | ||||
|   /> | ||||
| </div> | ||||
|  |  | |||
|  | @ -95,7 +95,11 @@ | |||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <div bind:this={questionboxElem} class="marker-questionbox-root" class:hidden={_questionsToAsk.length === 0 && skipped === 0 && answered === 0}> | ||||
| <div | ||||
|   bind:this={questionboxElem} | ||||
|   class="marker-questionbox-root" | ||||
|   class:hidden={_questionsToAsk.length === 0 && skipped === 0 && answered === 0} | ||||
| > | ||||
|   {#if _questionsToAsk.length === 0} | ||||
|     {#if skipped + answered > 0} | ||||
|       <div class="thanks"> | ||||
|  |  | |||
|  | @ -1,48 +1,51 @@ | |||
| <script lang="ts"> | ||||
|   import { Translation } from "../../i18n/Translation"; | ||||
|   import SpecialVisualizations from "../../SpecialVisualizations"; | ||||
|   import Locale from "../../i18n/Locale"; | ||||
|   import type { RenderingSpecification, SpecialVisualizationState } from "../../SpecialVisualization"; | ||||
|   import { Utils } from "../../../Utils.js"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource.js"; | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte"; | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
|   import WeblateLink from "../../Base/WeblateLink.svelte"; | ||||
|   import FromHtml from "../../Base/FromHtml.svelte"; | ||||
|   import BaseUIElement from "../../BaseUIElement"; | ||||
|   import { Translation } from "../../i18n/Translation" | ||||
|   import SpecialVisualizations from "../../SpecialVisualizations" | ||||
|   import Locale from "../../i18n/Locale" | ||||
|   import type { | ||||
|     RenderingSpecification, | ||||
|     SpecialVisualizationState, | ||||
|   } from "../../SpecialVisualization" | ||||
|   import { Utils } from "../../../Utils.js" | ||||
|   import type { Feature } from "geojson" | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource.js" | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte" | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
|   import WeblateLink from "../../Base/WeblateLink.svelte" | ||||
|   import FromHtml from "../../Base/FromHtml.svelte" | ||||
|   import BaseUIElement from "../../BaseUIElement" | ||||
| 
 | ||||
|   /** | ||||
|    * The 'specialTranslation' renders a `Translation`-object, but interprets the special values as well | ||||
|    */ | ||||
|   export let t: Translation; | ||||
|   export let state: SpecialVisualizationState; | ||||
|   export let tags: UIEventSource<Record<string, string>>; | ||||
|   export let feature: Feature; | ||||
|   export let layer: LayerConfig | undefined; | ||||
|   export let t: Translation | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let tags: UIEventSource<Record<string, string>> | ||||
|   export let feature: Feature | ||||
|   export let layer: LayerConfig | undefined | ||||
| 
 | ||||
|   let language = Locale.language; | ||||
|   let txt: string = t.textFor($language); | ||||
|   let specs: RenderingSpecification[] = []; | ||||
|   let language = Locale.language | ||||
|   let txt: string = t.textFor($language) | ||||
|   let specs: RenderingSpecification[] = [] | ||||
|   $: { | ||||
|     try { | ||||
|       if (txt !== undefined) { | ||||
|         const key = "cached_special_spec_" + $language; | ||||
|         specs = t[key]; | ||||
|         const key = "cached_special_spec_" + $language | ||||
|         specs = t[key] | ||||
|         if (specs === undefined) { | ||||
|           specs = SpecialVisualizations.constructSpecification(txt); | ||||
|           t[key] = specs; | ||||
|           specs = SpecialVisualizations.constructSpecification(txt) | ||||
|           t[key] = specs | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       console.error("Could not construct a specification and with arguments", txt, "due to", e); | ||||
|       console.error("Could not construct a specification and with arguments", txt, "due to", e) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function createVisualisation(specpart: Exclude<RenderingSpecification, string>): BaseUIElement { | ||||
|     { | ||||
|       try { | ||||
|         return specpart.func.constr(state, tags, specpart.args, feature, layer); | ||||
|         return specpart.func.constr(state, tags, specpart.args, feature, layer) | ||||
|       } catch (e) { | ||||
|         console.error( | ||||
|           "Could not construct a special visualisation with specification", | ||||
|  | @ -51,7 +54,7 @@ | |||
|           tags, | ||||
|           "due to", | ||||
|           e | ||||
|         ); | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -1,27 +1,27 @@ | |||
| <script lang="ts"> | ||||
|   import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import { Store, UIEventSource } from "../../../Logic/UIEventSource"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization"; | ||||
|   import TagRenderingAnswer from "./TagRenderingAnswer.svelte"; | ||||
|   import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import TagRenderingQuestion from "./TagRenderingQuestion.svelte"; | ||||
|   import { onDestroy } from "svelte"; | ||||
|   import Tr from "../../Base/Tr.svelte"; | ||||
|   import Translations from "../../i18n/Translations.js"; | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
|   import { Utils } from "../../../Utils"; | ||||
|   import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import { Store, UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import type { Feature } from "geojson" | ||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization" | ||||
|   import TagRenderingAnswer from "./TagRenderingAnswer.svelte" | ||||
|   import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import TagRenderingQuestion from "./TagRenderingQuestion.svelte" | ||||
|   import { onDestroy } from "svelte" | ||||
|   import Tr from "../../Base/Tr.svelte" | ||||
|   import Translations from "../../i18n/Translations.js" | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
|   import { Utils } from "../../../Utils" | ||||
| 
 | ||||
|   export let config: TagRenderingConfig; | ||||
|   export let tags: UIEventSource<Record<string, string>>; | ||||
|   export let selectedElement: Feature | undefined; | ||||
|   export let state: SpecialVisualizationState; | ||||
|   export let layer: LayerConfig = undefined; | ||||
|   export let config: TagRenderingConfig | ||||
|   export let tags: UIEventSource<Record<string, string>> | ||||
|   export let selectedElement: Feature | undefined | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let layer: LayerConfig = undefined | ||||
| 
 | ||||
|   export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge; | ||||
|    | ||||
|   export let highlightedRendering: UIEventSource<string> = undefined; | ||||
|   export let showQuestionIfUnknown: boolean = false; | ||||
|   export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge | ||||
| 
 | ||||
|   export let highlightedRendering: UIEventSource<string> = undefined | ||||
|   export let showQuestionIfUnknown: boolean = false | ||||
|   /** | ||||
|    * Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property | ||||
|    */ | ||||
|  | @ -31,10 +31,10 @@ | |||
|       tags.addCallbackD((tags) => { | ||||
|         editMode = !config.IsKnown(tags) | ||||
|       }) | ||||
|     ); | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   let htmlElem: HTMLDivElement; | ||||
|   let htmlElem: HTMLDivElement | ||||
|   $: { | ||||
|     if (editMode && htmlElem !== undefined && config.IsKnown(tags)) { | ||||
|       // EditMode switched to true yet the answer is already known, so the person wants to make a change | ||||
|  | @ -42,32 +42,32 @@ | |||
| 
 | ||||
|       // Some delay is applied to give Svelte the time to render the _question_ | ||||
|       window.setTimeout(() => { | ||||
|         Utils.scrollIntoView(<any>htmlElem); | ||||
|       }, 50); | ||||
|         Utils.scrollIntoView(<any>htmlElem) | ||||
|       }, 50) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const _htmlElement = new UIEventSource<HTMLElement>(undefined); | ||||
|   $: _htmlElement.setData(htmlElem); | ||||
|   const _htmlElement = new UIEventSource<HTMLElement>(undefined) | ||||
|   $: _htmlElement.setData(htmlElem) | ||||
| 
 | ||||
|   function setHighlighting() { | ||||
|     if (highlightedRendering === undefined) { | ||||
|       return; | ||||
|       return | ||||
|     } | ||||
|     if (htmlElem === undefined) { | ||||
|       return; | ||||
|       return | ||||
|     } | ||||
|     const highlighted = highlightedRendering.data; | ||||
|     const highlighted = highlightedRendering.data | ||||
|     if (config.id === highlighted) { | ||||
|       htmlElem.classList.add("glowing-shadow"); | ||||
|       htmlElem.classList.add("glowing-shadow") | ||||
|     } else { | ||||
|       htmlElem.classList.remove("glowing-shadow"); | ||||
|       htmlElem.classList.remove("glowing-shadow") | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (highlightedRendering) { | ||||
|     onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting())); | ||||
|     onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting())); | ||||
|     onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting())) | ||||
|     onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting())) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
|  | @ -106,7 +106,7 @@ | |||
|       </div> | ||||
|     {/if} | ||||
|   {:else} | ||||
|     <div class="overflow-hidden p-2 w-full"> | ||||
|     <div class="w-full overflow-hidden p-2"> | ||||
|       <TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} /> | ||||
|     </div> | ||||
|   {/if} | ||||
|  |  | |||
|  | @ -1,43 +1,43 @@ | |||
| <script lang="ts"> | ||||
|   import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"; | ||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization"; | ||||
|   import Tr from "../../Base/Tr.svelte"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import { TagsFilter } from "../../../Logic/Tags/TagsFilter"; | ||||
|   import FreeformInput from "./FreeformInput.svelte"; | ||||
|   import Translations from "../../i18n/Translations.js"; | ||||
|   import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"; | ||||
|   import { createEventDispatcher, onDestroy } from "svelte"; | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
|   import SpecialTranslation from "./SpecialTranslation.svelte"; | ||||
|   import TagHint from "../TagHint.svelte"; | ||||
|   import LoginToggle from "../../Base/LoginToggle.svelte"; | ||||
|   import SubtleButton from "../../Base/SubtleButton.svelte"; | ||||
|   import Loading from "../../Base/Loading.svelte"; | ||||
|   import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"; | ||||
|   import { Translation } from "../../i18n/Translation"; | ||||
|   import Constants from "../../../Models/Constants"; | ||||
|   import { Unit } from "../../../Models/Unit"; | ||||
|   import UserRelatedState from "../../../Logic/State/UserRelatedState"; | ||||
|   import { twJoin } from "tailwind-merge"; | ||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils"; | ||||
|   import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization" | ||||
|   import Tr from "../../Base/Tr.svelte" | ||||
|   import type { Feature } from "geojson" | ||||
|   import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import { TagsFilter } from "../../../Logic/Tags/TagsFilter" | ||||
|   import FreeformInput from "./FreeformInput.svelte" | ||||
|   import Translations from "../../i18n/Translations.js" | ||||
|   import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction" | ||||
|   import { createEventDispatcher, onDestroy } from "svelte" | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
|   import SpecialTranslation from "./SpecialTranslation.svelte" | ||||
|   import TagHint from "../TagHint.svelte" | ||||
|   import LoginToggle from "../../Base/LoginToggle.svelte" | ||||
|   import SubtleButton from "../../Base/SubtleButton.svelte" | ||||
|   import Loading from "../../Base/Loading.svelte" | ||||
|   import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte" | ||||
|   import { Translation } from "../../i18n/Translation" | ||||
|   import Constants from "../../../Models/Constants" | ||||
|   import { Unit } from "../../../Models/Unit" | ||||
|   import UserRelatedState from "../../../Logic/State/UserRelatedState" | ||||
|   import { twJoin } from "tailwind-merge" | ||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils" | ||||
| 
 | ||||
|   export let config: TagRenderingConfig; | ||||
|   export let tags: UIEventSource<Record<string, string>>; | ||||
|   export let selectedElement: Feature; | ||||
|   export let state: SpecialVisualizationState; | ||||
|   export let layer: LayerConfig | undefined; | ||||
|   export let config: TagRenderingConfig | ||||
|   export let tags: UIEventSource<Record<string, string>> | ||||
|   export let selectedElement: Feature | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let layer: LayerConfig | undefined | ||||
| 
 | ||||
|   let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined); | ||||
|   let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) | ||||
| 
 | ||||
|   let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)); | ||||
|   let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)) | ||||
| 
 | ||||
|   // Will be bound if a freeform is available | ||||
|   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]); | ||||
|   let selectedMapping: number = undefined; | ||||
|   let checkedMappings: boolean[]; | ||||
|   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]) | ||||
|   let selectedMapping: number = undefined | ||||
|   let checkedMappings: boolean[] | ||||
| 
 | ||||
|   /** | ||||
|    * Prepares and fills the checkedMappings | ||||
|  | @ -45,12 +45,12 @@ | |||
|   function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) { | ||||
|     mappings = confg.mappings?.filter((m) => { | ||||
|       if (typeof m.hideInAnswer === "boolean") { | ||||
|         return !m.hideInAnswer; | ||||
|         return !m.hideInAnswer | ||||
|       } | ||||
|       return !m.hideInAnswer.matchesProperties(tgs); | ||||
|     }); | ||||
|       return !m.hideInAnswer.matchesProperties(tgs) | ||||
|     }) | ||||
|     // We received a new config -> reinit | ||||
|     unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)); | ||||
|     unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)) | ||||
| 
 | ||||
|     if ( | ||||
|       confg.mappings?.length > 0 && | ||||
|  | @ -58,56 +58,56 @@ | |||
|       (checkedMappings === undefined || | ||||
|         checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0)) | ||||
|     ) { | ||||
|       const seenFreeforms = []; | ||||
|       TagUtils.FlattenMultiAnswer(); | ||||
|       const seenFreeforms = [] | ||||
|       TagUtils.FlattenMultiAnswer() | ||||
|       checkedMappings = [ | ||||
|         ...confg.mappings.map((mapping) => { | ||||
|           const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs); | ||||
|           const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs) | ||||
|           if (matches && confg.freeform) { | ||||
|             const newProps = TagUtils.changeAsProperties(mapping.if.asChange()); | ||||
|             seenFreeforms.push(newProps[confg.freeform.key]); | ||||
|             const newProps = TagUtils.changeAsProperties(mapping.if.asChange()) | ||||
|             seenFreeforms.push(newProps[confg.freeform.key]) | ||||
|           } | ||||
|           return matches; | ||||
|         }) | ||||
|       ]; | ||||
|           return matches | ||||
|         }), | ||||
|       ] | ||||
| 
 | ||||
|       if (tgs !== undefined && confg.freeform) { | ||||
|         const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []; | ||||
|         const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? [] | ||||
|         for (const seenFreeform of seenFreeforms) { | ||||
|           if (!seenFreeform) { | ||||
|             continue; | ||||
|             continue | ||||
|           } | ||||
|           const index = unseenFreeformValues.indexOf(seenFreeform); | ||||
|           const index = unseenFreeformValues.indexOf(seenFreeform) | ||||
|           if (index < 0) { | ||||
|             continue; | ||||
|             continue | ||||
|           } | ||||
|           unseenFreeformValues.splice(index, 1); | ||||
|           unseenFreeformValues.splice(index, 1) | ||||
|         } | ||||
|         // TODO this has _to much_ values | ||||
|         freeformInput.setData(unseenFreeformValues.join(";")); | ||||
|         checkedMappings.push(unseenFreeformValues.length > 0); | ||||
|         freeformInput.setData(unseenFreeformValues.join(";")) | ||||
|         checkedMappings.push(unseenFreeformValues.length > 0) | ||||
|       } | ||||
|     } | ||||
|     if (confg.freeform?.key) { | ||||
|       if (!confg.multiAnswer) { | ||||
|         // Somehow, setting multi-answer freeform values is broken if this is not set | ||||
|         freeformInput.setData(tgs[confg.freeform.key]); | ||||
|         freeformInput.setData(tgs[confg.freeform.key]) | ||||
|       } | ||||
|     } else { | ||||
|       freeformInput.setData(undefined); | ||||
|       freeformInput.setData(undefined) | ||||
|     } | ||||
|     feedback.setData(undefined); | ||||
|     feedback.setData(undefined) | ||||
|   } | ||||
| 
 | ||||
|   $: { | ||||
|     // Even though 'config' is not declared as a store, Svelte uses it as one to update the component | ||||
|     // We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes | ||||
|     initialize($tags, config); | ||||
|     initialize($tags, config) | ||||
|   } | ||||
|   export let selectedTags: TagsFilter = undefined; | ||||
|   export let selectedTags: TagsFilter = undefined | ||||
| 
 | ||||
|   let mappings: Mapping[] = config?.mappings; | ||||
|   let searchTerm: UIEventSource<string> = new UIEventSource(""); | ||||
|   let mappings: Mapping[] = config?.mappings | ||||
|   let searchTerm: UIEventSource<string> = new UIEventSource("") | ||||
| 
 | ||||
|   $: { | ||||
|     try { | ||||
|  | @ -116,10 +116,10 @@ | |||
|         selectedMapping, | ||||
|         checkedMappings, | ||||
|         tags.data | ||||
|       ); | ||||
|       ) | ||||
|     } catch (e) { | ||||
|       console.error("Could not calculate changeSpecification:", e); | ||||
|       selectedTags = undefined; | ||||
|       console.error("Could not calculate changeSpecification:", e) | ||||
|       selectedTags = undefined | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -128,55 +128,56 @@ | |||
|       config: TagRenderingConfig | ||||
|       applied: TagsFilter | ||||
|     } | ||||
|   }>(); | ||||
|   }>() | ||||
| 
 | ||||
|   function onSave() { | ||||
|     if (selectedTags === undefined) { | ||||
|       console.log("SelectedTags is undefined, ignoring 'onSave'-event") | ||||
|       return; | ||||
|       return | ||||
|     } | ||||
|     if (layer === undefined || layer?.source === null) { | ||||
|       /** | ||||
|        * This is a special, priviliged layer. | ||||
|        * We simply apply the tags onto the records | ||||
|        */ | ||||
|       const kv = selectedTags.asChange(tags.data); | ||||
|       const kv = selectedTags.asChange(tags.data) | ||||
|       for (const { k, v } of kv) { | ||||
|         if (v === undefined || v === "") { | ||||
|           delete tags.data[k]; | ||||
|           delete tags.data[k] | ||||
|         } else { | ||||
|           tags.data[k] = v; | ||||
|           tags.data[k] = v | ||||
|         } | ||||
|       } | ||||
|       tags.ping(); | ||||
|       return; | ||||
|       tags.ping() | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     dispatch("saved", { config, applied: selectedTags }); | ||||
|     dispatch("saved", { config, applied: selectedTags }) | ||||
|     const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, { | ||||
|       theme: state.layout.id, | ||||
|       changeType: "answer" | ||||
|     }); | ||||
|     freeformInput.setData(undefined); | ||||
|     selectedMapping = undefined; | ||||
|     selectedTags = undefined; | ||||
|       changeType: "answer", | ||||
|     }) | ||||
|     freeformInput.setData(undefined) | ||||
|     selectedMapping = undefined | ||||
|     selectedTags = undefined | ||||
| 
 | ||||
|     change | ||||
|       .CreateChangeDescriptions() | ||||
|       .then((changes) => state.changes.applyChanges(changes)) | ||||
|       .catch(console.error); | ||||
|       .catch(console.error) | ||||
|   } | ||||
| 
 | ||||
|   let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false); | ||||
|   let featureSwitchIsDebugging = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false); | ||||
|   let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined); | ||||
|   let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0; | ||||
|   let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false) | ||||
|   let featureSwitchIsDebugging = | ||||
|     state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false) | ||||
|   let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined) | ||||
|   let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0 | ||||
|   if (state?.osmConnection) { | ||||
|     onDestroy( | ||||
|       state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { | ||||
|         numberOfCs = ud.csCount; | ||||
|         numberOfCs = ud.csCount | ||||
|       }) | ||||
|     ); | ||||
|     ) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
|  | @ -249,7 +250,9 @@ | |||
|               bind:group={selectedMapping} | ||||
|               name={"mappings-radio-" + config.id} | ||||
|               value={i} | ||||
|               on:keypress={e => {if(e.key === "Enter") onSave()}} | ||||
|               on:keypress={(e) => { | ||||
|                 if (e.key === "Enter") onSave() | ||||
|               }} | ||||
|             /> | ||||
|           </TagRenderingMappingInput> | ||||
|         {/each} | ||||
|  |  | |||
|  | @ -1,44 +1,48 @@ | |||
| <script lang="ts"> | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import type { | ||||
|     QuestionableTagRenderingConfigJson | ||||
|   } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte" | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
| 
 | ||||
|   export let schema: ConfigMeta; | ||||
|   export let state: EditLayerState; | ||||
|   export let path: (string | number)[] = []; | ||||
|   export let schema: ConfigMeta | ||||
|   export let state: EditLayerState | ||||
|   export let path: (string | number)[] = [] | ||||
| 
 | ||||
|   const configJson: QuestionableTagRenderingConfigJson = { | ||||
|     mappings: schema.hints.suggestions, | ||||
|     multiAnswer: true, | ||||
|     id: "multi_anwser_"+path.join("_"), | ||||
|     question: schema.hints.question | ||||
|     id: "multi_anwser_" + path.join("_"), | ||||
|     question: schema.hints.question, | ||||
|   } | ||||
|   const tags = new UIEventSource({}) | ||||
| 
 | ||||
|   { | ||||
|     // Setting the initial value | ||||
|     const v = <string[]> state.getCurrentValueFor(path) | ||||
|     if(v && v.length > 0){ | ||||
|       tags.setData({value: v.join(";")}) | ||||
|     const v = <string[]>state.getCurrentValueFor(path) | ||||
|     if (v && v.length > 0) { | ||||
|       tags.setData({ value: v.join(";") }) | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   tags.addCallbackD(tags => { | ||||
| 
 | ||||
|   tags.addCallbackD((tags) => { | ||||
|     const values = tags["value"]?.split(";") | ||||
|     if(!values){ | ||||
|     if (!values) { | ||||
|       return | ||||
|     } | ||||
|     state.setValueAt(path, values) | ||||
|   }) | ||||
|    | ||||
| 
 | ||||
|   const config = new TagRenderingConfig(configJson) | ||||
| </script> | ||||
| 
 | ||||
| <div> | ||||
|   <TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} /> | ||||
|   <TagRenderingEditable | ||||
|     {config} | ||||
|     selectedElement={undefined} | ||||
|     showQuestionIfUnknown={true} | ||||
|     {state} | ||||
|     {tags} | ||||
|   /> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,18 +1,17 @@ | |||
| <script lang="ts"> | ||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection"; | ||||
|   import EditItemButton from "./EditItemButton.svelte"; | ||||
| 
 | ||||
|   export let layerIds: { id: string, owner: number }[]; | ||||
|   export let category: "layers" | "themes" = "layers"; | ||||
|   export let osmConnection: OsmConnection; | ||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
|   import EditItemButton from "./EditItemButton.svelte" | ||||
| 
 | ||||
|   export let layerIds: { id: string; owner: number }[] | ||||
|   export let category: "layers" | "themes" = "layers" | ||||
|   export let osmConnection: OsmConnection | ||||
| </script> | ||||
| 
 | ||||
| {#if layerIds.length > 0} | ||||
|   <slot name="title" /> | ||||
|   <div class="flex flex-wrap"> | ||||
|     {#each Array.from(layerIds) as layer} | ||||
|       <EditItemButton info={layer} {category} {osmConnection} on:layerSelected/> | ||||
|       <EditItemButton info={layer} {category} {osmConnection} on:layerSelected /> | ||||
|     {/each} | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,35 +1,36 @@ | |||
| <script lang="ts"> | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection"; | ||||
|   import Marker from "../Map/Marker.svelte"; | ||||
|   import NextButton from "../Base/NextButton.svelte"; | ||||
|   import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"; | ||||
|   import { AllSharedLayers } from "../../Customizations/AllSharedLayers"; | ||||
|   import { createEventDispatcher } from "svelte"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
|   import Marker from "../Map/Marker.svelte" | ||||
|   import NextButton from "../Base/NextButton.svelte" | ||||
|   import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts" | ||||
|   import { AllSharedLayers } from "../../Customizations/AllSharedLayers" | ||||
|   import { createEventDispatcher } from "svelte" | ||||
| 
 | ||||
|   export let info: { id: string, owner: number }; | ||||
|   export let category: "layers" | "themes"; | ||||
|   export let osmConnection: OsmConnection; | ||||
|   export let info: { id: string; owner: number } | ||||
|   export let category: "layers" | "themes" | ||||
|   export let osmConnection: OsmConnection | ||||
| 
 | ||||
|   let displayName = UIEventSource.FromPromise(osmConnection.getInformationAboutUser(info.owner)).mapD(response => response.display_name); | ||||
|   let displayName = UIEventSource.FromPromise( | ||||
|     osmConnection.getInformationAboutUser(info.owner) | ||||
|   ).mapD((response) => response.display_name) | ||||
| 
 | ||||
|   let selfId = osmConnection.userDetails.mapD(ud => ud.uid) | ||||
|   let selfId = osmConnection.userDetails.mapD((ud) => ud.uid) | ||||
|   function fetchIconDescription(layerId): any { | ||||
|     if (category === "themes") { | ||||
|       return AllKnownLayouts.allKnownLayouts.get(layerId).icon; | ||||
|       return AllKnownLayouts.allKnownLayouts.get(layerId).icon | ||||
|     } | ||||
|     return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon; | ||||
|     return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon | ||||
|   } | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ layerSelected: string }>(); | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ layerSelected: string }>() | ||||
| </script> | ||||
| 
 | ||||
| <NextButton clss="small" on:click={() => dispatch("layerSelected", info)}> | ||||
|   <div class="w-4 h-4 mr-1"> | ||||
|   <div class="mr-1 h-4 w-4"> | ||||
|     <Marker icons={fetchIconDescription(info.id)} /> | ||||
|   </div> | ||||
|   <b class="px-1">  {info.id}</b> | ||||
|   <b class="px-1">{info.id}</b> | ||||
|   {#if info.owner && info.owner !== $selfId} | ||||
|     (made by {$displayName ?? info.owner}) | ||||
|   {/if} | ||||
|  |  | |||
|  | @ -1,81 +1,81 @@ | |||
| <script lang="ts"> | ||||
|   import type { HighlightedTagRendering } from "./EditLayerState"; | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json"; | ||||
|   import Region from "./Region.svelte"; | ||||
|   import TabbedGroup from "../Base/TabbedGroup.svelte"; | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import { Utils } from "../../Utils"; | ||||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; | ||||
|   import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"; | ||||
|   import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte"; | ||||
|   import FloatOver from "../Base/FloatOver.svelte"; | ||||
|   import TagRenderingInput from "./TagRenderingInput.svelte"; | ||||
|   import FromHtml from "../Base/FromHtml.svelte"; | ||||
|   import AllTagsPanel from "../Popup/AllTagsPanel.svelte"; | ||||
|   import QuestionPreview from "./QuestionPreview.svelte"; | ||||
|   import ShowConversionMessages from "./ShowConversionMessages.svelte"; | ||||
|   import type { HighlightedTagRendering } from "./EditLayerState" | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json" | ||||
|   import Region from "./Region.svelte" | ||||
|   import TabbedGroup from "../Base/TabbedGroup.svelte" | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import { Utils } from "../../Utils" | ||||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion" | ||||
|   import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte" | ||||
|   import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte" | ||||
|   import FloatOver from "../Base/FloatOver.svelte" | ||||
|   import TagRenderingInput from "./TagRenderingInput.svelte" | ||||
|   import FromHtml from "../Base/FromHtml.svelte" | ||||
|   import AllTagsPanel from "../Popup/AllTagsPanel.svelte" | ||||
|   import QuestionPreview from "./QuestionPreview.svelte" | ||||
|   import ShowConversionMessages from "./ShowConversionMessages.svelte" | ||||
| 
 | ||||
|   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; | ||||
|   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   let messages = state.messages; | ||||
|   let hasErrors = messages.mapD((m: ConversionMessage[]) => m.filter(m => m.level === "error").length); | ||||
|   const configuration = state.configuration; | ||||
|   export let state: EditLayerState | ||||
|   let messages = state.messages | ||||
|   let hasErrors = messages.mapD( | ||||
|     (m: ConversionMessage[]) => m.filter((m) => m.level === "error").length | ||||
|   ) | ||||
|   const configuration = state.configuration | ||||
| 
 | ||||
|   const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group)); | ||||
|   const allNames = Utils.Dedup(layerSchema.map((meta) => meta.hints.group)) | ||||
| 
 | ||||
|   const perRegion: Record<string, ConfigMeta[]> = {}; | ||||
|   const perRegion: Record<string, ConfigMeta[]> = {} | ||||
|   for (const region of allNames) { | ||||
|     perRegion[region] = layerSchema.filter(meta => meta.hints.group === region); | ||||
|     perRegion[region] = layerSchema.filter((meta) => meta.hints.group === region) | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   let title: Store<string> = state.getStoreFor(["id"]); | ||||
|   const wl = window.location; | ||||
|   const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout="; | ||||
|   let title: Store<string> = state.getStoreFor(["id"]) | ||||
|   const wl = window.location | ||||
|   const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=" | ||||
| 
 | ||||
|   function firstPathsFor(...regionNames: string[]): Set<string> { | ||||
|     const pathNames = new Set<string>(); | ||||
|     const pathNames = new Set<string>() | ||||
|     for (const regionName of regionNames) { | ||||
|       const region: ConfigMeta[] = perRegion[regionName]; | ||||
|       const region: ConfigMeta[] = perRegion[regionName] | ||||
|       for (const configMeta of region) { | ||||
|         pathNames.add(configMeta.path[0]); | ||||
|         pathNames.add(configMeta.path[0]) | ||||
|       } | ||||
|     } | ||||
|     return pathNames; | ||||
| 
 | ||||
|     return pathNames | ||||
|   } | ||||
| 
 | ||||
|   function configForRequiredField(id: string): ConfigMeta { | ||||
|     let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id); | ||||
|     config = Utils.Clone(config); | ||||
|     config.required = true; | ||||
|     config.hints.ifunset = undefined; | ||||
|     return config; | ||||
|     let config = layerSchema.find((config) => config.path.length === 1 && config.path[0] === id) | ||||
|     config = Utils.Clone(config) | ||||
|     config.required = true | ||||
|     config.hints.ifunset = undefined | ||||
|     return config | ||||
|   } | ||||
| 
 | ||||
|   let requiredFields = ["id", "name", "description", "source"]; | ||||
|   let currentlyMissing = state.configuration.map(config => { | ||||
|   let requiredFields = ["id", "name", "description", "source"] | ||||
|   let currentlyMissing = state.configuration.map((config) => { | ||||
|     if (!config) { | ||||
|       return []; | ||||
|       return [] | ||||
|     } | ||||
|     const missing = []; | ||||
|     const missing = [] | ||||
|     for (const requiredField of requiredFields) { | ||||
|       if (!config[requiredField]) { | ||||
|         missing.push(requiredField); | ||||
|         missing.push(requiredField) | ||||
|       } | ||||
|     } | ||||
|     return missing; | ||||
|   }); | ||||
|     return missing | ||||
|   }) | ||||
| 
 | ||||
|   let highlightedItem: UIEventSource<HighlightedTagRendering> = state.highlightedItem; | ||||
|   let highlightedItem: UIEventSource<HighlightedTagRendering> = state.highlightedItem | ||||
| </script> | ||||
| <div class="h-screen flex flex-col"> | ||||
| 
 | ||||
|   <div class="w-full flex justify-between my-2"> | ||||
| <div class="flex h-screen flex-col"> | ||||
|   <div class="my-2 flex w-full justify-between"> | ||||
|     <slot /> | ||||
|     {#if $title === undefined} | ||||
|       <h3>Creating a new layer</h3> | ||||
|  | @ -83,11 +83,17 @@ | |||
|       <h3>Editing layer {$title}</h3> | ||||
|     {/if} | ||||
|     {#if $currentlyMissing.length > 0} | ||||
|       <div class="w-16"/> <!-- Empty div, simply hide this --> | ||||
|       <div class="w-16" /> | ||||
|       <!-- Empty div, simply hide this --> | ||||
|     {:else if $hasErrors > 0} | ||||
|       <div class="alert">{$hasErrors} errors detected</div> | ||||
|     {:else} | ||||
|       <a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener"> | ||||
|       <a | ||||
|         class="primary button" | ||||
|         href={baseUrl + state.server.layerUrl(title.data)} | ||||
|         target="_blank" | ||||
|         rel="noopener" | ||||
|       > | ||||
|         Try it out | ||||
|         <ChevronRightIcon class="h-6 w-6 shrink-0" /> | ||||
|       </a> | ||||
|  | @ -95,29 +101,29 @@ | |||
|   </div> | ||||
| 
 | ||||
|   {#if $currentlyMissing.length > 0} | ||||
| 
 | ||||
|     {#each requiredFields as required} | ||||
|       <SchemaBasedInput {state} | ||||
|                         schema={configForRequiredField(required)} | ||||
|                         path={[required]} /> | ||||
|       <SchemaBasedInput {state} schema={configForRequiredField(required)} path={[required]} /> | ||||
|     {/each} | ||||
|   {:else} | ||||
|     <div class="m4 h-full overflow-y-auto"> | ||||
|       <TabbedGroup> | ||||
|         <div slot="title0" class="flex">General properties | ||||
|         <div slot="title0" class="flex"> | ||||
|           General properties | ||||
|           <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} /> | ||||
|         </div> | ||||
|         <div class="flex flex-col" slot="content0"> | ||||
|           <Region {state} configs={perRegion["Basic"]} /> | ||||
| 
 | ||||
|         </div> | ||||
| 
 | ||||
| 
 | ||||
|         <div slot="title1" class="flex">Information panel (questions and answers) | ||||
|           <ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /> | ||||
|         <div slot="title1" class="flex"> | ||||
|           Information panel (questions and answers) | ||||
|           <ErrorIndicatorForRegion | ||||
|             firstPaths={firstPathsFor("title", "tagrenderings", "editing")} | ||||
|             {state} | ||||
|           /> | ||||
|         </div> | ||||
|         <div slot="content1"> | ||||
|           <QuestionPreview path={["title"]} {state} schema={perRegion["title"][0]}></QuestionPreview> | ||||
|           <QuestionPreview path={["title"]} {state} schema={perRegion["title"][0]} /> | ||||
|           <Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" /> | ||||
|           <Region configs={perRegion["editing"]} {state} title="Other editing elements" /> | ||||
|         </div> | ||||
|  | @ -131,16 +137,21 @@ | |||
|           <Region {state} configs={perRegion["presets"]} /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div slot="title3" class="flex">Rendering on the map | ||||
|           <ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} /> | ||||
|         <div slot="title3" class="flex"> | ||||
|           Rendering on the map | ||||
|           <ErrorIndicatorForRegion | ||||
|             firstPaths={firstPathsFor("linerendering", "pointrendering")} | ||||
|             {state} | ||||
|           /> | ||||
|         </div> | ||||
|         <div slot="content3"> | ||||
|           <Region configs={perRegion["linerendering"]} {state} /> | ||||
|           <Region configs={perRegion["pointrendering"]} {state} /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div slot="title4" class="flex">Advanced functionality | ||||
|           <ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /> | ||||
|         <div slot="title4" class="flex"> | ||||
|           Advanced functionality | ||||
|           <ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced", "expert")} {state} /> | ||||
|         </div> | ||||
|         <div slot="content4"> | ||||
|           <Region configs={perRegion["advanced"]} {state} /> | ||||
|  | @ -149,30 +160,33 @@ | |||
|         <div slot="title5">Configuration file</div> | ||||
|         <div slot="content5"> | ||||
|           <div> | ||||
|             Below, you'll find the raw configuration file in `.json`-format. | ||||
|             This is mostly for debugging purposes | ||||
|             Below, you'll find the raw configuration file in `.json`-format. This is mostly for | ||||
|             debugging purposes | ||||
|           </div> | ||||
|           <div class="literal-code"> | ||||
|             <FromHtml src={JSON.stringify($configuration, null, "  ").replaceAll("\n","</br>")} /> | ||||
|             <FromHtml src={JSON.stringify($configuration, null, "  ").replaceAll("\n", "</br>")} /> | ||||
|           </div> | ||||
| 
 | ||||
|           <ShowConversionMessages messages={$messages} /> | ||||
|           <div> | ||||
|             The testobject (which is used to render the questions in the 'information panel' item has the following | ||||
|             tags: | ||||
|             The testobject (which is used to render the questions in the 'information panel' item | ||||
|             has the following tags: | ||||
|           </div> | ||||
| 
 | ||||
|           <AllTagsPanel tags={state.testTags}></AllTagsPanel> | ||||
|           <AllTagsPanel tags={state.testTags} /> | ||||
|         </div> | ||||
|       </TabbedGroup> | ||||
|     </div> | ||||
|     {#if $highlightedItem !== undefined} | ||||
|       <FloatOver on:close={() => highlightedItem.setData(undefined)}> | ||||
|         <div> | ||||
|           <TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} /> | ||||
|           <TagRenderingInput | ||||
|             path={$highlightedItem.path} | ||||
|             {state} | ||||
|             schema={$highlightedItem.schema} | ||||
|           /> | ||||
|         </div> | ||||
|       </FloatOver> | ||||
|     {/if} | ||||
| 
 | ||||
|   {/if} | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,37 +1,45 @@ | |||
| <script lang="ts"> | ||||
|   import { EditThemeState } from "./EditLayerState"; | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; | ||||
|   import TabbedGroup from "../Base/TabbedGroup.svelte"; | ||||
|   import ShowConversionMessages from "./ShowConversionMessages.svelte"; | ||||
|   import Region from "./Region.svelte"; | ||||
|   import { EditThemeState } from "./EditLayerState" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion" | ||||
|   import TabbedGroup from "../Base/TabbedGroup.svelte" | ||||
|   import ShowConversionMessages from "./ShowConversionMessages.svelte" | ||||
|   import Region from "./Region.svelte" | ||||
| 
 | ||||
|   export let state: EditThemeState; | ||||
|   let schema: ConfigMeta[] = state.schema.filter(schema => schema.path.length > 0); | ||||
|   let config = state.configuration; | ||||
|   let messages = state.messages; | ||||
|   let hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length); | ||||
|   let title = state.getStoreFor(["id"]); | ||||
|   const wl = window.location; | ||||
|   const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout="; | ||||
|   export let state: EditThemeState | ||||
|   let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0) | ||||
|   let config = state.configuration | ||||
|   let messages = state.messages | ||||
|   let hasErrors = messages.map( | ||||
|     (m: ConversionMessage[]) => m.filter((m) => m.level === "error").length | ||||
|   ) | ||||
|   let title = state.getStoreFor(["id"]) | ||||
|   const wl = window.location | ||||
|   const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=" | ||||
| 
 | ||||
|   const perRegion: Record<string, ConfigMeta[]> = {}; | ||||
|   const perRegion: Record<string, ConfigMeta[]> = {} | ||||
|   for (const schemaElement of schema) { | ||||
|     const key = schemaElement.hints.group ?? "no-group"; | ||||
|     const list = perRegion[key] ?? (perRegion[key] = []); | ||||
|     list.push(schemaElement); | ||||
|     const key = schemaElement.hints.group ?? "no-group" | ||||
|     const list = perRegion[key] ?? (perRegion[key] = []) | ||||
|     list.push(schemaElement) | ||||
|   } | ||||
| console.log({perRegion, schema}) | ||||
|   console.log({ perRegion, schema }) | ||||
| </script> | ||||
| <div class="flex flex-col h-screen"> | ||||
|   <div class="w-full flex justify-between my-2"> | ||||
| 
 | ||||
| <div class="flex h-screen flex-col"> | ||||
|   <div class="my-2 flex w-full justify-between"> | ||||
|     <slot /> | ||||
|     <h3>Editing theme {$title}</h3> | ||||
|     {#if $hasErrors > 0} | ||||
|       <div class="alert">{$hasErrors} errors detected</div> | ||||
|     {:else} | ||||
|       <a class="primary button" href={baseUrl+state.server.urlFor($title, "themes")} target="_blank" rel="noopener"> | ||||
|       <a | ||||
|         class="primary button" | ||||
|         href={baseUrl + state.server.urlFor($title, "themes")} | ||||
|         target="_blank" | ||||
|         rel="noopener" | ||||
|       > | ||||
|         Try it out | ||||
|         <ChevronRightIcon class="h-6 w-6 shrink-0" /> | ||||
|       </a> | ||||
|  | @ -43,36 +51,32 @@ console.log({perRegion, schema}) | |||
|     <TabbedGroup> | ||||
|       <div slot="title0">Basic properties</div> | ||||
|       <div slot="content0"> | ||||
|         <Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties"/> | ||||
|         <Region configs={perRegion["start_location"]} path={[]} {state} title="Start location"/> | ||||
|          | ||||
|         <Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties" /> | ||||
|         <Region configs={perRegion["start_location"]} path={[]} {state} title="Start location" /> | ||||
|       </div> | ||||
|        | ||||
| 
 | ||||
|       <div slot="title1">Layers</div> | ||||
|       <div slot="content1"> | ||||
|         <Region configs={perRegion["layers"]} path={[]} {state} /> | ||||
|          | ||||
|       </div> | ||||
|       <div slot="title2">Feature switches</div> | ||||
|       <div slot="content2"> | ||||
|         <Region configs={perRegion["feature_switches"]} path={[]} {state}></Region> | ||||
|         <Region configs={perRegion["feature_switches"]} path={[]} {state} /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div slot="title3">Advanced options</div> | ||||
|       <div slot="content3"> | ||||
|         <Region configs={perRegion["advanced"]} path={[]} {state}></Region> | ||||
|         <Region configs={perRegion["advanced"]} path={[]} {state} /> | ||||
|       </div> | ||||
|        | ||||
| 
 | ||||
|       <div slot="title4">Configuration file</div> | ||||
|       <div slot="content4"> | ||||
|         <div class="literal-code"> | ||||
|           {JSON.stringify($config)} | ||||
|         </div> | ||||
| 
 | ||||
|         <ShowConversionMessages messages={$messages}></ShowConversionMessages> | ||||
| 
 | ||||
|         <ShowConversionMessages messages={$messages} /> | ||||
|       </div> | ||||
|     </TabbedGroup> | ||||
|   </div> | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,19 +1,21 @@ | |||
| <script lang="ts"> | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
| 
 | ||||
|   export let firstPaths: Set<string>; | ||||
|   export let state: EditLayerState; | ||||
|   let messagesCount = state.messages.map(msgs => msgs.filter(msg => { | ||||
|     const pth = msg.context.path | ||||
|     return firstPaths.has(pth[0]) || (pth.length > 1 && firstPaths.has(pth[1])); | ||||
|   }).length); | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
| 
 | ||||
|   export let firstPaths: Set<string> | ||||
|   export let state: EditLayerState | ||||
|   let messagesCount = state.messages.map( | ||||
|     (msgs) => | ||||
|       msgs.filter((msg) => { | ||||
|         const pth = msg.context.path | ||||
|         return firstPaths.has(pth[0]) || (pth.length > 1 && firstPaths.has(pth[1])) | ||||
|       }).length | ||||
|   ) | ||||
| </script> | ||||
| 
 | ||||
| {#if $messagesCount > 0} | ||||
|   <span class="alert flex w-min"> | ||||
|     <ExclamationIcon class="w-6 h-6" /> | ||||
|     <ExclamationIcon class="h-6 w-6" /> | ||||
|     {$messagesCount} | ||||
|   </span> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,56 +1,62 @@ | |||
| <script lang="ts"> | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
|   import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||
|   import FromHtml from "../Base/FromHtml.svelte" | ||||
|   import { PencilIcon } from "@rgossiaux/svelte-heroicons/outline" | ||||
|   import Region from "./Region.svelte" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import configs from "../../assets/schemas/questionabletagrenderingconfigmeta.json" | ||||
|   import { Utils } from "../../Utils" | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte" | ||||
|   import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| 
 | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"; | ||||
|   import { TagUtils } from "../../Logic/Tags/TagUtils"; | ||||
|   import FromHtml from "../Base/FromHtml.svelte"; | ||||
|   import { PencilIcon } from "@rgossiaux/svelte-heroicons/outline"; | ||||
|   import Region from "./Region.svelte"; | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import configs from "../../assets/schemas/questionabletagrenderingconfigmeta.json"; | ||||
|   import { Utils } from "../../Utils"; | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte"; | ||||
|   import { VariableUiElement } from "../Base/VariableUIElement"; | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   export let path: (string | number)[]; | ||||
|   let tag: UIEventSource<TagConfigJson> = state.getStoreFor([...path, "if"]); | ||||
|   let parsedTag = tag.map(t => t ? TagUtils.Tag(t) : undefined); | ||||
|   let exampleTags = parsedTag.map(pt => { | ||||
|   export let state: EditLayerState | ||||
|   export let path: (string | number)[] | ||||
|   let tag: UIEventSource<TagConfigJson> = state.getStoreFor([...path, "if"]) | ||||
|   let parsedTag = tag.map((t) => (t ? TagUtils.Tag(t) : undefined)) | ||||
|   let exampleTags = parsedTag.map((pt) => { | ||||
|     if (!pt) { | ||||
|       return {}; | ||||
|       return {} | ||||
|     } | ||||
|     const keys = pt.usedKeys(); | ||||
|     const o = {}; | ||||
|     const keys = pt.usedKeys() | ||||
|     const o = {} | ||||
|     for (const key of keys) { | ||||
|       o[key] = "value"; | ||||
|       o[key] = "value" | ||||
|     } | ||||
|     return o; | ||||
|   }); | ||||
|   let uploadableOnly: boolean = true; | ||||
|     return o | ||||
|   }) | ||||
|   let uploadableOnly: boolean = true | ||||
| 
 | ||||
|   let thenText: UIEventSource<Record<string, string>> = state.getStoreFor([...path, "then"]) | ||||
|   let thenTextEn = thenText   .mapD(translation => typeof translation === "string" ? translation : translation["en"] ) | ||||
|   let editMode = Object.keys($thenText ?? {})?.length === 0; | ||||
|   let thenTextEn = thenText.mapD((translation) => | ||||
|     typeof translation === "string" ? translation : translation["en"] | ||||
|   ) | ||||
|   let editMode = Object.keys($thenText ?? {})?.length === 0 | ||||
| 
 | ||||
|   let mappingConfigs: ConfigMeta[] = configs.filter(c => c.path[0] === "mappings") | ||||
|     .map(c => <ConfigMeta>Utils.Clone(c)) | ||||
|     .map(c => { | ||||
|       c.path.splice(0, 1); | ||||
|       return c; | ||||
|   let mappingConfigs: ConfigMeta[] = configs | ||||
|     .filter((c) => c.path[0] === "mappings") | ||||
|     .map((c) => <ConfigMeta>Utils.Clone(c)) | ||||
|     .map((c) => { | ||||
|       c.path.splice(0, 1) | ||||
|       return c | ||||
|     }) | ||||
|     .filter(c => c.path.length == 1 && c.hints.group !== "hidden"); | ||||
|     .filter((c) => c.path.length == 1 && c.hints.group !== "hidden") | ||||
| </script> | ||||
| 
 | ||||
| <button on:click={() => {editMode = !editMode}}> | ||||
|   <PencilIcon class="w-6 h-6" /> | ||||
| <button | ||||
|   on:click={() => { | ||||
|     editMode = !editMode | ||||
|   }} | ||||
| > | ||||
|   <PencilIcon class="h-6 w-6" /> | ||||
| </button> | ||||
| 
 | ||||
| {#if editMode} | ||||
|   <div class="flex justify-between items-start w-full"> | ||||
|     <div class="flex flex-col w-full"> | ||||
|       <Region {state} configs={mappingConfigs} path={path} /> | ||||
|   <div class="flex w-full items-start justify-between"> | ||||
|     <div class="flex w-full flex-col"> | ||||
|       <Region {state} configs={mappingConfigs} {path} /> | ||||
|     </div> | ||||
| 
 | ||||
|     <slot name="delete" /> | ||||
|  | @ -64,7 +70,6 @@ | |||
|     {:else} | ||||
|       <i>No then is set</i> | ||||
|     {/if} | ||||
|     <FromHtml src={ $parsedTag?.asHumanString(false, false, $exampleTags)} /> | ||||
|     <FromHtml src={$parsedTag?.asHumanString(false, false, $exampleTags)} /> | ||||
|   </div> | ||||
| {/if} | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,86 +1,82 @@ | |||
| <script lang="ts"> | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import * as questions from "../../assets/generated/layers/questions.json"; | ||||
|   import { ImmutableStore, Store } from "../../Logic/UIEventSource"; | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import nmd from "nano-markdown"; | ||||
|   import type { | ||||
|     QuestionableTagRenderingConfigJson | ||||
|   } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"; | ||||
|   import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"; | ||||
|   import FromHtml from "../Base/FromHtml.svelte"; | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte"; | ||||
|   import NextButton from "../Base/NextButton.svelte"; | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import * as questions from "../../assets/generated/layers/questions.json" | ||||
|   import { ImmutableStore, Store } from "../../Logic/UIEventSource" | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte" | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import nmd from "nano-markdown" | ||||
|   import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js" | ||||
|   import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson" | ||||
|   import FromHtml from "../Base/FromHtml.svelte" | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte" | ||||
|   import NextButton from "../Base/NextButton.svelte" | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   export let path: ReadonlyArray<string | number>; | ||||
|   export let schema: ConfigMeta; | ||||
|   let value = state.getStoreFor(path); | ||||
|   export let state: EditLayerState | ||||
|   export let path: ReadonlyArray<string | number> | ||||
|   export let schema: ConfigMeta | ||||
|   let value = state.getStoreFor(path) | ||||
| 
 | ||||
|   let perId: Record<string, TagRenderingConfigJson[]> = {}; | ||||
|   let perId: Record<string, TagRenderingConfigJson[]> = {} | ||||
|   for (let tagRendering of questions.tagRenderings) { | ||||
|     if (tagRendering.labels) { | ||||
|       for (let label of tagRendering.labels) { | ||||
|         perId[label] = (perId[label] ?? []).concat(tagRendering); | ||||
|         perId[label] = (perId[label] ?? []).concat(tagRendering) | ||||
|       } | ||||
|     } | ||||
|     perId[tagRendering.id] = [tagRendering]; | ||||
|     perId[tagRendering.id] = [tagRendering] | ||||
|   } | ||||
| 
 | ||||
|   let configJson: Store<QuestionableTagRenderingConfigJson[]> = value.map(x => { | ||||
|   let configJson: Store<QuestionableTagRenderingConfigJson[]> = value.map((x) => { | ||||
|     if (typeof x === "string") { | ||||
|       return perId[x]; | ||||
|       return perId[x] | ||||
|     } else { | ||||
|       return [x]; | ||||
|       return [x] | ||||
|     } | ||||
|   }); | ||||
|   let configs: Store<TagRenderingConfig[]> = configJson.map(configs => { | ||||
|   }) | ||||
|   let configs: Store<TagRenderingConfig[]> = configJson.map((configs) => { | ||||
|     if (!configs) { | ||||
|       return [{ error: "No configuartions found" }]; | ||||
|       return [{ error: "No configuartions found" }] | ||||
|     } | ||||
|     console.log("Regenerating configs"); | ||||
|     return configs.map(config => { | ||||
|     console.log("Regenerating configs") | ||||
|     return configs.map((config) => { | ||||
|       try { | ||||
|         return new TagRenderingConfig(config); | ||||
|         return new TagRenderingConfig(config) | ||||
|       } catch (e) { | ||||
|         return { error: e }; | ||||
|         return { error: e } | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
|   let id: Store<string> = value.mapD(c => { | ||||
|     }) | ||||
|   }) | ||||
|   let id: Store<string> = value.mapD((c) => { | ||||
|     if (c?.id) { | ||||
|       return c.id; | ||||
|       return c.id | ||||
|     } | ||||
|     if (typeof c === "string") { | ||||
|       return c; | ||||
|       return c | ||||
|     } | ||||
|     return undefined; | ||||
|   }); | ||||
|     return undefined | ||||
|   }) | ||||
| 
 | ||||
|   let tags = state.testTags; | ||||
|   let tags = state.testTags | ||||
| 
 | ||||
|   let messages = state.messagesFor(path); | ||||
|   let messages = state.messagesFor(path) | ||||
| 
 | ||||
|   let description = schema.description; | ||||
|   let description = schema.description | ||||
|   if (description) { | ||||
|     try { | ||||
|       description = nmd(description); | ||||
|       description = nmd(description) | ||||
|     } catch (e) { | ||||
|       console.error("Could not convert description to markdown", { description }); | ||||
|       console.error("Could not convert description to markdown", { description }) | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex"> | ||||
| 
 | ||||
|   <div class="flex flex-col interactive border-interactive m-4 w-full"> | ||||
| 
 | ||||
|   <div class="interactive border-interactive m-4 flex w-full flex-col"> | ||||
|     {#if $id} | ||||
|       TagRendering {$id} | ||||
|     {/if} | ||||
|     <NextButton clss="primary" on:click={() => state.highlightedItem.setData({path, schema})}> | ||||
|     <NextButton clss="primary" on:click={() => state.highlightedItem.setData({ path, schema })}> | ||||
|       {#if schema.hints.question} | ||||
|         {schema.hints.question} | ||||
|       {/if} | ||||
|  | @ -92,40 +88,40 @@ | |||
|       <ShowConversionMessage {message} /> | ||||
|     {/each} | ||||
| 
 | ||||
|     <slot class="self-end my-4"></slot> | ||||
| 
 | ||||
| 
 | ||||
|     <slot class="my-4 self-end" /> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="flex flex-col w-full m-4"> | ||||
|   <div class="m-4 flex w-full flex-col"> | ||||
|     <h3>Preview of this question</h3> | ||||
|     {#each $configs as config} | ||||
|       {#if config.error !== undefined} | ||||
|         <div class="alert">Could not create a preview of this tagRendering: {config.error}</div> | ||||
|       {:else if config.condition && !config.condition.matchesProperties($tags)} | ||||
|         This tagRendering is currently not shown. It will appear if the feature matches the | ||||
|         condition | ||||
|         <b> | ||||
|           <FromHtml src={config.condition.asHumanString(true, false, {})} /> | ||||
|         </b> | ||||
| 
 | ||||
|         Try to answer the relevant question above | ||||
|       {:else if config.metacondition && !config.metacondition.matchesProperties($tags)} | ||||
|         This tagRendering is currently not shown. It will appear if the feature matches the | ||||
|         metacondition | ||||
|         <b> | ||||
|           <FromHtml src={config.metacondition.asHumanString(true, false, {})} /> | ||||
|         </b> | ||||
|         For a breakdown of usable meta conditions, go to a mapcomplete theme > settings and enable debug-data. | ||||
|         The meta-tags will appear at the bottom | ||||
|       {:else} | ||||
|         {#if config.condition && !config.condition.matchesProperties($tags)} | ||||
|           This tagRendering is currently not shown. It will appear if the feature matches the condition | ||||
|           <b> | ||||
|             <FromHtml src={config.condition.asHumanString(true, false, {})} /> | ||||
|           </b> | ||||
|            | ||||
|           Try to answer the relevant question above | ||||
|           {:else if config.metacondition && !config.metacondition.matchesProperties($tags)} | ||||
|             This tagRendering is currently not shown. It will appear if the feature matches the metacondition | ||||
|             <b> | ||||
|               <FromHtml src={config.metacondition.asHumanString(true, false, {})} /> | ||||
|             </b> | ||||
|           For a breakdown of usable meta conditions, go to a mapcomplete theme > settings and enable debug-data. The meta-tags will appear at the bottom  | ||||
|         {:else} | ||||
|           <TagRenderingEditable | ||||
|             selectedElement={state.exampleFeature} | ||||
|             config={config} editingEnabled={new ImmutableStore(true)} showQuestionIfUnknown={true} | ||||
|             {state} | ||||
|             {tags}></TagRenderingEditable> | ||||
|         {/if} | ||||
|         <TagRenderingEditable | ||||
|           selectedElement={state.exampleFeature} | ||||
|           {config} | ||||
|           editingEnabled={new ImmutableStore(true)} | ||||
|           showQuestionIfUnknown={true} | ||||
|           {state} | ||||
|           {tags} | ||||
|         /> | ||||
|       {/if} | ||||
|     {/each} | ||||
|   </div> | ||||
| 
 | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,30 +1,33 @@ | |||
| <script lang="ts">/*** | ||||
|  * A 'region' is a collection of properties that can be edited which are somewhat related. | ||||
|  * They will typically be a subset of some properties | ||||
|  */ | ||||
| import type { ConfigMeta } from "./configMeta"; | ||||
| import EditLayerState from "./EditLayerState"; | ||||
| import SchemaBasedInput from "./SchemaBasedInput.svelte"; | ||||
| <script lang="ts"> | ||||
|   /*** | ||||
|    * A 'region' is a collection of properties that can be edited which are somewhat related. | ||||
|    * They will typically be a subset of some properties | ||||
|    */ | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte" | ||||
| 
 | ||||
| export let state: EditLayerState; | ||||
| export let configs: ConfigMeta[]; | ||||
| export let title: string | undefined = undefined; | ||||
|   export let state: EditLayerState | ||||
|   export let configs: ConfigMeta[] | ||||
|   export let title: string | undefined = undefined | ||||
| 
 | ||||
| export let path: (string | number)[] = []; | ||||
| 
 | ||||
| let expertMode = state.expertMode | ||||
| let configsNoHidden = configs.filter(schema => schema.hints?.group !== "hidden") | ||||
| let configsFiltered = $expertMode ? configsNoHidden : configsNoHidden.filter(schema => schema.hints?.group !== "expert") | ||||
|   export let path: (string | number)[] = [] | ||||
| 
 | ||||
|   let expertMode = state.expertMode | ||||
|   let configsNoHidden = configs.filter((schema) => schema.hints?.group !== "hidden") | ||||
|   let configsFiltered = $expertMode | ||||
|     ? configsNoHidden | ||||
|     : configsNoHidden.filter((schema) => schema.hints?.group !== "expert") | ||||
| </script> | ||||
| 
 | ||||
| {#if configs === undefined} | ||||
|   Bug: 'Region' received 'undefined' | ||||
| {:else if configs.length === 0} | ||||
|   Bug: Region received empty list as configuration | ||||
| {:else if title} | ||||
|   <div class="w-full flex flex-col"> | ||||
|   <div class="flex w-full flex-col"> | ||||
|     <h3>{title}</h3> | ||||
|     <div class="pl-2 border border-black flex flex-col gap-y-1 w-full"> | ||||
|     <div class="flex w-full flex-col gap-y-1 border border-black pl-2"> | ||||
|       <slot name="description" /> | ||||
|       {#each configsFiltered as config} | ||||
|         <SchemaBasedInput {state} path={config.path} schema={config} /> | ||||
|  | @ -32,10 +35,9 @@ let configsFiltered = $expertMode ? configsNoHidden : configsNoHidden.filter(sch | |||
|     </div> | ||||
|   </div> | ||||
| {:else} | ||||
|   <div class="pl-2 flex flex-col gap-y-1 w-full"> | ||||
|   <div class="flex w-full flex-col gap-y-1 pl-2"> | ||||
|     {#each configsFiltered as config} | ||||
|       <SchemaBasedInput {state} path={path.concat(config.path)} schema={config} /> | ||||
|     {/each} | ||||
|   </div> | ||||
| {/if} | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,66 +1,70 @@ | |||
| <script lang="ts"> | ||||
|     import EditLayerState from "./EditLayerState"; | ||||
|     import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
|     import type {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; | ||||
|     import FullTagInput from "./TagInput/FullTagInput.svelte"; | ||||
|     import type {ConfigMeta} from "./configMeta"; | ||||
|     import {PencilAltIcon} from "@rgossiaux/svelte-heroicons/solid"; | ||||
|     import { onDestroy } from "svelte"; | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
|   import FullTagInput from "./TagInput/FullTagInput.svelte" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { onDestroy } from "svelte" | ||||
| 
 | ||||
|     /** | ||||
|      * Thin wrapper around 'TagInput' which registers the output with the state | ||||
|      */ | ||||
|     export let path: (string | number)[] | ||||
|     export let state: EditLayerState | ||||
|   /** | ||||
|    * Thin wrapper around 'TagInput' which registers the output with the state | ||||
|    */ | ||||
|   export let path: (string | number)[] | ||||
|   export let state: EditLayerState | ||||
| 
 | ||||
|     export let schema: ConfigMeta | ||||
|   export let schema: ConfigMeta | ||||
| 
 | ||||
|     const initialValue = state.getCurrentValueFor(path) | ||||
|     let tag: UIEventSource<TagConfigJson> = new UIEventSource<TagConfigJson>(initialValue) | ||||
|   const initialValue = state.getCurrentValueFor(path) | ||||
|   let tag: UIEventSource<TagConfigJson> = new UIEventSource<TagConfigJson>(initialValue) | ||||
| 
 | ||||
|     onDestroy(state.register(path, tag)) | ||||
|   onDestroy(state.register(path, tag)) | ||||
| 
 | ||||
|     let mode: "editing" | "set" = tag.data === undefined ? "editing" : "set" | ||||
|   let mode: "editing" | "set" = tag.data === undefined ? "editing" : "set" | ||||
| 
 | ||||
|     function simplify(tag: TagConfigJson): string { | ||||
|         if (typeof tag === "string") { | ||||
|             return tag | ||||
|         } | ||||
|         if (tag["and"]) { | ||||
|             return "{ and: " + simplify(tag["and"].map(simplify).join(" ; ") + " }") | ||||
|         } | ||||
|         if (tag["or"]) { | ||||
|             return "{ or: " + simplify(tag["or"].map(simplify).join(" ; ") + " }") | ||||
|         } | ||||
|   function simplify(tag: TagConfigJson): string { | ||||
|     if (typeof tag === "string") { | ||||
|       return tag | ||||
|     } | ||||
| 
 | ||||
|     if (tag["and"]) { | ||||
|       return "{ and: " + simplify(tag["and"].map(simplify).join(" ; ") + " }") | ||||
|     } | ||||
|     if (tag["or"]) { | ||||
|       return "{ or: " + simplify(tag["or"].map(simplify).join(" ; ") + " }") | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| {#if mode === "editing"} | ||||
|     <div class="interactive border-interactive"> | ||||
|         <h3>{schema.hints.question ?? "What tags should be applied?"}</h3> | ||||
|         {schema.description} | ||||
|         <FullTagInput {tag}/> | ||||
|         <div class="flex justify-end"> | ||||
| 
 | ||||
|             <button class="primary w-fit" on:click={() => {mode = "set"}}> | ||||
|                 Save | ||||
|             </button> | ||||
|         </div> | ||||
|         <div class="subtle">RegisteredTagInput based on schema: {JSON.stringify(schema)}</div> | ||||
|   <div class="interactive border-interactive"> | ||||
|     <h3>{schema.hints.question ?? "What tags should be applied?"}</h3> | ||||
|     {schema.description} | ||||
|     <FullTagInput {tag} /> | ||||
|     <div class="flex justify-end"> | ||||
|       <button | ||||
|         class="primary w-fit" | ||||
|         on:click={() => { | ||||
|           mode = "set" | ||||
|         }} | ||||
|       > | ||||
|         Save | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="subtle">RegisteredTagInput based on schema: {JSON.stringify(schema)}</div> | ||||
|   </div> | ||||
| {:else} | ||||
|     <div class="low-interaction flex justify-between"> | ||||
|         <div> | ||||
| 
 | ||||
|             {schema.path.at(-1)} | ||||
|             {simplify($tag)} | ||||
|         </div> | ||||
|         <button | ||||
|                 on:click={() => {mode = "editing"}} | ||||
|                 class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1" | ||||
|         > | ||||
|             <PencilAltIcon/> | ||||
|         </button> | ||||
|   <div class="low-interaction flex justify-between"> | ||||
|     <div> | ||||
|       {schema.path.at(-1)} | ||||
|       {simplify($tag)} | ||||
|     </div> | ||||
|     <button | ||||
|       on:click={() => { | ||||
|         mode = "editing" | ||||
|       }} | ||||
|       class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1" | ||||
|     > | ||||
|       <PencilAltIcon /> | ||||
|     </button> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,117 +1,114 @@ | |||
| <script lang="ts"> | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte"; | ||||
|   import SchemaBasedField from "./SchemaBasedField.svelte"; | ||||
|   import { TrashIcon } from "@babeard/svelte-heroicons/mini"; | ||||
|   import QuestionPreview from "./QuestionPreview.svelte"; | ||||
|   import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"; | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte"; | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte" | ||||
|   import SchemaBasedField from "./SchemaBasedField.svelte" | ||||
|   import { TrashIcon } from "@babeard/svelte-heroicons/mini" | ||||
|   import QuestionPreview from "./QuestionPreview.svelte" | ||||
|   import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte" | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte" | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   export let schema: ConfigMeta; | ||||
|   export let state: EditLayerState | ||||
|   export let schema: ConfigMeta | ||||
| 
 | ||||
| 
 | ||||
|   let title = schema.path.at(-1); | ||||
|   let singular = title; | ||||
|   let title = schema.path.at(-1) | ||||
|   let singular = title | ||||
|   if (title?.endsWith("s")) { | ||||
|     singular = title.slice(0, title.length - 1); | ||||
|     singular = title.slice(0, title.length - 1) | ||||
|   } | ||||
|   let article = "a"; | ||||
|   let article = "a" | ||||
|   if (singular?.match(/^[aeoui]/)) { | ||||
|     article = "an"; | ||||
|     article = "an" | ||||
|   } | ||||
|   export let path: (string | number)[] = []; | ||||
|    | ||||
|   const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings"; | ||||
|   export let path: (string | number)[] = [] | ||||
| 
 | ||||
|   const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings" | ||||
| 
 | ||||
|   if (isTagRenderingBlock) { | ||||
|     schema = { ...schema }; | ||||
|     schema.description = undefined; | ||||
|     schema = { ...schema } | ||||
|     schema.description = undefined | ||||
|   } | ||||
| 
 | ||||
|   const subparts: ConfigMeta = state.getSchemaStartingWith(schema.path) | ||||
|     .filter(part => part.path.length - 1 === schema.path.length); | ||||
|    | ||||
|   const subparts: ConfigMeta = state | ||||
|     .getSchemaStartingWith(schema.path) | ||||
|     .filter((part) => part.path.length - 1 === schema.path.length) | ||||
| 
 | ||||
|   let messages = state.messagesFor(path) | ||||
|    | ||||
|    | ||||
|   const currentValue : UIEventSource<any[]> = state.getStoreFor(path); | ||||
|   if(currentValue.data === undefined){ | ||||
| 
 | ||||
|   const currentValue: UIEventSource<any[]> = state.getStoreFor(path) | ||||
|   if (currentValue.data === undefined) { | ||||
|     currentValue.setData([]) | ||||
|   } | ||||
| 
 | ||||
|   function createItem(valueToSet?: any) { | ||||
|     if(currentValue.data === undefined){ | ||||
|     if (currentValue.data === undefined) { | ||||
|       currentValue.setData([]) | ||||
|     } | ||||
|     currentValue.data.push(valueToSet) | ||||
|     currentValue.ping() | ||||
|      | ||||
|     if(isTagRenderingBlock){ | ||||
|         state.highlightedItem.setData({path: [...path, currentValue.data.length - 1], schema}) | ||||
| 
 | ||||
|     if (isTagRenderingBlock) { | ||||
|       state.highlightedItem.setData({ path: [...path, currentValue.data.length - 1], schema }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function fusePath(i: number, subpartPath: string[]): (string | number)[] { | ||||
|     const newPath = [...path, i]; | ||||
|     const toAdd = [...subpartPath]; | ||||
|     const newPath = [...path, i] | ||||
|     const toAdd = [...subpartPath] | ||||
|     for (const part of path) { | ||||
|       if (toAdd[0] === part) { | ||||
|         toAdd.splice(0, 1); | ||||
|       }else{ | ||||
|         toAdd.splice(0, 1) | ||||
|       } else { | ||||
|         break | ||||
|       } | ||||
|     } | ||||
|     newPath.push(...toAdd); | ||||
|     console.log({newPath}) | ||||
|     return newPath; | ||||
|     newPath.push(...toAdd) | ||||
|     console.log({ newPath }) | ||||
|     return newPath | ||||
|   } | ||||
| 
 | ||||
|   function schemaForMultitype() { | ||||
|     const sch = {...schema} | ||||
|     const sch = { ...schema } | ||||
|     sch.hints.typehint = undefined | ||||
|     return sch | ||||
|   } | ||||
|    | ||||
|    | ||||
|   function del(i: number){ | ||||
| 
 | ||||
|   function del(i: number) { | ||||
|     currentValue.data.splice(i, 1) | ||||
|     currentValue.ping() | ||||
|   } | ||||
|    | ||||
| 
 | ||||
|   function swap(i: number, j: number) { | ||||
|     const x = currentValue.data[i] | ||||
|     currentValue.data[i] = currentValue.data[j]  | ||||
|     currentValue.data[i] = currentValue.data[j] | ||||
|     currentValue.data[j] = x | ||||
|     currentValue.ping() | ||||
|   } | ||||
|    | ||||
|   function moveTo(source: number, target: number){ | ||||
| 
 | ||||
|   function moveTo(source: number, target: number) { | ||||
|     const x = currentValue.data[source] | ||||
|     currentValue.data.splice(source, 1) | ||||
|     currentValue.data.splice(target, 0, x) | ||||
|     currentValue.ping() | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div class="pl-2"> | ||||
|   <h3>{schema.path.at(-1)}</h3> | ||||
| 
 | ||||
|   {#if subparts.length > 0} | ||||
|         <span class="subtle"> | ||||
|             {schema.description} | ||||
|         </span> | ||||
|     <span class="subtle"> | ||||
|       {schema.description} | ||||
|     </span> | ||||
|   {/if} | ||||
|   {#if $currentValue === undefined} | ||||
|       No array defined | ||||
|     No array defined | ||||
|   {:else if $currentValue.length === 0} | ||||
|     No values are defined | ||||
|     {#if $messages.length > 0} | ||||
|       {#each $messages as message} | ||||
|         <ShowConversionMessage {message}/> | ||||
|         <ShowConversionMessage {message} /> | ||||
|       {/each} | ||||
|     {/if} | ||||
|   {:else if subparts.length === 0} | ||||
|  | @ -119,52 +116,79 @@ | |||
|     {#each $currentValue as value, i} | ||||
|       <div class="flex w-full"> | ||||
|         <SchemaBasedField {state} {schema} path={[...path, i]} /> | ||||
|         <button class="border-black border rounded-full p-1 w-fit h-fit" | ||||
|                 on:click={() => {del(i)}}> | ||||
|           <TrashIcon class="w-4 h-4" /> | ||||
|         <button | ||||
|           class="h-fit w-fit rounded-full border border-black p-1" | ||||
|           on:click={() => { | ||||
|             del(i) | ||||
|           }} | ||||
|         > | ||||
|           <TrashIcon class="h-4 w-4" /> | ||||
|         </button> | ||||
|       </div> | ||||
|     {/each} | ||||
|   {:else} | ||||
|     {#each $currentValue as value, i} | ||||
|       {#if !isTagRenderingBlock} | ||||
|         <div class="flex justify-between items-center"> | ||||
|         <div class="flex items-center justify-between"> | ||||
|           <h3 class="m-0">{singular} {i}</h3> | ||||
|           <button class="border-black border rounded-full p-1 w-fit h-fit" | ||||
|                   on:click={() => {del(i)}}> | ||||
|             <TrashIcon class="w-4 h-4" /> | ||||
|           <button | ||||
|             class="h-fit w-fit rounded-full border border-black p-1" | ||||
|             on:click={() => { | ||||
|               del(i) | ||||
|             }} | ||||
|           > | ||||
|             <TrashIcon class="h-4 w-4" /> | ||||
|           </button> | ||||
|         </div> | ||||
|       {/if} | ||||
|       <div class="border border-black"> | ||||
|         {#if isTagRenderingBlock} | ||||
|           <QuestionPreview {state} path={[...path, i]} {schema}> | ||||
|             <button on:click={() => {del(i)}}> | ||||
|               <TrashIcon class="w-4 h-4" /> | ||||
|             <button | ||||
|               on:click={() => { | ||||
|                 del(i) | ||||
|               }} | ||||
|             > | ||||
|               <TrashIcon class="h-4 w-4" /> | ||||
|               Delete this question | ||||
|             </button> | ||||
| 
 | ||||
|             {#if i > 0} | ||||
|               <button on:click={() => {moveTo(i, 0)}}> | ||||
|               <button | ||||
|                 on:click={() => { | ||||
|                   moveTo(i, 0) | ||||
|                 }} | ||||
|               > | ||||
|                 Move to front | ||||
|               </button> | ||||
| 
 | ||||
|               <button on:click={() => {swap(i, i-1)}}> | ||||
|               <button | ||||
|                 on:click={() => { | ||||
|                   swap(i, i - 1) | ||||
|                 }} | ||||
|               > | ||||
|                 Move up | ||||
|               </button> | ||||
|             {/if} | ||||
|             {#if i + 1 < $currentValue.length} | ||||
|               <button on:click={() => {swap(i, i+1)}}> | ||||
|               <button | ||||
|                 on:click={() => { | ||||
|                   swap(i, i + 1) | ||||
|                 }} | ||||
|               > | ||||
|                 Move down | ||||
|               </button> | ||||
|               <button on:click={() => {moveTo(i, $currentValue.length-1)}}> | ||||
|               <button | ||||
|                 on:click={() => { | ||||
|                   moveTo(i, $currentValue.length - 1) | ||||
|                 }} | ||||
|               > | ||||
|                 Move to back | ||||
|               </button> | ||||
|             {/if} | ||||
| 
 | ||||
|           </QuestionPreview> | ||||
|           {:else if schema.hints.types} | ||||
|          <SchemaBasedMultiType {state} path={fusePath(i, [])} schema={schemaForMultitype()}/> | ||||
|         {:else if schema.hints.types} | ||||
|           <SchemaBasedMultiType {state} path={fusePath(i, [])} schema={schemaForMultitype()} /> | ||||
|         {:else} | ||||
|           {#each subparts as subpart} | ||||
|             <SchemaBasedInput {state} path={fusePath(i, subpart.path)} schema={subpart} /> | ||||
|  | @ -176,7 +200,13 @@ | |||
|   <div class="flex"> | ||||
|     <button on:click={() => createItem()}>Add {article} {singular}</button> | ||||
|     {#if path.length === 1 && path[0] === "tagRenderings"} | ||||
|       <button on:click={() => {createItem("images");}}>Add a builtin tagRendering</button> | ||||
|       <button | ||||
|         on:click={() => { | ||||
|           createItem("images") | ||||
|         }} | ||||
|       > | ||||
|         Add a builtin tagRendering | ||||
|       </button> | ||||
|     {/if} | ||||
|     <slot name="extra-button" /> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -1,50 +1,55 @@ | |||
| <script lang="ts"> | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte" | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import nmd from "nano-markdown" | ||||
|   import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import { onDestroy } from "svelte" | ||||
|   import type { JsonSchemaType } from "./jsonSchema" | ||||
|   import { ConfigMetaUtils } from "./configMeta.ts" | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte" | ||||
| 
 | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import nmd from "nano-markdown"; | ||||
|   import type { | ||||
|     QuestionableTagRenderingConfigJson | ||||
|   } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import { onDestroy } from "svelte"; | ||||
|   import type { JsonSchemaType } from "./jsonSchema"; | ||||
|   import { ConfigMetaUtils } from "./configMeta.ts"; | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte"; | ||||
|   export let state: EditLayerState | ||||
|   export let path: (string | number)[] = [] | ||||
|   export let schema: ConfigMeta | ||||
|   export let startInEditModeIfUnset: boolean = schema.hints && !schema.hints.ifunset | ||||
|   let value = new UIEventSource<string | any>(undefined) | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   export let path: (string | number)[] = []; | ||||
|   export let schema: ConfigMeta; | ||||
|   export let startInEditModeIfUnset: boolean = schema.hints && !schema.hints.ifunset; | ||||
|   let value = new UIEventSource<string | any>(undefined); | ||||
|   const isTranslation = | ||||
|     schema.hints?.typehint === "translation" || | ||||
|     schema.hints?.typehint === "rendered" || | ||||
|     ConfigMetaUtils.isTranslation(schema) | ||||
|   let type = schema.hints.typehint ?? "string" | ||||
| 
 | ||||
|   const isTranslation = schema.hints?.typehint === "translation" || schema.hints?.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema); | ||||
|   let type = schema.hints.typehint ?? "string"; | ||||
| 
 | ||||
|   let rendervalue = (schema.hints.inline ?? schema.path.join(".")) + (isTranslation ? " <b>{translated(value)}</b>" : " <b>{value}</b>"); | ||||
|   let rendervalue = | ||||
|     (schema.hints.inline ?? schema.path.join(".")) + | ||||
|     (isTranslation ? " <b>{translated(value)}</b>" : " <b>{value}</b>") | ||||
| 
 | ||||
|   if (schema.type === "boolean") { | ||||
|     rendervalue = undefined; | ||||
|     rendervalue = undefined | ||||
|   } | ||||
|   if (schema.hints.typehint === "tag" || schema.hints.typehint === "simple_tag") { | ||||
|     rendervalue = "{tags()}"; | ||||
|     rendervalue = "{tags()}" | ||||
|   } | ||||
| 
 | ||||
|   let helperArgs = schema.hints.typehelper?.split(","); | ||||
|   let inline = schema.hints.inline !== undefined; | ||||
|   let helperArgs = schema.hints.typehelper?.split(",") | ||||
|   let inline = schema.hints.inline !== undefined | ||||
|   if (isTranslation) { | ||||
|     type = "translation"; | ||||
|     type = "translation" | ||||
|     if (schema.hints.inline) { | ||||
|       const inlineValue = schema.hints.inline; | ||||
|       rendervalue = inlineValue; | ||||
|       inline = false; | ||||
|       helperArgs = [inlineValue.substring(0, inlineValue.indexOf("{")), inlineValue.substring(inlineValue.indexOf("}") + 1)]; | ||||
|       const inlineValue = schema.hints.inline | ||||
|       rendervalue = inlineValue | ||||
|       inline = false | ||||
|       helperArgs = [ | ||||
|         inlineValue.substring(0, inlineValue.indexOf("{")), | ||||
|         inlineValue.substring(inlineValue.indexOf("}") + 1), | ||||
|       ] | ||||
|     } | ||||
|   } | ||||
|   if (type.endsWith("[]")) { | ||||
|     type = type.substring(0, type.length - 2); | ||||
|     type = type.substring(0, type.length - 2) | ||||
|   } | ||||
| 
 | ||||
|   const configJson: QuestionableTagRenderingConfigJson = { | ||||
|  | @ -52,125 +57,154 @@ | |||
|     render: rendervalue, | ||||
|     question: schema.hints.question, | ||||
|     questionHint: nmd(schema.description), | ||||
|     freeform: schema.type === "boolean" ? undefined : { | ||||
|       key: "value", | ||||
|       type, | ||||
|       inline, | ||||
|       helperArgs | ||||
|     } | ||||
|   }; | ||||
|     freeform: | ||||
|       schema.type === "boolean" | ||||
|         ? undefined | ||||
|         : { | ||||
|             key: "value", | ||||
|             type, | ||||
|             inline, | ||||
|             helperArgs, | ||||
|           }, | ||||
|   } | ||||
| 
 | ||||
|   if (schema.hints.default) { | ||||
|     configJson.mappings = [{ | ||||
|       if: "value=", // We leave this blank | ||||
|       then: path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? "") | ||||
|     }]; | ||||
|     configJson.mappings = [ | ||||
|       { | ||||
|         if: "value=", // We leave this blank | ||||
|         then: | ||||
|           path.at(-1) + | ||||
|           " is not set. The default value <b>" + | ||||
|           schema.hints.default + | ||||
|           "</b> will be used. " + | ||||
|           (schema.hints.ifunset ?? ""), | ||||
|       }, | ||||
|     ] | ||||
|   } else if (!schema.required) { | ||||
|     configJson.mappings = [{ | ||||
|       if: "value=", | ||||
|       then: path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "") | ||||
|     }]; | ||||
|     configJson.mappings = [ | ||||
|       { | ||||
|         if: "value=", | ||||
|         then: path.at(-1) + " is not set. " + (schema.hints.ifunset ?? ""), | ||||
|       }, | ||||
|     ] | ||||
|   } | ||||
| 
 | ||||
|   function mightBeBoolean(type: undefined | JsonSchemaType): boolean { | ||||
|     if (type === undefined) { | ||||
|       return false; | ||||
|       return false | ||||
|     } | ||||
|     if (type["type"]) { | ||||
|       type = type["type"]; | ||||
|       type = type["type"] | ||||
|     } | ||||
|     if (type === "boolean") { | ||||
|       return true; | ||||
|       return true | ||||
|     } | ||||
|     if (!Array.isArray(type)) { | ||||
|       return false; | ||||
|       return false | ||||
|     } | ||||
| 
 | ||||
|     return type.some(t => mightBeBoolean(t)); | ||||
|     return type.some((t) => mightBeBoolean(t)) | ||||
|   } | ||||
| 
 | ||||
|   if (mightBeBoolean(schema.type)) { | ||||
|     configJson.mappings = configJson.mappings ?? []; | ||||
|     configJson.mappings = configJson.mappings ?? [] | ||||
|     configJson.mappings.push( | ||||
|       { | ||||
|         if: "value=true", | ||||
|         then: schema.hints?.iftrue ?? "Yes" | ||||
|         then: schema.hints?.iftrue ?? "Yes", | ||||
|       }, | ||||
|       { | ||||
|         if: "value=false", | ||||
|         then: schema.hints?.iffalse ?? "No" | ||||
|         then: schema.hints?.iffalse ?? "No", | ||||
|       } | ||||
|     ); | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   if (schema.hints.suggestions) { | ||||
|     if (!configJson.mappings) { | ||||
|       configJson.mappings = []; | ||||
|       configJson.mappings = [] | ||||
|     } | ||||
|     configJson.mappings.push(...schema.hints.suggestions); | ||||
|     configJson.mappings.push(...schema.hints.suggestions) | ||||
|   } | ||||
|   let config: TagRenderingConfig; | ||||
|   let err: string = undefined; | ||||
|   let messages = state.messagesFor(path); | ||||
|   let config: TagRenderingConfig | ||||
|   let err: string = undefined | ||||
|   let messages = state.messagesFor(path) | ||||
|   try { | ||||
|     config = new TagRenderingConfig(configJson, "config based on " + schema.path.join(".")); | ||||
|     config = new TagRenderingConfig(configJson, "config based on " + schema.path.join(".")) | ||||
|   } catch (e) { | ||||
|     console.error(e, config); | ||||
|     err = path.join(".") + " " + e; | ||||
|     console.error(e, config) | ||||
|     err = path.join(".") + " " + e | ||||
|   } | ||||
|   let startValue = state.getCurrentValueFor(path); | ||||
|   let startInEditMode = !startValue && startInEditModeIfUnset; | ||||
|   const tags = new UIEventSource<Record<string, string>>({ value: startValue }); | ||||
|   let startValue = state.getCurrentValueFor(path) | ||||
|   let startInEditMode = !startValue && startInEditModeIfUnset | ||||
|   const tags = new UIEventSource<Record<string, string>>({ value: startValue }) | ||||
|   try { | ||||
|     onDestroy(state.register(path, tags.map(tgs => { | ||||
|       const v = tgs["value"]; | ||||
|       if (typeof v !== "string") { | ||||
|         return { ...v }; | ||||
|       } | ||||
|       if (schema.type === "boolan") { | ||||
|         return v === "true" || v === "yes" || v === "1"; | ||||
|       } | ||||
|       if (mightBeBoolean(schema.type)) { | ||||
|         if (v === "true" || v === "yes" || v === "1") { | ||||
|           return true; | ||||
|         } | ||||
|         if (v === "false" || v === "no" || v === "0") { | ||||
|           return false; | ||||
|         } | ||||
|       } | ||||
|       if (schema.type === "number") { | ||||
|         if (v === "") { | ||||
|           return undefined; | ||||
|         } | ||||
|         return Number(v); | ||||
|       } | ||||
|       if (isTranslation && typeof v === "string") { | ||||
|         if (v === "") { | ||||
|           return {}; | ||||
|         } | ||||
|         return JSON.parse(v); | ||||
|       } | ||||
|       return v; | ||||
|     }), isTranslation)); | ||||
|     onDestroy( | ||||
|       state.register( | ||||
|         path, | ||||
|         tags.map((tgs) => { | ||||
|           const v = tgs["value"] | ||||
|           if (typeof v !== "string") { | ||||
|             return { ...v } | ||||
|           } | ||||
|           if (schema.type === "boolan") { | ||||
|             return v === "true" || v === "yes" || v === "1" | ||||
|           } | ||||
|           if (mightBeBoolean(schema.type)) { | ||||
|             if (v === "true" || v === "yes" || v === "1") { | ||||
|               return true | ||||
|             } | ||||
|             if (v === "false" || v === "no" || v === "0") { | ||||
|               return false | ||||
|             } | ||||
|           } | ||||
|           if (schema.type === "number") { | ||||
|             if (v === "") { | ||||
|               return undefined | ||||
|             } | ||||
|             return Number(v) | ||||
|           } | ||||
|           if (isTranslation && typeof v === "string") { | ||||
|             if (v === "") { | ||||
|               return {} | ||||
|             } | ||||
|             return JSON.parse(v) | ||||
|           } | ||||
|           return v | ||||
|         }), | ||||
|         isTranslation | ||||
|       ) | ||||
|     ) | ||||
|   } catch (e) { | ||||
|     console.error("Could not register", path, "due to", e); | ||||
|     console.error("Could not register", path, "due to", e) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| {#if err !== undefined} | ||||
|   <span class="alert">{err}</span> | ||||
| {:else} | ||||
|   <div class="w-full flex flex-col"> | ||||
|     <TagRenderingEditable editMode={startInEditMode} {config} selectedElement={undefined} showQuestionIfUnknown={true} | ||||
|                           {state} {tags} /> | ||||
|   <div class="flex w-full flex-col"> | ||||
|     <TagRenderingEditable | ||||
|       editMode={startInEditMode} | ||||
|       {config} | ||||
|       selectedElement={undefined} | ||||
|       showQuestionIfUnknown={true} | ||||
|       {state} | ||||
|       {tags} | ||||
|     /> | ||||
|     {#if $messages.length > 0} | ||||
|       {#each $messages as message} | ||||
|         <ShowConversionMessage {message} /> | ||||
|       {/each} | ||||
|     {/if} | ||||
|     {#if window.location.hostname === "127.0.0.1"} | ||||
|       <span class="subtle" on:click={() => console.log(schema)}>SchemaBasedField <b>{path.join(".")}</b> <span class="cursor-pointer" | ||||
|                                                                           on:click={() => console.log(schema)}>{schema.hints.typehint}</span> Group: {schema.hints.group}</span> | ||||
|       <span class="subtle" on:click={() => console.log(schema)}> | ||||
|         SchemaBasedField <b>{path.join(".")}</b> | ||||
|         <span class="cursor-pointer" on:click={() => console.log(schema)}> | ||||
|           {schema.hints.typehint} | ||||
|         </span> | ||||
|         Group: {schema.hints.group} | ||||
|       </span> | ||||
|     {/if} | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,16 +1,17 @@ | |||
| <script lang="ts"> | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import SchemaBasedField from "./SchemaBasedField.svelte"; | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import SchemaBasedArray from "./SchemaBasedArray.svelte"; | ||||
|   import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"; | ||||
|   import ArrayMultiAnswer from "./ArrayMultiAnswer.svelte"; | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import SchemaBasedField from "./SchemaBasedField.svelte" | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import SchemaBasedArray from "./SchemaBasedArray.svelte" | ||||
|   import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte" | ||||
|   import ArrayMultiAnswer from "./ArrayMultiAnswer.svelte" | ||||
| 
 | ||||
|   export let schema: ConfigMeta; | ||||
|   export let state: EditLayerState; | ||||
|   export let path: (string | number)[] = []; | ||||
|   let expertMode = state.expertMode; | ||||
|   export let schema: ConfigMeta | ||||
|   export let state: EditLayerState | ||||
|   export let path: (string | number)[] = [] | ||||
|   let expertMode = state.expertMode | ||||
| </script> | ||||
| 
 | ||||
| {#if (schema.hints?.group !== "expert" || $expertMode) && schema.hints.group !== "hidden"} | ||||
|   {#if schema.hints?.typehint?.endsWith("[]")} | ||||
|     <!-- We cheat a bit here by matching this 'magical' type... --> | ||||
|  | @ -25,5 +26,7 @@ | |||
|     <SchemaBasedField {path} {state} {schema} /> | ||||
|   {/if} | ||||
| {:else if window.location.hostname === "127.0.0.1"} | ||||
|   <div class="subtle">Not showing SBI {schema.path.join(".")} due to group {schema.hints.group}</div> | ||||
|   <div class="subtle"> | ||||
|     Not showing SBI {schema.path.join(".")} due to group {schema.hints.group} | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,223 +1,236 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import type { | ||||
|     QuestionableTagRenderingConfigJson | ||||
|   } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import { onDestroy } from "svelte"; | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte"; | ||||
|   import type { JsonSchemaType } from "./jsonSchema"; | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte" | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import { onDestroy } from "svelte" | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte" | ||||
|   import type { JsonSchemaType } from "./jsonSchema" | ||||
|   // @ts-ignore | ||||
|   import nmd from "nano-markdown"; | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte"; | ||||
|   import nmd from "nano-markdown" | ||||
|   import ShowConversionMessage from "./ShowConversionMessage.svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * If 'types' is defined: allow the user to pick one of the types to input. | ||||
|    */ | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   export let path: (string | number)[] = []; | ||||
|   export let schema: ConfigMeta; | ||||
|   let expertMode = state.expertMode; | ||||
|   const defaultOption = schema.hints.typesdefault ? Number(schema.hints.typesdefault) : undefined; | ||||
|   export let state: EditLayerState | ||||
|   export let path: (string | number)[] = [] | ||||
|   export let schema: ConfigMeta | ||||
|   let expertMode = state.expertMode | ||||
|   const defaultOption = schema.hints.typesdefault ? Number(schema.hints.typesdefault) : undefined | ||||
| 
 | ||||
|   const hasBooleanOption = (<JsonSchemaType[]>schema.type)?.findIndex(t => t["type"] === "boolean"); | ||||
|   const types = schema.hints.types.split(";"); | ||||
|   const hasBooleanOption = (<JsonSchemaType[]>schema.type)?.findIndex( | ||||
|     (t) => t["type"] === "boolean" | ||||
|   ) | ||||
|   const types = schema.hints.types.split(";") | ||||
|   if (hasBooleanOption >= 0) { | ||||
|     types.splice(hasBooleanOption); | ||||
|     types.splice(hasBooleanOption) | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   let lastIsString = false; | ||||
|   let lastIsString = false | ||||
|   { | ||||
|     const types: string | string[] = Array.isArray(schema.type) ? schema.type[schema.type.length - 1].type : []; | ||||
|     lastIsString = types === "string" || (Array.isArray(types) && types.some(i => i === "string")); | ||||
|     const types: string | string[] = Array.isArray(schema.type) | ||||
|       ? schema.type[schema.type.length - 1].type | ||||
|       : [] | ||||
|     lastIsString = types === "string" || (Array.isArray(types) && types.some((i) => i === "string")) | ||||
|   } | ||||
| 
 | ||||
|   if (lastIsString) { | ||||
|     types.splice(types.length - 1, 1); | ||||
|     types.splice(types.length - 1, 1) | ||||
|   } | ||||
|   const configJson: QuestionableTagRenderingConfigJson = { | ||||
|     id: "TYPE_OF:" + path.join("_"), | ||||
|     question: "Which subcategory is needed for " + schema.path.at(-1) + "?", | ||||
|     questionHint: nmd(schema.description), | ||||
|     mappings: types.map(opt => opt.trim()).filter(opt => opt.length > 0).map((opt, i) => ({ | ||||
|       if: "chosen_type_index=" + i, | ||||
|       addExtraTags: ["value="], | ||||
|       then: opt + (i === defaultOption ? " (Default)" : "") | ||||
|     })), | ||||
|     render: !lastIsString ? undefined : (schema.hints.inline ?? "Use a hardcoded value: <b>{value}</b>"), | ||||
|     freeform: !lastIsString ? undefined : { | ||||
|       key: "value", | ||||
|       inline: true, | ||||
|       type: schema.hints.typehint, | ||||
|       addExtraTags: ["chosen_type_index="] | ||||
|     } | ||||
|   }; | ||||
|   let tags = new UIEventSource<Record<string, string>>({}); | ||||
|     mappings: types | ||||
|       .map((opt) => opt.trim()) | ||||
|       .filter((opt) => opt.length > 0) | ||||
|       .map((opt, i) => ({ | ||||
|         if: "chosen_type_index=" + i, | ||||
|         addExtraTags: ["value="], | ||||
|         then: opt + (i === defaultOption ? " (Default)" : ""), | ||||
|       })), | ||||
|     render: !lastIsString | ||||
|       ? undefined | ||||
|       : schema.hints.inline ?? "Use a hardcoded value: <b>{value}</b>", | ||||
|     freeform: !lastIsString | ||||
|       ? undefined | ||||
|       : { | ||||
|           key: "value", | ||||
|           inline: true, | ||||
|           type: schema.hints.typehint, | ||||
|           addExtraTags: ["chosen_type_index="], | ||||
|         }, | ||||
|   } | ||||
|   let tags = new UIEventSource<Record<string, string>>({}) | ||||
| 
 | ||||
|   if (schema.hints.ifunset) { | ||||
|     configJson.mappings.push( | ||||
|       { | ||||
|         if: { and: ["value=", "chosen_type_index="] }, | ||||
|         then: schema.hints.ifunset | ||||
|       } | ||||
|     ); | ||||
|     configJson.mappings.push({ | ||||
|       if: { and: ["value=", "chosen_type_index="] }, | ||||
|       then: schema.hints.ifunset, | ||||
|     }) | ||||
|   } | ||||
|   if (schema.hints.suggestions) { | ||||
|     configJson.mappings.push(...schema.hints.suggestions); | ||||
|     configJson.mappings.push(...schema.hints.suggestions) | ||||
|   } | ||||
| 
 | ||||
|   if (hasBooleanOption >= 0) { | ||||
|     configJson.mappings.unshift( | ||||
|       { | ||||
|         if: "value=true", | ||||
|         then: (schema.hints.iftrue ?? "Yes"), | ||||
|         addExtraTags: ["chosen_type_index="] | ||||
|         then: schema.hints.iftrue ?? "Yes", | ||||
|         addExtraTags: ["chosen_type_index="], | ||||
|       }, | ||||
|       { | ||||
|         if: "value=false", | ||||
|         then: (schema.hints.iffalse ?? "No"), | ||||
|         addExtraTags: ["chosen_type_index="] | ||||
|         then: schema.hints.iffalse ?? "No", | ||||
|         addExtraTags: ["chosen_type_index="], | ||||
|       } | ||||
|     ); | ||||
|     ) | ||||
|   } | ||||
|   const config = new TagRenderingConfig(configJson, "config based on " + schema.path.join(".")); | ||||
|   let chosenOption: number = (defaultOption); | ||||
|   const config = new TagRenderingConfig(configJson, "config based on " + schema.path.join(".")) | ||||
|   let chosenOption: number = defaultOption | ||||
| 
 | ||||
| 
 | ||||
|   const existingValue = state.getCurrentValueFor(path); | ||||
|   let hasOverride = existingValue?.override !== undefined; | ||||
|   const existingValue = state.getCurrentValueFor(path) | ||||
|   let hasOverride = existingValue?.override !== undefined | ||||
|   if (hasBooleanOption >= 0 && (existingValue === true || existingValue === false)) { | ||||
|     tags.setData({ value: "" + existingValue }); | ||||
|     tags.setData({ value: "" + existingValue }) | ||||
|   } else if (lastIsString && typeof existingValue === "string") { | ||||
|     tags.setData({ value: existingValue }); | ||||
|     chosenOption = undefined; | ||||
|     tags.setData({ value: existingValue }) | ||||
|     chosenOption = undefined | ||||
|   } else if (existingValue) { | ||||
|     // We found an existing value. Let's figure out what type it matches and select that one | ||||
|     // We run over all possibilities and check what is required | ||||
|     const possibleTypes: { index: number, matchingPropertiesCount: number, optionalMatches: number }[] = []; | ||||
|     const possibleTypes: { | ||||
|       index: number | ||||
|       matchingPropertiesCount: number | ||||
|       optionalMatches: number | ||||
|     }[] = [] | ||||
|     outer: for (let i = 0; i < (<[]>schema.type).length; i++) { | ||||
|       const type = schema.type[i]; | ||||
|       let optionalMatches = 0; | ||||
|       const type = schema.type[i] | ||||
|       let optionalMatches = 0 | ||||
|       for (const key of Object.keys(type.properties ?? {})) { | ||||
|         if (!!existingValue[key]) { | ||||
|           optionalMatches++; | ||||
|           optionalMatches++ | ||||
|         } | ||||
|       } | ||||
|       if (type.required) { | ||||
|         let numberOfMatches = 0; | ||||
|         let numberOfMatches = 0 | ||||
| 
 | ||||
|         for (const requiredAttribute of type.required) { | ||||
|           if (existingValue[requiredAttribute] === undefined) { | ||||
|             // The 'existingValue' does _not_ have this required attribute, so it cannot be of this type | ||||
|             continue outer; | ||||
|             continue outer | ||||
|           } | ||||
|           numberOfMatches++; | ||||
|           numberOfMatches++ | ||||
|         } | ||||
|         possibleTypes.push({ index: i, matchingPropertiesCount: numberOfMatches, optionalMatches }); | ||||
|         possibleTypes.push({ index: i, matchingPropertiesCount: numberOfMatches, optionalMatches }) | ||||
|       } else { | ||||
|         possibleTypes.push({ index: i, matchingPropertiesCount: 0, optionalMatches }); | ||||
|         possibleTypes.push({ index: i, matchingPropertiesCount: 0, optionalMatches }) | ||||
|       } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
|     possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches); | ||||
|     possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount); | ||||
|     possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches) | ||||
|     possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount) | ||||
|     if (possibleTypes.length > 0) { | ||||
|       chosenOption = possibleTypes[0].index; | ||||
|       tags.setData({ chosen_type_index: "" + chosenOption }); | ||||
| 
 | ||||
|       chosenOption = possibleTypes[0].index | ||||
|       tags.setData({ chosen_type_index: "" + chosenOption }) | ||||
|     } | ||||
|   } else if (defaultOption !== undefined) { | ||||
|     tags.setData({ chosen_type_index: "" + defaultOption }); | ||||
|     tags.setData({ chosen_type_index: "" + defaultOption }) | ||||
|   } else { | ||||
|     chosenOption = defaultOption; | ||||
|     chosenOption = defaultOption | ||||
|   } | ||||
| 
 | ||||
|   if (hasBooleanOption >= 0 || lastIsString) { | ||||
| 
 | ||||
|     const directValue = tags.mapD(tags => { | ||||
|     const directValue = tags.mapD((tags) => { | ||||
|       if (tags["chosen_type_index"]) { | ||||
|         return ""; | ||||
|         return "" | ||||
|       } | ||||
|       if (lastIsString) { | ||||
|         return tags["value"]; | ||||
|         return tags["value"] | ||||
|       } | ||||
|       return tags["value"] === "true"; | ||||
|     }); | ||||
|     onDestroy(state.register(path, directValue)); | ||||
|       return tags["value"] === "true" | ||||
|     }) | ||||
|     onDestroy(state.register(path, directValue)) | ||||
|   } | ||||
| 
 | ||||
|   let subSchemas: ConfigMeta[] = []; | ||||
|   let subSchemas: ConfigMeta[] = [] | ||||
| 
 | ||||
|   let subpath = path; | ||||
|   const store = state.getStoreFor(path); | ||||
|   onDestroy(tags.addCallbackAndRun(tags => { | ||||
|     if (tags["value"] !== undefined && tags["value"] !== "") { | ||||
|       chosenOption = undefined; | ||||
|       return; | ||||
|     } | ||||
|     const oldOption = chosenOption; | ||||
|     chosenOption = tags["chosen_type_index"] ? Number(tags["chosen_type_index"]) : defaultOption; | ||||
|     const type = schema.type[chosenOption]; | ||||
|     if (chosenOption !== oldOption) { | ||||
|       // Reset the values beneath | ||||
|       subSchemas = []; | ||||
|       const o = state.getCurrentValueFor(path) ?? {}; | ||||
|       for (const key of type?.required ?? []) { | ||||
|         o[key] ??= {}; | ||||
|   let subpath = path | ||||
|   const store = state.getStoreFor(path) | ||||
|   onDestroy( | ||||
|     tags.addCallbackAndRun((tags) => { | ||||
|       if (tags["value"] !== undefined && tags["value"] !== "") { | ||||
|         chosenOption = undefined | ||||
|         return | ||||
|       } | ||||
|       store.setData(o); | ||||
|     } | ||||
|     if (!type) { | ||||
|       return; | ||||
|     } | ||||
|     subpath = path; | ||||
|     const cleanPath = <string[]>path.filter(p => typeof p === "string"); | ||||
|     if (type["$ref"] === "#/definitions/Record<string,string>") { | ||||
|       // The subtype is a translation object | ||||
|       const schema = state.getTranslationAt(cleanPath); | ||||
|       subSchemas.push(schema); | ||||
|       subpath = path.slice(0, path.length - 2); | ||||
|       return; | ||||
|     } | ||||
|     if (!type.properties) { | ||||
|       return; | ||||
|     } | ||||
|     for (const crumble of Object.keys(type.properties)) { | ||||
|       subSchemas.push(...(state.getSchema([...cleanPath, crumble]))); | ||||
|     } | ||||
|   })); | ||||
|   let messages = state.messagesFor(path); | ||||
| 
 | ||||
|       const oldOption = chosenOption | ||||
|       chosenOption = tags["chosen_type_index"] ? Number(tags["chosen_type_index"]) : defaultOption | ||||
|       const type = schema.type[chosenOption] | ||||
|       if (chosenOption !== oldOption) { | ||||
|         // Reset the values beneath | ||||
|         subSchemas = [] | ||||
|         const o = state.getCurrentValueFor(path) ?? {} | ||||
|         for (const key of type?.required ?? []) { | ||||
|           o[key] ??= {} | ||||
|         } | ||||
|         store.setData(o) | ||||
|       } | ||||
|       if (!type) { | ||||
|         return | ||||
|       } | ||||
|       subpath = path | ||||
|       const cleanPath = <string[]>path.filter((p) => typeof p === "string") | ||||
|       if (type["$ref"] === "#/definitions/Record<string,string>") { | ||||
|         // The subtype is a translation object | ||||
|         const schema = state.getTranslationAt(cleanPath) | ||||
|         subSchemas.push(schema) | ||||
|         subpath = path.slice(0, path.length - 2) | ||||
|         return | ||||
|       } | ||||
|       if (!type.properties) { | ||||
|         return | ||||
|       } | ||||
|       for (const crumble of Object.keys(type.properties)) { | ||||
|         subSchemas.push(...state.getSchema([...cleanPath, crumble])) | ||||
|       } | ||||
|     }) | ||||
|   ) | ||||
|   let messages = state.messagesFor(path) | ||||
| </script> | ||||
| 
 | ||||
| <div class="p-2 border-2 border-dashed border-gray-300 flex flex-col gap-y-2 m-1"> | ||||
| <div class="m-1 flex flex-col gap-y-2 border-2 border-dashed border-gray-300 p-2"> | ||||
|   {#if schema.hints.title !== undefined} | ||||
|     <h3>{schema.hints.title}</h3> | ||||
|     <div> {schema.description} </div> | ||||
|     <div>{schema.description}</div> | ||||
|   {/if} | ||||
|   {#if hasOverride} | ||||
|     This object refers to {existingValue.builtin} and overrides some properties. This cannot be edited with MapComplete | ||||
|     Studio | ||||
|     This object refers to {existingValue.builtin} and overrides some properties. This cannot be edited | ||||
|     with MapComplete Studio | ||||
|   {:else} | ||||
|     <div> | ||||
|       <TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={!schema.hints?.ifunset} {state} {tags} /> | ||||
|       <TagRenderingEditable | ||||
|         {config} | ||||
|         selectedElement={undefined} | ||||
|         showQuestionIfUnknown={!schema.hints?.ifunset} | ||||
|         {state} | ||||
|         {tags} | ||||
|       /> | ||||
|     </div> | ||||
| 
 | ||||
|     {#if chosenOption !== undefined} | ||||
|       {#each subSchemas as subschema} | ||||
|         {#if $expertMode || subschema.hints?.group !== "expert"} | ||||
|           <SchemaBasedInput {state} schema={subschema} | ||||
|                             path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput> | ||||
|           <SchemaBasedInput | ||||
|             {state} | ||||
|             schema={subschema} | ||||
|             path={[...subpath, subschema?.path?.at(-1) ?? "???"]} | ||||
|           /> | ||||
|         {:else if window.location.hostname === "127.0.0.1"} | ||||
|           <span class="subtle">Omitted expert question {subschema.path.join(".")}</span> | ||||
| 
 | ||||
|         {/if} | ||||
|       {/each} | ||||
|     {:else if $messages.length > 0} | ||||
|  | @ -227,7 +240,11 @@ | |||
|     {/if} | ||||
|   {/if} | ||||
|   {#if window.location.hostname === "127.0.0.1"} | ||||
|     <span class="subtle">SchemaBasedMultiType <b>{path.join(".")}</b> <span class="cursor-pointer" | ||||
|                                                                             on:click={() => console.log(schema)}>{schema.hints.typehint}</span></span> | ||||
|     <span class="subtle"> | ||||
|       SchemaBasedMultiType <b>{path.join(".")}</b> | ||||
|       <span class="cursor-pointer" on:click={() => console.log(schema)}> | ||||
|         {schema.hints.typehint} | ||||
|       </span> | ||||
|     </span> | ||||
|   {/if} | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,16 +1,19 @@ | |||
| <script lang="ts"> | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import TranslationInput from "../InputElement/Helpers/TranslationInput.svelte"; | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import TranslationInput from "../InputElement/Helpers/TranslationInput.svelte" | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   export let path: (string | number)[] = []; | ||||
|   export let schema: ConfigMeta; | ||||
|   export let state: EditLayerState | ||||
|   export let path: (string | number)[] = [] | ||||
|   export let schema: ConfigMeta | ||||
| 
 | ||||
|   let value = new UIEventSource<string>({}); | ||||
|   let value = new UIEventSource<string>({}) | ||||
|   console.log("Registering translation to path", path) | ||||
|   state.register(path, value.mapD(v => JSON.parse(value.data  ))); | ||||
|   state.register( | ||||
|     path, | ||||
|     value.mapD((v) => JSON.parse(value.data)) | ||||
|   ) | ||||
| </script> | ||||
| 
 | ||||
| <TranslationInput {value} /> | ||||
|  |  | |||
|  | @ -1,29 +1,29 @@ | |||
| <script lang="ts"> | ||||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; | ||||
|   import { ExclamationIcon, InformationCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion" | ||||
|   import { ExclamationIcon, InformationCircleIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
| 
 | ||||
|   /** | ||||
|    * Single conversion message, styled depending on the type | ||||
|    */ | ||||
|   export let message: ConversionMessage; | ||||
|   export let message: ConversionMessage | ||||
| </script> | ||||
| 
 | ||||
| {#if message.level === "error"} | ||||
|   <div class="alert flex justify-between items-center"> | ||||
|     <ExclamationIcon class="w-6 h-6 mx-1 shrink-0" /> | ||||
|   <div class="alert flex items-center justify-between"> | ||||
|     <ExclamationIcon class="mx-1 h-6 w-6 shrink-0" /> | ||||
|     {message.message} | ||||
|     <div/> | ||||
|     <div /> | ||||
|   </div> | ||||
| {:else if message.level === "warning"} | ||||
|   <div class="warning flex justify-between items-center"> | ||||
|     <ExclamationIcon class="w-6 h-6 mx-1 shrink-0" /> | ||||
|   <div class="warning flex items-center justify-between"> | ||||
|     <ExclamationIcon class="mx-1 h-6 w-6 shrink-0" /> | ||||
|     {message.message} | ||||
|     <div/> | ||||
|     <div /> | ||||
|   </div> | ||||
| {:else if message.level === "information"} | ||||
|   <div class="information flex justify-between items-center"> | ||||
|     <InformationCircleIcon class="w-6 h-6 mx-1 shrink-0" /> | ||||
|   <div class="information flex items-center justify-between"> | ||||
|     <InformationCircleIcon class="mx-1 h-6 w-6 shrink-0" /> | ||||
|     {message.message} | ||||
|     <div/> | ||||
|     <div /> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,14 +1,12 @@ | |||
| <script lang="ts"> | ||||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; | ||||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion" | ||||
| 
 | ||||
|   export let messages: ConversionMessage[]; | ||||
|   export let messages: ConversionMessage[] | ||||
| </script> | ||||
| 
 | ||||
| {#if messages.length === 0} | ||||
| <div class="thanks"> | ||||
|   No errors, warnings or messages | ||||
| </div> | ||||
|   {/if} | ||||
|   <div class="thanks">No errors, warnings or messages</div> | ||||
| {/if} | ||||
| 
 | ||||
| {#each messages as message} | ||||
|   <li> | ||||
|  | @ -16,7 +14,7 @@ | |||
|     <span class="literal-code">{message.context.path.join(".")}</span> | ||||
|     {message.message} | ||||
|     <span class="literal-code"> | ||||
|                 {message.context.operation.join(".")} | ||||
|               </span> | ||||
|       {message.context.operation.join(".")} | ||||
|     </span> | ||||
|   </li> | ||||
| {/each} | ||||
|  |  | |||
|  | @ -1,134 +1,130 @@ | |||
| <script lang="ts">/** | ||||
|  * Allows to create `and` and `or` expressions graphically | ||||
|  */ | ||||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Allows to create `and` and `or` expressions graphically | ||||
|    */ | ||||
| 
 | ||||
| import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
| import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"; | ||||
| import BasicTagInput from "./TagInput/BasicTagInput.svelte"; | ||||
| import FullTagInput from "./TagInput/FullTagInput.svelte"; | ||||
| import { TrashIcon } from "@babeard/svelte-heroicons/mini"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||
|   import BasicTagInput from "./TagInput/BasicTagInput.svelte" | ||||
|   import FullTagInput from "./TagInput/FullTagInput.svelte" | ||||
|   import { TrashIcon } from "@babeard/svelte-heroicons/mini" | ||||
| 
 | ||||
| export let tag: UIEventSource<TagConfigJson>; | ||||
| let mode: "and" | "or" = "and"; | ||||
|   export let tag: UIEventSource<TagConfigJson> | ||||
|   let mode: "and" | "or" = "and" | ||||
| 
 | ||||
| let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([]); | ||||
|   let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([]) | ||||
| 
 | ||||
| /** | ||||
|  * Sub-expressions | ||||
|  */ | ||||
| let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([]); | ||||
|   /** | ||||
|    * Sub-expressions | ||||
|    */ | ||||
|   let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([]) | ||||
| 
 | ||||
| export let uploadableOnly: boolean; | ||||
| export let overpassSupportNeeded: boolean; | ||||
|   export let uploadableOnly: boolean | ||||
|   export let overpassSupportNeeded: boolean | ||||
| 
 | ||||
| export let silent: boolean; | ||||
|   export let silent: boolean | ||||
| 
 | ||||
| function update(_) { | ||||
|   let config: TagConfigJson = <any>{}; | ||||
|   if (!mode) { | ||||
|     return; | ||||
|   } | ||||
|   const tags = []; | ||||
| 
 | ||||
|   const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data); | ||||
|   for (const src of subpartSources) { | ||||
|     const t = src.data; | ||||
|     if (!t) { | ||||
|       // We indicate upstream that this value is invalid | ||||
|       tag.setData(undefined); | ||||
|       return; | ||||
|   function update(_) { | ||||
|     let config: TagConfigJson = <any>{} | ||||
|     if (!mode) { | ||||
|       return | ||||
|     } | ||||
|     const tags = [] | ||||
| 
 | ||||
|     const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat( | ||||
|       expressions.data | ||||
|     ) | ||||
|     for (const src of subpartSources) { | ||||
|       const t = src.data | ||||
|       if (!t) { | ||||
|         // We indicate upstream that this value is invalid | ||||
|         tag.setData(undefined) | ||||
|         return | ||||
|       } | ||||
|       tags.push(t) | ||||
|     } | ||||
|     if (tags.length === 1) { | ||||
|       tag.setData(tags[0]) | ||||
|     } else { | ||||
|       config[mode] = tags | ||||
|       tag.setData(config) | ||||
|     } | ||||
|     tags.push(t); | ||||
|   } | ||||
|   if (tags.length === 1) { | ||||
|     tag.setData(tags[0]); | ||||
| 
 | ||||
|   function addBasicTag(value?: string) { | ||||
|     const src = new UIEventSource(value) | ||||
|     basicTags.data.push(src) | ||||
|     basicTags.ping() | ||||
|     src.addCallbackAndRunD((_) => update(_)) | ||||
|   } | ||||
| 
 | ||||
|   function removeTag(basicTag: UIEventSource<any>) { | ||||
|     const index = basicTags.data.indexOf(basicTag) | ||||
|     console.log("Removing", index, basicTag) | ||||
|     if (index >= 0) { | ||||
|       basicTag.setData(undefined) | ||||
|       basicTags.data.splice(index, 1) | ||||
|       basicTags.ping() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function removeExpression(expr: UIEventSource<any>) { | ||||
|     const index = expressions.data.indexOf(expr) | ||||
|     if (index >= 0) { | ||||
|       expr.setData(undefined) | ||||
|       expressions.data.splice(index, 1) | ||||
|       expressions.ping() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function addExpression(expr?: TagConfigJson) { | ||||
|     const src = new UIEventSource(expr) | ||||
|     expressions.data.push(src) | ||||
|     expressions.ping() | ||||
|     src.addCallbackAndRunD((_) => update(_)) | ||||
|   } | ||||
| 
 | ||||
|   $: update(mode) | ||||
|   expressions.addCallback((_) => update(_)) | ||||
|   basicTags.addCallback((_) => update(_)) | ||||
| 
 | ||||
|   let initialTag: TagConfigJson = tag.data | ||||
| 
 | ||||
|   function initWith(initialTag: TagConfigJson) { | ||||
|     if (typeof initialTag === "string") { | ||||
|       if (initialTag.startsWith("{")) { | ||||
|         initialTag = JSON.parse(initialTag) | ||||
|       } else { | ||||
|         addBasicTag(initialTag) | ||||
|         return | ||||
|       } | ||||
|     } | ||||
|     mode = <"or" | "and">Object.keys(initialTag)[0] | ||||
|     const subExprs = <TagConfigJson[]>initialTag[mode] | ||||
|     if (!subExprs || subExprs.length == 0) { | ||||
|       return | ||||
|     } | ||||
|     if (subExprs.length == 1) { | ||||
|       initWith(subExprs[0]) | ||||
|       return | ||||
|     } | ||||
|     for (const subExpr of subExprs) { | ||||
|       if (typeof subExpr === "string") { | ||||
|         addBasicTag(subExpr) | ||||
|       } else { | ||||
|         addExpression(subExpr) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (!initialTag) { | ||||
|     addBasicTag() | ||||
|   } else { | ||||
|     config[mode] = tags; | ||||
|     tag.setData(config); | ||||
|     initWith(initialTag) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function addBasicTag(value?: string) { | ||||
|   const src = new UIEventSource(value); | ||||
|   basicTags.data.push(src); | ||||
|   basicTags.ping(); | ||||
|   src.addCallbackAndRunD(_ => update(_)); | ||||
| } | ||||
| 
 | ||||
| function removeTag(basicTag: UIEventSource<any>) { | ||||
|   const index = basicTags.data.indexOf(basicTag); | ||||
|   console.log("Removing", index, basicTag); | ||||
|   if (index >= 0) { | ||||
|     basicTag.setData(undefined); | ||||
|     basicTags.data.splice(index, 1); | ||||
|     basicTags.ping(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function removeExpression(expr: UIEventSource<any>) { | ||||
|   const index = expressions.data.indexOf(expr); | ||||
|   if (index >= 0) { | ||||
|     expr.setData(undefined); | ||||
|     expressions.data.splice(index, 1); | ||||
|     expressions.ping(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function addExpression(expr?: TagConfigJson) { | ||||
|   const src = new UIEventSource(expr); | ||||
|   expressions.data.push(src); | ||||
|   expressions.ping(); | ||||
|   src.addCallbackAndRunD(_ => update(_)); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| $: update(mode); | ||||
| expressions.addCallback(_ => update(_)); | ||||
| basicTags.addCallback(_ => update(_)); | ||||
| 
 | ||||
| let initialTag: TagConfigJson = tag.data; | ||||
| 
 | ||||
| function initWith(initialTag: TagConfigJson) { | ||||
|   if (typeof initialTag === "string") { | ||||
|     if (initialTag.startsWith("{")) { | ||||
|       initialTag = JSON.parse(initialTag); | ||||
|     } else { | ||||
|       addBasicTag(initialTag); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   mode = <"or" | "and">Object.keys(initialTag)[0]; | ||||
|   const subExprs = (<TagConfigJson[]>initialTag[mode]); | ||||
|   if (!subExprs || subExprs.length == 0) { | ||||
|     return; | ||||
|   } | ||||
|   if (subExprs.length == 1) { | ||||
|     initWith(subExprs[0]); | ||||
|     return; | ||||
|   } | ||||
|   for (const subExpr of subExprs) { | ||||
|     if (typeof subExpr === "string") { | ||||
|       addBasicTag(subExpr); | ||||
|     } else { | ||||
|       addExpression(subExpr); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| if (!initialTag) { | ||||
|   addBasicTag(); | ||||
| } else { | ||||
|   initWith(initialTag); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <div class="flex items-center"> | ||||
| 
 | ||||
|   {#if !uploadableOnly} | ||||
|     <select bind:value={mode}> | ||||
|       <option value="and">and</option> | ||||
|  | @ -136,14 +132,16 @@ if (!initialTag) { | |||
|     </select> | ||||
|   {/if} | ||||
| 
 | ||||
|   <div class="border-l-4 border-black flex flex-col ml-1 pl-1"> | ||||
|   <div class="ml-1 flex flex-col border-l-4 border-black pl-1"> | ||||
|     {#each $basicTags as basicTag (basicTag)} | ||||
|       <div class="flex"> | ||||
|         <BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} on:submit /> | ||||
|         {#if $basicTags.length + $expressions.length > 1} | ||||
|           <button class="border border-black rounded-full w-fit h-fit p-0" | ||||
|                   on:click={() => removeTag(basicTag)}> | ||||
|             <TrashIcon class="w-4 h-4 p-1" /> | ||||
|           <button | ||||
|             class="h-fit w-fit rounded-full border border-black p-0" | ||||
|             on:click={() => removeTag(basicTag)} | ||||
|           > | ||||
|             <TrashIcon class="h-4 w-4 p-1" /> | ||||
|           </button> | ||||
|         {/if} | ||||
|       </div> | ||||
|  | @ -151,23 +149,18 @@ if (!initialTag) { | |||
|     {#each $expressions as expression} | ||||
|       <FullTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={expression}> | ||||
|         <button class="small" slot="delete" on:click={() => removeExpression(expression)}> | ||||
|           <TrashIcon class="w-3 h-3 p-0" /> | ||||
|           <TrashIcon class="h-3 w-3 p-0" /> | ||||
|           Delete subexpression | ||||
|         </button> | ||||
|       </FullTagInput> | ||||
|     {/each} | ||||
|     <div class="flex"> | ||||
|       <button class="w-fit small" on:click={() => addBasicTag()}> | ||||
|         Add a tag | ||||
|       </button> | ||||
|       <button class="small w-fit" on:click={() => addBasicTag()}>Add a tag</button> | ||||
|       {#if !uploadableOnly} | ||||
|         <!-- Do not allow to add an expression, as everything is 'and' anyway --> | ||||
|         <button class="w-fit small" on:click={() => addExpression()}> | ||||
|           Add an expression | ||||
|         </button> | ||||
|         <button class="small w-fit" on:click={() => addExpression()}>Add an expression</button> | ||||
|       {/if} | ||||
|       <slot name="delete" /> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,63 +1,67 @@ | |||
| <script lang="ts">/** | ||||
|  * A small component showing statistics from tagInfo. | ||||
|  * Will show this in an 'alert' if very little (<250) tags are known | ||||
|  */ | ||||
| import { TagUtils } from "../../Logic/Tags/TagUtils"; | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||
| import type { TagInfoStats } from "../../Logic/Web/TagInfo"; | ||||
| import TagInfo from "../../Logic/Web/TagInfo"; | ||||
| import { twMerge } from "tailwind-merge"; | ||||
| import Loading from "../Base/Loading.svelte"; | ||||
| <script lang="ts"> | ||||
|   /** | ||||
|    * A small component showing statistics from tagInfo. | ||||
|    * Will show this in an 'alert' if very little (<250) tags are known | ||||
|    */ | ||||
|   import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { TagInfoStats } from "../../Logic/Web/TagInfo" | ||||
|   import TagInfo from "../../Logic/Web/TagInfo" | ||||
|   import { twMerge } from "tailwind-merge" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
| 
 | ||||
| export let silent = false; | ||||
| export let tag: UIEventSource<string>; | ||||
| const tagStabilized = tag.stabilized(500); | ||||
| const tagInfoStats: Store<TagInfoStats> = tagStabilized.bind(tag => { | ||||
|   export let silent = false | ||||
|   export let tag: UIEventSource<string> | ||||
|   const tagStabilized = tag.stabilized(500) | ||||
|   const tagInfoStats: Store<TagInfoStats> = tagStabilized.bind((tag) => { | ||||
|     if (!tag) { | ||||
|         return undefined; | ||||
|       return undefined | ||||
|     } | ||||
|     try { | ||||
| 
 | ||||
|         const t = TagUtils.Tag(tag); | ||||
|         const k = t["key"]; | ||||
|         let v = t["value"]; | ||||
|         if (typeof v !== "string") { | ||||
|             v = undefined; | ||||
|         } | ||||
|         if (!k) { | ||||
|             return undefined; | ||||
|         } | ||||
|         return UIEventSource.FromPromise(TagInfo.global.getStats(k, v)); | ||||
|       const t = TagUtils.Tag(tag) | ||||
|       const k = t["key"] | ||||
|       let v = t["value"] | ||||
|       if (typeof v !== "string") { | ||||
|         v = undefined | ||||
|       } | ||||
|       if (!k) { | ||||
|         return undefined | ||||
|       } | ||||
|       return UIEventSource.FromPromise(TagInfo.global.getStats(k, v)) | ||||
|     } catch (e) { | ||||
|         return undefined; | ||||
|       return undefined | ||||
|     } | ||||
| }); | ||||
| const tagInfoUrl: Store<string> = tagStabilized.mapD(tag => { | ||||
|   }) | ||||
|   const tagInfoUrl: Store<string> = tagStabilized.mapD((tag) => { | ||||
|     try { | ||||
| 
 | ||||
|         const t = TagUtils.Tag(tag); | ||||
|         const k = t["key"]; | ||||
|         let v = t["value"]; | ||||
|         if (typeof v !== "string") { | ||||
|             v = undefined; | ||||
|         } | ||||
|         if (!k) { | ||||
|             return undefined; | ||||
|         } | ||||
|         return TagInfo.global.webUrl(k, v); | ||||
|       const t = TagUtils.Tag(tag) | ||||
|       const k = t["key"] | ||||
|       let v = t["value"] | ||||
|       if (typeof v !== "string") { | ||||
|         v = undefined | ||||
|       } | ||||
|       if (!k) { | ||||
|         return undefined | ||||
|       } | ||||
|       return TagInfo.global.webUrl(k, v) | ||||
|     } catch (e) { | ||||
|         return undefined; | ||||
|       return undefined | ||||
|     } | ||||
| }); | ||||
| const total = tagInfoStats.mapD(data => data.data.find(i => i.type === "all").count); | ||||
|   }) | ||||
|   const total = tagInfoStats.mapD((data) => data.data.find((i) => i.type === "all").count) | ||||
| </script> | ||||
| 
 | ||||
| {#if $tagStabilized !== $tag} | ||||
|     {#if !silent} | ||||
|         <Loading /> | ||||
|     {/if} | ||||
| {:else if $tagInfoStats && (!silent || $total < 250) } | ||||
|     <a href={$tagInfoUrl} target="_blank" class={twMerge(($total < 250) ? "alert" : "thanks", "w-fit link-underline")}> | ||||
|         {$total} features have <span class="literal-code">{$tag}</span> | ||||
|     </a> | ||||
|   {#if !silent} | ||||
|     <Loading /> | ||||
|   {/if} | ||||
| {:else if $tagInfoStats && (!silent || $total < 250)} | ||||
|   <a | ||||
|     href={$tagInfoUrl} | ||||
|     target="_blank" | ||||
|     class={twMerge($total < 250 ? "alert" : "thanks", "link-underline w-fit")} | ||||
|   > | ||||
|     {$total} features have | ||||
|     <span class="literal-code">{$tag}</span> | ||||
|   </a> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,134 +1,138 @@ | |||
| <script lang="ts"> | ||||
|   import ValidatedInput from "../../InputElement/ValidatedInput.svelte" | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import { onDestroy } from "svelte" | ||||
|   import Tr from "../../Base/Tr.svelte" | ||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils" | ||||
|   import TagInfoStats from "../TagInfoStats.svelte" | ||||
|   import { Translation } from "../../i18n/Translation" | ||||
| 
 | ||||
|     import ValidatedInput from "../../InputElement/ValidatedInput.svelte"; | ||||
|     import {UIEventSource} from "../../../Logic/UIEventSource"; | ||||
|     import {onDestroy} from "svelte"; | ||||
|     import Tr from "../../Base/Tr.svelte"; | ||||
|     import {TagUtils} from "../../../Logic/Tags/TagUtils"; | ||||
|     import TagInfoStats from "../TagInfoStats.svelte"; | ||||
|     import { Translation } from "../../i18n/Translation"; | ||||
|   export let tag: UIEventSource<string> = new UIEventSource<string>(undefined) | ||||
|   export let uploadableOnly: boolean | ||||
|   export let overpassSupportNeeded: boolean | ||||
| 
 | ||||
|     export let tag: UIEventSource<string> = new UIEventSource<string>(undefined) | ||||
|     export let uploadableOnly: boolean | ||||
|     export let overpassSupportNeeded: boolean | ||||
|      | ||||
|     export  let dropdownFocussed = new UIEventSource(false) | ||||
|   export let dropdownFocussed = new UIEventSource(false) | ||||
| 
 | ||||
|     /** | ||||
|      * If set, do not show tagInfo if there are many features matching | ||||
|      */ | ||||
|     export let silent : boolean = false | ||||
|      | ||||
|     export let selected: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|      | ||||
|     let feedbackGlobal = tag.map(tag => { | ||||
|         if (!tag) { | ||||
|             return undefined | ||||
|         } | ||||
|         try { | ||||
|             TagUtils.Tag(tag) | ||||
|             return undefined | ||||
|         } catch (e) { | ||||
|             return e | ||||
|         } | ||||
|   /** | ||||
|    * If set, do not show tagInfo if there are many features matching | ||||
|    */ | ||||
|   export let silent: boolean = false | ||||
| 
 | ||||
|     }) | ||||
|   export let selected: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
| 
 | ||||
|     let feedbackKey = new UIEventSource<Translation>(undefined) | ||||
|     let keyValue = new UIEventSource<string>(undefined) | ||||
| 
 | ||||
| 
 | ||||
|     let feedbackValue = new UIEventSource<Translation>(undefined) | ||||
|     /** | ||||
|      * The value of the tag. The name is a bit confusing | ||||
|      */ | ||||
|     let valueValue = new UIEventSource<string>(undefined) | ||||
| 
 | ||||
| 
 | ||||
|     export let mode: string = "=" | ||||
|     let modes: string[] = [] | ||||
| 
 | ||||
|     for (const k in TagUtils.modeDocumentation) { | ||||
|         const docs = TagUtils.modeDocumentation[k] | ||||
|         if (overpassSupportNeeded && !docs.overpassSupport) { | ||||
|             continue | ||||
|         } | ||||
|         if (uploadableOnly && !docs.uploadable) { | ||||
|             continue | ||||
|         } | ||||
|         modes.push(k) | ||||
|   let feedbackGlobal = tag.map((tag) => { | ||||
|     if (!tag) { | ||||
|       return undefined | ||||
|     } | ||||
|     if (!uploadableOnly && !overpassSupportNeeded) { | ||||
|         modes.push(...TagUtils.comparators.map(c => c[0])) | ||||
|     try { | ||||
|       TagUtils.Tag(tag) | ||||
|       return undefined | ||||
|     } catch (e) { | ||||
|       return e | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   let feedbackKey = new UIEventSource<Translation>(undefined) | ||||
|   let keyValue = new UIEventSource<string>(undefined) | ||||
| 
 | ||||
|     if (tag.data) { | ||||
|         const sortedModes = [...modes] | ||||
|         sortedModes.sort((a, b) => b.length - a.length) | ||||
|         const t = tag.data | ||||
|         console.log(t) | ||||
|         for (const m of sortedModes) { | ||||
|             if (t.indexOf(m) >= 0) { | ||||
|                 const [k, v] = t.split(m) | ||||
|                 keyValue.setData(k) | ||||
|                 valueValue.setData(v) | ||||
|                 mode = m | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|   let feedbackValue = new UIEventSource<Translation>(undefined) | ||||
|   /** | ||||
|    * The value of the tag. The name is a bit confusing | ||||
|    */ | ||||
|   let valueValue = new UIEventSource<string>(undefined) | ||||
| 
 | ||||
|   export let mode: string = "=" | ||||
|   let modes: string[] = [] | ||||
| 
 | ||||
|   for (const k in TagUtils.modeDocumentation) { | ||||
|     const docs = TagUtils.modeDocumentation[k] | ||||
|     if (overpassSupportNeeded && !docs.overpassSupport) { | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     onDestroy(valueValue.addCallbackAndRun(setTag)) | ||||
|     onDestroy(keyValue.addCallbackAndRun(setTag)) | ||||
| 
 | ||||
|     $: { | ||||
|         setTag(mode) | ||||
|     if (uploadableOnly && !docs.uploadable) { | ||||
|       continue | ||||
|     } | ||||
|     modes.push(k) | ||||
|   } | ||||
|   if (!uploadableOnly && !overpassSupportNeeded) { | ||||
|     modes.push(...TagUtils.comparators.map((c) => c[0])) | ||||
|   } | ||||
| 
 | ||||
|     function setTag(_) { | ||||
|         const k = keyValue.data | ||||
|         const v = valueValue.data ?? "" | ||||
|         if(k === undefined || k === ""){ | ||||
|             tag.setData(undefined) | ||||
|             return | ||||
|         } | ||||
|         const t = k + mode + v | ||||
|         try { | ||||
|             TagUtils.Tag(t) | ||||
|             tag.setData(t) | ||||
|         } catch (e) { | ||||
|             tag.setData(undefined) | ||||
|         } | ||||
|   if (tag.data) { | ||||
|     const sortedModes = [...modes] | ||||
|     sortedModes.sort((a, b) => b.length - a.length) | ||||
|     const t = tag.data | ||||
|     console.log(t) | ||||
|     for (const m of sortedModes) { | ||||
|       if (t.indexOf(m) >= 0) { | ||||
|         const [k, v] = t.split(m) | ||||
|         keyValue.setData(k) | ||||
|         valueValue.setData(v) | ||||
|         mode = m | ||||
|         break | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onDestroy(valueValue.addCallbackAndRun(setTag)) | ||||
|   onDestroy(keyValue.addCallbackAndRun(setTag)) | ||||
| 
 | ||||
|   $: { | ||||
|     setTag(mode) | ||||
|   } | ||||
| 
 | ||||
|   function setTag(_) { | ||||
|     const k = keyValue.data | ||||
|     const v = valueValue.data ?? "" | ||||
|     if (k === undefined || k === "") { | ||||
|       tag.setData(undefined) | ||||
|       return | ||||
|     } | ||||
|     const t = k + mode + v | ||||
|     try { | ||||
|       TagUtils.Tag(t) | ||||
|       tag.setData(t) | ||||
|     } catch (e) { | ||||
|       tag.setData(undefined) | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <div class="flex items-center"> | ||||
|   <div class="flex h-fit"> | ||||
|     <ValidatedInput | ||||
|       feedback={feedbackKey} | ||||
|       placeholder="The key of the tag" | ||||
|       type="key" | ||||
|       value={keyValue} | ||||
|       on:submit | ||||
|     /> | ||||
|     <select | ||||
|       bind:value={mode} | ||||
|       on:focusin={() => dropdownFocussed.setData(true)} | ||||
|       on:focusout={() => dropdownFocussed.setData(false)} | ||||
|     > | ||||
|       {#each modes as option} | ||||
|         <option value={option}> | ||||
|           {option} | ||||
|         </option> | ||||
|       {/each} | ||||
|     </select> | ||||
|     <ValidatedInput | ||||
|       feedback={feedbackValue} | ||||
|       placeholder="The value of the tag" | ||||
|       type="string" | ||||
|       value={valueValue} | ||||
|       on:submit | ||||
|     /> | ||||
|   </div> | ||||
| 
 | ||||
|     <div class="flex h-fit "> | ||||
| 
 | ||||
|         <ValidatedInput feedback={feedbackKey} placeholder="The key of the tag" type="key" | ||||
|                         value={keyValue} on:submit></ValidatedInput> | ||||
|         <select bind:value={mode} on:focusin={() => dropdownFocussed.setData(true)} on:focusout={() => dropdownFocussed.setData(false)}> | ||||
|             {#each modes as option} | ||||
|                 <option value={option}> | ||||
|                     {option} | ||||
|                 </option> | ||||
|             {/each} | ||||
|         </select> | ||||
|         <ValidatedInput feedback={feedbackValue} placeholder="The value of the tag" type="string" | ||||
|                         value={valueValue} on:submit></ValidatedInput> | ||||
|     </div> | ||||
| 
 | ||||
|     {#if $feedbackKey} | ||||
|         <Tr cls="alert" t={$feedbackKey}/> | ||||
|     {:else if $feedbackValue} | ||||
|         <Tr cls="alert" t={$feedbackValue}/> | ||||
|     {:else if $feedbackGlobal} | ||||
|         <Tr cls="alert" t={$feedbackGlobal}/> | ||||
|     {/if} | ||||
|     <TagInfoStats {silent} {tag}/> | ||||
|   {#if $feedbackKey} | ||||
|     <Tr cls="alert" t={$feedbackKey} /> | ||||
|   {:else if $feedbackValue} | ||||
|     <Tr cls="alert" t={$feedbackValue} /> | ||||
|   {:else if $feedbackGlobal} | ||||
|     <Tr cls="alert" t={$feedbackGlobal} /> | ||||
|   {/if} | ||||
|   <TagInfoStats {silent} {tag} /> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,19 +1,19 @@ | |||
| <script lang="ts">/** | ||||
|  * An element input a tag; has `and`, `or`, `regex`, ... | ||||
|  */ | ||||
| import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson"; | ||||
| import { UIEventSource } from "../../../Logic/UIEventSource"; | ||||
| import TagExpression from "../TagExpression.svelte"; | ||||
| <script lang="ts"> | ||||
|   /** | ||||
|    * An element input a tag; has `and`, `or`, `regex`, ... | ||||
|    */ | ||||
|   import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson" | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import TagExpression from "../TagExpression.svelte" | ||||
| 
 | ||||
| 
 | ||||
| export let tag: UIEventSource<string | TagConfigJson> | ||||
| export let uploadableOnly: boolean | ||||
| export let overpassSupportNeeded: boolean | ||||
| export let silent: boolean | ||||
|   export let tag: UIEventSource<string | TagConfigJson> | ||||
|   export let uploadableOnly: boolean | ||||
|   export let overpassSupportNeeded: boolean | ||||
|   export let silent: boolean | ||||
| </script> | ||||
| 
 | ||||
| <div class="m-2"> | ||||
|     <TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly} on:submit> | ||||
|         <slot name="delete" slot="delete"/> | ||||
|     </TagExpression> | ||||
|   <TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly} on:submit> | ||||
|     <slot name="delete" slot="delete" /> | ||||
|   </TagExpression> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,14 +1,12 @@ | |||
| <script lang="ts"> | ||||
|      | ||||
| import SchemaBasedInput from "./SchemaBasedInput.svelte"; | ||||
| import EditLayerState from "./EditLayerState"; | ||||
| import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte" | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
| 
 | ||||
| export let state: EditLayerState | ||||
| export let path : (number | string)[] | ||||
| 
 | ||||
| let schema : TagRenderingConfig | ||||
|   export let state: EditLayerState | ||||
|   export let path: (number | string)[] | ||||
| 
 | ||||
|   let schema: TagRenderingConfig | ||||
| </script> | ||||
| 
 | ||||
| XYZ | ||||
|  |  | |||
|  | @ -1,173 +1,227 @@ | |||
| <script lang="ts">/** | ||||
|  * Little helper class to deal with choosing a builtin tagRendering or defining one yourself. | ||||
|  * Breaks the ideology that everything should be schema based | ||||
|  */ | ||||
| import EditLayerState from "./EditLayerState"; | ||||
| import type { ConfigMeta } from "./configMeta"; | ||||
| import type { | ||||
|   MappingConfigJson, | ||||
|   QuestionableTagRenderingConfigJson | ||||
| } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; | ||||
| import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
| import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||
| import * as questions from "../../assets/generated/layers/questions.json"; | ||||
| import MappingInput from "./MappingInput.svelte"; | ||||
| import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline"; | ||||
| import questionableTagRenderingSchemaRaw from "../../assets/schemas/questionabletagrenderingconfigmeta.json"; | ||||
| import SchemaBasedField from "./SchemaBasedField.svelte"; | ||||
| import Region from "./Region.svelte"; | ||||
| import NextButton from "../Base/NextButton.svelte"; | ||||
| import { QuestionMarkCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
| import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"; | ||||
| import { onMount } from "svelte"; | ||||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Little helper class to deal with choosing a builtin tagRendering or defining one yourself. | ||||
|    * Breaks the ideology that everything should be schema based | ||||
|    */ | ||||
|   import EditLayerState from "./EditLayerState" | ||||
|   import type { ConfigMeta } from "./configMeta" | ||||
|   import type { | ||||
|     MappingConfigJson, | ||||
|     QuestionableTagRenderingConfigJson, | ||||
|   } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte" | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import * as questions from "../../assets/generated/layers/questions.json" | ||||
|   import MappingInput from "./MappingInput.svelte" | ||||
|   import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline" | ||||
|   import questionableTagRenderingSchemaRaw from "../../assets/schemas/questionabletagrenderingconfigmeta.json" | ||||
|   import SchemaBasedField from "./SchemaBasedField.svelte" | ||||
|   import Region from "./Region.svelte" | ||||
|   import NextButton from "../Base/NextButton.svelte" | ||||
|   import { QuestionMarkCircleIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" | ||||
|   import { onMount } from "svelte" | ||||
| 
 | ||||
| export let state: EditLayerState; | ||||
| export let schema: ConfigMeta; | ||||
| export let path: (string | number)[]; | ||||
| let expertMode = state.expertMode; | ||||
| const store = state.getStoreFor(path); | ||||
| let value = store.data; | ||||
| let hasSeenIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false")) | ||||
| onMount(() => { | ||||
| if(!hasSeenIntro.data){ | ||||
|   state.showIntro.setData("tagrenderings") | ||||
|   hasSeenIntro.setData(true) | ||||
| } | ||||
| }) | ||||
| /** | ||||
|  * Allows the theme builder to create 'writable' themes. | ||||
|  * Should only be enabled for 'tagrenderings' in the theme, if the source is OSM | ||||
|  */ | ||||
| let allowQuestions: Store<boolean> = (state.configuration.mapD(config => path.at(0) === "tagRenderings" && config.source?.geoJson === undefined)); | ||||
|   export let state: EditLayerState | ||||
|   export let schema: ConfigMeta | ||||
|   export let path: (string | number)[] | ||||
|   let expertMode = state.expertMode | ||||
|   const store = state.getStoreFor(path) | ||||
|   let value = store.data | ||||
|   let hasSeenIntro = UIEventSource.asBoolean( | ||||
|     LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false") | ||||
|   ) | ||||
|   onMount(() => { | ||||
|     if (!hasSeenIntro.data) { | ||||
|       state.showIntro.setData("tagrenderings") | ||||
|       hasSeenIntro.setData(true) | ||||
|     } | ||||
|   }) | ||||
|   /** | ||||
|    * Allows the theme builder to create 'writable' themes. | ||||
|    * Should only be enabled for 'tagrenderings' in the theme, if the source is OSM | ||||
|    */ | ||||
|   let allowQuestions: Store<boolean> = state.configuration.mapD( | ||||
|     (config) => path.at(0) === "tagRenderings" && config.source?.geoJson === undefined | ||||
|   ) | ||||
| 
 | ||||
|   let mappingsBuiltin: MappingConfigJson[] = [] | ||||
|   let perLabel: Record<string, MappingConfigJson> = {} | ||||
|   for (const tr of questions.tagRenderings) { | ||||
|     let description = tr["description"] ?? tr["question"] ?? "No description available" | ||||
|     description = description["en"] ?? description | ||||
|     if (tr["labels"]) { | ||||
|       const labels: string[] = tr["labels"] | ||||
|       for (const label of labels) { | ||||
|         let labelMapping: MappingConfigJson = perLabel[label] | ||||
| 
 | ||||
| let mappingsBuiltin: MappingConfigJson[] = []; | ||||
| let perLabel: Record<string, MappingConfigJson> = {}; | ||||
| for (const tr of questions.tagRenderings) { | ||||
|   let description = tr["description"] ?? tr["question"] ?? "No description available"; | ||||
|   description = description["en"] ?? description; | ||||
|   if (tr["labels"]) { | ||||
|     const labels: string[] = tr["labels"]; | ||||
|     for (const label of labels) { | ||||
|       let labelMapping: MappingConfigJson = perLabel[label]; | ||||
| 
 | ||||
|       if (!labelMapping) { | ||||
|         labelMapping = { | ||||
|           if: "value=" + label, | ||||
|           then: { | ||||
|             en: "Builtin collection <b>" + label + "</b>:" | ||||
|         if (!labelMapping) { | ||||
|           labelMapping = { | ||||
|             if: "value=" + label, | ||||
|             then: { | ||||
|               en: "Builtin collection <b>" + label + "</b>:", | ||||
|             }, | ||||
|           } | ||||
|         }; | ||||
|         perLabel[label] = labelMapping; | ||||
|         mappingsBuiltin.push(labelMapping); | ||||
|           perLabel[label] = labelMapping | ||||
|           mappingsBuiltin.push(labelMapping) | ||||
|         } | ||||
|         labelMapping.then.en = labelMapping.then.en + "<div>" + description + "</div>" | ||||
|       } | ||||
|       labelMapping.then.en = labelMapping.then.en + "<div>" + description + "</div>"; | ||||
|     } | ||||
| 
 | ||||
|     mappingsBuiltin.push({ | ||||
|       if: "value=" + tr["id"], | ||||
|       then: { | ||||
|         en: "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>", | ||||
|       }, | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson>{ | ||||
|     question: "Which builtin element should be shown?", | ||||
|     mappings: mappingsBuiltin, | ||||
|   }) | ||||
| 
 | ||||
|   const tags = new UIEventSource({ value }) | ||||
| 
 | ||||
|   tags.addCallbackAndRunD((tgs) => { | ||||
|     store.setData(tgs["value"]) | ||||
|   }) | ||||
| 
 | ||||
|   let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]) | ||||
| 
 | ||||
|   const topLevelItems: Record<string, ConfigMeta> = {} | ||||
|   for (const item of questionableTagRenderingSchemaRaw) { | ||||
|     if (item.path.length === 1) { | ||||
|       topLevelItems[item.path[0]] = <ConfigMeta>item | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   mappingsBuiltin.push({ | ||||
|     if: "value=" + tr["id"], | ||||
|     then: { | ||||
|       "en": "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>" | ||||
|   function initMappings() { | ||||
|     if (mappings.data === undefined) { | ||||
|       mappings.setData([]) | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson>{ | ||||
|   question: "Which builtin element should be shown?", | ||||
|   mappings: mappingsBuiltin | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| const tags = new UIEventSource({ value }); | ||||
| 
 | ||||
| tags.addCallbackAndRunD(tgs => { | ||||
|   store.setData(tgs["value"]); | ||||
| }); | ||||
| 
 | ||||
| let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]); | ||||
| 
 | ||||
| const topLevelItems: Record<string, ConfigMeta> = {}; | ||||
| for (const item of questionableTagRenderingSchemaRaw) { | ||||
|   if (item.path.length === 1) { | ||||
|     topLevelItems[item.path[0]] = <ConfigMeta>item; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function initMappings() { | ||||
|   if (mappings.data === undefined) { | ||||
|     mappings.setData([]); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const items = new Set(["question", "questionHint", "multiAnswer", "freeform", "render", "condition", "metacondition", "mappings", "icon"]); | ||||
| const ignored = new Set(["labels", "description", "classes"]); | ||||
| 
 | ||||
| const freeformSchemaAll = <ConfigMeta[]>questionableTagRenderingSchemaRaw | ||||
|   .filter(schema => schema.path.length == 2 && schema.path[0] === "freeform" && ($allowQuestions || schema.path[1] === "key")); | ||||
| let freeformSchema = $expertMode ? freeformSchemaAll : freeformSchemaAll.filter(schema => schema.hints?.group !== "expert"); | ||||
| const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0])).map(schema => schema.path.join(".")); | ||||
| console.log({ state }); | ||||
|   const items = new Set([ | ||||
|     "question", | ||||
|     "questionHint", | ||||
|     "multiAnswer", | ||||
|     "freeform", | ||||
|     "render", | ||||
|     "condition", | ||||
|     "metacondition", | ||||
|     "mappings", | ||||
|     "icon", | ||||
|   ]) | ||||
|   const ignored = new Set(["labels", "description", "classes"]) | ||||
| 
 | ||||
|   const freeformSchemaAll = <ConfigMeta[]>( | ||||
|     questionableTagRenderingSchemaRaw.filter( | ||||
|       (schema) => | ||||
|         schema.path.length == 2 && | ||||
|         schema.path[0] === "freeform" && | ||||
|         ($allowQuestions || schema.path[1] === "key") | ||||
|     ) | ||||
|   ) | ||||
|   let freeformSchema = $expertMode | ||||
|     ? freeformSchemaAll | ||||
|     : freeformSchemaAll.filter((schema) => schema.hints?.group !== "expert") | ||||
|   const missing: string[] = questionableTagRenderingSchemaRaw | ||||
|     .filter( | ||||
|       (schema) => | ||||
|         schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0]) | ||||
|     ) | ||||
|     .map((schema) => schema.path.join(".")) | ||||
|   console.log({ state }) | ||||
| </script> | ||||
| 
 | ||||
| {#if typeof $store === "string"} | ||||
|   <div class="flex low-interaction"> | ||||
|     <TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state} | ||||
|                           {tags} /> | ||||
|   <div class="low-interaction flex"> | ||||
|     <TagRenderingEditable | ||||
|       config={configBuiltin} | ||||
|       selectedElement={undefined} | ||||
|       showQuestionIfUnknown={true} | ||||
|       {state} | ||||
|       {tags} | ||||
|     /> | ||||
|     <slot name="upper-right" /> | ||||
|   </div> | ||||
| {:else} | ||||
|   <div class="flex flex-col w-full p-1 gap-y-1 pr-12"> | ||||
|   <div class="flex w-full flex-col gap-y-1 p-1 pr-12"> | ||||
|     <div class="flex justify-end"> | ||||
|       <slot name="upper-right" /> | ||||
|     </div> | ||||
|     {#if $allowQuestions} | ||||
|       <SchemaBasedField startInEditModeIfUnset={true} {state} path={[...path,"question"]} | ||||
|                         schema={topLevelItems["question"]} /> | ||||
|       <SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} /> | ||||
|       <SchemaBasedField | ||||
|         startInEditModeIfUnset={true} | ||||
|         {state} | ||||
|         path={[...path, "question"]} | ||||
|         schema={topLevelItems["question"]} | ||||
|       /> | ||||
|       <SchemaBasedField | ||||
|         {state} | ||||
|         path={[...path, "questionHint"]} | ||||
|         schema={topLevelItems["questionHint"]} | ||||
|       /> | ||||
|     {/if} | ||||
|     {#each ($mappings ?? []) as mapping, i (mapping)} | ||||
|       <div class="flex interactive w-full"> | ||||
|     {#each $mappings ?? [] as mapping, i (mapping)} | ||||
|       <div class="interactive flex w-full"> | ||||
|         <MappingInput {state} path={path.concat(["mappings", i])}> | ||||
|           <button slot="delete" class="rounded-full no-image-background" on:click={() => {  | ||||
|             initMappings(); | ||||
|                       mappings.data.splice(i, 1) | ||||
|                       mappings.ping() | ||||
|                    }}> | ||||
|             <TrashIcon class="w-4 h-4" /> | ||||
|           <button | ||||
|             slot="delete" | ||||
|             class="no-image-background rounded-full" | ||||
|             on:click={() => { | ||||
|               initMappings() | ||||
|               mappings.data.splice(i, 1) | ||||
|               mappings.ping() | ||||
|             }} | ||||
|           > | ||||
|             <TrashIcon class="h-4 w-4" /> | ||||
|           </button> | ||||
|         </MappingInput> | ||||
|       </div> | ||||
|     {/each} | ||||
| 
 | ||||
|     <button class="primary" | ||||
|             on:click={() =>{ initMappings(); mappings.data.push({if: undefined, then: {}}); mappings.ping()} }> | ||||
|     <button | ||||
|       class="primary" | ||||
|       on:click={() => { | ||||
|         initMappings() | ||||
|         mappings.data.push({ if: undefined, then: {} }) | ||||
|         mappings.ping() | ||||
|       }} | ||||
|     > | ||||
|       Add a predefined option | ||||
|     </button> | ||||
| 
 | ||||
|     <SchemaBasedField {state} path={[...path,"multiAnswer"]} schema={topLevelItems["multiAnswer"]} /> | ||||
|     <SchemaBasedField | ||||
|       {state} | ||||
|       path={[...path, "multiAnswer"]} | ||||
|       schema={topLevelItems["multiAnswer"]} | ||||
|     /> | ||||
| 
 | ||||
|     <h3>Text field and input element configuration</h3> | ||||
|     <div class="border-l pl-2 border-gray-800 border-dashed"> | ||||
|       <SchemaBasedField {state} path={[...path,"render"]} schema={topLevelItems["render"]} /> | ||||
|     <div class="border-l border-dashed border-gray-800 pl-2"> | ||||
|       <SchemaBasedField {state} path={[...path, "render"]} schema={topLevelItems["render"]} /> | ||||
|       <Region {state} {path} configs={freeformSchema} /> | ||||
|       <SchemaBasedField {state} path={[...path,"icon"]} schema={topLevelItems["icon"]} /> | ||||
| 
 | ||||
|       <SchemaBasedField {state} path={[...path, "icon"]} schema={topLevelItems["icon"]} /> | ||||
|     </div> | ||||
| 
 | ||||
|     <SchemaBasedField {state} path={[...path,"condition"]} schema={topLevelItems["condition"]} /> | ||||
|     <SchemaBasedField {state} path={[...path, "condition"]} schema={topLevelItems["condition"]} /> | ||||
|     {#if $expertMode} | ||||
|       <SchemaBasedField {state} path={[...path,"metacondition"]} schema={topLevelItems["metacondition"]} /> | ||||
|       <SchemaBasedField | ||||
|         {state} | ||||
|         path={[...path, "metacondition"]} | ||||
|         schema={topLevelItems["metacondition"]} | ||||
|       /> | ||||
|     {/if} | ||||
|     {#each missing as field} | ||||
|       <SchemaBasedField {state} path={[...path,field]} schema={topLevelItems[field]} /> | ||||
|       <SchemaBasedField {state} path={[...path, field]} schema={topLevelItems[field]} /> | ||||
|     {/each} | ||||
| 
 | ||||
|     <NextButton clss="small mt-8" on:click={() => state.showIntro.setData("tagrenderings")}><QuestionMarkCircleIcon class="h-6 w-6"/> Show the introduction again</NextButton> | ||||
|      | ||||
|     <NextButton clss="small mt-8" on:click={() => state.showIntro.setData("tagrenderings")}> | ||||
|       <QuestionMarkCircleIcon class="h-6 w-6" /> Show the introduction again | ||||
|     </NextButton> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,136 +1,157 @@ | |||
| <script lang="ts"> | ||||
|   import NextButton from "./Base/NextButton.svelte" | ||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||
|   import EditLayerState, { EditThemeState } from "./Studio/EditLayerState" | ||||
|   import EditLayer from "./Studio/EditLayer.svelte" | ||||
|   import Loading from "../assets/svg/Loading.svelte" | ||||
|   import StudioServer from "./Studio/StudioServer" | ||||
|   import LoginToggle from "./Base/LoginToggle.svelte" | ||||
|   import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
|   import { QueryParameters } from "../Logic/Web/QueryParameters" | ||||
| 
 | ||||
|   import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json" | ||||
|   import layoutSchemaRaw from "../../src/assets/schemas/layoutconfigmeta.json" | ||||
| 
 | ||||
|   import NextButton from "./Base/NextButton.svelte"; | ||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource"; | ||||
|   import EditLayerState, { EditThemeState } from "./Studio/EditLayerState"; | ||||
|   import EditLayer from "./Studio/EditLayer.svelte"; | ||||
|   import Loading from "../assets/svg/Loading.svelte"; | ||||
|   import StudioServer from "./Studio/StudioServer"; | ||||
|   import LoginToggle from "./Base/LoginToggle.svelte"; | ||||
|   import { OsmConnection } from "../Logic/Osm/OsmConnection"; | ||||
|   import { QueryParameters } from "../Logic/Web/QueryParameters"; | ||||
|   import If from "./Base/If.svelte" | ||||
|   import BackButton from "./Base/BackButton.svelte" | ||||
|   import ChooseLayerToEdit from "./Studio/ChooseLayerToEdit.svelte" | ||||
|   import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" | ||||
|   import FloatOver from "./Base/FloatOver.svelte" | ||||
|   import Walkthrough from "./Walkthrough/Walkthrough.svelte" | ||||
|   import * as intro from "../assets/studio_introduction.json" | ||||
|   import * as intro_tagrenderings from "../assets/studio_tagrenderings_intro.json" | ||||
| 
 | ||||
|   import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json"; | ||||
|   import layoutSchemaRaw from "../../src/assets/schemas/layoutconfigmeta.json"; | ||||
|   import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini" | ||||
|   import type { ConfigMeta } from "./Studio/configMeta" | ||||
|   import EditTheme from "./Studio/EditTheme.svelte" | ||||
|   import * as meta from "../../package.json" | ||||
|   import Checkbox from "./Base/Checkbox.svelte" | ||||
| 
 | ||||
|   import If from "./Base/If.svelte"; | ||||
|   import BackButton from "./Base/BackButton.svelte"; | ||||
|   import ChooseLayerToEdit from "./Studio/ChooseLayerToEdit.svelte"; | ||||
|   import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"; | ||||
|   import FloatOver from "./Base/FloatOver.svelte"; | ||||
|   import Walkthrough from "./Walkthrough/Walkthrough.svelte"; | ||||
|   import * as intro from "../assets/studio_introduction.json"; | ||||
|   import * as intro_tagrenderings from "../assets/studio_tagrenderings_intro.json"; | ||||
|    | ||||
|   import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini"; | ||||
|   import type { ConfigMeta } from "./Studio/configMeta"; | ||||
|   import EditTheme from "./Studio/EditTheme.svelte"; | ||||
|   import * as meta from "../../package.json"; | ||||
|   import Checkbox from "./Base/Checkbox.svelte"; | ||||
|   export let studioUrl = | ||||
|     window.location.hostname === "127.0.0.2" | ||||
|       ? "http://127.0.0.1:1235" | ||||
|       : "https://studio.mapcomplete.org" | ||||
| 
 | ||||
|   export let studioUrl = window.location.hostname === "127.0.0.2" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org"; | ||||
|   let osmConnection = new OsmConnection( | ||||
|     new OsmConnection({ | ||||
|       oauth_token: QueryParameters.GetQueryParameter( | ||||
|         "oauth_token", | ||||
|         undefined, | ||||
|         "Used to complete the login" | ||||
|       ), | ||||
|     }) | ||||
|   ) | ||||
|   const expertMode = UIEventSource.asBoolean( | ||||
|     osmConnection.GetPreference("studio-expert-mode", "false", { | ||||
|       documentation: "Indicates if more options are shown in mapcomplete studio", | ||||
|     }) | ||||
|   ) | ||||
|   expertMode.addCallbackAndRunD((expert) => console.log("Expert mode is", expert)) | ||||
|   const createdBy = osmConnection.userDetails.data.name | ||||
|   const uid = osmConnection.userDetails.map((ud) => ud?.uid) | ||||
|   const studio = new StudioServer(studioUrl, uid) | ||||
| 
 | ||||
|   let osmConnection = new OsmConnection(new OsmConnection({ | ||||
|     oauth_token: QueryParameters.GetQueryParameter( | ||||
|       "oauth_token", | ||||
|       undefined, | ||||
|       "Used to complete the login" | ||||
|     ) | ||||
|   })); | ||||
|   const expertMode = UIEventSource.asBoolean(osmConnection.GetPreference("studio-expert-mode", "false", { | ||||
|     documentation: "Indicates if more options are shown in mapcomplete studio" | ||||
|   })); | ||||
|   expertMode.addCallbackAndRunD(expert => console.log("Expert mode is", expert)) | ||||
|   const createdBy = osmConnection.userDetails.data.name; | ||||
|   const uid = osmConnection.userDetails.map(ud => ud?.uid); | ||||
|   const studio = new StudioServer(studioUrl, uid); | ||||
|   let layersWithErr = UIEventSource.FromPromiseWithErr(studio.fetchOverview()) | ||||
|   let layers: Store<{ owner: number }[]> = layersWithErr.mapD((l) => | ||||
|     l.success?.filter((l) => l.category === "layers") | ||||
|   ) | ||||
|   let selfLayers = layers.mapD((ls) => ls.filter((l) => l.owner === uid.data), [uid]) | ||||
|   let otherLayers = layers.mapD( | ||||
|     (ls) => ls.filter((l) => l.owner !== undefined && l.owner !== uid.data), | ||||
|     [uid] | ||||
|   ) | ||||
|   let officialLayers = layers.mapD((ls) => ls.filter((l) => l.owner === undefined), [uid]) | ||||
| 
 | ||||
|   let layersWithErr = UIEventSource.FromPromiseWithErr(studio.fetchOverview()); | ||||
|   let layers: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success?.filter(l => l.category === "layers")); | ||||
|   let selfLayers = layers.mapD(ls => ls.filter(l => l.owner === uid.data), [uid]); | ||||
|   let otherLayers = layers.mapD(ls => ls.filter(l => l.owner !== undefined && l.owner !== uid.data), [uid]); | ||||
|   let officialLayers = layers.mapD(ls => ls.filter(l => l.owner === undefined), [uid]); | ||||
|   let themes: Store<{ owner: number }[]> = layersWithErr.mapD((l) => | ||||
|     l.success?.filter((l) => l.category === "themes") | ||||
|   ) | ||||
|   let selfThemes = themes.mapD((ls) => ls.filter((l) => l.owner === uid.data), [uid]) | ||||
|   let otherThemes = themes.mapD( | ||||
|     (ls) => ls.filter((l) => l.owner !== undefined && l.owner !== uid.data), | ||||
|     [uid] | ||||
|   ) | ||||
|   let officialThemes = themes.mapD((ls) => ls.filter((l) => l.owner === undefined), [uid]) | ||||
| 
 | ||||
|   let state: | ||||
|     | undefined | ||||
|     | "edit_layer" | ||||
|     | "edit_theme" | ||||
|     | "editing_layer" | ||||
|     | "editing_theme" | ||||
|     | "loading" = undefined | ||||
| 
 | ||||
|   let themes: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success?.filter(l => l.category === "themes")); | ||||
|   let selfThemes = themes.mapD(ls => ls.filter(l => l.owner === uid.data), [uid]); | ||||
|   let otherThemes = themes.mapD(ls => ls.filter(l => l.owner !== undefined && l.owner !== uid.data), [uid]); | ||||
|   let officialThemes = themes.mapD(ls => ls.filter(l => l.owner === undefined), [uid]); | ||||
| 
 | ||||
|   let state: undefined | "edit_layer" | "edit_theme" | "editing_layer" | "editing_theme" | "loading" = undefined; | ||||
| 
 | ||||
|   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; | ||||
|   let editLayerState = new EditLayerState(layerSchema, studio, osmConnection, { expertMode }); | ||||
|   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw | ||||
|   let editLayerState = new EditLayerState(layerSchema, studio, osmConnection, { expertMode }) | ||||
|   let showIntro = editLayerState.showIntro | ||||
| 
 | ||||
|   const layoutSchema: ConfigMeta[] = <any>layoutSchemaRaw; | ||||
|   let editThemeState = new EditThemeState(layoutSchema, studio, { expertMode }); | ||||
|   const layoutSchema: ConfigMeta[] = <any>layoutSchemaRaw | ||||
|   let editThemeState = new EditThemeState(layoutSchema, studio, { expertMode }) | ||||
| 
 | ||||
|   let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id); | ||||
|   let layerId = editLayerState.configuration.map((layerConfig) => layerConfig.id) | ||||
| 
 | ||||
|   const version = meta.version; | ||||
|   const version = meta.version | ||||
| 
 | ||||
|   async function editLayer(event: Event) { | ||||
|     const layerId: { owner: number, id: string } = event.detail; | ||||
|     state = "loading"; | ||||
|     editLayerState.startSavingUpdates(false); | ||||
|     editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner)); | ||||
|     editLayerState.startSavingUpdates(); | ||||
|     state = "editing_layer"; | ||||
|     const layerId: { owner: number; id: string } = event.detail | ||||
|     state = "loading" | ||||
|     editLayerState.startSavingUpdates(false) | ||||
|     editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner)) | ||||
|     editLayerState.startSavingUpdates() | ||||
|     state = "editing_layer" | ||||
|   } | ||||
| 
 | ||||
|   async function editTheme(event: Event) { | ||||
|     const id: { id: string, owner: number } = event.detail; | ||||
|     state = "loading"; | ||||
|     editThemeState.startSavingUpdates(false); | ||||
|     editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner)); | ||||
|     editThemeState.startSavingUpdates(); | ||||
|     state = "editing_theme"; | ||||
|     const id: { id: string; owner: number } = event.detail | ||||
|     state = "loading" | ||||
|     editThemeState.startSavingUpdates(false) | ||||
|     editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner)) | ||||
|     editThemeState.startSavingUpdates() | ||||
|     state = "editing_theme" | ||||
|   } | ||||
| 
 | ||||
|   async function createNewLayer() { | ||||
|     state = "loading"; | ||||
|     state = "loading" | ||||
|     const initialLayerConfig = { | ||||
|       credits: createdBy, | ||||
|       minzoom: 15, | ||||
|       pointRendering: [ | ||||
|         { | ||||
|           location: ["point", "centroid"], | ||||
|           marker: [{ | ||||
|             icon: "circle", | ||||
|             color: "white" | ||||
|           }] | ||||
|         } | ||||
|           marker: [ | ||||
|             { | ||||
|               icon: "circle", | ||||
|               color: "white", | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       ], | ||||
|       tagRenderings: ["images"], | ||||
|       lineRendering: [{ | ||||
|         width: 1, | ||||
|         color: "blue" | ||||
|       }] | ||||
|     }; | ||||
|     editLayerState.configuration.setData(initialLayerConfig); | ||||
|     editLayerState.startSavingUpdates(); | ||||
|     state = "editing_layer"; | ||||
|       lineRendering: [ | ||||
|         { | ||||
|           width: 1, | ||||
|           color: "blue", | ||||
|         }, | ||||
|       ], | ||||
|     } | ||||
|     editLayerState.configuration.setData(initialLayerConfig) | ||||
|     editLayerState.startSavingUpdates() | ||||
|     state = "editing_layer" | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <If condition={layersWithErr.map(d => d?.error !== undefined)}> | ||||
| <If condition={layersWithErr.map((d) => d?.error !== undefined)}> | ||||
|   <div> | ||||
|     <div class="alert"> | ||||
|       Something went wrong while contacting the MapComplete Studio Server: {$layersWithErr["error"]} | ||||
|     </div> | ||||
|     The server might be offline. Please: | ||||
|     <ul> | ||||
| 
 | ||||
|       <li>Try again in a few minutes</li> | ||||
|       <li> | ||||
|         Try again in a few minutes | ||||
|       </li> | ||||
|       <li> | ||||
|         Contact <a href="https://app.element.io/#/room/#MapComplete:matrix.org">the MapComplete community via the | ||||
|         chat.</a> Someone might be able to help you | ||||
|         Contact <a href="https://app.element.io/#/room/#MapComplete:matrix.org"> | ||||
|           the MapComplete community via the chat. | ||||
|         </a> | ||||
|          Someone might be able to help you | ||||
|       </li> | ||||
|       <li> | ||||
|         File <a href="https://github.com/pietervdvn/MapComplete/issues">an issue</a> | ||||
|  | @ -140,44 +161,53 @@ | |||
|       </li> | ||||
|     </ul> | ||||
|   </div> | ||||
|   <LoginToggle ignoreLoading={true} slot="else" state={{osmConnection}}> | ||||
|   <LoginToggle ignoreLoading={true} slot="else" state={{ osmConnection }}> | ||||
|     <div slot="not-logged-in"> | ||||
|       <NextButton clss="primary" on:click={() => osmConnection.AttemptLogin()}> | ||||
|         Please log in to use MapComplete Studio | ||||
|       </NextButton> | ||||
|     </div> | ||||
|     {#if state === undefined} | ||||
|       <div class="p-4   flex flex-col justify-between h-full"> | ||||
|         <div class="w-full flex flex-col"> | ||||
|       <div class="flex h-full flex-col justify-between p-4"> | ||||
|         <div class="flex w-full flex-col"> | ||||
|           <h1>MapComplete Studio</h1> | ||||
| 
 | ||||
|           <NextButton on:click={() => state = "edit_layer"}> | ||||
|             Edit an existing layer | ||||
|           </NextButton> | ||||
|           <NextButton on:click={() => createNewLayer()}> | ||||
|             Create a new layer | ||||
|           </NextButton> | ||||
|           <NextButton on:click={() => state = "edit_theme"}> | ||||
|             Edit a theme | ||||
|           </NextButton> | ||||
|           <NextButton on:click={() => {editThemeState.configuration.setData({}); state = "editing_theme"}}> | ||||
|           <NextButton on:click={() => (state = "edit_layer")}>Edit an existing layer</NextButton> | ||||
|           <NextButton on:click={() => createNewLayer()}>Create a new layer</NextButton> | ||||
|           <NextButton on:click={() => (state = "edit_theme")}>Edit a theme</NextButton> | ||||
|           <NextButton | ||||
|             on:click={() => { | ||||
|               editThemeState.configuration.setData({}) | ||||
|               state = "editing_theme" | ||||
|             }} | ||||
|           > | ||||
|             Create a new theme | ||||
|           </NextButton> | ||||
|           <NextButton clss="small" on:click={() => {showIntro.setData("intro")} }> | ||||
|             <QuestionMarkCircleIcon class="w-6 h-6" /> | ||||
|           <NextButton | ||||
|             clss="small" | ||||
|             on:click={() => { | ||||
|               showIntro.setData("intro") | ||||
|             }} | ||||
|           > | ||||
|             <QuestionMarkCircleIcon class="h-6 w-6" /> | ||||
|             Show the introduction again | ||||
|           </NextButton> | ||||
|         </div> | ||||
|         <div> | ||||
|            | ||||
|           <Checkbox selected={expertMode} >Enable more options (expert mode)</Checkbox> | ||||
|         <span class="subtle">MapComplete version {version}</span> | ||||
|           <Checkbox selected={expertMode}>Enable more options (expert mode)</Checkbox> | ||||
|           <span class="subtle">MapComplete version {version}</span> | ||||
|         </div> | ||||
|       </div> | ||||
|     {:else if state === "edit_layer"} | ||||
| 
 | ||||
|       <div class="flex flex-col m-4"> | ||||
|         <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio | ||||
|       <div class="m-4 flex flex-col"> | ||||
|         <BackButton | ||||
|           clss="small p-1" | ||||
|           imageClass="w-8 h-8" | ||||
|           on:click={() => { | ||||
|             state = undefined | ||||
|           }} | ||||
|         > | ||||
|           MapComplete Studio | ||||
|         </BackButton> | ||||
|         <h2>Choose a layer to edit</h2> | ||||
|         <ChooseLayerToEdit {osmConnection} layerIds={$selfLayers} on:layerSelected={editLayer}> | ||||
|  | @ -187,12 +217,22 @@ | |||
|         <ChooseLayerToEdit {osmConnection} layerIds={$otherLayers} on:layerSelected={editLayer} /> | ||||
| 
 | ||||
|         <h3>Official layers by MapComplete</h3> | ||||
|         <ChooseLayerToEdit {osmConnection} layerIds={$officialLayers} on:layerSelected={editLayer} /> | ||||
|         <ChooseLayerToEdit | ||||
|           {osmConnection} | ||||
|           layerIds={$officialLayers} | ||||
|           on:layerSelected={editLayer} | ||||
|         /> | ||||
|       </div> | ||||
|     {:else if state === "edit_theme"} | ||||
| 
 | ||||
|       <div class="flex flex-col m-4"> | ||||
|         <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio | ||||
|       <div class="m-4 flex flex-col"> | ||||
|         <BackButton | ||||
|           clss="small p-1" | ||||
|           imageClass="w-8 h-8" | ||||
|           on:click={() => { | ||||
|             state = undefined | ||||
|           }} | ||||
|         > | ||||
|           MapComplete Studio | ||||
|         </BackButton> | ||||
|         <h2>Choose a theme to edit</h2> | ||||
|         <ChooseLayerToEdit {osmConnection} layerIds={$selfThemes} on:layerSelected={editTheme}> | ||||
|  | @ -201,33 +241,57 @@ | |||
|         <h3>Themes by other contributors</h3> | ||||
|         <ChooseLayerToEdit {osmConnection} layerIds={$otherThemes} on:layerSelected={editTheme} /> | ||||
|         <h3>Official themes by MapComplete</h3> | ||||
|         <ChooseLayerToEdit {osmConnection} layerIds={$officialThemes} on:layerSelected={editTheme} /> | ||||
| 
 | ||||
|         <ChooseLayerToEdit | ||||
|           {osmConnection} | ||||
|           layerIds={$officialThemes} | ||||
|           on:layerSelected={editTheme} | ||||
|         /> | ||||
|       </div> | ||||
|     {:else if state === "loading"} | ||||
|       <div class="w-8 h-8"> | ||||
|       <div class="h-8 w-8"> | ||||
|         <Loading /> | ||||
|       </div> | ||||
|     {:else if state === "editing_layer"} | ||||
|       <EditLayer state={editLayerState}> | ||||
|         <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio | ||||
|         <BackButton | ||||
|           clss="small p-1" | ||||
|           imageClass="w-8 h-8" | ||||
|           on:click={() => { | ||||
|             state = undefined | ||||
|           }} | ||||
|         > | ||||
|           MapComplete Studio | ||||
|         </BackButton> | ||||
|       </EditLayer> | ||||
|     {:else if state === "editing_theme"} | ||||
|       <EditTheme state={editThemeState}> | ||||
|         <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio | ||||
|         <BackButton | ||||
|           clss="small p-1" | ||||
|           imageClass="w-8 h-8" | ||||
|           on:click={() => { | ||||
|             state = undefined | ||||
|           }} | ||||
|         > | ||||
|           MapComplete Studio | ||||
|         </BackButton> | ||||
|       </EditTheme> | ||||
|     {/if} | ||||
|   </LoginToggle> | ||||
| </If> | ||||
| 
 | ||||
| 
 | ||||
| {#if {intro, tagrenderings: intro_tagrenderings}[$showIntro]?.sections} | ||||
|   <FloatOver on:close={() => {showIntro.setData("no")}}> | ||||
|     <div class="flex p-4 h-full pr-12"> | ||||
|       <Walkthrough pages={{intro, tagrenderings: intro_tagrenderings}[$showIntro]?.sections} on:done={() => {showIntro.setData("no")}} /> | ||||
| {#if { intro, tagrenderings: intro_tagrenderings }[$showIntro]?.sections} | ||||
|   <FloatOver | ||||
|     on:close={() => { | ||||
|       showIntro.setData("no") | ||||
|     }} | ||||
|   > | ||||
|     <div class="flex h-full p-4 pr-12"> | ||||
|       <Walkthrough | ||||
|         pages={{ intro, tagrenderings: intro_tagrenderings }[$showIntro]?.sections} | ||||
|         on:done={() => { | ||||
|           showIntro.setData("no") | ||||
|         }} | ||||
|       /> | ||||
|     </div> | ||||
|   </FloatOver> | ||||
| 
 | ||||
| {/if} | ||||
|  |  | |||
|  | @ -21,7 +21,6 @@ | |||
|     <div class="information">Some important information</div> | ||||
|     <div class="thanks">Thank you! Operation successful</div> | ||||
| 
 | ||||
| 
 | ||||
|     <ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} /> | ||||
|     <Loading>Loading...</Loading> | ||||
|   </div> | ||||
|  | @ -46,7 +45,7 @@ | |||
|         <ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} /> | ||||
|         Main action (disabled) | ||||
|       </button> | ||||
|        | ||||
| 
 | ||||
|       <button class="small"> | ||||
|         <ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} /> | ||||
|         Small button | ||||
|  | @ -89,7 +88,7 @@ | |||
|     <div class="warning">Warning</div> | ||||
|     <div class="information">Some important information</div> | ||||
|     <div class="thanks">Thank you! Operation successful</div> | ||||
|      | ||||
| 
 | ||||
|     <ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} /> | ||||
|     <Loading>Loading...</Loading> | ||||
|   </div> | ||||
|  | @ -130,7 +129,6 @@ | |||
|     <div class="information">Some important information</div> | ||||
|     <div class="thanks">Thank you! Operation successful</div> | ||||
| 
 | ||||
| 
 | ||||
|     <ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} /> | ||||
|     <Loading>Loading...</Loading> | ||||
|     <div> | ||||
|  | @ -147,7 +145,7 @@ | |||
|         JavaScript | ||||
|       </label> | ||||
|     </div> | ||||
|      | ||||
| 
 | ||||
|     <div class="border-interactive"> | ||||
|       Area with extreme high interactivity due to `border-interactive` | ||||
|     </div> | ||||
|  |  | |||
|  | @ -76,7 +76,12 @@ | |||
|       } | ||||
| 
 | ||||
|       const tags = state.featureProperties.getStore(selectedElement.properties.id) | ||||
|       return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags }).SetClass("h-full w-full") | ||||
|       return new SvelteUIElement(SelectedElementView, { | ||||
|         state, | ||||
|         layer, | ||||
|         selectedElement, | ||||
|         tags, | ||||
|       }).SetClass("h-full w-full") | ||||
|     }, | ||||
|     [selectedLayer] | ||||
|   ) | ||||
|  | @ -290,7 +295,9 @@ | |||
|       selectedElement.setData(undefined) | ||||
|     }} | ||||
|   > | ||||
|     <ToSvelte construct={new VariableUiElement(selectedElementView).SetClass("h-full w-full flex")} /> | ||||
|     <ToSvelte | ||||
|       construct={new VariableUiElement(selectedElementView).SetClass("h-full w-full flex")} | ||||
|     /> | ||||
|   </FloatOver> | ||||
| </If> | ||||
| 
 | ||||
|  | @ -370,7 +377,7 @@ | |||
| 
 | ||||
| <IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}> | ||||
|   <!-- background layer selector --> | ||||
|   <FloatOver   | ||||
|   <FloatOver | ||||
|     on:close={() => { | ||||
|       state.guistate.backgroundLayerSelectionIsOpened.setData(false) | ||||
|     }} | ||||
|  |  | |||
|  | @ -1,32 +1,33 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import nmd from "nano-markdown"; | ||||
|   import { createEventDispatcher } from "svelte"; | ||||
|   import WalkthroughStep from "./WalkthroughStep.svelte"; | ||||
|   import FromHtml from "../Base/FromHtml.svelte"; | ||||
|   import nmd from "nano-markdown" | ||||
|   import { createEventDispatcher } from "svelte" | ||||
|   import WalkthroughStep from "./WalkthroughStep.svelte" | ||||
|   import FromHtml from "../Base/FromHtml.svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * Markdown | ||||
|    */ | ||||
|   export let pages: string[]; | ||||
|   export let pages: string[] | ||||
| 
 | ||||
|   let currentPage: number = 0; | ||||
|   let currentPage: number = 0 | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ done }>(); | ||||
|   const dispatch = createEventDispatcher<{ done }>() | ||||
| 
 | ||||
|   function step(incr: number) { | ||||
|     if (incr > 0 && currentPage + 1 === pages.length) { | ||||
|       dispatch("done"); | ||||
|       dispatch("done") | ||||
|       currentPage = 0 | ||||
|       return | ||||
|     } | ||||
|     currentPage = Math.min(Math.max(0, currentPage + incr), pages.length); | ||||
|     currentPage = Math.min(Math.max(0, currentPage + incr), pages.length) | ||||
|   } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <WalkthroughStep on:back={() => step(-1)} on:next={() => step(1)} isFirst={currentPage === 0} islast={currentPage + 1 === pages.length}> | ||||
| <WalkthroughStep | ||||
|   on:back={() => step(-1)} | ||||
|   on:next={() => step(1)} | ||||
|   isFirst={currentPage === 0} | ||||
|   islast={currentPage + 1 === pages.length} | ||||
| > | ||||
|   <FromHtml src={nmd(pages[currentPage])} /> | ||||
| </WalkthroughStep> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,29 +1,24 @@ | |||
| <script lang="ts"> | ||||
|   import BackButton from "../Base/BackButton.svelte" | ||||
|   import NextButton from "../Base/NextButton.svelte" | ||||
|   import { createEventDispatcher } from "svelte" | ||||
| 
 | ||||
|   import BackButton from "../Base/BackButton.svelte"; | ||||
|   import NextButton from "../Base/NextButton.svelte"; | ||||
|   import { createEventDispatcher } from "svelte"; | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ back, next }>(); | ||||
|   export let islast = false; | ||||
|   const dispatch = createEventDispatcher<{ back; next }>() | ||||
|   export let islast = false | ||||
|   export let isFirst = false | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <div class="flex flex-col h-full w-full justify-between"> | ||||
| 
 | ||||
| <div class="flex h-full w-full flex-col justify-between"> | ||||
|   <div class="overflow-y-auto"> | ||||
|     <slot /> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="flex w-full"> | ||||
|     {#if !isFirst} | ||||
|     <BackButton clss="w-full" on:click={() => dispatch("back")}> | ||||
|       Back | ||||
|     </BackButton> | ||||
|       {:else} | ||||
|       <div class="w-full"/> | ||||
|       {/if} | ||||
|       <BackButton clss="w-full" on:click={() => dispatch("back")}>Back</BackButton> | ||||
|     {:else} | ||||
|       <div class="w-full" /> | ||||
|     {/if} | ||||
|     <NextButton clss="primary w-full" on:click={() => dispatch("next")}> | ||||
|       {#if islast} | ||||
|         Finish | ||||
|  | @ -32,5 +27,4 @@ | |||
|       {/if} | ||||
|     </NextButton> | ||||
|   </div> | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue