Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-02-15 17:48:26 +01:00
commit f0823f4c4d
524 changed files with 18747 additions and 8546 deletions

View file

@ -28,12 +28,14 @@
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 Confirm from "../../../assets/svg/Confirm.svelte"
import Close from "../../../assets/svg/Close.svelte"
import Layers from "../../../assets/svg/Layers.svelte"
import { Translation } from "../../i18n/Translation"
import ToSvelte from "../../Base/ToSvelte.svelte"
import BaseUIElement from "../../BaseUIElement"
export let coordinate: { lon: number; lat: number }
export let state: SpecialVisualizationState
@ -41,8 +43,9 @@
let selectedPreset: {
preset: PresetConfig
layer: LayerConfig
icon: string
tags: Record<string, string>
icon: BaseUIElement
tags: Record<string, string>,
text: Translation
} = undefined
let checkedOfGlobalFilters: number = 0
let confirmedCategory = false
@ -142,7 +145,6 @@
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()
}
@ -198,7 +200,7 @@
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
<Layers class="w-12"/>
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
@ -239,7 +241,7 @@
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
<Layers class="w-12"/>
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
</div>
@ -251,8 +253,6 @@
/>
</h2>
<Tr t={Translations.t.general.add.confirmIntro} />
{#if selectedPreset.preset.description}
<Tr t={selectedPreset.preset.description} />
{/if}
@ -284,7 +284,7 @@
<NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full">
<div slot="image" class="relative">
<ToSvelte construct={selectedPreset.icon} />
<ToSvelte construct={selectedPreset.icon}/>
<Confirm class="absolute bottom-0 right-0 h-4 w-4" />
</div>
<div class="w-full">
@ -303,7 +303,7 @@
<Tr
slot="message"
t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({
preset: selectedPreset.preset,
preset: selectedPreset.text
})}
/>
</SubtleButton>

View file

@ -9,11 +9,10 @@
import { ImmutableStore } from "../../../Logic/UIEventSource"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
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 Combine from "../../Base/Combine"
/**
* This component lists all the presets and allows the user to select one
@ -24,6 +23,10 @@
preset: PresetConfig
layer: LayerConfig
text: Translation
/**
* Same as `this.preset.description.firstSentence()`
*/
description: Translation,
icon: BaseUIElement
tags: Record<string, string>
}[] = []
@ -37,7 +40,7 @@
"Not showing presets for layer",
flayer.layerDef.id,
"as not displayed and featureSwitchFilter.data is set",
state.featureSwitches.featureSwitchFilter.data
state.featureSwitches.featureSwitchFilter.data,
)
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
continue
@ -52,9 +55,9 @@
for (const preset of layer.presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
const icon: BaseUIElement = layer.mapRendering[0]
.RenderIcon(new ImmutableStore<any>(tags))
.html.SetClass("w-12 h-12 block relative mr-4")
const markers = layer.mapRendering.map((mr, i) => mr.RenderIcon(new ImmutableStore<any>(tags), {noSize: i == 0})
.html.SetClass(i == 0 ? "w-full h-full" : ""))
const icon: BaseUIElement = new Combine(markers.map(m => new Combine([m]).SetClass("absolute top-0 left-0 w-full h-full flex justify-around items-center"))).SetClass("w-12 h-12 block relative mr-4")
const description = preset.description?.FirstSentence()
@ -66,7 +69,7 @@
tags,
text: Translations.t.general.add.addNew.Subs(
{ category: preset.title },
preset.title["context"]
preset.title["context"],
),
}
presets.push(simplified)
@ -74,7 +77,13 @@
}
const dispatch = createEventDispatcher<{
select: { preset: PresetConfig; layer: LayerConfig; icon: string; tags: Record<string, string> }
select: {
preset: PresetConfig;
layer: LayerConfig;
icon: BaseUIElement;
tags: Record<string, string>,
text: Translation
}
}>()
</script>

View file

@ -1,46 +0,0 @@
import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
import { Store } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement"
import all_languages from "../../assets/language_translations.json"
import { Translation } from "../i18n/Translation"
export class AllLanguagesSelector extends SearchablePillsSelector<string> {
constructor(options?: {
mode?: "select-many" | "select-one"
currentCountry?: Store<string>
supportedLanguages?: Record<string, string> & { _meta?: { countries?: string[] } }
}) {
const possibleValues: {
show: BaseUIElement
value: string
mainTerm: Record<string, string>
searchTerms?: Record<string, string[]>
hasPriority?: Store<boolean>
}[] = []
const langs = options?.supportedLanguages ?? all_languages
for (const ln in langs) {
let languageInfo: Record<string, string> & { _meta?: { countries: string[] } } =
all_languages[ln]
const countries = languageInfo._meta?.countries?.map((c) => c.toLowerCase())
languageInfo = { ...languageInfo }
delete languageInfo._meta
const term = {
show: new Translation(languageInfo),
value: ln,
mainTerm: languageInfo,
searchTerms: { "*": [ln] },
hasPriority:
countries === undefined
? undefined
: options?.currentCountry?.map(
(country) => countries?.indexOf(country.toLowerCase()) >= 0
),
}
possibleValues.push(term)
}
super(possibleValues, {
mode: options?.mode ?? "select-many",
})
}
}

View file

@ -1,67 +1,100 @@
<script lang="ts">
import ToSvelte from "../Base/ToSvelte.svelte"
import Table from "../Base/Table"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import SimpleMetaTaggers from "../../Logic/SimpleMetaTagger"
import { FixedUiElement } from "../Base/FixedUiElement"
import { onDestroy } from "svelte"
import Toggle from "../Input/Toggle"
import Lazy from "../Base/Lazy"
import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { VariableUiElement } from "../Base/VariableUIElement"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
//Svelte props
export let tags: UIEventSource<any>
export let state: { layoutToUse: LayoutConfig } = undefined
export let tags: UIEventSource<Record<string, any>>
export let tagKeys = tags.map(tgs => Object.keys(tgs))
const calculatedTags = [].concat(
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
)
export let layer: LayerConfig | undefined = undefined
const allTags = tags.mapD((tags) => {
const parts: (string | BaseUIElement)[][] = []
for (const key in tags) {
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"])
/**
* The names (keys) of the calculated tags. Each will normally start with an underscore (but in rare cases not)
*/
let calculatedTags: string[] = []
for (const calculated of layer?.calculatedTags ?? []) {
if(calculated){
continue
}
const name = calculated[0]
calculatedTags.push(name)
}
let knownValues: Store<string[]> = tags.map(tags => Object.keys(tags))
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
let type = ""
if (typeof value !== "string") {
type = " <i>" + typeof value + "</i>"
}
parts.push(["<i>" + key + "</i>", value])
}
const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map(k => k.keys))
let allCalculatedTags = new Set<string>([...calculatedTags, ...metaKeys])
for (const metatag of SimpleMetaTaggers.metatags.filter((mt) => mt.isLazy)) {
const title = "<i>" + metatag.keys.join(";") + "</i> (lazy)"
const toggleState = new UIEventSource(false)
const toggle: BaseUIElement = new Toggle(
new Lazy(() => new FixedUiElement(metatag.keys.map((key) => tags[key]).join(";"))),
new FixedUiElement("Evaluate").onClick(() => toggleState.setData(true)),
toggleState
)
parts.push([title, toggle])
}
return parts
})
const tagsTable = new VariableUiElement(
allTags.mapD((_allTags) =>
new Table(["Key", "Value"], _allTags).SetClass("zebra-table break-all")
)
)
</script>
<section>
<ToSvelte construct={tagsTable} />
<table class="zebra-table break-all">
<tr>
<th>Key</th>
<th>Value</th>
</tr>
<tr>
<th colspan="2">Normal tags</th>
</tr>
{#each $tagKeys as key}
{#if !allCalculatedTags.has(key)}
<tr>
<td>{key}</td>
<td>
{#if $tags[key] === undefined}
<i>undefined</i>
{:else if $tags[key] === ""}
<i>Empty string</i>
{:else}
{$tags[key]}
{/if}
</td>
</tr>
{/if}
{/each}
<tr>
<th colspan="2">Calculated tags</th>
</tr>
{#if calculatedTags.length === 0}
<tr>
<td colspan="2"><i>This layer does not use calculated tags</i></td>
</tr>
{/if}
{#each calculatedTags as key}
<tr>
<td>{key}</td>
<td>
{#if $tags[key] === undefined}
<i>undefined</i>
{:else if $tags[key] === ""}
<i>Empty string</i>
{:else if $tags[key] !== "string"}
<span class="literal-code">{$tags[key]}</span>
<i>{typeof $tags[key]}</i>
{:else}
{$tags[key]}
{/if}
</td>
</tr>
{/each}
<tr>
<th colspan="2">Metatags tags</th>
</tr>
{#each metaKeys as key}
<tr>
<td>{key}</td>
<td>
{#if $knownValues.indexOf(key) < 0 }
<button class="small" on:click={_ => {console.log($tags[key])}}>Evaluate</button>
{:else if !$tags[key] === undefined}
<i>Undefined</i>
{:else if $tags[key] === ""}
<i>Empty string</i>
{:else}
{$tags[key]}
{/if}
</td>
</tr>
{/each}
</table>
</section>

View file

@ -10,9 +10,8 @@
import type { Feature } from "geojson"
import { UIEventSource } from "../../../Logic/UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction"
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
@ -43,7 +42,7 @@
const t = Translations.t.delete
let selectedTags: TagsFilter
let selectedTags: UploadableTag
let changedProperties = undefined
$: changedProperties = TagUtils.changeAsProperties(selectedTags?.asChange(tags?.data ?? {}) ?? [])
let isHardDelete = undefined
@ -66,7 +65,7 @@
theme: state?.layout?.id ?? "unknown",
specialMotivation: deleteReason,
},
canBeDeleted.data
canBeDeleted.data,
)
} else {
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping

View file

@ -2,7 +2,6 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import Histogram from "../BigComponents/Histogram"
import { Feature } from "geojson"
import Constants from "../../Models/Constants"
export class HistogramViz implements SpecialVisualization {
funcName = "histogram"

View file

@ -86,8 +86,7 @@ export default class ConflateImportButtonViz implements SpecialVisualization, Au
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
feature: Feature
): BaseUIElement {
const canBeImported =
feature.geometry.type === "LineString" ||

View file

@ -1,42 +1,58 @@
<script lang="ts">
/**
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
* They show some default components
*/
import ImportFlow from "./ImportFlow"
import LoginToggle from "../../Base/LoginToggle.svelte"
import BackButton from "../../Base/BackButton.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import NextButton from "../../Base/NextButton.svelte"
import { createEventDispatcher } from "svelte"
import Loading from "../../Base/Loading.svelte"
import { And } from "../../../Logic/Tags/And"
import TagHint from "../TagHint.svelte"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Store } from "../../../Logic/UIEventSource"
import Svg from "../../../Svg"
import ToSvelte from "../../Base/ToSvelte.svelte"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import Confirm from "../../../assets/svg/Confirm.svelte"
import type { ImportFlowArguments } from "./ImportFlow"
/**
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
* They show some default components
*/
import ImportFlow from "./ImportFlow"
import LoginToggle from "../../Base/LoginToggle.svelte"
import BackButton from "../../Base/BackButton.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import NextButton from "../../Base/NextButton.svelte"
import { createEventDispatcher, onDestroy } from "svelte"
import Loading from "../../Base/Loading.svelte"
import { And } from "../../../Logic/Tags/And"
import TagHint from "../TagHint.svelte"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Store } from "../../../Logic/UIEventSource"
import Svg from "../../../Svg"
import ToSvelte from "../../Base/ToSvelte.svelte"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import FilteredLayer from "../../../Models/FilteredLayer"
import Confirm from "../../../assets/svg/Confirm.svelte"
export let importFlow: ImportFlow
let state = importFlow.state
export let importFlow: ImportFlow<ImportFlowArguments>
let state = importFlow.state
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
const isLoading = state.dataIsLoading
const dispatch = createEventDispatcher<{ confirm }>()
const canBeImported = importFlow.canBeImported()
const tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
const isLoading = state.dataIsLoading
let dispatch = createEventDispatcher<{ confirm }>()
let canBeImported = importFlow.canBeImported()
let tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
const isDisplayed = importFlow.targetLayer.isDisplayed
const hasFilter = importFlow.targetLayer.hasFilter
function abort() {
state.selectedElement.setData(undefined)
state.selectedLayer.setData(undefined)
}
let targetLayers = importFlow.targetLayer
let filteredLayer: FilteredLayer
let undisplayedLayer: FilteredLayer
function updateIsDisplayed() {
filteredLayer = targetLayers.find(tl => tl.hasFilter.data)
undisplayedLayer = targetLayers.find(tl => !tl.isDisplayed.data)
}
updateIsDisplayed()
for (const tl of targetLayers) {
onDestroy(
tl.isDisplayed.addCallback(updateIsDisplayed),
)
}
function abort() {
state.selectedElement.setData(undefined)
}
</script>
<LoginToggle {state}>
@ -45,13 +61,13 @@
{#if $canBeImported.extraHelp}
<Tr t={$canBeImported.extraHelp} />
{/if}
{:else if !$isDisplayed}
{:else if undisplayedLayer !== undefined}
<!-- Check that the layer is enabled, so that we don't add a duplicate -->
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" />
<Tr
t={Translations.t.general.add.layerNotEnabled.Subs({
layer: importFlow.targetLayer.layerDef.name,
layer: undisplayedLayer.layerDef.name,
})}
/>
</div>
@ -61,7 +77,7 @@
class="flex w-full gap-x-1"
on:click={() => {
abort()
state.guistate.openFilterView(importFlow.targetLayer.layerDef)
state.guistate.openFilterView(filteredLayer.layerDef)
}}
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
@ -71,19 +87,19 @@
<button
class="primary flex w-full gap-x-1"
on:click={() => {
isDisplayed.setData(true)
undisplayedLayer.isDisplayed.setData(true)
abort()
}}
>
<EyeIcon class="w-12" />
<Tr
t={Translations.t.general.add.enableLayer.Subs({
name: importFlow.targetLayer.layerDef.name,
name: undisplayedLayer.layerDef.name,
})}
/>
</button>
</div>
{:else if $hasFilter}
{:else if filteredLayer !== undefined}
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" />
@ -94,7 +110,7 @@
class="primary flex w-full gap-x-1"
on:click={() => {
abort()
importFlow.targetLayer.disableAllFilters()
filteredLayer.disableAllFilters()
}}
>
<EyeOffIcon class="w-12" />
@ -104,7 +120,7 @@
class="flex w-full gap-x-1"
on:click={() => {
abort()
state.guistate.openFilterView(importFlow.targetLayer.layerDef)
state.guistate.openFilterView(filteredLayer.layerDef)
}}
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />

View file

@ -6,7 +6,6 @@ import TagApplyButton from "../TagApplyButton"
import { PointImportFlowArguments } from "./PointImportFlowState"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
import FilteredLayer from "../../../Models/FilteredLayer"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson"
@ -25,7 +24,7 @@ export class ImportFlowUtils {
public static readonly conflationLayer = new LayerConfig(
<LayerConfigJson>conflation_json,
"all_known_layers",
true
true,
)
public static readonly documentationGeneral = `\n\n\nNote that the contributor must zoom to at least zoomlevel 18 to be able to use this functionality.
@ -67,7 +66,7 @@ ${Utils.special_visualizations_importRequirementDocs}
*/
public static getTagsToApply(
originalFeatureTags: UIEventSource<any>,
args: { tags: string }
args: { tags: string },
): Store<Tag[]> {
if (originalFeatureTags === undefined) {
return undefined
@ -83,9 +82,9 @@ ${Utils.special_visualizations_importRequirementDocs}
const items: string = originalFeatureTags.data[tags]
console.debug(
"The import button is using tags from properties[" +
tags +
"] of this object, namely ",
items
tags +
"] of this object, namely ",
items,
)
if (items.startsWith("{")) {
@ -108,13 +107,12 @@ ${Utils.special_visualizations_importRequirementDocs}
* - targetLayer
*
* Others (e.g.: snapOnto-layers) are not to be handled here
* @param argsRaw
*/
public static getLayerDependencies(argsRaw: string[], argSpec?) {
public static getLayerDependencies(argsRaw: string[], argSpec?): string[] {
const args: ImportFlowArguments = <any>(
Utils.ParseVisArgs(argSpec ?? ImportFlowUtils.generalArguments, argsRaw)
)
return [args.targetLayer]
return args.targetLayer.split(" ")
}
public static getLayerDependenciesWithSnapOnto(
@ -122,7 +120,7 @@ ${Utils.special_visualizations_importRequirementDocs}
name: string
defaultValue?: string
}[],
argsRaw: string[]
argsRaw: string[],
): string[] {
const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec)
const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw)
@ -130,30 +128,6 @@ ${Utils.special_visualizations_importRequirementDocs}
deps.push(...snapOntoLayers)
return deps
}
public static buildTagSpec(
args: ImportFlowArguments,
tagSource: Store<Record<string, string>>
): Store<string> {
let tagSpec = args.tags
return tagSource.mapD((tags) => {
if (
tagSpec.indexOf(" ") < 0 &&
tagSpec.indexOf(";") < 0 &&
tags[args.tags] !== undefined
) {
// This is probably a key
tagSpec = tags[args.tags]
console.debug(
"The import button is using tags from properties[" +
args.tags +
"] of this object, namely ",
tagSpec
)
}
return tagSpec
})
}
}
/**
@ -164,7 +138,7 @@ ${Utils.special_visualizations_importRequirementDocs}
export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
public readonly state: SpecialVisualizationState
public readonly args: ArgT
public readonly targetLayer: FilteredLayer
public readonly targetLayer: FilteredLayer[]
public readonly tagsToApply: Store<Tag[]>
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>
@ -172,13 +146,19 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
state: SpecialVisualizationState,
args: ArgT,
tagsToApply: Store<Tag[]>,
originalTags: UIEventSource<Record<string, string>>
originalTags: UIEventSource<Record<string, string>>,
) {
this.state = state
this.args = args
this.tagsToApply = tagsToApply
this._originalFeatureTags = originalTags
this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer)
this.targetLayer = args.targetLayer.split(" ").map(tl => {
let found = state.layerState.filteredLayers.get(tl)
if (!found) {
throw "Layer " + tl + " not found"
}
return found
})
}
/**
@ -218,7 +198,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
return undefined
},
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags]
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags],
)
}
}

View file

@ -17,7 +17,7 @@ export class PointImportButtonViz implements SpecialVisualization {
public readonly funcName: string
public readonly docs: string | BaseUIElement
public readonly example?: string
public readonly args: { name: string; defaultValue?: string; doc: string }[]
public readonly args: { name: string; defaultValue?: string; doc: string, split?: boolean }[]
public needsUrls = []
constructor() {
@ -51,8 +51,7 @@ export class PointImportButtonViz implements SpecialVisualization {
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
feature: Feature
): BaseUIElement {
if (feature.geometry.type !== "Point") {
return Translations.t.general.add.import.wrongType.SetClass("alert")

View file

@ -13,7 +13,7 @@
const args = importFlow.args
// The following variables are used for the map
const targetLayer: LayerConfig = state.layout.layers.find((l) => l.id === args.targetLayer)
const targetLayers: LayerConfig[] = args.targetLayer.split(" ").map(tl => state.layout.layers.find((l) => l.id === tl))
const snapToLayers: string[] | undefined =
args.snap_onto_layers?.split(",")?.map((l) => l.trim()) ?? []
const maxSnapDistance: number = Number(args.max_snap_distance ?? 25) ?? 25
@ -33,21 +33,20 @@
async function onConfirm(): Promise<void> {
const importedId = await importFlow.onConfirm(value.data, snappedTo.data)
state.selectedLayer.setData(targetLayer)
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(importedId))
}
</script>
<ImportFlow {importFlow} on:confirm={onConfirm}>
<div class="relative" slot="map">
<div class="h-32">
<div class="h-64">
<NewPointLocationInput
coordinate={startCoordinate}
{maxSnapDistance}
{snapToLayers}
{snappedTo}
{state}
{targetLayer}
targetLayer={targetLayers}
{value}
/>
</div>

View file

@ -1,254 +0,0 @@
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import BaseUIElement from "../BaseUIElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import { VariableUiElement } from "../Base/VariableUIElement"
import all_languages from "../../assets/language_translations.json"
import { Translation } from "../i18n/Translation"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
import Lazy from "../Base/Lazy"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
import List from "../Base/List"
import { AllLanguagesSelector } from "./AllLanguagesSelector"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../Logic/Tags/And"
import { Tag } from "../../Logic/Tags/Tag"
import { EditButton, SaveButton } from "./SaveButton"
import Translations from "../i18n/Translations"
import Toggle from "../Input/Toggle"
import { Feature } from "geojson"
export class LanguageElement implements SpecialVisualization {
funcName: string = "language_chooser"
needsUrls = []
docs: string | BaseUIElement =
"The language element allows to show and pick all known (modern) languages. The key can be set"
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
{
name: "key",
required: true,
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `language:nl=yes` if nl is picked ",
},
{
name: "question",
required: true,
doc: "What to ask if no questions are known",
},
{
name: "render_list_item",
doc: "How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain).",
defaultValue: "{language()}",
},
{
name: "render_single_language",
doc: "What will be shown if the feature only supports a single language",
required: true,
},
{
name: "render_all",
doc: "The full rendering. Use `{list}` to show where the list of languages must come. Optional if mode=single",
defaultValue: "{list()}",
},
{
name: "no_known_languages",
doc: "The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead",
},
{
name: "mode",
doc: "If one or many languages can be selected. Should be 'multi' or 'single'",
defaultValue: "multi",
},
]
example: `
\`\`\`json
{"special":
"type": "language_chooser",
"key": "school:language",
"question": {"en": "What are the main (and administrative) languages spoken in this school?"},
"render_single_language": {"en": "{language()} is spoken on this school"},
"render_list_item": {"en": "{language()}"},
"render_all": {"en": "The following languages are spoken here:{list()}"}
"mode":"multi"
}
\`\`\`
`
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature
): BaseUIElement {
let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] =
argument
if (mode === undefined || mode.length == 0) {
mode = "multi"
}
if (item_render === undefined || item_render.trim() === "") {
item_render = "{language()}"
}
if (all_render === undefined || all_render.length == 0) {
all_render = "{list()}"
}
if (mode !== "single" && mode !== "multi") {
throw (
"Error while calling language_chooser: mode must be either 'single' or 'multi' but it is " +
mode
)
}
if (single_render.indexOf("{language()") < 0) {
throw (
"Error while calling language_chooser: render_single_language must contain '{language()}' but it is " +
single_render
)
}
if (item_render.indexOf("{language()") < 0) {
throw (
"Error while calling language_chooser: render_list_item must contain '{language()}' but it is " +
item_render
)
}
if (all_render.indexOf("{list()") < 0) {
throw "Error while calling language_chooser: render_all must contain '{list()}'"
}
const prefix = key + ":"
const foundLanguages = tagSource.map((tags) => {
const foundLanguages: string[] = []
for (const k in tags) {
const v = tags[k]
if (v !== "yes") {
continue
}
if (k.startsWith(prefix)) {
foundLanguages.push(k.substring(prefix.length))
}
}
return foundLanguages
})
const forceInputMode = new UIEventSource(false)
const inputEl = new Lazy(() => {
const selector = new AllLanguagesSelector({
mode: mode === "single" ? "select-one" : "select-many",
currentCountry: tagSource.map((tgs) => tgs["_country"]),
})
const cancelButton = Toggle.If(forceInputMode, () =>
Translations.t.general.cancel
.Clone()
.SetClass("btn btn-secondary")
.onClick(() => forceInputMode.setData(false))
)
const saveButton = new SaveButton(
selector.GetValue().map((lngs) => (lngs.length > 0 ? "true" : undefined)),
state
).onClick(() => {
const selectedLanguages = selector.GetValue().data
const currentLanguages = foundLanguages.data
const selection: Tag[] = selectedLanguages.map((ln) => new Tag(prefix + ln, "yes"))
for (const currentLanguage of currentLanguages) {
if (selectedLanguages.indexOf(currentLanguage) >= 0) {
continue
}
// Erase language that is not spoken anymore
selection.push(new Tag(prefix + currentLanguage, ""))
}
if (state.featureSwitchIsTesting.data) {
for (const tag of selection) {
tagSource.data[tag.key] = tag.value
}
tagSource.ping()
} else {
;(state?.changes)
.applyAction(
new ChangeTagAction(
tagSource.data.id,
new And(selection),
tagSource.data,
{
theme: state?.layout?.id ?? "unkown",
changeType: "answer",
}
)
)
.then((_) => {
console.log("Tagchanges applied")
})
}
forceInputMode.setData(false)
})
return new Combine([
new Title(question),
selector,
new Combine([cancelButton, saveButton]).SetClass("flex justify-end"),
]).SetClass("flex flex-col question disable-links")
})
const editButton = new EditButton(state.osmConnection, () => forceInputMode.setData(true))
return new VariableUiElement(
foundLanguages.map(
(foundLanguages) => {
if (forceInputMode.data) {
return inputEl
}
if (foundLanguages.length === 0) {
// No languages found - we show the question and the input element
if (
on_no_known_languages !== undefined &&
on_no_known_languages.length > 0
) {
return new Combine([on_no_known_languages, editButton]).SetClass(
"flex justify-end"
)
}
return inputEl
}
let rendered: BaseUIElement
if (foundLanguages.length === 1) {
const ln = foundLanguages[0]
let mapping = new Map<string, BaseUIElement>()
mapping.set("language", new Translation(all_languages[ln]))
rendered = new SubstitutedTranslation(
new Translation({ "*": single_render }, undefined),
tagSource,
state,
mapping
)
} else {
let mapping = new Map<string, BaseUIElement>()
const languagesList = new List(
foundLanguages.map((ln) => {
let mappingLn = new Map<string, BaseUIElement>()
mappingLn.set("language", new Translation(all_languages[ln]))
return new SubstitutedTranslation(
new Translation({ "*": item_render }, undefined),
tagSource,
state,
mappingLn
)
})
)
mapping.set("list", languagesList)
rendered = new SubstitutedTranslation(
new Translation({ "*": all_render }, undefined),
tagSource,
state,
mapping
)
}
return new Combine([rendered, editButton]).SetClass("flex justify-between")
},
[forceInputMode]
)
)
}
}

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import SpecialTranslation from "../TagRendering/SpecialTranslation.svelte"
import { Translation, TypedTranslation } from "../../i18n/Translation"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import * as all_languages from "../../../assets/language_translations.json"
/**
* Visualizes a list of the known languages
*/
export let languages: Store<string[]>
export let single_render: string
export let item_render: string
export let render_all: string // Should contain one `{list()}`
export let tags: UIEventSource<Record<string, string>>
export let state: SpecialVisualizationState
export let feature: Feature
export let layer: LayerConfig | undefined
let [beforeListing, afterListing] = (render_all ?? "{list()}").split("{list()}")
</script>
{#if $languages.length === 1}
<SpecialTranslation {state} {tags} {feature} {layer}
t={new TypedTranslation({"*": single_render}).PartialSubsTr(
"language()",
new Translation(all_languages[$languages[0]], undefined)
)}/>
{:else}
{beforeListing}
<ul>
{#each $languages as language}
<li>
<SpecialTranslation {state} {tags} {feature} {layer} t={
new TypedTranslation({"*": item_render}).PartialSubsTr("language()",
new Translation(all_languages[language], undefined) )}
/>
</li>
{/each}
</ul>
{afterListing}
{/if}

View file

@ -0,0 +1,69 @@
<script lang="ts">
import { UIEventSource } from "../../../Logic/UIEventSource"
import LanguageQuestion from "./LanguageQuestion.svelte"
import LanguageAnswer from "./LanguageAnswer.svelte"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import EditButton from "../TagRendering/EditButton.svelte"
export let key: string
export let tags: UIEventSource<Record<string, string>>
export let state: SpecialVisualizationState
export let feature: Feature
export let layer: LayerConfig | undefined
export let question: string
export let on_no_known_languages: string = undefined
export let single_render: string
export let item_render: string
export let render_all: string // Should contain one `{list()}`
let prefix = key + ":"
let foundLanguages = tags.map((tags) => {
const foundLanguages: string[] = []
for (const k in tags) {
const v = tags[k]
if (v !== "yes") {
continue
}
if (k.startsWith(prefix)) {
foundLanguages.push(k.substring(prefix.length))
}
}
return foundLanguages
})
const forceInputMode = new UIEventSource(false)
</script>
{#if $foundLanguages.length === 0 && on_no_known_languages && !$forceInputMode}
<div class="p-1 flex items-center justify-between low-interaction rounded">
<div>
{on_no_known_languages}
</div>
<EditButton on:click={_ => forceInputMode.setData(true)} />
</div>
{:else if $forceInputMode || $foundLanguages.length === 0}
<LanguageQuestion {question} {foundLanguages} {prefix} {state} {tags} {feature} {layer}
on:save={_ => forceInputMode.setData(false)}>
<span slot="cancel-button">
{#if $forceInputMode}
<button on:click={_ => forceInputMode.setData(false)}>
<Tr t={Translations.t.general.cancel} />
</button>
{/if}
</span>
</LanguageQuestion>
{:else}
<div class="p-2 flex items-center justify-between low-interaction rounded">
<div>
<LanguageAnswer {single_render} {item_render} {render_all} languages={foundLanguages} {state} {tags} { feature}
{layer} />
</div>
<EditButton on:click={_ => forceInputMode.setData(true)} />
</div>
{/if}

View file

@ -0,0 +1,107 @@
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import BaseUIElement from "../../BaseUIElement"
import { UIEventSource } from "../../../Logic/UIEventSource"
import SvelteUIElement from "../../Base/SvelteUIElement"
import { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { default as LanguageElementSvelte } from "./LanguageElement.svelte"
export class LanguageElement implements SpecialVisualization {
funcName: string = "language_chooser"
needsUrls = []
docs: string | BaseUIElement =
"The language element allows to show and pick all known (modern) languages. The key can be set"
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
{
name: "key",
required: true,
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `language:nl=yes` if nl is picked ",
},
{
name: "question",
required: true,
doc: "What to ask if no questions are known",
},
{
name: "render_list_item",
doc: "How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain).",
defaultValue: "{language()}",
},
{
name: "render_single_language",
doc: "What will be shown if the feature only supports a single language",
required: true,
},
{
name: "render_all",
doc: "The full rendering. Use `{list}` to show where the list of languages must come. Optional if mode=single",
defaultValue: "{list()}",
},
{
name: "no_known_languages",
doc: "The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead",
},
]
example: `
\`\`\`json
{"special":
"type": "language_chooser",
"key": "school:language",
"question": {"en": "What are the main (and administrative) languages spoken in this school?"},
"render_single_language": {"en": "{language()} is spoken on this school"},
"render_list_item": {"en": "{language()}"},
"render_all": {"en": "The following languages are spoken here:{list()}"}
}
\`\`\`
`
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
let [key, question, item_render, single_render, all_render, on_no_known_languages] =
argument
if (item_render === undefined || item_render.trim() === "") {
item_render = "{language()}"
}
if (all_render === undefined || all_render.length == 0) {
all_render = "{list()}"
}
if (single_render.indexOf("{language()") < 0) {
throw (
"Error while calling language_chooser: render_single_language must contain '{language()}' but it is " +
single_render
)
}
if (item_render.indexOf("{language()") < 0) {
throw (
"Error while calling language_chooser: render_list_item must contain '{language()}' but it is " +
item_render
)
}
if (all_render.indexOf("{list()") < 0) {
throw "Error while calling language_chooser: render_all must contain '{list()}'"
}
if (on_no_known_languages === "") {
on_no_known_languages = undefined
}
return new SvelteUIElement(LanguageElementSvelte, {
key,
tags: tagSource,
state,
feature,
layer,
question,
on_no_known_languages,
single_render,
item_render,
})
}
}

View file

@ -0,0 +1,133 @@
<script lang="ts">/**
* An input element which allows to select one or more langauges
*/
import { UIEventSource } from "../../../Logic/UIEventSource"
import all_languages from "../../../assets/language_translations.json"
import { Translation } from "../../i18n/Translation"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import Locale from "../../i18n/Locale"
/**
* Will contain one or more ISO-language codes
*/
export let selectedLanguages: UIEventSource<string[]>
/**
* The country (countries) that the point lies in.
* Note that a single place might be claimed by multiple countries
*/
export let countries: Set<string>
let searchValue: UIEventSource<string> = new UIEventSource<string>("")
let searchLC = searchValue.mapD(search => search.toLowerCase())
const knownLanguagecodes = Object.keys(all_languages)
let probableLanguages = []
let isChecked = {}
for (const lng of knownLanguagecodes) {
const lngInfo = all_languages[lng]
if (lngInfo._meta?.countries?.some(l => countries.has(l))) {
probableLanguages.push(lng)
}
isChecked[lng] = false
}
let newlyChecked: UIEventSource<string[]> = new UIEventSource<string[]>([])
function update(isChecked: Record<string, boolean>) {
const currentlyChecked = new Set<string>(selectedLanguages.data)
const languages: string[] = []
for (const lng in isChecked) {
if (isChecked[lng]) {
languages.push(lng)
if (!currentlyChecked.has(lng)) {
newlyChecked.data.push(lng)
newlyChecked.ping()
}
}
}
selectedLanguages.setData(languages)
}
function matchesSearch(lng: string, searchLc: string | undefined): boolean {
if(!searchLc){
return
}
if(lng.indexOf(searchLc) >= 0){
return true
}
const languageInfo = all_languages[lng]
const native : string = languageInfo[lng]?.toLowerCase()
if(native?.indexOf(searchLc) >= 0){
return true
}
const current : string = languageInfo[Locale.language.data]?.toLowerCase()
if(current?.indexOf(searchLc) >= 0){
return true
}
return false
}
function onEnter(){
// we select the first match which is not yet checked
for (const lng of knownLanguagecodes) {
if(lng === searchLC.data){
isChecked[lng] = true
return
}
}
for (const lng of knownLanguagecodes) {
if(matchesSearch(lng, searchLC.data)){
isChecked[lng] = true
return
}
}
}
$: {
update(isChecked)
}
searchValue.addCallback(_ => {
newlyChecked.setData([])
})
</script>
<form on:submit|preventDefault={() => onEnter()}>
{#each probableLanguages as lng}
<label class="no-image-background flex items-center gap-1">
<input bind:checked={isChecked[lng]} type="checkbox" />
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/each}
<label class="block relative neutral-label m-4 mx-16">
<SearchIcon class="w-6 h-6 absolute right-0" />
<input bind:value={$searchValue} type="text" />
<Tr t={Translations.t.general.useSearch} />
</label>
<div class="overflow-auto" style="max-height: 25vh">
{#each knownLanguagecodes as lng}
{#if (isChecked[lng]) && $newlyChecked.indexOf(lng) < 0 && probableLanguages.indexOf(lng) < 0}
<label class="no-image-background flex items-center gap-1">
<input bind:checked={isChecked[lng]} type="checkbox" />
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/if}
{/each}
{#each knownLanguagecodes as lng}
{#if $searchLC.length > 0 && matchesSearch(lng, $searchLC) && (!isChecked[lng] || $newlyChecked.indexOf(lng) >= 0) && probableLanguages.indexOf(lng) < 0}
<label class="no-image-background flex items-center gap-1">
<input bind:checked={isChecked[lng]} type="checkbox" />
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/if}
{/each}
</div>
</form>

View file

@ -0,0 +1,87 @@
<script lang="ts">/**
* The 'languageQuestion' is a special element which asks about the (possible) languages of a feature
* (e.g. which speech output an ATM has, in what language(s) the braille writing is or what languages are spoken at a school)
*
* This is written into a `key`.
*
*/
import { Translation } from "../../i18n/Translation"
import SpecialTranslation from "../TagRendering/SpecialTranslation.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Store } from "../../../Logic/UIEventSource"
import { UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import LanguageOptions from "./LanguageOptions.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import { createEventDispatcher } from "svelte"
import { Tag } from "../../../Logic/Tags/Tag"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../../Logic/Tags/And"
export let question: string
export let prefix: string
export let foundLanguages: Store<string[]>
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig | undefined
let dispatch = createEventDispatcher<{ save }>()
let selectedLanguages: UIEventSource<string[]> = new UIEventSource<string[]>([])
let countries: Store<Set<string>> = tags.mapD(tags => new Set<string>(tags["_country"]?.toUpperCase()?.split(";") ?? []))
async function applySelectedLanguages() {
const selectedLngs = selectedLanguages.data
const selection: Tag[] = selectedLanguages.data.map((ln) => new Tag(prefix + ln, "yes"))
if (selection.length === 0) {
return
}
const currentLanguages = foundLanguages.data
for (const currentLanguage of currentLanguages) {
if (selectedLngs.indexOf(currentLanguage) >= 0) {
continue
}
// Erase languages that are not spoken anymore
selection.push(new Tag(prefix + currentLanguage, ""))
}
if (state === undefined || state?.featureSwitchIsTesting?.data) {
for (const tag of selection) {
tags.data[tag.key] = tag.value
}
tags.ping()
} else if (state.changes) {
await state.changes
.applyAction(
new ChangeTagAction(
tags.data.id,
new And(selection),
tags.data,
{
theme: state?.layout?.id ?? "unkown",
changeType: "answer",
},
),
)
}
dispatch("save")
}
</script>
<div class="flex flex-col disable-links interactive border-interactive p-2">
<div class="interactive justify-between pt-1 font-bold">
<SpecialTranslation {feature} {layer} {state} t={new Translation({"*":question})} {tags} />
</div>
<LanguageOptions {selectedLanguages} countries={$countries}/>
<div class="flex justify-end flex-wrap-reverse w-full">
<slot name="cancel-button"></slot>
<button class="primary" class:disabled={$selectedLanguages.length === 0} on:click={_ => applySelectedLanguages()}>
<Tr t={Translations.t.general.save} />
</button>
</div>
</div>

View file

@ -6,7 +6,6 @@ import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import SvelteUIElement from "../Base/SvelteUIElement"
import MaplibreMap from "../Map/MaplibreMap.svelte"
import ShowDataLayer from "../Map/ShowDataLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { GeoOperations } from "../../Logic/GeoOperations"
import { BBox } from "../../Logic/BBox"
@ -32,7 +31,7 @@ export class MinimapViz implements SpecialVisualization {
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
feature: Feature
) {
if (state === undefined || feature === undefined) {
return undefined

View file

@ -18,6 +18,7 @@
import Geosearch from "../BigComponents/Geosearch.svelte"
import If from "../Base/If.svelte"
import Constants from "../../Models/Constants"
import LoginToggle from "../Base/LoginToggle.svelte"
export let state: SpecialVisualizationState
@ -49,7 +50,7 @@
let notAllowed = moveWizardState.moveDisallowedReason
let currentMapProperties: MapProperties = undefined
</script>
<LoginToggle {state}>
{#if moveWizardState.reasons.length > 0}
{#if $notAllowed}
<div class="m-2 flex rounded-lg bg-gray-200 p-2">
@ -165,3 +166,4 @@
</div>
{/if}
{/if}
</LoginToggle>

View file

@ -10,8 +10,8 @@ import { Tag } from "../../Logic/Tags/Tag"
import { SpecialVisualizationState } from "../SpecialVisualization"
import { Feature, Point } from "geojson"
import SvelteUIElement from "../Base/SvelteUIElement"
import Confirm from "../../assets/svg/Confirm.svelte"
import Relocation from "../../assets/svg/Relocation.svelte"
import Location from "../../assets/svg/Location.svelte"
export interface MoveReason {
text: Translation | string
@ -62,7 +62,7 @@ export class MoveWizardState {
reasons.push({
text: t.reasons.reasonInaccurate,
invitingText: t.inviteToMove.reasonInaccurate,
icon: new SvelteUIElement(Confirm),
icon: new SvelteUIElement(Location),
changesetCommentValue: "improve_accuracy",
lockBounds: true,
includeSearch: false,

View file

@ -18,8 +18,7 @@ export class AddNoteCommentViz implements SpecialVisualization {
public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[]
tags: UIEventSource<Record<string, string>>
) {
return new SvelteUIElement(AddNoteComment, { state, tags })
}

View file

@ -1,7 +1,6 @@
import BaseUIElement from "../../BaseUIElement"
import Translations from "../../i18n/Translations"
import { Utils } from "../../../Utils"
import Svg from "../../../Svg"
import Img from "../../Base/Img"
import { SubtleButton } from "../../Base/SubtleButton"
import Toggle from "../../Input/Toggle"

View file

@ -1,6 +1,5 @@
import Combine from "../../Base/Combine"
import BaseUIElement from "../../BaseUIElement"
import Svg from "../../../Svg"
import Link from "../../Base/Link"
import { FixedUiElement } from "../../Base/FixedUiElement"
import Translations from "../../i18n/Translations"
@ -11,6 +10,10 @@ import { Stores, UIEventSource } from "../../../Logic/UIEventSource"
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
import { VariableUiElement } from "../../Base/VariableUIElement"
import { SpecialVisualizationState } from "../../SpecialVisualization"
import SvelteUIElement from "../../Base/SvelteUIElement"
import Note from "../../../assets/svg/Note.svelte"
import Resolved from "../../../assets/svg/Resolved.svelte"
import Speech_bubble from "../../../assets/svg/Speech_bubble.svelte"
export default class NoteCommentElement extends Combine {
constructor(
@ -32,11 +35,11 @@ export default class NoteCommentElement extends Combine {
let actionIcon: BaseUIElement
if (comment.action === "opened" || comment.action === "reopened") {
actionIcon = Svg.note_svg()
actionIcon = new SvelteUIElement(Note)
} else if (comment.action === "closed") {
actionIcon = Svg.resolved_svg()
actionIcon = new SvelteUIElement(Resolved)
} else {
actionIcon = Svg.speech_bubble_svg()
actionIcon = new SvelteUIElement(Speech_bubble)
}
let user: BaseUIElement
@ -72,6 +75,7 @@ export default class NoteCommentElement extends Combine {
const extension = link.substring(lastDotIndex + 1, link.length)
return Utils.imageExtensions.has(extension)
})
.filter(link => !link.startsWith("https://wiki.openstreetmap.org/wiki/File:"))
let imagesEl: BaseUIElement = undefined
if (images.length > 0) {
const imageEls = images.map((i) =>

View file

@ -1,47 +0,0 @@
import { Store } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import BaseUIElement from "../BaseUIElement"
import Combine from "../Base/Combine"
import Svg from "../../Svg"
import { LoginToggle } from "./LoginButton"
export class EditButton extends Toggle {
constructor(osmConnection: OsmConnection, onClick: () => void) {
super(
new Combine([Svg.pencil_svg()])
.SetClass("block relative h-10 w-10 p-2 float-right")
.SetStyle("border: 1px solid black; border-radius: 0.7em")
.onClick(onClick),
undefined,
osmConnection.isLoggedIn
)
}
}
export class SaveButton extends LoginToggle {
constructor(
value: Store<any>,
state: {
readonly osmConnection?: OsmConnection
readonly featureSwitchUserbadge?: Store<boolean>
},
textEnabled?: BaseUIElement,
textDisabled?: BaseUIElement
) {
if (value === undefined) {
throw "No event source for savebutton, something is wrong"
}
const isSaveable = value.map((v) => v !== false && (v ?? "") !== "")
const saveEnabled = (textEnabled ?? Translations.t.general.save.Clone()).SetClass(`btn`)
const saveDisabled = (textDisabled ?? Translations.t.general.save.Clone()).SetClass(
`btn btn-disabled`
)
const save = new Toggle(saveEnabled, saveDisabled, isSaveable)
super(save, Translations.t.general.loginToStart, state)
}
}

View file

@ -4,6 +4,7 @@
import ToSvelte from "../Base/ToSvelte.svelte"
import { Utils } from "../../Utils"
import { Store } from "../../Logic/UIEventSource"
import Envelope from "../../assets/svg/Envelope.svelte"
export let tags: Store<OsmTags>
export let args: string[]
@ -14,6 +15,6 @@
</script>
<a class="button flex w-full items-center" href={url} style="margin-left: 0">
<ToSvelte construct={Svg.envelope_svg().SetClass("w-8 h-8 mr-4 shrink-0")} />
<Envelope class="w-8 h-8 mr-4 shrink-0"/>
{button_text}
</a>

View file

@ -1,5 +1,4 @@
import Toggle from "../Input/Toggle"
import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
@ -19,6 +18,7 @@ import { Changes } from "../../Logic/Osm/Changes"
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
import Scissors from "../../assets/svg/Scissors.svelte"
export default class SplitRoadWizard extends Combine {
public dialogIsOpened: UIEventSource<boolean>
@ -26,8 +26,8 @@ export default class SplitRoadWizard extends Combine {
/**
* A UI Element used for splitting roads
*
* @param id: The id of the road to remove
* @param state: the state of the application
* @param id The id of the road to remove
* @param state the state of the application
*/
constructor(
id: WayId,
@ -68,7 +68,7 @@ export default class SplitRoadWizard extends Combine {
// Toggle between splitmap
const splitButton = new SubtleButton(
Svg.scissors_svg().SetStyle("height: 1.5rem; width: auto"),
new SvelteUIElement(Scissors).SetClass("h-6 w-6"),
new Toggle(
t.splitAgain.Clone().SetClass("text-lg font-bold"),
t.inviteToSplit.Clone().SetClass("text-lg font-bold"),

View file

@ -18,6 +18,9 @@ import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Maproulette from "../../Logic/Maproulette"
import SvelteUIElement from "../Base/SvelteUIElement"
import Icon from "../Map/Icon.svelte"
import { Map } from "maplibre-gl"
export default class TagApplyButton implements AutoAction, SpecialVisualization {
public readonly funcName = "tag_apply"
@ -45,9 +48,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
doc: "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element",
},
{
name: "maproulette_task_id",
name: "maproulette_id",
defaultValue: undefined,
doc: "If specified, this maproulette-challenge will be closed when the tags are applied",
doc: "If specified, this maproulette-challenge will be closed when the tags are applied. This should be the ID of the task, _not_ the task_id.",
},
]
public readonly example =
@ -81,7 +84,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
for (const [key, value] of tgsSpec) {
if (value.indexOf("$") >= 0) {
let parts = value.split("$")
// THe first of the split won't start with a '$', so no substitution needed
// The first item of the split won't start with a '$', so no substitution needed
let actualValue = parts[0]
parts.shift()
@ -111,7 +114,6 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
while (spec.length > 0) {
const [part] = spec.match(/((\\;)|[^;])*/)
console.log("Spec is", part, spec)
spec = spec.substring(part.length + 1) // +1 to remove the pending ';' as well
const kv = part.split("=").map((s) => s.trim().replace("\\;", ";"))
if (kv.length == 2) {
@ -133,12 +135,8 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
}
public async applyActionOn(
feature: Feature,
state: {
layout: LayoutConfig
changes: Changes
indexedFeatures: IndexedFeatureSource
},
_: Feature,
state: SpecialVisualizationState,
tags: UIEventSource<any>,
args: string[]
): Promise<void> {
@ -156,14 +154,22 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
}
)
await state.changes.applyAction(changeAction)
try {
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(targetId))
}catch (e) {
console.error(e)
}
const maproulette_id_key = args[4]
if (maproulette_id_key) {
const maproulette_id = Number(tags.data[maproulette_id_key])
await Maproulette.singleton.closeTask(maproulette_id, Maproulette.STATUS_FIXED, {
const maproulette_id = tags.data[ maproulette_id_key]
const maproulette_feature= state.indexedFeatures.featuresById.data.get(
maproulette_id)
const maproulette_task_id = Number(maproulette_feature.properties.mr_taskId)
await Maproulette.singleton.closeTask(maproulette_task_id, Maproulette.STATUS_FIXED, {
comment: "Tags are copied onto " + targetId + " with MapComplete",
})
tags.data["mr_taskStatus"] = "Fixed"
tags.ping()
maproulette_feature.properties["mr_taskStatus"] = "Fixed"
state.featureProperties.getStore(maproulette_id).ping()
}
}
@ -180,6 +186,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
if (image === "" || image === "undefined") {
image = undefined
}
const targetIdKey = args[3]
const t = Translations.t.general.apply_button
@ -195,9 +202,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
})
).SetClass("subtle")
const self = this
const applied = new UIEventSource(tags?.data?.["mr_taskStatus"] !== "Created") // This will default to 'false' for non-maproulette challenges
const applied = new UIEventSource(tags?.data?.["mr_taskStatus"] !== undefined && tags?.data?.["mr_taskStatus"] !== "Created") // This will default to 'false' for non-maproulette challenges
const applyButton = new SubtleButton(
image,
new SvelteUIElement(Icon, {icon: image}),
new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
).onClick(async () => {
applied.setData(true)

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid";
import { ariaLabel } from "../../../Utils/ariaLabel.js";
import { Translation } from "../../i18n/Translation"
/**
* A small, round button with an edit-icon (and aria-labels etc)
*/
/**
* What arialabel to apply onto this button?
*/
export let arialabel : Translation = undefined;
export let ariaLabelledBy: string = undefined
</script>
<button
on:click
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
aria-labelledby={arialabel === undefined ? ariaLabelledBy : undefined}
use:ariaLabel={arialabel}
>
<PencilAltIcon />
</button>

View file

@ -12,6 +12,7 @@
import type { SpecialVisualizationState } from "../../SpecialVisualization"
export let value: UIEventSource<string>
export let unvalidatedText: UIEventSource<string> = new UIEventSource<string>(value.data)
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
@ -64,6 +65,7 @@
type={config.freeform.type}
{placeholder}
{value}
{unvalidatedText}
/>
{/if}
@ -74,5 +76,6 @@
{value}
{state}
on:submit
{unvalidatedText}
/>
</div>

View file

@ -6,7 +6,6 @@
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Feature } from "geojson"
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import { onDestroy } from "svelte"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { twMerge } from "tailwind-merge"
@ -24,7 +23,7 @@
throw "Config is undefined in tagRenderingAnswer"
}
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) =>
Utils.NoNull(config?.GetRenderValues(tags))
Utils.NoNull(config?.GetRenderValues(tags)),
)
</script>

View file

@ -13,6 +13,8 @@
import { Utils } from "../../../Utils"
import { twMerge } from "tailwind-merge"
import { ariaLabel } from "../../../Utils/ariaLabel"
import EditButton from "./EditButton.svelte"
import EditItemButton from "../../Studio/EditItemButton.svelte"
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
@ -63,7 +65,6 @@
if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow")
htmlElem.tabIndex = -1
console.log("Scrolling to", htmlElem)
htmlElem.scrollIntoView({ behavior: "smooth" })
Utils.focusOnFocusableChild(htmlElem)
} else {
@ -88,6 +89,7 @@
{state}
{layer}
on:saved={() => (editMode = false)}
allowDeleteOfFreeform={true}
>
<button
slot="cancel"
@ -102,16 +104,12 @@
{:else}
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">
<TagRenderingAnswer id={answerId} {config} {tags} {selectedElement} {state} {layer} />
<button
<EditButton
arialabel={config.editButtonAriaLabel}
ariaLabelledBy={answerId}
on:click={() => {
editMode = true
}}
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
aria-labelledby={config.editButtonAriaLabel === undefined ? answerId : undefined}
use:ariaLabel={config.editButtonAriaLabel}
>
<PencilAltIcon />
</button>
editMode = true
}}/>
</div>
{/if}
{:else}

View file

@ -29,7 +29,7 @@
{#if mapping.icon !== undefined}
<div class="inline-flex items-center">
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-2")} />
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass ?? "small"}`, "mr-2")} />
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} />
</div>
{:else if mapping.then !== undefined}

View file

@ -22,17 +22,23 @@
import { Unit } from "../../../Models/Unit"
import UserRelatedState from "../../../Logic/State/UserRelatedState"
import { twJoin } from "tailwind-merge"
import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import Search from "../../../assets/svg/Search.svelte"
import Login from "../../../assets/svg/Login.svelte"
import { placeholder } from "../../../Utils/placeholder"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Tag } from "../../../Logic/Tags/Tag"
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 selectedTags: TagsFilter = undefined
export let selectedTags: UploadableTag = undefined
export let allowDeleteOfFreeform: boolean = false
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
@ -40,13 +46,15 @@
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
let freeformInputUnvalidated = new UIEventSource<string>(freeformInput.data)
let selectedMapping: number = undefined
/**
* A list of booleans, used if multiAnswer is set
*/
let checkedMappings: boolean[]
let mappings: Mapping[] = config?.mappings
let mappings: Mapping[] = config?.mappings ?? []
let searchTerm: UIEventSource<string> = new UIEventSource("")
let dispatch = createEventDispatcher<{
@ -128,7 +136,6 @@
}
freeformInput.addCallbackAndRun((freeformValue) => {
console.log("FreeformValue:", freeformValue)
if (!mappings || mappings?.length == 0 || config.freeform?.key === undefined) {
return
}
@ -146,20 +153,25 @@
}
})
$: {
try {
selectedTags = config?.constructChangeSpecification(
$freeformInput,
selectedMapping,
checkedMappings,
tags.data,
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined
if (allowDeleteOfFreeform && $freeformInput === undefined && $freeformInputUnvalidated === "" && (mappings?.length ?? 0) === 0) {
selectedTags = new Tag(config.freeform.key, "")
} else {
try {
selectedTags = config?.constructChangeSpecification(
$freeformInput,
selectedMapping,
checkedMappings,
tags.data,
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined
}
}
}
function onSave(e) {
function onSave(_ = undefined) {
if (selectedTags === undefined) {
return
}
@ -198,9 +210,9 @@
function onInputKeypress(e: KeyboardEvent) {
if (e.key === "Enter") {
e.preventDefault()
e.stopPropagation()
onSave(e)
e.preventDefault()
e.stopPropagation()
onSave()
}
}
@ -231,136 +243,139 @@
<fieldset>
<legend>
<div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</div>
<div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</div>
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
</legend>
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" />
<input
type="text"
bind:value={$searchTerm}
class="w-full"
use:placeholder={Translations.t.general.searchAnswer}
/>
</div>
{/if}
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" />
<input
type="text"
bind:value={$searchTerm}
class="w-full"
use:placeholder={Translations.t.general.searchAnswer}
/>
</div>
{/if}
{#if config.freeform?.key && !(mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput
{config}
{#if config.freeform?.key && !(mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
unvalidatedText={freeformInputUnvalidated}
on:submit={onSave}
/>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
<TagRenderingMappingInput
{mapping}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={selectedMapping === i}
>
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={i}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex gap-x-1">
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={config.mappings?.length}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:selected={() => (selectedMapping = config.mappings?.length)}
on:submit={onSave}
/>
</label>
{/if}
</div>
{:else if mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={checkedMappings[i]}
>
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex gap-x-1">
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
</label>
{/if}
</div>
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={selectedMapping === i}
>
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={i}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex gap-x-1">
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={config.mappings?.length}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
unvalidatedText={freeformInputUnvalidated}
on:selected={() => (selectedMapping = config.mappings?.length)}
on:submit={onSave}
/>
</label>
{/if}
</div>
{:else if mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={checkedMappings[i]}
>
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex gap-x-1">
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
unvalidatedText={freeformInputUnvalidated}
on:submit={onSave}
/>
</label>
{/if}
</div>
{/if}
<LoginToggle {state}>
<Loading slot="loading" />
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
@ -379,12 +394,19 @@
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>
<button
on:click={onSave}
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
>
<Tr t={Translations.t.general.save} />
</button>
{#if allowDeleteOfFreeform && (mappings?.length ?? 0) === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
<button class="primary flex" on:click|stopPropagation|preventDefault={onSave}>
<TrashIcon class="w-6 h-6 text-red-500" />
<Tr t={Translations.t.general.eraseValue}/>
</button>
{:else}
<button
on:click={onSave}
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
>
<Tr t={Translations.t.general.save} />
</button>
{/if}
</slot>
</div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}