A11y: more screenreader a11y tweaks, UX tweaks

This commit is contained in:
Pieter Vander Vennet 2023-12-31 20:57:45 +01:00
parent 8122826ddc
commit 3059d2ed26
16 changed files with 291 additions and 248 deletions

View file

@ -1,13 +1,16 @@
{ {
"id": "mapcomplete-changes", "id": "mapcomplete-changes",
"title": { "title": {
"en": "Changes made with MapComplete" "en": "Changes made with MapComplete",
"de": "Mit MapComplete vorgenommene Änderungen"
}, },
"shortDescription": { "shortDescription": {
"en": "Shows changes made by MapComplete" "en": "Shows changes made by MapComplete",
"de": "Zeigt die von MapComplete vorgenommenen Änderungen an"
}, },
"description": { "description": {
"en": "This maps shows all the changes made with MapComplete" "en": "This maps shows all the changes made with MapComplete",
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen"
}, },
"icon": "./assets/svg/logo.svg", "icon": "./assets/svg/logo.svg",
"hideFromOverview": true, "hideFromOverview": true,
@ -20,7 +23,8 @@
{ {
"id": "mapcomplete-changes", "id": "mapcomplete-changes",
"name": { "name": {
"en": "Changeset centers" "en": "Changeset centers",
"de": "Zentrum der Änderungssätze"
}, },
"minzoom": 0, "minzoom": 0,
"source": { "source": {
@ -31,41 +35,48 @@
}, },
"title": { "title": {
"render": { "render": {
"en": "Changeset for {theme}" "en": "Changeset for {theme}",
"de": "Änderungssatz für {theme}"
} }
}, },
"description": { "description": {
"en": "Shows all MapComplete changes" "en": "Shows all MapComplete changes",
"de": "Zeigt alle MapComplete-Änderungen"
}, },
"tagRenderings": [ "tagRenderings": [
{ {
"id": "show_changeset_id", "id": "show_changeset_id",
"render": { "render": {
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>" "en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"de": "Änderungssatz <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
} }
}, },
{ {
"id": "contributor", "id": "contributor",
"question": { "question": {
"en": "What contributor did make this change?" "en": "What contributor did make this change?",
"de": "Wer hat diese Änderung vorgenommen?"
}, },
"freeform": { "freeform": {
"key": "user" "key": "user"
}, },
"render": { "render": {
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>" "en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"de": "Änderung von <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
} }
}, },
{ {
"id": "theme-id", "id": "theme-id",
"question": { "question": {
"en": "What theme was used to make this change?" "en": "What theme was used to make this change?",
"de": "Welches Theme wurde für diese Änderung verwendet?"
}, },
"freeform": { "freeform": {
"key": "theme" "key": "theme"
}, },
"render": { "render": {
"en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>" "en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>",
"de": "Geändert mit Thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
} }
}, },
{ {
@ -74,19 +85,23 @@
"key": "locale" "key": "locale"
}, },
"question": { "question": {
"en": "What locale (language) was this change made in?" "en": "What locale (language) was this change made in?",
"de": "In welcher Benutzersprache wurde diese Änderung vorgenommen?"
}, },
"render": { "render": {
"en": "User locale is {locale}" "en": "User locale is {locale}",
"de": "Benutzersprache {locale}"
} }
}, },
{ {
"id": "host", "id": "host",
"render": { "render": {
"en": "Change with with <a href='{host}'>{host}</a>" "en": "Change with with <a href='{host}'>{host}</a>",
"de": "Geändert über <a href='{host}'>{host}</a>"
}, },
"question": { "question": {
"en": "What host (website) was this change made with?" "en": "What host (website) was this change made with?",
"de": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?"
}, },
"freeform": { "freeform": {
"key": "host" "key": "host"
@ -107,10 +122,12 @@
{ {
"id": "version", "id": "version",
"question": { "question": {
"en": "What version of MapComplete was used to make this change?" "en": "What version of MapComplete was used to make this change?",
"de": "Welche Version von MapComplete wurde verwendet, um diese Änderung vorzunehmen?"
}, },
"render": { "render": {
"en": "Made with {editor}" "en": "Made with {editor}",
"de": "Erstellt mit {editor}"
}, },
"freeform": { "freeform": {
"key": "editor" "key": "editor"
@ -460,7 +477,8 @@
} }
], ],
"question": { "question": {
"en": "Themename contains {search}" "en": "Themename contains {search}",
"de": "Themename enthält {search}"
} }
} }
] ]
@ -476,7 +494,8 @@
} }
], ],
"question": { "question": {
"en": "Themename does <b>not</b> contain {search}" "en": "Themename does <b>not</b> contain {search}",
"de": "Der Name enthält <b>nicht</b> {search}"
} }
} }
] ]
@ -492,7 +511,8 @@
} }
], ],
"question": { "question": {
"en": "Made by contributor {search}" "en": "Made by contributor {search}",
"de": "Der Name enthält <b>nicht</b> {search}"
} }
} }
] ]
@ -508,7 +528,8 @@
} }
], ],
"question": { "question": {
"en": "<b>Not</b> made by contributor {search}" "en": "<b>Not</b> made by contributor {search}",
"de": "<b>Nicht</b> erstellt von {search}"
} }
} }
] ]
@ -525,7 +546,8 @@
} }
], ],
"question": { "question": {
"en": "Made before {search}" "en": "Made before {search}",
"de": "Erstellt vor {search}"
} }
} }
] ]
@ -542,7 +564,8 @@
} }
], ],
"question": { "question": {
"en": "Made after {search}" "en": "Made after {search}",
"de": "Erstellt nach {search}"
} }
} }
] ]
@ -558,7 +581,8 @@
} }
], ],
"question": { "question": {
"en": "User language (iso-code) {search}" "en": "User language (iso-code) {search}",
"de": "Benutzersprache (ISO-Code) {search}"
} }
} }
] ]
@ -574,7 +598,8 @@
} }
], ],
"question": { "question": {
"en": "Made with host {search}" "en": "Made with host {search}",
"de": "Erstellt mit Host {search}"
} }
} }
] ]
@ -585,7 +610,8 @@
{ {
"osmTags": "add-image>0", "osmTags": "add-image>0",
"question": { "question": {
"en": "Changeset added at least one image" "en": "Changeset added at least one image",
"de": "Änderungssatz hat mindestens ein Bild hinzugefügt"
} }
} }
] ]
@ -596,7 +622,8 @@
{ {
"osmTags": "theme!=grb", "osmTags": "theme!=grb",
"question": { "question": {
"en": "Exclude GRB theme" "en": "Exclude GRB theme",
"de": "GRB-Theme ausschließen"
} }
} }
] ]
@ -607,7 +634,8 @@
{ {
"osmTags": "theme!=etymology", "osmTags": "theme!=etymology",
"question": { "question": {
"en": "Exclude etymology theme" "en": "Exclude etymology theme",
"de": "Etymologie-Thema ausschließen"
} }
} }
] ]
@ -622,7 +650,8 @@
{ {
"id": "link_to_more", "id": "link_to_more",
"render": { "render": {
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>" "en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
"de": "Mehr Statistiken gibt es <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>"
} }
}, },
{ {

View file

@ -8543,6 +8543,18 @@
}, },
"question": "Does this stair have a handrail?" "question": "Does this stair have a handrail?"
}, },
"incline": {
"mappings": {
"0": {
"then": "The upward direction is {direction_absolute()}"
},
"1": {
"then": "The downward direction is {direction_absolute()}"
}
},
"question": "What is the incline of these stairs?",
"render": "These stairs have an incline of {incline}"
},
"multilevels": { "multilevels": {
"override": { "override": {
"question": "Between which levels are these stairs?", "question": "Between which levels are these stairs?",

View file

@ -370,7 +370,7 @@
"useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien", "useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien",
"useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…", "useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…",
"visualFeedback": { "visualFeedback": {
"closestFeaturesAre": "{n} object in beeld.", "closestFeaturesAre": "{n} objecten in beeld.",
"east": "Naar het oosten", "east": "Naar het oosten",
"in": "Aan het inzoomen naar zoomlevel {z}", "in": "Aan het inzoomen naar zoomlevel {z}",
"islocked": "Bewegen vergrendeld rond je huidige locatie. Duw op de geolocatie-knop om te ontgrendelen.", "islocked": "Bewegen vergrendeld rond je huidige locatie. Duw op de geolocatie-knop om te ontgrendelen.",
@ -378,6 +378,7 @@
"navigation": "Gebruik de pijltjestoetsen om te bewegen. Druk op spatie om het meest centrale punt te selecteren. Druk op een cijfertoets om andere items te selecteren.", "navigation": "Gebruik de pijltjestoetsen om te bewegen. Druk op spatie om het meest centrale punt te selecteren. Druk op een cijfertoets om andere items te selecteren.",
"noCloseFeatures": "Niet in beeld", "noCloseFeatures": "Niet in beeld",
"north": "Naar het noorden", "north": "Naar het noorden",
"oneFeatureInView": "Eén object in beeld.",
"out": "Aan het uitzoomen naar zoomlevel {z}", "out": "Aan het uitzoomen naar zoomlevel {z}",
"south": "Naar het zuiden", "south": "Naar het zuiden",
"unlocked": "Bewegen ontgrendeld", "unlocked": "Bewegen ontgrendeld",

View file

@ -79,7 +79,7 @@ export default class TagRenderingConfig {
public readonly mappings?: Mapping[] public readonly mappings?: Mapping[]
public readonly editButtonAriaLabel?: Translation public readonly editButtonAriaLabel?: Translation
public readonly labels: string[] public readonly labels: string[]
public readonly classes: string[] public readonly classes: string[] | undefined
constructor( constructor(
config: string | TagRenderingConfigJson | QuestionableTagRenderingConfigJson, config: string | TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
@ -131,6 +131,9 @@ export default class TagRenderingConfig {
this.classes = json.classes ?? [] this.classes = json.classes ?? []
} }
this.classes = [].concat(...this.classes.map((cl) => cl.split(" "))) this.classes = [].concat(...this.classes.map((cl) => cl.split(" ")))
if (this.classes.length === 0) {
this.classes = undefined
}
this.render = Translations.T(<any>json.render, translationKey + ".render") this.render = Translations.T(<any>json.render, translationKey + ".render")
this.question = Translations.T(json.question, translationKey + ".question") this.question = Translations.T(json.question, translationKey + ".question")

View file

@ -8,6 +8,7 @@
onMount(() => { onMount(() => {
const uiElem = typeof construct === "function" ? construct() : construct const uiElem = typeof construct === "function" ? construct() : construct
html = uiElem?.ConstructElement() html = uiElem?.ConstructElement()
if (html !== undefined) { if (html !== undefined) {
elem?.replaceWith(html) elem?.replaceWith(html)
} }

View file

@ -127,13 +127,6 @@
<button slot="cancel" class="items-center" on:click={() => (currentState = "start")}> <button slot="cancel" class="items-center" on:click={() => (currentState = "start")}>
<Tr t={t.cancel} /> <Tr t={t.cancel} />
</button> </button>
<XCircleIcon
slot="upper-right"
class="h-8 w-8 cursor-pointer"
on:click={() => {
currentState = "start"
}}
/>
<div slot="under-buttons"> <div slot="under-buttons">
{#if selectedTags !== undefined} {#if selectedTags !== undefined}

View file

@ -20,6 +20,7 @@ export class ShareLinkViz implements SpecialVisualization {
}, },
] ]
needsUrls = [] needsUrls = []
svelteBased = true
public constr( public constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
@ -52,6 +53,8 @@ export class ShareLinkViz implements SpecialVisualization {
} }
} }
return new SvelteUIElement(ShareButton, { generateShareData, text }) return new SvelteUIElement(ShareButton, { generateShareData, text }).SetClass(
"w-full h-full"
)
} }
} }

View file

@ -41,7 +41,7 @@
} }
let skippedQuestions = new UIEventSource<Set<string>>(new Set<string>()) let skippedQuestions = new UIEventSource<Set<string>>(new Set<string>())
let questionboxElem: HTMLBaseElement let questionboxElem: HTMLDivElement
let questionsToAsk = tags.map( let questionsToAsk = tags.map(
(tags) => { (tags) => {
const baseQuestions = (layer.tagRenderings ?? [])?.filter( const baseQuestions = (layer.tagRenderings ?? [])?.filter(
@ -49,7 +49,7 @@
) )
const questionsToAsk: TagRenderingConfig[] = [] const questionsToAsk: TagRenderingConfig[] = []
for (const baseQuestion of baseQuestions) { for (const baseQuestion of baseQuestions) {
if (skippedQuestions.data.has(baseQuestion.id) > 0) { if (skippedQuestions.data.has(baseQuestion.id)) {
continue continue
} }
if ( if (
@ -88,6 +88,7 @@
<div <div
bind:this={questionboxElem} bind:this={questionboxElem}
aria-live="polite"
class="marker-questionbox-root" class="marker-questionbox-root"
class:hidden={$questionsToAsk.length === 0 && skipped === 0 && answered === 0} class:hidden={$questionsToAsk.length === 0 && skipped === 0 && answered === 0}
> >

View file

@ -29,9 +29,9 @@
</script> </script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))} {#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
<div {id} class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}> <div {id} class={twMerge("link-underline inline-block w-full", config?.classes ?? "flex items-center", extraClasses)}>
{#if $trs.length === 1} {#if $trs.length === 1}
<TagRenderingMapping mapping={$trs[0]} {tags} {state} {selectedElement} {layer} /> <TagRenderingMapping clss={extraClasses} mapping={$trs[0]} {tags} {state} {selectedElement} {layer} />
{/if} {/if}
{#if $trs.length > 1} {#if $trs.length > 1}
<ul> <ul>

View file

@ -98,16 +98,6 @@
> >
<Tr t={Translations.t.general.cancel} /> <Tr t={Translations.t.general.cancel} />
</button> </button>
<button
slot="upper-right"
class="h-8 w-8 cursor-pointer border-none p-0"
use:ariaLabel={Translations.t.general.cancel}
on:click={() => {
editMode = false
}}
>
<XCircleIcon />
</button>
</TagRenderingQuestion> </TagRenderingQuestion>
{:else} {:else}
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2"> <div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">

View file

@ -12,7 +12,6 @@
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let layer: LayerConfig export let layer: LayerConfig
export let mapping: { export let mapping: {
readonly then: Translation readonly then: Translation
readonly searchTerms?: Record<string, string[]> readonly searchTerms?: Record<string, string[]>
@ -30,7 +29,7 @@
{#if mapping.icon !== undefined} {#if mapping.icon !== undefined}
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mx-2")} /> <Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-2")} />
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} /> <SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} />
</div> </div>
{:else if mapping.then !== undefined} {:else if mapping.then !== undefined}

View file

@ -83,7 +83,7 @@
</script> </script>
{#if $matchesTerm && !$mappingIsHidden} {#if $matchesTerm && !$mappingIsHidden}
<label class={twJoin("flex", mappingIsSelected && "checked")}> <label class={twJoin("flex gap-x-1", mappingIsSelected && "checked")}>
<slot /> <slot />
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} /> <TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} />
</label> </label>

View file

@ -151,7 +151,7 @@
$freeformInput, $freeformInput,
selectedMapping, selectedMapping,
checkedMappings, checkedMappings,
tags.data tags.data,
) )
} catch (e) { } catch (e) {
console.error("Could not calculate changeSpecification:", e) console.error("Could not calculate changeSpecification:", e)
@ -213,136 +213,53 @@
onDestroy( onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount numberOfCs = ud.csCount
}) }),
) )
} }
</script> </script>
{#if question !== undefined} {#if question !== undefined}
<form <div class="relative">
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
style="max-height: 75vh"
on:submit|preventDefault={() => onSave()}
>
<label class="neutral-label">
<div class="interactive sticky top-0 flex justify-between pt-1" style="z-index: 11">
<span class="font-bold">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</span>
<slot name="upper-right" />
</div>
{#if config.questionhint} <form
<div class="max-h-60 overflow-y-auto"> class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
<SpecialTranslation style="max-height: 75vh"
t={config.questionhint} on:submit|preventDefault={() => onSave()}
{tags} >
{state} <fieldset>
{layer}
feature={selectedElement}
/>
</div>
{/if}
{#if config.mappings?.length >= 8} <legend>
<div class="sticky flex w-full" aria-hidden="true"> <div class="interactive sticky top-0 flex justify-between pt-1 font-bold" style="z-index: 11">
<Search class="h-6 w-6" /> <SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
<input </div>
type="text"
bind:value={$searchTerm}
class="w-full"
use:placeholder={Translations.t.general.searchAnswer}
/>
</div>
{/if}
{#if config.freeform?.key && !(mappings?.length > 0)} {#if config.questionhint}
<!-- There are no options to choose from, simply show the input element: fill out the text field --> <div class="max-h-60 overflow-y-auto">
<FreeformInput <SpecialTranslation
{config} t={config.questionhint}
{tags} {tags}
{feedback} {state}
{unit} {layer}
{state} feature={selectedElement}
feature={selectedElement} />
value={freeformInput} </div>
on:submit={onSave} {/if}
/> </legend>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping --> {#if config.mappings?.length >= 8}
<div class="flex flex-col"> <div class="sticky flex w-full" aria-hidden="true">
{#each config.mappings as mapping, i (mapping.then)} <Search class="h-6 w-6" />
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices--> <input
<TagRenderingMappingInput type="text"
{mapping} bind:value={$searchTerm}
{tags} class="w-full"
{state} use:placeholder={Translations.t.general.searchAnswer}
{selectedElement} />
{layer} </div>
{searchTerm} {/if}
mappingIsSelected={selectedMapping === i}
> {#if config.freeform?.key && !(mappings?.length > 0)}
<input <!-- There are no options to choose from, simply show the input element: fill out the text field -->
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">
<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">
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput <FreeformInput
{config} {config}
{tags} {tags}
@ -353,39 +270,122 @@
value={freeformInput} value={freeformInput}
on:submit={onSave} on:submit={onSave}
/> />
</label> {: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>
{/if}
<LoginToggle {state}>
<Loading slot="loading" />
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
<Login slot="image" class="h-8 w-8" />
<Tr t={Translations.t.general.loginToStart} slot="message" />
</SubtleButton>
{#if $feedback !== undefined}
<div class="alert" aria-live="assertive" role="alert">
<Tr t={$feedback} />
</div>
{/if} {/if}
</div> <div
{/if} class="interactive sticky bottom-0 flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap"
</label> style="z-index: 11"
<LoginToggle {state}>
<Loading slot="loading" />
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
<Login slot="image" class="h-8 w-8" />
<Tr t={Translations.t.general.loginToStart} slot="message" />
</SubtleButton>
{#if $feedback !== undefined}
<div class="alert" aria-live="assertive" role="alert">
<Tr t={$feedback} />
</div>
{/if}
<div
class="interactive sticky bottom-0 flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap"
style="z-index: 11"
>
<!-- 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} /> <!-- TagRenderingQuestion-buttons -->
</button> <slot name="cancel" />
</slot> <slot name="save-button" {selectedTags}>
</div> <button
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging} on:click={onSave}
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
>
<Tr t={Translations.t.general.save} />
</button>
</slot>
</div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="flex flex-wrap justify-between"> <span class="flex flex-wrap justify-between">
<TagHint {state} tags={selectedTags} currentProperties={$tags} /> <TagHint {state} tags={selectedTags} currentProperties={$tags} />
<span class="flex flex-wrap"> <span class="flex flex-wrap">
@ -397,8 +397,12 @@
{/if} {/if}
</span> </span>
</span> </span>
{/if} {/if}
<slot name="under-buttons" /> <slot name="under-buttons" />
</LoginToggle> </LoginToggle>
</form> </fieldset>
</form>
</div>
{/if} {/if}

View file

@ -98,7 +98,7 @@ export interface SpecialVisualization {
readonly funcName: string readonly funcName: string
readonly docs: string | BaseUIElement readonly docs: string | BaseUIElement
readonly example?: string readonly example?: string
readonly needsUrls: string[] | ((args: string[]) => string) readonly needsUrls?: string[] | ((args: string[]) => string)
/** /**
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included

View file

@ -102,6 +102,7 @@ class NearbyImageVis implements SpecialVisualization {
"A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature"
funcName = "nearby_images" funcName = "nearby_images"
needsUrls = NearbyImagesSearch.apiUrls needsUrls = NearbyImagesSearch.apiUrls
svelteBased = true
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
@ -141,6 +142,7 @@ class StealViz implements SpecialVisualization {
}, },
] ]
needsUrls = [] needsUrls = []
svelteBased = true
constr(state: SpecialVisualizationState, featureTags, args) { constr(state: SpecialVisualizationState, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args const [featureIdKey, layerAndtagRenderingIds] = args
@ -213,6 +215,7 @@ export class QuestionViz implements SpecialVisualization {
doc: "One or more ';'-separated labels of questions which should _not_ be included", doc: "One or more ';'-separated labels of questions which should _not_ be included",
}, },
] ]
svelteBased = true
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
@ -236,7 +239,7 @@ export class QuestionViz implements SpecialVisualization {
state, state,
onlyForLabels: labels, onlyForLabels: labels,
notForLabels: blacklist, notForLabels: blacklist,
}) }).SetClass("w-full")
} }
} }
@ -437,7 +440,7 @@ export default class SpecialVisualizations {
funcName: "add_new_point", funcName: "add_new_point",
docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`", docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`",
args: [], args: [],
needsUrls: [],
constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement { constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement {
let [lon, lat] = GeoOperations.centerpointCoordinates(feature) let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(AddNewPoint, { return new SvelteUIElement(AddNewPoint, {
@ -449,7 +452,7 @@ export default class SpecialVisualizations {
{ {
funcName: "user_profile", funcName: "user_profile",
args: [], args: [],
needsUrls: [],
docs: "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'", docs: "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'",
constr(state: SpecialVisualizationState): BaseUIElement { constr(state: SpecialVisualizationState): BaseUIElement {
return new SvelteUIElement(UserProfile, { return new SvelteUIElement(UserProfile, {
@ -460,7 +463,6 @@ export default class SpecialVisualizations {
{ {
funcName: "language_picker", funcName: "language_picker",
args: [], args: [],
needsUrls: [],
docs: "A component to set the language of the user interface", docs: "A component to set the language of the user interface",
constr(state: SpecialVisualizationState): BaseUIElement { constr(state: SpecialVisualizationState): BaseUIElement {
return new SvelteUIElement(LanguagePicker, { return new SvelteUIElement(LanguagePicker, {
@ -477,6 +479,7 @@ export default class SpecialVisualizations {
args: [], args: [],
needsUrls: [Constants.osmAuthConfig.url], needsUrls: [Constants.osmAuthConfig.url],
docs: "Shows a button where the user can log out", docs: "Shows a button where the user can log out",
constr(state: SpecialVisualizationState): BaseUIElement { constr(state: SpecialVisualizationState): BaseUIElement {
return new SvelteUIElement(LogoutButton, { osmConnection: state.osmConnection }) return new SvelteUIElement(LogoutButton, { osmConnection: state.osmConnection })
}, },
@ -488,7 +491,7 @@ export default class SpecialVisualizations {
funcName: "split_button", funcName: "split_button",
docs: "Adds a button which allows to split a way", docs: "Adds a button which allows to split a way",
args: [], args: [],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>> tagSource: UIEventSource<Record<string, string>>
@ -509,7 +512,7 @@ export default class SpecialVisualizations {
funcName: "move_button", funcName: "move_button",
docs: "Adds a button which allows to move the object to another location. The config will be read from the layer config", docs: "Adds a button which allows to move the object to another location. The config will be read from the layer config",
args: [], args: [],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -532,7 +535,7 @@ export default class SpecialVisualizations {
funcName: "delete_button", funcName: "delete_button",
docs: "Adds a button which allows to delete the object at this location. The config will be read from the layer config", docs: "Adds a button which allows to delete the object at this location. The config will be read from the layer config",
args: [], args: [],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -597,6 +600,7 @@ export default class SpecialVisualizations {
}, },
], ],
needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls], needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls],
example: example:
"`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height", "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height",
constr: (_, tagsSource, args) => { constr: (_, tagsSource, args) => {
@ -650,7 +654,6 @@ export default class SpecialVisualizations {
funcName: "all_tags", funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging", docs: "Prints all key-value pairs of the object - used for debugging",
args: [], args: [],
needsUrls: [],
constr: (state, tags: UIEventSource<any>) => constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }), new SvelteUIElement(AllTagsPanel, { tags, state }),
}, },
@ -820,7 +823,7 @@ export default class SpecialVisualizations {
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__", doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
}, },
], ],
needsUrls: [],
example: example:
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`", "A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
constr: (state, tagSource: UIEventSource<any>, args) => { constr: (state, tagSource: UIEventSource<any>, args) => {
@ -848,14 +851,13 @@ export default class SpecialVisualizations {
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__", doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
}, },
], ],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig
): BaseUIElement { ): SvelteUIElement {
const keyToUse = args[0] const keyToUse = args[0]
const prefix = args[1] const prefix = args[1]
const postfix = args[2] const postfix = args[2]
@ -870,7 +872,7 @@ export default class SpecialVisualizations {
}, },
{ {
funcName: "canonical", funcName: "canonical",
needsUrls: [],
docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ", docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ",
example: example:
"If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...", "If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...",
@ -910,7 +912,7 @@ export default class SpecialVisualizations {
funcName: "export_as_geojson", funcName: "export_as_geojson",
docs: "Exports the selected feature as GeoJson-file", docs: "Exports the selected feature as GeoJson-file",
args: [], args: [],
needsUrls: [],
constr: (state, tagSource, tagsSource, feature, layer) => { constr: (state, tagSource, tagsSource, feature, layer) => {
const t = Translations.t.general.download const t = Translations.t.general.download
@ -942,7 +944,7 @@ export default class SpecialVisualizations {
funcName: "open_in_iD", funcName: "open_in_iD",
docs: "Opens the current view in the iD-editor", docs: "Opens the current view in the iD-editor",
args: [], args: [],
needsUrls: [],
constr: (state, feature) => { constr: (state, feature) => {
return new SvelteUIElement(OpenIdEditor, { return new SvelteUIElement(OpenIdEditor, {
mapProperties: state.mapProperties, mapProperties: state.mapProperties,
@ -964,7 +966,7 @@ export default class SpecialVisualizations {
funcName: "clear_location_history", funcName: "clear_location_history",
docs: "A button to remove the travelled track information from the device", docs: "A button to remove the travelled track information from the device",
args: [], args: [],
needsUrls: [],
constr: (state) => { constr: (state) => {
return new SubtleButton( return new SubtleButton(
Svg.delete_icon_svg().SetStyle("height: 1.5rem"), Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
@ -1023,6 +1025,7 @@ export default class SpecialVisualizations {
}, },
], ],
needsUrls: [Imgur.apiUrl], needsUrls: [Imgur.apiUrl],
constr: (state, tags, args) => { constr: (state, tags, args) => {
const id = tags.data[args[0] ?? "id"] const id = tags.data[args[0] ?? "id"]
tags = state.featureProperties.getStore(id) tags = state.featureProperties.getStore(id)
@ -1033,7 +1036,7 @@ export default class SpecialVisualizations {
{ {
funcName: "title", funcName: "title",
args: [], args: [],
needsUrls: [],
docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'", docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
example: example:
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.", "`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
@ -1145,6 +1148,7 @@ export default class SpecialVisualizations {
defaultValue: "mr_taskId", defaultValue: "mr_taskId",
}, },
], ],
constr: (state, tagsSource, args) => { constr: (state, tagsSource, args) => {
let [message, image, message_closed, statusToSet, maproulette_id_key] = args let [message, image, message_closed, statusToSet, maproulette_id_key] = args
if (image === "") { if (image === "") {
@ -1168,7 +1172,7 @@ export default class SpecialVisualizations {
funcName: "statistics", funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
args: [], args: [],
needsUrls: [],
constr: (state) => { constr: (state) => {
return new Combine( return new Combine(
state.layout.layers state.layout.layers
@ -1211,7 +1215,6 @@ export default class SpecialVisualizations {
required: true, required: true,
}, },
], ],
needsUrls: [],
constr(__, tags, args) { constr(__, tags, args) {
return new SvelteUIElement(SendEmail, { args, tags }) return new SvelteUIElement(SendEmail, { args, tags })
@ -1244,7 +1247,7 @@ export default class SpecialVisualizations {
doc: "If set, this text will be used as aria-label", doc: "If set, this text will be used as aria-label",
}, },
], ],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -1273,7 +1276,7 @@ export default class SpecialVisualizations {
{ {
funcName: "multi", funcName: "multi",
docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering", docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering",
needsUrls: [],
example: example:
"```json\n" + "```json\n" +
JSON.stringify( JSON.stringify(
@ -1327,7 +1330,7 @@ export default class SpecialVisualizations {
{ {
funcName: "translated", funcName: "translated",
docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes", docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes",
needsUrls: [],
args: [ args: [
{ {
name: "key", name: "key",
@ -1366,7 +1369,7 @@ export default class SpecialVisualizations {
required: true, required: true,
}, },
], ],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -1396,7 +1399,7 @@ export default class SpecialVisualizations {
{ {
funcName: "braced", funcName: "braced",
docs: "Show a literal text within braces", docs: "Show a literal text within braces",
needsUrls: [],
args: [ args: [
{ {
name: "text", name: "text",
@ -1417,7 +1420,7 @@ export default class SpecialVisualizations {
{ {
funcName: "tags", funcName: "tags",
docs: "Shows a (json of) tags in a human-readable way + links to the wiki", docs: "Shows a (json of) tags in a human-readable way + links to the wiki",
needsUrls: [],
args: [ args: [
{ {
name: "key", name: "key",
@ -1468,6 +1471,7 @@ export default class SpecialVisualizations {
], ],
docs: "Shows events that are happening based on a Giggity URL", docs: "Shows events that are happening based on a Giggity URL",
needsUrls: (args) => args[0], needsUrls: (args) => args[0],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -1481,7 +1485,7 @@ export default class SpecialVisualizations {
}, },
{ {
funcName: "gps_all_tags", funcName: "gps_all_tags",
needsUrls: [],
docs: "Shows the current tags of the GPS-representing object, used for debugging", docs: "Shows the current tags of the GPS-representing object, used for debugging",
args: [], args: [],
constr( constr(
@ -1507,9 +1511,10 @@ export default class SpecialVisualizations {
}, },
{ {
funcName: "favourite_status", funcName: "favourite_status",
needsUrls: [],
docs: "A button that allows a (logged in) contributor to mark a location as a favourite location", docs: "A button that allows a (logged in) contributor to mark a location as a favourite location",
args: [], args: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
@ -1527,7 +1532,7 @@ export default class SpecialVisualizations {
}, },
{ {
funcName: "favourite_icon", funcName: "favourite_icon",
needsUrls: [],
docs: "A small button that allows a (logged in) contributor to mark a location as a favourite location, sized to fit a title-icon", docs: "A small button that allows a (logged in) contributor to mark a location as a favourite location, sized to fit a title-icon",
args: [], args: [],
constr( constr(
@ -1542,13 +1547,13 @@ export default class SpecialVisualizations {
state, state,
layer, layer,
feature, feature,
}) }).SetClass("w-full h-full")
}, },
}, },
{ {
funcName: "direction_indicator", funcName: "direction_indicator",
args: [], args: [],
needsUrls: [],
docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object", docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object",
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
@ -1563,7 +1568,7 @@ export default class SpecialVisualizations {
{ {
funcName: "qr_code", funcName: "qr_code",
args: [], args: [],
needsUrls: [],
docs: "Generates a QR-code to share the selected object", docs: "Generates a QR-code to share the selected object",
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
@ -1608,7 +1613,7 @@ export default class SpecialVisualizations {
defaultValue: "_direction:centerpoint", defaultValue: "_direction:centerpoint",
}, },
], ],
needsUrls: [],
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,

View file

@ -1448,7 +1448,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
d.setUTCMinutes(0) d.setUTCMinutes(0)
} }
public static scrollIntoView(element: HTMLBaseElement) { public static scrollIntoView(element: HTMLBaseElement | HTMLDivElement) {
// Is the element completely in the view? // Is the element completely in the view?
const parentRect = Utils.findParentWithScrolling(element).getBoundingClientRect() const parentRect = Utils.findParentWithScrolling(element).getBoundingClientRect()
const elementRect = element.getBoundingClientRect() const elementRect = element.getBoundingClientRect()
@ -1680,7 +1680,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}) })
} }
private static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement { private static findParentWithScrolling(
element: HTMLBaseElement | HTMLDivElement
): HTMLBaseElement | HTMLDivElement {
// Check if the element itself has scrolling // Check if the element itself has scrolling
if (element.scrollHeight > element.clientHeight) { if (element.scrollHeight > element.clientHeight) {
return element return element