Chore: formatting

This commit is contained in:
Pieter Vander Vennet 2023-11-09 16:30:26 +01:00
parent 6c3c67af56
commit 286578bfc7
58 changed files with 2199 additions and 1915 deletions

View file

@ -24,7 +24,8 @@
unknown: t.loginFailedUnreachableMode, unknown: t.loginFailedUnreachableMode,
readonly: t.loginFailedReadonlyMode, readonly: t.loginFailedReadonlyMode,
} }
const apiState = state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online") const apiState =
state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online")
</script> </script>
{#if $badge} {#if $badge}

View file

@ -20,6 +20,8 @@
<slot name="image" slot="image" /> <slot name="image" slot="image" />
<div class="flex w-full items-center justify-between" slot="message"> <div class="flex w-full items-center justify-between" slot="message">
<slot /> <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> </div>
</SubtleButton> </SubtleButton>

View file

@ -3,9 +3,9 @@
* Thin wrapper around 'TabGroup' which binds the state * Thin wrapper around 'TabGroup' which binds the state
*/ */
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"; import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"; import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import { twJoin } from "tailwind-merge"; import { twJoin } from "tailwind-merge"
/** /**
* If a condition is given for a certain tab, it will only be shown if this condition is true. * 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"> <div class="interactive sticky top-0 flex items-center justify-between">
<TabList class="flex flex-wrap"> <TabList class="flex flex-wrap">
{#if $$slots.title0} {#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"> <div bind:this={tabElements[0]} class="flex">
<slot name="title0">Tab 0</slot> <slot name="title0">Tab 0</slot>
</div> </div>
</Tab> </Tab>
{/if} {/if}
{#if $$slots.title1} {#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"> <div bind:this={tabElements[1]} class="flex">
<slot name="title1" /> <slot name="title1" />
</div> </div>
</Tab> </Tab>
{/if} {/if}
{#if $$slots.title2} {#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"> <div bind:this={tabElements[2]} class="flex">
<slot name="title2" /> <slot name="title2" />
</div> </div>
</Tab> </Tab>
{/if} {/if}
{#if $$slots.title3} {#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"> <div bind:this={tabElements[3]} class="flex">
<slot name="title3" /> <slot name="title3" />
</div> </div>
</Tab> </Tab>
{/if} {/if}
{#if $$slots.title4} {#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"> <div bind:this={tabElements[4]} class="flex">
<slot name="title4" /> <slot name="title4" />
</div> </div>
</Tab> </Tab>
{/if} {/if}
{#if $$slots.title5} {#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"> <div bind:this={tabElements[5]} class="flex">
<slot name="title5" /> <slot name="title5" />
</div> </div>
</Tab> </Tab>
{/if} {/if}
{#if $$slots.title6} {#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"> <div bind:this={tabElements[6]} class="flex">
<slot name="title6" /> <slot name="title6" />
</div> </div>

View file

@ -28,7 +28,7 @@
<Tr t={Translations.t.general.returnToTheMap} /> <Tr t={Translations.t.general.returnToTheMap} />
</button> </button>
{:else} {: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)} {#each layer.tagRenderings as config (config.id)}
{#if (config.condition?.matchesProperties($tags) ?? true) && config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true)} {#if (config.condition?.matchesProperties($tags) ?? true) && config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true)}
{#if config.IsKnown($tags)} {#if config.IsKnown($tags)}

View file

@ -27,7 +27,7 @@
mapExtent: state.mapProperties.bounds.data, mapExtent: state.mapProperties.bounds.data,
width: maindiv.offsetWidth, width: maindiv.offsetWidth,
height: maindiv.offsetHeight, height: maindiv.offsetHeight,
noSelfIntersectingLines: true noSelfIntersectingLines: true,
}) })
} }
</script> </script>
@ -84,7 +84,7 @@
helperText={t.downloadAsSvgLinesOnlyHelper} helperText={t.downloadAsSvgLinesOnlyHelper}
construct={() => offerSvg(true)} construct={() => offerSvg(true)}
/> />
<DownloadButton <DownloadButton
{state} {state}
{metaIsIncluded} {metaIsIncluded}

View file

@ -4,7 +4,7 @@
*/ */
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import { ImmutableStore, Store } from "../../Logic/UIEventSource"; import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import type { OsmTags } from "../../Models/OsmFeature" import type { OsmTags } from "../../Models/OsmFeature"
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"

View file

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import {UIEventSource} from "../../../Logic/UIEventSource"; import { UIEventSource } from "../../../Logic/UIEventSource"
/** /**
* Simply shows the image * Simply shows the image
*/ */
export let value: UIEventSource<undefined | string> export let value: UIEventSource<undefined | string>
</script> </script>
<img src={$value}/> <img src={$value} />

View file

@ -1,31 +1,39 @@
<script lang="ts">/** <script lang="ts">
* Input helper to create a tag. The tag is JSON-encoded /**
*/ * Input helper to create a tag. The tag is JSON-encoded
import { UIEventSource } from "../../../Logic/UIEventSource"; */
import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte"; import { UIEventSource } from "../../../Logic/UIEventSource"
import { TagUtils } from "../../../Logic/Tags/TagUtils"; import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte"
import nmd from "nano-markdown" import { TagUtils } from "../../../Logic/Tags/TagUtils"
import FromHtml from "../../Base/FromHtml.svelte"; import nmd from "nano-markdown"
export let value: UIEventSource<undefined | string>; import FromHtml from "../../Base/FromHtml.svelte"
export let args: string[] = []; export let value: UIEventSource<undefined | string>
let uploadableOnly: boolean = args[0] === "uploadableOnly"; export let args: string[] = []
export let overpassSupportNeeded: boolean; let uploadableOnly: boolean = args[0] === "uploadableOnly"
export let overpassSupportNeeded: boolean
/** /**
* Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries) * Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries)
*/ */
export let silent: boolean = false; export let silent: boolean = false
let mode: string = "="; let mode: string = "="
let dropdownFocussed = new UIEventSource(false); let dropdownFocussed = new UIEventSource(false)
let documentation = TagUtils.modeDocumentation[mode]; let documentation = TagUtils.modeDocumentation[mode]
$: documentation = TagUtils.modeDocumentation[mode]; $: documentation = TagUtils.modeDocumentation[mode]
</script> </script>
<BasicTagInput
<BasicTagInput bind:mode={mode} {dropdownFocussed} {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} on:submit /> bind:mode
{dropdownFocussed}
{overpassSupportNeeded}
{silent}
tag={value}
{uploadableOnly}
on:submit
/>
{#if $dropdownFocussed} {#if $dropdownFocussed}
<div class="border border-dashed border-black p-2 m-2"> <div class="m-2 border border-dashed border-black p-2">
<b>{documentation.name}</b> <b>{documentation.name}</b>
<FromHtml src={nmd(documentation.docs)}/> <FromHtml src={nmd(documentation.docs)} />
</div> </div>
{/if} {/if}

View file

@ -1,22 +1,21 @@
<script lang="ts">/** <script lang="ts">
* Input helper to create a tag. The tag is JSON-encoded /**
*/ * 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 { UIEventSource } from "../../../Logic/UIEventSource"
import FullTagInput from "../../Studio/TagInput/FullTagInput.svelte"; import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson"
import FullTagInput from "../../Studio/TagInput/FullTagInput.svelte"
export let value: UIEventSource<TagConfigJson>; export let value: UIEventSource<TagConfigJson>
export let uploadableOnly: boolean; export let uploadableOnly: boolean
export let overpassSupportNeeded: boolean; export let overpassSupportNeeded: boolean
/** /**
* Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries) * Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries)
*/ */
export let silent: boolean = false; export let silent: boolean = false
let tag: UIEventSource<string | TagConfigJson> = value
let tag: UIEventSource<string | TagConfigJson> = value
</script> </script>
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit />
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit/>

View file

@ -1,55 +1,59 @@
<script lang="ts"> <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"; export let value: UIEventSource<Record<string, string>> = new UIEventSource<
import LanguageUtils from "../../../Utils/LanguageUtils"; Record<string, string>
import { createEventDispatcher, onDestroy } from "svelte"; >({})
import ValidatedInput from "../ValidatedInput.svelte";
export let value: UIEventSource<Record<string, string>> = new UIEventSource<Record<string, string>>({});
export let args: string[] = [] export let args: string[] = []
let prefix = args[0] ?? "" let prefix = args[0] ?? ""
let postfix = args[1] ?? "" let postfix = args[1] ?? ""
let translations: UIEventSource<Record<string, string>> = value let translations: UIEventSource<Record<string, string>> = value
const allLanguages: string[] = LanguageUtils.usedLanguagesSorted; const allLanguages: string[] = LanguageUtils.usedLanguagesSorted
let currentLang = new UIEventSource("en"); let currentLang = new UIEventSource("en")
const currentVal = new UIEventSource<string>(""); const currentVal = new UIEventSource<string>("")
let dispatch = createEventDispatcher<{ submit }>() let dispatch = createEventDispatcher<{ submit }>()
function update() { function update() {
const v = currentVal.data; const v = currentVal.data
const l = currentLang.data; const l = currentLang.data
if(translations.data === "" || translations.data === undefined){ if (translations.data === "" || translations.data === undefined) {
translations.data = {} translations.data = {}
} }
if (translations.data[l] === v) { if (translations.data[l] === v) {
return; return
} }
translations.data[l] = v; translations.data[l] = v
translations.ping(); translations.ping()
} }
onDestroy(currentLang.addCallbackAndRunD(currentLang => { onDestroy(
console.log("Applying current lang:", currentLang); currentLang.addCallbackAndRunD((currentLang) => {
if(!translations.data){ console.log("Applying current lang:", currentLang)
translations.data = {} if (!translations.data) {
} translations.data = {}
translations.data[currentLang] = translations.data[currentLang] ?? ""; }
currentVal.setData(translations.data[currentLang]); translations.data[currentLang] = translations.data[currentLang] ?? ""
})); currentVal.setData(translations.data[currentLang])
})
)
onDestroy(currentVal.addCallbackAndRunD(v => {
update();
}));
onDestroy(
currentVal.addCallbackAndRunD((v) => {
update()
})
)
</script> </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> <span>
{prefix} {prefix}
</span> </span>
<select bind:value={$currentLang}> <select bind:value={$currentLang}>
{#each allLanguages as language} {#each allLanguages as language}
@ -58,8 +62,13 @@
</option> </option>
{/each} {/each}
</select> </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> <span>
{postfix} {postfix}
</span> </span>
</div> </div>

View file

@ -4,50 +4,49 @@
* Note that all values are stringified * Note that all values are stringified
*/ */
import { UIEventSource } from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators"; import type { ValidatorType } from "./Validators"
import InputHelpers from "./InputHelpers"; import InputHelpers from "./InputHelpers"
import ToSvelte from "../Base/ToSvelte.svelte"; import ToSvelte from "../Base/ToSvelte.svelte"
import type { Feature } from "geojson"; import type { Feature } from "geojson"
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte"
import ImageHelper from "./Helpers/ImageHelper.svelte"; import ImageHelper from "./Helpers/ImageHelper.svelte"
import TranslationInput from "./Helpers/TranslationInput.svelte"; import TranslationInput from "./Helpers/TranslationInput.svelte"
import TagInput from "./Helpers/TagInput.svelte"; import TagInput from "./Helpers/TagInput.svelte"
import SimpleTagInput from "./Helpers/SimpleTagInput.svelte"; import SimpleTagInput from "./Helpers/SimpleTagInput.svelte"
import DirectionInput from "./Helpers/DirectionInput.svelte"; import DirectionInput from "./Helpers/DirectionInput.svelte"
import DateInput from "./Helpers/DateInput.svelte"; import DateInput from "./Helpers/DateInput.svelte"
import ColorInput from "./Helpers/ColorInput.svelte"; import ColorInput from "./Helpers/ColorInput.svelte"
import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"; import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
export let type: ValidatorType; export let type: ValidatorType
export let value: UIEventSource<string | object>; export let value: UIEventSource<string | object>
export let feature: Feature; export let feature: Feature
export let args: (string | number | boolean)[] = undefined; export let args: (string | number | boolean)[] = undefined
let properties = { feature, args: args ?? [] }; let properties = { feature, args: args ?? [] }
let dispatch = createEventDispatcher<{ let dispatch = createEventDispatcher<{
selected selected
}>(); }>()
</script> </script>
{#if type === "translation" } {#if type === "translation"}
<TranslationInput {value} on:submit {args} /> <TranslationInput {value} on:submit {args} />
{:else if type === "direction"} {:else if type === "direction"}
<DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} /> <DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} />
{:else if type === "date"} {:else if type === "date"}
<DateInput { value } /> <DateInput {value} />
{:else if type === "color"} {:else if type === "color"}
<ColorInput { value } /> <ColorInput {value} />
{:else if type === "image"} {:else if type === "image"}
<ImageHelper { value } /> <ImageHelper {value} />
{:else if type === "tag"} {:else if type === "tag"}
<TagInput { value } on:submit /> <TagInput {value} on:submit />
{:else if type === "simple_tag"} {:else if type === "simple_tag"}
<SimpleTagInput { value } {args} on:submit /> <SimpleTagInput {value} {args} on:submit />
{:else if type === "opening_hours"} {:else if type === "opening_hours"}
<OpeningHoursInput { value } /> <OpeningHoursInput {value} />
{:else if type === "wikidata"} {:else if type === "wikidata"}
<ToSvelte construct={() => InputHelpers.constructWikidataHelper(value, properties)} /> <ToSvelte construct={() => InputHelpers.constructWikidataHelper(value, properties)} />
{/if} {/if}

View file

@ -1,145 +1,156 @@
<script lang="ts"> <script lang="ts">
import {UIEventSource} from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import type {ValidatorType} from "./Validators" import type { ValidatorType } from "./Validators"
import Validators from "./Validators" import Validators from "./Validators"
import {ExclamationIcon} from "@rgossiaux/svelte-heroicons/solid" import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"
import {Translation} from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import {createEventDispatcher, onDestroy} from "svelte" import { createEventDispatcher, onDestroy } from "svelte"
import {Validator} from "./Validator" import { Validator } from "./Validator"
import {Unit} from "../../Models/Unit" import { Unit } from "../../Models/Unit"
import UnitInput from "../Popup/UnitInput.svelte" import UnitInput from "../Popup/UnitInput.svelte"
import {Utils} from "../../Utils"; import { Utils } from "../../Utils"
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge"
export let type: ValidatorType export let type: ValidatorType
export let feedback: UIEventSource<Translation> | undefined = undefined export let feedback: UIEventSource<Translation> | undefined = undefined
export let cls : string = undefined export let cls: string = undefined
export let getCountry: () => string | undefined export let getCountry: () => string | undefined
export let placeholder: string | Translation | undefined export let placeholder: string | Translation | undefined
export let unit: Unit = undefined export let unit: Unit = undefined
export let value: UIEventSource<string> export let value: UIEventSource<string>
/** /**
* Internal state bound to the input element. * Internal state bound to the input element.
* *
* This is only copied to 'value' when appropriate so that no invalid values leak outside; * This is only copied to 'value' when appropriate so that no invalid values leak outside;
* Additionally, the unit is added when copying * Additionally, the unit is added when copying
*/ */
let _value = new UIEventSource(value.data ?? "") let _value = new UIEventSource(value.data ?? "")
let validator: Validator = Validators.get(type ?? "string") let validator: Validator = Validators.get(type ?? "string")
if(validator === undefined){ if (validator === undefined) {
console.warn("Didn't find a validator for type", type) console.warn("Didn't find a validator for type", type)
} }
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined) let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
function initValueAndDenom() { function initValueAndDenom() {
if (unit && value.data) { if (unit && value.data) {
const [v, denom] = unit?.findDenomination(value.data, getCountry) const [v, denom] = unit?.findDenomination(value.data, getCountry)
if (denom) { if (denom) {
_value.setData(v) _value.setData(v)
selectedUnit.setData(denom.canonical) selectedUnit.setData(denom.canonical)
} else { } else {
_value.setData(value.data ?? "") _value.setData(value.data ?? "")
} }
} else { } else {
_value.setData(value.data ?? "") _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() initValueAndDenom()
}
$: { function setValues() {
// The type changed -> reset some values // Update the value stores
validator = Validators.get(type ?? "string") const v = _value.data
if (!validator?.isValid(v, getCountry) || v === "") {
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type feedback?.setData(validator?.getFeedback(v, getCountry))
feedback?.setData(validator?.getFeedback(_value.data, getCountry)) value.setData("")
return
initValueAndDenom()
} }
function setValues() { if (unit !== undefined && isNaN(Number(v))) {
// Update the value stores value.setData(undefined)
const v = _value.data return
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(", ")
} }
const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true) feedback?.setData(undefined)
if (selectedUnit.data) {
let htmlElem: HTMLInputElement value.setData(v + selectedUnit.data)
} else {
let dispatch = createEventDispatcher<{ selected, submit }>() value.setData(v)
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected")
}
} }
}
/** onDestroy(_value.addCallbackAndRun((_) => setValues()))
* Dispatches the submit, but only if the value is valid onDestroy(
*/ value.addCallbackAndRunD((fromUpstream) => {
function sendSubmit(){ if (_value.data !== fromUpstream && fromUpstream !== "") {
if(feedback?.data){ _value.setData(fromUpstream)
console.log("Not sending a submit as there is feedback") }
} })
dispatch("submit") )
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> </script>
{#if validator?.textArea} {#if validator?.textArea}
<form on:submit|preventDefault={() => sendSubmit()}> <form on:submit|preventDefault={() => sendSubmit()}>
<textarea
<textarea class="w-full"
class="w-full" bind:value={$_value}
bind:value={$_value} inputmode={validator?.inputmode ?? "text"}
inputmode={validator?.inputmode ?? "text"} placeholder={_placeholder}
placeholder={_placeholder}></textarea> />
</form> </form>
{:else} {:else}
<form class={twMerge("inline-flex",cls )} on:submit|preventDefault={() => sendSubmit()}> <form class={twMerge("inline-flex", cls)} on:submit|preventDefault={() => sendSubmit()}>
<input <input
bind:this={htmlElem} bind:this={htmlElem}
bind:value={$_value} bind:value={$_value}
class="w-full" class="w-full"
inputmode={validator?.inputmode ?? "text"} inputmode={validator?.inputmode ?? "text"}
placeholder={_placeholder} placeholder={_placeholder}
/> />
{#if !$isValid} {#if !$isValid}
<ExclamationIcon class="-ml-6 h-6 w-6"/> <ExclamationIcon class="-ml-6 h-6 w-6" />
{/if} {/if}
{#if unit !== undefined} {#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value}/> <UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} />
{/if} {/if}
</form> </form>
{/if} {/if}

View file

@ -3,7 +3,6 @@ import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations" import Translations from "../../i18n/Translations"
export default class TagKeyValidator extends Validator { export default class TagKeyValidator extends Validator {
public readonly isMeta = true public readonly isMeta = true
constructor() { constructor() {
super("key", "Validates a key, mostly that no weird characters are used") super("key", "Validates a key, mostly that no weird characters are used")

View file

@ -8,7 +8,6 @@ import SimpleTagValidator from "./SimpleTagValidator"
* Checks that the input conforms a JSON-encoded tag expression or a simpleTag`key=value`, * Checks that the input conforms a JSON-encoded tag expression or a simpleTag`key=value`,
*/ */
export default class TagValidator extends Validator { export default class TagValidator extends Validator {
public readonly isMeta = true public readonly isMeta = true
constructor() { constructor() {
super("tag", "A simple tag of the format `key=value` OR a tagExpression") super("tag", "A simple tag of the format `key=value` OR a tagExpression")

View file

@ -1,7 +1,6 @@
import { Validator } from "../Validator" import { Validator } from "../Validator"
export default class TranslationValidator extends Validator { export default class TranslationValidator extends Validator {
public readonly isMeta = true public readonly isMeta = true
constructor() { constructor() {
super("translation", "Makes sure the the string is of format `Record<string, string>` ") super("translation", "Makes sure the the string is of format `Record<string, string>` ")

View file

@ -3,8 +3,9 @@ import { Validator } from "../Validator"
export default class UrlValidator extends Validator { export default class UrlValidator extends Validator {
constructor(name?: string, explanation?: string) { constructor(name?: string, explanation?: string) {
super( super(
name ??"url", 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", 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" "url"
) )
} }

View file

@ -54,7 +54,7 @@
</td> </td>
<td> <td>
<b>{contributor.nrOfImages}</b> <b>{contributor.nrOfImages}</b>
total images total images
</td> </td>
</tr> </tr>
{/each} {/each}

View file

@ -1,41 +1,40 @@
<script lang="ts"> <script lang="ts">
import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"; import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"
import { Store } from "../../Logic/UIEventSource"; import { Store } from "../../Logic/UIEventSource"
import Pin from "../../assets/svg/Pin.svelte"; import Pin from "../../assets/svg/Pin.svelte"
import Square from "../../assets/svg/Square.svelte"; import Square from "../../assets/svg/Square.svelte"
import Circle from "../../assets/svg/Circle.svelte"; import Circle from "../../assets/svg/Circle.svelte"
import Checkmark from "../../assets/svg/Checkmark.svelte"; import Checkmark from "../../assets/svg/Checkmark.svelte"
import Clock from "../../assets/svg/Clock.svelte"; import Clock from "../../assets/svg/Clock.svelte"
import Close from "../../assets/svg/Close.svelte"; import Close from "../../assets/svg/Close.svelte"
import Crosshair from "../../assets/svg/Crosshair.svelte"; import Crosshair from "../../assets/svg/Crosshair.svelte"
import Help from "../../assets/svg/Help.svelte"; import Help from "../../assets/svg/Help.svelte"
import Home from "../../assets/svg/Home.svelte"; import Home from "../../assets/svg/Home.svelte"
import Invalid from "../../assets/svg/Invalid.svelte"; import Invalid from "../../assets/svg/Invalid.svelte"
import Location from "../../assets/svg/Location.svelte"; import Location from "../../assets/svg/Location.svelte"
import Location_empty from "../../assets/svg/Location_empty.svelte"; import Location_empty from "../../assets/svg/Location_empty.svelte"
import Location_locked from "../../assets/svg/Location_locked.svelte"; import Location_locked from "../../assets/svg/Location_locked.svelte"
import Note from "../../assets/svg/Note.svelte"; import Note from "../../assets/svg/Note.svelte"
import Resolved from "../../assets/svg/Resolved.svelte"; import Resolved from "../../assets/svg/Resolved.svelte"
import Ring from "../../assets/svg/Ring.svelte"; import Ring from "../../assets/svg/Ring.svelte"
import Scissors from "../../assets/svg/Scissors.svelte"; import Scissors from "../../assets/svg/Scissors.svelte"
import Teardrop from "../../assets/svg/Teardrop.svelte"; import Teardrop from "../../assets/svg/Teardrop.svelte"
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"; import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
import Triangle from "../../assets/svg/Triangle.svelte"; import Triangle from "../../assets/svg/Triangle.svelte"
import Icon from "./Icon.svelte"; import Icon from "./Icon.svelte"
/** /**
* Renders a single icon. * Renders a single icon.
* *
* Icons -placed on top of each other- form a 'Marker' together * Icons -placed on top of each other- form a 'Marker' together
*/ */
export let icon: IconConfig; export let icon: IconConfig
export let tags: Store<Record<string, string>>; 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";
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> </script>
<Icon icon={iconItem} {color}/> <Icon icon={iconItem} {color} />

View file

@ -1,19 +1,18 @@
<script lang="ts"> <script lang="ts">
import PointRenderingConfig, { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"
import PointRenderingConfig, { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"; import { Store } from "../../Logic/UIEventSource"
import { Store } from "../../Logic/UIEventSource"; import DynamicIcon from "./DynamicIcon.svelte"
import DynamicIcon from "./DynamicIcon.svelte";
/** /**
* Renders a 'marker', which consists of multiple 'icons' * Renders a 'marker', which consists of multiple 'icons'
*/ */
export let config: PointRenderingConfig; export let config: PointRenderingConfig
let icons: IconConfig[] = config.marker; let icons: IconConfig[] = config.marker
export let tags: Store<Record<string, string>>; export let tags: Store<Record<string, string>>
</script> </script>
{#if config !== undefined} {#if config !== undefined}
<div class="relative w-full h-full"> <div class="relative h-full w-full">
{#each icons as icon} {#each icons as icon}
<DynamicIcon {icon} {tags} /> <DynamicIcon {icon} {tags} />
{/each} {/each}

View file

@ -1,24 +1,24 @@
<script lang="ts"> <script lang="ts">
import Pin from "../../assets/svg/Pin.svelte"; import Pin from "../../assets/svg/Pin.svelte"
import Square from "../../assets/svg/Square.svelte"; import Square from "../../assets/svg/Square.svelte"
import Circle from "../../assets/svg/Circle.svelte"; import Circle from "../../assets/svg/Circle.svelte"
import Checkmark from "../../assets/svg/Checkmark.svelte"; import Checkmark from "../../assets/svg/Checkmark.svelte"
import Clock from "../../assets/svg/Clock.svelte"; import Clock from "../../assets/svg/Clock.svelte"
import Close from "../../assets/svg/Close.svelte"; import Close from "../../assets/svg/Close.svelte"
import Crosshair from "../../assets/svg/Crosshair.svelte"; import Crosshair from "../../assets/svg/Crosshair.svelte"
import Help from "../../assets/svg/Help.svelte"; import Help from "../../assets/svg/Help.svelte"
import Home from "../../assets/svg/Home.svelte"; import Home from "../../assets/svg/Home.svelte"
import Invalid from "../../assets/svg/Invalid.svelte"; import Invalid from "../../assets/svg/Invalid.svelte"
import Location from "../../assets/svg/Location.svelte"; import Location from "../../assets/svg/Location.svelte"
import Location_empty from "../../assets/svg/Location_empty.svelte"; import Location_empty from "../../assets/svg/Location_empty.svelte"
import Location_locked from "../../assets/svg/Location_locked.svelte"; import Location_locked from "../../assets/svg/Location_locked.svelte"
import Note from "../../assets/svg/Note.svelte"; import Note from "../../assets/svg/Note.svelte"
import Resolved from "../../assets/svg/Resolved.svelte"; import Resolved from "../../assets/svg/Resolved.svelte"
import Ring from "../../assets/svg/Ring.svelte"; import Ring from "../../assets/svg/Ring.svelte"
import Scissors from "../../assets/svg/Scissors.svelte"; import Scissors from "../../assets/svg/Scissors.svelte"
import Teardrop from "../../assets/svg/Teardrop.svelte"; import Teardrop from "../../assets/svg/Teardrop.svelte"
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"; import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
import Triangle from "../../assets/svg/Triangle.svelte"; import Triangle from "../../assets/svg/Triangle.svelte"
/** /**
* Renders a single icon. * Renders a single icon.
@ -26,13 +26,12 @@
* Icons -placed on top of each other- form a 'Marker' together * Icons -placed on top of each other- form a 'Marker' together
*/ */
export let icon: string | undefined; export let icon: string | undefined
export let color: string | undefined; export let color: string | undefined
</script> </script>
{#if icon} {#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"} {#if icon === "pin"}
<Pin {color} /> <Pin {color} />
{:else if icon === "square"} {:else if icon === "square"}
@ -74,7 +73,7 @@
{:else if icon === "triangle"} {:else if icon === "triangle"}
<Triangle {color} /> <Triangle {color} />
{:else} {:else}
<img class="w-full h-full" src={icon} /> <img class="h-full w-full" src={icon} />
{/if} {/if}
</div> </div>
{/if} {/if}

View file

@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import Icon from "./Icon.svelte"
import Icon from "./Icon.svelte";
/** /**
* Renders a 'marker', which consists of multiple 'icons' * Renders a 'marker', which consists of multiple 'icons'
*/ */
export let icons: { icon: string, color: string }[] export let icons: { icon: string; color: string }[]
</script> </script>
{#if icons !== undefined && icons.length > 0} {#if icons !== undefined && icons.length > 0}
<div class="relative w-full h-full"> <div class="relative h-full w-full">
{#each icons as icon} {#each icons as icon}
<Icon icon={icon.icon} color={icon.color} /> <Icon icon={icon.icon} color={icon.color} />
{/each} {/each}

View file

@ -423,10 +423,7 @@ class LineRenderingLayer {
} }
export default class ShowDataLayer { export default class ShowDataLayer {
private static rangeLayer = new LayerConfig( private static rangeLayer = new LayerConfig(<any>range_layer, "ShowDataLayer.ts:range.json")
<any>range_layer,
"ShowDataLayer.ts:range.json"
)
private readonly _options: ShowDataLayerOptions & { private readonly _options: ShowDataLayerOptions & {
layer: LayerConfig layer: LayerConfig
drawMarkers?: true | boolean drawMarkers?: true | boolean

View file

@ -3,109 +3,109 @@
* This component ties together all the steps that are needed to create a new point. * This component ties together all the steps that are needed to create a new point.
* There are many subcomponents which help with that * There are many subcomponents which help with that
*/ */
import type { SpecialVisualizationState } from "../../SpecialVisualization"; import type { SpecialVisualizationState } from "../../SpecialVisualization"
import PresetList from "./PresetList.svelte"; import PresetList from "./PresetList.svelte"
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"; import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import Tr from "../../Base/Tr.svelte"; import Tr from "../../Base/Tr.svelte"
import SubtleButton from "../../Base/SubtleButton.svelte"; import SubtleButton from "../../Base/SubtleButton.svelte"
import FromHtml from "../../Base/FromHtml.svelte"; import FromHtml from "../../Base/FromHtml.svelte"
import Translations from "../../i18n/Translations.js"; import Translations from "../../i18n/Translations.js"
import TagHint from "../TagHint.svelte"; import TagHint from "../TagHint.svelte"
import { And } from "../../../Logic/Tags/And.js"; import { And } from "../../../Logic/Tags/And.js"
import LoginToggle from "../../Base/LoginToggle.svelte"; import LoginToggle from "../../Base/LoginToggle.svelte"
import Constants from "../../../Models/Constants.js"; import Constants from "../../../Models/Constants.js"
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer"
import { Store, UIEventSource } from "../../../Logic/UIEventSource"; import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"; import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import LoginButton from "../../Base/LoginButton.svelte"; import LoginButton from "../../Base/LoginButton.svelte"
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"; import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"; import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
import { OsmWay } from "../../../Logic/Osm/OsmObject"; import { OsmWay } from "../../../Logic/Osm/OsmObject"
import { Tag } from "../../../Logic/Tags/Tag"; import { Tag } from "../../../Logic/Tags/Tag"
import type { WayId } from "../../../Models/OsmFeature"; import type { WayId } from "../../../Models/OsmFeature"
import Loading from "../../Base/Loading.svelte"; import Loading from "../../Base/Loading.svelte"
import type { GlobalFilter } from "../../../Models/GlobalFilter"; import type { GlobalFilter } from "../../../Models/GlobalFilter"
import { onDestroy } from "svelte"; import { onDestroy } from "svelte"
import NextButton from "../../Base/NextButton.svelte"; import NextButton from "../../Base/NextButton.svelte"
import BackButton from "../../Base/BackButton.svelte"; import BackButton from "../../Base/BackButton.svelte"
import ToSvelte from "../../Base/ToSvelte.svelte"; import ToSvelte from "../../Base/ToSvelte.svelte"
import Svg from "../../../Svg"; import Svg from "../../../Svg"
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"; import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
import { twJoin } from "tailwind-merge"; import { twJoin } from "tailwind-merge"
export let coordinate: { lon: number; lat: number }; export let coordinate: { lon: number; lat: number }
export let state: SpecialVisualizationState; export let state: SpecialVisualizationState
let selectedPreset: { let selectedPreset: {
preset: PresetConfig preset: PresetConfig
layer: LayerConfig layer: LayerConfig
icon: string icon: string
tags: Record<string, string> tags: Record<string, string>
} = undefined; } = undefined
let checkedOfGlobalFilters: number = 0; let checkedOfGlobalFilters: number = 0
let confirmedCategory = false; let confirmedCategory = false
$: if (selectedPreset === undefined) { $: if (selectedPreset === undefined) {
confirmedCategory = false; confirmedCategory = false
creating = false; creating = false
checkedOfGlobalFilters = 0; checkedOfGlobalFilters = 0
} }
let flayer: FilteredLayer = undefined; let flayer: FilteredLayer = undefined
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined; let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined
let layerHasFilters: Store<boolean> | undefined = undefined; let layerHasFilters: Store<boolean> | undefined = undefined
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters; let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters
let _globalFilter: GlobalFilter[] = []; let _globalFilter: GlobalFilter[] = []
onDestroy( onDestroy(
globalFilter.addCallbackAndRun((globalFilter) => { globalFilter.addCallbackAndRun((globalFilter) => {
console.log("Global filters are", globalFilter); console.log("Global filters are", globalFilter)
_globalFilter = globalFilter ?? []; _globalFilter = globalFilter ?? []
}) })
); )
$: { $: {
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id); flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id)
layerIsDisplayed = flayer?.isDisplayed; layerIsDisplayed = flayer?.isDisplayed
layerHasFilters = flayer?.hasFilter; 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; const isLoading = state.dataIsLoading
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined); let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(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 // 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. * Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters.
* Will delete the lastclick-location * Will delete the lastclick-location
*/ */
function abort() { function abort() {
state.selectedElement.setData(undefined); state.selectedElement.setData(undefined)
// When aborted, we force the contributors to place the pin _again_ // 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 // This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
state.lastClickObject.features.setData([]); state.lastClickObject.features.setData([])
preciseInputIsTapped = false; preciseInputIsTapped = false
} }
async function confirm() { async function confirm() {
creating = true; creating = true
const location: { lon: number; lat: number } = preciseCoordinate.data; const location: { lon: number; lat: number } = preciseCoordinate.data
const snapTo: WayId | undefined = <WayId>snappedToObject.data; const snapTo: WayId | undefined = <WayId>snappedToObject.data
const tags: Tag[] = selectedPreset.preset.tags.concat( const tags: Tag[] = selectedPreset.preset.tags.concat(
..._globalFilter.map((f) => f?.onNewPoint?.tags ?? []) ..._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) { if (snapTo !== undefined && snapTo !== null) {
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0); const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0)
if (downloaded !== "deleted") { if (downloaded !== "deleted") {
snapToWay = downloaded; snapToWay = downloaded
} }
} }
@ -113,44 +113,44 @@
theme: state.layout?.id ?? "unkown", theme: state.layout?.id ?? "unkown",
changeType: "create", changeType: "create",
snapOnto: snapToWay, snapOnto: snapToWay,
reusePointWithinMeters: 1 reusePointWithinMeters: 1,
}); })
await state.changes.applyAction(newElementAction); await state.changes.applyAction(newElementAction)
state.newFeatures.features.ping(); state.newFeatures.features.ping()
// The 'changes' should have created a new point, which added this into the 'featureProperties' // The 'changes' should have created a new point, which added this into the 'featureProperties'
const newId = newElementAction.newElementId; const newId = newElementAction.newElementId
console.log("Applied pending changes, fetching store for", newId); console.log("Applied pending changes, fetching store for", newId)
const tagsStore = state.featureProperties.getStore(newId); const tagsStore = state.featureProperties.getStore(newId)
if (!tagsStore) { if (!tagsStore) {
console.error("Bug: no tagsStore found for", newId); console.error("Bug: no tagsStore found for", newId)
} }
{ {
// Set some metainfo // Set some metainfo
const properties = tagsStore.data; const properties = tagsStore.data
if (snapTo) { if (snapTo) {
// metatags (starting with underscore) are not uploaded, so we can safely mark this // metatags (starting with underscore) are not uploaded, so we can safely mark this
delete properties["_referencing_ways"]; delete properties["_referencing_ways"]
properties["_referencing_ways"] = `["${snapTo}"]`; properties["_referencing_ways"] = `["${snapTo}"]`
} }
properties["_backend"] = state.osmConnection.Backend(); properties["_backend"] = state.osmConnection.Backend()
properties["_last_edit:timestamp"] = new Date().toISOString(); properties["_last_edit:timestamp"] = new Date().toISOString()
const userdetails = state.osmConnection.userDetails.data; const userdetails = state.osmConnection.userDetails.data
properties["_last_edit:contributor"] = userdetails.name; properties["_last_edit:contributor"] = userdetails.name
properties["_last_edit:uid"] = "" + userdetails.uid; properties["_last_edit:uid"] = "" + userdetails.uid
tagsStore.ping(); tagsStore.ping()
} }
const feature = state.indexedFeatures.featuresById.data.get(newId); const feature = state.indexedFeatures.featuresById.data.get(newId)
console.log("Selecting feature", feature, "and opening their popup"); console.log("Selecting feature", feature, "and opening their popup")
abort(); abort()
state.selectedLayer.setData(selectedPreset.layer); state.selectedLayer.setData(selectedPreset.layer)
state.selectedElement.setData(feature); state.selectedElement.setData(feature)
tagsStore.ping(); tagsStore.ping()
} }
function confirmSync() { function confirmSync() {
confirm() confirm()
.then((_) => console.debug("New point successfully handled")) .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> </script>
@ -163,7 +163,6 @@
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} /> <Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
</LoginButton> </LoginButton>
<div class="h-full w-full"> <div class="h-full w-full">
{#if $zoom < Constants.minZoomLevelToAddNewPoint} {#if $zoom < Constants.minZoomLevelToAddNewPoint}
<div class="alert"> <div class="alert">
<Tr t={Translations.t.general.add.zoomInFurther} /> <Tr t={Translations.t.general.add.zoomInFurther} />
@ -179,8 +178,8 @@
<PresetList <PresetList
{state} {state}
on:select={(event) => { on:select={(event) => {
selectedPreset = event.detail selectedPreset = event.detail
}} }}
/> />
{:else if !$layerIsDisplayed} {:else if !$layerIsDisplayed}
<!-- Check that the layer is enabled, so that we don't add a duplicate --> <!-- Check that the layer is enabled, so that we don't add a duplicate -->
@ -195,9 +194,9 @@
<button <button
class="flex w-full gap-x-1" class="flex w-full gap-x-1"
on:click={() => { on:click={() => {
abort() abort()
state.guistate.openFilterView(selectedPreset.layer) state.guistate.openFilterView(selectedPreset.layer)
}} }}
> >
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} /> <ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
<Tr t={Translations.t.general.add.openLayerControl} /> <Tr t={Translations.t.general.add.openLayerControl} />
@ -206,12 +205,14 @@
<button <button
class="primary flex w-full gap-x-1" class="primary flex w-full gap-x-1"
on:click={() => { on:click={() => {
layerIsDisplayed.setData(true) layerIsDisplayed.setData(true)
abort() abort()
}} }}
> >
<EyeIcon class="w-12" /> <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> </button>
</div> </div>
{:else if $layerHasFilters} {:else if $layerHasFilters}
@ -224,9 +225,9 @@
<button <button
class="primary flex w-full gap-x-1" class="primary flex w-full gap-x-1"
on:click={() => { on:click={() => {
abort() abort()
state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters() state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters()
}} }}
> >
<EyeOffIcon class="w-12" /> <EyeOffIcon class="w-12" />
<Tr t={Translations.t.general.add.disableFilters} /> <Tr t={Translations.t.general.add.disableFilters} />
@ -234,9 +235,9 @@
<button <button
class="flex w-full gap-x-1" class="flex w-full gap-x-1"
on:click={() => { on:click={() => {
abort() abort()
state.guistate.openFilterView(selectedPreset.layer) state.guistate.openFilterView(selectedPreset.layer)
}} }}
> >
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} /> <ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
<Tr t={Translations.t.general.add.openLayerControl} /> <Tr t={Translations.t.general.add.openLayerControl} />
@ -265,10 +266,10 @@
{/if} {/if}
</h3> </h3>
<span class="flex flex-wrap items-stretch"> <span class="flex flex-wrap items-stretch">
{#each selectedPreset.preset.exampleImages as src} {#each selectedPreset.preset.exampleImages as src}
<img {src} class="m-1 h-64 w-auto rounded-lg" /> <img {src} class="m-1 h-64 w-auto rounded-lg" />
{/each} {/each}
</span> </span>
{/if} {/if}
<TagHint <TagHint
embedIn={(tags) => t.presetInfo.Subs({ tags })} embedIn={(tags) => t.presetInfo.Subs({ tags })}
@ -295,8 +296,8 @@
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12" /> <Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12" />
<SubtleButton <SubtleButton
on:click={() => { on:click={() => {
checkedOfGlobalFilters = checkedOfGlobalFilters + 1 checkedOfGlobalFilters = checkedOfGlobalFilters + 1
}} }}
> >
<img <img
slot="image" slot="image"
@ -306,27 +307,27 @@
<Tr <Tr
slot="message" slot="message"
t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({ t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({
preset: selectedPreset.preset, preset: selectedPreset.preset,
})} })}
/> />
</SubtleButton> </SubtleButton>
<SubtleButton <SubtleButton
on:click={() => { on:click={() => {
globalFilter.setData([]) globalFilter.setData([])
abort() abort()
}} }}
> >
<img slot="image" src="./assets/svg/close.svg" class="h-8 w-8" /> <img slot="image" src="./assets/svg/close.svg" class="h-8 w-8" />
<Tr slot="message" t={Translations.t.general.cancel} /> <Tr slot="message" t={Translations.t.general.cancel} />
</SubtleButton> </SubtleButton>
{:else if !creating} {:else if !creating}
<div class="flex flex-col h-full"> <div class="flex h-full flex-col">
<div class="relative min-h-20 h-full w-full p-1 "> <div class="min-h-20 relative h-full w-full p-1">
<div class="h-full w-full overflow-hidden rounded-xl"> <div class="h-full w-full overflow-hidden rounded-xl">
<NewPointLocationInput <NewPointLocationInput
on:click={() => { on:click={() => {
preciseInputIsTapped = true preciseInputIsTapped = true
}} }}
value={preciseCoordinate} value={preciseCoordinate}
snappedTo={snappedToObject} snappedTo={snappedToObject}
{state} {state}
@ -338,9 +339,9 @@
<div <div
class={twJoin( class={twJoin(
!preciseInputIsTapped && "hidden", !preciseInputIsTapped && "hidden",
"absolute top-0 flex w-full justify-center p-12" "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 --> <!-- 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"> <NextButton on:click={confirmSync} clss="primary w-fit">

View file

@ -11,9 +11,9 @@
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import FromHtml from "../../Base/FromHtml.svelte" import FromHtml from "../../Base/FromHtml.svelte"
import NextButton from "../../Base/NextButton.svelte" import NextButton from "../../Base/NextButton.svelte"
import { UIElement } from "../../UIElement"; import { UIElement } from "../../UIElement"
import ToSvelte from "../../Base/ToSvelte.svelte"; import ToSvelte from "../../Base/ToSvelte.svelte"
import BaseUIElement from "../../BaseUIElement"; import BaseUIElement from "../../BaseUIElement"
/** /**
* This component lists all the presets and allows the user to select one * This component lists all the presets and allows the user to select one
@ -86,7 +86,7 @@
{#each presets as preset} {#each presets as preset}
<NextButton on:click={() => dispatch("select", preset)}> <NextButton on:click={() => dispatch("select", preset)}>
<ToSvelte slot="image" construct={() => preset.icon} /> <ToSvelte slot="image" construct={() => preset.icon} />
<div class="flex flex-col"> <div class="flex flex-col">
<b class="w-fit"> <b class="w-fit">
<Tr t={preset.text} /> <Tr t={preset.text} />

View file

@ -8,11 +8,11 @@
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import Lazy from "../Base/Lazy" import Lazy from "../Base/Lazy"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
//Svelte props //Svelte props
export let tags: UIEventSource<any> export let tags: UIEventSource<any>
export let state: {layoutToUse: LayoutConfig} = undefined export let state: { layoutToUse: LayoutConfig } = undefined
const calculatedTags = [].concat( const calculatedTags = [].concat(
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? []) ...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])

View file

@ -8,7 +8,7 @@
import InputHelper from "../../InputElement/InputHelper.svelte" import InputHelper from "../../InputElement/InputHelper.svelte"
import type { Feature } from "geojson" import type { Feature } from "geojson"
import { Unit } from "../../../Models/Unit" import { Unit } from "../../../Models/Unit"
import InputHelpers from "../../InputElement/InputHelpers"; import InputHelpers from "../../InputElement/InputHelpers"
export let value: UIEventSource<string> export let value: UIEventSource<string>
export let config: TagRenderingConfig export let config: TagRenderingConfig
@ -25,8 +25,8 @@
inline = config.freeform?.inline inline = config.freeform?.inline
} }
const dispatch = createEventDispatcher<{selected}>() const dispatch = createEventDispatcher<{ selected }>()
export let feedback: UIEventSource<Translation> export let feedback: UIEventSource<Translation>
onDestroy( onDestroy(
value.addCallbackD(() => { value.addCallbackD(() => {
dispatch("selected") dispatch("selected")
@ -65,5 +65,11 @@
/> />
{/if} {/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> </div>

View file

@ -95,7 +95,11 @@
} }
</script> </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 _questionsToAsk.length === 0}
{#if skipped + answered > 0} {#if skipped + answered > 0}
<div class="thanks"> <div class="thanks">

View file

@ -1,48 +1,51 @@
<script lang="ts"> <script lang="ts">
import { Translation } from "../../i18n/Translation"; import { Translation } from "../../i18n/Translation"
import SpecialVisualizations from "../../SpecialVisualizations"; import SpecialVisualizations from "../../SpecialVisualizations"
import Locale from "../../i18n/Locale"; import Locale from "../../i18n/Locale"
import type { RenderingSpecification, SpecialVisualizationState } from "../../SpecialVisualization"; import type {
import { Utils } from "../../../Utils.js"; RenderingSpecification,
import type { Feature } from "geojson"; SpecialVisualizationState,
import { UIEventSource } from "../../../Logic/UIEventSource.js"; } from "../../SpecialVisualization"
import ToSvelte from "../../Base/ToSvelte.svelte"; import { Utils } from "../../../Utils.js"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; import type { Feature } from "geojson"
import WeblateLink from "../../Base/WeblateLink.svelte"; import { UIEventSource } from "../../../Logic/UIEventSource.js"
import FromHtml from "../../Base/FromHtml.svelte"; import ToSvelte from "../../Base/ToSvelte.svelte"
import BaseUIElement from "../../BaseUIElement"; 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 * The 'specialTranslation' renders a `Translation`-object, but interprets the special values as well
*/ */
export let t: Translation; export let t: Translation
export let state: SpecialVisualizationState; export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>; export let tags: UIEventSource<Record<string, string>>
export let feature: Feature; export let feature: Feature
export let layer: LayerConfig | undefined; export let layer: LayerConfig | undefined
let language = Locale.language; let language = Locale.language
let txt: string = t.textFor($language); let txt: string = t.textFor($language)
let specs: RenderingSpecification[] = []; let specs: RenderingSpecification[] = []
$: { $: {
try { try {
if (txt !== undefined) { if (txt !== undefined) {
const key = "cached_special_spec_" + $language; const key = "cached_special_spec_" + $language
specs = t[key]; specs = t[key]
if (specs === undefined) { if (specs === undefined) {
specs = SpecialVisualizations.constructSpecification(txt); specs = SpecialVisualizations.constructSpecification(txt)
t[key] = specs; t[key] = specs
} }
} }
} catch (e) { } 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 { function createVisualisation(specpart: Exclude<RenderingSpecification, string>): BaseUIElement {
{ {
try { try {
return specpart.func.constr(state, tags, specpart.args, feature, layer); return specpart.func.constr(state, tags, specpart.args, feature, layer)
} catch (e) { } catch (e) {
console.error( console.error(
"Could not construct a special visualisation with specification", "Could not construct a special visualisation with specification",
@ -51,7 +54,7 @@
tags, tags,
"due to", "due to",
e e
); )
} }
} }
} }

View file

@ -1,27 +1,27 @@
<script lang="ts"> <script lang="ts">
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { Store, UIEventSource } from "../../../Logic/UIEventSource"; import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"; import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"; import type { SpecialVisualizationState } from "../../SpecialVisualization"
import TagRenderingAnswer from "./TagRenderingAnswer.svelte"; import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"; import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
import { onDestroy } from "svelte"; import { onDestroy } from "svelte"
import Tr from "../../Base/Tr.svelte"; import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"; import Translations from "../../i18n/Translations.js"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../../Utils"; import { Utils } from "../../../Utils"
export let config: TagRenderingConfig; export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>; export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature | undefined; export let selectedElement: Feature | undefined
export let state: SpecialVisualizationState; export let state: SpecialVisualizationState
export let layer: LayerConfig = undefined; export let layer: LayerConfig = undefined
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge; export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge
export let highlightedRendering: UIEventSource<string> = undefined; export let highlightedRendering: UIEventSource<string> = undefined
export let showQuestionIfUnknown: boolean = false; export let showQuestionIfUnknown: boolean = false
/** /**
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property * Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
*/ */
@ -31,10 +31,10 @@
tags.addCallbackD((tags) => { tags.addCallbackD((tags) => {
editMode = !config.IsKnown(tags) editMode = !config.IsKnown(tags)
}) })
); )
} }
let htmlElem: HTMLDivElement; let htmlElem: HTMLDivElement
$: { $: {
if (editMode && htmlElem !== undefined && config.IsKnown(tags)) { 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 // 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_ // Some delay is applied to give Svelte the time to render the _question_
window.setTimeout(() => { window.setTimeout(() => {
Utils.scrollIntoView(<any>htmlElem); Utils.scrollIntoView(<any>htmlElem)
}, 50); }, 50)
} }
} }
const _htmlElement = new UIEventSource<HTMLElement>(undefined); const _htmlElement = new UIEventSource<HTMLElement>(undefined)
$: _htmlElement.setData(htmlElem); $: _htmlElement.setData(htmlElem)
function setHighlighting() { function setHighlighting() {
if (highlightedRendering === undefined) { if (highlightedRendering === undefined) {
return; return
} }
if (htmlElem === undefined) { if (htmlElem === undefined) {
return; return
} }
const highlighted = highlightedRendering.data; const highlighted = highlightedRendering.data
if (config.id === highlighted) { if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow"); htmlElem.classList.add("glowing-shadow")
} else { } else {
htmlElem.classList.remove("glowing-shadow"); htmlElem.classList.remove("glowing-shadow")
} }
} }
if (highlightedRendering) { if (highlightedRendering) {
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting())); onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting())); onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
} }
</script> </script>
@ -106,7 +106,7 @@
</div> </div>
{/if} {/if}
{:else} {:else}
<div class="overflow-hidden p-2 w-full"> <div class="w-full overflow-hidden p-2">
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} /> <TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
</div> </div>
{/if} {/if}

View file

@ -1,43 +1,43 @@
<script lang="ts"> <script lang="ts">
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"; import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"
import type { SpecialVisualizationState } from "../../SpecialVisualization"; import type { SpecialVisualizationState } from "../../SpecialVisualization"
import Tr from "../../Base/Tr.svelte"; import Tr from "../../Base/Tr.svelte"
import type { Feature } from "geojson"; import type { Feature } from "geojson"
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"; import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"; import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import FreeformInput from "./FreeformInput.svelte"; import FreeformInput from "./FreeformInput.svelte"
import Translations from "../../i18n/Translations.js"; import Translations from "../../i18n/Translations.js"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"; import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import { createEventDispatcher, onDestroy } from "svelte"; import { createEventDispatcher, onDestroy } from "svelte"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import SpecialTranslation from "./SpecialTranslation.svelte"; import SpecialTranslation from "./SpecialTranslation.svelte"
import TagHint from "../TagHint.svelte"; import TagHint from "../TagHint.svelte"
import LoginToggle from "../../Base/LoginToggle.svelte"; import LoginToggle from "../../Base/LoginToggle.svelte"
import SubtleButton from "../../Base/SubtleButton.svelte"; import SubtleButton from "../../Base/SubtleButton.svelte"
import Loading from "../../Base/Loading.svelte"; import Loading from "../../Base/Loading.svelte"
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"; import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"
import { Translation } from "../../i18n/Translation"; import { Translation } from "../../i18n/Translation"
import Constants from "../../../Models/Constants"; import Constants from "../../../Models/Constants"
import { Unit } from "../../../Models/Unit"; import { Unit } from "../../../Models/Unit"
import UserRelatedState from "../../../Logic/State/UserRelatedState"; import UserRelatedState from "../../../Logic/State/UserRelatedState"
import { twJoin } from "tailwind-merge"; import { twJoin } from "tailwind-merge"
import { TagUtils } from "../../../Logic/Tags/TagUtils"; import { TagUtils } from "../../../Logic/Tags/TagUtils"
export let config: TagRenderingConfig; export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>; export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature; export let selectedElement: Feature
export let state: SpecialVisualizationState; export let state: SpecialVisualizationState
export let layer: LayerConfig | undefined; 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 // Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]); let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
let selectedMapping: number = undefined; let selectedMapping: number = undefined
let checkedMappings: boolean[]; let checkedMappings: boolean[]
/** /**
* Prepares and fills the checkedMappings * Prepares and fills the checkedMappings
@ -45,12 +45,12 @@
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) { function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
mappings = confg.mappings?.filter((m) => { mappings = confg.mappings?.filter((m) => {
if (typeof m.hideInAnswer === "boolean") { 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 // 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 ( if (
confg.mappings?.length > 0 && confg.mappings?.length > 0 &&
@ -58,56 +58,56 @@
(checkedMappings === undefined || (checkedMappings === undefined ||
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0)) checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
) { ) {
const seenFreeforms = []; const seenFreeforms = []
TagUtils.FlattenMultiAnswer(); TagUtils.FlattenMultiAnswer()
checkedMappings = [ checkedMappings = [
...confg.mappings.map((mapping) => { ...confg.mappings.map((mapping) => {
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs); const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs)
if (matches && confg.freeform) { if (matches && confg.freeform) {
const newProps = TagUtils.changeAsProperties(mapping.if.asChange()); const newProps = TagUtils.changeAsProperties(mapping.if.asChange())
seenFreeforms.push(newProps[confg.freeform.key]); seenFreeforms.push(newProps[confg.freeform.key])
} }
return matches; return matches
}) }),
]; ]
if (tgs !== undefined && confg.freeform) { if (tgs !== undefined && confg.freeform) {
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []; const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []
for (const seenFreeform of seenFreeforms) { for (const seenFreeform of seenFreeforms) {
if (!seenFreeform) { if (!seenFreeform) {
continue; continue
} }
const index = unseenFreeformValues.indexOf(seenFreeform); const index = unseenFreeformValues.indexOf(seenFreeform)
if (index < 0) { if (index < 0) {
continue; continue
} }
unseenFreeformValues.splice(index, 1); unseenFreeformValues.splice(index, 1)
} }
// TODO this has _to much_ values // TODO this has _to much_ values
freeformInput.setData(unseenFreeformValues.join(";")); freeformInput.setData(unseenFreeformValues.join(";"))
checkedMappings.push(unseenFreeformValues.length > 0); checkedMappings.push(unseenFreeformValues.length > 0)
} }
} }
if (confg.freeform?.key) { if (confg.freeform?.key) {
if (!confg.multiAnswer) { if (!confg.multiAnswer) {
// Somehow, setting multi-answer freeform values is broken if this is not set // 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 { } 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 // 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 // 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 mappings: Mapping[] = config?.mappings
let searchTerm: UIEventSource<string> = new UIEventSource(""); let searchTerm: UIEventSource<string> = new UIEventSource("")
$: { $: {
try { try {
@ -116,10 +116,10 @@
selectedMapping, selectedMapping,
checkedMappings, checkedMappings,
tags.data tags.data
); )
} catch (e) { } catch (e) {
console.error("Could not calculate changeSpecification:", e); console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined; selectedTags = undefined
} }
} }
@ -128,55 +128,56 @@
config: TagRenderingConfig config: TagRenderingConfig
applied: TagsFilter applied: TagsFilter
} }
}>(); }>()
function onSave() { function onSave() {
if (selectedTags === undefined) { if (selectedTags === undefined) {
console.log("SelectedTags is undefined, ignoring 'onSave'-event") console.log("SelectedTags is undefined, ignoring 'onSave'-event")
return; return
} }
if (layer === undefined || layer?.source === null) { if (layer === undefined || layer?.source === null) {
/** /**
* This is a special, priviliged layer. * This is a special, priviliged layer.
* We simply apply the tags onto the records * 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) { for (const { k, v } of kv) {
if (v === undefined || v === "") { if (v === undefined || v === "") {
delete tags.data[k]; delete tags.data[k]
} else { } else {
tags.data[k] = v; tags.data[k] = v
} }
} }
tags.ping(); tags.ping()
return; return
} }
dispatch("saved", { config, applied: selectedTags }); dispatch("saved", { config, applied: selectedTags })
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, { const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
theme: state.layout.id, theme: state.layout.id,
changeType: "answer" changeType: "answer",
}); })
freeformInput.setData(undefined); freeformInput.setData(undefined)
selectedMapping = undefined; selectedMapping = undefined
selectedTags = undefined; selectedTags = undefined
change change
.CreateChangeDescriptions() .CreateChangeDescriptions()
.then((changes) => state.changes.applyChanges(changes)) .then((changes) => state.changes.applyChanges(changes))
.catch(console.error); .catch(console.error)
} }
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false); let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
let featureSwitchIsDebugging = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false); let featureSwitchIsDebugging =
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined); state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0; let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined)
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0
if (state?.osmConnection) { if (state?.osmConnection) {
onDestroy( onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount; numberOfCs = ud.csCount
}) })
); )
} }
</script> </script>
@ -249,7 +250,9 @@
bind:group={selectedMapping} bind:group={selectedMapping}
name={"mappings-radio-" + config.id} name={"mappings-radio-" + config.id}
value={i} value={i}
on:keypress={e => {if(e.key === "Enter") onSave()}} on:keypress={(e) => {
if (e.key === "Enter") onSave()
}}
/> />
</TagRenderingMappingInput> </TagRenderingMappingInput>
{/each} {/each}

View file

@ -1,44 +1,48 @@
<script lang="ts"> <script lang="ts">
import type { ConfigMeta } from "./configMeta"; import type { ConfigMeta } from "./configMeta"
import EditLayerState from "./EditLayerState"; import EditLayerState from "./EditLayerState"
import type { import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
QuestionableTagRenderingConfigJson import { UIEventSource } from "../../Logic/UIEventSource"
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
export let schema: ConfigMeta; export let schema: ConfigMeta
export let state: EditLayerState; export let state: EditLayerState
export let path: (string | number)[] = []; export let path: (string | number)[] = []
const configJson: QuestionableTagRenderingConfigJson = { const configJson: QuestionableTagRenderingConfigJson = {
mappings: schema.hints.suggestions, mappings: schema.hints.suggestions,
multiAnswer: true, multiAnswer: true,
id: "multi_anwser_"+path.join("_"), id: "multi_anwser_" + path.join("_"),
question: schema.hints.question question: schema.hints.question,
} }
const tags = new UIEventSource({}) const tags = new UIEventSource({})
{ {
// Setting the initial value // Setting the initial value
const v = <string[]> state.getCurrentValueFor(path) const v = <string[]>state.getCurrentValueFor(path)
if(v && v.length > 0){ if (v && v.length > 0) {
tags.setData({value: v.join(";")}) tags.setData({ value: v.join(";") })
} }
} }
tags.addCallbackD(tags => { tags.addCallbackD((tags) => {
const values = tags["value"]?.split(";") const values = tags["value"]?.split(";")
if(!values){ if (!values) {
return return
} }
state.setValueAt(path, values) state.setValueAt(path, values)
}) })
const config = new TagRenderingConfig(configJson) const config = new TagRenderingConfig(configJson)
</script> </script>
<div> <div>
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} /> <TagRenderingEditable
{config}
selectedElement={undefined}
showQuestionIfUnknown={true}
{state}
{tags}
/>
</div> </div>

View file

@ -1,18 +1,17 @@
<script lang="ts"> <script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection"; import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import EditItemButton from "./EditItemButton.svelte"; import EditItemButton from "./EditItemButton.svelte"
export let layerIds: { id: string, owner: number }[];
export let category: "layers" | "themes" = "layers";
export let osmConnection: OsmConnection;
export let layerIds: { id: string; owner: number }[]
export let category: "layers" | "themes" = "layers"
export let osmConnection: OsmConnection
</script> </script>
{#if layerIds.length > 0} {#if layerIds.length > 0}
<slot name="title" /> <slot name="title" />
<div class="flex flex-wrap"> <div class="flex flex-wrap">
{#each Array.from(layerIds) as layer} {#each Array.from(layerIds) as layer}
<EditItemButton info={layer} {category} {osmConnection} on:layerSelected/> <EditItemButton info={layer} {category} {osmConnection} on:layerSelected />
{/each} {/each}
</div> </div>
{/if} {/if}

View file

@ -1,35 +1,36 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"; import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Marker from "../Map/Marker.svelte"; import Marker from "../Map/Marker.svelte"
import NextButton from "../Base/NextButton.svelte"; import NextButton from "../Base/NextButton.svelte"
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"; import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"; import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte"
export let info: { id: string, owner: number }; export let info: { id: string; owner: number }
export let category: "layers" | "themes"; export let category: "layers" | "themes"
export let osmConnection: OsmConnection; 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 { function fetchIconDescription(layerId): any {
if (category === "themes") { 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> </script>
<NextButton clss="small" on:click={() => dispatch("layerSelected", info)}> <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)} /> <Marker icons={fetchIconDescription(info.id)} />
</div> </div>
<b class="px-1"> {info.id}</b> <b class="px-1">{info.id}</b>
{#if info.owner && info.owner !== $selfId} {#if info.owner && info.owner !== $selfId}
(made by {$displayName ?? info.owner}) (made by {$displayName ?? info.owner})
{/if} {/if}

View file

@ -1,81 +1,81 @@
<script lang="ts"> <script lang="ts">
import type { HighlightedTagRendering } from "./EditLayerState"; import type { HighlightedTagRendering } from "./EditLayerState"
import EditLayerState from "./EditLayerState"; import EditLayerState from "./EditLayerState"
import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json"; import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json"
import Region from "./Region.svelte"; import Region from "./Region.svelte"
import TabbedGroup from "../Base/TabbedGroup.svelte"; import TabbedGroup from "../Base/TabbedGroup.svelte"
import { Store, UIEventSource } from "../../Logic/UIEventSource"; import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { ConfigMeta } from "./configMeta"; import type { ConfigMeta } from "./configMeta"
import { Utils } from "../../Utils"; import { Utils } from "../../Utils"
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"
import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"; import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"
import SchemaBasedInput from "./SchemaBasedInput.svelte"; import SchemaBasedInput from "./SchemaBasedInput.svelte"
import FloatOver from "../Base/FloatOver.svelte"; import FloatOver from "../Base/FloatOver.svelte"
import TagRenderingInput from "./TagRenderingInput.svelte"; import TagRenderingInput from "./TagRenderingInput.svelte"
import FromHtml from "../Base/FromHtml.svelte"; import FromHtml from "../Base/FromHtml.svelte"
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"; import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
import QuestionPreview from "./QuestionPreview.svelte"; import QuestionPreview from "./QuestionPreview.svelte"
import ShowConversionMessages from "./ShowConversionMessages.svelte"; import ShowConversionMessages from "./ShowConversionMessages.svelte"
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
export let state: EditLayerState; export let state: EditLayerState
let messages = state.messages; let messages = state.messages
let hasErrors = messages.mapD((m: ConversionMessage[]) => m.filter(m => m.level === "error").length); let hasErrors = messages.mapD(
const configuration = state.configuration; (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) { 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"])
let title: Store<string> = state.getStoreFor(["id"]); const wl = window.location
const wl = window.location; const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout="
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
function firstPathsFor(...regionNames: string[]): Set<string> { function firstPathsFor(...regionNames: string[]): Set<string> {
const pathNames = new Set<string>(); const pathNames = new Set<string>()
for (const regionName of regionNames) { for (const regionName of regionNames) {
const region: ConfigMeta[] = perRegion[regionName]; const region: ConfigMeta[] = perRegion[regionName]
for (const configMeta of region) { for (const configMeta of region) {
pathNames.add(configMeta.path[0]); pathNames.add(configMeta.path[0])
} }
} }
return pathNames; return pathNames
} }
function configForRequiredField(id: string): ConfigMeta { function configForRequiredField(id: string): ConfigMeta {
let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id); let config = layerSchema.find((config) => config.path.length === 1 && config.path[0] === id)
config = Utils.Clone(config); config = Utils.Clone(config)
config.required = true; config.required = true
config.hints.ifunset = undefined; config.hints.ifunset = undefined
return config; return config
} }
let requiredFields = ["id", "name", "description", "source"]; let requiredFields = ["id", "name", "description", "source"]
let currentlyMissing = state.configuration.map(config => { let currentlyMissing = state.configuration.map((config) => {
if (!config) { if (!config) {
return []; return []
} }
const missing = []; const missing = []
for (const requiredField of requiredFields) { for (const requiredField of requiredFields) {
if (!config[requiredField]) { 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> </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 /> <slot />
{#if $title === undefined} {#if $title === undefined}
<h3>Creating a new layer</h3> <h3>Creating a new layer</h3>
@ -83,11 +83,17 @@
<h3>Editing layer {$title}</h3> <h3>Editing layer {$title}</h3>
{/if} {/if}
{#if $currentlyMissing.length > 0} {#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} {:else if $hasErrors > 0}
<div class="alert">{$hasErrors} errors detected</div> <div class="alert">{$hasErrors} errors detected</div>
{:else} {: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 Try it out
<ChevronRightIcon class="h-6 w-6 shrink-0" /> <ChevronRightIcon class="h-6 w-6 shrink-0" />
</a> </a>
@ -95,29 +101,29 @@
</div> </div>
{#if $currentlyMissing.length > 0} {#if $currentlyMissing.length > 0}
{#each requiredFields as required} {#each requiredFields as required}
<SchemaBasedInput {state} <SchemaBasedInput {state} schema={configForRequiredField(required)} path={[required]} />
schema={configForRequiredField(required)}
path={[required]} />
{/each} {/each}
{:else} {:else}
<div class="m4 h-full overflow-y-auto"> <div class="m4 h-full overflow-y-auto">
<TabbedGroup> <TabbedGroup>
<div slot="title0" class="flex">General properties <div slot="title0" class="flex">
General properties
<ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} /> <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
</div> </div>
<div class="flex flex-col" slot="content0"> <div class="flex flex-col" slot="content0">
<Region {state} configs={perRegion["Basic"]} /> <Region {state} configs={perRegion["Basic"]} />
</div> </div>
<div slot="title1" class="flex">
<div slot="title1" class="flex">Information panel (questions and answers) Information panel (questions and answers)
<ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /> <ErrorIndicatorForRegion
firstPaths={firstPathsFor("title", "tagrenderings", "editing")}
{state}
/>
</div> </div>
<div slot="content1"> <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["tagrenderings"]} {state} title="Popup contents" />
<Region configs={perRegion["editing"]} {state} title="Other editing elements" /> <Region configs={perRegion["editing"]} {state} title="Other editing elements" />
</div> </div>
@ -131,16 +137,21 @@
<Region {state} configs={perRegion["presets"]} /> <Region {state} configs={perRegion["presets"]} />
</div> </div>
<div slot="title3" class="flex">Rendering on the map <div slot="title3" class="flex">
<ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} /> Rendering on the map
<ErrorIndicatorForRegion
firstPaths={firstPathsFor("linerendering", "pointrendering")}
{state}
/>
</div> </div>
<div slot="content3"> <div slot="content3">
<Region configs={perRegion["linerendering"]} {state} /> <Region configs={perRegion["linerendering"]} {state} />
<Region configs={perRegion["pointrendering"]} {state} /> <Region configs={perRegion["pointrendering"]} {state} />
</div> </div>
<div slot="title4" class="flex">Advanced functionality <div slot="title4" class="flex">
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /> Advanced functionality
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced", "expert")} {state} />
</div> </div>
<div slot="content4"> <div slot="content4">
<Region configs={perRegion["advanced"]} {state} /> <Region configs={perRegion["advanced"]} {state} />
@ -149,30 +160,33 @@
<div slot="title5">Configuration file</div> <div slot="title5">Configuration file</div>
<div slot="content5"> <div slot="content5">
<div> <div>
Below, you'll find the raw configuration file in `.json`-format. Below, you'll find the raw configuration file in `.json`-format. This is mostly for
This is mostly for debugging purposes debugging purposes
</div> </div>
<div class="literal-code"> <div class="literal-code">
<FromHtml src={JSON.stringify($configuration, null, " ").replaceAll("\n","</br>")} /> <FromHtml src={JSON.stringify($configuration, null, " ").replaceAll("\n", "</br>")} />
</div> </div>
<ShowConversionMessages messages={$messages} /> <ShowConversionMessages messages={$messages} />
<div> <div>
The testobject (which is used to render the questions in the 'information panel' item has the following The testobject (which is used to render the questions in the 'information panel' item
tags: has the following tags:
</div> </div>
<AllTagsPanel tags={state.testTags}></AllTagsPanel> <AllTagsPanel tags={state.testTags} />
</div> </div>
</TabbedGroup> </TabbedGroup>
</div> </div>
{#if $highlightedItem !== undefined} {#if $highlightedItem !== undefined}
<FloatOver on:close={() => highlightedItem.setData(undefined)}> <FloatOver on:close={() => highlightedItem.setData(undefined)}>
<div> <div>
<TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} /> <TagRenderingInput
path={$highlightedItem.path}
{state}
schema={$highlightedItem.schema}
/>
</div> </div>
</FloatOver> </FloatOver>
{/if} {/if}
{/if} {/if}
</div> </div>

View file

@ -1,37 +1,45 @@
<script lang="ts"> <script lang="ts">
import { EditThemeState } from "./EditLayerState"; import { EditThemeState } from "./EditLayerState"
import type { ConfigMeta } from "./configMeta"; import type { ConfigMeta } from "./configMeta"
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"
import TabbedGroup from "../Base/TabbedGroup.svelte"; import TabbedGroup from "../Base/TabbedGroup.svelte"
import ShowConversionMessages from "./ShowConversionMessages.svelte"; import ShowConversionMessages from "./ShowConversionMessages.svelte"
import Region from "./Region.svelte"; import Region from "./Region.svelte"
export let state: EditThemeState; export let state: EditThemeState
let schema: ConfigMeta[] = state.schema.filter(schema => schema.path.length > 0); let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0)
let config = state.configuration; let config = state.configuration
let messages = state.messages; let messages = state.messages
let hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length); let hasErrors = messages.map(
let title = state.getStoreFor(["id"]); (m: ConversionMessage[]) => m.filter((m) => m.level === "error").length
const wl = window.location; )
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout="; 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) { for (const schemaElement of schema) {
const key = schemaElement.hints.group ?? "no-group"; const key = schemaElement.hints.group ?? "no-group"
const list = perRegion[key] ?? (perRegion[key] = []); const list = perRegion[key] ?? (perRegion[key] = [])
list.push(schemaElement); list.push(schemaElement)
} }
console.log({perRegion, schema}) console.log({ perRegion, schema })
</script> </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 /> <slot />
<h3>Editing theme {$title}</h3> <h3>Editing theme {$title}</h3>
{#if $hasErrors > 0} {#if $hasErrors > 0}
<div class="alert">{$hasErrors} errors detected</div> <div class="alert">{$hasErrors} errors detected</div>
{:else} {: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 Try it out
<ChevronRightIcon class="h-6 w-6 shrink-0" /> <ChevronRightIcon class="h-6 w-6 shrink-0" />
</a> </a>
@ -43,36 +51,32 @@ console.log({perRegion, schema})
<TabbedGroup> <TabbedGroup>
<div slot="title0">Basic properties</div> <div slot="title0">Basic properties</div>
<div slot="content0"> <div slot="content0">
<Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties"/> <Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties" />
<Region configs={perRegion["start_location"]} path={[]} {state} title="Start location"/> <Region configs={perRegion["start_location"]} path={[]} {state} title="Start location" />
</div> </div>
<div slot="title1">Layers</div> <div slot="title1">Layers</div>
<div slot="content1"> <div slot="content1">
<Region configs={perRegion["layers"]} path={[]} {state} /> <Region configs={perRegion["layers"]} path={[]} {state} />
</div> </div>
<div slot="title2">Feature switches</div> <div slot="title2">Feature switches</div>
<div slot="content2"> <div slot="content2">
<Region configs={perRegion["feature_switches"]} path={[]} {state}></Region> <Region configs={perRegion["feature_switches"]} path={[]} {state} />
</div> </div>
<div slot="title3">Advanced options</div> <div slot="title3">Advanced options</div>
<div slot="content3"> <div slot="content3">
<Region configs={perRegion["advanced"]} path={[]} {state}></Region> <Region configs={perRegion["advanced"]} path={[]} {state} />
</div> </div>
<div slot="title4">Configuration file</div> <div slot="title4">Configuration file</div>
<div slot="content4"> <div slot="content4">
<div class="literal-code"> <div class="literal-code">
{JSON.stringify($config)} {JSON.stringify($config)}
</div> </div>
<ShowConversionMessages messages={$messages}></ShowConversionMessages> <ShowConversionMessages messages={$messages} />
</div> </div>
</TabbedGroup> </TabbedGroup>
</div> </div>
</div> </div>

View file

@ -1,19 +1,21 @@
<script lang="ts"> <script lang="ts">
import EditLayerState from "./EditLayerState"; import EditLayerState from "./EditLayerState"
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"; 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);
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> </script>
{#if $messagesCount > 0} {#if $messagesCount > 0}
<span class="alert flex w-min"> <span class="alert flex w-min">
<ExclamationIcon class="w-6 h-6" /> <ExclamationIcon class="h-6 w-6" />
{$messagesCount} {$messagesCount}
</span> </span>
{/if} {/if}

View file

@ -1,56 +1,62 @@
<script lang="ts"> <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"; export let state: EditLayerState
import { UIEventSource } from "../../Logic/UIEventSource"; export let path: (string | number)[]
import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"; let tag: UIEventSource<TagConfigJson> = state.getStoreFor([...path, "if"])
import { TagUtils } from "../../Logic/Tags/TagUtils"; let parsedTag = tag.map((t) => (t ? TagUtils.Tag(t) : undefined))
import FromHtml from "../Base/FromHtml.svelte"; let exampleTags = parsedTag.map((pt) => {
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 => {
if (!pt) { if (!pt) {
return {}; return {}
} }
const keys = pt.usedKeys(); const keys = pt.usedKeys()
const o = {}; const o = {}
for (const key of keys) { for (const key of keys) {
o[key] = "value"; o[key] = "value"
} }
return o; return o
}); })
let uploadableOnly: boolean = true; let uploadableOnly: boolean = true
let thenText: UIEventSource<Record<string, string>> = state.getStoreFor([...path, "then"]) let thenText: UIEventSource<Record<string, string>> = state.getStoreFor([...path, "then"])
let thenTextEn = thenText .mapD(translation => typeof translation === "string" ? translation : translation["en"] ) let thenTextEn = thenText.mapD((translation) =>
let editMode = Object.keys($thenText ?? {})?.length === 0; typeof translation === "string" ? translation : translation["en"]
)
let editMode = Object.keys($thenText ?? {})?.length === 0
let mappingConfigs: ConfigMeta[] = configs.filter(c => c.path[0] === "mappings") let mappingConfigs: ConfigMeta[] = configs
.map(c => <ConfigMeta>Utils.Clone(c)) .filter((c) => c.path[0] === "mappings")
.map(c => { .map((c) => <ConfigMeta>Utils.Clone(c))
c.path.splice(0, 1); .map((c) => {
return 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> </script>
<button on:click={() => {editMode = !editMode}}> <button
<PencilIcon class="w-6 h-6" /> on:click={() => {
editMode = !editMode
}}
>
<PencilIcon class="h-6 w-6" />
</button> </button>
{#if editMode} {#if editMode}
<div class="flex justify-between items-start w-full"> <div class="flex w-full items-start justify-between">
<div class="flex flex-col w-full"> <div class="flex w-full flex-col">
<Region {state} configs={mappingConfigs} path={path} /> <Region {state} configs={mappingConfigs} {path} />
</div> </div>
<slot name="delete" /> <slot name="delete" />
@ -64,7 +70,6 @@
{:else} {:else}
<i>No then is set</i> <i>No then is set</i>
{/if} {/if}
<FromHtml src={ $parsedTag?.asHumanString(false, false, $exampleTags)} /> <FromHtml src={$parsedTag?.asHumanString(false, false, $exampleTags)} />
</div> </div>
{/if} {/if}

View file

@ -1,86 +1,82 @@
<script lang="ts"> <script lang="ts">
import type { ConfigMeta } from "./configMeta"; import type { ConfigMeta } from "./configMeta"
import EditLayerState from "./EditLayerState"; import EditLayerState from "./EditLayerState"
import * as questions from "../../assets/generated/layers/questions.json"; import * as questions from "../../assets/generated/layers/questions.json"
import { ImmutableStore, Store } from "../../Logic/UIEventSource"; import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import nmd from "nano-markdown"; import nmd from "nano-markdown"
import type { import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"
QuestionableTagRenderingConfigJson import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"; import FromHtml from "../Base/FromHtml.svelte"
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"; import ShowConversionMessage from "./ShowConversionMessage.svelte"
import FromHtml from "../Base/FromHtml.svelte"; import NextButton from "../Base/NextButton.svelte"
import ShowConversionMessage from "./ShowConversionMessage.svelte";
import NextButton from "../Base/NextButton.svelte";
export let state: EditLayerState; export let state: EditLayerState
export let path: ReadonlyArray<string | number>; export let path: ReadonlyArray<string | number>
export let schema: ConfigMeta; export let schema: ConfigMeta
let value = state.getStoreFor(path); let value = state.getStoreFor(path)
let perId: Record<string, TagRenderingConfigJson[]> = {}; let perId: Record<string, TagRenderingConfigJson[]> = {}
for (let tagRendering of questions.tagRenderings) { for (let tagRendering of questions.tagRenderings) {
if (tagRendering.labels) { if (tagRendering.labels) {
for (let label of 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") { if (typeof x === "string") {
return perId[x]; return perId[x]
} else { } else {
return [x]; return [x]
} }
}); })
let configs: Store<TagRenderingConfig[]> = configJson.map(configs => { let configs: Store<TagRenderingConfig[]> = configJson.map((configs) => {
if (!configs) { if (!configs) {
return [{ error: "No configuartions found" }]; return [{ error: "No configuartions found" }]
} }
console.log("Regenerating configs"); console.log("Regenerating configs")
return configs.map(config => { return configs.map((config) => {
try { try {
return new TagRenderingConfig(config); return new TagRenderingConfig(config)
} catch (e) { } 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) { if (c?.id) {
return c.id; return c.id
} }
if (typeof c === "string") { 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) { if (description) {
try { try {
description = nmd(description); description = nmd(description)
} catch (e) { } catch (e) {
console.error("Could not convert description to markdown", { description }); console.error("Could not convert description to markdown", { description })
} }
} }
</script> </script>
<div class="flex"> <div class="flex">
<div class="interactive border-interactive m-4 flex w-full flex-col">
<div class="flex flex-col interactive border-interactive m-4 w-full">
{#if $id} {#if $id}
TagRendering {$id} TagRendering {$id}
{/if} {/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} {#if schema.hints.question}
{schema.hints.question} {schema.hints.question}
{/if} {/if}
@ -92,40 +88,40 @@
<ShowConversionMessage {message} /> <ShowConversionMessage {message} />
{/each} {/each}
<slot class="self-end my-4"></slot> <slot class="my-4 self-end" />
</div> </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> <h3>Preview of this question</h3>
{#each $configs as config} {#each $configs as config}
{#if config.error !== undefined} {#if config.error !== undefined}
<div class="alert">Could not create a preview of this tagRendering: {config.error}</div> <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} {:else}
{#if config.condition && !config.condition.matchesProperties($tags)} <TagRenderingEditable
This tagRendering is currently not shown. It will appear if the feature matches the condition selectedElement={state.exampleFeature}
<b> {config}
<FromHtml src={config.condition.asHumanString(true, false, {})} /> editingEnabled={new ImmutableStore(true)}
</b> showQuestionIfUnknown={true}
{state}
Try to answer the relevant question above {tags}
{: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}
{/if} {/if}
{/each} {/each}
</div> </div>
</div> </div>

View file

@ -1,30 +1,33 @@
<script lang="ts">/*** <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 * 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 type { ConfigMeta } from "./configMeta"
import SchemaBasedInput from "./SchemaBasedInput.svelte"; import EditLayerState from "./EditLayerState"
import SchemaBasedInput from "./SchemaBasedInput.svelte"
export let state: EditLayerState; export let state: EditLayerState
export let configs: ConfigMeta[]; export let configs: ConfigMeta[]
export let title: string | undefined = undefined; export let title: string | undefined = undefined
export let path: (string | number)[] = []; 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")
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> </script>
{#if configs === undefined} {#if configs === undefined}
Bug: 'Region' received 'undefined' Bug: 'Region' received 'undefined'
{:else if configs.length === 0} {:else if configs.length === 0}
Bug: Region received empty list as configuration Bug: Region received empty list as configuration
{:else if title} {:else if title}
<div class="w-full flex flex-col"> <div class="flex w-full flex-col">
<h3>{title}</h3> <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" /> <slot name="description" />
{#each configsFiltered as config} {#each configsFiltered as config}
<SchemaBasedInput {state} path={config.path} schema={config} /> <SchemaBasedInput {state} path={config.path} schema={config} />
@ -32,10 +35,9 @@ let configsFiltered = $expertMode ? configsNoHidden : configsNoHidden.filter(sch
</div> </div>
</div> </div>
{:else} {: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} {#each configsFiltered as config}
<SchemaBasedInput {state} path={path.concat(config.path)} schema={config} /> <SchemaBasedInput {state} path={path.concat(config.path)} schema={config} />
{/each} {/each}
</div> </div>
{/if} {/if}

View file

@ -1,66 +1,70 @@
<script lang="ts"> <script lang="ts">
import EditLayerState from "./EditLayerState"; import EditLayerState from "./EditLayerState"
import {UIEventSource} from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import type {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import FullTagInput from "./TagInput/FullTagInput.svelte"; import FullTagInput from "./TagInput/FullTagInput.svelte"
import type {ConfigMeta} from "./configMeta"; import type { ConfigMeta } from "./configMeta"
import {PencilAltIcon} from "@rgossiaux/svelte-heroicons/solid"; import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid"
import { onDestroy } from "svelte"; import { onDestroy } from "svelte"
/** /**
* Thin wrapper around 'TagInput' which registers the output with the state * Thin wrapper around 'TagInput' which registers the output with the state
*/ */
export let path: (string | number)[] export let path: (string | number)[]
export let state: EditLayerState export let state: EditLayerState
export let schema: ConfigMeta export let schema: ConfigMeta
const initialValue = state.getCurrentValueFor(path) const initialValue = state.getCurrentValueFor(path)
let tag: UIEventSource<TagConfigJson> = new UIEventSource<TagConfigJson>(initialValue) 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 { function simplify(tag: TagConfigJson): string {
if (typeof tag === "string") { if (typeof tag === "string") {
return tag return tag
}
if (tag["and"]) {
return "{ and: " + simplify(tag["and"].map(simplify).join(" ; ") + " }")
}
if (tag["or"]) {
return "{ or: " + simplify(tag["or"].map(simplify).join(" ; ") + " }")
}
} }
if (tag["and"]) {
return "{ and: " + simplify(tag["and"].map(simplify).join(" ; ") + " }")
}
if (tag["or"]) {
return "{ or: " + simplify(tag["or"].map(simplify).join(" ; ") + " }")
}
}
</script> </script>
{#if mode === "editing"} {#if mode === "editing"}
<div class="interactive border-interactive"> <div class="interactive border-interactive">
<h3>{schema.hints.question ?? "What tags should be applied?"}</h3> <h3>{schema.hints.question ?? "What tags should be applied?"}</h3>
{schema.description} {schema.description}
<FullTagInput {tag}/> <FullTagInput {tag} />
<div class="flex justify-end"> <div class="flex justify-end">
<button
<button class="primary w-fit" on:click={() => {mode = "set"}}> class="primary w-fit"
Save on:click={() => {
</button> mode = "set"
</div> }}
<div class="subtle">RegisteredTagInput based on schema: {JSON.stringify(schema)}</div> >
Save
</button>
</div> </div>
<div class="subtle">RegisteredTagInput based on schema: {JSON.stringify(schema)}</div>
</div>
{:else} {:else}
<div class="low-interaction flex justify-between"> <div class="low-interaction flex justify-between">
<div> <div>
{schema.path.at(-1)}
{schema.path.at(-1)} {simplify($tag)}
{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> </div>
<button
on:click={() => {
mode = "editing"
}}
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
>
<PencilAltIcon />
</button>
</div>
{/if} {/if}

View file

@ -1,117 +1,114 @@
<script lang="ts"> <script lang="ts">
import EditLayerState from "./EditLayerState"; import EditLayerState from "./EditLayerState"
import type { ConfigMeta } from "./configMeta"; import type { ConfigMeta } from "./configMeta"
import { UIEventSource } from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import SchemaBasedInput from "./SchemaBasedInput.svelte"; import SchemaBasedInput from "./SchemaBasedInput.svelte"
import SchemaBasedField from "./SchemaBasedField.svelte"; import SchemaBasedField from "./SchemaBasedField.svelte"
import { TrashIcon } from "@babeard/svelte-heroicons/mini"; import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import QuestionPreview from "./QuestionPreview.svelte"; import QuestionPreview from "./QuestionPreview.svelte"
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"; import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
import ShowConversionMessage from "./ShowConversionMessage.svelte"; import ShowConversionMessage from "./ShowConversionMessage.svelte"
export let state: EditLayerState; export let state: EditLayerState
export let schema: ConfigMeta; export let schema: ConfigMeta
let title = schema.path.at(-1)
let title = schema.path.at(-1); let singular = title
let singular = title;
if (title?.endsWith("s")) { 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]/)) { if (singular?.match(/^[aeoui]/)) {
article = "an"; article = "an"
} }
export let path: (string | number)[] = []; export let path: (string | number)[] = []
const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings"; const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings"
if (isTagRenderingBlock) { if (isTagRenderingBlock) {
schema = { ...schema }; schema = { ...schema }
schema.description = undefined; schema.description = undefined
} }
const subparts: ConfigMeta = state.getSchemaStartingWith(schema.path) const subparts: ConfigMeta = state
.filter(part => part.path.length - 1 === schema.path.length); .getSchemaStartingWith(schema.path)
.filter((part) => part.path.length - 1 === schema.path.length)
let messages = state.messagesFor(path) let messages = state.messagesFor(path)
const currentValue: UIEventSource<any[]> = state.getStoreFor(path)
const currentValue : UIEventSource<any[]> = state.getStoreFor(path); if (currentValue.data === undefined) {
if(currentValue.data === undefined){
currentValue.setData([]) currentValue.setData([])
} }
function createItem(valueToSet?: any) { function createItem(valueToSet?: any) {
if(currentValue.data === undefined){ if (currentValue.data === undefined) {
currentValue.setData([]) currentValue.setData([])
} }
currentValue.data.push(valueToSet) currentValue.data.push(valueToSet)
currentValue.ping() currentValue.ping()
if(isTagRenderingBlock){ if (isTagRenderingBlock) {
state.highlightedItem.setData({path: [...path, currentValue.data.length - 1], schema}) state.highlightedItem.setData({ path: [...path, currentValue.data.length - 1], schema })
} }
} }
function fusePath(i: number, subpartPath: string[]): (string | number)[] { function fusePath(i: number, subpartPath: string[]): (string | number)[] {
const newPath = [...path, i]; const newPath = [...path, i]
const toAdd = [...subpartPath]; const toAdd = [...subpartPath]
for (const part of path) { for (const part of path) {
if (toAdd[0] === part) { if (toAdd[0] === part) {
toAdd.splice(0, 1); toAdd.splice(0, 1)
}else{ } else {
break break
} }
} }
newPath.push(...toAdd); newPath.push(...toAdd)
console.log({newPath}) console.log({ newPath })
return newPath; return newPath
} }
function schemaForMultitype() { function schemaForMultitype() {
const sch = {...schema} const sch = { ...schema }
sch.hints.typehint = undefined sch.hints.typehint = undefined
return sch return sch
} }
function del(i: number) {
function del(i: number){
currentValue.data.splice(i, 1) currentValue.data.splice(i, 1)
currentValue.ping() currentValue.ping()
} }
function swap(i: number, j: number) { function swap(i: number, j: number) {
const x = currentValue.data[i] const x = currentValue.data[i]
currentValue.data[i] = currentValue.data[j] currentValue.data[i] = currentValue.data[j]
currentValue.data[j] = x currentValue.data[j] = x
currentValue.ping() currentValue.ping()
} }
function moveTo(source: number, target: number){ function moveTo(source: number, target: number) {
const x = currentValue.data[source] const x = currentValue.data[source]
currentValue.data.splice(source, 1) currentValue.data.splice(source, 1)
currentValue.data.splice(target, 0, x) currentValue.data.splice(target, 0, x)
currentValue.ping() currentValue.ping()
} }
</script> </script>
<div class="pl-2"> <div class="pl-2">
<h3>{schema.path.at(-1)}</h3> <h3>{schema.path.at(-1)}</h3>
{#if subparts.length > 0} {#if subparts.length > 0}
<span class="subtle"> <span class="subtle">
{schema.description} {schema.description}
</span> </span>
{/if} {/if}
{#if $currentValue === undefined} {#if $currentValue === undefined}
No array defined No array defined
{:else if $currentValue.length === 0} {:else if $currentValue.length === 0}
No values are defined No values are defined
{#if $messages.length > 0} {#if $messages.length > 0}
{#each $messages as message} {#each $messages as message}
<ShowConversionMessage {message}/> <ShowConversionMessage {message} />
{/each} {/each}
{/if} {/if}
{:else if subparts.length === 0} {:else if subparts.length === 0}
@ -119,52 +116,79 @@
{#each $currentValue as value, i} {#each $currentValue as value, i}
<div class="flex w-full"> <div class="flex w-full">
<SchemaBasedField {state} {schema} path={[...path, i]} /> <SchemaBasedField {state} {schema} path={[...path, i]} />
<button class="border-black border rounded-full p-1 w-fit h-fit" <button
on:click={() => {del(i)}}> class="h-fit w-fit rounded-full border border-black p-1"
<TrashIcon class="w-4 h-4" /> on:click={() => {
del(i)
}}
>
<TrashIcon class="h-4 w-4" />
</button> </button>
</div> </div>
{/each} {/each}
{:else} {:else}
{#each $currentValue as value, i} {#each $currentValue as value, i}
{#if !isTagRenderingBlock} {#if !isTagRenderingBlock}
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<h3 class="m-0">{singular} {i}</h3> <h3 class="m-0">{singular} {i}</h3>
<button class="border-black border rounded-full p-1 w-fit h-fit" <button
on:click={() => {del(i)}}> class="h-fit w-fit rounded-full border border-black p-1"
<TrashIcon class="w-4 h-4" /> on:click={() => {
del(i)
}}
>
<TrashIcon class="h-4 w-4" />
</button> </button>
</div> </div>
{/if} {/if}
<div class="border border-black"> <div class="border border-black">
{#if isTagRenderingBlock} {#if isTagRenderingBlock}
<QuestionPreview {state} path={[...path, i]} {schema}> <QuestionPreview {state} path={[...path, i]} {schema}>
<button on:click={() => {del(i)}}> <button
<TrashIcon class="w-4 h-4" /> on:click={() => {
del(i)
}}
>
<TrashIcon class="h-4 w-4" />
Delete this question Delete this question
</button> </button>
{#if i > 0} {#if i > 0}
<button on:click={() => {moveTo(i, 0)}}> <button
on:click={() => {
moveTo(i, 0)
}}
>
Move to front Move to front
</button> </button>
<button on:click={() => {swap(i, i-1)}}> <button
on:click={() => {
swap(i, i - 1)
}}
>
Move up Move up
</button> </button>
{/if} {/if}
{#if i + 1 < $currentValue.length} {#if i + 1 < $currentValue.length}
<button on:click={() => {swap(i, i+1)}}> <button
on:click={() => {
swap(i, i + 1)
}}
>
Move down Move down
</button> </button>
<button on:click={() => {moveTo(i, $currentValue.length-1)}}> <button
on:click={() => {
moveTo(i, $currentValue.length - 1)
}}
>
Move to back Move to back
</button> </button>
{/if} {/if}
</QuestionPreview> </QuestionPreview>
{:else if schema.hints.types} {:else if schema.hints.types}
<SchemaBasedMultiType {state} path={fusePath(i, [])} schema={schemaForMultitype()}/> <SchemaBasedMultiType {state} path={fusePath(i, [])} schema={schemaForMultitype()} />
{:else} {:else}
{#each subparts as subpart} {#each subparts as subpart}
<SchemaBasedInput {state} path={fusePath(i, subpart.path)} schema={subpart} /> <SchemaBasedInput {state} path={fusePath(i, subpart.path)} schema={subpart} />
@ -176,7 +200,13 @@
<div class="flex"> <div class="flex">
<button on:click={() => createItem()}>Add {article} {singular}</button> <button on:click={() => createItem()}>Add {article} {singular}</button>
{#if path.length === 1 && path[0] === "tagRenderings"} {#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} {/if}
<slot name="extra-button" /> <slot name="extra-button" />
</div> </div>

View file

@ -1,50 +1,55 @@
<script lang="ts"> <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"; export let state: EditLayerState
import type { ConfigMeta } from "./configMeta"; export let path: (string | number)[] = []
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; export let schema: ConfigMeta
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; export let startInEditModeIfUnset: boolean = schema.hints && !schema.hints.ifunset
import nmd from "nano-markdown"; let value = new UIEventSource<string | any>(undefined)
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; const isTranslation =
export let path: (string | number)[] = []; schema.hints?.typehint === "translation" ||
export let schema: ConfigMeta; schema.hints?.typehint === "rendered" ||
export let startInEditModeIfUnset: boolean = schema.hints && !schema.hints.ifunset; ConfigMetaUtils.isTranslation(schema)
let value = new UIEventSource<string | any>(undefined); let type = schema.hints.typehint ?? "string"
const isTranslation = schema.hints?.typehint === "translation" || schema.hints?.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema); let rendervalue =
let type = schema.hints.typehint ?? "string"; (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") { if (schema.type === "boolean") {
rendervalue = undefined; rendervalue = undefined
} }
if (schema.hints.typehint === "tag" || schema.hints.typehint === "simple_tag") { if (schema.hints.typehint === "tag" || schema.hints.typehint === "simple_tag") {
rendervalue = "{tags()}"; rendervalue = "{tags()}"
} }
let helperArgs = schema.hints.typehelper?.split(","); let helperArgs = schema.hints.typehelper?.split(",")
let inline = schema.hints.inline !== undefined; let inline = schema.hints.inline !== undefined
if (isTranslation) { if (isTranslation) {
type = "translation"; type = "translation"
if (schema.hints.inline) { if (schema.hints.inline) {
const inlineValue = schema.hints.inline; const inlineValue = schema.hints.inline
rendervalue = inlineValue; rendervalue = inlineValue
inline = false; inline = false
helperArgs = [inlineValue.substring(0, inlineValue.indexOf("{")), inlineValue.substring(inlineValue.indexOf("}") + 1)]; helperArgs = [
inlineValue.substring(0, inlineValue.indexOf("{")),
inlineValue.substring(inlineValue.indexOf("}") + 1),
]
} }
} }
if (type.endsWith("[]")) { if (type.endsWith("[]")) {
type = type.substring(0, type.length - 2); type = type.substring(0, type.length - 2)
} }
const configJson: QuestionableTagRenderingConfigJson = { const configJson: QuestionableTagRenderingConfigJson = {
@ -52,125 +57,154 @@
render: rendervalue, render: rendervalue,
question: schema.hints.question, question: schema.hints.question,
questionHint: nmd(schema.description), questionHint: nmd(schema.description),
freeform: schema.type === "boolean" ? undefined : { freeform:
key: "value", schema.type === "boolean"
type, ? undefined
inline, : {
helperArgs key: "value",
} type,
}; inline,
helperArgs,
},
}
if (schema.hints.default) { if (schema.hints.default) {
configJson.mappings = [{ 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 ?? "") 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) { } else if (!schema.required) {
configJson.mappings = [{ configJson.mappings = [
if: "value=", {
then: path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "") if: "value=",
}]; then: path.at(-1) + " is not set. " + (schema.hints.ifunset ?? ""),
},
]
} }
function mightBeBoolean(type: undefined | JsonSchemaType): boolean { function mightBeBoolean(type: undefined | JsonSchemaType): boolean {
if (type === undefined) { if (type === undefined) {
return false; return false
} }
if (type["type"]) { if (type["type"]) {
type = type["type"]; type = type["type"]
} }
if (type === "boolean") { if (type === "boolean") {
return true; return true
} }
if (!Array.isArray(type)) { if (!Array.isArray(type)) {
return false; return false
} }
return type.some(t => mightBeBoolean(t)); return type.some((t) => mightBeBoolean(t))
} }
if (mightBeBoolean(schema.type)) { if (mightBeBoolean(schema.type)) {
configJson.mappings = configJson.mappings ?? []; configJson.mappings = configJson.mappings ?? []
configJson.mappings.push( configJson.mappings.push(
{ {
if: "value=true", if: "value=true",
then: schema.hints?.iftrue ?? "Yes" then: schema.hints?.iftrue ?? "Yes",
}, },
{ {
if: "value=false", if: "value=false",
then: schema.hints?.iffalse ?? "No" then: schema.hints?.iffalse ?? "No",
} }
); )
} }
if (schema.hints.suggestions) { if (schema.hints.suggestions) {
if (!configJson.mappings) { if (!configJson.mappings) {
configJson.mappings = []; configJson.mappings = []
} }
configJson.mappings.push(...schema.hints.suggestions); configJson.mappings.push(...schema.hints.suggestions)
} }
let config: TagRenderingConfig; let config: TagRenderingConfig
let err: string = undefined; let err: string = undefined
let messages = state.messagesFor(path); let messages = state.messagesFor(path)
try { try {
config = new TagRenderingConfig(configJson, "config based on " + schema.path.join(".")); config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."))
} catch (e) { } catch (e) {
console.error(e, config); console.error(e, config)
err = path.join(".") + " " + e; err = path.join(".") + " " + e
} }
let startValue = state.getCurrentValueFor(path); let startValue = state.getCurrentValueFor(path)
let startInEditMode = !startValue && startInEditModeIfUnset; let startInEditMode = !startValue && startInEditModeIfUnset
const tags = new UIEventSource<Record<string, string>>({ value: startValue }); const tags = new UIEventSource<Record<string, string>>({ value: startValue })
try { try {
onDestroy(state.register(path, tags.map(tgs => { onDestroy(
const v = tgs["value"]; state.register(
if (typeof v !== "string") { path,
return { ...v }; tags.map((tgs) => {
} const v = tgs["value"]
if (schema.type === "boolan") { if (typeof v !== "string") {
return v === "true" || v === "yes" || v === "1"; return { ...v }
} }
if (mightBeBoolean(schema.type)) { if (schema.type === "boolan") {
if (v === "true" || v === "yes" || v === "1") { return v === "true" || v === "yes" || v === "1"
return true; }
} if (mightBeBoolean(schema.type)) {
if (v === "false" || v === "no" || v === "0") { if (v === "true" || v === "yes" || v === "1") {
return false; return true
} }
} if (v === "false" || v === "no" || v === "0") {
if (schema.type === "number") { return false
if (v === "") { }
return undefined; }
} if (schema.type === "number") {
return Number(v); if (v === "") {
} return undefined
if (isTranslation && typeof v === "string") { }
if (v === "") { return Number(v)
return {}; }
} if (isTranslation && typeof v === "string") {
return JSON.parse(v); if (v === "") {
} return {}
return v; }
}), isTranslation)); return JSON.parse(v)
}
return v
}),
isTranslation
)
)
} catch (e) { } catch (e) {
console.error("Could not register", path, "due to", e); console.error("Could not register", path, "due to", e)
} }
</script> </script>
{#if err !== undefined} {#if err !== undefined}
<span class="alert">{err}</span> <span class="alert">{err}</span>
{:else} {:else}
<div class="w-full flex flex-col"> <div class="flex w-full flex-col">
<TagRenderingEditable editMode={startInEditMode} {config} selectedElement={undefined} showQuestionIfUnknown={true} <TagRenderingEditable
{state} {tags} /> editMode={startInEditMode}
{config}
selectedElement={undefined}
showQuestionIfUnknown={true}
{state}
{tags}
/>
{#if $messages.length > 0} {#if $messages.length > 0}
{#each $messages as message} {#each $messages as message}
<ShowConversionMessage {message} /> <ShowConversionMessage {message} />
{/each} {/each}
{/if} {/if}
{#if window.location.hostname === "127.0.0.1"} {#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" <span class="subtle" on:click={() => console.log(schema)}>
on:click={() => console.log(schema)}>{schema.hints.typehint}</span> Group: {schema.hints.group}</span> SchemaBasedField <b>{path.join(".")}</b>
<span class="cursor-pointer" on:click={() => console.log(schema)}>
{schema.hints.typehint}
</span>
Group: {schema.hints.group}
</span>
{/if} {/if}
</div> </div>
{/if} {/if}

View file

@ -1,16 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { ConfigMeta } from "./configMeta"; import type { ConfigMeta } from "./configMeta"
import SchemaBasedField from "./SchemaBasedField.svelte"; import SchemaBasedField from "./SchemaBasedField.svelte"
import EditLayerState from "./EditLayerState"; import EditLayerState from "./EditLayerState"
import SchemaBasedArray from "./SchemaBasedArray.svelte"; import SchemaBasedArray from "./SchemaBasedArray.svelte"
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"; import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
import ArrayMultiAnswer from "./ArrayMultiAnswer.svelte"; import ArrayMultiAnswer from "./ArrayMultiAnswer.svelte"
export let schema: ConfigMeta; export let schema: ConfigMeta
export let state: EditLayerState; export let state: EditLayerState
export let path: (string | number)[] = []; export let path: (string | number)[] = []
let expertMode = state.expertMode; let expertMode = state.expertMode
</script> </script>
{#if (schema.hints?.group !== "expert" || $expertMode) && schema.hints.group !== "hidden"} {#if (schema.hints?.group !== "expert" || $expertMode) && schema.hints.group !== "hidden"}
{#if schema.hints?.typehint?.endsWith("[]")} {#if schema.hints?.typehint?.endsWith("[]")}
<!-- We cheat a bit here by matching this 'magical' type... --> <!-- We cheat a bit here by matching this 'magical' type... -->
@ -25,5 +26,7 @@
<SchemaBasedField {path} {state} {schema} /> <SchemaBasedField {path} {state} {schema} />
{/if} {/if}
{:else if window.location.hostname === "127.0.0.1"} {: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} {/if}

View file

@ -1,223 +1,236 @@
<script lang="ts"> <script lang="ts">
import EditLayerState from "./EditLayerState"
import EditLayerState from "./EditLayerState"; import type { ConfigMeta } from "./configMeta"
import type { ConfigMeta } from "./configMeta"; import { UIEventSource } from "../../Logic/UIEventSource"
import { UIEventSource } from "../../Logic/UIEventSource"; import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import type { import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
QuestionableTagRenderingConfigJson import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; import { onDestroy } from "svelte"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; import SchemaBasedInput from "./SchemaBasedInput.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; import type { JsonSchemaType } from "./jsonSchema"
import { onDestroy } from "svelte";
import SchemaBasedInput from "./SchemaBasedInput.svelte";
import type { JsonSchemaType } from "./jsonSchema";
// @ts-ignore // @ts-ignore
import nmd from "nano-markdown"; import nmd from "nano-markdown"
import ShowConversionMessage from "./ShowConversionMessage.svelte"; import ShowConversionMessage from "./ShowConversionMessage.svelte"
/** /**
* If 'types' is defined: allow the user to pick one of the types to input. * If 'types' is defined: allow the user to pick one of the types to input.
*/ */
export let state: EditLayerState; export let state: EditLayerState
export let path: (string | number)[] = []; export let path: (string | number)[] = []
export let schema: ConfigMeta; export let schema: ConfigMeta
let expertMode = state.expertMode; let expertMode = state.expertMode
const defaultOption = schema.hints.typesdefault ? Number(schema.hints.typesdefault) : undefined; const defaultOption = schema.hints.typesdefault ? Number(schema.hints.typesdefault) : undefined
const hasBooleanOption = (<JsonSchemaType[]>schema.type)?.findIndex(t => t["type"] === "boolean"); const hasBooleanOption = (<JsonSchemaType[]>schema.type)?.findIndex(
const types = schema.hints.types.split(";"); (t) => t["type"] === "boolean"
)
const types = schema.hints.types.split(";")
if (hasBooleanOption >= 0) { 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 : []; const types: string | string[] = Array.isArray(schema.type)
lastIsString = types === "string" || (Array.isArray(types) && types.some(i => i === "string")); ? schema.type[schema.type.length - 1].type
: []
lastIsString = types === "string" || (Array.isArray(types) && types.some((i) => i === "string"))
} }
if (lastIsString) { if (lastIsString) {
types.splice(types.length - 1, 1); types.splice(types.length - 1, 1)
} }
const configJson: QuestionableTagRenderingConfigJson = { const configJson: QuestionableTagRenderingConfigJson = {
id: "TYPE_OF:" + path.join("_"), id: "TYPE_OF:" + path.join("_"),
question: "Which subcategory is needed for " + schema.path.at(-1) + "?", question: "Which subcategory is needed for " + schema.path.at(-1) + "?",
questionHint: nmd(schema.description), questionHint: nmd(schema.description),
mappings: types.map(opt => opt.trim()).filter(opt => opt.length > 0).map((opt, i) => ({ mappings: types
if: "chosen_type_index=" + i, .map((opt) => opt.trim())
addExtraTags: ["value="], .filter((opt) => opt.length > 0)
then: opt + (i === defaultOption ? " (Default)" : "") .map((opt, i) => ({
})), if: "chosen_type_index=" + i,
render: !lastIsString ? undefined : (schema.hints.inline ?? "Use a hardcoded value: <b>{value}</b>"), addExtraTags: ["value="],
freeform: !lastIsString ? undefined : { then: opt + (i === defaultOption ? " (Default)" : ""),
key: "value", })),
inline: true, render: !lastIsString
type: schema.hints.typehint, ? undefined
addExtraTags: ["chosen_type_index="] : schema.hints.inline ?? "Use a hardcoded value: <b>{value}</b>",
} freeform: !lastIsString
}; ? undefined
let tags = new UIEventSource<Record<string, string>>({}); : {
key: "value",
inline: true,
type: schema.hints.typehint,
addExtraTags: ["chosen_type_index="],
},
}
let tags = new UIEventSource<Record<string, string>>({})
if (schema.hints.ifunset) { if (schema.hints.ifunset) {
configJson.mappings.push( configJson.mappings.push({
{ if: { and: ["value=", "chosen_type_index="] },
if: { and: ["value=", "chosen_type_index="] }, then: schema.hints.ifunset,
then: schema.hints.ifunset })
}
);
} }
if (schema.hints.suggestions) { if (schema.hints.suggestions) {
configJson.mappings.push(...schema.hints.suggestions); configJson.mappings.push(...schema.hints.suggestions)
} }
if (hasBooleanOption >= 0) { if (hasBooleanOption >= 0) {
configJson.mappings.unshift( configJson.mappings.unshift(
{ {
if: "value=true", if: "value=true",
then: (schema.hints.iftrue ?? "Yes"), then: schema.hints.iftrue ?? "Yes",
addExtraTags: ["chosen_type_index="] addExtraTags: ["chosen_type_index="],
}, },
{ {
if: "value=false", if: "value=false",
then: (schema.hints.iffalse ?? "No"), then: schema.hints.iffalse ?? "No",
addExtraTags: ["chosen_type_index="] addExtraTags: ["chosen_type_index="],
} }
); )
} }
const config = new TagRenderingConfig(configJson, "config based on " + schema.path.join(".")); const config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."))
let chosenOption: number = (defaultOption); let chosenOption: number = defaultOption
const existingValue = state.getCurrentValueFor(path)
const existingValue = state.getCurrentValueFor(path); let hasOverride = existingValue?.override !== undefined
let hasOverride = existingValue?.override !== undefined;
if (hasBooleanOption >= 0 && (existingValue === true || existingValue === false)) { if (hasBooleanOption >= 0 && (existingValue === true || existingValue === false)) {
tags.setData({ value: "" + existingValue }); tags.setData({ value: "" + existingValue })
} else if (lastIsString && typeof existingValue === "string") { } else if (lastIsString && typeof existingValue === "string") {
tags.setData({ value: existingValue }); tags.setData({ value: existingValue })
chosenOption = undefined; chosenOption = undefined
} else if (existingValue) { } else if (existingValue) {
// We found an existing value. Let's figure out what type it matches and select that one // 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 // 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++) { outer: for (let i = 0; i < (<[]>schema.type).length; i++) {
const type = schema.type[i]; const type = schema.type[i]
let optionalMatches = 0; let optionalMatches = 0
for (const key of Object.keys(type.properties ?? {})) { for (const key of Object.keys(type.properties ?? {})) {
if (!!existingValue[key]) { if (!!existingValue[key]) {
optionalMatches++; optionalMatches++
} }
} }
if (type.required) { if (type.required) {
let numberOfMatches = 0; let numberOfMatches = 0
for (const requiredAttribute of type.required) { for (const requiredAttribute of type.required) {
if (existingValue[requiredAttribute] === undefined) { if (existingValue[requiredAttribute] === undefined) {
// The 'existingValue' does _not_ have this required attribute, so it cannot be of this type // 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 { } 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.optionalMatches - a.optionalMatches)
possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount); possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount)
if (possibleTypes.length > 0) { if (possibleTypes.length > 0) {
chosenOption = possibleTypes[0].index; chosenOption = possibleTypes[0].index
tags.setData({ chosen_type_index: "" + chosenOption }); tags.setData({ chosen_type_index: "" + chosenOption })
} }
} else if (defaultOption !== undefined) { } else if (defaultOption !== undefined) {
tags.setData({ chosen_type_index: "" + defaultOption }); tags.setData({ chosen_type_index: "" + defaultOption })
} else { } else {
chosenOption = defaultOption; chosenOption = defaultOption
} }
if (hasBooleanOption >= 0 || lastIsString) { if (hasBooleanOption >= 0 || lastIsString) {
const directValue = tags.mapD((tags) => {
const directValue = tags.mapD(tags => {
if (tags["chosen_type_index"]) { if (tags["chosen_type_index"]) {
return ""; return ""
} }
if (lastIsString) { if (lastIsString) {
return tags["value"]; return tags["value"]
} }
return tags["value"] === "true"; return tags["value"] === "true"
}); })
onDestroy(state.register(path, directValue)); onDestroy(state.register(path, directValue))
} }
let subSchemas: ConfigMeta[] = []; let subSchemas: ConfigMeta[] = []
let subpath = path; let subpath = path
const store = state.getStoreFor(path); const store = state.getStoreFor(path)
onDestroy(tags.addCallbackAndRun(tags => { onDestroy(
if (tags["value"] !== undefined && tags["value"] !== "") { tags.addCallbackAndRun((tags) => {
chosenOption = undefined; if (tags["value"] !== undefined && tags["value"] !== "") {
return; 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] ??= {};
} }
store.setData(o); const oldOption = chosenOption
} chosenOption = tags["chosen_type_index"] ? Number(tags["chosen_type_index"]) : defaultOption
if (!type) { const type = schema.type[chosenOption]
return; if (chosenOption !== oldOption) {
} // Reset the values beneath
subpath = path; subSchemas = []
const cleanPath = <string[]>path.filter(p => typeof p === "string"); const o = state.getCurrentValueFor(path) ?? {}
if (type["$ref"] === "#/definitions/Record<string,string>") { for (const key of type?.required ?? []) {
// The subtype is a translation object o[key] ??= {}
const schema = state.getTranslationAt(cleanPath); }
subSchemas.push(schema); store.setData(o)
subpath = path.slice(0, path.length - 2); }
return; if (!type) {
} return
if (!type.properties) { }
return; subpath = path
} const cleanPath = <string[]>path.filter((p) => typeof p === "string")
for (const crumble of Object.keys(type.properties)) { if (type["$ref"] === "#/definitions/Record<string,string>") {
subSchemas.push(...(state.getSchema([...cleanPath, crumble]))); // The subtype is a translation object
} const schema = state.getTranslationAt(cleanPath)
})); subSchemas.push(schema)
let messages = state.messagesFor(path); 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> </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} {#if schema.hints.title !== undefined}
<h3>{schema.hints.title}</h3> <h3>{schema.hints.title}</h3>
<div> {schema.description} </div> <div>{schema.description}</div>
{/if} {/if}
{#if hasOverride} {#if hasOverride}
This object refers to {existingValue.builtin} and overrides some properties. This cannot be edited with MapComplete This object refers to {existingValue.builtin} and overrides some properties. This cannot be edited
Studio with MapComplete Studio
{:else} {:else}
<div> <div>
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={!schema.hints?.ifunset} {state} {tags} /> <TagRenderingEditable
{config}
selectedElement={undefined}
showQuestionIfUnknown={!schema.hints?.ifunset}
{state}
{tags}
/>
</div> </div>
{#if chosenOption !== undefined} {#if chosenOption !== undefined}
{#each subSchemas as subschema} {#each subSchemas as subschema}
{#if $expertMode || subschema.hints?.group !== "expert"} {#if $expertMode || subschema.hints?.group !== "expert"}
<SchemaBasedInput {state} schema={subschema} <SchemaBasedInput
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput> {state}
schema={subschema}
path={[...subpath, subschema?.path?.at(-1) ?? "???"]}
/>
{:else if window.location.hostname === "127.0.0.1"} {:else if window.location.hostname === "127.0.0.1"}
<span class="subtle">Omitted expert question {subschema.path.join(".")}</span> <span class="subtle">Omitted expert question {subschema.path.join(".")}</span>
{/if} {/if}
{/each} {/each}
{:else if $messages.length > 0} {:else if $messages.length > 0}
@ -227,7 +240,11 @@
{/if} {/if}
{/if} {/if}
{#if window.location.hostname === "127.0.0.1"} {#if window.location.hostname === "127.0.0.1"}
<span class="subtle">SchemaBasedMultiType <b>{path.join(".")}</b> <span class="cursor-pointer" <span class="subtle">
on:click={() => console.log(schema)}>{schema.hints.typehint}</span></span> SchemaBasedMultiType <b>{path.join(".")}</b>
<span class="cursor-pointer" on:click={() => console.log(schema)}>
{schema.hints.typehint}
</span>
</span>
{/if} {/if}
</div> </div>

View file

@ -1,16 +1,19 @@
<script lang="ts"> <script lang="ts">
import EditLayerState from "./EditLayerState"; import EditLayerState from "./EditLayerState"
import type { ConfigMeta } from "./configMeta"; import type { ConfigMeta } from "./configMeta"
import { UIEventSource } from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import TranslationInput from "../InputElement/Helpers/TranslationInput.svelte"; import TranslationInput from "../InputElement/Helpers/TranslationInput.svelte"
export let state: EditLayerState; export let state: EditLayerState
export let path: (string | number)[] = []; export let path: (string | number)[] = []
export let schema: ConfigMeta; export let schema: ConfigMeta
let value = new UIEventSource<string>({}); let value = new UIEventSource<string>({})
console.log("Registering translation to path", path) 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> </script>
<TranslationInput {value} /> <TranslationInput {value} />

View file

@ -1,29 +1,29 @@
<script lang="ts"> <script lang="ts">
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"
import { ExclamationIcon, InformationCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; import { ExclamationIcon, InformationCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
/** /**
* Single conversion message, styled depending on the type * Single conversion message, styled depending on the type
*/ */
export let message: ConversionMessage; export let message: ConversionMessage
</script> </script>
{#if message.level === "error"} {#if message.level === "error"}
<div class="alert flex justify-between items-center"> <div class="alert flex items-center justify-between">
<ExclamationIcon class="w-6 h-6 mx-1 shrink-0" /> <ExclamationIcon class="mx-1 h-6 w-6 shrink-0" />
{message.message} {message.message}
<div/> <div />
</div> </div>
{:else if message.level === "warning"} {:else if message.level === "warning"}
<div class="warning flex justify-between items-center"> <div class="warning flex items-center justify-between">
<ExclamationIcon class="w-6 h-6 mx-1 shrink-0" /> <ExclamationIcon class="mx-1 h-6 w-6 shrink-0" />
{message.message} {message.message}
<div/> <div />
</div> </div>
{:else if message.level === "information"} {:else if message.level === "information"}
<div class="information flex justify-between items-center"> <div class="information flex items-center justify-between">
<InformationCircleIcon class="w-6 h-6 mx-1 shrink-0" /> <InformationCircleIcon class="mx-1 h-6 w-6 shrink-0" />
{message.message} {message.message}
<div/> <div />
</div> </div>
{/if} {/if}

View file

@ -1,14 +1,12 @@
<script lang="ts"> <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> </script>
{#if messages.length === 0} {#if messages.length === 0}
<div class="thanks"> <div class="thanks">No errors, warnings or messages</div>
No errors, warnings or messages {/if}
</div>
{/if}
{#each messages as message} {#each messages as message}
<li> <li>
@ -16,7 +14,7 @@
<span class="literal-code">{message.context.path.join(".")}</span> <span class="literal-code">{message.context.path.join(".")}</span>
{message.message} {message.message}
<span class="literal-code"> <span class="literal-code">
{message.context.operation.join(".")} {message.context.operation.join(".")}
</span> </span>
</li> </li>
{/each} {/each}

View file

@ -1,134 +1,130 @@
<script lang="ts">/** <script lang="ts">
* Allows to create `and` and `or` expressions graphically /**
*/ * Allows to create `and` and `or` expressions graphically
*/
import { UIEventSource } from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"; import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import BasicTagInput from "./TagInput/BasicTagInput.svelte"; import BasicTagInput from "./TagInput/BasicTagInput.svelte"
import FullTagInput from "./TagInput/FullTagInput.svelte"; import FullTagInput from "./TagInput/FullTagInput.svelte"
import { TrashIcon } from "@babeard/svelte-heroicons/mini"; import { TrashIcon } from "@babeard/svelte-heroicons/mini"
export let tag: UIEventSource<TagConfigJson>; export let tag: UIEventSource<TagConfigJson>
let mode: "and" | "or" = "and"; let mode: "and" | "or" = "and"
let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([]); let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([])
/** /**
* Sub-expressions * Sub-expressions
*/ */
let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([]); let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([])
export let uploadableOnly: boolean; export let uploadableOnly: boolean
export let overpassSupportNeeded: boolean; export let overpassSupportNeeded: boolean
export let silent: boolean; export let silent: boolean
function update(_) { function update(_) {
let config: TagConfigJson = <any>{}; let config: TagConfigJson = <any>{}
if (!mode) { if (!mode) {
return; return
} }
const tags = []; const tags = []
const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data); const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(
for (const src of subpartSources) { expressions.data
const t = src.data; )
if (!t) { for (const src of subpartSources) {
// We indicate upstream that this value is invalid const t = src.data
tag.setData(undefined); if (!t) {
return; // 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 { } else {
config[mode] = tags; initWith(initialTag)
tag.setData(config);
} }
}
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> </script>
<div class="flex items-center"> <div class="flex items-center">
{#if !uploadableOnly} {#if !uploadableOnly}
<select bind:value={mode}> <select bind:value={mode}>
<option value="and">and</option> <option value="and">and</option>
@ -136,14 +132,16 @@ if (!initialTag) {
</select> </select>
{/if} {/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)} {#each $basicTags as basicTag (basicTag)}
<div class="flex"> <div class="flex">
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} on:submit /> <BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} on:submit />
{#if $basicTags.length + $expressions.length > 1} {#if $basicTags.length + $expressions.length > 1}
<button class="border border-black rounded-full w-fit h-fit p-0" <button
on:click={() => removeTag(basicTag)}> class="h-fit w-fit rounded-full border border-black p-0"
<TrashIcon class="w-4 h-4 p-1" /> on:click={() => removeTag(basicTag)}
>
<TrashIcon class="h-4 w-4 p-1" />
</button> </button>
{/if} {/if}
</div> </div>
@ -151,23 +149,18 @@ if (!initialTag) {
{#each $expressions as expression} {#each $expressions as expression}
<FullTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={expression}> <FullTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={expression}>
<button class="small" slot="delete" on:click={() => removeExpression(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 Delete subexpression
</button> </button>
</FullTagInput> </FullTagInput>
{/each} {/each}
<div class="flex"> <div class="flex">
<button class="w-fit small" on:click={() => addBasicTag()}> <button class="small w-fit" on:click={() => addBasicTag()}>Add a tag</button>
Add a tag
</button>
{#if !uploadableOnly} {#if !uploadableOnly}
<!-- Do not allow to add an expression, as everything is 'and' anyway --> <!-- Do not allow to add an expression, as everything is 'and' anyway -->
<button class="w-fit small" on:click={() => addExpression()}> <button class="small w-fit" on:click={() => addExpression()}>Add an expression</button>
Add an expression
</button>
{/if} {/if}
<slot name="delete" /> <slot name="delete" />
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,63 +1,67 @@
<script lang="ts">/** <script lang="ts">
* A small component showing statistics from tagInfo. /**
* Will show this in an 'alert' if very little (<250) tags are known * 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 { TagUtils } from "../../Logic/Tags/TagUtils"
import type { TagInfoStats } from "../../Logic/Web/TagInfo"; import { Store, UIEventSource } from "../../Logic/UIEventSource"
import TagInfo from "../../Logic/Web/TagInfo"; import type { TagInfoStats } from "../../Logic/Web/TagInfo"
import { twMerge } from "tailwind-merge"; import TagInfo from "../../Logic/Web/TagInfo"
import Loading from "../Base/Loading.svelte"; import { twMerge } from "tailwind-merge"
import Loading from "../Base/Loading.svelte"
export let silent = false; export let silent = false
export let tag: UIEventSource<string>; export let tag: UIEventSource<string>
const tagStabilized = tag.stabilized(500); const tagStabilized = tag.stabilized(500)
const tagInfoStats: Store<TagInfoStats> = tagStabilized.bind(tag => { const tagInfoStats: Store<TagInfoStats> = tagStabilized.bind((tag) => {
if (!tag) { if (!tag) {
return undefined; return undefined
} }
try { try {
const t = TagUtils.Tag(tag)
const t = TagUtils.Tag(tag); const k = t["key"]
const k = t["key"]; let v = t["value"]
let v = t["value"]; if (typeof v !== "string") {
if (typeof v !== "string") { v = undefined
v = undefined; }
} if (!k) {
if (!k) { return undefined
return undefined; }
} return UIEventSource.FromPromise(TagInfo.global.getStats(k, v))
return UIEventSource.FromPromise(TagInfo.global.getStats(k, v));
} catch (e) { } catch (e) {
return undefined; return undefined
} }
}); })
const tagInfoUrl: Store<string> = tagStabilized.mapD(tag => { const tagInfoUrl: Store<string> = tagStabilized.mapD((tag) => {
try { try {
const t = TagUtils.Tag(tag)
const t = TagUtils.Tag(tag); const k = t["key"]
const k = t["key"]; let v = t["value"]
let v = t["value"]; if (typeof v !== "string") {
if (typeof v !== "string") { v = undefined
v = undefined; }
} if (!k) {
if (!k) { return undefined
return undefined; }
} return TagInfo.global.webUrl(k, v)
return TagInfo.global.webUrl(k, v);
} catch (e) { } 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> </script>
{#if $tagStabilized !== $tag} {#if $tagStabilized !== $tag}
{#if !silent} {#if !silent}
<Loading /> <Loading />
{/if} {/if}
{:else if $tagInfoStats && (!silent || $total < 250) } {:else if $tagInfoStats && (!silent || $total < 250)}
<a href={$tagInfoUrl} target="_blank" class={twMerge(($total < 250) ? "alert" : "thanks", "w-fit link-underline")}> <a
{$total} features have <span class="literal-code">{$tag}</span> href={$tagInfoUrl}
</a> target="_blank"
class={twMerge($total < 250 ? "alert" : "thanks", "link-underline w-fit")}
>
{$total} features have
<span class="literal-code">{$tag}</span>
</a>
{/if} {/if}

View file

@ -1,134 +1,138 @@
<script lang="ts"> <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"; export let tag: UIEventSource<string> = new UIEventSource<string>(undefined)
import {UIEventSource} from "../../../Logic/UIEventSource"; export let uploadableOnly: boolean
import {onDestroy} from "svelte"; export let overpassSupportNeeded: boolean
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 dropdownFocussed = new UIEventSource(false)
export let uploadableOnly: boolean
export let overpassSupportNeeded: boolean
export let dropdownFocussed = new UIEventSource(false)
/** /**
* If set, do not show tagInfo if there are many features matching * If set, do not show tagInfo if there are many features matching
*/ */
export let silent : boolean = false 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
}
}) export let selected: UIEventSource<boolean> = new UIEventSource<boolean>(false)
let feedbackKey = new UIEventSource<Translation>(undefined) let feedbackGlobal = tag.map((tag) => {
let keyValue = new UIEventSource<string>(undefined) if (!tag) {
return 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)
} }
if (!uploadableOnly && !overpassSupportNeeded) { try {
modes.push(...TagUtils.comparators.map(c => c[0])) TagUtils.Tag(tag)
return undefined
} catch (e) {
return e
} }
})
let feedbackKey = new UIEventSource<Translation>(undefined)
let keyValue = new UIEventSource<string>(undefined)
if (tag.data) { let feedbackValue = new UIEventSource<Translation>(undefined)
const sortedModes = [...modes] /**
sortedModes.sort((a, b) => b.length - a.length) * The value of the tag. The name is a bit confusing
const t = tag.data */
console.log(t) let valueValue = new UIEventSource<string>(undefined)
for (const m of sortedModes) {
if (t.indexOf(m) >= 0) { export let mode: string = "="
const [k, v] = t.split(m) let modes: string[] = []
keyValue.setData(k)
valueValue.setData(v) for (const k in TagUtils.modeDocumentation) {
mode = m const docs = TagUtils.modeDocumentation[k]
break if (overpassSupportNeeded && !docs.overpassSupport) {
} continue
}
} }
if (uploadableOnly && !docs.uploadable) {
continue
onDestroy(valueValue.addCallbackAndRun(setTag))
onDestroy(keyValue.addCallbackAndRun(setTag))
$: {
setTag(mode)
} }
modes.push(k)
}
if (!uploadableOnly && !overpassSupportNeeded) {
modes.push(...TagUtils.comparators.map((c) => c[0]))
}
function setTag(_) { if (tag.data) {
const k = keyValue.data const sortedModes = [...modes]
const v = valueValue.data ?? "" sortedModes.sort((a, b) => b.length - a.length)
if(k === undefined || k === ""){ const t = tag.data
tag.setData(undefined) console.log(t)
return for (const m of sortedModes) {
} if (t.indexOf(m) >= 0) {
const t = k + mode + v const [k, v] = t.split(m)
try { keyValue.setData(k)
TagUtils.Tag(t) valueValue.setData(v)
tag.setData(t) mode = m
} catch (e) { break
tag.setData(undefined) }
}
} }
}
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> </script>
<div class="flex items-center"> <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 "> {#if $feedbackKey}
<Tr cls="alert" t={$feedbackKey} />
<ValidatedInput feedback={feedbackKey} placeholder="The key of the tag" type="key" {:else if $feedbackValue}
value={keyValue} on:submit></ValidatedInput> <Tr cls="alert" t={$feedbackValue} />
<select bind:value={mode} on:focusin={() => dropdownFocussed.setData(true)} on:focusout={() => dropdownFocussed.setData(false)}> {:else if $feedbackGlobal}
{#each modes as option} <Tr cls="alert" t={$feedbackGlobal} />
<option value={option}> {/if}
{option} <TagInfoStats {silent} {tag} />
</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}/>
</div> </div>

View file

@ -1,19 +1,19 @@
<script lang="ts">/** <script lang="ts">
* An element input a tag; has `and`, `or`, `regex`, ... /**
*/ * An element input a tag; has `and`, `or`, `regex`, ...
import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson"; */
import { UIEventSource } from "../../../Logic/UIEventSource"; import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson"
import TagExpression from "../TagExpression.svelte"; import { UIEventSource } from "../../../Logic/UIEventSource"
import TagExpression from "../TagExpression.svelte"
export let tag: UIEventSource<string | TagConfigJson>
export let tag: UIEventSource<string | TagConfigJson> export let uploadableOnly: boolean
export let uploadableOnly: boolean export let overpassSupportNeeded: boolean
export let overpassSupportNeeded: boolean export let silent: boolean
export let silent: boolean
</script> </script>
<div class="m-2"> <div class="m-2">
<TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly} on:submit> <TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly} on:submit>
<slot name="delete" slot="delete"/> <slot name="delete" slot="delete" />
</TagExpression> </TagExpression>
</div> </div>

View file

@ -1,14 +1,12 @@
<script lang="ts"> <script lang="ts">
import SchemaBasedInput from "./SchemaBasedInput.svelte"
import SchemaBasedInput from "./SchemaBasedInput.svelte"; import EditLayerState from "./EditLayerState"
import EditLayerState from "./EditLayerState"; import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
export let state: EditLayerState export let state: EditLayerState
export let path : (number | string)[] export let path: (number | string)[]
let schema : TagRenderingConfig
let schema: TagRenderingConfig
</script> </script>
XYZ XYZ

View file

@ -1,173 +1,227 @@
<script lang="ts">/** <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 * 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 EditLayerState from "./EditLayerState"
import type { import type { ConfigMeta } from "./configMeta"
MappingConfigJson, import type {
QuestionableTagRenderingConfigJson MappingConfigJson,
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; QuestionableTagRenderingConfigJson,
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { Store, UIEventSource } from "../../Logic/UIEventSource"; import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import * as questions from "../../assets/generated/layers/questions.json"; import { Store, UIEventSource } from "../../Logic/UIEventSource"
import MappingInput from "./MappingInput.svelte"; import * as questions from "../../assets/generated/layers/questions.json"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline"; import MappingInput from "./MappingInput.svelte"
import questionableTagRenderingSchemaRaw from "../../assets/schemas/questionabletagrenderingconfigmeta.json"; import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline"
import SchemaBasedField from "./SchemaBasedField.svelte"; import questionableTagRenderingSchemaRaw from "../../assets/schemas/questionabletagrenderingconfigmeta.json"
import Region from "./Region.svelte"; import SchemaBasedField from "./SchemaBasedField.svelte"
import NextButton from "../Base/NextButton.svelte"; import Region from "./Region.svelte"
import { QuestionMarkCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; import NextButton from "../Base/NextButton.svelte"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"; import { QuestionMarkCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { onMount } from "svelte"; import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import { onMount } from "svelte"
export let state: EditLayerState; export let state: EditLayerState
export let schema: ConfigMeta; export let schema: ConfigMeta
export let path: (string | number)[]; export let path: (string | number)[]
let expertMode = state.expertMode; let expertMode = state.expertMode
const store = state.getStoreFor(path); const store = state.getStoreFor(path)
let value = store.data; let value = store.data
let hasSeenIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false")) let hasSeenIntro = UIEventSource.asBoolean(
onMount(() => { LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false")
if(!hasSeenIntro.data){ )
state.showIntro.setData("tagrenderings") onMount(() => {
hasSeenIntro.setData(true) 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 /**
*/ * Allows the theme builder to create 'writable' themes.
let allowQuestions: Store<boolean> = (state.configuration.mapD(config => path.at(0) === "tagRenderings" && config.source?.geoJson === undefined)); * 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[] = []; if (!labelMapping) {
let perLabel: Record<string, MappingConfigJson> = {}; labelMapping = {
for (const tr of questions.tagRenderings) { if: "value=" + label,
let description = tr["description"] ?? tr["question"] ?? "No description available"; then: {
description = description["en"] ?? description; en: "Builtin collection <b>" + label + "</b>:",
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>:"
} }
}; perLabel[label] = labelMapping
perLabel[label] = labelMapping; mappingsBuiltin.push(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
} }
} }
function initMappings() {
mappingsBuiltin.push({ if (mappings.data === undefined) {
if: "value=" + tr["id"], mappings.setData([])
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;
} }
}
function initMappings() { const items = new Set([
if (mappings.data === undefined) { "question",
mappings.setData([]); "questionHint",
} "multiAnswer",
} "freeform",
"render",
const items = new Set(["question", "questionHint", "multiAnswer", "freeform", "render", "condition", "metacondition", "mappings", "icon"]); "condition",
const ignored = new Set(["labels", "description", "classes"]); "metacondition",
"mappings",
const freeformSchemaAll = <ConfigMeta[]>questionableTagRenderingSchemaRaw "icon",
.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 ignored = new Set(["labels", "description", "classes"])
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 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> </script>
{#if typeof $store === "string"} {#if typeof $store === "string"}
<div class="flex low-interaction"> <div class="low-interaction flex">
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state} <TagRenderingEditable
{tags} /> config={configBuiltin}
selectedElement={undefined}
showQuestionIfUnknown={true}
{state}
{tags}
/>
<slot name="upper-right" /> <slot name="upper-right" />
</div> </div>
{:else} {: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"> <div class="flex justify-end">
<slot name="upper-right" /> <slot name="upper-right" />
</div> </div>
{#if $allowQuestions} {#if $allowQuestions}
<SchemaBasedField startInEditModeIfUnset={true} {state} path={[...path,"question"]} <SchemaBasedField
schema={topLevelItems["question"]} /> startInEditModeIfUnset={true}
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} /> {state}
path={[...path, "question"]}
schema={topLevelItems["question"]}
/>
<SchemaBasedField
{state}
path={[...path, "questionHint"]}
schema={topLevelItems["questionHint"]}
/>
{/if} {/if}
{#each ($mappings ?? []) as mapping, i (mapping)} {#each $mappings ?? [] as mapping, i (mapping)}
<div class="flex interactive w-full"> <div class="interactive flex w-full">
<MappingInput {state} path={path.concat(["mappings", i])}> <MappingInput {state} path={path.concat(["mappings", i])}>
<button slot="delete" class="rounded-full no-image-background" on:click={() => { <button
initMappings(); slot="delete"
mappings.data.splice(i, 1) class="no-image-background rounded-full"
mappings.ping() on:click={() => {
}}> initMappings()
<TrashIcon class="w-4 h-4" /> mappings.data.splice(i, 1)
mappings.ping()
}}
>
<TrashIcon class="h-4 w-4" />
</button> </button>
</MappingInput> </MappingInput>
</div> </div>
{/each} {/each}
<button class="primary" <button
on:click={() =>{ initMappings(); mappings.data.push({if: undefined, then: {}}); mappings.ping()} }> class="primary"
on:click={() => {
initMappings()
mappings.data.push({ if: undefined, then: {} })
mappings.ping()
}}
>
Add a predefined option Add a predefined option
</button> </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> <h3>Text field and input element configuration</h3>
<div class="border-l pl-2 border-gray-800 border-dashed"> <div class="border-l border-dashed border-gray-800 pl-2">
<SchemaBasedField {state} path={[...path,"render"]} schema={topLevelItems["render"]} /> <SchemaBasedField {state} path={[...path, "render"]} schema={topLevelItems["render"]} />
<Region {state} {path} configs={freeformSchema} /> <Region {state} {path} configs={freeformSchema} />
<SchemaBasedField {state} path={[...path,"icon"]} schema={topLevelItems["icon"]} /> <SchemaBasedField {state} path={[...path, "icon"]} schema={topLevelItems["icon"]} />
</div> </div>
<SchemaBasedField {state} path={[...path,"condition"]} schema={topLevelItems["condition"]} /> <SchemaBasedField {state} path={[...path, "condition"]} schema={topLevelItems["condition"]} />
{#if $expertMode} {#if $expertMode}
<SchemaBasedField {state} path={[...path,"metacondition"]} schema={topLevelItems["metacondition"]} /> <SchemaBasedField
{state}
path={[...path, "metacondition"]}
schema={topLevelItems["metacondition"]}
/>
{/if} {/if}
{#each missing as field} {#each missing as field}
<SchemaBasedField {state} path={[...path,field]} schema={topLevelItems[field]} /> <SchemaBasedField {state} path={[...path, field]} schema={topLevelItems[field]} />
{/each} {/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> </div>
{/if} {/if}

View file

@ -1,136 +1,157 @@
<script lang="ts"> <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 If from "./Base/If.svelte"
import { Store, UIEventSource } from "../Logic/UIEventSource"; import BackButton from "./Base/BackButton.svelte"
import EditLayerState, { EditThemeState } from "./Studio/EditLayerState"; import ChooseLayerToEdit from "./Studio/ChooseLayerToEdit.svelte"
import EditLayer from "./Studio/EditLayer.svelte"; import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import Loading from "../assets/svg/Loading.svelte"; import FloatOver from "./Base/FloatOver.svelte"
import StudioServer from "./Studio/StudioServer"; import Walkthrough from "./Walkthrough/Walkthrough.svelte"
import LoginToggle from "./Base/LoginToggle.svelte"; import * as intro from "../assets/studio_introduction.json"
import { OsmConnection } from "../Logic/Osm/OsmConnection"; import * as intro_tagrenderings from "../assets/studio_tagrenderings_intro.json"
import { QueryParameters } from "../Logic/Web/QueryParameters";
import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json"; import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini"
import layoutSchemaRaw from "../../src/assets/schemas/layoutconfigmeta.json"; 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"; export let studioUrl =
import BackButton from "./Base/BackButton.svelte"; window.location.hostname === "127.0.0.2"
import ChooseLayerToEdit from "./Studio/ChooseLayerToEdit.svelte"; ? "http://127.0.0.1:1235"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"; : "https://studio.mapcomplete.org"
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"; 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({ let layersWithErr = UIEventSource.FromPromiseWithErr(studio.fetchOverview())
oauth_token: QueryParameters.GetQueryParameter( let layers: Store<{ owner: number }[]> = layersWithErr.mapD((l) =>
"oauth_token", l.success?.filter((l) => l.category === "layers")
undefined, )
"Used to complete the login" 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),
const expertMode = UIEventSource.asBoolean(osmConnection.GetPreference("studio-expert-mode", "false", { [uid]
documentation: "Indicates if more options are shown in mapcomplete studio" )
})); let officialLayers = layers.mapD((ls) => ls.filter((l) => l.owner === undefined), [uid])
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 themes: Store<{ owner: number }[]> = layersWithErr.mapD((l) =>
let layers: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success?.filter(l => l.category === "layers")); l.success?.filter((l) => l.category === "themes")
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 selfThemes = themes.mapD((ls) => ls.filter((l) => l.owner === uid.data), [uid])
let officialLayers = layers.mapD(ls => ls.filter(l => l.owner === undefined), [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")); const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
let selfThemes = themes.mapD(ls => ls.filter(l => l.owner === uid.data), [uid]); let editLayerState = new EditLayerState(layerSchema, studio, osmConnection, { expertMode })
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 });
let showIntro = editLayerState.showIntro let showIntro = editLayerState.showIntro
const layoutSchema: ConfigMeta[] = <any>layoutSchemaRaw; const layoutSchema: ConfigMeta[] = <any>layoutSchemaRaw
let editThemeState = new EditThemeState(layoutSchema, studio, { expertMode }); 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) { async function editLayer(event: Event) {
const layerId: { owner: number, id: string } = event.detail; const layerId: { owner: number; id: string } = event.detail
state = "loading"; state = "loading"
editLayerState.startSavingUpdates(false); editLayerState.startSavingUpdates(false)
editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner)); editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner))
editLayerState.startSavingUpdates(); editLayerState.startSavingUpdates()
state = "editing_layer"; state = "editing_layer"
} }
async function editTheme(event: Event) { async function editTheme(event: Event) {
const id: { id: string, owner: number } = event.detail; const id: { id: string; owner: number } = event.detail
state = "loading"; state = "loading"
editThemeState.startSavingUpdates(false); editThemeState.startSavingUpdates(false)
editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner)); editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner))
editThemeState.startSavingUpdates(); editThemeState.startSavingUpdates()
state = "editing_theme"; state = "editing_theme"
} }
async function createNewLayer() { async function createNewLayer() {
state = "loading"; state = "loading"
const initialLayerConfig = { const initialLayerConfig = {
credits: createdBy, credits: createdBy,
minzoom: 15, minzoom: 15,
pointRendering: [ pointRendering: [
{ {
location: ["point", "centroid"], location: ["point", "centroid"],
marker: [{ marker: [
icon: "circle", {
color: "white" icon: "circle",
}] color: "white",
} },
],
},
], ],
tagRenderings: ["images"], tagRenderings: ["images"],
lineRendering: [{ lineRendering: [
width: 1, {
color: "blue" width: 1,
}] color: "blue",
}; },
editLayerState.configuration.setData(initialLayerConfig); ],
editLayerState.startSavingUpdates(); }
state = "editing_layer"; editLayerState.configuration.setData(initialLayerConfig)
editLayerState.startSavingUpdates()
state = "editing_layer"
} }
</script> </script>
<If condition={layersWithErr.map(d => d?.error !== undefined)}> <If condition={layersWithErr.map((d) => d?.error !== undefined)}>
<div> <div>
<div class="alert"> <div class="alert">
Something went wrong while contacting the MapComplete Studio Server: {$layersWithErr["error"]} Something went wrong while contacting the MapComplete Studio Server: {$layersWithErr["error"]}
</div> </div>
The server might be offline. Please: The server might be offline. Please:
<ul> <ul>
<li>Try again in a few minutes</li>
<li> <li>
Try again in a few minutes Contact <a href="https://app.element.io/#/room/#MapComplete:matrix.org">
</li> the MapComplete community via the chat.
<li> </a>
Contact <a href="https://app.element.io/#/room/#MapComplete:matrix.org">the MapComplete community via the Someone might be able to help you
chat.</a> Someone might be able to help you
</li> </li>
<li> <li>
File <a href="https://github.com/pietervdvn/MapComplete/issues">an issue</a> File <a href="https://github.com/pietervdvn/MapComplete/issues">an issue</a>
@ -140,44 +161,53 @@
</li> </li>
</ul> </ul>
</div> </div>
<LoginToggle ignoreLoading={true} slot="else" state={{osmConnection}}> <LoginToggle ignoreLoading={true} slot="else" state={{ osmConnection }}>
<div slot="not-logged-in"> <div slot="not-logged-in">
<NextButton clss="primary" on:click={() => osmConnection.AttemptLogin()}> <NextButton clss="primary" on:click={() => osmConnection.AttemptLogin()}>
Please log in to use MapComplete Studio Please log in to use MapComplete Studio
</NextButton> </NextButton>
</div> </div>
{#if state === undefined} {#if state === undefined}
<div class="p-4 flex flex-col justify-between h-full"> <div class="flex h-full flex-col justify-between p-4">
<div class="w-full flex flex-col"> <div class="flex w-full flex-col">
<h1>MapComplete Studio</h1> <h1>MapComplete Studio</h1>
<NextButton on:click={() => state = "edit_layer"}> <NextButton on:click={() => (state = "edit_layer")}>Edit an existing layer</NextButton>
Edit an existing layer <NextButton on:click={() => createNewLayer()}>Create a new layer</NextButton>
</NextButton> <NextButton on:click={() => (state = "edit_theme")}>Edit a theme</NextButton>
<NextButton on:click={() => createNewLayer()}> <NextButton
Create a new layer on:click={() => {
</NextButton> editThemeState.configuration.setData({})
<NextButton on:click={() => state = "edit_theme"}> state = "editing_theme"
Edit a theme }}
</NextButton> >
<NextButton on:click={() => {editThemeState.configuration.setData({}); state = "editing_theme"}}>
Create a new theme Create a new theme
</NextButton> </NextButton>
<NextButton clss="small" on:click={() => {showIntro.setData("intro")} }> <NextButton
<QuestionMarkCircleIcon class="w-6 h-6" /> clss="small"
on:click={() => {
showIntro.setData("intro")
}}
>
<QuestionMarkCircleIcon class="h-6 w-6" />
Show the introduction again Show the introduction again
</NextButton> </NextButton>
</div> </div>
<div> <div>
<Checkbox selected={expertMode}>Enable more options (expert mode)</Checkbox>
<Checkbox selected={expertMode} >Enable more options (expert mode)</Checkbox> <span class="subtle">MapComplete version {version}</span>
<span class="subtle">MapComplete version {version}</span>
</div> </div>
</div> </div>
{:else if state === "edit_layer"} {:else if state === "edit_layer"}
<div class="m-4 flex flex-col">
<div class="flex flex-col m-4"> <BackButton
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
>
MapComplete Studio
</BackButton> </BackButton>
<h2>Choose a layer to edit</h2> <h2>Choose a layer to edit</h2>
<ChooseLayerToEdit {osmConnection} layerIds={$selfLayers} on:layerSelected={editLayer}> <ChooseLayerToEdit {osmConnection} layerIds={$selfLayers} on:layerSelected={editLayer}>
@ -187,12 +217,22 @@
<ChooseLayerToEdit {osmConnection} layerIds={$otherLayers} on:layerSelected={editLayer} /> <ChooseLayerToEdit {osmConnection} layerIds={$otherLayers} on:layerSelected={editLayer} />
<h3>Official layers by MapComplete</h3> <h3>Official layers by MapComplete</h3>
<ChooseLayerToEdit {osmConnection} layerIds={$officialLayers} on:layerSelected={editLayer} /> <ChooseLayerToEdit
{osmConnection}
layerIds={$officialLayers}
on:layerSelected={editLayer}
/>
</div> </div>
{:else if state === "edit_theme"} {:else if state === "edit_theme"}
<div class="m-4 flex flex-col">
<div class="flex flex-col m-4"> <BackButton
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
>
MapComplete Studio
</BackButton> </BackButton>
<h2>Choose a theme to edit</h2> <h2>Choose a theme to edit</h2>
<ChooseLayerToEdit {osmConnection} layerIds={$selfThemes} on:layerSelected={editTheme}> <ChooseLayerToEdit {osmConnection} layerIds={$selfThemes} on:layerSelected={editTheme}>
@ -201,33 +241,57 @@
<h3>Themes by other contributors</h3> <h3>Themes by other contributors</h3>
<ChooseLayerToEdit {osmConnection} layerIds={$otherThemes} on:layerSelected={editTheme} /> <ChooseLayerToEdit {osmConnection} layerIds={$otherThemes} on:layerSelected={editTheme} />
<h3>Official themes by MapComplete</h3> <h3>Official themes by MapComplete</h3>
<ChooseLayerToEdit {osmConnection} layerIds={$officialThemes} on:layerSelected={editTheme} /> <ChooseLayerToEdit
{osmConnection}
layerIds={$officialThemes}
on:layerSelected={editTheme}
/>
</div> </div>
{:else if state === "loading"} {:else if state === "loading"}
<div class="w-8 h-8"> <div class="h-8 w-8">
<Loading /> <Loading />
</div> </div>
{:else if state === "editing_layer"} {:else if state === "editing_layer"}
<EditLayer state={editLayerState}> <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> </BackButton>
</EditLayer> </EditLayer>
{:else if state === "editing_theme"} {:else if state === "editing_theme"}
<EditTheme state={editThemeState}> <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> </BackButton>
</EditTheme> </EditTheme>
{/if} {/if}
</LoginToggle> </LoginToggle>
</If> </If>
{#if { intro, tagrenderings: intro_tagrenderings }[$showIntro]?.sections}
{#if {intro, tagrenderings: intro_tagrenderings}[$showIntro]?.sections} <FloatOver
<FloatOver on:close={() => {showIntro.setData("no")}}> on:close={() => {
<div class="flex p-4 h-full pr-12"> showIntro.setData("no")
<Walkthrough pages={{intro, tagrenderings: intro_tagrenderings}[$showIntro]?.sections} on:done={() => {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> </div>
</FloatOver> </FloatOver>
{/if} {/if}

View file

@ -21,7 +21,6 @@
<div class="information">Some important information</div> <div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div> <div class="thanks">Thank you! Operation successful</div>
<ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} /> <ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} />
<Loading>Loading...</Loading> <Loading>Loading...</Loading>
</div> </div>
@ -46,7 +45,7 @@
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} /> <ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
Main action (disabled) Main action (disabled)
</button> </button>
<button class="small"> <button class="small">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} /> <ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
Small button Small button
@ -89,7 +88,7 @@
<div class="warning">Warning</div> <div class="warning">Warning</div>
<div class="information">Some important information</div> <div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div> <div class="thanks">Thank you! Operation successful</div>
<ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} /> <ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} />
<Loading>Loading...</Loading> <Loading>Loading...</Loading>
</div> </div>
@ -130,7 +129,6 @@
<div class="information">Some important information</div> <div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div> <div class="thanks">Thank you! Operation successful</div>
<ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} /> <ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")} />
<Loading>Loading...</Loading> <Loading>Loading...</Loading>
<div> <div>
@ -147,7 +145,7 @@
JavaScript JavaScript
</label> </label>
</div> </div>
<div class="border-interactive"> <div class="border-interactive">
Area with extreme high interactivity due to `border-interactive` Area with extreme high interactivity due to `border-interactive`
</div> </div>

View file

@ -76,7 +76,12 @@
} }
const tags = state.featureProperties.getStore(selectedElement.properties.id) 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] [selectedLayer]
) )
@ -290,7 +295,9 @@
selectedElement.setData(undefined) 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> </FloatOver>
</If> </If>
@ -370,7 +377,7 @@
<IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}> <IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}>
<!-- background layer selector --> <!-- background layer selector -->
<FloatOver <FloatOver
on:close={() => { on:close={() => {
state.guistate.backgroundLayerSelectionIsOpened.setData(false) state.guistate.backgroundLayerSelectionIsOpened.setData(false)
}} }}

View file

@ -1,32 +1,33 @@
<script lang="ts"> <script lang="ts">
import nmd from "nano-markdown"
import nmd from "nano-markdown"; import { createEventDispatcher } from "svelte"
import { createEventDispatcher } from "svelte"; import WalkthroughStep from "./WalkthroughStep.svelte"
import WalkthroughStep from "./WalkthroughStep.svelte"; import FromHtml from "../Base/FromHtml.svelte"
import FromHtml from "../Base/FromHtml.svelte";
/** /**
* Markdown * 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) { function step(incr: number) {
if (incr > 0 && currentPage + 1 === pages.length) { if (incr > 0 && currentPage + 1 === pages.length) {
dispatch("done"); dispatch("done")
currentPage = 0 currentPage = 0
return return
} }
currentPage = Math.min(Math.max(0, currentPage + incr), pages.length); currentPage = Math.min(Math.max(0, currentPage + incr), pages.length)
} }
</script> </script>
<WalkthroughStep
<WalkthroughStep on:back={() => step(-1)} on:next={() => step(1)} isFirst={currentPage === 0} islast={currentPage + 1 === pages.length}> on:back={() => step(-1)}
on:next={() => step(1)}
isFirst={currentPage === 0}
islast={currentPage + 1 === pages.length}
>
<FromHtml src={nmd(pages[currentPage])} /> <FromHtml src={nmd(pages[currentPage])} />
</WalkthroughStep> </WalkthroughStep>

View file

@ -1,29 +1,24 @@
<script lang="ts"> <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"; const dispatch = createEventDispatcher<{ back; next }>()
import NextButton from "../Base/NextButton.svelte"; export let islast = false
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher<{ back, next }>();
export let islast = false;
export let isFirst = false export let isFirst = false
</script> </script>
<div class="flex h-full w-full flex-col justify-between">
<div class="flex flex-col h-full w-full justify-between">
<div class="overflow-y-auto"> <div class="overflow-y-auto">
<slot /> <slot />
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
{#if !isFirst} {#if !isFirst}
<BackButton clss="w-full" on:click={() => dispatch("back")}> <BackButton clss="w-full" on:click={() => dispatch("back")}>Back</BackButton>
Back {:else}
</BackButton> <div class="w-full" />
{:else} {/if}
<div class="w-full"/>
{/if}
<NextButton clss="primary w-full" on:click={() => dispatch("next")}> <NextButton clss="primary w-full" on:click={() => dispatch("next")}>
{#if islast} {#if islast}
Finish Finish
@ -32,5 +27,4 @@
{/if} {/if}
</NextButton> </NextButton>
</div> </div>
</div> </div>