forked from MapComplete/MapComplete
		
	Accessibility: add focus trapping, debug tab cycling, UI tweaks for mobile browser
This commit is contained in:
		
							parent
							
								
									307549b593
								
							
						
					
					
						commit
						8ae4d810d6
					
				
					 19 changed files with 123 additions and 77 deletions
				
			
		
							
								
								
									
										13
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -6,7 +6,7 @@ | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "mapcomplete", |       "name": "mapcomplete", | ||||||
|       "version": "0.36.1", |       "version": "0.36.2", | ||||||
|       "license": "GPL-3.0-or-later", |       "license": "GPL-3.0-or-later", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@rgossiaux/svelte-headlessui": "^1.0.2", |         "@rgossiaux/svelte-headlessui": "^1.0.2", | ||||||
|  | @ -56,6 +56,7 @@ | ||||||
|         "svg-path-parser": "^1.1.0", |         "svg-path-parser": "^1.1.0", | ||||||
|         "tailwind-merge": "^1.13.1", |         "tailwind-merge": "^1.13.1", | ||||||
|         "tailwindcss": "^3.1.8", |         "tailwindcss": "^3.1.8", | ||||||
|  |         "trap-focus-svelte": "^1.0.1", | ||||||
|         "vite-node": "^0.28.3", |         "vite-node": "^0.28.3", | ||||||
|         "vitest": "^0.28.3", |         "vitest": "^0.28.3", | ||||||
|         "wikibase-sdk": "^7.14.0", |         "wikibase-sdk": "^7.14.0", | ||||||
|  | @ -12140,6 +12141,11 @@ | ||||||
|         "node": ">=14" |         "node": ">=14" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/trap-focus-svelte": { | ||||||
|  |       "version": "1.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/trap-focus-svelte/-/trap-focus-svelte-1.0.1.tgz", | ||||||
|  |       "integrity": "sha512-qacSd68+c12mudUu9Mo70Ea16263ich2APFh1d0K7k9rLtwNcxlxNqA6l7Wv7xdzhJbC9TASxroiDSkiN2349w==" | ||||||
|  |     }, | ||||||
|     "node_modules/ts-api-utils": { |     "node_modules/ts-api-utils": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", |       "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", | ||||||
|  | @ -22659,6 +22665,11 @@ | ||||||
|         "punycode": "^2.3.0" |         "punycode": "^2.3.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "trap-focus-svelte": { | ||||||
|  |       "version": "1.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/trap-focus-svelte/-/trap-focus-svelte-1.0.1.tgz", | ||||||
|  |       "integrity": "sha512-qacSd68+c12mudUu9Mo70Ea16263ich2APFh1d0K7k9rLtwNcxlxNqA6l7Wv7xdzhJbC9TASxroiDSkiN2349w==" | ||||||
|  |     }, | ||||||
|     "ts-api-utils": { |     "ts-api-utils": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", |       "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", | ||||||
|  |  | ||||||
|  | @ -143,6 +143,7 @@ | ||||||
|     "svg-path-parser": "^1.1.0", |     "svg-path-parser": "^1.1.0", | ||||||
|     "tailwind-merge": "^1.13.1", |     "tailwind-merge": "^1.13.1", | ||||||
|     "tailwindcss": "^3.1.8", |     "tailwindcss": "^3.1.8", | ||||||
|  |     "trap-focus-svelte": "^1.0.1", | ||||||
|     "vite-node": "^0.28.3", |     "vite-node": "^0.28.3", | ||||||
|     "vitest": "^0.28.3", |     "vitest": "^0.28.3", | ||||||
|     "wikibase-sdk": "^7.14.0", |     "wikibase-sdk": "^7.14.0", | ||||||
|  |  | ||||||
|  | @ -1796,14 +1796,14 @@ video { | ||||||
|   padding: 0.25rem; |   padding: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .p-0\.5 { |  | ||||||
|   padding: 0.125rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .p-0 { | .p-0 { | ||||||
|   padding: 0px; |   padding: 0px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .p-0\.5 { | ||||||
|  |   padding: 0.125rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .p-12 { | .p-12 { | ||||||
|   padding: 3rem; |   padding: 3rem; | ||||||
| } | } | ||||||
|  | @ -2244,7 +2244,6 @@ body { | ||||||
| 
 | 
 | ||||||
| .focusable { | .focusable { | ||||||
|   /* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */ |   /* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */ | ||||||
|   border: 1px solid red |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| svg, | svg, | ||||||
|  |  | ||||||
|  | @ -190,19 +190,45 @@ export default class GenerateImageAnalysis extends Script { | ||||||
|         if (!existsSync(viewDir)) { |         if (!existsSync(viewDir)) { | ||||||
|             mkdirSync(viewDir) |             mkdirSync(viewDir) | ||||||
|         } |         } | ||||||
|  |         const targetpath = datapath + "/views.csv" | ||||||
| 
 | 
 | ||||||
|  |         const total = allImages.size | ||||||
|  |         let dloaded = 0 | ||||||
|  |         let skipped = 0 | ||||||
|  |         let err = 0 | ||||||
|         for (const image of Array.from(allImages)) { |         for (const image of Array.from(allImages)) { | ||||||
|             const cachedView = viewDir + "/" + image.replace(/\\/g, "_") |             const cachedView = viewDir + "/" + image.replace(/\//g, "_") | ||||||
|             let attribution: LicenseInfo |             let attribution: LicenseInfo | ||||||
|             if (existsSync(cachedView)) { |             if (existsSync(cachedView)) { | ||||||
|                 attribution = JSON.parse(readFileSync(cachedView, "utf8")) |                 attribution = JSON.parse(readFileSync(cachedView, "utf8")) | ||||||
|  |                 skipped++ | ||||||
|             } else { |             } else { | ||||||
|                 attribution = await Imgur.singleton.DownloadAttribution(image) |                 try { | ||||||
|                 writeFileSync(cachedView, JSON.stringify(attribution)) |                     attribution = await Imgur.singleton.DownloadAttribution(image) | ||||||
|  |                     await ScriptUtils.sleep(500) | ||||||
|  |                     writeFileSync(cachedView, JSON.stringify(attribution)) | ||||||
|  |                     dloaded++ | ||||||
|  |                 } catch (e) { | ||||||
|  |                     err++ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|             results.push([image, attribution.views]) |             results.push([image, attribution.views]) | ||||||
|  |             if (dloaded % 50 === 0) { | ||||||
|  |                 console.log({ | ||||||
|  |                     dloaded, | ||||||
|  |                     skipped, | ||||||
|  |                     total, | ||||||
|  |                     err, | ||||||
|  |                     progress: Math.round(dloaded + skipped + err), | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if ((dloaded + skipped + err) % 100 === 0) { | ||||||
|  |                 console.log("Writing views to", targetpath) | ||||||
|  |                 fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n")) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         const targetpath = datapath + "/views.csv" |  | ||||||
|         console.log("Writing views to", targetpath) |         console.log("Writing views to", targetpath) | ||||||
|         fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n")) |         fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n")) | ||||||
|     } |     } | ||||||
|  | @ -416,8 +442,8 @@ export default class GenerateImageAnalysis extends Script { | ||||||
|         const imageBackupPath = args[0] |         const imageBackupPath = args[0] | ||||||
|         await this.downloadData(datapath, cached) |         await this.downloadData(datapath, cached) | ||||||
| 
 | 
 | ||||||
|         await this.downloadMetadata(datapath) |  | ||||||
|         await this.downloadViews(datapath) |         await this.downloadViews(datapath) | ||||||
|  |         await this.downloadMetadata(datapath) | ||||||
|         await this.downloadAllImages(datapath, imageBackupPath) |         await this.downloadAllImages(datapath, imageBackupPath) | ||||||
|         this.analyze(datapath) |         this.analyze(datapath) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -39,7 +39,7 @@ export default class UserRelatedState { | ||||||
|     public readonly installedUserThemes: Store<string[]> |     public readonly installedUserThemes: Store<string[]> | ||||||
|     public readonly showAllQuestionsAtOnce: UIEventSource<boolean> |     public readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||||
|     public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> |     public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> | ||||||
|     public readonly showCrosshair: UIEventSource<"yes" | undefined> |     public readonly showCrosshair: UIEventSource<"yes" | "always" | "no" | undefined> | ||||||
|     public readonly fixateNorth: UIEventSource<undefined | "yes"> |     public readonly fixateNorth: UIEventSource<undefined | "yes"> | ||||||
|     public readonly homeLocation: FeatureSource |     public readonly homeLocation: FeatureSource | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import { Translation } from "../UI/i18n/Translation" | import { Translation } from "../UI/i18n/Translation" | ||||||
| import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson" | import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson" | ||||||
| import Translations from "../UI/i18n/Translations" | import Translations from "../UI/i18n/Translations" | ||||||
| import { Store } from "../Logic/UIEventSource" |  | ||||||
| import BaseUIElement from "../UI/BaseUIElement" |  | ||||||
| import Toggle from "../UI/Input/Toggle" |  | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * A 'denomination' is one way to write a certain quantity. | ||||||
|  |  * For example, 'meter', 'kilometer', 'mile' and 'foot' are all possible ways to quantify 'length' | ||||||
|  |  */ | ||||||
| export class Denomination { | export class Denomination { | ||||||
|     public readonly canonical: string |     public readonly canonical: string | ||||||
|     public readonly _canonicalSingular: string |     public readonly _canonicalSingular: string | ||||||
|  | @ -53,8 +54,8 @@ export class Denomination { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Create a representation of the given value |      * Create a representation of the given value | ||||||
|      * @param value: the value from OSM |      * @param value the value from OSM | ||||||
|      * @param actAsDefault: if set and the value can be parsed as number, will be parsed and trimmed |      * @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed | ||||||
|      * |      * | ||||||
|      * const unit = new Denomination({ |      * const unit = new Denomination({ | ||||||
|      *               canonicalDenomination: "m", |      *               canonicalDenomination: "m", | ||||||
|  | @ -82,6 +83,8 @@ export class Denomination { | ||||||
|      * unit.canonicalValue("42", true) // =>"42"
 |      * unit.canonicalValue("42", true) // =>"42"
 | ||||||
|      * unit.canonicalValue("42 m", true) // =>"42"
 |      * unit.canonicalValue("42 m", true) // =>"42"
 | ||||||
|      * unit.canonicalValue("42 meter", true) // =>"42"
 |      * unit.canonicalValue("42 meter", true) // =>"42"
 | ||||||
|  |      * | ||||||
|  |      * | ||||||
|      */ |      */ | ||||||
|     public canonicalValue(value: string, actAsDefault: boolean): string { |     public canonicalValue(value: string, actAsDefault: boolean): string { | ||||||
|         if (value === undefined) { |         if (value === undefined) { | ||||||
|  |  | ||||||
|  | @ -1,28 +1,35 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { createEventDispatcher, onMount } from "svelte"; |   import { createEventDispatcher, onMount } from "svelte"; | ||||||
|   import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" |   import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||||
|   import { twMerge } from "tailwind-merge" |   import { twMerge } from "tailwind-merge"; | ||||||
| 
 |   import { Utils } from "../../Utils"; | ||||||
|  |   import { trapFocus } from 'trap-focus-svelte' | ||||||
|   /** |   /** | ||||||
|    * The slotted element will be shown on top, with a lower-opacity border |    * The slotted element will be shown on top, with a lower-opacity border | ||||||
|    */ |    */ | ||||||
|   const dispatch = createEventDispatcher<{ close }>() |   const dispatch = createEventDispatcher<{ close }>(); | ||||||
|    | 
 | ||||||
|   export let extraClasses = "p-4 md:p-6" |   export let extraClasses = "p-4 md:p-6"; | ||||||
|    | 
 | ||||||
|   let mainContent: HTMLElement |   let mainContent: HTMLElement; | ||||||
|   onMount(() => { |   onMount(() => { | ||||||
|     console.log("Mounting floatover") |     requestAnimationFrame(() => { | ||||||
|     mainContent?.focus() |       Utils.focusOnFocusableChild(mainContent); | ||||||
|   }) |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div |   <!-- Draw the background over the total screen --> | ||||||
|   class={twMerge("absolute top-0 right-0 h-screen w-screen", extraClasses)} | <div class="w-screen h-screen absolute top-0 left-0" style="background-color: #00000088; z-index: 20"   on:click={() => { | ||||||
|   style="background-color: #00000088; z-index: 20" |  | ||||||
|   on:click={() => { |  | ||||||
|     dispatch("close") |     dispatch("close") | ||||||
|   }} |   }}> | ||||||
|  | </div> | ||||||
|  | <!-- draw a _second_ absolute div, placed using 'bottom' which will be above the navigation bar on mobile browsers --> | ||||||
|  | <div | ||||||
|  |   class={twMerge("absolute bottom-0 right-0 h-full w-screen", extraClasses)} | ||||||
|  |   use:trapFocus | ||||||
|  |   style="z-index: 21" | ||||||
| > | > | ||||||
|   <div bind:this={mainContent} class="content normal-background" on:click|stopPropagation={() => {}}> |   <div bind:this={mainContent} class="content normal-background" on:click|stopPropagation={() => {}}> | ||||||
|     <div class="h-full rounded-xl"> |     <div class="h-full rounded-xl"> | ||||||
|  | @ -30,21 +37,23 @@ | ||||||
|     </div> |     </div> | ||||||
|     <slot name="close-button"> |     <slot name="close-button"> | ||||||
|       <!-- The close button is placed _after_ the default slot in order to always paint it on top --> |       <!-- The close button is placed _after_ the default slot in order to always paint it on top --> | ||||||
|       <div |       <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 p-0 border-none bg-white" | ||||||
|         on:click={() => dispatch("close")} |         on:click={() => dispatch("close")} | ||||||
|       > |       > | ||||||
|         <XCircleIcon /> |         <XCircleIcon /> | ||||||
|       </div> |       </button> | ||||||
|     </slot> |     </slot> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| <style> | <style> | ||||||
|   .content { |     .content { | ||||||
|     height: 100%; |         height: 100%; | ||||||
|     border-radius: 0.5rem; |         border-radius: 0.5rem; | ||||||
|     overflow-x: hidden; |         overflow-x: hidden; | ||||||
|     box-shadow: 0 0 1rem #00000088; |         box-shadow: 0 0 1rem #00000088; | ||||||
|   } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -10,14 +10,14 @@ | ||||||
| 
 | 
 | ||||||
|   export let state: { |   export let state: { | ||||||
|     osmConnection: OsmConnection |     osmConnection: OsmConnection | ||||||
|     featureSwitches?: { featureSwitchUserbadge?: UIEventSource<boolean> } |     featureSwitches?: { featureSwitchEnableLogin?: UIEventSource<boolean> } | ||||||
|   } |   } | ||||||
|   /** |   /** | ||||||
|    * If set, 'loading' will act as if we are already logged in. |    * If set, 'loading' will act as if we are already logged in. | ||||||
|    */ |    */ | ||||||
|   export let ignoreLoading: boolean = false |   export let ignoreLoading: boolean = false | ||||||
|   let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in") |   let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in") | ||||||
|   let badge = state?.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true) |   let badge = state?.featureSwitches?.featureSwitchEnableLogin ?? new ImmutableStore(true) | ||||||
|   const t = Translations.t.general |   const t = Translations.t.general | ||||||
|   const offlineModes: Partial<Record<OsmServiceState, Translation>> = { |   const offlineModes: Partial<Record<OsmServiceState, Translation>> = { | ||||||
|     offline: t.loginFailedOfflineMode, |     offline: t.loginFailedOfflineMode, | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
|   import { createEventDispatcher, onMount } from "svelte"; |   import { createEventDispatcher, onMount } from "svelte"; | ||||||
|   import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; |   import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||||
|   import { Utils } from "../../Utils"; |   import { Utils } from "../../Utils"; | ||||||
|  |   import { trapFocus } from 'trap-focus-svelte' | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * The slotted element will be shown on the right side |    * The slotted element will be shown on the right side | ||||||
|  | @ -13,13 +14,13 @@ | ||||||
|   onMount(() => { |   onMount(() => { | ||||||
|     window.setTimeout( |     window.setTimeout( | ||||||
|       () => Utils.focusOnFocusableChild(mainContent), 250 |       () => Utils.focusOnFocusableChild(mainContent), 250 | ||||||
|        |     ); | ||||||
|     ) |   }); | ||||||
|   }) |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div | <div | ||||||
|   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" |   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" | ||||||
|   style="max-width: 100vw; max-height: 100vh" |   style="max-width: 100vw; max-height: 100vh" | ||||||
| > | > | ||||||
|  |  | ||||||
|  | @ -14,10 +14,8 @@ | ||||||
|   export let selectedElement: Feature |   export let selectedElement: Feature | ||||||
|   export let highlightedRendering: UIEventSource<string> = undefined |   export let highlightedRendering: UIEventSource<string> = undefined | ||||||
| 
 | 
 | ||||||
|   let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(selectedElement.properties.id) |   export let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(selectedElement.properties.id) | ||||||
|   $: { | 
 | ||||||
|     tags = state.featureProperties.getStore(selectedElement.properties.id) |  | ||||||
|   } |  | ||||||
|   let _metatags: Record<string, string> |   let _metatags: Record<string, string> | ||||||
|   onDestroy( |   onDestroy( | ||||||
|     state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => { |     state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => { | ||||||
|  | @ -28,7 +26,7 @@ | ||||||
|   let knownTagRenderings: Store<TagRenderingConfig[]> =  tags.mapD(tgs =>  layer.tagRenderings.filter( |   let knownTagRenderings: Store<TagRenderingConfig[]> =  tags.mapD(tgs =>  layer.tagRenderings.filter( | ||||||
|     (config) => |     (config) => | ||||||
|       (config.condition?.matchesProperties(tgs) ?? true) && |       (config.condition?.matchesProperties(tgs) ?? true) && | ||||||
|       config.metacondition?.matchesProperties({ ...tgs, ..._metatags } ?? true) && |       (config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true) && | ||||||
|       config.IsKnown(tgs) |       config.IsKnown(tgs) | ||||||
|   )) |   )) | ||||||
| </script> | </script> | ||||||
|  | @ -39,7 +37,7 @@ | ||||||
|     <Tr t={Translations.t.general.returnToTheMap} /> |     <Tr t={Translations.t.general.returnToTheMap} /> | ||||||
|   </button> |   </button> | ||||||
| {:else} | {:else} | ||||||
|   <div class="flex h-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 focusable" tabindex="-1"> | ||||||
|     {#each $knownTagRenderings as config (config.id)} |     {#each $knownTagRenderings as config (config.id)} | ||||||
|       <TagRenderingEditable |       <TagRenderingEditable | ||||||
|         {tags} |         {tags} | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ import { twMerge } from "tailwind-merge"; | ||||||
| export let image: ProvidedImage | export let image: ProvidedImage | ||||||
| export let clss: string = undefined | export let clss: string = undefined | ||||||
| async function download() { | async function download() { | ||||||
|     const response = await fetch(image.url) |     const response = await fetch(image.url_hd ?? image.url ) | ||||||
|     const blob = await response.blob() |     const blob = await response.blob() | ||||||
|     Utils.offerContentsAsDownloadableFile(blob, new URL(image.url).pathname.split("/").at(-1), { |     Utils.offerContentsAsDownloadableFile(blob, new URL(image.url).pathname.split("/").at(-1), { | ||||||
|         mimetype: "image/jpg", |         mimetype: "image/jpg", | ||||||
|  | @ -22,11 +22,11 @@ async function download() { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class={twMerge("w-full h-full relative", clss)}> | <div class={twMerge("w-full h-full relative", clss)}> | ||||||
|   <div class="absolute top-0 left-0 w-full h-full overflow-hidden"> |   <div class="absolute top-0 left-0 w-full h-full overflow-hidden panzoom-container focusable"> | ||||||
|     <ImagePreview image={image} /> |     <ImagePreview image={image} /> | ||||||
|   </div> |   </div> | ||||||
|   <div class="absolute bottom-0 left-0 w-full pointer-events-none flex flex-wrap justify-between items-end"> |   <div class="absolute bottom-0 left-0 w-full pointer-events-none flex flex-wrap justify-between items-end"> | ||||||
|     <div class="pointer-events-auto w-fit opacity-50 hover:opacity-100 transition-colors duration-200"> |     <div class="pointer-events-auto w-fit opacity-50 hover:opacity-100 transition-colors duration-200 m-1"> | ||||||
|       <ImageAttribution image={image} /> |       <ImageAttribution image={image} /> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -25,4 +25,4 @@ | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <img bind:this={panzoomEl} src={image.url_hd ?? image.url} class="w-full h-auto"/> | <img bind:this={panzoomEl} src={image.url_hd ?? image.url} class="w-fit h-fit panzoom-image"/> | ||||||
|  |  | ||||||
|  | @ -13,8 +13,11 @@ | ||||||
|   import Loading from "../Base/Loading.svelte" |   import Loading from "../Base/Loading.svelte" | ||||||
| 
 | 
 | ||||||
|   export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState | ||||||
|   export let tags: Store<OsmTags> |   export let tags: Store<OsmTags> = undefined | ||||||
|   export let featureId = tags.data.id |   export let featureId = tags?.data?.id | ||||||
|  |   if(featureId === undefined){ | ||||||
|  |     throw "No tags or featureID given" | ||||||
|  |   } | ||||||
|   export let showThankYou: boolean = true |   export let showThankYou: boolean = true | ||||||
|   const { uploadStarted, uploadFinished, retried, failed } = |   const { uploadStarted, uploadFinished, retried, failed } = | ||||||
|     state.imageUploadManager.getCountsFor(featureId) |     state.imageUploadManager.getCountsFor(featureId) | ||||||
|  |  | ||||||
|  | @ -77,7 +77,7 @@ export interface SpecialVisualizationState { | ||||||
|         readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> |         readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> | ||||||
|         readonly mangroveIdentity: MangroveIdentity |         readonly mangroveIdentity: MangroveIdentity | ||||||
|         readonly showAllQuestionsAtOnce: UIEventSource<boolean> |         readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||||
|         readonly preferencesAsTags: Store<Record<string, string>> |         readonly preferencesAsTags: UIEventSource<Record<string, string>> | ||||||
|         readonly language: UIEventSource<string> |         readonly language: UIEventSource<string> | ||||||
|     } |     } | ||||||
|     readonly lastClickObject: WritableFeatureSource |     readonly lastClickObject: WritableFeatureSource | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ import FeatureReviews from "../Logic/Web/MangroveReviews" | ||||||
| import Maproulette from "../Logic/Maproulette" | import Maproulette from "../Logic/Maproulette" | ||||||
| import SvelteUIElement from "./Base/SvelteUIElement" | import SvelteUIElement from "./Base/SvelteUIElement" | ||||||
| import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" | import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" | ||||||
| import { Feature, Point } from "geojson" | import { Feature } from "geojson" | ||||||
| import { GeoOperations } from "../Logic/GeoOperations" | import { GeoOperations } from "../Logic/GeoOperations" | ||||||
| import CreateNewNote from "./Popup/CreateNewNote.svelte" | import CreateNewNote from "./Popup/CreateNewNote.svelte" | ||||||
| import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" | import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" | ||||||
|  | @ -48,8 +48,7 @@ import UserProfile from "./BigComponents/UserProfile.svelte" | ||||||
| import Link from "./Base/Link" | import Link from "./Base/Link" | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" | import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" | ||||||
| import { OsmTags, WayId } from "../Models/OsmFeature" | import { WayId } from "../Models/OsmFeature" | ||||||
| import MoveWizard from "./Popup/MoveWizard" |  | ||||||
| import SplitRoadWizard from "./Popup/SplitRoadWizard" | import SplitRoadWizard from "./Popup/SplitRoadWizard" | ||||||
| import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" | import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" | ||||||
| import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" | import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" | ||||||
|  | @ -82,6 +81,8 @@ import MarkAsFavouriteMini from "./Popup/MarkAsFavouriteMini.svelte" | ||||||
| import NextChangeViz from "./OpeningHours/NextChangeViz.svelte" | import NextChangeViz from "./OpeningHours/NextChangeViz.svelte" | ||||||
| import NearbyImages from "./Image/NearbyImages.svelte" | import NearbyImages from "./Image/NearbyImages.svelte" | ||||||
| import NearbyImagesCollapsed from "./Image/NearbyImagesCollapsed.svelte" | import NearbyImagesCollapsed from "./Image/NearbyImagesCollapsed.svelte" | ||||||
|  | import { svelte } from "@sveltejs/vite-plugin-svelte" | ||||||
|  | import MoveWizard from "./Popup/MoveWizard.svelte" | ||||||
| 
 | 
 | ||||||
| class NearbyImageVis implements SpecialVisualization { | class NearbyImageVis implements SpecialVisualization { | ||||||
|     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 |     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 | ||||||
|  | @ -515,12 +516,11 @@ export default class SpecialVisualizations { | ||||||
|                         return undefined |                         return undefined | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     return new MoveWizard( |                     return new SvelteUIElement(MoveWizard, { | ||||||
|                         <Feature<Point>>feature, |  | ||||||
|                         <UIEventSource<OsmTags>>tagSource, |  | ||||||
|                         state, |                         state, | ||||||
|                         layer.allowMove |                         featureToMove: feature, | ||||||
|                     ) |                         layer, | ||||||
|  |                     }) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|  |  | ||||||
|  | @ -89,7 +89,7 @@ | ||||||
|      |      | ||||||
|   }) |   }) | ||||||
|    |    | ||||||
|   let selectedLayer: UIEventSource<LayerConfig> = state.selectedElement.mapD(element => state.layout.getMatchingLayer(element.properties)); |   let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD(element => state.layout.getMatchingLayer(element.properties)); | ||||||
| 
 | 
 | ||||||
|   let currentZoom = state.mapProperties.zoom; |   let currentZoom = state.mapProperties.zoom; | ||||||
|   let showCrosshair = state.userRelatedState.showCrosshair; |   let showCrosshair = state.userRelatedState.showCrosshair; | ||||||
|  | @ -125,7 +125,6 @@ | ||||||
|         bounds={state.mapProperties.bounds} |         bounds={state.mapProperties.bounds} | ||||||
|         perLayer={state.perLayer} |         perLayer={state.perLayer} | ||||||
|         selectedElement={state.selectedElement} |         selectedElement={state.selectedElement} | ||||||
|         {selectedLayer} |  | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|   </If> |   </If> | ||||||
|  | @ -144,7 +143,6 @@ | ||||||
|     {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} |     {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} | ||||||
|       <MapControlButton |       <MapControlButton | ||||||
|         on:click={() => { |         on:click={() => { | ||||||
|           selectedLayer.setData(currentViewLayer) |  | ||||||
|           selectedElement.setData(state.currentView.features?.data?.[0]) |           selectedElement.setData(state.currentView.features?.data?.[0]) | ||||||
|         }} |         }} | ||||||
|       > |       > | ||||||
|  | @ -269,7 +267,7 @@ | ||||||
|     > |     > | ||||||
|       <XCircleIcon /> |       <XCircleIcon /> | ||||||
|     </div> |     </div> | ||||||
|     <ImageOperations clss="focusable" image={$previewedImage} /> |     <ImageOperations image={$previewedImage} /> | ||||||
|   </FloatOver> |   </FloatOver> | ||||||
| </If> | </If> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1658,7 +1658,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|             } |             } | ||||||
|             const child = <HTMLElement>childs.item(0) |             const child = <HTMLElement>childs.item(0) | ||||||
|             if (child === null) { |             if (child === null) { | ||||||
|                 console.log("Focussing on child element: no child element found for", el) |  | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|             if ( |             if ( | ||||||
|  | @ -1668,7 +1667,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|             ) { |             ) { | ||||||
|                 child.setAttribute("tabindex", "-1") |                 child.setAttribute("tabindex", "-1") | ||||||
|             } |             } | ||||||
|             console.log("Focussing on", child) |  | ||||||
|             child?.focus() |             child?.focus() | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -69,12 +69,11 @@ body { | ||||||
|     color: var(--foreground-color); |     color: var(--foreground-color); | ||||||
|     font-family: "Helvetica Neue", Arial, sans-serif; |     font-family: "Helvetica Neue", Arial, sans-serif; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| .focusable { | .focusable { | ||||||
|     /* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */ |     /* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */ | ||||||
|     border: 1px solid red |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| svg, | svg, | ||||||
| img { | img { | ||||||
|     box-sizing: content-box; |     box-sizing: content-box; | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ | ||||||
| <body> | <body> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <div class="h-full" id="maindiv"> | <div class="h-screen" id="maindiv"> | ||||||
|     <div id="default-main h-full"> |     <div id="default-main h-full"> | ||||||
|         <div class="w-full h-screen flex flex-col items-center justify-between  p-8"> |         <div class="w-full h-screen flex flex-col items-center justify-between  p-8"> | ||||||
|             <div class="w-full h-full flex flex-col items-center"> |             <div class="w-full h-full flex flex-col items-center"> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue