A11y: screen navigation improvements, see #1181
This commit is contained in:
parent
66369ef0b4
commit
af4d9bb2bf
25 changed files with 483 additions and 325 deletions
|
@ -336,6 +336,7 @@
|
||||||
"searchShort": "Search…",
|
"searchShort": "Search…",
|
||||||
"searching": "Searching…"
|
"searching": "Searching…"
|
||||||
},
|
},
|
||||||
|
"searchAnswer": "Search an option…",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"sharescreen": {
|
"sharescreen": {
|
||||||
"copiedToClipboard": "Link copied to clipboard",
|
"copiedToClipboard": "Link copied to clipboard",
|
||||||
|
@ -382,6 +383,20 @@
|
||||||
"uploadingChanges": "Uploading changes…",
|
"uploadingChanges": "Uploading changes…",
|
||||||
"useSearch": "Use the search above to see presets",
|
"useSearch": "Use the search above to see presets",
|
||||||
"useSearchForMore": "Use the search function to search within {total} more values…",
|
"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…",
|
"waitingForGeopermission": "Waiting for your permission to use the geolocation…",
|
||||||
"waitingForLocation": "Searching your current location…",
|
"waitingForLocation": "Searching your current location…",
|
||||||
"weekdays": {
|
"weekdays": {
|
||||||
|
@ -449,6 +464,7 @@
|
||||||
"dontDelete": "Cancel",
|
"dontDelete": "Cancel",
|
||||||
"isDeleted": "Deleted",
|
"isDeleted": "Deleted",
|
||||||
"nearby": {
|
"nearby": {
|
||||||
|
"close": "Collapse panel with nearby images",
|
||||||
"link": "This picture shows the object",
|
"link": "This picture shows the object",
|
||||||
"noNearbyImages": "No nearby images were found",
|
"noNearbyImages": "No nearby images were found",
|
||||||
"seeNearby": "Browse and link nearby pictures",
|
"seeNearby": "Browse and link nearby pictures",
|
||||||
|
|
|
@ -3400,6 +3400,18 @@
|
||||||
"question": "How wide is the gap between the cycleway and the road?",
|
"question": "How wide is the gap between the cycleway and the road?",
|
||||||
"render": "The buffer besides this cycleway is {cycleway:buffer} m"
|
"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?": {
|
"is lit?": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"0": {
|
"0": {
|
||||||
|
|
|
@ -1342,6 +1342,10 @@ video {
|
||||||
resize: both;
|
resize: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-none {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
.appearance-none {
|
.appearance-none {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
@ -2229,10 +2233,6 @@ body {
|
||||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
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,
|
svg,
|
||||||
img {
|
img {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
|
|
@ -267,7 +267,7 @@ export default class UserRelatedState {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the 'amended preferences'.
|
* 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(
|
private initAmendedPrefs(
|
||||||
layout?: LayoutConfig,
|
layout?: LayoutConfig,
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import { BBox } from "../Logic/BBox"
|
import { BBox } from "../Logic/BBox"
|
||||||
import { RasterLayerPolygon } from "./RasterLayers"
|
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 {
|
export interface MapProperties {
|
||||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||||
readonly zoom: UIEventSource<number>
|
readonly zoom: UIEventSource<number>
|
||||||
|
@ -14,7 +18,13 @@ export interface MapProperties {
|
||||||
readonly allowRotating: UIEventSource<true | boolean>
|
readonly allowRotating: UIEventSource<true | boolean>
|
||||||
readonly lastClickLocation: Store<{ lon: number; lat: number }>
|
readonly lastClickLocation: Store<{ lon: number; lat: number }>
|
||||||
readonly allowZooming: UIEventSource<true | boolean>
|
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 {
|
export interface ExportableMap {
|
||||||
|
|
|
@ -128,6 +128,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
* All 'level'-tags that are available with the current features
|
* All 'level'-tags that are available with the current features
|
||||||
*/
|
*/
|
||||||
readonly floors: Store<string[]>
|
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
|
private readonly newPointDialog: FilteredLayer
|
||||||
|
|
||||||
constructor(layout: LayoutConfig) {
|
constructor(layout: LayoutConfig) {
|
||||||
|
@ -372,6 +377,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
public focusOnMap() {
|
public focusOnMap() {
|
||||||
if (this.map.data) {
|
if (this.map.data) {
|
||||||
this.map.data.getCanvas().focus()
|
this.map.data.getCanvas().focus()
|
||||||
|
console.log("Focused on map")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.map.addCallbackAndRunD((map) => {
|
this.map.addCallbackAndRunD((map) => {
|
||||||
|
@ -437,6 +443,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
* Various small methods that need to be called
|
* Various small methods that need to be called
|
||||||
*/
|
*/
|
||||||
private miscSetup() {
|
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.userRelatedState.markLayoutAsVisited(this.layout)
|
||||||
|
|
||||||
this.selectedElement.addCallbackAndRunD((feature) => {
|
this.selectedElement.addCallbackAndRunD((feature) => {
|
||||||
|
@ -460,7 +473,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private selectClosestAtCenter(i: number = 0) {
|
private selectClosestAtCenter(i: number = 0) {
|
||||||
this.mapProperties.lastKeyNavigation.setData(Date.now() / 1000)
|
this.visualFeedback.setData(true)
|
||||||
const toSelect = this.closestFeatures.features.data[i]
|
const toSelect = this.closestFeatures.features.data[i]
|
||||||
if (!toSelect) {
|
if (!toSelect) {
|
||||||
return
|
return
|
||||||
|
@ -495,23 +508,20 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => {
|
|
||||||
Hotkeys.RegisterHotkey(
|
Hotkeys.RegisterHotkey(
|
||||||
{
|
{
|
||||||
nomod: " ",
|
nomod: " ",
|
||||||
onUp: true,
|
onUp: true,
|
||||||
},
|
},
|
||||||
Translations.t.hotkeyDocumentation.selectItem,
|
Translations.t.hotkeyDocumentation.selectItem,
|
||||||
() => this.selectClosestAtCenter(0)
|
() => {
|
||||||
)
|
if (this.selectedElement.data !== undefined) {
|
||||||
Hotkeys.RegisterHotkey(
|
return false
|
||||||
{
|
}
|
||||||
nomod: "Spacebar",
|
this.selectClosestAtCenter(0)
|
||||||
onUp: true,
|
}
|
||||||
},
|
|
||||||
Translations.t.hotkeyDocumentation.selectItem,
|
|
||||||
() => this.selectClosestAtCenter(0)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for (let i = 1; i < 9; i++) {
|
for (let i = 1; i < 9; i++) {
|
||||||
Hotkeys.RegisterHotkey(
|
Hotkeys.RegisterHotkey(
|
||||||
{
|
{
|
||||||
|
@ -522,8 +532,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
() => this.selectClosestAtCenter(i - 1)
|
() => this.selectClosestAtCenter(i - 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return true // unregister
|
|
||||||
})
|
|
||||||
|
|
||||||
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
|
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
|
||||||
if (!enable) {
|
if (!enable) {
|
||||||
|
|
|
@ -11,12 +11,6 @@
|
||||||
|
|
||||||
export let extraClasses = "p-4 md:p-6";
|
export let extraClasses = "p-4 md:p-6";
|
||||||
|
|
||||||
let mainContent: HTMLElement;
|
|
||||||
onMount(() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
Utils.focusOnFocusableChild(mainContent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -31,7 +25,7 @@
|
||||||
use:trapFocus
|
use:trapFocus
|
||||||
style="z-index: 21"
|
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">
|
<div class="h-full rounded-xl">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,16 +22,13 @@ export default class Hotkeys {
|
||||||
}[]
|
}[]
|
||||||
>([])
|
>([])
|
||||||
|
|
||||||
private static textElementSelected(event: KeyboardEvent): boolean {
|
/**
|
||||||
if (event.ctrlKey || event.altKey) {
|
* Register a hotkey
|
||||||
// This is an event with a modifier-key, lets not ignore it
|
* @param key
|
||||||
return false
|
* @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
|
||||||
if (event.key === "Escape") {
|
* @constructor
|
||||||
return false // Another not-printable character that should not be ignored
|
*/
|
||||||
}
|
|
||||||
return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase())
|
|
||||||
}
|
|
||||||
public static RegisterHotkey(
|
public static RegisterHotkey(
|
||||||
key: (
|
key: (
|
||||||
| {
|
| {
|
||||||
|
@ -50,7 +47,7 @@ export default class Hotkeys {
|
||||||
onUp?: boolean
|
onUp?: boolean
|
||||||
},
|
},
|
||||||
documentation: string | Translation,
|
documentation: string | Translation,
|
||||||
action: () => void
|
action: () => void | false
|
||||||
) {
|
) {
|
||||||
const type = key["onUp"] ? "keyup" : "keypress"
|
const type = key["onUp"] ? "keyup" : "keypress"
|
||||||
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
|
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
|
||||||
|
@ -69,9 +66,10 @@ export default class Hotkeys {
|
||||||
if (key["ctrl"] !== undefined) {
|
if (key["ctrl"] !== undefined) {
|
||||||
document.addEventListener("keydown", function (event) {
|
document.addEventListener("keydown", function (event) {
|
||||||
if (event.ctrlKey && event.key === keycode) {
|
if (event.ctrlKey && event.key === keycode) {
|
||||||
action()
|
if (action() !== false) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else if (key["shift"] !== undefined) {
|
} else if (key["shift"] !== undefined) {
|
||||||
document.addEventListener(type, function (event) {
|
document.addEventListener(type, function (event) {
|
||||||
|
@ -80,16 +78,18 @@ export default class Hotkeys {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.shiftKey && event.key === keycode) {
|
if (event.shiftKey && event.key === keycode) {
|
||||||
action()
|
if (action() !== false) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else if (key["alt"] !== undefined) {
|
} else if (key["alt"] !== undefined) {
|
||||||
document.addEventListener(type, function (event) {
|
document.addEventListener(type, function (event) {
|
||||||
if (event.altKey && event.key === keycode) {
|
if (event.altKey && event.key === keycode) {
|
||||||
action()
|
if (action() !== false) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else if (key["nomod"] !== undefined) {
|
} else if (key["nomod"] !== undefined) {
|
||||||
document.addEventListener(type, function (event) {
|
document.addEventListener(type, function (event) {
|
||||||
|
@ -98,9 +98,11 @@ export default class Hotkeys {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === keycode) {
|
if (event.key === keycode) {
|
||||||
action()
|
const result = action()
|
||||||
|
if (result !== false) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +115,9 @@ export default class Hotkeys {
|
||||||
if (keycode.length == 1) {
|
if (keycode.length == 1) {
|
||||||
keycode = keycode.toUpperCase()
|
keycode = keycode.toUpperCase()
|
||||||
}
|
}
|
||||||
|
if (keycode === " ") {
|
||||||
|
keycode = "Spacebar"
|
||||||
|
}
|
||||||
modifiers.push(keycode)
|
modifiers.push(keycode)
|
||||||
return <[string, string | Translation]>[modifiers.join("+"), documentation]
|
return <[string, string | Translation]>[modifiers.join("+"), documentation]
|
||||||
})
|
})
|
||||||
|
@ -139,4 +144,15 @@ export default class Hotkeys {
|
||||||
static generateDocumentationDynamic(): BaseUIElement {
|
static generateDocumentationDynamic(): BaseUIElement {
|
||||||
return new VariableUiElement(Hotkeys._docs.map((_) => Hotkeys.generateDocumentation()))
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Loading from "./Loading.svelte"
|
import Loading from "./Loading.svelte"
|
||||||
import type { OsmServiceState } from "../../Logic/Osm/OsmConnection"
|
import type { OsmServiceState } from "../../Logic/Osm/OsmConnection"
|
||||||
|
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Tr from "./Tr.svelte"
|
import Tr from "./Tr.svelte"
|
||||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
|
||||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||||
|
|
||||||
|
@ -35,10 +35,12 @@
|
||||||
<Loading />
|
<Loading />
|
||||||
</slot>
|
</slot>
|
||||||
{:else if $loadingStatus === "error"}
|
{:else if $loadingStatus === "error"}
|
||||||
|
<slot name="error">
|
||||||
<div class="alert max-w-64 flex items-center">
|
<div class="alert max-w-64 flex items-center">
|
||||||
<Invalid class="m-2 h-8 w-8 shrink-0" />
|
<Invalid class="m-2 h-8 w-8 shrink-0" />
|
||||||
<Tr t={offlineModes[$apiState]} />
|
<Tr t={offlineModes[$apiState]} />
|
||||||
</div>
|
</div>
|
||||||
|
</slot>
|
||||||
{:else if $loadingStatus === "logged-in"}
|
{:else if $loadingStatus === "logged-in"}
|
||||||
<slot />
|
<slot />
|
||||||
{:else if $loadingStatus === "not-attempted"}
|
{:else if $loadingStatus === "not-attempted"}
|
||||||
|
|
|
@ -1,30 +1,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onMount } from "svelte";
|
import { createEventDispatcher } from "svelte"
|
||||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
import { Utils } from "../../Utils";
|
import { trapFocus } from "trap-focus-svelte"
|
||||||
import { trapFocus } from 'trap-focus-svelte'
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The slotted element will be shown on the right side
|
* The slotted element will be shown on the right side
|
||||||
*/
|
*/
|
||||||
const dispatch = createEventDispatcher<{ close }>();
|
const dispatch = createEventDispatcher<{ close }>()
|
||||||
let mainContent: HTMLElement;
|
let mainContent: HTMLElement
|
||||||
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
window.setTimeout(
|
|
||||||
() => Utils.focusOnFocusableChild(mainContent), 250
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
autofocus
|
||||||
bind:this={mainContent}
|
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 normal-background flex flex-col"
|
||||||
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"
|
role="dialog"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-modal="true"
|
||||||
style="max-width: 100vw; max-height: 100vh"
|
style="max-width: 100vw; max-height: 100vh"
|
||||||
|
use:trapFocus
|
||||||
>
|
>
|
||||||
<div class="normal-background m-0 flex flex-col">
|
|
||||||
<slot name="close-button">
|
<slot name="close-button">
|
||||||
<button
|
<button
|
||||||
class="absolute right-10 top-10 h-8 w-8 cursor-pointer"
|
class="absolute right-10 top-10 h-8 w-8 cursor-pointer"
|
||||||
|
@ -33,6 +31,7 @@
|
||||||
<XCircleIcon />
|
<XCircleIcon />
|
||||||
</button>
|
</button>
|
||||||
</slot>
|
</slot>
|
||||||
|
<div role="document" >
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="tabbedgroup flex h-full w-full focusable">
|
<div class="tabbedgroup flex h-full w-full">
|
||||||
<TabGroup
|
<TabGroup
|
||||||
class="flex h-full w-full flex-col"
|
class="flex h-full w-full flex-col"
|
||||||
defaultIndex={1}
|
defaultIndex={1}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||||
import { createEventDispatcher, onDestroy } from "svelte"
|
import { createEventDispatcher, onDestroy } from "svelte"
|
||||||
import { placeholder } from "../../Utils/placeholder"
|
import { placeholder } from "../../Utils/placeholder"
|
||||||
|
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
|
|
||||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||||
export let bounds: UIEventSource<BBox>
|
export let bounds: UIEventSource<BBox>
|
||||||
|
@ -117,7 +118,5 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
<div class="h-6 w-6 self-end" on:click={performSearch}>
|
<SearchIcon class="h-6 w-6 self-end" aria-hidden="true" on:click={performSearch}/>
|
||||||
<ToSvelte construct={Svg.search_svg} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<Tr t={Translations.t.general.returnToTheMap} />
|
<Tr t={Translations.t.general.returnToTheMap} />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{: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)}
|
{#each $knownTagRenderings as config (config.id)}
|
||||||
<TagRenderingEditable
|
<TagRenderingEditable
|
||||||
{tags}
|
{tags}
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Feature } from "geojson"
|
import type { Feature } from "geojson"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import SelectedElementTitle from "./SelectedElementTitle.svelte"
|
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||||
|
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
export let feature: Feature
|
export let feature: Feature
|
||||||
|
export let i: number = undefined
|
||||||
let id = feature.properties.id
|
let id = feature.properties.id
|
||||||
let tags = state.featureProperties.getStore(id)
|
let tags = state.featureProperties.getStore(id)
|
||||||
let layer: LayerConfig = state.layout.getMatchingLayer(tags.data)
|
let layer: LayerConfig = state.layout.getMatchingLayer(tags.data)
|
||||||
|
|
||||||
function select(){
|
function select() {
|
||||||
state.selectedElement.setData(undefined)
|
state.selectedElement.setData(undefined)
|
||||||
state.selectedLayer.setData(layer)
|
state.selectedLayer.setData(layer)
|
||||||
state.selectedElement.setData(feature)
|
state.selectedElement.setData(feature)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div on:click={() => select()} class="cursor-pointer">
|
<button class="cursor-pointer small" on:click={() => select()}>
|
||||||
<TagRenderingAnswer config={layer.title} selectedElement={feature} {state} {tags} {layer} />
|
{#if i !== undefined}
|
||||||
</div>
|
<span class="font-bold">{i + 1}.</span>
|
||||||
|
{/if}
|
||||||
|
<TagRenderingAnswer config={layer.title} {layer} selectedElement={feature} {state} {tags} />
|
||||||
|
</button>
|
||||||
|
|
41
src/UI/BigComponents/VisualFeedbackPanel.svelte
Normal file
41
src/UI/BigComponents/VisualFeedbackPanel.svelte
Normal 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>
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import type { OsmTags } from "../../Models/OsmFeature"
|
import type { OsmTags } from "../../Models/OsmFeature"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
|
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
import AttributedImage from "./AttributedImage.svelte"
|
import AttributedImage from "./AttributedImage.svelte"
|
||||||
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
||||||
|
|
||||||
export let tags: Store<OsmTags>
|
export let tags: UIEventSource<OsmTags>
|
||||||
export let lon: number
|
export let lon: number
|
||||||
export let lat: number
|
export let lat: number
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
|
@ -32,10 +32,10 @@
|
||||||
url: image.thumbUrl ?? image.pictureUrl,
|
url: image.thumbUrl ?? image.pictureUrl,
|
||||||
provider: AllImageProviders.byName(image.provider),
|
provider: AllImageProviders.byName(image.provider),
|
||||||
date: new Date(image.date),
|
date: new Date(image.date),
|
||||||
id: Object.values(image.osmTags)[0]
|
id: Object.values(image.osmTags)[0],
|
||||||
}
|
}
|
||||||
let distance = Math.round(
|
let distance = Math.round(
|
||||||
GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c)
|
GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c),
|
||||||
)
|
)
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
|
|
||||||
<div class="flex w-fit shrink-0 flex-col">
|
<div class="flex w-fit shrink-0 flex-col">
|
||||||
<div on:click={() => state.previewedImage.setData(providedImage)}>
|
<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>
|
</div>
|
||||||
{#if linkable}
|
{#if linkable}
|
||||||
<label>
|
<label>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid";
|
import { XCircleIcon } from "@babeard/svelte-heroicons/solid";
|
||||||
import Camera_plus from "../../assets/svg/Camera_plus.svelte";
|
import Camera_plus from "../../assets/svg/Camera_plus.svelte";
|
||||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||||
|
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||||
|
|
||||||
export let tags: Store<OsmTags>;
|
export let tags: Store<OsmTags>;
|
||||||
export let state: SpecialVisualizationState;
|
export let state: SpecialVisualizationState;
|
||||||
|
@ -26,9 +27,11 @@
|
||||||
<LoginToggle {state}>
|
<LoginToggle {state}>
|
||||||
|
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable}>
|
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer}>
|
||||||
<button slot="corner"
|
<button slot="corner"
|
||||||
class="h-6 w-6 cursor-pointer no-image-background p-0 border-none"
|
class="h-6 w-6 cursor-pointer no-image-background p-0 border-none"
|
||||||
|
use:ariaLabel={t.close}
|
||||||
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
expanded = false
|
expanded = false
|
||||||
}}>
|
}}>
|
||||||
|
|
|
@ -39,7 +39,6 @@
|
||||||
|
|
||||||
|
|
||||||
function handleOrientation(event) {
|
function handleOrientation(event) {
|
||||||
console.debug("Got gyro measurement")
|
|
||||||
gotMeasurement.setData(true)
|
gotMeasurement.setData(true)
|
||||||
// IF the phone is lying flat, then:
|
// IF the phone is lying flat, then:
|
||||||
// alpha is the compass direction (but not absolute)
|
// alpha is the compass direction (but not absolute)
|
||||||
|
|
|
@ -4,11 +4,12 @@ import { Map as MlMap, SourceSpecification } from "maplibre-gl"
|
||||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { BBox } from "../../Logic/BBox"
|
import { BBox } from "../../Logic/BBox"
|
||||||
import { ExportableMap, MapProperties } from "../../Models/MapProperties"
|
import { ExportableMap, KeyNavigationEvent, MapProperties } from "../../Models/MapProperties"
|
||||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||||
import MaplibreMap from "./MaplibreMap.svelte"
|
import MaplibreMap from "./MaplibreMap.svelte"
|
||||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||||
import * as htmltoimage from "html-to-image"
|
import * as htmltoimage from "html-to-image"
|
||||||
|
import { ALL } from "node:dns"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
* 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 lastClickLocation: Store<undefined | { lon: number; lat: number }>
|
||||||
readonly minzoom: UIEventSource<number>
|
readonly minzoom: UIEventSource<number>
|
||||||
readonly maxzoom: UIEventSource<number>
|
readonly maxzoom: UIEventSource<number>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When was the last navigation by arrow keys?
|
* Functions that are called when one of those actions has happened
|
||||||
* If set, this is a hint to use arrow compatibility
|
* @private
|
||||||
* Number of _seconds_ since epoch
|
|
||||||
*/
|
*/
|
||||||
readonly lastKeyNavigation: UIEventSource<number> = new UIEventSource<number>(undefined)
|
private _onKeyNavigation: ((event: KeyNavigationEvent) => void | boolean)[] = []
|
||||||
|
|
||||||
private readonly _maplibreMap: Store<MLMap>
|
private readonly _maplibreMap: Store<MLMap>
|
||||||
/**
|
/**
|
||||||
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
||||||
|
@ -132,13 +134,32 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
handleClick(e)
|
handleClick(e)
|
||||||
})
|
})
|
||||||
map.getContainer().addEventListener("keydown", (event) => {
|
map.getContainer().addEventListener("keydown", (event) => {
|
||||||
if (
|
let locked: "islocked" = undefined
|
||||||
event.key === "ArrowRight" ||
|
if (!this.allowMoving.data) {
|
||||||
event.key === "ArrowLeft" ||
|
locked = "islocked"
|
||||||
event.key === "ArrowUp" ||
|
}
|
||||||
event.key === "ArrowDown"
|
switch (event.key) {
|
||||||
) {
|
case "ArrowUp":
|
||||||
this.lastKeyNavigation.setData(Date.now() / 1000)
|
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.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
|
||||||
this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox))
|
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) =>
|
this.allowRotating.addCallbackAndRunD((allowRotating) =>
|
||||||
self.setAllowRotating(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(
|
public async exportAsPng(
|
||||||
rescaleIcons: number = 1,
|
rescaleIcons: number = 1,
|
||||||
progress: UIEventSource<{ current: number; total: number }> = undefined
|
progress: UIEventSource<{ current: number; total: number }> = undefined
|
||||||
|
@ -268,6 +299,24 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
return await MapLibreAdaptor.toBlob(drawOn)
|
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.
|
* Exports the background map and lines to PNG.
|
||||||
* Markers are _not_ rendered
|
* Markers are _not_ rendered
|
||||||
|
@ -373,7 +422,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
for (const label of labels) {
|
for (const label of labels) {
|
||||||
if (isDisplayed(label)) {
|
if (isDisplayed(label)) {
|
||||||
console.log("Exporting label", label)
|
|
||||||
await this.drawElement(drawOn, <HTMLElement>label, rescaleIcons, pixelRatio)
|
await this.drawElement(drawOn, <HTMLElement>label, rescaleIcons, pixelRatio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -565,16 +613,17 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
map.rotateTo(0, { duration: 0 })
|
map.rotateTo(0, { duration: 0 })
|
||||||
map.setPitch(0)
|
map.setPitch(0)
|
||||||
map.dragRotate.disable()
|
map.dragRotate.disable()
|
||||||
|
map.keyboard.disableRotation()
|
||||||
map.touchZoomRotate.disableRotation()
|
map.touchZoomRotate.disableRotation()
|
||||||
} else {
|
} else {
|
||||||
map.dragRotate.enable()
|
map.dragRotate.enable()
|
||||||
|
map.keyboard.enableRotation()
|
||||||
map.touchZoomRotate.enableRotation()
|
map.touchZoomRotate.enableRotation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setAllowMoving(allow: true | boolean | undefined) {
|
private setAllowMoving(allow: true | boolean | undefined) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
console.log("Setting 'allowMoving' to", allow)
|
|
||||||
if (!map) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
import { get, writable } from "svelte/store"
|
import { get, writable } from "svelte/store"
|
||||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'MaplibreMap' maps various event sources onto MapLibre.
|
* The 'MaplibreMap' maps various event sources onto MapLibre.
|
||||||
|
@ -19,7 +21,6 @@
|
||||||
|
|
||||||
let container: HTMLElement
|
let container: HTMLElement
|
||||||
|
|
||||||
export let attribution = false
|
|
||||||
export let center: { lng: number; lat: number } | Readable<{ lng: number; lat: number }> =
|
export let center: { lng: number; lat: number } | Readable<{ lng: number; lat: number }> =
|
||||||
writable({ lng: 0, lat: 0 })
|
writable({ lng: 0, lat: 0 })
|
||||||
export let zoom: Readable<number> = writable(1)
|
export let zoom: Readable<number> = writable(1)
|
||||||
|
@ -49,6 +50,9 @@
|
||||||
})
|
})
|
||||||
_map.on("load", function() {
|
_map.on("load", function() {
|
||||||
_map.resize()
|
_map.resize()
|
||||||
|
const canvas = _map.getCanvas()
|
||||||
|
ariaLabel(canvas, Translations.t.general.visualFeedback.navigation)
|
||||||
|
canvas.role="application"
|
||||||
})
|
})
|
||||||
map.set(_map)
|
map.set(_map)
|
||||||
})
|
})
|
||||||
|
@ -57,6 +61,8 @@
|
||||||
if (_map) _map.remove()
|
if (_map) _map.remove()
|
||||||
map = null
|
map = null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
|
@ -1,41 +1,42 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
import type { Feature } from "geojson";
|
import type { Feature } from "geojson"
|
||||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||||
import TagRenderingAnswer from "./TagRenderingAnswer.svelte";
|
import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
|
||||||
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
|
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
|
||||||
import { onDestroy } from "svelte";
|
import { onDestroy } from "svelte"
|
||||||
import Tr from "../../Base/Tr.svelte";
|
import Tr from "../../Base/Tr.svelte"
|
||||||
import Translations from "../../i18n/Translations.js";
|
import Translations from "../../i18n/Translations.js"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
import { Utils } from "../../../Utils";
|
import { Utils } from "../../../Utils"
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge"
|
||||||
|
import { ariaLabel } from "../../../Utils/ariaLabel"
|
||||||
|
|
||||||
export let config: TagRenderingConfig;
|
export let config: TagRenderingConfig
|
||||||
export let tags: UIEventSource<Record<string, string>>;
|
export let tags: UIEventSource<Record<string, string>>
|
||||||
export let selectedElement: Feature | undefined;
|
export let selectedElement: Feature | undefined
|
||||||
export let state: SpecialVisualizationState;
|
export let state: SpecialVisualizationState
|
||||||
export let layer: LayerConfig = undefined;
|
export let layer: LayerConfig = undefined
|
||||||
|
|
||||||
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge;
|
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge
|
||||||
|
|
||||||
export let highlightedRendering: UIEventSource<string> = undefined;
|
export let highlightedRendering: UIEventSource<string> = undefined
|
||||||
export let clss;
|
export let clss
|
||||||
/**
|
/**
|
||||||
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
|
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
|
||||||
*/
|
*/
|
||||||
export let editMode = !config.IsKnown(tags.data); // || showQuestionIfUnknown;
|
export let editMode = !config.IsKnown(tags.data) // || showQuestionIfUnknown;
|
||||||
if (tags) {
|
if (tags) {
|
||||||
onDestroy(
|
onDestroy(
|
||||||
tags.addCallbackD((tags) => {
|
tags.addCallbackD((tags) => {
|
||||||
editMode = !config.IsKnown(tags);
|
editMode = !config.IsKnown(tags)
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let htmlElem: HTMLDivElement;
|
let htmlElem: HTMLDivElement
|
||||||
$: {
|
$: {
|
||||||
if (editMode && htmlElem !== undefined && config.IsKnown(tags)) {
|
if (editMode && htmlElem !== undefined && config.IsKnown(tags)) {
|
||||||
// EditMode switched to true yet the answer is already known, so the person wants to make a change
|
// EditMode switched to true yet the answer is already known, so the person wants to make a change
|
||||||
|
@ -43,43 +44,44 @@
|
||||||
|
|
||||||
// Some delay is applied to give Svelte the time to render the _question_
|
// Some delay is applied to give Svelte the time to render the _question_
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
Utils.scrollIntoView(<any>htmlElem);
|
Utils.scrollIntoView(<any>htmlElem)
|
||||||
}, 50);
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _htmlElement = new UIEventSource<HTMLElement>(undefined);
|
const _htmlElement = new UIEventSource<HTMLElement>(undefined)
|
||||||
$: _htmlElement.setData(htmlElem);
|
$: _htmlElement.setData(htmlElem)
|
||||||
|
|
||||||
function setHighlighting() {
|
function setHighlighting() {
|
||||||
if (highlightedRendering === undefined) {
|
if (highlightedRendering === undefined) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (htmlElem === undefined) {
|
if (htmlElem === undefined) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const highlighted = highlightedRendering.data;
|
const highlighted = highlightedRendering.data
|
||||||
if (config.id === highlighted) {
|
if (config.id === highlighted) {
|
||||||
htmlElem.classList.add("glowing-shadow");
|
htmlElem.classList.add("glowing-shadow")
|
||||||
htmlElem.tabIndex = "-1";
|
htmlElem.tabIndex = -1
|
||||||
console.log("Scrolling to", htmlElem);
|
console.log("Scrolling to", htmlElem)
|
||||||
htmlElem.scrollIntoView({ behavior: "smooth" });
|
htmlElem.scrollIntoView({ behavior: "smooth" })
|
||||||
Utils.focusOnFocusableChild(htmlElem);
|
Utils.focusOnFocusableChild(htmlElem)
|
||||||
} else {
|
} else {
|
||||||
htmlElem.classList.remove("glowing-shadow");
|
htmlElem.classList.remove("glowing-shadow")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (highlightedRendering) {
|
if (highlightedRendering) {
|
||||||
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()));
|
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
|
||||||
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()));
|
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
|
||||||
}
|
}
|
||||||
|
let answerId = "answer-"+Utils.randomString(5)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={htmlElem} class={twMerge(clss, "tr-"+config.id)}>
|
<div bind:this={htmlElem} class={twMerge(clss, "tr-"+config.id)}>
|
||||||
{#if config.question && (!editingEnabled || $editingEnabled)}
|
{#if config.question && (!editingEnabled || $editingEnabled)}
|
||||||
{#if editMode}
|
{#if editMode}
|
||||||
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}>
|
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer} on:saved={() => editMode = false}>
|
||||||
<button
|
<button
|
||||||
slot="cancel"
|
slot="cancel"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
|
@ -91,6 +93,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button slot="upper-right"
|
<button slot="upper-right"
|
||||||
class="h-8 w-8 cursor-pointer border-none p-0"
|
class="h-8 w-8 cursor-pointer border-none p-0"
|
||||||
|
use:ariaLabel={Translations.t.general.cancel}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editMode = false
|
editMode = false
|
||||||
}}>
|
}}>
|
||||||
|
@ -99,12 +102,15 @@
|
||||||
</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">
|
||||||
|
<div id={answerId}>
|
||||||
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editMode = true
|
editMode = true
|
||||||
}}
|
}}
|
||||||
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
|
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
|
||||||
|
aria-labelledby={answerId}
|
||||||
>
|
>
|
||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,59 +1,60 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource";
|
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||||
import Tr from "../../Base/Tr.svelte";
|
import Tr from "../../Base/Tr.svelte"
|
||||||
import type { Feature } from "geojson";
|
import type { Feature } from "geojson"
|
||||||
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig";
|
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
|
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||||
import FreeformInput from "./FreeformInput.svelte";
|
import FreeformInput from "./FreeformInput.svelte"
|
||||||
import Translations from "../../i18n/Translations.js";
|
import Translations from "../../i18n/Translations.js"
|
||||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
|
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
|
||||||
import { createEventDispatcher, onDestroy } from "svelte";
|
import { createEventDispatcher, onDestroy } from "svelte"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
import SpecialTranslation from "./SpecialTranslation.svelte";
|
import SpecialTranslation from "./SpecialTranslation.svelte"
|
||||||
import TagHint from "../TagHint.svelte";
|
import TagHint from "../TagHint.svelte"
|
||||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||||
import SubtleButton from "../../Base/SubtleButton.svelte";
|
import SubtleButton from "../../Base/SubtleButton.svelte"
|
||||||
import Loading from "../../Base/Loading.svelte";
|
import Loading from "../../Base/Loading.svelte"
|
||||||
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte";
|
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"
|
||||||
import { Translation } from "../../i18n/Translation";
|
import { Translation } from "../../i18n/Translation"
|
||||||
import Constants from "../../../Models/Constants";
|
import Constants from "../../../Models/Constants"
|
||||||
import { Unit } from "../../../Models/Unit";
|
import { Unit } from "../../../Models/Unit"
|
||||||
import UserRelatedState from "../../../Logic/State/UserRelatedState";
|
import UserRelatedState from "../../../Logic/State/UserRelatedState"
|
||||||
import { twJoin } from "tailwind-merge";
|
import { twJoin } from "tailwind-merge"
|
||||||
import { TagUtils } from "../../../Logic/Tags/TagUtils";
|
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||||
import Search from "../../../assets/svg/Search.svelte";
|
import Search from "../../../assets/svg/Search.svelte"
|
||||||
import Login from "../../../assets/svg/Login.svelte";
|
import Login from "../../../assets/svg/Login.svelte"
|
||||||
|
import { placeholder } from "../../../Utils/placeholder"
|
||||||
|
|
||||||
export let config: TagRenderingConfig;
|
export let config: TagRenderingConfig
|
||||||
export let tags: UIEventSource<Record<string, string>>;
|
export let tags: UIEventSource<Record<string, string>>
|
||||||
export let selectedElement: Feature;
|
export let selectedElement: Feature
|
||||||
export let state: SpecialVisualizationState;
|
export let state: SpecialVisualizationState
|
||||||
export let layer: LayerConfig | undefined;
|
export let layer: LayerConfig | undefined
|
||||||
export let selectedTags: TagsFilter = 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
|
// Will be bound if a freeform is available
|
||||||
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]);
|
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
|
||||||
let selectedMapping: number = undefined;
|
let selectedMapping: number = undefined
|
||||||
/**
|
/**
|
||||||
* A list of booleans, used if multiAnswer is set
|
* A list of booleans, used if multiAnswer is set
|
||||||
*/
|
*/
|
||||||
let checkedMappings: boolean[];
|
let checkedMappings: boolean[]
|
||||||
|
|
||||||
let mappings: Mapping[] = config?.mappings;
|
let mappings: Mapping[] = config?.mappings
|
||||||
let searchTerm: UIEventSource<string> = new UIEventSource("");
|
let searchTerm: UIEventSource<string> = new UIEventSource("")
|
||||||
|
|
||||||
let dispatch = createEventDispatcher<{
|
let dispatch = createEventDispatcher<{
|
||||||
saved: {
|
saved: {
|
||||||
config: TagRenderingConfig
|
config: TagRenderingConfig
|
||||||
applied: TagsFilter
|
applied: TagsFilter
|
||||||
}
|
}
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares and fills the checkedMappings
|
* Prepares and fills the checkedMappings
|
||||||
|
@ -61,12 +62,12 @@
|
||||||
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
|
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
|
||||||
mappings = confg.mappings?.filter((m) => {
|
mappings = confg.mappings?.filter((m) => {
|
||||||
if (typeof m.hideInAnswer === "boolean") {
|
if (typeof m.hideInAnswer === "boolean") {
|
||||||
return !m.hideInAnswer;
|
return !m.hideInAnswer
|
||||||
}
|
}
|
||||||
return !m.hideInAnswer.matchesProperties(tgs);
|
return !m.hideInAnswer.matchesProperties(tgs)
|
||||||
});
|
})
|
||||||
// We received a new config -> reinit
|
// We received a new config -> reinit
|
||||||
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key));
|
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
confg.mappings?.length > 0 &&
|
confg.mappings?.length > 0 &&
|
||||||
|
@ -74,55 +75,55 @@
|
||||||
(checkedMappings === undefined ||
|
(checkedMappings === undefined ||
|
||||||
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
|
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
|
||||||
) {
|
) {
|
||||||
const seenFreeforms = [];
|
const seenFreeforms = []
|
||||||
// Initial setup of the mappings
|
// Initial setup of the mappings
|
||||||
checkedMappings = [
|
checkedMappings = [
|
||||||
...confg.mappings.map((mapping) => {
|
...confg.mappings.map((mapping) => {
|
||||||
if(mapping.hideInAnswer === true){
|
if (mapping.hideInAnswer === true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs);
|
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs)
|
||||||
if (matches && confg.freeform) {
|
if (matches && confg.freeform) {
|
||||||
const newProps = TagUtils.changeAsProperties(mapping.if.asChange({}));
|
const newProps = TagUtils.changeAsProperties(mapping.if.asChange({}))
|
||||||
seenFreeforms.push(newProps[confg.freeform.key]);
|
seenFreeforms.push(newProps[confg.freeform.key])
|
||||||
}
|
}
|
||||||
return matches;
|
return matches
|
||||||
})
|
}),
|
||||||
];
|
]
|
||||||
|
|
||||||
if (tgs !== undefined && confg.freeform) {
|
if (tgs !== undefined && confg.freeform) {
|
||||||
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? [];
|
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []
|
||||||
for (const seenFreeform of seenFreeforms) {
|
for (const seenFreeform of seenFreeforms) {
|
||||||
if (!seenFreeform) {
|
if (!seenFreeform) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
const index = unseenFreeformValues.indexOf(seenFreeform);
|
const index = unseenFreeformValues.indexOf(seenFreeform)
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
unseenFreeformValues.splice(index, 1);
|
unseenFreeformValues.splice(index, 1)
|
||||||
}
|
}
|
||||||
// TODO this has _to much_ values
|
// TODO this has _to much_ values
|
||||||
freeformInput.setData(unseenFreeformValues.join(";"));
|
freeformInput.setData(unseenFreeformValues.join(";"))
|
||||||
checkedMappings.push(unseenFreeformValues.length > 0);
|
checkedMappings.push(unseenFreeformValues.length > 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (confg.freeform?.key) {
|
if (confg.freeform?.key) {
|
||||||
if (!confg.multiAnswer) {
|
if (!confg.multiAnswer) {
|
||||||
// Somehow, setting multi-answer freeform values is broken if this is not set
|
// Somehow, setting multi-answer freeform values is broken if this is not set
|
||||||
freeformInput.setData(tgs[confg.freeform.key]);
|
freeformInput.setData(tgs[confg.freeform.key])
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
freeformInput.setData(undefined);
|
freeformInput.setData(undefined)
|
||||||
}
|
}
|
||||||
feedback.setData(undefined);
|
feedback.setData(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// Even though 'config' is not declared as a store, Svelte uses it as one to update the component
|
// Even though 'config' is not declared as a store, Svelte uses it as one to update the component
|
||||||
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
|
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
|
||||||
initialize($tags, config);
|
initialize($tags, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -131,54 +132,55 @@
|
||||||
$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)
|
||||||
selectedTags = undefined;
|
selectedTags = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onSave() {
|
function onSave() {
|
||||||
if (selectedTags === undefined) {
|
if (selectedTags === undefined) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) {
|
if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) {
|
||||||
/**
|
/**
|
||||||
* This is a special, priviliged layer.
|
* This is a special, priviliged layer.
|
||||||
* We simply apply the tags onto the records
|
* We simply apply the tags onto the records
|
||||||
*/
|
*/
|
||||||
const kv = selectedTags.asChange(tags.data);
|
const kv = selectedTags.asChange(tags.data)
|
||||||
for (const { k, v } of kv) {
|
for (const { k, v } of kv) {
|
||||||
if (v === undefined || v === "") {
|
if (v === undefined) {
|
||||||
delete tags.data[k];
|
// 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 {
|
} else {
|
||||||
tags.data[k] = v
|
tags.data[k] = v
|
||||||
}
|
}
|
||||||
feedback.setData(undefined);
|
feedback.setData(undefined)
|
||||||
}
|
}
|
||||||
tags.ping()
|
tags.ping()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
dispatch("saved", { config, applied: selectedTags });
|
dispatch("saved", { config, applied: selectedTags })
|
||||||
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
|
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
|
||||||
theme: tags.data["_orig_theme"] ?? state.layout.id,
|
theme: tags.data["_orig_theme"] ?? state.layout.id,
|
||||||
changeType: "answer"
|
changeType: "answer",
|
||||||
});
|
})
|
||||||
freeformInput.setData(undefined);
|
freeformInput.setData(undefined)
|
||||||
selectedMapping = undefined;
|
selectedMapping = undefined
|
||||||
selectedTags = undefined;
|
selectedTags = undefined
|
||||||
|
|
||||||
change
|
change
|
||||||
.CreateChangeDescriptions()
|
.CreateChangeDescriptions()
|
||||||
.then((changes) => state.changes.applyChanges(changes))
|
.then((changes) => state.changes.applyChanges(changes))
|
||||||
.catch(console.error);
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInputKeypress(e: Event) {
|
function onInputKeypress(e: KeyboardEvent) {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
onSave();
|
onSave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,28 +190,28 @@
|
||||||
$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)
|
||||||
selectedTags = undefined;
|
selectedTags = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false);
|
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
|
||||||
let featureSwitchIsDebugging =
|
let featureSwitchIsDebugging =
|
||||||
state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false);
|
state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
|
||||||
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined);
|
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined)
|
||||||
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0;
|
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0
|
||||||
let question = config.question;
|
let question = config.question
|
||||||
$: question = config.question;
|
$: question = config.question
|
||||||
if (state?.osmConnection) {
|
if (state?.osmConnection) {
|
||||||
onDestroy(
|
onDestroy(
|
||||||
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
|
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
|
||||||
numberOfCs = ud.csCount;
|
numberOfCs = ud.csCount
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -219,7 +221,7 @@
|
||||||
style="max-height: 75vh"
|
style="max-height: 75vh"
|
||||||
>
|
>
|
||||||
<div class="sticky top-0 interactive pt-1 flex justify-between" style="z-index: 11">
|
<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} />
|
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
|
||||||
</span>
|
</span>
|
||||||
<slot name="upper-right" />
|
<slot name="upper-right" />
|
||||||
|
@ -238,9 +240,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if config.mappings?.length >= 8}
|
{#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" />
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -351,7 +354,7 @@
|
||||||
<Tr t={Translations.t.general.loginToStart} slot="message" />
|
<Tr t={Translations.t.general.loginToStart} slot="message" />
|
||||||
</SubtleButton>
|
</SubtleButton>
|
||||||
{#if $feedback !== undefined}
|
{#if $feedback !== undefined}
|
||||||
<div class="alert">
|
<div class="alert" aria-live="assertive" role="alert">
|
||||||
<Tr t={$feedback} />
|
<Tr t={$feedback} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -48,7 +48,6 @@
|
||||||
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
|
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
|
||||||
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
|
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
|
||||||
import Cross from "../assets/svg/Cross.svelte"
|
import Cross from "../assets/svg/Cross.svelte"
|
||||||
import Summary from "./BigComponents/Summary.svelte"
|
|
||||||
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
|
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
|
||||||
import Mastodon from "../assets/svg/Mastodon.svelte"
|
import Mastodon from "../assets/svg/Mastodon.svelte"
|
||||||
import Bug from "../assets/svg/Bug.svelte"
|
import Bug from "../assets/svg/Bug.svelte"
|
||||||
|
@ -64,7 +63,7 @@
|
||||||
import Share from "../assets/svg/Share.svelte"
|
import Share from "../assets/svg/Share.svelte"
|
||||||
import Favourites from "./Favourites/Favourites.svelte"
|
import Favourites from "./Favourites/Favourites.svelte"
|
||||||
import ImageOperations from "./Image/ImageOperations.svelte"
|
import ImageOperations from "./Image/ImageOperations.svelte"
|
||||||
import { ariaLabel } from "../Utils/ariaLabel"
|
import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte"
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState
|
||||||
let layout = state.layout
|
let layout = state.layout
|
||||||
|
@ -92,9 +91,7 @@
|
||||||
|
|
||||||
let currentZoom = state.mapProperties.zoom
|
let currentZoom = state.mapProperties.zoom
|
||||||
let showCrosshair = state.userRelatedState.showCrosshair
|
let showCrosshair = state.userRelatedState.showCrosshair
|
||||||
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation
|
let arrowKeysWereUsed = state.visualFeedback
|
||||||
let centerFeatures = state.closestFeatures.features
|
|
||||||
|
|
||||||
|
|
||||||
let mapproperties: MapProperties = state.mapProperties
|
let mapproperties: MapProperties = state.mapProperties
|
||||||
let featureSwitches: FeatureSwitchState = state.featureSwitches
|
let featureSwitches: FeatureSwitchState = state.featureSwitches
|
||||||
|
@ -114,10 +111,10 @@
|
||||||
|
|
||||||
function forwardEventToMap(e: KeyboardEvent) {
|
function forwardEventToMap(e: KeyboardEvent) {
|
||||||
const mlmap = state.map.data
|
const mlmap = state.map.data
|
||||||
if(!mlmap){
|
if (!mlmap) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(!mlmap.keyboard.isEnabled()){
|
if (!mlmap.keyboard.isEnabled()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const animation = mlmap.keyboard?.keydown(e)
|
const animation = mlmap.keyboard?.keydown(e)
|
||||||
|
@ -135,9 +132,9 @@
|
||||||
<div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2">
|
<div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2">
|
||||||
<Geosearch
|
<Geosearch
|
||||||
bounds={state.mapProperties.bounds}
|
bounds={state.mapProperties.bounds}
|
||||||
|
on:searchCompleted={() => {state.map?.data?.getCanvas()?.focus()}}
|
||||||
perLayer={state.perLayer}
|
perLayer={state.perLayer}
|
||||||
selectedElement={state.selectedElement}
|
selectedElement={state.selectedElement}
|
||||||
on:searchCompleted={() => {state.map?.data?.getCanvas()?.focus()}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</If>
|
||||||
|
@ -152,9 +149,9 @@
|
||||||
</div>
|
</div>
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
<MapControlButton
|
<MapControlButton
|
||||||
|
arialabel={Translations.t.general.labels.menu}
|
||||||
on:click={() => state.guistate.menuIsOpened.setData(true)}
|
on:click={() => state.guistate.menuIsOpened.setData(true)}
|
||||||
on:keydown={forwardEventToMap}
|
on:keydown={forwardEventToMap}
|
||||||
arialabel={Translations.t.general.labels.menu}
|
|
||||||
>
|
>
|
||||||
<MenuIcon class="h-8 w-8 cursor-pointer" />
|
<MenuIcon class="h-8 w-8 cursor-pointer" />
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
|
@ -211,8 +208,9 @@
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<!-- bottom left elements -->
|
<!-- bottom left elements -->
|
||||||
<If condition={state.featureSwitches.featureSwitchFilter}>
|
<If condition={state.featureSwitches.featureSwitchFilter}>
|
||||||
<MapControlButton on:click={() => state.guistate.openFilterView()} on:keydown={forwardEventToMap}
|
<MapControlButton arialabel={Translations.t.general.labels.filter}
|
||||||
arialabel={Translations.t.general.labels.filter}
|
on:click={() => state.guistate.openFilterView()}
|
||||||
|
on:keydown={forwardEventToMap}
|
||||||
>
|
>
|
||||||
<Filter class="h-6 w-6" />
|
<Filter class="h-6 w-6" />
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
|
@ -231,17 +229,11 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="flex flex-col items-end">
|
||||||
<!-- bottom right elements -->
|
<!-- bottom right elements -->
|
||||||
<If condition={state.floors.map((f) => f.length > 1)}>
|
<If condition={state.floors.map((f) => f.length > 1)}>
|
||||||
|
@ -253,20 +245,22 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</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}
|
on:keydown={forwardEventToMap}
|
||||||
arialabel={Translations.t.general.labels.zoomIn}
|
|
||||||
>
|
>
|
||||||
<Plus class="h-8 w-8" />
|
<Plus class="h-8 w-8" />
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z - 1)} on:keydown={forwardEventToMap}
|
<MapControlButton arialabel={Translations.t.general.labels.zoomOut}
|
||||||
arialabel={Translations.t.general.labels.zoomOut}
|
on:click={() => mapproperties.zoom.update((z) => z - 1)}
|
||||||
|
on:keydown={forwardEventToMap}
|
||||||
>
|
>
|
||||||
<Min class="h-8 w-8" />
|
<Min class="h-8 w-8" />
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
<If condition={featureSwitches.featureSwitchGeolocation}>
|
<If condition={featureSwitches.featureSwitchGeolocation}>
|
||||||
<MapControlButton on:keydown={forwardEventToMap} on:click={() => geolocationControl.handleClick()}
|
<MapControlButton arialabel={Translations.t.general.labels.jumpToLocation}
|
||||||
arialabel={Translations.t.general.labels.jumpToLocation}
|
on:click={() => geolocationControl.handleClick()}
|
||||||
|
on:keydown={forwardEventToMap}
|
||||||
>
|
>
|
||||||
<ToSvelte
|
<ToSvelte
|
||||||
construct={geolocationControl.SetClass("block w-8 h-8")}
|
construct={geolocationControl.SetClass("block w-8 h-8")}
|
||||||
|
@ -277,14 +271,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<LoginToggle ignoreLoading={true} {state}>
|
<LoginToggle ignoreLoading={true} {state}>
|
||||||
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $arrowKeysWereUsed !== undefined}
|
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $arrowKeysWereUsed}
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center"
|
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center"
|
||||||
>
|
>
|
||||||
<Cross class="h-4 w-4" />
|
<Cross class="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<svelte:fragment slot="error" /> <!-- Add in an empty container to remove errors -->
|
||||||
</LoginToggle>
|
</LoginToggle>
|
||||||
|
|
||||||
<If condition={state.previewedImage.map(i => i!==undefined)}>
|
<If condition={state.previewedImage.map(i => i!==undefined)}>
|
||||||
|
@ -322,7 +318,7 @@
|
||||||
selectedElement.setData(undefined)
|
selectedElement.setData(undefined)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="h-full w-full flex focusable">
|
<div class="h-full w-full flex">
|
||||||
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
||||||
</div>
|
</div>
|
||||||
</FloatOver>
|
</FloatOver>
|
||||||
|
@ -410,7 +406,7 @@
|
||||||
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
|
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="h-full p-2 focusable">
|
<div class="h-full p-2">
|
||||||
<RasterLayerOverview
|
<RasterLayerOverview
|
||||||
{availableLayers}
|
{availableLayers}
|
||||||
map={state.map}
|
map={state.map}
|
||||||
|
|
|
@ -69,10 +69,6 @@ body {
|
||||||
color: var(--foreground-color);
|
color: var(--foreground-color);
|
||||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
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,
|
svg,
|
||||||
img {
|
img {
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<!-- THEME-SPECIFIC-END-->
|
<!-- THEME-SPECIFIC-END-->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body tabindex="-1">
|
||||||
|
|
||||||
|
|
||||||
<div class="h-screen" id="maindiv">
|
<div class="h-screen" id="maindiv">
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
<div class="flex justify-between items-start w-full">
|
<div class="flex justify-between items-start w-full">
|
||||||
|
|
||||||
<!-- IMAGE-START -->
|
<!-- 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 -->
|
<!-- IMAGE-END -->
|
||||||
<div class="h-min subtle">
|
<div class="h-min subtle">
|
||||||
Version
|
Version
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 src="./src/UI/RemoveOtherLanguages.js"></script>
|
||||||
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
|
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
|
||||||
<script defer src="./src/index.ts" type="module"></script>
|
<script defer src="./src/index.ts" type="module"></script>
|
||||||
|
|
Loading…
Reference in a new issue