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; | ||||
| } | ||||
| 
 | ||||
| .right-1\/3 { | ||||
|   right: 33.333333%; | ||||
| } | ||||
| 
 | ||||
| .right-0 { | ||||
|   right: 0px; | ||||
| } | ||||
| 
 | ||||
| .right-10 { | ||||
|   right: 2.5rem; | ||||
| .right-1\/3 { | ||||
|   right: 33.333333%; | ||||
| } | ||||
| 
 | ||||
| .top-10 { | ||||
|   top: 2.5rem; | ||||
| .top-4 { | ||||
|   top: 1rem; | ||||
| } | ||||
| 
 | ||||
| .right-4 { | ||||
|   right: 1rem; | ||||
| } | ||||
| 
 | ||||
| .left-1\/4 { | ||||
|  | @ -1236,10 +1236,6 @@ input[type="range"].range-lg::-moz-range-thumb { | |||
|   top: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .top-4 { | ||||
|   top: 1rem; | ||||
| } | ||||
| 
 | ||||
| .top-1 { | ||||
|   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); | ||||
| } | ||||
| 
 | ||||
| .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 { | ||||
|   --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); | ||||
|  |  | |||
|  | @ -1,46 +1,18 @@ | |||
| <script lang="ts"> | ||||
|   // A fake 'page' which can be shown; kind of a modal | ||||
|   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 | ||||
|   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 " | ||||
|   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 fullscreen: boolean = false | ||||
|   export let shown: UIEventSource<boolean> | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if !onlyLink} | ||||
|   <Modal open={_shown} on:close={() => shown.set(false)} outsideclose | ||||
|          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> | ||||
| <Popup {shown} {bodyPadding} {fullscreen}/> | ||||
| {:else} | ||||
|   <button class="as-link sidebar-button" on:click={() => shown.setData(true)}> | ||||
|     <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 { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   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> | ||||
|   let fallbackImage: string = undefined | ||||
|  | @ -16,21 +20,42 @@ | |||
| 
 | ||||
|   let imgEl: HTMLImageElement | ||||
|   export let imgClass: string = undefined | ||||
|   export let previewedImage: UIEventSource<ProvidedImage> = undefined | ||||
|   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 showBigPreview =  new UIEventSource(false) | ||||
|   onDestroy(showBigPreview.addCallbackAndRun(shown=>{ | ||||
|     if(!shown){ | ||||
|       previewedImage.set(false) | ||||
|     } | ||||
|   })) | ||||
|   onDestroy(previewedImage.addCallbackAndRun(previewedImage => { | ||||
|     showBigPreview.set(previewedImage?.id === image.id) | ||||
|   })) | ||||
| </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 w-fit"> | ||||
|     <img | ||||
|       bind:this={imgEl} | ||||
|       on:load={() => loaded = true} | ||||
|       class={imgClass ?? ""} | ||||
|       class:cursor-zoom-in={previewedImage !== undefined} | ||||
|       class:cursor-zoom-in={canZoom} | ||||
|       on:click={() => { | ||||
|       previewedImage?.setData(image) | ||||
|         previewedImage.set(image) | ||||
|     }} | ||||
|       on:error={() => { | ||||
|       if (fallbackImage) { | ||||
|  | @ -41,8 +66,9 @@ | |||
|     /> | ||||
| 
 | ||||
|     {#if canZoom && loaded} | ||||
|       <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" /> | ||||
|       <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" /> | ||||
|       </div> | ||||
|     {/if} | ||||
| 
 | ||||
|  |  | |||
|  | @ -45,6 +45,8 @@ | |||
|       <ImageAttribution {image} attributionFormat="large"/> | ||||
|     </div> | ||||
| 
 | ||||
|     <slot/> | ||||
| 
 | ||||
|     <button | ||||
|       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()} | ||||
|  |  | |||
|  | @ -72,7 +72,19 @@ | |||
|     imgClass="max-h-64 w-auto" | ||||
|     previewedImage={state.previewedImage} | ||||
|     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}> | ||||
|     {#if linkable} | ||||
|       <label> | ||||
|  |  | |||
|  | @ -13,9 +13,7 @@ | |||
|   import type { MapProperties } from "../Models/MapProperties" | ||||
|   import Geosearch from "./BigComponents/Geosearch.svelte" | ||||
|   import Translations from "./i18n/Translations" | ||||
|   import { | ||||
|     MenuIcon, | ||||
|   } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import Tr from "./Base/Tr.svelte" | ||||
|   import FloatOver from "./Base/FloatOver.svelte" | ||||
|   import Constants from "../Models/Constants" | ||||
|  | @ -32,7 +30,6 @@ | |||
|   import Min from "../assets/svg/Min.svelte" | ||||
|   import Plus from "../assets/svg/Plus.svelte" | ||||
|   import Filter from "../assets/svg/Filter.svelte" | ||||
|   import ImageOperations from "./Image/ImageOperations.svelte" | ||||
|   import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte" | ||||
|   import { Orientation } from "../Sensors/Orientation" | ||||
|   import GeolocationIndicator from "./BigComponents/GeolocationIndicator.svelte" | ||||
|  | @ -47,7 +44,7 @@ | |||
|   import DrawerLeft from "./Base/DrawerLeft.svelte" | ||||
|   import Hash from "../Logic/Web/Hash" | ||||
|   import { Drawer } from "flowbite-svelte" | ||||
|   import { sineIn } from "svelte/easing" | ||||
|   import { linear, sineIn } from "svelte/easing" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let layout = state.layout | ||||
|  | @ -58,20 +55,26 @@ | |||
|   let compassLoaded = Orientation.singleton.gotMeasurement | ||||
|   Orientation.singleton.startMeasurements() | ||||
| 
 | ||||
|   state.selectedElement.addCallback((selected) => { | ||||
|     if (!selected) { | ||||
|       selectedElement.setData(selected) | ||||
|   let slideDuration = 150 // ms | ||||
|   state.selectedElement.addCallback((value) => { | ||||
|     if (!value) { | ||||
|       selectedElement.setData(undefined) | ||||
|       return | ||||
|     } | ||||
|     if (selected !== selectedElement.data) { | ||||
|       // We first set the selected element to 'undefined' to force the popup to close... | ||||
|       selectedElement.setData(undefined) | ||||
|     if(!selectedElement.data){ | ||||
|       // The store for this component doesn't have value right now, so we can simply set it | ||||
|       selectedElement.set(value) | ||||
|       return | ||||
|     } | ||||
|     // ... we give svelte some time to update with requestAnimationFrame ... | ||||
|     window.requestAnimationFrame(() => { | ||||
|       // ... and we force a fresh popup window | ||||
|       selectedElement.setData(selected) | ||||
|     }) | ||||
|     // We first set the selected element to 'undefined' to force the popup to close... | ||||
|     selectedElement.setData(undefined) | ||||
|     // ... and we give svelte some time to update with requestAnimationFrame ... | ||||
|     window.setTimeout(() => { | ||||
|       window.requestAnimationFrame(() => { | ||||
|         // ... and we force a fresh popup window | ||||
|         selectedElement.setData(value) | ||||
|       }) | ||||
|     }, slideDuration) | ||||
|   }) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -143,7 +146,6 @@ | |||
|       rasterLayerName = l.properties.name | ||||
|     }), | ||||
|   ) | ||||
|   let previewedImage = state.previewedImage | ||||
|   let addNewFeatureMode = state.userRelatedState.addNewFeatureMode | ||||
|   let gpsAvailable = state.geolocation.geolocationState.gpsAvailable | ||||
|   let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation | ||||
|  | @ -433,14 +435,14 @@ | |||
|       rightOffset="inset-y-0 right-0" | ||||
|       transitionParams={ { | ||||
|     x: 640, | ||||
|     duration: 200, | ||||
|     easing: sineIn | ||||
|     duration: slideDuration, | ||||
|     easing: linear | ||||
|   }} | ||||
|       divClass="overflow-y-auto z-50 " | ||||
|       hidden={$selectedElement === undefined} | ||||
|       on:close={() => {      state.selectedElement.setData(undefined) | ||||
|     }} | ||||
|       > | ||||
|     > | ||||
|       <div slot="close-button" /> | ||||
|       <SelectedElementPanel {state} selected={$state_selectedElement} /> | ||||
|     </Drawer> | ||||
|  | @ -469,12 +471,4 @@ | |||
|     {/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> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue