forked from MapComplete/MapComplete
		
	Refactoring: use popups for attributed images
This commit is contained in:
		
							parent
							
								
									f026ee6db9
								
							
						
					
					
						commit
						7565f13e39
					
				
					 7 changed files with 129 additions and 85 deletions
				
			
		|  | @ -1160,20 +1160,20 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   left: 0px; |   left: 0px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .right-1\/3 { |  | ||||||
|   right: 33.333333%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .right-0 { | .right-0 { | ||||||
|   right: 0px; |   right: 0px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .right-10 { | .right-1\/3 { | ||||||
|   right: 2.5rem; |   right: 33.333333%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .top-10 { | .top-4 { | ||||||
|   top: 2.5rem; |   top: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .right-4 { | ||||||
|  |   right: 1rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .left-1\/4 { | .left-1\/4 { | ||||||
|  | @ -1236,10 +1236,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   top: 0.75rem; |   top: 0.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .top-4 { |  | ||||||
|   top: 1rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .top-1 { | .top-1 { | ||||||
|   top: 0.25rem; |   top: 0.25rem; | ||||||
| } | } | ||||||
|  | @ -4861,11 +4857,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); |   filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .drop-shadow-2xl { |  | ||||||
|   --tw-drop-shadow: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15)); |  | ||||||
|   filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .drop-shadow-md { | .drop-shadow-md { | ||||||
|   --tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06)); |   --tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06)); | ||||||
|   filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); |   filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); | ||||||
|  |  | ||||||
|  | @ -1,46 +1,18 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   // A fake 'page' which can be shown; kind of a modal |   // A fake 'page' which can be shown; kind of a modal | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import { Modal } from "flowbite-svelte" |   import Popup from "./Popup.svelte" | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   export let shown: UIEventSource<boolean> |  | ||||||
|   let _shown = false |  | ||||||
|   export let onlyLink: boolean = false |   export let onlyLink: boolean = false | ||||||
|   shown.addCallbackAndRun(sh => { |  | ||||||
|     _shown = sh |  | ||||||
|   }) |  | ||||||
|   export let fullscreen: boolean = false |  | ||||||
| 
 |  | ||||||
|   const shared = "in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md" |  | ||||||
|   let defaultClass = "relative flex flex-col mx-auto w-full divide-y " + shared |  | ||||||
|   if (fullscreen) { |  | ||||||
|     defaultClass = shared |  | ||||||
|   } |  | ||||||
|   let dialogClass = "fixed top-0 start-0 end-0 h-modal inset-0 z-50 w-full p-4 flex" |  | ||||||
|   if (fullscreen) { |  | ||||||
|     dialogClass += " h-full-child" |  | ||||||
|   } |  | ||||||
|   export let bodyPadding = "p-4 md:p-5 " |   export let bodyPadding = "p-4 md:p-5 " | ||||||
|   let bodyClass = bodyPadding+" h-full space-y-4 flex-1 overflow-y-auto overscroll-contain" |   export let fullscreen: boolean = false | ||||||
| 
 |   export let shown: UIEventSource<boolean> | ||||||
|   let headerClass = "flex justify-between items-center p-2 px-4 md:px-5 rounded-t-lg"; |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if !onlyLink} | {#if !onlyLink} | ||||||
|   <Modal open={_shown} on:close={() => shown.set(false)} outsideclose | <Popup {shown} {bodyPadding} {fullscreen}/> | ||||||
|          size="xl" |  | ||||||
|          {defaultClass} {bodyClass} {dialogClass} {headerClass} |  | ||||||
|          color="none"> |  | ||||||
|     <h1 slot="header" class="page-header w-full"> |  | ||||||
|       <slot name="header" /> |  | ||||||
|     </h1> |  | ||||||
|     <slot /> |  | ||||||
|     {#if $$slots.footer} |  | ||||||
|       <slot name="footer" /> |  | ||||||
|     {/if} |  | ||||||
|   </Modal> |  | ||||||
| {:else} | {:else} | ||||||
|   <button class="as-link sidebar-button" on:click={() => shown.setData(true)}> |   <button class="as-link sidebar-button" on:click={() => shown.setData(true)}> | ||||||
|     <slot name="link"> |     <slot name="link"> | ||||||
|  |  | ||||||
							
								
								
									
										47
									
								
								src/UI/Base/Popup.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/UI/Base/Popup.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { Modal } from "flowbite-svelte" | ||||||
|  |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Basically a flowbite-svelte modal made more ergonomical | ||||||
|  |    */ | ||||||
|  | 
 | ||||||
|  |   export let fullscreen: boolean = false | ||||||
|  | 
 | ||||||
|  |   const shared = "in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md" | ||||||
|  |   let defaultClass = "relative flex flex-col mx-auto w-full divide-y " + shared | ||||||
|  |   if (fullscreen) { | ||||||
|  |     defaultClass = shared | ||||||
|  |   } | ||||||
|  |   let dialogClass = "fixed top-0 start-0 end-0 h-modal inset-0 z-50 w-full p-4 flex" | ||||||
|  |   if (fullscreen) { | ||||||
|  |     dialogClass += " h-full-child" | ||||||
|  |   } | ||||||
|  |   export let bodyPadding = "p-4 md:p-5 " | ||||||
|  |   let bodyClass = bodyPadding + " h-full space-y-4 flex-1 overflow-y-auto overscroll-contain" | ||||||
|  | 
 | ||||||
|  |   let headerClass = "flex justify-between items-center p-2 px-4 md:px-5 rounded-t-lg" | ||||||
|  | 
 | ||||||
|  |   export let shown: UIEventSource<boolean> | ||||||
|  |   let _shown = false | ||||||
|  |   shown.addCallbackAndRun(sh => { | ||||||
|  |     _shown = sh | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <Modal open={_shown} on:close={() => shown.set(false)} outsideclose | ||||||
|  |        size="xl" | ||||||
|  |        dismissable={false} | ||||||
|  |        {defaultClass} {bodyClass} {dialogClass} {headerClass} | ||||||
|  |        color="none"> | ||||||
|  |   <h1 slot="header" class="page-header w-full"> | ||||||
|  |     <slot name="header" /> | ||||||
|  |   </h1> | ||||||
|  |   <slot /> | ||||||
|  |   {#if $$slots.footer} | ||||||
|  |     <slot name="footer" /> | ||||||
|  |   {/if} | ||||||
|  | </Modal> | ||||||
|  | @ -7,6 +7,10 @@ | ||||||
|   import { Mapillary } from "../../Logic/ImageProviders/Mapillary" |   import { Mapillary } from "../../Logic/ImageProviders/Mapillary" | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline" |   import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline" | ||||||
|  |   import { CloseButton, Modal } from "flowbite-svelte" | ||||||
|  |   import ImageOperations from "./ImageOperations.svelte" | ||||||
|  |   import Popup from "../Base/Popup.svelte" | ||||||
|  |   import { onDestroy } from "svelte" | ||||||
| 
 | 
 | ||||||
|   export let image: Partial<ProvidedImage> |   export let image: Partial<ProvidedImage> | ||||||
|   let fallbackImage: string = undefined |   let fallbackImage: string = undefined | ||||||
|  | @ -16,21 +20,42 @@ | ||||||
| 
 | 
 | ||||||
|   let imgEl: HTMLImageElement |   let imgEl: HTMLImageElement | ||||||
|   export let imgClass: string = undefined |   export let imgClass: string = undefined | ||||||
|   export let previewedImage: UIEventSource<ProvidedImage> = undefined |  | ||||||
|   export let attributionFormat: "minimal" | "medium" | "large" = "medium" |   export let attributionFormat: "minimal" | "medium" | "large" = "medium" | ||||||
|   let canZoom = previewedImage !== undefined // We check if there is a SOURCE, not if there is data in it! |   export let previewedImage: UIEventSource<ProvidedImage> | ||||||
|  |   export let canZoom = previewedImage !== undefined | ||||||
|   let loaded = false |   let loaded = false | ||||||
|  |   let showBigPreview =  new UIEventSource(false) | ||||||
|  |   onDestroy(showBigPreview.addCallbackAndRun(shown=>{ | ||||||
|  |     if(!shown){ | ||||||
|  |       previewedImage.set(false) | ||||||
|  |     } | ||||||
|  |   })) | ||||||
|  |   onDestroy(previewedImage.addCallbackAndRun(previewedImage => { | ||||||
|  |     showBigPreview.set(previewedImage?.id === image.id) | ||||||
|  |   })) | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <Popup shown={showBigPreview} bodyPadding="p-0"> | ||||||
|  |   <div slot="close" /> | ||||||
|  |   <div style="height: 80vh"> | ||||||
|  |     <ImageOperations {image}> | ||||||
|  |       <slot name="preview-action" /> | ||||||
|  |     </ImageOperations> | ||||||
|  |   </div> | ||||||
|  |   <div class="absolute top-4 right-4"> | ||||||
|  |     <CloseButton class="normal-background" | ||||||
|  |                  on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton> | ||||||
|  |   </div> | ||||||
|  | </Popup> | ||||||
| <div class="relative shrink-0"> | <div class="relative shrink-0"> | ||||||
|   <div class="relative w-fit"> |   <div class="relative w-fit"> | ||||||
|     <img |     <img | ||||||
|       bind:this={imgEl} |       bind:this={imgEl} | ||||||
|       on:load={() => loaded = true} |       on:load={() => loaded = true} | ||||||
|       class={imgClass ?? ""} |       class={imgClass ?? ""} | ||||||
|       class:cursor-zoom-in={previewedImage !== undefined} |       class:cursor-zoom-in={canZoom} | ||||||
|       on:click={() => { |       on:click={() => { | ||||||
|       previewedImage?.setData(image) |         previewedImage.set(image) | ||||||
|     }} |     }} | ||||||
|       on:error={() => { |       on:error={() => { | ||||||
|       if (fallbackImage) { |       if (fallbackImage) { | ||||||
|  | @ -41,7 +66,8 @@ | ||||||
|     /> |     /> | ||||||
| 
 | 
 | ||||||
|     {#if canZoom && loaded} |     {#if canZoom && loaded} | ||||||
|       <div class="absolute right-0 top-0 bg-black-transparent rounded-bl-full" on:click={() => previewedImage.set(image)}> |       <div class="absolute right-0 top-0 bg-black-transparent rounded-bl-full" | ||||||
|  |            on:click={() => previewedImage.set(image)}> | ||||||
|         <MagnifyingGlassPlusIcon class="w-8 h-8 pl-3 pb-3 cursor-zoom-in" color="white" /> |         <MagnifyingGlassPlusIcon class="w-8 h-8 pl-3 pb-3 cursor-zoom-in" color="white" /> | ||||||
|       </div> |       </div> | ||||||
|     {/if} |     {/if} | ||||||
|  |  | ||||||
|  | @ -45,6 +45,8 @@ | ||||||
|       <ImageAttribution {image} attributionFormat="large"/> |       <ImageAttribution {image} attributionFormat="large"/> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|  |     <slot/> | ||||||
|  | 
 | ||||||
|     <button |     <button | ||||||
|       class="no-image-background pointer-events-auto flex items-center bg-black text-white opacity-50 transition-colors duration-200 hover:opacity-100" |       class="no-image-background pointer-events-auto flex items-center bg-black text-white opacity-50 transition-colors duration-200 hover:opacity-100" | ||||||
|       on:click={() => download()} |       on:click={() => download()} | ||||||
|  |  | ||||||
|  | @ -72,7 +72,19 @@ | ||||||
|     imgClass="max-h-64 w-auto" |     imgClass="max-h-64 w-auto" | ||||||
|     previewedImage={state.previewedImage} |     previewedImage={state.previewedImage} | ||||||
|     attributionFormat="minimal" |     attributionFormat="minimal" | ||||||
|   /> |   > | ||||||
|  |     <!-- | ||||||
|  |     <div slot="preview-action" class="self-center" > | ||||||
|  |     <LoginToggle {state} silentFail={true}> | ||||||
|  |       {#if linkable} | ||||||
|  |         <label class="normal-background p-2 rounded-full pointer-events-auto"> | ||||||
|  |           <input bind:checked={$isLinked} type="checkbox" /> | ||||||
|  |           <SpecialTranslation t={t.link} {tags} {state} {layer} {feature} /> | ||||||
|  |         </label> | ||||||
|  |       {/if} | ||||||
|  |     </LoginToggle> | ||||||
|  |     </div>--> | ||||||
|  |   </AttributedImage> | ||||||
|   <LoginToggle {state} silentFail={true}> |   <LoginToggle {state} silentFail={true}> | ||||||
|     {#if linkable} |     {#if linkable} | ||||||
|       <label> |       <label> | ||||||
|  |  | ||||||
|  | @ -13,9 +13,7 @@ | ||||||
|   import type { MapProperties } from "../Models/MapProperties" |   import type { MapProperties } from "../Models/MapProperties" | ||||||
|   import Geosearch from "./BigComponents/Geosearch.svelte" |   import Geosearch from "./BigComponents/Geosearch.svelte" | ||||||
|   import Translations from "./i18n/Translations" |   import Translations from "./i18n/Translations" | ||||||
|   import { |   import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|     MenuIcon, |  | ||||||
|   } from "@rgossiaux/svelte-heroicons/solid" |  | ||||||
|   import Tr from "./Base/Tr.svelte" |   import Tr from "./Base/Tr.svelte" | ||||||
|   import FloatOver from "./Base/FloatOver.svelte" |   import FloatOver from "./Base/FloatOver.svelte" | ||||||
|   import Constants from "../Models/Constants" |   import Constants from "../Models/Constants" | ||||||
|  | @ -32,7 +30,6 @@ | ||||||
|   import Min from "../assets/svg/Min.svelte" |   import Min from "../assets/svg/Min.svelte" | ||||||
|   import Plus from "../assets/svg/Plus.svelte" |   import Plus from "../assets/svg/Plus.svelte" | ||||||
|   import Filter from "../assets/svg/Filter.svelte" |   import Filter from "../assets/svg/Filter.svelte" | ||||||
|   import ImageOperations from "./Image/ImageOperations.svelte" |  | ||||||
|   import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte" |   import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte" | ||||||
|   import { Orientation } from "../Sensors/Orientation" |   import { Orientation } from "../Sensors/Orientation" | ||||||
|   import GeolocationIndicator from "./BigComponents/GeolocationIndicator.svelte" |   import GeolocationIndicator from "./BigComponents/GeolocationIndicator.svelte" | ||||||
|  | @ -47,7 +44,7 @@ | ||||||
|   import DrawerLeft from "./Base/DrawerLeft.svelte" |   import DrawerLeft from "./Base/DrawerLeft.svelte" | ||||||
|   import Hash from "../Logic/Web/Hash" |   import Hash from "../Logic/Web/Hash" | ||||||
|   import { Drawer } from "flowbite-svelte" |   import { Drawer } from "flowbite-svelte" | ||||||
|   import { sineIn } from "svelte/easing" |   import { linear, sineIn } from "svelte/easing" | ||||||
| 
 | 
 | ||||||
|   export let state: ThemeViewState |   export let state: ThemeViewState | ||||||
|   let layout = state.layout |   let layout = state.layout | ||||||
|  | @ -58,20 +55,26 @@ | ||||||
|   let compassLoaded = Orientation.singleton.gotMeasurement |   let compassLoaded = Orientation.singleton.gotMeasurement | ||||||
|   Orientation.singleton.startMeasurements() |   Orientation.singleton.startMeasurements() | ||||||
| 
 | 
 | ||||||
|   state.selectedElement.addCallback((selected) => { |   let slideDuration = 150 // ms | ||||||
|     if (!selected) { |   state.selectedElement.addCallback((value) => { | ||||||
|       selectedElement.setData(selected) |     if (!value) { | ||||||
|  |       selectedElement.setData(undefined) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     if(!selectedElement.data){ | ||||||
|  |       // The store for this component doesn't have value right now, so we can simply set it | ||||||
|  |       selectedElement.set(value) | ||||||
|       return |       return | ||||||
|     } |     } | ||||||
|     if (selected !== selectedElement.data) { |  | ||||||
|     // We first set the selected element to 'undefined' to force the popup to close... |     // We first set the selected element to 'undefined' to force the popup to close... | ||||||
|     selectedElement.setData(undefined) |     selectedElement.setData(undefined) | ||||||
|     } |     // ... and we give svelte some time to update with requestAnimationFrame ... | ||||||
|     // ... we give svelte some time to update with requestAnimationFrame ... |     window.setTimeout(() => { | ||||||
|       window.requestAnimationFrame(() => { |       window.requestAnimationFrame(() => { | ||||||
|         // ... and we force a fresh popup window |         // ... and we force a fresh popup window | ||||||
|       selectedElement.setData(selected) |         selectedElement.setData(value) | ||||||
|       }) |       }) | ||||||
|  |     }, slideDuration) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -143,7 +146,6 @@ | ||||||
|       rasterLayerName = l.properties.name |       rasterLayerName = l.properties.name | ||||||
|     }), |     }), | ||||||
|   ) |   ) | ||||||
|   let previewedImage = state.previewedImage |  | ||||||
|   let addNewFeatureMode = state.userRelatedState.addNewFeatureMode |   let addNewFeatureMode = state.userRelatedState.addNewFeatureMode | ||||||
|   let gpsAvailable = state.geolocation.geolocationState.gpsAvailable |   let gpsAvailable = state.geolocation.geolocationState.gpsAvailable | ||||||
|   let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation |   let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation | ||||||
|  | @ -433,8 +435,8 @@ | ||||||
|       rightOffset="inset-y-0 right-0" |       rightOffset="inset-y-0 right-0" | ||||||
|       transitionParams={ { |       transitionParams={ { | ||||||
|     x: 640, |     x: 640, | ||||||
|     duration: 200, |     duration: slideDuration, | ||||||
|     easing: sineIn |     easing: linear | ||||||
|   }} |   }} | ||||||
|       divClass="overflow-y-auto z-50 " |       divClass="overflow-y-auto z-50 " | ||||||
|       hidden={$selectedElement === undefined} |       hidden={$selectedElement === undefined} | ||||||
|  | @ -469,12 +471,4 @@ | ||||||
|     {/if} |     {/if} | ||||||
|   {/if} |   {/if} | ||||||
| 
 | 
 | ||||||
|   <!-- Image preview --> |  | ||||||
|   <If condition={state.previewedImage.map((i) => i !== undefined)}> |  | ||||||
|     <FloatOver on:close={() => state.previewedImage.setData(undefined)}> |  | ||||||
|       <ImageOperations image={$previewedImage} /> |  | ||||||
|     </FloatOver> |  | ||||||
|   </If> |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| </main> | </main> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue