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,
readonly: t.loginFailedReadonlyMode,
}
const apiState = state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online")
const apiState =
state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online")
</script>
{#if $badge}

View file

@ -20,6 +20,8 @@
<slot name="image" slot="image" />
<div class="flex w-full items-center justify-between" slot="message">
<slot />
<ChevronRightIcon class={clss?.indexOf("small") >= 0? "h-4 w-4 shrink-0": "h-12 w-12 shrink-0" }/>
<ChevronRightIcon
class={clss?.indexOf("small") >= 0 ? "h-4 w-4 shrink-0" : "h-12 w-12 shrink-0"}
/>
</div>
</SubtleButton>

View file

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

View file

@ -28,7 +28,7 @@
<Tr t={Translations.t.general.returnToTheMap} />
</button>
{:else}
<div class="flex flex-col gap-y-2 overflow-y-auto p-1 px-2 h-full">
<div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-2">
{#each layer.tagRenderings as config (config.id)}
{#if (config.condition?.matchesProperties($tags) ?? true) && config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true)}
{#if config.IsKnown($tags)}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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`,
*/
export default class TagValidator extends Validator {
public readonly isMeta = true
constructor() {
super("tag", "A simple tag of the format `key=value` OR a tagExpression")

View file

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

View file

@ -3,8 +3,9 @@ import { Validator } from "../Validator"
export default class UrlValidator extends Validator {
constructor(name?: string, explanation?: string) {
super(
name ??"url",
explanation?? "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed",
name ?? "url",
explanation ??
"The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed",
"url"
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -95,7 +95,11 @@
}
</script>
<div bind:this={questionboxElem} class="marker-questionbox-root" class:hidden={_questionsToAsk.length === 0 && skipped === 0 && answered === 0}>
<div
bind:this={questionboxElem}
class="marker-questionbox-root"
class:hidden={_questionsToAsk.length === 0 && skipped === 0 && answered === 0}
>
{#if _questionsToAsk.length === 0}
{#if skipped + answered > 0}
<div class="thanks">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,35 +1,36 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
import Marker from "../Map/Marker.svelte";
import NextButton from "../Base/NextButton.svelte";
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts";
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
import { createEventDispatcher } from "svelte";
import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Marker from "../Map/Marker.svelte"
import NextButton from "../Base/NextButton.svelte"
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
import { createEventDispatcher } from "svelte"
export let info: { id: string, owner: number };
export let category: "layers" | "themes";
export let osmConnection: OsmConnection;
export let info: { id: string; owner: number }
export let category: "layers" | "themes"
export let osmConnection: OsmConnection
let displayName = UIEventSource.FromPromise(osmConnection.getInformationAboutUser(info.owner)).mapD(response => response.display_name);
let displayName = UIEventSource.FromPromise(
osmConnection.getInformationAboutUser(info.owner)
).mapD((response) => response.display_name)
let selfId = osmConnection.userDetails.mapD(ud => ud.uid)
let selfId = osmConnection.userDetails.mapD((ud) => ud.uid)
function fetchIconDescription(layerId): any {
if (category === "themes") {
return AllKnownLayouts.allKnownLayouts.get(layerId).icon;
return AllKnownLayouts.allKnownLayouts.get(layerId).icon
}
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon
}
const dispatch = createEventDispatcher<{ layerSelected: string }>();
const dispatch = createEventDispatcher<{ layerSelected: string }>()
</script>
<NextButton clss="small" on:click={() => dispatch("layerSelected", info)}>
<div class="w-4 h-4 mr-1">
<div class="mr-1 h-4 w-4">
<Marker icons={fetchIconDescription(info.id)} />
</div>
<b class="px-1"> {info.id}</b>
<b class="px-1">{info.id}</b>
{#if info.owner && info.owner !== $selfId}
(made by {$displayName ?? info.owner})
{/if}

View file

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

View file

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

View file

@ -1,19 +1,21 @@
<script lang="ts">
import EditLayerState from "./EditLayerState";
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
export let firstPaths: Set<string>;
export let state: EditLayerState;
let messagesCount = state.messages.map(msgs => msgs.filter(msg => {
const pth = msg.context.path
return firstPaths.has(pth[0]) || (pth.length > 1 && firstPaths.has(pth[1]));
}).length);
import EditLayerState from "./EditLayerState"
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"
export let firstPaths: Set<string>
export let state: EditLayerState
let messagesCount = state.messages.map(
(msgs) =>
msgs.filter((msg) => {
const pth = msg.context.path
return firstPaths.has(pth[0]) || (pth.length > 1 && firstPaths.has(pth[1]))
}).length
)
</script>
{#if $messagesCount > 0}
<span class="alert flex w-min">
<ExclamationIcon class="w-6 h-6" />
<ExclamationIcon class="h-6 w-6" />
{$messagesCount}
</span>
{/if}

View file

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

View file

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

View file

@ -1,30 +1,33 @@
<script lang="ts">/***
* A 'region' is a collection of properties that can be edited which are somewhat related.
* They will typically be a subset of some properties
*/
import type { ConfigMeta } from "./configMeta";
import EditLayerState from "./EditLayerState";
import SchemaBasedInput from "./SchemaBasedInput.svelte";
<script lang="ts">
/***
* A 'region' is a collection of properties that can be edited which are somewhat related.
* They will typically be a subset of some properties
*/
import type { ConfigMeta } from "./configMeta"
import EditLayerState from "./EditLayerState"
import SchemaBasedInput from "./SchemaBasedInput.svelte"
export let state: EditLayerState;
export let configs: ConfigMeta[];
export let title: string | undefined = undefined;
export let state: EditLayerState
export let configs: ConfigMeta[]
export let title: string | undefined = undefined
export let path: (string | number)[] = [];
let expertMode = state.expertMode
let configsNoHidden = configs.filter(schema => schema.hints?.group !== "hidden")
let configsFiltered = $expertMode ? configsNoHidden : configsNoHidden.filter(schema => schema.hints?.group !== "expert")
export let path: (string | number)[] = []
let expertMode = state.expertMode
let configsNoHidden = configs.filter((schema) => schema.hints?.group !== "hidden")
let configsFiltered = $expertMode
? configsNoHidden
: configsNoHidden.filter((schema) => schema.hints?.group !== "expert")
</script>
{#if configs === undefined}
Bug: 'Region' received 'undefined'
{:else if configs.length === 0}
Bug: Region received empty list as configuration
{:else if title}
<div class="w-full flex flex-col">
<div class="flex w-full flex-col">
<h3>{title}</h3>
<div class="pl-2 border border-black flex flex-col gap-y-1 w-full">
<div class="flex w-full flex-col gap-y-1 border border-black pl-2">
<slot name="description" />
{#each configsFiltered as config}
<SchemaBasedInput {state} path={config.path} schema={config} />
@ -32,10 +35,9 @@ let configsFiltered = $expertMode ? configsNoHidden : configsNoHidden.filter(sch
</div>
</div>
{:else}
<div class="pl-2 flex flex-col gap-y-1 w-full">
<div class="flex w-full flex-col gap-y-1 pl-2">
{#each configsFiltered as config}
<SchemaBasedInput {state} path={path.concat(config.path)} schema={config} />
{/each}
</div>
{/if}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,12 @@
<script lang="ts">
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion";
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"
export let messages: ConversionMessage[];
export let messages: ConversionMessage[]
</script>
{#if messages.length === 0}
<div class="thanks">
No errors, warnings or messages
</div>
{/if}
<div class="thanks">No errors, warnings or messages</div>
{/if}
{#each messages as message}
<li>
@ -16,7 +14,7 @@
<span class="literal-code">{message.context.path.join(".")}</span>
{message.message}
<span class="literal-code">
{message.context.operation.join(".")}
</span>
{message.context.operation.join(".")}
</span>
</li>
{/each}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,173 +1,227 @@
<script lang="ts">/**
* Little helper class to deal with choosing a builtin tagRendering or defining one yourself.
* Breaks the ideology that everything should be schema based
*/
import EditLayerState from "./EditLayerState";
import type { ConfigMeta } from "./configMeta";
import type {
MappingConfigJson,
QuestionableTagRenderingConfigJson
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import * as questions from "../../assets/generated/layers/questions.json";
import MappingInput from "./MappingInput.svelte";
import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline";
import questionableTagRenderingSchemaRaw from "../../assets/schemas/questionabletagrenderingconfigmeta.json";
import SchemaBasedField from "./SchemaBasedField.svelte";
import Region from "./Region.svelte";
import NextButton from "../Base/NextButton.svelte";
import { QuestionMarkCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource";
import { onMount } from "svelte";
<script lang="ts">
/**
* Little helper class to deal with choosing a builtin tagRendering or defining one yourself.
* Breaks the ideology that everything should be schema based
*/
import EditLayerState from "./EditLayerState"
import type { ConfigMeta } from "./configMeta"
import type {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import * as questions from "../../assets/generated/layers/questions.json"
import MappingInput from "./MappingInput.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline"
import questionableTagRenderingSchemaRaw from "../../assets/schemas/questionabletagrenderingconfigmeta.json"
import SchemaBasedField from "./SchemaBasedField.svelte"
import Region from "./Region.svelte"
import NextButton from "../Base/NextButton.svelte"
import { QuestionMarkCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import { onMount } from "svelte"
export let state: EditLayerState;
export let schema: ConfigMeta;
export let path: (string | number)[];
let expertMode = state.expertMode;
const store = state.getStoreFor(path);
let value = store.data;
let hasSeenIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false"))
onMount(() => {
if(!hasSeenIntro.data){
state.showIntro.setData("tagrenderings")
hasSeenIntro.setData(true)
}
})
/**
* Allows the theme builder to create 'writable' themes.
* Should only be enabled for 'tagrenderings' in the theme, if the source is OSM
*/
let allowQuestions: Store<boolean> = (state.configuration.mapD(config => path.at(0) === "tagRenderings" && config.source?.geoJson === undefined));
export let state: EditLayerState
export let schema: ConfigMeta
export let path: (string | number)[]
let expertMode = state.expertMode
const store = state.getStoreFor(path)
let value = store.data
let hasSeenIntro = UIEventSource.asBoolean(
LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false")
)
onMount(() => {
if (!hasSeenIntro.data) {
state.showIntro.setData("tagrenderings")
hasSeenIntro.setData(true)
}
})
/**
* Allows the theme builder to create 'writable' themes.
* Should only be enabled for 'tagrenderings' in the theme, if the source is OSM
*/
let allowQuestions: Store<boolean> = state.configuration.mapD(
(config) => path.at(0) === "tagRenderings" && config.source?.geoJson === undefined
)
let mappingsBuiltin: MappingConfigJson[] = []
let perLabel: Record<string, MappingConfigJson> = {}
for (const tr of questions.tagRenderings) {
let description = tr["description"] ?? tr["question"] ?? "No description available"
description = description["en"] ?? description
if (tr["labels"]) {
const labels: string[] = tr["labels"]
for (const label of labels) {
let labelMapping: MappingConfigJson = perLabel[label]
let mappingsBuiltin: MappingConfigJson[] = [];
let perLabel: Record<string, MappingConfigJson> = {};
for (const tr of questions.tagRenderings) {
let description = tr["description"] ?? tr["question"] ?? "No description available";
description = description["en"] ?? description;
if (tr["labels"]) {
const labels: string[] = tr["labels"];
for (const label of labels) {
let labelMapping: MappingConfigJson = perLabel[label];
if (!labelMapping) {
labelMapping = {
if: "value=" + label,
then: {
en: "Builtin collection <b>" + label + "</b>:"
if (!labelMapping) {
labelMapping = {
if: "value=" + label,
then: {
en: "Builtin collection <b>" + label + "</b>:",
},
}
};
perLabel[label] = labelMapping;
mappingsBuiltin.push(labelMapping);
perLabel[label] = labelMapping
mappingsBuiltin.push(labelMapping)
}
labelMapping.then.en = labelMapping.then.en + "<div>" + description + "</div>"
}
labelMapping.then.en = labelMapping.then.en + "<div>" + description + "</div>";
}
mappingsBuiltin.push({
if: "value=" + tr["id"],
then: {
en: "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>",
},
})
}
const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson>{
question: "Which builtin element should be shown?",
mappings: mappingsBuiltin,
})
const tags = new UIEventSource({ value })
tags.addCallbackAndRunD((tgs) => {
store.setData(tgs["value"])
})
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"])
const topLevelItems: Record<string, ConfigMeta> = {}
for (const item of questionableTagRenderingSchemaRaw) {
if (item.path.length === 1) {
topLevelItems[item.path[0]] = <ConfigMeta>item
}
}
mappingsBuiltin.push({
if: "value=" + tr["id"],
then: {
"en": "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>"
function initMappings() {
if (mappings.data === undefined) {
mappings.setData([])
}
});
}
const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson>{
question: "Which builtin element should be shown?",
mappings: mappingsBuiltin
});
const tags = new UIEventSource({ value });
tags.addCallbackAndRunD(tgs => {
store.setData(tgs["value"]);
});
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]);
const topLevelItems: Record<string, ConfigMeta> = {};
for (const item of questionableTagRenderingSchemaRaw) {
if (item.path.length === 1) {
topLevelItems[item.path[0]] = <ConfigMeta>item;
}
}
function initMappings() {
if (mappings.data === undefined) {
mappings.setData([]);
}
}
const items = new Set(["question", "questionHint", "multiAnswer", "freeform", "render", "condition", "metacondition", "mappings", "icon"]);
const ignored = new Set(["labels", "description", "classes"]);
const freeformSchemaAll = <ConfigMeta[]>questionableTagRenderingSchemaRaw
.filter(schema => schema.path.length == 2 && schema.path[0] === "freeform" && ($allowQuestions || schema.path[1] === "key"));
let freeformSchema = $expertMode ? freeformSchemaAll : freeformSchemaAll.filter(schema => schema.hints?.group !== "expert");
const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0])).map(schema => schema.path.join("."));
console.log({ state });
const items = new Set([
"question",
"questionHint",
"multiAnswer",
"freeform",
"render",
"condition",
"metacondition",
"mappings",
"icon",
])
const ignored = new Set(["labels", "description", "classes"])
const freeformSchemaAll = <ConfigMeta[]>(
questionableTagRenderingSchemaRaw.filter(
(schema) =>
schema.path.length == 2 &&
schema.path[0] === "freeform" &&
($allowQuestions || schema.path[1] === "key")
)
)
let freeformSchema = $expertMode
? freeformSchemaAll
: freeformSchemaAll.filter((schema) => schema.hints?.group !== "expert")
const missing: string[] = questionableTagRenderingSchemaRaw
.filter(
(schema) =>
schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0])
)
.map((schema) => schema.path.join("."))
console.log({ state })
</script>
{#if typeof $store === "string"}
<div class="flex low-interaction">
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
{tags} />
<div class="low-interaction flex">
<TagRenderingEditable
config={configBuiltin}
selectedElement={undefined}
showQuestionIfUnknown={true}
{state}
{tags}
/>
<slot name="upper-right" />
</div>
{:else}
<div class="flex flex-col w-full p-1 gap-y-1 pr-12">
<div class="flex w-full flex-col gap-y-1 p-1 pr-12">
<div class="flex justify-end">
<slot name="upper-right" />
</div>
{#if $allowQuestions}
<SchemaBasedField startInEditModeIfUnset={true} {state} path={[...path,"question"]}
schema={topLevelItems["question"]} />
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
<SchemaBasedField
startInEditModeIfUnset={true}
{state}
path={[...path, "question"]}
schema={topLevelItems["question"]}
/>
<SchemaBasedField
{state}
path={[...path, "questionHint"]}
schema={topLevelItems["questionHint"]}
/>
{/if}
{#each ($mappings ?? []) as mapping, i (mapping)}
<div class="flex interactive w-full">
{#each $mappings ?? [] as mapping, i (mapping)}
<div class="interactive flex w-full">
<MappingInput {state} path={path.concat(["mappings", i])}>
<button slot="delete" class="rounded-full no-image-background" on:click={() => {
initMappings();
mappings.data.splice(i, 1)
mappings.ping()
}}>
<TrashIcon class="w-4 h-4" />
<button
slot="delete"
class="no-image-background rounded-full"
on:click={() => {
initMappings()
mappings.data.splice(i, 1)
mappings.ping()
}}
>
<TrashIcon class="h-4 w-4" />
</button>
</MappingInput>
</div>
{/each}
<button class="primary"
on:click={() =>{ initMappings(); mappings.data.push({if: undefined, then: {}}); mappings.ping()} }>
<button
class="primary"
on:click={() => {
initMappings()
mappings.data.push({ if: undefined, then: {} })
mappings.ping()
}}
>
Add a predefined option
</button>
<SchemaBasedField {state} path={[...path,"multiAnswer"]} schema={topLevelItems["multiAnswer"]} />
<SchemaBasedField
{state}
path={[...path, "multiAnswer"]}
schema={topLevelItems["multiAnswer"]}
/>
<h3>Text field and input element configuration</h3>
<div class="border-l pl-2 border-gray-800 border-dashed">
<SchemaBasedField {state} path={[...path,"render"]} schema={topLevelItems["render"]} />
<div class="border-l border-dashed border-gray-800 pl-2">
<SchemaBasedField {state} path={[...path, "render"]} schema={topLevelItems["render"]} />
<Region {state} {path} configs={freeformSchema} />
<SchemaBasedField {state} path={[...path,"icon"]} schema={topLevelItems["icon"]} />
<SchemaBasedField {state} path={[...path, "icon"]} schema={topLevelItems["icon"]} />
</div>
<SchemaBasedField {state} path={[...path,"condition"]} schema={topLevelItems["condition"]} />
<SchemaBasedField {state} path={[...path, "condition"]} schema={topLevelItems["condition"]} />
{#if $expertMode}
<SchemaBasedField {state} path={[...path,"metacondition"]} schema={topLevelItems["metacondition"]} />
<SchemaBasedField
{state}
path={[...path, "metacondition"]}
schema={topLevelItems["metacondition"]}
/>
{/if}
{#each missing as field}
<SchemaBasedField {state} path={[...path,field]} schema={topLevelItems[field]} />
<SchemaBasedField {state} path={[...path, field]} schema={topLevelItems[field]} />
{/each}
<NextButton clss="small mt-8" on:click={() => state.showIntro.setData("tagrenderings")}><QuestionMarkCircleIcon class="h-6 w-6"/> Show the introduction again</NextButton>
<NextButton clss="small mt-8" on:click={() => state.showIntro.setData("tagrenderings")}>
<QuestionMarkCircleIcon class="h-6 w-6" /> Show the introduction again
</NextButton>
</div>
{/if}

View file

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

View file

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

View file

@ -76,7 +76,12 @@
}
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags }).SetClass("h-full w-full")
return new SvelteUIElement(SelectedElementView, {
state,
layer,
selectedElement,
tags,
}).SetClass("h-full w-full")
},
[selectedLayer]
)
@ -290,7 +295,9 @@
selectedElement.setData(undefined)
}}
>
<ToSvelte construct={new VariableUiElement(selectedElementView).SetClass("h-full w-full flex")} />
<ToSvelte
construct={new VariableUiElement(selectedElementView).SetClass("h-full w-full flex")}
/>
</FloatOver>
</If>
@ -370,7 +377,7 @@
<IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}>
<!-- background layer selector -->
<FloatOver
<FloatOver
on:close={() => {
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
}}

View file

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

View file

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