A11y: screen navigation improvements, see #1181

This commit is contained in:
Pieter Vander Vennet 2023-12-15 01:46:01 +01:00
parent 66369ef0b4
commit af4d9bb2bf
25 changed files with 483 additions and 325 deletions

View file

@ -336,6 +336,7 @@
"searchShort": "Search…",
"searching": "Searching…"
},
"searchAnswer": "Search an option…",
"share": "Share",
"sharescreen": {
"copiedToClipboard": "Link copied to clipboard",
@ -382,6 +383,20 @@
"uploadingChanges": "Uploading changes…",
"useSearch": "Use the search above to see presets",
"useSearchForMore": "Use the search function to search within {total} more values…",
"visualFeedback": {
"closestFeaturesAre": "Closest features are:",
"east": "Moving east",
"in": "Zooming in",
"islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.",
"locked": "View is now locked to your GPS-location, moving disabled.",
"navigation": "Use arrow keys to move the map, press space to select the closest feature",
"noCloseFeatures": "No features in view",
"north": "Moving north",
"out": "Zooming out",
"south": "Moving south",
"unlocked": "Moving enabled.",
"west": "Moving west"
},
"waitingForGeopermission": "Waiting for your permission to use the geolocation…",
"waitingForLocation": "Searching your current location…",
"weekdays": {
@ -449,6 +464,7 @@
"dontDelete": "Cancel",
"isDeleted": "Deleted",
"nearby": {
"close": "Collapse panel with nearby images",
"link": "This picture shows the object",
"noNearbyImages": "No nearby images were found",
"seeNearby": "Browse and link nearby pictures",

View file

@ -3400,6 +3400,18 @@
"question": "How wide is the gap between the cycleway and the road?",
"render": "The buffer besides this cycleway is {cycleway:buffer} m"
},
"incline": {
"mappings": {
"0": {
"then": "There is (probably) no incline here"
},
"1": {
"then": "This road has a slope"
}
},
"question": "Does {title()} have an incline?",
"render": "This road has an slope of {incline}"
},
"is lit?": {
"mappings": {
"0": {

View file

@ -1342,6 +1342,10 @@ video {
resize: both;
}
.list-none {
list-style-type: none;
}
.appearance-none {
-webkit-appearance: none;
appearance: none;
@ -2229,10 +2233,6 @@ body {
font-family: "Helvetica Neue", Arial, sans-serif;
}
.focusable {
/* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */
}
svg,
img {
box-sizing: content-box;

View file

@ -267,7 +267,7 @@ export default class UserRelatedState {
/**
* Initialize the 'amended preferences'.
* This is inherently a dirty and chaotic method, as it shoves many properties into this EventSourcd
* This is inherently a dirty and chaotic method, as it shoves many properties into this EventSource
* */
private initAmendedPrefs(
layout?: LayoutConfig,

View file

@ -1,7 +1,11 @@
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { BBox } from "../Logic/BBox"
import { RasterLayerPolygon } from "./RasterLayers"
import { B } from "vitest/dist/types-aac763a5"
export interface KeyNavigationEvent {
date: Date
key: "north" | "east" | "south" | "west" | "in" | "out" | "islocked" | "locked" | "unlocked"
}
export interface MapProperties {
readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number>
@ -14,7 +18,13 @@ export interface MapProperties {
readonly allowRotating: UIEventSource<true | boolean>
readonly lastClickLocation: Store<{ lon: number; lat: number }>
readonly allowZooming: UIEventSource<true | boolean>
readonly lastKeyNavigation: UIEventSource<number>
/**
* Triggered when the user navigated by using the keyboard.
* The callback might return 'true' if it wants to be unregistered
* @param f
*/
onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean): () => void
}
export interface ExportableMap {

View file

@ -128,6 +128,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
* All 'level'-tags that are available with the current features
*/
readonly floors: Store<string[]>
/**
* If true, the user interface will toggle some extra aids for people using screenreaders and keyboard navigation
* Triggered by navigating the map with arrows or by pressing 'space' or 'enter'
*/
public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false)
private readonly newPointDialog: FilteredLayer
constructor(layout: LayoutConfig) {
@ -372,6 +377,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
public focusOnMap() {
if (this.map.data) {
this.map.data.getCanvas().focus()
console.log("Focused on map")
return
}
this.map.addCallbackAndRunD((map) => {
@ -437,6 +443,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Various small methods that need to be called
*/
private miscSetup() {
this.mapProperties.onKeyNavigationEvent((keyEvent) => {
if (["north", "east", "south", "west"].indexOf(keyEvent.key) >= 0) {
this.visualFeedback.setData(true)
return true // Our job is done, unregister
}
})
this.userRelatedState.markLayoutAsVisited(this.layout)
this.selectedElement.addCallbackAndRunD((feature) => {
@ -460,7 +473,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
* @private
*/
private selectClosestAtCenter(i: number = 0) {
this.mapProperties.lastKeyNavigation.setData(Date.now() / 1000)
this.visualFeedback.setData(true)
const toSelect = this.closestFeatures.features.data[i]
if (!toSelect) {
return
@ -495,35 +508,30 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
)
this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => {
Hotkeys.RegisterHotkey(
{
nomod: " ",
onUp: true,
},
Translations.t.hotkeyDocumentation.selectItem,
() => this.selectClosestAtCenter(0)
)
Hotkeys.RegisterHotkey(
{
nomod: "Spacebar",
onUp: true,
},
Translations.t.hotkeyDocumentation.selectItem,
() => this.selectClosestAtCenter(0)
)
for (let i = 1; i < 9; i++) {
Hotkeys.RegisterHotkey(
{
nomod: "" + i,
onUp: true,
},
Translations.t.hotkeyDocumentation.selectItem,
() => this.selectClosestAtCenter(i - 1)
)
Hotkeys.RegisterHotkey(
{
nomod: " ",
onUp: true,
},
Translations.t.hotkeyDocumentation.selectItem,
() => {
if (this.selectedElement.data !== undefined) {
return false
}
this.selectClosestAtCenter(0)
}
return true // unregister
})
)
for (let i = 1; i < 9; i++) {
Hotkeys.RegisterHotkey(
{
nomod: "" + i,
onUp: true,
},
Translations.t.hotkeyDocumentation.selectItem,
() => this.selectClosestAtCenter(i - 1)
)
}
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
if (!enable) {

View file

@ -11,12 +11,6 @@
export let extraClasses = "p-4 md:p-6";
let mainContent: HTMLElement;
onMount(() => {
requestAnimationFrame(() => {
Utils.focusOnFocusableChild(mainContent);
});
});
</script>
@ -31,7 +25,7 @@
use:trapFocus
style="z-index: 21"
>
<div bind:this={mainContent} class="content normal-background" on:click|stopPropagation={() => {}}>
<div class="content normal-background" on:click|stopPropagation={() => {}}>
<div class="h-full rounded-xl">
<slot />
</div>

View file

@ -22,16 +22,13 @@ export default class Hotkeys {
}[]
>([])
private static textElementSelected(event: KeyboardEvent): boolean {
if (event.ctrlKey || event.altKey) {
// This is an event with a modifier-key, lets not ignore it
return false
}
if (event.key === "Escape") {
return false // Another not-printable character that should not be ignored
}
return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase())
}
/**
* Register a hotkey
* @param key
* @param documentation
* @param action the function to run. It might return 'false', indicating that it didn't do anything and gives control back to the default flow
* @constructor
*/
public static RegisterHotkey(
key: (
| {
@ -50,7 +47,7 @@ export default class Hotkeys {
onUp?: boolean
},
documentation: string | Translation,
action: () => void
action: () => void | false
) {
const type = key["onUp"] ? "keyup" : "keypress"
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
@ -69,8 +66,9 @@ export default class Hotkeys {
if (key["ctrl"] !== undefined) {
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.key === keycode) {
action()
event.preventDefault()
if (action() !== false) {
event.preventDefault()
}
}
})
} else if (key["shift"] !== undefined) {
@ -80,15 +78,17 @@ export default class Hotkeys {
return
}
if (event.shiftKey && event.key === keycode) {
action()
event.preventDefault()
if (action() !== false) {
event.preventDefault()
}
}
})
} else if (key["alt"] !== undefined) {
document.addEventListener(type, function (event) {
if (event.altKey && event.key === keycode) {
action()
event.preventDefault()
if (action() !== false) {
event.preventDefault()
}
}
})
} else if (key["nomod"] !== undefined) {
@ -98,8 +98,10 @@ export default class Hotkeys {
return
}
if (event.key === keycode) {
action()
event.preventDefault()
const result = action()
if (result !== false) {
event.preventDefault()
}
}
})
}
@ -113,6 +115,9 @@ export default class Hotkeys {
if (keycode.length == 1) {
keycode = keycode.toUpperCase()
}
if (keycode === " ") {
keycode = "Spacebar"
}
modifiers.push(keycode)
return <[string, string | Translation]>[modifiers.join("+"), documentation]
})
@ -139,4 +144,15 @@ export default class Hotkeys {
static generateDocumentationDynamic(): BaseUIElement {
return new VariableUiElement(Hotkeys._docs.map((_) => Hotkeys.generateDocumentation()))
}
private static textElementSelected(event: KeyboardEvent): boolean {
if (event.ctrlKey || event.altKey) {
// This is an event with a modifier-key, lets not ignore it
return false
}
if (event.key === "Escape") {
return false // Another not-printable character that should not be ignored
}
return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase())
}
}

View file

@ -1,10 +1,10 @@
<script lang="ts">
import Loading from "./Loading.svelte"
import type { OsmServiceState } from "../../Logic/Osm/OsmConnection"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Translation } from "../i18n/Translation"
import Translations from "../i18n/Translations"
import Tr from "./Tr.svelte"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
import Invalid from "../../assets/svg/Invalid.svelte"
@ -35,10 +35,12 @@
<Loading />
</slot>
{:else if $loadingStatus === "error"}
<div class="alert max-w-64 flex items-center">
<Invalid class="m-2 h-8 w-8 shrink-0" />
<Tr t={offlineModes[$apiState]} />
</div>
<slot name="error">
<div class="alert max-w-64 flex items-center">
<Invalid class="m-2 h-8 w-8 shrink-0" />
<Tr t={offlineModes[$apiState]} />
</div>
</slot>
{:else if $loadingStatus === "logged-in"}
<slot />
{:else if $loadingStatus === "not-attempted"}

View file

@ -1,38 +1,37 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Utils } from "../../Utils";
import { trapFocus } from 'trap-focus-svelte'
import { createEventDispatcher } from "svelte"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { trapFocus } from "trap-focus-svelte"
import { Utils } from "../../Utils"
/**
* The slotted element will be shown on the right side
*/
const dispatch = createEventDispatcher<{ close }>();
let mainContent: HTMLElement;
const dispatch = createEventDispatcher<{ close }>()
let mainContent: HTMLElement
onMount(() => {
window.setTimeout(
() => Utils.focusOnFocusableChild(mainContent), 250
);
});
</script>
<div
autofocus
bind:this={mainContent}
use:trapFocus
class="absolute top-0 right-0 h-screen w-full overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
class="absolute top-0 right-0 h-screen w-full overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12 normal-background flex flex-col"
role="dialog"
tabindex="-1"
aria-modal="true"
style="max-width: 100vw; max-height: 100vh"
use:trapFocus
>
<div class="normal-background m-0 flex flex-col">
<slot name="close-button">
<button
class="absolute right-10 top-10 h-8 w-8 cursor-pointer"
on:click={() => dispatch("close")}
>
<XCircleIcon />
</button>
</slot>
<slot />
<slot name="close-button">
<button
class="absolute right-10 top-10 h-8 w-8 cursor-pointer"
on:click={() => dispatch("close")}
>
<XCircleIcon />
</button>
</slot>
<div role="document" >
<slot />
</div>
</div>

View file

@ -30,7 +30,7 @@
}
</script>
<div class="tabbedgroup flex h-full w-full focusable">
<div class="tabbedgroup flex h-full w-full">
<TabGroup
class="flex h-full w-full flex-col"
defaultIndex={1}

View file

@ -11,6 +11,7 @@
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
import { createEventDispatcher, onDestroy } from "svelte"
import { placeholder } from "../../Utils/placeholder"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
export let bounds: UIEventSource<BBox>
@ -117,7 +118,5 @@
/>
{/if}
</form>
<div class="h-6 w-6 self-end" on:click={performSearch}>
<ToSvelte construct={Svg.search_svg} />
</div>
<SearchIcon class="h-6 w-6 self-end" aria-hidden="true" on:click={performSearch}/>
</div>

View file

@ -37,7 +37,7 @@
<Tr t={Translations.t.general.returnToTheMap} />
</button>
{:else}
<div class="flex h-full w-full flex-col gap-y-2 overflow-y-auto p-1 px-2 focusable" tabindex="-1">
<div class="flex h-full w-full flex-col gap-y-2 overflow-y-auto p-1 px-2" tabindex="-1">
{#each $knownTagRenderings as config (config.id)}
<TagRenderingEditable
{tags}

View file

@ -1,23 +1,26 @@
<script lang="ts">
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import SelectedElementTitle from "./SelectedElementTitle.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
export let state: SpecialVisualizationState
export let feature: Feature
export let i: number = undefined
let id = feature.properties.id
let tags = state.featureProperties.getStore(id)
let layer: LayerConfig = state.layout.getMatchingLayer(tags.data)
function select(){
function select() {
state.selectedElement.setData(undefined)
state.selectedLayer.setData(layer)
state.selectedElement.setData(feature)
}
</script>
<div on:click={() => select()} class="cursor-pointer">
<TagRenderingAnswer config={layer.title} selectedElement={feature} {state} {tags} {layer} />
</div>
<button class="cursor-pointer small" on:click={() => select()}>
{#if i !== undefined}
<span class="font-bold">{i + 1}.</span>
{/if}
<TagRenderingAnswer config={layer.title} {layer} selectedElement={feature} {state} {tags} />
</button>

View file

@ -0,0 +1,41 @@
<script lang="ts">
/**
* The visual feedback panel gives visual (and auditive) feedback on the main map view
*/
import Translations from "../i18n/Translations"
import ThemeViewState from "../../Models/ThemeViewState"
import Summary from "./Summary.svelte"
import Tr from "../Base/Tr.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import type { KeyNavigationEvent } from "../../Models/MapProperties"
export let state: ThemeViewState
let centerFeatures = state.closestFeatures.features
let lastAction: UIEventSource<KeyNavigationEvent> = new UIEventSource<KeyNavigationEvent>(undefined)
state.mapProperties.onKeyNavigationEvent((event) => {
lastAction.setData(event)
})
lastAction.stabilized(750).addCallbackAndRunD(_ => lastAction.setData(undefined))
</script>
<div aria-live="assertive" class=" interactive p-1" role="alert">
{#if $lastAction !== undefined}
<Tr t={Translations.t.general.visualFeedback[$lastAction.key]} />
{:else if $centerFeatures.length === 0}
<Tr t={Translations.t.general.visualFeedback.noCloseFeatures} />
{:else}
<div class="pointer-events-auto">
<Tr t={Translations.t.general.visualFeedback.closestFeaturesAre} />
<ol class="list-none">
{#each $centerFeatures as feat, i (feat.properties.id)}
<li class="flex">
<Summary {state} feature={feat} {i}/>
</li>
{/each}
</ol>
</div>
{/if}
</div>

View file

@ -1,21 +1,21 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource"
import type { OsmTags } from "../../Models/OsmFeature"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import LinkImageAction from "../../Logic/Osm/Actions/LinkImageAction"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag"
import { GeoOperations } from "../../Logic/GeoOperations"
import type { Feature } from "geojson"
import Translations from "../i18n/Translations"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import AttributedImage from "./AttributedImage.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import type { OsmTags } from "../../Models/OsmFeature"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import LinkImageAction from "../../Logic/Osm/Actions/LinkImageAction"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag"
import { GeoOperations } from "../../Logic/GeoOperations"
import type { Feature } from "geojson"
import Translations from "../i18n/Translations"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import AttributedImage from "./AttributedImage.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
export let tags: Store<OsmTags>
export let tags: UIEventSource<OsmTags>
export let lon: number
export let lat: number
export let state: SpecialVisualizationState
@ -29,13 +29,13 @@
const t = Translations.t.image.nearby
const c = [lon, lat]
const providedImage: ProvidedImage = {
url: image.thumbUrl ?? image.pictureUrl,
provider: AllImageProviders.byName(image.provider),
date: new Date(image.date),
id: Object.values(image.osmTags)[0]
url: image.thumbUrl ?? image.pictureUrl,
provider: AllImageProviders.byName(image.provider),
date: new Date(image.date),
id: Object.values(image.osmTags)[0],
}
let distance = Math.round(
GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c)
GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c),
)
$: {
@ -44,7 +44,7 @@
const url = image.osmTags[key]
if (isLinked) {
const action = new LinkImageAction(currentTags.id, key, url, tags, {
theme: tags.data._orig_theme ?? state.layout.id,
theme: tags.data._orig_theme ?? state.layout.id,
changeType: "link-image",
})
state.changes.applyAction(action)
@ -65,7 +65,7 @@
<div class="flex w-fit shrink-0 flex-col">
<div on:click={() => state.previewedImage.setData(providedImage)}>
<AttributedImage image={providedImage} imgClass="max-h-64 w-auto"/>
<AttributedImage image={providedImage} imgClass="max-h-64 w-auto" />
</div>
{#if linkable}
<label>

View file

@ -10,6 +10,7 @@
import { XCircleIcon } from "@babeard/svelte-heroicons/solid";
import Camera_plus from "../../assets/svg/Camera_plus.svelte";
import LoginToggle from "../Base/LoginToggle.svelte";
import { ariaLabel } from "../../Utils/ariaLabel"
export let tags: Store<OsmTags>;
export let state: SpecialVisualizationState;
@ -26,9 +27,11 @@
<LoginToggle {state}>
{#if expanded}
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable}>
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer}>
<button slot="corner"
class="h-6 w-6 cursor-pointer no-image-background p-0 border-none"
use:ariaLabel={t.close}
on:click={() => {
expanded = false
}}>

View file

@ -39,7 +39,6 @@
function handleOrientation(event) {
console.debug("Got gyro measurement")
gotMeasurement.setData(true)
// IF the phone is lying flat, then:
// alpha is the compass direction (but not absolute)

View file

@ -4,11 +4,12 @@ import { Map as MlMap, SourceSpecification } from "maplibre-gl"
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
import { BBox } from "../../Logic/BBox"
import { ExportableMap, MapProperties } from "../../Models/MapProperties"
import { ExportableMap, KeyNavigationEvent, MapProperties } from "../../Models/MapProperties"
import SvelteUIElement from "../Base/SvelteUIElement"
import MaplibreMap from "./MaplibreMap.svelte"
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
import * as htmltoimage from "html-to-image"
import { ALL } from "node:dns"
/**
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
@ -40,12 +41,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
readonly minzoom: UIEventSource<number>
readonly maxzoom: UIEventSource<number>
/**
* When was the last navigation by arrow keys?
* If set, this is a hint to use arrow compatibility
* Number of _seconds_ since epoch
* Functions that are called when one of those actions has happened
* @private
*/
readonly lastKeyNavigation: UIEventSource<number> = new UIEventSource<number>(undefined)
private _onKeyNavigation: ((event: KeyNavigationEvent) => void | boolean)[] = []
private readonly _maplibreMap: Store<MLMap>
/**
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
@ -132,13 +134,32 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
handleClick(e)
})
map.getContainer().addEventListener("keydown", (event) => {
if (
event.key === "ArrowRight" ||
event.key === "ArrowLeft" ||
event.key === "ArrowUp" ||
event.key === "ArrowDown"
) {
this.lastKeyNavigation.setData(Date.now() / 1000)
let locked: "islocked" = undefined
if (!this.allowMoving.data) {
locked = "islocked"
}
switch (event.key) {
case "ArrowUp":
this.pingKeycodeEvent(locked ?? "north")
break
case "ArrowRight":
this.pingKeycodeEvent(locked ?? "east")
break
case "ArrowDown":
this.pingKeycodeEvent(locked ?? "south")
break
case "ArrowLeft":
this.pingKeycodeEvent(locked ?? "west")
break
case "+":
this.pingKeycodeEvent("in")
break
case "=":
this.pingKeycodeEvent("in")
break
case "-":
this.pingKeycodeEvent("out")
break
}
})
})
@ -154,7 +175,10 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
})
this.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox))
this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving))
this.allowMoving.addCallbackAndRun((allowMoving) => {
self.setAllowMoving(allowMoving)
self.pingKeycodeEvent(allowMoving ? "unlocked" : "locked")
})
this.allowRotating.addCallbackAndRunD((allowRotating) =>
self.setAllowRotating(allowRotating)
)
@ -240,6 +264,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
})
}
public onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean) {
this._onKeyNavigation.push(f)
return () => {
this._onKeyNavigation.splice(this._onKeyNavigation.indexOf(f), 1)
}
}
public async exportAsPng(
rescaleIcons: number = 1,
progress: UIEventSource<{ current: number; total: number }> = undefined
@ -268,6 +299,24 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
return await MapLibreAdaptor.toBlob(drawOn)
}
private pingKeycodeEvent(
key: "north" | "east" | "south" | "west" | "in" | "out" | "islocked" | "locked" | "unlocked"
) {
const event = {
date: new Date(),
key: key,
}
for (let i = 0; i < this._onKeyNavigation.length; i++) {
const f = this._onKeyNavigation[i]
const unregister = f(event)
if (unregister === true) {
this._onKeyNavigation.splice(i, 1)
i--
}
}
}
/**
* Exports the background map and lines to PNG.
* Markers are _not_ rendered
@ -373,7 +422,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
for (const label of labels) {
if (isDisplayed(label)) {
console.log("Exporting label", label)
await this.drawElement(drawOn, <HTMLElement>label, rescaleIcons, pixelRatio)
}
}
@ -565,16 +613,17 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
map.rotateTo(0, { duration: 0 })
map.setPitch(0)
map.dragRotate.disable()
map.keyboard.disableRotation()
map.touchZoomRotate.disableRotation()
} else {
map.dragRotate.enable()
map.keyboard.enableRotation()
map.touchZoomRotate.enableRotation()
}
}
private setAllowMoving(allow: true | boolean | undefined) {
const map = this._maplibreMap.data
console.log("Setting 'allowMoving' to", allow)
if (!map) {
return
}

View file

@ -6,6 +6,8 @@
import { get, writable } from "svelte/store"
import { AvailableRasterLayers } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
import { ariaLabel } from "../../Utils/ariaLabel"
import Translations from "../i18n/Translations"
/**
* The 'MaplibreMap' maps various event sources onto MapLibre.
@ -19,7 +21,6 @@
let container: HTMLElement
export let attribution = false
export let center: { lng: number; lat: number } | Readable<{ lng: number; lat: number }> =
writable({ lng: 0, lat: 0 })
export let zoom: Readable<number> = writable(1)
@ -49,6 +50,9 @@
})
_map.on("load", function() {
_map.resize()
const canvas = _map.getCanvas()
ariaLabel(canvas, Translations.t.general.visualFeedback.navigation)
canvas.role="application"
})
map.set(_map)
})
@ -57,6 +61,8 @@
if (_map) _map.remove()
map = null
})
</script>
<svelte:head>

View file

@ -1,41 +1,42 @@
<script lang="ts">
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
import type { Feature } from "geojson";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import TagRenderingAnswer from "./TagRenderingAnswer.svelte";
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
import { onDestroy } from "svelte";
import Tr from "../../Base/Tr.svelte";
import Translations from "../../i18n/Translations.js";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import { Utils } from "../../../Utils";
import { twMerge } from "tailwind-merge";
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
import { onDestroy } from "svelte"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../../Utils"
import { twMerge } from "tailwind-merge"
import { ariaLabel } from "../../../Utils/ariaLabel"
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let selectedElement: Feature | undefined;
export let state: SpecialVisualizationState;
export let layer: LayerConfig = undefined;
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature | undefined
export let state: SpecialVisualizationState
export let layer: LayerConfig = undefined
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge;
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge
export let highlightedRendering: UIEventSource<string> = undefined;
export let clss;
export let highlightedRendering: UIEventSource<string> = undefined
export let clss
/**
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
*/
export let editMode = !config.IsKnown(tags.data); // || showQuestionIfUnknown;
export let editMode = !config.IsKnown(tags.data) // || showQuestionIfUnknown;
if (tags) {
onDestroy(
tags.addCallbackD((tags) => {
editMode = !config.IsKnown(tags);
})
);
editMode = !config.IsKnown(tags)
}),
)
}
let htmlElem: HTMLDivElement;
let htmlElem: HTMLDivElement
$: {
if (editMode && htmlElem !== undefined && config.IsKnown(tags)) {
// EditMode switched to true yet the answer is already known, so the person wants to make a change
@ -43,43 +44,44 @@
// Some delay is applied to give Svelte the time to render the _question_
window.setTimeout(() => {
Utils.scrollIntoView(<any>htmlElem);
}, 50);
Utils.scrollIntoView(<any>htmlElem)
}, 50)
}
}
const _htmlElement = new UIEventSource<HTMLElement>(undefined);
$: _htmlElement.setData(htmlElem);
const _htmlElement = new UIEventSource<HTMLElement>(undefined)
$: _htmlElement.setData(htmlElem)
function setHighlighting() {
if (highlightedRendering === undefined) {
return;
return
}
if (htmlElem === undefined) {
return;
return
}
const highlighted = highlightedRendering.data;
const highlighted = highlightedRendering.data
if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow");
htmlElem.tabIndex = "-1";
console.log("Scrolling to", htmlElem);
htmlElem.scrollIntoView({ behavior: "smooth" });
Utils.focusOnFocusableChild(htmlElem);
htmlElem.classList.add("glowing-shadow")
htmlElem.tabIndex = -1
console.log("Scrolling to", htmlElem)
htmlElem.scrollIntoView({ behavior: "smooth" })
Utils.focusOnFocusableChild(htmlElem)
} else {
htmlElem.classList.remove("glowing-shadow");
htmlElem.classList.remove("glowing-shadow")
}
}
if (highlightedRendering) {
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()));
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()));
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
}
let answerId = "answer-"+Utils.randomString(5)
</script>
<div bind:this={htmlElem} class={twMerge(clss, "tr-"+config.id)}>
{#if config.question && (!editingEnabled || $editingEnabled)}
{#if editMode}
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}>
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer} on:saved={() => editMode = false}>
<button
slot="cancel"
class="secondary"
@ -91,6 +93,7 @@
</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
}}>
@ -99,12 +102,15 @@
</TagRenderingQuestion>
{:else}
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
<div id={answerId}>
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
</div>
<button
on:click={() => {
editMode = true
}}
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
aria-labelledby={answerId}
>
<PencilAltIcon />
</button>

View file

@ -1,59 +1,60 @@
<script lang="ts">
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import Tr from "../../Base/Tr.svelte";
import type { Feature } from "geojson";
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig";
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
import FreeformInput from "./FreeformInput.svelte";
import Translations from "../../i18n/Translations.js";
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
import { createEventDispatcher, onDestroy } from "svelte";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import SpecialTranslation from "./SpecialTranslation.svelte";
import TagHint from "../TagHint.svelte";
import LoginToggle from "../../Base/LoginToggle.svelte";
import SubtleButton from "../../Base/SubtleButton.svelte";
import Loading from "../../Base/Loading.svelte";
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte";
import { Translation } from "../../i18n/Translation";
import Constants from "../../../Models/Constants";
import { Unit } from "../../../Models/Unit";
import UserRelatedState from "../../../Logic/State/UserRelatedState";
import { twJoin } from "tailwind-merge";
import { TagUtils } from "../../../Logic/Tags/TagUtils";
import Search from "../../../assets/svg/Search.svelte";
import Login from "../../../assets/svg/Login.svelte";
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import Tr from "../../Base/Tr.svelte"
import type { Feature } from "geojson"
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import FreeformInput from "./FreeformInput.svelte"
import Translations from "../../i18n/Translations.js"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import { createEventDispatcher, onDestroy } from "svelte"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import SpecialTranslation from "./SpecialTranslation.svelte"
import TagHint from "../TagHint.svelte"
import LoginToggle from "../../Base/LoginToggle.svelte"
import SubtleButton from "../../Base/SubtleButton.svelte"
import Loading from "../../Base/Loading.svelte"
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"
import { Translation } from "../../i18n/Translation"
import Constants from "../../../Models/Constants"
import { Unit } from "../../../Models/Unit"
import UserRelatedState from "../../../Logic/State/UserRelatedState"
import { twJoin } from "tailwind-merge"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import Search from "../../../assets/svg/Search.svelte"
import Login from "../../../assets/svg/Login.svelte"
import { placeholder } from "../../../Utils/placeholder"
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 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
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key));
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]);
let selectedMapping: number = undefined;
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
let selectedMapping: number = undefined
/**
* A list of booleans, used if multiAnswer is set
*/
let checkedMappings: boolean[];
let checkedMappings: boolean[]
let mappings: Mapping[] = config?.mappings;
let searchTerm: UIEventSource<string> = new UIEventSource("");
let mappings: Mapping[] = config?.mappings
let searchTerm: UIEventSource<string> = new UIEventSource("")
let dispatch = createEventDispatcher<{
saved: {
config: TagRenderingConfig
applied: TagsFilter
}
}>();
}>()
/**
* Prepares and fills the checkedMappings
@ -61,12 +62,12 @@
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
mappings = confg.mappings?.filter((m) => {
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer;
return !m.hideInAnswer
}
return !m.hideInAnswer.matchesProperties(tgs);
});
return !m.hideInAnswer.matchesProperties(tgs)
})
// We received a new config -> reinit
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key));
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
if (
confg.mappings?.length > 0 &&
@ -74,55 +75,55 @@
(checkedMappings === undefined ||
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
) {
const seenFreeforms = [];
const seenFreeforms = []
// Initial setup of the mappings
checkedMappings = [
...confg.mappings.map((mapping) => {
if(mapping.hideInAnswer === true){
if (mapping.hideInAnswer === true) {
return false
}
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs);
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs)
if (matches && confg.freeform) {
const newProps = TagUtils.changeAsProperties(mapping.if.asChange({}));
seenFreeforms.push(newProps[confg.freeform.key]);
const newProps = TagUtils.changeAsProperties(mapping.if.asChange({}))
seenFreeforms.push(newProps[confg.freeform.key])
}
return matches;
})
];
return matches
}),
]
if (tgs !== undefined && confg.freeform) {
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? [];
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []
for (const seenFreeform of seenFreeforms) {
if (!seenFreeform) {
continue;
continue
}
const index = unseenFreeformValues.indexOf(seenFreeform);
const index = unseenFreeformValues.indexOf(seenFreeform)
if (index < 0) {
continue;
continue
}
unseenFreeformValues.splice(index, 1);
unseenFreeformValues.splice(index, 1)
}
// TODO this has _to much_ values
freeformInput.setData(unseenFreeformValues.join(";"));
checkedMappings.push(unseenFreeformValues.length > 0);
freeformInput.setData(unseenFreeformValues.join(";"))
checkedMappings.push(unseenFreeformValues.length > 0)
}
}
if (confg.freeform?.key) {
if (!confg.multiAnswer) {
// Somehow, setting multi-answer freeform values is broken if this is not set
freeformInput.setData(tgs[confg.freeform.key]);
freeformInput.setData(tgs[confg.freeform.key])
}
} else {
freeformInput.setData(undefined);
freeformInput.setData(undefined)
}
feedback.setData(undefined);
feedback.setData(undefined)
}
$: {
// Even though 'config' is not declared as a store, Svelte uses it as one to update the component
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
initialize($tags, config);
initialize($tags, config)
}
$: {
@ -131,54 +132,55 @@
$freeformInput,
selectedMapping,
checkedMappings,
tags.data
);
tags.data,
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e);
selectedTags = undefined;
console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined
}
}
function onSave() {
if (selectedTags === undefined) {
return;
return
}
if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) {
/**
* This is a special, priviliged layer.
* We simply apply the tags onto the records
*/
const kv = selectedTags.asChange(tags.data);
const kv = selectedTags.asChange(tags.data)
for (const { k, v } of kv) {
if (v === undefined || v === "") {
delete tags.data[k];
if (v === undefined) {
// Note: we _only_ delete if it is undefined. We _leave_ the empty string and assign it, so that data consumers get correct information
delete tags.data[k]
} else {
tags.data[k] = v
}
feedback.setData(undefined);
feedback.setData(undefined)
}
tags.ping()
return;
return
}
dispatch("saved", { config, applied: selectedTags });
dispatch("saved", { config, applied: selectedTags })
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
theme: tags.data["_orig_theme"] ?? state.layout.id,
changeType: "answer"
});
freeformInput.setData(undefined);
selectedMapping = undefined;
selectedTags = undefined;
changeType: "answer",
})
freeformInput.setData(undefined)
selectedMapping = undefined
selectedTags = undefined
change
.CreateChangeDescriptions()
.then((changes) => state.changes.applyChanges(changes))
.catch(console.error);
.catch(console.error)
}
function onInputKeypress(e: Event) {
function onInputKeypress(e: KeyboardEvent) {
if (e.key === "Enter") {
onSave();
onSave()
}
}
@ -188,28 +190,28 @@
$freeformInput,
selectedMapping,
checkedMappings,
tags.data
);
tags.data,
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e);
selectedTags = undefined;
console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined
}
}
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false);
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
let featureSwitchIsDebugging =
state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false);
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined);
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0;
let question = config.question;
$: question = config.question;
state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined)
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0
let question = config.question
$: question = config.question
if (state?.osmConnection) {
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount;
})
);
numberOfCs = ud.csCount
}),
)
}
</script>
@ -219,7 +221,7 @@
style="max-height: 75vh"
>
<div class="sticky top-0 interactive pt-1 flex justify-between" style="z-index: 11">
<span class="font-bold">
<span class="font-bold" aria-live="assertive">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</span>
<slot name="upper-right" />
@ -238,9 +240,10 @@
{/if}
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full">
<div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" />
<input type="text" bind:value={$searchTerm} class="w-full" />
<input type="text" bind:value={$searchTerm} class="w-full"
use:placeholder={Translations.t.general.searchAnswer} />
</div>
{/if}
@ -351,7 +354,7 @@
<Tr t={Translations.t.general.loginToStart} slot="message" />
</SubtleButton>
{#if $feedback !== undefined}
<div class="alert">
<div class="alert" aria-live="assertive" role="alert">
<Tr t={$feedback} />
</div>
{/if}

View file

@ -48,7 +48,6 @@
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
import Cross from "../assets/svg/Cross.svelte"
import Summary from "./BigComponents/Summary.svelte"
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
import Mastodon from "../assets/svg/Mastodon.svelte"
import Bug from "../assets/svg/Bug.svelte"
@ -64,7 +63,7 @@
import Share from "../assets/svg/Share.svelte"
import Favourites from "./Favourites/Favourites.svelte"
import ImageOperations from "./Image/ImageOperations.svelte"
import { ariaLabel } from "../Utils/ariaLabel"
import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte"
export let state: ThemeViewState
let layout = state.layout
@ -92,9 +91,7 @@
let currentZoom = state.mapProperties.zoom
let showCrosshair = state.userRelatedState.showCrosshair
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation
let centerFeatures = state.closestFeatures.features
let arrowKeysWereUsed = state.visualFeedback
let mapproperties: MapProperties = state.mapProperties
let featureSwitches: FeatureSwitchState = state.featureSwitches
@ -111,13 +108,13 @@
let previewedImage = state.previewedImage
let geolocationControl = new GeolocationControl(state.geolocation, mapproperties, state.lastGeolocationRequestMoment)
function forwardEventToMap(e: KeyboardEvent) {
const mlmap = state.map.data
if(!mlmap){
if (!mlmap) {
return
}
if(!mlmap.keyboard.isEnabled()){
if (!mlmap.keyboard.isEnabled()) {
return
}
const animation = mlmap.keyboard?.keydown(e)
@ -135,14 +132,14 @@
<div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2">
<Geosearch
bounds={state.mapProperties.bounds}
on:searchCompleted={() => {state.map?.data?.getCanvas()?.focus()}}
perLayer={state.perLayer}
selectedElement={state.selectedElement}
on:searchCompleted={() => {state.map?.data?.getCanvas()?.focus()}}
/>
</div>
</If>
<div class="float-left m-1 flex flex-col sm:mt-2">
<MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)}
<MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)}
on:keydown={forwardEventToMap}>
<div class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 md:mx-2">
<img class="mr-0.5 block h-6 w-6 sm:mr-1 md:mr-2 md:h-8 md:w-8" src={layout.icon} />
@ -152,9 +149,9 @@
</div>
</MapControlButton>
<MapControlButton
on:click={() => state.guistate.menuIsOpened.setData(true)}
on:keydown={forwardEventToMap}
arialabel={Translations.t.general.labels.menu}
on:click={() => state.guistate.menuIsOpened.setData(true)}
on:keydown={forwardEventToMap}
>
<MenuIcon class="h-8 w-8 cursor-pointer" />
</MapControlButton>
@ -162,7 +159,7 @@
<MapControlButton
on:click={() => {
selectedElement.setData(state.currentView.features?.data?.[0])
}}
}}
on:keydown={forwardEventToMap}
>
<ToSvelte
@ -211,8 +208,9 @@
<div class="flex">
<!-- bottom left elements -->
<If condition={state.featureSwitches.featureSwitchFilter}>
<MapControlButton on:click={() => state.guistate.openFilterView()} on:keydown={forwardEventToMap}
arialabel={Translations.t.general.labels.filter}
<MapControlButton arialabel={Translations.t.general.labels.filter}
on:click={() => state.guistate.openFilterView()}
on:keydown={forwardEventToMap}
>
<Filter class="h-6 w-6" />
</MapControlButton>
@ -231,17 +229,11 @@
</a>
</div>
</div>
<If condition={state.visualFeedback}>
<VisualFeedbackPanel {state} />
</If>
{#if $arrowKeysWereUsed !== undefined && $centerFeatures?.length > 0}
<div class="pointer-events-auto interactive p-1">
{#each $centerFeatures as feat, i (feat.properties.id)}
<div class="flex">
<b>{i + 1}.</b>
<Summary {state} feature={feat} />
</div>
{/each}
</div>
{/if}
<div class="flex flex-col items-end">
<!-- bottom right elements -->
<If condition={state.floors.map((f) => f.length > 1)}>
@ -253,20 +245,22 @@
/>
</div>
</If>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z + 1)}
<MapControlButton arialabel={Translations.t.general.labels.zoomIn}
on:click={() => mapproperties.zoom.update((z) => z + 1)}
on:keydown={forwardEventToMap}
arialabel={Translations.t.general.labels.zoomIn}
>
<Plus class="h-8 w-8" />
</MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z - 1)} on:keydown={forwardEventToMap}
arialabel={Translations.t.general.labels.zoomOut}
<MapControlButton arialabel={Translations.t.general.labels.zoomOut}
on:click={() => mapproperties.zoom.update((z) => z - 1)}
on:keydown={forwardEventToMap}
>
<Min class="h-8 w-8" />
</MapControlButton>
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton on:keydown={forwardEventToMap} on:click={() => geolocationControl.handleClick()}
arialabel={Translations.t.general.labels.jumpToLocation}
<MapControlButton arialabel={Translations.t.general.labels.jumpToLocation}
on:click={() => geolocationControl.handleClick()}
on:keydown={forwardEventToMap}
>
<ToSvelte
construct={geolocationControl.SetClass("block w-8 h-8")}
@ -277,14 +271,16 @@
</div>
</div>
<LoginToggle ignoreLoading={true} {state}>
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $arrowKeysWereUsed !== undefined}
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $arrowKeysWereUsed}
<div
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center"
>
<Cross class="h-4 w-4" />
</div>
{/if}
<svelte:fragment slot="error" /> <!-- Add in an empty container to remove errors -->
</LoginToggle>
<If condition={state.previewedImage.map(i => i!==undefined)}>
@ -322,7 +318,7 @@
selectedElement.setData(undefined)
}}
>
<div class="h-full w-full flex focusable">
<div class="h-full w-full flex">
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
</div>
</FloatOver>
@ -410,7 +406,7 @@
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
}}
>
<div class="h-full p-2 focusable">
<div class="h-full p-2">
<RasterLayerOverview
{availableLayers}
map={state.map}

View file

@ -69,10 +69,6 @@ body {
color: var(--foreground-color);
font-family: "Helvetica Neue", Arial, sans-serif;
}
.focusable {
/* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */
}
svg,
img {

View file

@ -37,7 +37,7 @@
<!-- THEME-SPECIFIC-END-->
</head>
<body>
<body tabindex="-1">
<div class="h-screen" id="maindiv">
@ -62,7 +62,7 @@
<div class="flex justify-between items-start w-full">
<!-- IMAGE-START -->
<img class="p-8 h-32 w-32 self-start" src="./assets/svg/add.svg">
<img aria-hidden="true" class="p-8 h-32 w-32 self-start" src="./assets/svg/add.svg">
<!-- IMAGE-END -->
<div class="h-min subtle">
Version
@ -72,7 +72,7 @@
</div>
</div>
</div>
<div id="belowmap" class="absolute top-0 left-0 -z-10">Below</div>
<div aria-hidden="true" id="belowmap" class="absolute top-0 left-0 -z-10">Below</div>
<script src="./src/UI/RemoveOtherLanguages.js"></script>
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
<script defer src="./src/index.ts" type="module"></script>