forked from MapComplete/MapComplete
		
	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…", | ||||
|             "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", | ||||
|  |  | |||
|  | @ -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": { | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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()) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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"} | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										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,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> | ||||
|  |  | |||
|  | @ -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 | ||||
|         }}> | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|         } | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue