forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						423618847b
					
				
					 334 changed files with 9307 additions and 6025 deletions
				
			
		|  | @ -3,28 +3,27 @@ | |||
|   import { sineIn } from "svelte/easing" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource.js" | ||||
| 
 | ||||
|   export let shown: UIEventSource<boolean>; | ||||
|   export let shown: UIEventSource<boolean> | ||||
|   let transitionParams = { | ||||
|     x: -320, | ||||
|     duration: 200, | ||||
|     easing: sineIn | ||||
|   }; | ||||
|     easing: sineIn, | ||||
|   } | ||||
|   let hidden = !shown.data | ||||
|   $: { | ||||
|     shown.setData(!hidden) | ||||
|   } | ||||
|   shown.addCallback(sh => { | ||||
|   shown.addCallback((sh) => { | ||||
|     hidden = !sh | ||||
|   }) | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <Drawer placement="left" | ||||
|         transitionType="fly" {transitionParams} | ||||
|         divClass = "overflow-y-auto z-50 " | ||||
|         bind:hidden={hidden}> | ||||
|   <slot> | ||||
|     CONTENTS | ||||
|   </slot> | ||||
| <Drawer | ||||
|   placement="left" | ||||
|   transitionType="fly" | ||||
|   {transitionParams} | ||||
|   divClass="overflow-y-auto z-50 " | ||||
|   bind:hidden | ||||
| > | ||||
|   <slot>CONTENTS</slot> | ||||
| </Drawer> | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ | |||
|   }} | ||||
| > | ||||
|   <div | ||||
|     class="content relative normal-background pointer-events-auto h-full" | ||||
|     class="content normal-background pointer-events-auto relative h-full" | ||||
|     on:click|stopPropagation={() => {}} | ||||
|   > | ||||
|     <div class="h-full rounded-xl"> | ||||
|  | @ -39,20 +39,16 @@ | |||
|     <slot name="close-button"> | ||||
|       <!-- The close button is placed _after_ the default slot in order to always paint it on top --> | ||||
|       <div class="absolute top-0 right-0"> | ||||
| 
 | ||||
|         <CloseButton class="normal-background mt-2 mr-2" | ||||
|                      on:click={() => dispatch("close")} | ||||
|         /> | ||||
|         <CloseButton class="normal-background mt-2 mr-2" on:click={() => dispatch("close")} /> | ||||
|       </div> | ||||
| 
 | ||||
|     </slot> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     .content { | ||||
|         border-radius: 0.5rem; | ||||
|         overflow-x: hidden; | ||||
|         box-shadow: 0 0 1rem #00000088; | ||||
|     } | ||||
|   .content { | ||||
|     border-radius: 0.5rem; | ||||
|     overflow-x: hidden; | ||||
|     box-shadow: 0 0 1rem #00000088; | ||||
|   } | ||||
| </style> | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ | |||
|   export let text: string | ||||
|   export let href: string | ||||
| 
 | ||||
| 
 | ||||
|   export let classnames: string = undefined | ||||
|   export let download: string = undefined | ||||
|   export let ariaLabel: string = undefined | ||||
|  | @ -13,7 +12,7 @@ | |||
| </script> | ||||
| 
 | ||||
| <a | ||||
|   href={Utils.prepareHref(href) } | ||||
|   href={Utils.prepareHref(href)} | ||||
|   aria-label={ariaLabel} | ||||
|   title={ariaLabel} | ||||
|   target={newTab ? "_blank" : undefined} | ||||
|  |  | |||
|  | @ -14,6 +14,6 @@ | |||
|     osmConnection.LogOut() | ||||
|   }} | ||||
| > | ||||
|   <ArrowRightOnRectangle class="h-6 w-6 max-h-full" /> | ||||
|   <ArrowRightOnRectangle class="h-6 max-h-full w-6" /> | ||||
|   <Tr t={Translations.t.general.logout} /> | ||||
| </button> | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ | |||
|   export let arialabel: Translation = undefined | ||||
|   export let arialabelDynamic: Store<Translation> = new ImmutableStore(arialabel) | ||||
|   let arialabelString = arialabelDynamic.bind((tr) => tr?.current) | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <button | ||||
|  |  | |||
|  | @ -1,59 +1,39 @@ | |||
| <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 bodyPadding = "p-4 md:p-5 " | ||||
|   export let fullscreen: boolean = false | ||||
|   export let shown: UIEventSource<boolean> | ||||
| 
 | ||||
|   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" | ||||
|   } | ||||
|   let bodyClass = "h-full p-4 md:p-5 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"; | ||||
| </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> | ||||
|   <Popup {shown} {bodyPadding} {fullscreen}> | ||||
|     <slot name="header" slot="header" /> | ||||
|     <slot /> | ||||
|     {#if $$slots.footer} | ||||
|       <slot name="footer" /> | ||||
|     {/if} | ||||
|   </Modal> | ||||
|     <slot name="footer" slot="footer" /> | ||||
|   </Popup> | ||||
| {:else} | ||||
|   <button class="as-link sidebar-button" on:click={() => shown.setData(true)}> | ||||
|     <slot name="link"> | ||||
|     <slot name="header" /> | ||||
|       <slot name="header" /> | ||||
|     </slot> | ||||
|   </button> | ||||
| {/if} | ||||
| 
 | ||||
| <style> | ||||
|     :global(.page-header) { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|     } | ||||
|   :global(.page-header) { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|   } | ||||
| 
 | ||||
|   :global(.page-header svg) { | ||||
|       width: 2rem; | ||||
|       height: 2rem; | ||||
|       margin-right: 0.75rem; | ||||
|     width: 2rem; | ||||
|     height: 2rem; | ||||
|     margin-right: 0.75rem; | ||||
|   } | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										55
									
								
								src/UI/Base/Popup.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/UI/Base/Popup.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| <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" | ||||
|   if (!$$slots.header) { | ||||
|     headerClass = "hidden" | ||||
|   } | ||||
|   export let shown: UIEventSource<boolean> | ||||
|   export let dismissable = true | ||||
|   let _shown = false | ||||
|   shown.addCallbackAndRun(sh => { | ||||
|     _shown = sh | ||||
|   }) | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <Modal open={_shown} on:close={() => shown.set(false)} outsideclose | ||||
|        size="xl" | ||||
|        {dismissable} | ||||
|        {defaultClass} {bodyClass} {dialogClass} {headerClass} | ||||
|        color="none"> | ||||
| 
 | ||||
|   <svelte:fragment slot="header"> | ||||
|     {#if $$slots.header} | ||||
|       <h1 class="page-header w-full"> | ||||
|         <slot name="header" /> | ||||
|       </h1> | ||||
|     {/if} | ||||
|   </svelte:fragment> | ||||
|   <slot /> | ||||
|   {#if $$slots.footer} | ||||
|     <slot name="footer" /> | ||||
|   {/if} | ||||
| </Modal> | ||||
|  | @ -7,6 +7,5 @@ | |||
|     <slot /> | ||||
|   </div> | ||||
| 
 | ||||
| 
 | ||||
|   <slot class="border-t-gray-300 mt-1" name="footer" /> | ||||
|   <slot class="mt-1 border-t-gray-300" name="footer" /> | ||||
| </div> | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ | |||
|     const license: SmallLicense = licenses[key] | ||||
|     allLicenses[license.path] = license | ||||
|   } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#each iconAttributions as iconAttribution} | ||||
|  |  | |||
|  | @ -20,7 +20,6 @@ | |||
|   const t = Translations.t.general.attribution | ||||
|   const layoutToUse = state.layout | ||||
| 
 | ||||
| 
 | ||||
|   let maintainer: Translation = undefined | ||||
|   if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") { | ||||
|     maintainer = t.themeBy.Subs({ author: layoutToUse.credits }) | ||||
|  | @ -48,8 +47,6 @@ | |||
|     return Translations.t.general.attribution.attributionBackgroundLayer.Subs(props) | ||||
|   }) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   function calculateDataContributions(contributions: Map<string, number>): Translation { | ||||
|     if (contributions === undefined) { | ||||
|       return undefined | ||||
|  | @ -147,7 +144,6 @@ | |||
|     <Tr t={codeContributors(translators, t.translatedBy)} /> | ||||
|   </div> | ||||
| 
 | ||||
| 
 | ||||
|   <div class="self-end"> | ||||
|     MapComplete {Constants.vNumber} | ||||
|   </div> | ||||
|  |  | |||
							
								
								
									
										0
									
								
								src/UI/BigComponents/Geosearch.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/UI/BigComponents/Geosearch.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -15,31 +15,31 @@ | |||
|   } | ||||
| </script> | ||||
| 
 | ||||
|   <Tr t={t.intro} /> | ||||
|   <table> | ||||
| <Tr t={t.intro} /> | ||||
| <table> | ||||
|   <tr> | ||||
|     <th> | ||||
|       <Tr t={t.key} /> | ||||
|     </th> | ||||
|     <th> | ||||
|       <Tr t={t.action} /> | ||||
|     </th> | ||||
|   </tr> | ||||
|   {#each byKey as [key, doc, alsoTriggeredBy]} | ||||
|     <tr> | ||||
|       <th> | ||||
|         <Tr t={t.key} /> | ||||
|       </th> | ||||
|       <th> | ||||
|         <Tr t={t.action} /> | ||||
|       </th> | ||||
|       <td class="flex items-center justify-center"> | ||||
|         {#if alsoTriggeredBy} | ||||
|           <div class="flex items-center justify-center gap-x-1"> | ||||
|             <div class="literal-code h-fit w-fit">{key}</div> | ||||
|             <div class="literal-code h-fit w-fit">{alsoTriggeredBy}</div> | ||||
|           </div> | ||||
|         {:else} | ||||
|           <div class="literal-code flex h-fit w-fit w-full items-center">{key}</div> | ||||
|         {/if} | ||||
|       </td> | ||||
|       <td> | ||||
|         <Tr t={doc} /> | ||||
|       </td> | ||||
|     </tr> | ||||
|     {#each byKey as [key, doc, alsoTriggeredBy]} | ||||
|       <tr> | ||||
|         <td class="flex items-center justify-center"> | ||||
|           {#if alsoTriggeredBy} | ||||
|             <div class="flex items-center justify-center gap-x-1"> | ||||
|               <div class="literal-code h-fit w-fit">{key}</div> | ||||
|               <div class="literal-code h-fit w-fit">{alsoTriggeredBy}</div> | ||||
|             </div> | ||||
|           {:else} | ||||
|             <div class="literal-code flex h-fit w-fit w-full items-center">{key}</div> | ||||
|           {/if} | ||||
|         </td> | ||||
|         <td> | ||||
|           <Tr t={doc} /> | ||||
|         </td> | ||||
|       </tr> | ||||
|     {/each} | ||||
|   </table> | ||||
|   {/each} | ||||
| </table> | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   // All the relevant links | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import Translations from "../i18n/Translations" | ||||
|  | @ -63,15 +62,19 @@ | |||
|   const t = Translations.t.general.menu | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col p-2 sm:p-3 low-interaction gap-y-2 sm:gap-y-3 h-screen overflow-y-auto"> | ||||
| <div class="low-interaction flex h-screen flex-col gap-y-2 overflow-y-auto p-2 sm:gap-y-3 sm:p-3"> | ||||
|   <div class="flex justify-between"> | ||||
|     <h2> | ||||
|       <Tr t={t.title}/> | ||||
|       <Tr t={t.title} /> | ||||
|     </h2> | ||||
|     <CloseButton on:click={() => {pg.menu.set(false)}} /> | ||||
|     <CloseButton | ||||
|       on:click={() => { | ||||
|         pg.menu.set(false) | ||||
|       }} | ||||
|     /> | ||||
|   </div> | ||||
|   {#if $showHome} | ||||
|     <a class="flex button primary" href={Utils.HomepageLink()}> | ||||
|     <a class="button primary flex" href={Utils.HomepageLink()}> | ||||
|       <Squares2x2 class="h-10 w-10" /> | ||||
|       {#if Utils.isIframe} | ||||
|         <Tr t={Translations.t.general.seeIndex} /> | ||||
|  | @ -81,23 +84,21 @@ | |||
|     </a> | ||||
|   {/if} | ||||
| 
 | ||||
| 
 | ||||
|   <!-- User related: avatar, settings, favourits, logout --> | ||||
|   <SidebarUnit> | ||||
|     <LoginToggle {state}> | ||||
|       <LoginButton osmConnection={state.osmConnection} slot="not-logged-in"></LoginButton> | ||||
|       <div class="flex gap-x-4 items-center"> | ||||
|       <LoginButton osmConnection={state.osmConnection} slot="not-logged-in" /> | ||||
|       <div class="flex items-center gap-x-4"> | ||||
|         {#if $userdetails.img} | ||||
|           <img src={$userdetails.img} class="rounded-full w-14 h-14" /> | ||||
|           <img src={$userdetails.img} class="h-14 w-14 rounded-full" /> | ||||
|         {/if} | ||||
|         <b>{$userdetails.name}</b> | ||||
|       </div> | ||||
|     </LoginToggle> | ||||
| 
 | ||||
| 
 | ||||
|     <Page {onlyLink} shown={pg.usersettings}> | ||||
|     <Page {onlyLink} shown={pg.usersettings} bodyPadding="p-0"> | ||||
|       <svelte:fragment slot="header"> | ||||
|         <CogIcon/> | ||||
|         <CogIcon /> | ||||
|         <Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})} /> | ||||
|       </svelte:fragment> | ||||
| 
 | ||||
|  | @ -112,30 +113,24 @@ | |||
|           highlightedRendering={state.guistate.highlightedUserSetting} | ||||
|           layer={usersettingslayer} | ||||
|           selectedElement={{ | ||||
|                 type: "Feature", | ||||
|                 properties: { id: "settings" }, | ||||
|                 geometry: { type: "Point", coordinates: [0, 0] }, | ||||
|               }} | ||||
| 
 | ||||
|             type: "Feature", | ||||
|             properties: { id: "settings" }, | ||||
|             geometry: { type: "Point", coordinates: [0, 0] }, | ||||
|           }} | ||||
|           {state} | ||||
|           tags={state.userRelatedState.preferencesAsTags} | ||||
|         /> | ||||
|       </LoginToggle> | ||||
| 
 | ||||
| 
 | ||||
|     </Page> | ||||
| 
 | ||||
|     <LoginToggle {state}> | ||||
|       <Page {onlyLink} shown={pg.favourites}> | ||||
| 
 | ||||
|         <svelte:fragment slot="header"> | ||||
|           <HeartIcon /> | ||||
|           <Tr t={Translations.t.favouritePoi.tab} /> | ||||
|         </svelte:fragment> | ||||
| 
 | ||||
| 
 | ||||
|         <h3> | ||||
| 
 | ||||
|           <Tr t={Translations.t.favouritePoi.title} /> | ||||
|         </h3> | ||||
|         <div> | ||||
|  | @ -155,7 +150,6 @@ | |||
| 
 | ||||
|   </SidebarUnit> | ||||
| 
 | ||||
| 
 | ||||
|   <!-- Theme related: documentation links, download, ... --> | ||||
|   <SidebarUnit> | ||||
|     <h3> | ||||
|  | @ -163,12 +157,12 @@ | |||
|     </h3> | ||||
| 
 | ||||
|     <Page {onlyLink} shown={pg.about_theme}> | ||||
|       <div slot="link" class="flex"> | ||||
|         <Marker icons={layout.icon} size="h-6 w-6 mr-2" /> | ||||
|         <Tr t={t.showIntroduction} /> | ||||
|       </div> | ||||
|       <svelte:fragment  slot="header"> | ||||
|       <svelte:fragment slot="link"> | ||||
|         <Marker icons={layout.icon} /> | ||||
|         <Tr t={t.showIntroduction} /> | ||||
|       </svelte:fragment> | ||||
|       <svelte:fragment slot="header"> | ||||
|         <Marker size="h-6 w-6 mr-2"  icons={layout.icon} /> | ||||
|         <Tr t={layout.title} /> | ||||
|       </svelte:fragment> | ||||
|       <ThemeIntroPanel {state} /> | ||||
|  | @ -180,17 +174,16 @@ | |||
| 
 | ||||
|     <Page {onlyLink} shown={pg.share}> | ||||
|       <svelte:fragment slot="header"> | ||||
|         <Share/> | ||||
|         <Share /> | ||||
|         <Tr t={Translations.t.general.sharescreen.title} /> | ||||
|       </svelte:fragment> | ||||
|       <ShareScreen {state} /> | ||||
|     </Page> | ||||
| 
 | ||||
| 
 | ||||
|     {#if state.featureSwitches.featureSwitchEnableExport} | ||||
|       <Page {onlyLink} shown={pg.download}> | ||||
|         <svelte:fragment slot="header"> | ||||
|           <ArrowDownTray  /> | ||||
|           <ArrowDownTray /> | ||||
|           <Tr t={Translations.t.general.download.title} /> | ||||
|         </svelte:fragment> | ||||
|         <DownloadPanel {state} /> | ||||
|  | @ -201,15 +194,15 @@ | |||
|       <a | ||||
|         class="flex" | ||||
|         href={"https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Themes/" + | ||||
|         layout.id + | ||||
|         ".md"} | ||||
|           layout.id + | ||||
|           ".md"} | ||||
|         target="_blank" | ||||
|       > | ||||
|         <DocumentMagnifyingGlass class="h-6 w-6" /> | ||||
|         <Tr | ||||
|           t={Translations.t.general.attribution.openThemeDocumentation.Subs({ | ||||
|           name: layout.title, | ||||
|         })} | ||||
|             name: layout.title, | ||||
|           })} | ||||
|         /> | ||||
|       </a> | ||||
| 
 | ||||
|  | @ -220,7 +213,6 @@ | |||
|     {/if} | ||||
|   </SidebarUnit> | ||||
| 
 | ||||
| 
 | ||||
|   <!-- Other links and tools for the given location: open iD/JOSM; community index, ... --> | ||||
|   <SidebarUnit> | ||||
| 
 | ||||
|  | @ -228,16 +220,14 @@ | |||
|       <Tr t={t.moreUtilsTitle} /> | ||||
|     </h3> | ||||
| 
 | ||||
| 
 | ||||
|     <Page {onlyLink} shown={pg.community_index}> | ||||
|       <svelte:fragment slot="header"> | ||||
|         <Community/> | ||||
|         <Community /> | ||||
|         <Tr t={Translations.t.communityIndex.title} /> | ||||
|       </svelte:fragment> | ||||
|       <CommunityIndexView location={state.mapProperties.location} /> | ||||
|     </Page> | ||||
| 
 | ||||
| 
 | ||||
|     <If condition={featureSwitches.featureSwitchEnableLogin}> | ||||
|       <OpenIdEditor mapProperties={state.mapProperties} /> | ||||
|       <OpenJosm {state} /> | ||||
|  | @ -246,7 +236,6 @@ | |||
| 
 | ||||
|   </SidebarUnit> | ||||
| 
 | ||||
| 
 | ||||
|   <!-- About MC: various outward links, legal info, ... --> | ||||
|   <SidebarUnit> | ||||
| 
 | ||||
|  | @ -254,19 +243,16 @@ | |||
|       <Tr t={Translations.t.general.menu.aboutMapComplete} /> | ||||
|     </h3> | ||||
| 
 | ||||
|     <a | ||||
|       class="flex" | ||||
|       href={window.location.protocol + "//" + window.location.host + "/studio.html"} | ||||
|     > | ||||
|     <a class="flex" href={window.location.protocol + "//" + window.location.host + "/studio.html"}> | ||||
|       <Pencil class="mr-2 h-6 w-6" /> | ||||
|       <Tr t={Translations.t.general.morescreen.createYourOwnTheme} /> | ||||
|     </a> | ||||
| 
 | ||||
|     <div class="hidden-on-mobile w-full"> | ||||
|       <Page {onlyLink} shown={pg.hotkeys}> | ||||
|         <svelte:fragment  slot="header"> | ||||
|         <svelte:fragment slot="header"> | ||||
|           <BoltIcon /> | ||||
|           <Tr t={ Translations.t.hotkeyDocumentation.title} /> | ||||
|           <Tr t={Translations.t.hotkeyDocumentation.title} /> | ||||
|         </svelte:fragment> | ||||
|         <HotkeyTable /> | ||||
|       </Page> | ||||
|  | @ -282,7 +268,6 @@ | |||
|       <Tr t={Translations.t.general.attribution.openIssueTracker} /> | ||||
|     </a> | ||||
| 
 | ||||
| 
 | ||||
|     <a class="flex" href="https://en.osm.town/@MapComplete" target="_blank"> | ||||
|       <Mastodon class="h-6 w-6" /> | ||||
|       <Tr t={Translations.t.general.attribution.followOnMastodon} /> | ||||
|  | @ -293,7 +278,6 @@ | |||
|       <Tr t={Translations.t.general.attribution.donate} /> | ||||
|     </a> | ||||
| 
 | ||||
| 
 | ||||
|     <Page {onlyLink} shown={pg.copyright}> | ||||
|       <svelte:fragment slot="header"> | ||||
|         <Copyright /> | ||||
|  | @ -302,17 +286,14 @@ | |||
|       <CopyrightPanel {state} /> | ||||
|     </Page> | ||||
| 
 | ||||
| 
 | ||||
|     <Page {onlyLink} shown={pg.copyright_icons}> | ||||
|       <svelte:fragment slot="header" > | ||||
|         <Copyright/> | ||||
|         <Tr t={ Translations.t.general.attribution.iconAttribution.title} /> | ||||
|       <svelte:fragment slot="header"> | ||||
|         <Copyright /> | ||||
|         <Tr t={Translations.t.general.attribution.iconAttribution.title} /> | ||||
|       </svelte:fragment> | ||||
|       <CopyrightAllIcons {state} /> | ||||
| 
 | ||||
|     </Page> | ||||
| 
 | ||||
| 
 | ||||
|     <Page {onlyLink} shown={pg.privacy}> | ||||
|       <svelte:fragment slot="header"> | ||||
|         <EyeIcon /> | ||||
|  | @ -321,7 +302,6 @@ | |||
|       <PrivacyPolicy {state} /> | ||||
|     </Page> | ||||
| 
 | ||||
| 
 | ||||
|     <div class="subtle self-end"> | ||||
|       {Constants.vNumber} | ||||
|     </div> | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ export default class MoreScreen { | |||
|     } = themeOverview | ||||
|     public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>() | ||||
|     static { | ||||
|         for (const th of MoreScreen.officialThemes.themes) { | ||||
|         for (const th of MoreScreen.officialThemes.themes ?? []) { | ||||
|             MoreScreen.officialThemesById.set(th.id, th) | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
|   import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||
|   import { Tag } from "../../Logic/Tags/Tag" | ||||
|   import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||
|   import type { WayId } from "../../Models/OsmFeature" | ||||
| 
 | ||||
|   /** | ||||
|    * An advanced location input, which has support to: | ||||
|  | @ -45,11 +46,16 @@ | |||
|   } | ||||
|   export let snapToLayers: string[] | undefined = undefined | ||||
|   export let targetLayer: LayerConfig | undefined = undefined | ||||
|   /** | ||||
|    * If a 'targetLayer' is given, objects of this layer will be shown as well to avoid duplicates | ||||
|    * If you want to hide some of them, blacklist them here | ||||
|    */ | ||||
|   export let dontShow: string[] = [] | ||||
|   export let maxSnapDistance: number = undefined | ||||
|   export let presetProperties: Tag[] = [] | ||||
|   let presetPropertiesUnpacked = TagUtils.KVtoProperties(presetProperties) | ||||
| 
 | ||||
|   export let snappedTo: UIEventSource<string | undefined> | ||||
|   export let snappedTo: UIEventSource<WayId | undefined> | ||||
| 
 | ||||
|   let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{ | ||||
|     lon: number | ||||
|  | @ -57,7 +63,7 @@ | |||
|   }>(undefined) | ||||
| 
 | ||||
|   const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined) | ||||
|   let initialMapProperties: Partial<MapProperties> & { location } = { | ||||
|   export let mapProperties: Partial<MapProperties> & { location } = { | ||||
|     zoom: new UIEventSource<number>(19), | ||||
|     maxbounds: new UIEventSource(undefined), | ||||
|     /*If no snapping needed: the value is simply the map location; | ||||
|  | @ -77,8 +83,11 @@ | |||
| 
 | ||||
|   if (targetLayer) { | ||||
|     // Show already existing items | ||||
|     const featuresForLayer = state.perLayer.get(targetLayer.id) | ||||
|     let featuresForLayer: FeatureSource = state.perLayer.get(targetLayer.id) | ||||
|     if (featuresForLayer) { | ||||
|       if (dontShow) { | ||||
|         featuresForLayer = new StaticFeatureSource(featuresForLayer.features.map(feats => feats.filter(f => dontShow.indexOf(f.properties.id) < 0))) | ||||
|       } | ||||
|       new ShowDataLayer(map, { | ||||
|         layer: targetLayer, | ||||
|         features: featuresForLayer, | ||||
|  | @ -104,13 +113,13 @@ | |||
|     const snappedLocation = new SnappingFeatureSource( | ||||
|       new FeatureSourceMerger(...Utils.NoNull(snapSources)), | ||||
|       // We snap to the (constantly updating) map location | ||||
|       initialMapProperties.location, | ||||
|       mapProperties.location, | ||||
|       { | ||||
|         maxDistance: maxSnapDistance ?? 15, | ||||
|         allowUnsnapped: true, | ||||
|         snappedTo, | ||||
|         snapLocation: value, | ||||
|       } | ||||
|       }, | ||||
|     ) | ||||
|     const withCorrectedAttributes = new StaticFeatureSource( | ||||
|       snappedLocation.features.mapD((feats) => | ||||
|  | @ -124,8 +133,8 @@ | |||
|             ...f, | ||||
|             properties, | ||||
|           } | ||||
|         }) | ||||
|       ) | ||||
|         }), | ||||
|       ), | ||||
|     ) | ||||
|     // The actual point to be created, snapped at the new location | ||||
|     new ShowDataLayer(map, { | ||||
|  | @ -139,7 +148,7 @@ | |||
| <LocationInput | ||||
|   {map} | ||||
|   on:click | ||||
|   mapProperties={initialMapProperties} | ||||
|   {mapProperties} | ||||
|   value={preciseLocation} | ||||
|   initialCoordinate={coordinate} | ||||
|   maxDistanceInMeters={50} | ||||
|  |  | |||
|  | @ -74,9 +74,8 @@ | |||
|   </div> | ||||
|   <slot name="close-button"> | ||||
|     <div class="mt-4"> | ||||
|     <CloseButton  on:click={() => state.selectedElement.setData(undefined)}/> | ||||
|       <CloseButton on:click={() => state.selectedElement.setData(undefined)} /> | ||||
|     </div> | ||||
| 
 | ||||
|   </slot> | ||||
| </div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -118,8 +118,7 @@ | |||
|   ) | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col link-underline"> | ||||
| 
 | ||||
| <div class="link-underline flex flex-col"> | ||||
|   <a href="geo:{$location.lat},{$location.lon}">Open the current location in other applications</a> | ||||
| 
 | ||||
|   <div class="flex flex-col"> | ||||
|  |  | |||
|  | @ -54,7 +54,10 @@ | |||
|     <Tr t={layout.descriptionTail} /> | ||||
| 
 | ||||
|     <!-- Buttons: open map, go to location, search --> | ||||
|     <NextButton clss="primary w-full" on:click={() => state.guistate.pageStates.about_theme.setData(false)}> | ||||
|     <NextButton | ||||
|       clss="primary w-full" | ||||
|       on:click={() => state.guistate.pageStates.about_theme.setData(false)} | ||||
|     > | ||||
|       <div class="flex w-full flex-col items-center"> | ||||
|         <div class="flex w-full justify-center text-2xl"> | ||||
|           <Tr t={Translations.t.general.openTheMap} /> | ||||
|  | @ -97,10 +100,9 @@ | |||
|     {/if} | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="link-underline flex justify-end text-sm mt-8"> | ||||
|   <div class="link-underline mt-8 flex justify-end text-sm"> | ||||
|     <a href="https://mapcomplete.org" target="_blank"> | ||||
|       <Tr t={Translations.t.general.poweredByMapComplete} /> | ||||
|     </a> | ||||
|   </div> | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
|  | @ -47,7 +47,6 @@ | |||
| </script> | ||||
| 
 | ||||
| <LoginToggle {state} silentFail> | ||||
| 
 | ||||
|   {#if !$sourceUrl || !$enableLogin} | ||||
|     <!-- empty block --> | ||||
|   {:else if $externalData === undefined} | ||||
|  | @ -59,15 +58,15 @@ | |||
|   {:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} | ||||
|     <Tr cls="subtle" t={t.noDataLoaded} /> | ||||
|   {:else if !$hasDifferencesAtStart} | ||||
|   <span class="subtle text-sm"> | ||||
|     <Tr t={t.allIncluded.Subs({ source: $sourceUrl })} /> | ||||
|   </span> | ||||
|     <span class="subtle text-sm"> | ||||
|       <Tr t={t.allIncluded.Subs({ source: $sourceUrl })} /> | ||||
|     </span> | ||||
|   {:else if $comparisonState !== undefined} | ||||
|     <AccordionSingle expanded={!collapsed}> | ||||
|     <span slot="header" class="flex"> | ||||
|       <GlobeAlt class="h-6 w-6" /> | ||||
|       <Tr t={Translations.t.external.title} /> | ||||
|     </span> | ||||
|       <span slot="header" class="flex"> | ||||
|         <GlobeAlt class="h-6 w-6" /> | ||||
|         <Tr t={Translations.t.external.title} /> | ||||
|       </span> | ||||
|       <ComparisonTable | ||||
|         externalProperties={$externalData["success"]} | ||||
|         {state} | ||||
|  |  | |||
|  | @ -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,36 +20,58 @@ | |||
| 
 | ||||
|   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" dismissable={true}> | ||||
|   <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} | ||||
|       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) { | ||||
|         imgEl.src = fallbackImage | ||||
|       } | ||||
|     }} | ||||
|         if (fallbackImage) { | ||||
|           imgEl.src = fallbackImage | ||||
|         } | ||||
|       }} | ||||
|       src={image.url} | ||||
|     /> | ||||
| 
 | ||||
|     {#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="bg-black-transparent absolute right-0 top-0 rounded-bl-full" | ||||
|            on:click={() => previewedImage.set(image)}> | ||||
|         <MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" /> | ||||
|       </div> | ||||
|     {/if} | ||||
| 
 | ||||
|   </div> | ||||
|   <div class="absolute bottom-0 left-0"> | ||||
|     <ImageAttribution {image} {attributionFormat} /> | ||||
|  |  | |||
|  | @ -20,7 +20,9 @@ | |||
| </script> | ||||
| 
 | ||||
| {#if $license !== undefined} | ||||
|   <div class="no-images flex items-center rounded-lg bg-black-transparent p-0.5 px-3 text-sm text-white"> | ||||
|   <div | ||||
|     class="no-images bg-black-transparent flex items-center rounded-lg p-0.5 px-3 text-sm text-white" | ||||
|   > | ||||
|     {#if icon !== undefined} | ||||
|       <div class="mr-2 h-6 w-6"> | ||||
|         <ToSvelte construct={icon} /> | ||||
|  | @ -28,7 +30,7 @@ | |||
|     {/if} | ||||
| 
 | ||||
|     <div class="flex gap-x-2" class:flex-col={attributionFormat !== "minimal"}> | ||||
|       {#if attributionFormat !== "minimal" } | ||||
|       {#if attributionFormat !== "minimal"} | ||||
|         {#if $license.title} | ||||
|           {#if $license.informationLocation} | ||||
|             <a href={$license.informationLocation.href} target="_blank" rel="noopener nofollower"> | ||||
|  | @ -42,7 +44,7 @@ | |||
| 
 | ||||
|       {#if $license.artist} | ||||
|         {#if attributionFormat === "large"} | ||||
|           <Tr t={Translations.t.general.attribution.madeBy.Subs({author: $license.artist})} /> | ||||
|           <Tr t={Translations.t.general.attribution.madeBy.Subs({ author: $license.artist })} /> | ||||
|         {:else} | ||||
|           <div class="font-bold"> | ||||
|             {@html $license.artist} | ||||
|  | @ -58,7 +60,7 @@ | |||
| 
 | ||||
|       {#if attributionFormat !== "minimal"} | ||||
|         <div class="flex w-full justify-between gap-x-1"> | ||||
|           {#if ($license.license !== undefined || $license.licenseShortName !== undefined)} | ||||
|           {#if $license.license !== undefined || $license.licenseShortName !== undefined} | ||||
|             <div> | ||||
|               {$license?.license ?? $license?.licenseShortName} | ||||
|             </div> | ||||
|  | @ -72,7 +74,6 @@ | |||
|           {/if} | ||||
|         </div> | ||||
|       {/if} | ||||
| 
 | ||||
|     </div> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -39,18 +39,18 @@ | |||
|   <div | ||||
|     class="pointer-events-none absolute bottom-0 left-0 flex w-full flex-wrap items-end justify-between" | ||||
|   > | ||||
|     <div | ||||
|       class="pointer-events-auto m-1 w-fit transition-colors duration-200" | ||||
|     > | ||||
|       <ImageAttribution {image} attributionFormat="large"/> | ||||
|     <div class="pointer-events-auto m-1 w-fit transition-colors duration-200"> | ||||
|       <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()} | ||||
|     > | ||||
|       <DownloadIcon class="h-6 w-6 px-2 opacity-100" /> | ||||
|       <Tr t={Translations.t.general.download.downloadImage}/> | ||||
|       <Tr t={Translations.t.general.download.downloadImage} /> | ||||
|     </button> | ||||
|   </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ | |||
|     key: undefined, | ||||
|     provider: AllImageProviders.byName(image.provider), | ||||
|     date: new Date(image.date), | ||||
|     id: Object.values(image.osmTags)[0] | ||||
|     id: Object.values(image.osmTags)[0], | ||||
|   } | ||||
| 
 | ||||
|   async function applyLink(isLinked: boolean) { | ||||
|  | @ -44,7 +44,7 @@ | |||
|     if (isLinked) { | ||||
|       const action = new LinkImageAction(currentTags.id, key, url, tags, { | ||||
|         theme: tags.data._orig_theme ?? state.layout.id, | ||||
|         changeType: "link-image" | ||||
|         changeType: "link-image", | ||||
|       }) | ||||
|       await state.changes.applyAction(action) | ||||
|     } else { | ||||
|  | @ -53,7 +53,7 @@ | |||
|         if (v === url) { | ||||
|           const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, { | ||||
|             theme: tags.data._orig_theme ?? state.layout.id, | ||||
|             changeType: "remove-image" | ||||
|             changeType: "remove-image", | ||||
|           }) | ||||
|           state.changes.applyAction(action) | ||||
|         } | ||||
|  | @ -62,17 +62,31 @@ | |||
|   } | ||||
| 
 | ||||
|   isLinked.addCallback((isLinked) => applyLink(isLinked)) | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex w-fit shrink-0 flex-col rounded-lg overflow-hidden" class:border-interactive={$isLinked} | ||||
|      style="border-width: 2px"> | ||||
| <div | ||||
|   class="flex w-fit shrink-0 flex-col overflow-hidden rounded-lg" | ||||
|   class:border-interactive={$isLinked} | ||||
|   style="border-width: 2px" | ||||
| > | ||||
|   <AttributedImage | ||||
|     image={providedImage} | ||||
|     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> | ||||
|  |  | |||
|  | @ -1,104 +0,0 @@ | |||
| import { InputElement } from "./InputElement" | ||||
| import Translations from "../i18n/Translations" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| 
 | ||||
| /** | ||||
|  * @deprecated | ||||
|  */ | ||||
| export class DropDown<T> extends InputElement<T> { | ||||
|     private static _nextDropdownId = 0 | ||||
| 
 | ||||
|     private readonly _element: HTMLElement | ||||
| 
 | ||||
|     private readonly _value: UIEventSource<T> | ||||
|     private readonly _values: { value: T; shown: string | BaseUIElement }[] | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * const dropdown = new DropDown<number>("test",[{value: 42, shown: "the answer"}]) | ||||
|      * dropdown.GetValue().data // => 42
 | ||||
|      */ | ||||
|     constructor( | ||||
|         label: string | BaseUIElement, | ||||
|         values: { value: T; shown: string | BaseUIElement }[], | ||||
|         value: UIEventSource<T> = undefined, | ||||
|         options?: { | ||||
|             select_class?: string | ||||
|         } | ||||
|     ) { | ||||
|         super() | ||||
|         value = value ?? new UIEventSource<T>(values[0].value) | ||||
|         this._value = value | ||||
|         this._values = values | ||||
|         if (values.length <= 1) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const id = DropDown._nextDropdownId | ||||
|         DropDown._nextDropdownId++ | ||||
| 
 | ||||
|         const el = document.createElement("form") | ||||
|         this._element = el | ||||
|         el.id = "dropdown" + id | ||||
| 
 | ||||
|         { | ||||
|             const labelEl = Translations.W(label)?.ConstructElement() | ||||
|             if (labelEl !== undefined) { | ||||
|                 const labelHtml = document.createElement("label") | ||||
|                 labelHtml.appendChild(labelEl) | ||||
|                 labelHtml.htmlFor = el.id | ||||
|                 el.appendChild(labelHtml) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         options = options ?? {} | ||||
|         options.select_class = | ||||
|             options.select_class ?? "w-full bg-indigo-100 p-1 rounded hover:bg-indigo-200" | ||||
| 
 | ||||
|         { | ||||
|             const select = document.createElement("select") | ||||
|             select.classList.add(...(options.select_class.split(" ") ?? [])) | ||||
|             for (let i = 0; i < values.length; i++) { | ||||
|                 const option = document.createElement("option") | ||||
|                 option.value = "" + i | ||||
|                 option.appendChild(Translations.W(values[i].shown).ConstructElement()) | ||||
|                 select.appendChild(option) | ||||
|             } | ||||
|             el.appendChild(select) | ||||
| 
 | ||||
|             select.onchange = () => { | ||||
|                 const index = select.selectedIndex | ||||
|                 value.setData(values[index].value) | ||||
|             } | ||||
| 
 | ||||
|             value.addCallbackAndRun((selected) => { | ||||
|                 for (let i = 0; i < values.length; i++) { | ||||
|                     const value = values[i].value | ||||
|                     if (value === selected) { | ||||
|                         select.selectedIndex = i | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         this.onClick(() => {}) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes
 | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<T> { | ||||
|         return this._value | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: T): boolean { | ||||
|         for (const value of this._values) { | ||||
|             if (value.value === t) { | ||||
|                 return true | ||||
|             } | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         return this._element | ||||
|     } | ||||
| } | ||||
							
								
								
									
										64
									
								
								src/UI/InputElement/Helpers/OpeningHours/OHCell.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/UI/InputElement/Helpers/OpeningHours/OHCell.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| <script lang="ts"> | ||||
|   import { createEventDispatcher, onMount } from "svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * This is a pile of hacks to get the events working on mobile too | ||||
|    */ | ||||
|   export let wd: number | ||||
|   export let h: number | ||||
|   export let type: "full" | "half" | ||||
|   let dispatch = createEventDispatcher<{ "start", "end", "move","clear" }>() | ||||
|   let element: HTMLElement | ||||
| 
 | ||||
|   function send(signal: "start" | "end" | "move", ev: Event) { | ||||
|     ev?.preventDefault() | ||||
|     dispatch(signal) | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   let lastElement: HTMLElement | ||||
| 
 | ||||
|   function elementUnderTouch(ev: TouchEvent): HTMLElement { | ||||
|     for (const k in ev.targetTouches) { | ||||
|       const touch = ev.targetTouches[k] | ||||
|       if (touch.clientX === undefined || touch.clientY === undefined) { | ||||
|         continue | ||||
|       } | ||||
|       const el = document.elementFromPoint(touch.clientX, touch.clientY) | ||||
|       if (!el) { | ||||
|         continue | ||||
|       } | ||||
|       lastElement = <any>el | ||||
|       return <any>el | ||||
|     } | ||||
|     return lastElement | ||||
|   } | ||||
| 
 | ||||
|   onMount(() => { | ||||
|     element.addEventListener("mousedown", (ev) => send("start", ev)) | ||||
|     element.onmouseenter = (ev) => send("move", ev) | ||||
|     element.onmouseup = (ev) => send("end", ev) | ||||
| 
 | ||||
|     element.addEventListener("touchstart", ev => dispatch("start", ev)) | ||||
|     element.addEventListener("touchend", ev => { | ||||
| 
 | ||||
|       const el = elementUnderTouch(ev) | ||||
|       if (el?.onmouseup) { | ||||
|         el?.onmouseup(<any>ev) | ||||
|       }else{ | ||||
|         dispatch("clear") | ||||
|       } | ||||
| 
 | ||||
|     }) | ||||
|     element.addEventListener("touchmove", ev => { | ||||
|       elementUnderTouch(ev)?.onmouseenter(<any>ev) | ||||
|     }) | ||||
| 
 | ||||
| 
 | ||||
|   }) | ||||
| </script> | ||||
| 
 | ||||
| <td bind:this={element} id={"oh-"+type+"-"+h+"-"+wd} | ||||
|     class:border-black={(h + 1) % 6 === 0} | ||||
|     class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `} | ||||
| /> | ||||
							
								
								
									
										238
									
								
								src/UI/InputElement/Helpers/OpeningHours/OHTable.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								src/UI/InputElement/Helpers/OpeningHours/OHTable.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,238 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import { UIEventSource } from "../../../../Logic/UIEventSource" | ||||
|   import type { OpeningHour } from "../../../OpeningHours/OpeningHours" | ||||
|   import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours" | ||||
|   import { Translation } from "../../../i18n/Translation" | ||||
|   import Translations from "../../../i18n/Translations" | ||||
|   import Tr from "../../../Base/Tr.svelte" | ||||
|   import { Utils } from "../../../../Utils" | ||||
|   import { onMount } from "svelte" | ||||
|   import { TrashIcon } from "@babeard/svelte-heroicons/mini" | ||||
|   import OHCell from "./OHCell.svelte" | ||||
| 
 | ||||
|   export let value: UIEventSource<OpeningHour[]> | ||||
| 
 | ||||
|   const wd = Translations.t.general.weekdays.abbreviations | ||||
|   const days: Translation[] = [ | ||||
|     wd.monday, | ||||
|     wd.tuesday, | ||||
|     wd.wednesday, | ||||
|     wd.thursday, | ||||
|     wd.friday, | ||||
|     wd.saturday, | ||||
|     wd.sunday, | ||||
|   ] | ||||
| 
 | ||||
|   function range(n: number) { | ||||
|     return Utils.TimesT(n, n => n) | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   function clearSelection() { | ||||
|     const allCells = Array.from(document.getElementsByClassName("oh-timecell")) | ||||
|     for (const timecell of allCells) { | ||||
|       timecell.classList.remove("oh-timecell-selected") | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   function setSelectionNormalized(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) { | ||||
|     for (let wd = weekdayStart; wd <= weekdayEnd; wd++) { | ||||
|       for (let h = (hourStart); h < (hourEnd); h++) { | ||||
|         h = Math.floor(h) | ||||
|         if (h >= hourStart && h < hourEnd) { | ||||
|           const elFull = document.getElementById("oh-full-" + h + "-" + wd) | ||||
|           elFull?.classList?.add("oh-timecell-selected") | ||||
|         } | ||||
|         if (h + 0.5 < hourEnd) { | ||||
| 
 | ||||
|           const elHalf = document.getElementById("oh-half-" + h + "-" + wd) | ||||
|           elHalf?.classList?.add("oh-timecell-selected") | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   function setSelection(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) { | ||||
|     let hourA = hourStart | ||||
|     let hourB = hourEnd | ||||
|     if (hourA > hourB) { | ||||
|       hourA = hourEnd - 0.5 | ||||
|       hourB = hourStart + 0.5 | ||||
|     } | ||||
|     if (hourA == hourB) { | ||||
|       hourA -= 0.5 | ||||
|       hourB += 0.5 | ||||
|     } | ||||
|     setSelectionNormalized(Math.min(weekdayStart, weekdayEnd), Math.max(weekdayStart, weekdayEnd), | ||||
|       hourA, hourB) | ||||
|   } | ||||
| 
 | ||||
|   let selectionStart: [number, number] = undefined | ||||
| 
 | ||||
|   function startSelection(weekday: number, hour: number) { | ||||
|     selectionStart = [weekday, hour] | ||||
|   } | ||||
| 
 | ||||
|   function endSelection(weekday: number, hour: number) { | ||||
|     if (!selectionStart) { | ||||
|       return | ||||
|     } | ||||
|     setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5) | ||||
| 
 | ||||
|     hour += 0.5 | ||||
|     let start = Math.min(selectionStart[1], hour) | ||||
|     let end = Math.max(selectionStart[1], hour) | ||||
|     if (selectionStart[1] > hour) { | ||||
|       end += 0.5 | ||||
|       start -= 0.5 | ||||
|     } | ||||
| 
 | ||||
|     if (end === start) { | ||||
|       end += 0.5 | ||||
|       start -= 0.5 | ||||
|     } | ||||
|     let startMinutes = Math.round((start * 60) % 60) | ||||
|     let endMinutes = Math.round((end * 60) % 60) | ||||
|     let newOhs = [...value.data] | ||||
|     for (let wd = Math.min(selectionStart[0], weekday); wd <= Math.max(selectionStart[0], weekday); wd++) { | ||||
| 
 | ||||
|       const oh: OpeningHour = { | ||||
|         startHour: Math.floor(start), | ||||
|         endHour: Math.floor(end), | ||||
|         startMinutes, | ||||
|         endMinutes, | ||||
|         weekday: wd, | ||||
|       } | ||||
|       newOhs.push(oh) | ||||
|     } | ||||
|     value.set(OpeningHours.MergeTimes(newOhs)) | ||||
|     selectionStart = undefined | ||||
|     clearSelection() | ||||
|   } | ||||
| 
 | ||||
|   function moved(weekday: number, hour: number) { | ||||
|     if (selectionStart) { | ||||
|       clearSelection() | ||||
|       setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   let totalHeight = 0 | ||||
| 
 | ||||
|   onMount(() => { | ||||
|     requestAnimationFrame(() => { | ||||
|       const mondayMorning = document.getElementById("oh-full-" + 0 + "-" + 0) | ||||
|       const sundayEvening = document.getElementById("oh-half-" + 23 + "-" + 6) | ||||
|       const top = mondayMorning.getBoundingClientRect().top | ||||
|       const bottom = sundayEvening.getBoundingClientRect().bottom | ||||
|       totalHeight = bottom - top | ||||
|     }) | ||||
|   }) | ||||
| 
 | ||||
|   /** | ||||
|    * Determines 'top' and 'height-attributes, returns a CSS-string' | ||||
|    * @param oh | ||||
|    */ | ||||
|   function rangeStyle(oh: OpeningHour, totalHeight: number): string { | ||||
|     const top = (oh.startHour + oh.startMinutes / 60) * totalHeight / 24 | ||||
|     const height = (oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight / 24 | ||||
|     return `top: ${top}px; height: ${height}px; z-index: 20` | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <table class="oh-table no-weblate w-full" cellspacing="0" cellpadding="0"> | ||||
|   <tr> | ||||
| 
 | ||||
|     <th style="width: 9%"> | ||||
|       <!-- Top-left cell --> | ||||
|       <button class="absolute top-0 left-0 p-1 rounded-full" on:click={() => value.set([])} style="z-index: 10"> | ||||
|         <TrashIcon class="w-5 h-5" /> | ||||
|       </button> | ||||
|     </th> | ||||
|     {#each days as wd} | ||||
|       <th style="width: 13%"> | ||||
|         <Tr cls="w-full" t={wd} /> | ||||
|       </th> | ||||
|     {/each} | ||||
|   </tr> | ||||
| 
 | ||||
|   <tr class="h-0"> | ||||
|     <!-- Virtual row to add the ranges to--> | ||||
|     <td style="width: 9%" /> | ||||
|     {#each range(7) as wd} | ||||
|       <td style="width: 13%; position: relative;"> | ||||
| 
 | ||||
|         <div class="h-0 pointer-events-none" style="z-index: 10"> | ||||
|           {#each $value.filter(oh => oh.weekday === wd) as range } | ||||
|             <div class="absolute pointer-events-none px-1 md:px-2 w-full " | ||||
|                  style={rangeStyle(range, totalHeight)} | ||||
|             > | ||||
|               <div class="rounded-xl border-interactive h-full low-interaction flex flex-col justify-between"> | ||||
|                 <div class:hidden={range.endHour - range.startHour < 3}> | ||||
|                   {OpeningHours.hhmm(range.startHour, range.startMinutes)} | ||||
|                 </div> | ||||
|                 <button class="w-fit rounded-full p-1 self-center pointer-events-auto" | ||||
|                         on:click={() => { | ||||
|                           const cleaned = value.data.filter(v => !OpeningHours.isSame(v, range)) | ||||
|                           console.log("Cleaned", cleaned, value.data) | ||||
|                   value.set(cleaned) | ||||
|                 }}> | ||||
|                   <TrashIcon class="w-6 h-6" /> | ||||
|                 </button> | ||||
|                 <div class:hidden={range.endHour - range.startHour < 3}> | ||||
|                   {OpeningHours.hhmm(range.endHour, range.endMinutes)} | ||||
| 
 | ||||
|                 </div> | ||||
|               </div> | ||||
| 
 | ||||
|             </div> | ||||
|           {/each} | ||||
|         </div> | ||||
|       </td> | ||||
| 
 | ||||
|     {/each} | ||||
| 
 | ||||
|   </tr> | ||||
| 
 | ||||
|   {#each range(24) as h} | ||||
|     <tr style="height: 0.75rem; width: 9%"> <!-- even row, for the hour --> | ||||
|       <td rowspan={ h < 23 ? 2: 1 } | ||||
|           class="relative text-sm sm:text-base oh-left-col oh-timecell-full border-box interactive" | ||||
|           style={ h < 23 ? "top: 0.75rem" : "height:0; top: 0.75rem"}> | ||||
|         {#if h < 23} | ||||
|           {h + 1}:00 | ||||
|         {/if} | ||||
|       </td> | ||||
|       {#each range(7) as wd} | ||||
|         <OHCell type="full" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)} | ||||
|                 on:move={() => moved(wd, h)} on:clear={() => clearSelection()} /> | ||||
|       {/each} | ||||
|     </tr> | ||||
| 
 | ||||
|     <tr style="height: 0.75rem"> <!-- odd row, for the half hour --> | ||||
|       {#if h === 23} | ||||
|         <td/> | ||||
|         {/if} | ||||
|       {#each range(7) as wd} | ||||
|         <OHCell type="half" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)} | ||||
|                 on:move={() => moved(wd, h)} on:clear={() => clearSelection()} /> | ||||
|       {/each} | ||||
|     </tr> | ||||
| 
 | ||||
|   {/each} | ||||
| 
 | ||||
| </table> | ||||
| <style> | ||||
| 
 | ||||
|     th { | ||||
|         top: 0; | ||||
|         position: sticky; | ||||
|         z-index: 10; | ||||
|     } | ||||
| </style> | ||||
|  | @ -4,9 +4,39 @@ | |||
|    */ | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte" | ||||
|   import OpeningHoursInput from "../../OpeningHours/OpeningHoursInput" | ||||
|   import OpeningHoursInput from "../../OpeningHours/OpeningHoursState" | ||||
|   import PublicHolidaySelector from "../../OpeningHours/PublicHolidaySelector.svelte" | ||||
|   import OHTable from "./OpeningHours/OHTable.svelte" | ||||
|   import OpeningHoursState from "../../OpeningHours/OpeningHoursState" | ||||
|   import Popup from "../../Base/Popup.svelte" | ||||
| 
 | ||||
|   export let value: UIEventSource<string> | ||||
| </script> | ||||
|   export let args: string | ||||
|   let prefix = "" | ||||
|   let postfix = "" | ||||
|   if (args) { | ||||
|     try { | ||||
| 
 | ||||
| <ToSvelte construct={new OpeningHoursInput(value)} /> | ||||
|       const data = JSON.stringify(args) | ||||
|       if (data["prefix"]) { | ||||
|         prefix = data["prefix"] | ||||
|       } | ||||
|       if (data["postfix"]) { | ||||
|         postfix = data["postfix"] | ||||
|       } | ||||
|     } catch (e) { | ||||
|       console.error("Could not parse arguments") | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const state = new OpeningHoursState(value) | ||||
|   let expanded = new UIEventSource(false) | ||||
| </script> | ||||
| <Popup bodyPadding="p-0" shown={expanded}> | ||||
|   <OHTable value={state.normalOhs} /> | ||||
|   <div class="absolute w-full pointer-events-none bottom-0 flex justify-end"> | ||||
|     <button on:click={() => expanded.set(false)} class="primary pointer-events-auto">Done</button> | ||||
|   </div> | ||||
| </Popup> | ||||
| <button on:click={() => expanded.set(true)}>Pick opening hours</button> | ||||
| <PublicHolidaySelector value={state.phSelectorValue} /> | ||||
|  |  | |||
|  | @ -7,7 +7,6 @@ | |||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import type { ValidatorType } from "./Validators" | ||||
|   import InputHelpers from "./InputHelpers" | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte" | ||||
|   import type { Feature } from "geojson" | ||||
|   import ImageHelper from "./Helpers/ImageHelper.svelte" | ||||
|   import TranslationInput from "./Helpers/TranslationInput.svelte" | ||||
|  | @ -19,7 +18,6 @@ | |||
|   import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte" | ||||
|   import SlopeInput from "./Helpers/SlopeInput.svelte" | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import WikidataInput from "./Helpers/WikidataInput.svelte" | ||||
|   import WikidataInputHelper from "./WikidataInputHelper.svelte" | ||||
| 
 | ||||
|   export let type: ValidatorType | ||||
|  | @ -48,7 +46,7 @@ | |||
| {:else if type === "simple_tag"} | ||||
|   <SimpleTagInput {value} {args} on:submit /> | ||||
| {:else if type === "opening_hours"} | ||||
|   <OpeningHoursInput {value} /> | ||||
|   <OpeningHoursInput {value} {args} /> | ||||
| {:else if type === "slope"} | ||||
|   <SlopeInput {value} {feature} {state} /> | ||||
| {:else if type === "wikidata"} | ||||
|  |  | |||
|  | @ -55,7 +55,7 @@ | |||
|   } | ||||
| 
 | ||||
|   function onKeyPress(e: KeyboardEvent) { | ||||
|     if (e.key === "Enter") { | ||||
|     if (e.key === "Enter" && (!validator.textArea || e.ctrlKey)) { | ||||
|       e.stopPropagation() | ||||
|       e.preventDefault() | ||||
|       dispatch("submit") | ||||
|  |  | |||
|  | @ -13,12 +13,10 @@ export default class UrlValidator extends Validator { | |||
|         "tripadvisor.co.uk", | ||||
|         "tripadvisor.com.au", | ||||
|         "katestravelexperience.eu", | ||||
|         "hoteldetails.eu" | ||||
|         "hoteldetails.eu", | ||||
|     ]) | ||||
| 
 | ||||
|     private static readonly discouragedWebsites = new Set<string>([ | ||||
|         "facebook.com" | ||||
|     ]) | ||||
|     private static readonly discouragedWebsites = new Set<string>(["facebook.com"]) | ||||
| 
 | ||||
|     constructor(name?: string, explanation?: string, forceHttps?: boolean) { | ||||
|         super( | ||||
|  | @ -93,14 +91,10 @@ export default class UrlValidator extends Validator { | |||
|      * v.getFeedback("https://booking.com/some-hotel.html").textFor("en") // => Translations.t.validation.url.spamSite.Subs({host: "booking.com"}).textFor("en")
 | ||||
|      */ | ||||
|     getFeedback(s: string, getCountry?: () => string): Translation | undefined { | ||||
|         if ( | ||||
|             !s.startsWith("http://") && | ||||
|             !s.startsWith("https://") && | ||||
|             !s.startsWith("http:") | ||||
|         ) { | ||||
|         if (!s.startsWith("http://") && !s.startsWith("https://") && !s.startsWith("http:")) { | ||||
|             s = "https://" + s | ||||
|         } | ||||
|         try{ | ||||
|         try { | ||||
|             const url = new URL(s) | ||||
|             let host = url.host.toLowerCase() | ||||
|             if (host.startsWith("www.")) { | ||||
|  | @ -112,9 +106,7 @@ export default class UrlValidator extends Validator { | |||
|             if (UrlValidator.discouragedWebsites.has(host)) { | ||||
|                 return Translations.t.validation.url.aggregator.Subs({ host }) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|         }catch (e) { | ||||
|         } catch (e) { | ||||
|             // pass
 | ||||
|         } | ||||
|         const upstream = super.getFeedback(s, getCountry) | ||||
|  | @ -122,7 +114,6 @@ export default class UrlValidator extends Validator { | |||
|             return upstream | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|  | @ -131,7 +122,6 @@ export default class UrlValidator extends Validator { | |||
|      * v.isValid("https://booking.com/some-hotel.html") // => false
 | ||||
|      */ | ||||
|     isValid(str: string): boolean { | ||||
| 
 | ||||
|         try { | ||||
|             if ( | ||||
|                 !str.startsWith("http://") && | ||||
|  |  | |||
|  | @ -45,6 +45,7 @@ | |||
|   import BuildingStorefront from "@babeard/svelte-heroicons/outline/BuildingStorefront" | ||||
|   import LockClosed from "@babeard/svelte-heroicons/solid/LockClosed" | ||||
|   import Key from "@babeard/svelte-heroicons/solid/Key" | ||||
|   import Snap from "../../assets/svg/Snap.svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * Renders a single icon. | ||||
|  | @ -168,6 +169,8 @@ | |||
|     <Airport {color} class={clss}/> | ||||
|   {:else if icon === "building_storefront"} | ||||
|     <BuildingStorefront {color} class={clss}/> | ||||
|   {:else if icon === "snap"} | ||||
|     <Snap class={clss} /> | ||||
|   {:else if Utils.isEmoji(icon)} | ||||
|     <span style={`font-size: ${emojiHeight}; line-height: ${emojiHeight}`}> | ||||
|       {icon} | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ | |||
|     photo: ["photo", "historicphoto"], | ||||
|     map: ["map", "historicmap"], | ||||
|     other: ["other", "elevation"], | ||||
|     osmbasedmap: ["osmbasedmap"] | ||||
|     osmbasedmap: ["osmbasedmap"], | ||||
|   } | ||||
| 
 | ||||
|   function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> { | ||||
|  | @ -51,20 +51,18 @@ | |||
|   } | ||||
| 
 | ||||
|   export let onlyLink: boolean | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <Page {onlyLink} shown={shown} fullscreen={true}> | ||||
|   <div slot="header" class="flex" > | ||||
| <Page {onlyLink} {shown} fullscreen={true}> | ||||
|   <div slot="header" class="flex"> | ||||
|     <Square3Stack3dIcon class="h-6 w-6" /> | ||||
| 
 | ||||
|   <Tr t={Translations.t.general.backgroundMap} /> | ||||
|     <Tr t={Translations.t.general.backgroundMap} /> | ||||
|   </div> | ||||
|   {#if $_availableLayers?.length < 1} | ||||
|     <Loading /> | ||||
|   {:else} | ||||
| 
 | ||||
|     <div class="flex gap-x-2 flex-col sm:flex-row gap-y-2" style="height: calc( 100% - 5rem)"> | ||||
|     <div class="flex flex-col gap-x-2 gap-y-2 sm:flex-row" style="height: calc( 100% - 5rem)"> | ||||
|       <RasterLayerPicker | ||||
|         availableLayers={$photoLayers} | ||||
|         favourite={getPref("photo")} | ||||
|  |  | |||
|  | @ -859,6 +859,9 @@ This list will be sorted | |||
|         return ranges | ||||
|     } | ||||
| 
 | ||||
|     public static isSame(a: OpeningHour, b: OpeningHour){ | ||||
|         return a.weekday === b.weekday && a.startHour === b.startHour && a.startMinutes === b.startMinutes && a.endHour === b.endHour && a.endMinutes === b.endMinutes | ||||
|     } | ||||
|     private static multiply( | ||||
|         weekdays: number[], | ||||
|         timeranges: { | ||||
|  |  | |||
|  | @ -1,33 +0,0 @@ | |||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import OpeningHoursPickerTable from "./OpeningHoursPickerTable" | ||||
| import { OH, OpeningHour } from "./OpeningHours" | ||||
| import { InputElement } from "../Input/InputElement" | ||||
| 
 | ||||
| export default class OpeningHoursPicker extends InputElement<OpeningHour[]> { | ||||
|     private readonly _ohs: UIEventSource<OpeningHour[]> | ||||
|     private readonly _backgroundTable: OpeningHoursPickerTable | ||||
| 
 | ||||
|     constructor(ohs: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([])) { | ||||
|         super() | ||||
|         this._ohs = ohs | ||||
| 
 | ||||
|         ohs.addCallback((oh) => { | ||||
|             ohs.setData(OH.MergeTimes(oh)) | ||||
|         }) | ||||
| 
 | ||||
|         this._backgroundTable = new OpeningHoursPickerTable(this._ohs) | ||||
|         this._backgroundTable.ConstructElement() | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<OpeningHour[]> { | ||||
|         return this._ohs | ||||
|     } | ||||
| 
 | ||||
|     IsValid(_: OpeningHour[]): boolean { | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         return this._backgroundTable.ConstructElement() | ||||
|     } | ||||
| } | ||||
|  | @ -1,332 +0,0 @@ | |||
| /** | ||||
|  * This is the base-table which is selectable by hovering over it. | ||||
|  * It will genarate the currently selected opening hour. | ||||
|  */ | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { Utils } from "../../Utils" | ||||
| import { OpeningHour } from "./OpeningHours" | ||||
| import { InputElement } from "../Input/InputElement" | ||||
| import Translations from "../i18n/Translations" | ||||
| import { Translation } from "../i18n/Translation" | ||||
| import { FixedUiElement } from "../Base/FixedUiElement" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import Combine from "../Base/Combine" | ||||
| import OpeningHoursRange from "./OpeningHoursRange" | ||||
| 
 | ||||
| export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> { | ||||
|     public static readonly days: Translation[] = [ | ||||
|         Translations.t.general.weekdays.abbreviations.monday, | ||||
|         Translations.t.general.weekdays.abbreviations.tuesday, | ||||
|         Translations.t.general.weekdays.abbreviations.wednesday, | ||||
|         Translations.t.general.weekdays.abbreviations.thursday, | ||||
|         Translations.t.general.weekdays.abbreviations.friday, | ||||
|         Translations.t.general.weekdays.abbreviations.saturday, | ||||
|         Translations.t.general.weekdays.abbreviations.sunday, | ||||
|     ] | ||||
|     /* | ||||
|     These html-elements are an overlay over the table columns and act as a host for the ranges in the weekdays | ||||
|      */ | ||||
|     public readonly weekdayElements: HTMLElement[] = Utils.TimesT(7, () => | ||||
|         document.createElement("div") | ||||
|     ) | ||||
|     private readonly source: UIEventSource<OpeningHour[]> | ||||
| 
 | ||||
|     constructor(source?: UIEventSource<OpeningHour[]>) { | ||||
|         super() | ||||
|         this.source = source ?? new UIEventSource<OpeningHour[]>([]) | ||||
|         this.SetClass("w-full block") | ||||
|     } | ||||
| 
 | ||||
|     IsValid(_: OpeningHour[]): boolean { | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<OpeningHour[]> { | ||||
|         return this.source | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         const table = document.createElement("table") | ||||
|         table.classList.add("oh-table") | ||||
|         table.classList.add("no-weblate") | ||||
|         table.classList.add("relative") // Workaround for webkit-based viewers, see #1019
 | ||||
| 
 | ||||
|         const cellHeightInPx = 14 | ||||
| 
 | ||||
|         const headerRow = document.createElement("tr") | ||||
|         headerRow.appendChild(document.createElement("th")) | ||||
|         headerRow.classList.add("relative") | ||||
|         for (let i = 0; i < OpeningHoursPickerTable.days.length; i++) { | ||||
|             let weekday = OpeningHoursPickerTable.days[i].Clone() | ||||
|             const cell = document.createElement("th") | ||||
|             cell.style.width = "14%" | ||||
|             cell.appendChild(weekday.ConstructElement()) | ||||
| 
 | ||||
|             const fullColumnSpan = this.weekdayElements[i] | ||||
|             fullColumnSpan.classList.add("w-full", "relative") | ||||
| 
 | ||||
|             // We need to round! The table height is rounded as following, we use this to calculate the actual number of pixels afterwards
 | ||||
|             fullColumnSpan.style.height = cellHeightInPx * 48 + "px" | ||||
| 
 | ||||
|             const ranges = new VariableUiElement( | ||||
|                 this.source | ||||
|                     .map((ohs) => (ohs ?? []).filter((oh: OpeningHour) => oh.weekday === i)) | ||||
|                     .map((ohsForToday) => { | ||||
|                         return new Combine( | ||||
|                             ohsForToday.map( | ||||
|                                 (oh) => | ||||
|                                     new OpeningHoursRange(oh, () => { | ||||
|                                         this.source.data.splice(this.source.data.indexOf(oh), 1) | ||||
|                                         this.source.ping() | ||||
|                                     }) | ||||
|                             ) | ||||
|                         ) | ||||
|                     }) | ||||
|             ) | ||||
|             fullColumnSpan.appendChild(ranges.ConstructElement()) | ||||
| 
 | ||||
|             const fullColumnSpanWrapper = document.createElement("div") | ||||
|             fullColumnSpanWrapper.classList.add("absolute") | ||||
|             fullColumnSpanWrapper.style.zIndex = "10" | ||||
|             fullColumnSpanWrapper.style.width = "13.5%" | ||||
|             fullColumnSpanWrapper.style.pointerEvents = "none" | ||||
| 
 | ||||
|             fullColumnSpanWrapper.appendChild(fullColumnSpan) | ||||
| 
 | ||||
|             cell.appendChild(fullColumnSpanWrapper) | ||||
|             headerRow.appendChild(cell) | ||||
|         } | ||||
| 
 | ||||
|         table.appendChild(headerRow) | ||||
| 
 | ||||
|         const self = this | ||||
|         for (let h = 0; h < 24; h++) { | ||||
|             const hs = Utils.TwoDigits(h) | ||||
|             const firstCell = document.createElement("td") | ||||
|             firstCell.rowSpan = 2 | ||||
|             firstCell.classList.add("oh-left-col", "oh-timecell-full", "border-box") | ||||
|             firstCell.appendChild(new FixedUiElement(hs + ":00").ConstructElement()) | ||||
| 
 | ||||
|             const evenRow = document.createElement("tr") | ||||
|             evenRow.appendChild(firstCell) | ||||
| 
 | ||||
|             for (let weekday = 0; weekday < 7; weekday++) { | ||||
|                 const cell = document.createElement("td") | ||||
|                 cell.classList.add("oh-timecell", "oh-timecell-full", `oh-timecell-${weekday}`) | ||||
|                 evenRow.appendChild(cell) | ||||
|             } | ||||
|             evenRow.style.height = cellHeightInPx + "px" | ||||
|             evenRow.style.maxHeight = evenRow.style.height | ||||
|             evenRow.style.minHeight = evenRow.style.height | ||||
|             table.appendChild(evenRow) | ||||
| 
 | ||||
|             const oddRow = document.createElement("tr") | ||||
| 
 | ||||
|             for (let weekday = 0; weekday < 7; weekday++) { | ||||
|                 const cell = document.createElement("td") | ||||
|                 cell.classList.add("oh-timecell", "oh-timecell-half", `oh-timecell-${weekday}`) | ||||
|                 oddRow.appendChild(cell) | ||||
|             } | ||||
|             oddRow.style.minHeight = evenRow.style.height | ||||
|             oddRow.style.maxHeight = evenRow.style.height | ||||
| 
 | ||||
|             table.appendChild(oddRow) | ||||
|         } | ||||
| 
 | ||||
|         /**** Event handling below ***/ | ||||
| 
 | ||||
|         let mouseIsDown = false | ||||
|         let selectionStart: [number, number] = undefined | ||||
|         let selectionEnd: [number, number] = undefined | ||||
| 
 | ||||
|         function h(timeSegment: number) { | ||||
|             return Math.floor(timeSegment / 2) | ||||
|         } | ||||
| 
 | ||||
|         function m(timeSegment: number) { | ||||
|             return (timeSegment % 2) * 30 | ||||
|         } | ||||
| 
 | ||||
|         function startSelection(i: number, j: number) { | ||||
|             mouseIsDown = true | ||||
|             selectionStart = [i, j] | ||||
|             selectionEnd = [i, j] | ||||
|         } | ||||
| 
 | ||||
|         function endSelection() { | ||||
|             if (selectionStart === undefined) { | ||||
|                 return | ||||
|             } | ||||
|             if (!mouseIsDown) { | ||||
|                 return | ||||
|             } | ||||
|             mouseIsDown = false | ||||
|             const dStart = Math.min(selectionStart[1], selectionEnd[1]) | ||||
|             const dEnd = Math.max(selectionStart[1], selectionEnd[1]) | ||||
|             const timeStart = Math.min(selectionStart[0], selectionEnd[0]) - 1 | ||||
|             const timeEnd = Math.max(selectionStart[0], selectionEnd[0]) - 1 | ||||
|             for (let weekday = dStart; weekday <= dEnd; weekday++) { | ||||
|                 const oh: OpeningHour = { | ||||
|                     weekday: weekday, | ||||
|                     startHour: h(timeStart), | ||||
|                     startMinutes: m(timeStart), | ||||
|                     endHour: h(timeEnd + 1), | ||||
|                     endMinutes: m(timeEnd + 1), | ||||
|                 } | ||||
|                 if (oh.endHour > 23) { | ||||
|                     oh.endHour = 24 | ||||
|                     oh.endMinutes = 0 | ||||
|                 } | ||||
|                 self.source.data.push(oh) | ||||
|             } | ||||
|             self.source.ping() | ||||
| 
 | ||||
|             // Clear the highlighting
 | ||||
|             let header = table.rows[0] | ||||
|             for (let j = 1; j < header.cells.length; j++) { | ||||
|                 header.cells[j].classList?.remove("oh-timecol-selected") | ||||
|             } | ||||
|             for (let i = 1; i < table.rows.length; i++) { | ||||
|                 let row = table.rows[i] | ||||
|                 for (let j = 0; j < row.cells.length; j++) { | ||||
|                     let cell = row.cells[j] | ||||
|                     cell?.classList?.remove("oh-timecell-selected") | ||||
|                     row.classList?.remove("oh-timerow-selected") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         table.onmouseup = () => { | ||||
|             endSelection() | ||||
|         } | ||||
|         table.onmouseleave = () => { | ||||
|             endSelection() | ||||
|         } | ||||
| 
 | ||||
|         let lastSelectionIend, lastSelectionJEnd | ||||
| 
 | ||||
|         function selectAllBetween(iEnd, jEnd) { | ||||
|             if (lastSelectionIend === iEnd && lastSelectionJEnd === jEnd) { | ||||
|                 return // We already did this
 | ||||
|             } | ||||
|             lastSelectionIend = iEnd | ||||
|             lastSelectionJEnd = jEnd | ||||
| 
 | ||||
|             let iStart = selectionStart[0] | ||||
|             let jStart = selectionStart[1] | ||||
| 
 | ||||
|             if (iStart > iEnd) { | ||||
|                 const h = iStart | ||||
|                 iStart = iEnd | ||||
|                 iEnd = h | ||||
|             } | ||||
|             if (jStart > jEnd) { | ||||
|                 const h = jStart | ||||
|                 jStart = jEnd | ||||
|                 jEnd = h | ||||
|             } | ||||
| 
 | ||||
|             let header = table.rows[0] | ||||
|             for (let j = 1; j < header.cells.length; j++) { | ||||
|                 let cell = header.cells[j] | ||||
|                 cell.classList?.remove("oh-timecol-selected-round-left") | ||||
|                 cell.classList?.remove("oh-timecol-selected-round-right") | ||||
| 
 | ||||
|                 if (jStart + 1 <= j && j <= jEnd + 1) { | ||||
|                     cell.classList?.add("oh-timecol-selected") | ||||
|                     if (jStart + 1 == j) { | ||||
|                         cell.classList?.add("oh-timecol-selected-round-left") | ||||
|                     } | ||||
|                     if (jEnd + 1 == j) { | ||||
|                         cell.classList?.add("oh-timecol-selected-round-right") | ||||
|                     } | ||||
|                 } else { | ||||
|                     cell.classList?.remove("oh-timecol-selected") | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             for (let i = 1; i < table.rows.length; i++) { | ||||
|                 let row = table.rows[i] | ||||
|                 if (iStart <= i && i <= iEnd) { | ||||
|                     row.classList?.add("oh-timerow-selected") | ||||
|                 } else { | ||||
|                     row.classList?.remove("oh-timerow-selected") | ||||
|                 } | ||||
|                 for (let j = 0; j < row.cells.length; j++) { | ||||
|                     let cell = row.cells[j] | ||||
|                     if (cell === undefined) { | ||||
|                         continue | ||||
|                     } | ||||
|                     let offset = 0 | ||||
|                     if (i % 2 == 1) { | ||||
|                         if (j == 0) { | ||||
|                             // This is the first column of a full hour -> This is the time indication (skip)
 | ||||
|                             continue | ||||
|                         } | ||||
|                         offset = -1 | ||||
|                     } | ||||
| 
 | ||||
|                     if (iStart <= i && i <= iEnd && jStart <= j + offset && j + offset <= jEnd) { | ||||
|                         cell?.classList?.add("oh-timecell-selected") | ||||
|                     } else { | ||||
|                         cell?.classList?.remove("oh-timecell-selected") | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (let i = 1; i < table.rows.length; i++) { | ||||
|             let row = table.rows[i] | ||||
|             for (let j = 0; j < row.cells.length; j++) { | ||||
|                 let cell = row.cells[j] | ||||
|                 let offset = 0 | ||||
|                 if (i % 2 == 1) { | ||||
|                     if (j == 0) { | ||||
|                         continue | ||||
|                     } | ||||
|                     offset = -1 | ||||
|                 } | ||||
| 
 | ||||
|                 cell.onmousedown = (ev) => { | ||||
|                     ev.preventDefault() | ||||
|                     startSelection(i, j + offset) | ||||
|                     selectAllBetween(i, j + offset) | ||||
|                 } | ||||
|                 cell.ontouchstart = (ev) => { | ||||
|                     ev.preventDefault() | ||||
|                     startSelection(i, j + offset) | ||||
|                     selectAllBetween(i, j + offset) | ||||
|                 } | ||||
|                 cell.onmouseenter = () => { | ||||
|                     if (mouseIsDown) { | ||||
|                         selectionEnd = [i, j + offset] | ||||
|                         selectAllBetween(i, j + offset) | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 cell.ontouchmove = (ev: TouchEvent) => { | ||||
|                     ev.preventDefault() | ||||
|                     for (const k in ev.targetTouches) { | ||||
|                         const touch = ev.targetTouches[k] | ||||
|                         if (touch.clientX === undefined || touch.clientY === undefined) { | ||||
|                             continue | ||||
|                         } | ||||
|                         const elUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY) | ||||
|                         // @ts-ignore
 | ||||
|                         const f = elUnderTouch.onmouseenter | ||||
|                         if (f) { | ||||
|                             f() | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 cell.ontouchend = (ev) => { | ||||
|                     ev.preventDefault() | ||||
|                     endSelection() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return table | ||||
|     } | ||||
| } | ||||
|  | @ -38,7 +38,7 @@ export default class OpeningHoursRange extends BaseUIElement { | |||
|             }) | ||||
| 
 | ||||
|         let content: BaseUIElement | ||||
|         if (height > 2) { | ||||
|         if (height > 3) { | ||||
|             content = new Combine([startTime, deleteRange, endTime]).SetClass( | ||||
|                 "flex flex-col h-full justify-between" | ||||
|             ) | ||||
|  | @ -55,6 +55,10 @@ export default class OpeningHoursRange extends BaseUIElement { | |||
|         return el | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the relative height, in number of hours to display | ||||
|      * Range: ]0 - 24] | ||||
|      */ | ||||
|     private getHeight(): number { | ||||
|         const oh = this._oh | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,29 +3,19 @@ | |||
|  * Keeps track of unparsed rules | ||||
|  * Exports everything conveniently as a string, for direct use | ||||
|  */ | ||||
| import OpeningHoursPicker from "./OpeningHoursPicker" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import Combine from "../Base/Combine" | ||||
| import { FixedUiElement } from "../Base/FixedUiElement" | ||||
| import { OH, OpeningHour } from "./OpeningHours" | ||||
| import { InputElement } from "../Input/InputElement" | ||||
| import Translations from "../i18n/Translations" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import SvelteUIElement from "../Base/SvelteUIElement" | ||||
| import PublicHolidaySelector from "./PublicHolidaySelector.svelte" | ||||
| 
 | ||||
| export default class OpeningHoursInput extends InputElement<string> { | ||||
|     private readonly _value: UIEventSource<string> | ||||
|     private readonly _element: BaseUIElement | ||||
| export default class OpeningHoursState { | ||||
|     public readonly normalOhs: UIEventSource<OpeningHour[]> | ||||
|     public readonly leftoverRules: Store<string[]> | ||||
|     public readonly phSelectorValue: UIEventSource<string> | ||||
| 
 | ||||
|     constructor( | ||||
|         value: UIEventSource<string> = new UIEventSource<string>(""), | ||||
|         prefix = "", | ||||
|         postfix = "" | ||||
|         postfix = "", | ||||
|     ) { | ||||
|         super() | ||||
|         this._value = value | ||||
|         let valueWithoutPrefix = value | ||||
|         if (prefix !== "" && postfix !== "") { | ||||
|             valueWithoutPrefix = value.sync( | ||||
|  | @ -54,11 +44,11 @@ export default class OpeningHoursInput extends InputElement<string> { | |||
|                     } | ||||
| 
 | ||||
|                     return prefix + noPrefix + postfix | ||||
|                 } | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         const leftoverRules: Store<string[]> = valueWithoutPrefix.map((str) => { | ||||
|         this.leftoverRules = valueWithoutPrefix.map((str) => { | ||||
|             if (str === undefined) { | ||||
|                 return [] | ||||
|             } | ||||
|  | @ -88,24 +78,25 @@ export default class OpeningHoursInput extends InputElement<string> { | |||
|                 break | ||||
|             } | ||||
|         } | ||||
|         const phSelectorValue = new UIEventSource<string>(ph ?? "") | ||||
|         this.phSelectorValue = new UIEventSource<string>(ph ?? "") | ||||
| 
 | ||||
| 
 | ||||
|         // Note: MUST be bound AFTER the leftover rules!
 | ||||
|         const rulesFromOhPicker: UIEventSource<OpeningHour[]> = valueWithoutPrefix.sync( | ||||
|         this.normalOhs = valueWithoutPrefix.sync( | ||||
|             (str) => { | ||||
|                 return OH.Parse(str) | ||||
|             }, | ||||
|             [leftoverRules, phSelectorValue], | ||||
|             [this.leftoverRules, this.phSelectorValue], | ||||
|             (rules, oldString) => { | ||||
|                 // We always add a ';', to easily add new rules. We remove the ';' again at the end of the function
 | ||||
|                 // Important: spaces are _not_ allowed after a ';' as it'll destabilize the parsing!
 | ||||
|                 let str = OH.ToString(rules) + ";" | ||||
|                 const ph = phSelectorValue.data | ||||
|                 const ph = this.phSelectorValue.data | ||||
|                 if (ph) { | ||||
|                     str += ph + ";" | ||||
|                 } | ||||
| 
 | ||||
|                 str += leftoverRules.data.join(";") + ";" | ||||
|                 str += this.leftoverRules.data.join(";") + ";" | ||||
| 
 | ||||
|                 str = str.trim() | ||||
|                 while (str.endsWith(";")) { | ||||
|  | @ -120,41 +111,24 @@ export default class OpeningHoursInput extends InputElement<string> { | |||
|                     return oldString // We pass a reference to the old string to stabilize the EventSource
 | ||||
|                 } | ||||
|                 return str | ||||
|             } | ||||
|             }, | ||||
|         ) | ||||
|         /* | ||||
|                 const leftoverWarning = new VariableUiElement( | ||||
|                     leftoverRules.map((leftovers: string[]) => { | ||||
|                         if (leftovers.length == 0) { | ||||
|                             return "" | ||||
|                         } | ||||
|                         return new Combine([ | ||||
|                             Translations.t.general.opening_hours.not_all_rules_parsed, | ||||
|                             new FixedUiElement(leftovers.map((r) => `${r}<br/>`).join("")).SetClass( | ||||
|                                 "subtle" | ||||
|                             ), | ||||
|                         ]) | ||||
|                     }) | ||||
|                 )*/ | ||||
| 
 | ||||
|         const leftoverWarning = new VariableUiElement( | ||||
|             leftoverRules.map((leftovers: string[]) => { | ||||
|                 if (leftovers.length == 0) { | ||||
|                     return "" | ||||
|                 } | ||||
|                 return new Combine([ | ||||
|                     Translations.t.general.opening_hours.not_all_rules_parsed, | ||||
|                     new FixedUiElement(leftovers.map((r) => `${r}<br/>`).join("")).SetClass( | ||||
|                         "subtle" | ||||
|                     ), | ||||
|                 ]) | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
|         const ohPicker = new OpeningHoursPicker(rulesFromOhPicker) | ||||
| 
 | ||||
|         this._element = new Combine([ | ||||
|             leftoverWarning, | ||||
|             ohPicker, | ||||
|             new SvelteUIElement(PublicHolidaySelector, { value: phSelectorValue }), | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<string> { | ||||
|         return this._value | ||||
|     } | ||||
| 
 | ||||
|     IsValid(_: string): boolean { | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         return this._element.ConstructElement() | ||||
|     } | ||||
| } | ||||
|  | @ -55,7 +55,7 @@ | |||
| 
 | ||||
|     for (const preset of layer.presets) { | ||||
|       const tags = TagUtils.KVtoProperties(preset.tags ?? []) | ||||
|       if(preset.preciseInput.snapToLayers){ | ||||
|       if (preset.preciseInput.snapToLayers) { | ||||
|         tags["_referencing_ways"] = '["way/-1"]' | ||||
|       } | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,7 +10,6 @@ | |||
|   import type { MapProperties } from "../../Models/MapProperties" | ||||
|   import type { Feature, Point } from "geojson" | ||||
|   import { GeoOperations } from "../../Logic/GeoOperations" | ||||
|   import LocationInput from "../InputElement/Helpers/LocationInput.svelte" | ||||
|   import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte" | ||||
|   import If from "../Base/If.svelte" | ||||
|   import Constants from "../../Models/Constants" | ||||
|  | @ -19,6 +18,8 @@ | |||
|   import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import Icon from "../Map/Icon.svelte" | ||||
|   import NewPointLocationInput from "../BigComponents/NewPointLocationInput.svelte" | ||||
|   import type { WayId } from "../../Models/OsmFeature" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
| 
 | ||||
|  | @ -34,20 +35,22 @@ | |||
| 
 | ||||
|   let newLocation = new UIEventSource<{ lon: number; lat: number }>(undefined) | ||||
| 
 | ||||
|   function initMapProperties() { | ||||
|   let snappedTo = new UIEventSource<WayId | undefined>(undefined) | ||||
| 
 | ||||
|   function initMapProperties(reason: MoveReason) { | ||||
|     return <any>{ | ||||
|       allowMoving: new UIEventSource(true), | ||||
|       allowRotating: new UIEventSource(false), | ||||
|       allowZooming: new UIEventSource(true), | ||||
|       bounds: new UIEventSource(undefined), | ||||
|       location: new UIEventSource({ lon, lat }), | ||||
|       minzoom: new UIEventSource($reason.minZoom), | ||||
|       minzoom: new UIEventSource(reason.minZoom), | ||||
|       rasterLayer: state.mapProperties.rasterLayer, | ||||
|       zoom: new UIEventSource($reason?.startZoom ?? 16), | ||||
|       zoom: new UIEventSource(reason?.startZoom ?? 16), | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   let moveWizardState = new MoveWizardState(id, layer.allowMove, state) | ||||
|   let moveWizardState = new MoveWizardState(id, layer.allowMove, layer, state) | ||||
|   if (moveWizardState.reasons.length === 1) { | ||||
|     reason.setData(moveWizardState.reasons[0]) | ||||
|   } | ||||
|  | @ -55,8 +58,8 @@ | |||
|   let currentMapProperties: MapProperties = undefined | ||||
| </script> | ||||
| 
 | ||||
| <LoginToggle {state}> | ||||
|   {#if moveWizardState.reasons.length > 0} | ||||
| {#if moveWizardState.reasons.length > 0} | ||||
|   <LoginToggle {state}> | ||||
|     {#if $notAllowed} | ||||
|       <div class="m-2 flex rounded-lg bg-gray-200 p-2"> | ||||
|         <Move_not_allowed class="m-2 h-8 w-8" /> | ||||
|  | @ -79,7 +82,7 @@ | |||
|         <span class="flex flex-col p-2"> | ||||
|           {#if currentStep === "reason" && moveWizardState.reasons.length > 1} | ||||
|             {#each moveWizardState.reasons as reasonSpec} | ||||
|               <button | ||||
|               <button class="flex justify-start" | ||||
|                 on:click={() => { | ||||
|                   reason.setData(reasonSpec) | ||||
|                   currentStep = "pick_location" | ||||
|  | @ -91,10 +94,16 @@ | |||
|             {/each} | ||||
|           {:else if currentStep === "pick_location" || currentStep === "reason"} | ||||
|             <div class="relative h-64 w-full"> | ||||
|               <LocationInput | ||||
|                 mapProperties={(currentMapProperties = initMapProperties())} | ||||
|               <NewPointLocationInput | ||||
|                 mapProperties={(currentMapProperties = initMapProperties($reason))} | ||||
|                 value={newLocation} | ||||
|                 initialCoordinate={{ lon, lat }} | ||||
|                 {state} | ||||
|                 coordinate={{ lon, lat }} | ||||
|                 {snappedTo} | ||||
|                 maxSnapDistance={$reason.maxSnapDistance ?? 5} | ||||
|                 snapToLayers={$reason.snapTo} | ||||
|                 targetLayer={layer} | ||||
|                 dontShow={[id]} | ||||
|               /> | ||||
|               <div class="absolute bottom-0 left-0"> | ||||
|                 <OpenBackgroundSelectorButton {state} /> | ||||
|  | @ -114,7 +123,7 @@ | |||
|                 <button | ||||
|                   class="primary w-full" | ||||
|                   on:click={() => { | ||||
|                     moveWizardState.moveFeature(newLocation.data, reason.data, featureToMove) | ||||
|                     moveWizardState.moveFeature(newLocation.data, snappedTo.data, reason.data, featureToMove) | ||||
|                     currentStep = "moved" | ||||
|                   }} | ||||
|                 > | ||||
|  | @ -153,5 +162,5 @@ | |||
|         </span> | ||||
|       </AccordionSingle> | ||||
|     {/if} | ||||
|   {/if} | ||||
| </LoginToggle> | ||||
|   </LoginToggle> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -12,6 +12,8 @@ import { Feature, Point } from "geojson" | |||
| import SvelteUIElement from "../Base/SvelteUIElement" | ||||
| import Relocation from "../../assets/svg/Relocation.svelte" | ||||
| import Location from "../../assets/svg/Location.svelte" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { WayId } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| export interface MoveReason { | ||||
|     text: Translation | string | ||||
|  | @ -24,25 +26,40 @@ export interface MoveReason { | |||
|     startZoom: number | ||||
|     minZoom: number | ||||
|     eraseAddressFields: false | boolean | ||||
|     /** | ||||
|      * Snap to these layers | ||||
|      */ | ||||
|     snapTo?: string[] | ||||
|     maxSnapDistance?: number | ||||
| } | ||||
| 
 | ||||
| export class MoveWizardState { | ||||
|     public readonly reasons: ReadonlyArray<MoveReason> | ||||
| 
 | ||||
|     public readonly moveDisallowedReason = new UIEventSource<Translation>(undefined) | ||||
|     private readonly layer: LayerConfig | ||||
|     private readonly _state: SpecialVisualizationState | ||||
|     private readonly featureToMoveId: string | ||||
| 
 | ||||
|     constructor(id: string, options: MoveConfig, state: SpecialVisualizationState) { | ||||
|     /** | ||||
|      * Initialize the movestate for the feature of the given ID | ||||
|      * @param id of the feature that should be moved | ||||
|      * @param options | ||||
|      * @param layer | ||||
|      * @param state | ||||
|      */ | ||||
|     constructor(id: string, options: MoveConfig, layer: LayerConfig, state: SpecialVisualizationState) { | ||||
|         this.layer = layer | ||||
|         this._state = state | ||||
|         this.reasons = MoveWizardState.initReasons(options) | ||||
|         this.featureToMoveId = id | ||||
|         this.reasons = this.initReasons(options) | ||||
|         if (this.reasons.length > 0) { | ||||
|             this.checkIsAllowed(id) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static initReasons(options: MoveConfig): MoveReason[] { | ||||
|     private initReasons(options: MoveConfig): MoveReason[] { | ||||
|         const t = Translations.t.move | ||||
| 
 | ||||
|         const reasons: MoveReason[] = [] | ||||
|         if (options.enableRelocation) { | ||||
|             reasons.push({ | ||||
|  | @ -72,20 +89,52 @@ export class MoveWizardState { | |||
|                 eraseAddressFields: false, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         const tags = this._state.featureProperties.getStore(this.featureToMoveId).data | ||||
|         const matchingPresets = this.layer.presets.filter(preset => preset.preciseInput.snapToLayers && new And(preset.tags).matchesProperties(tags)) | ||||
|         const matchingPreset = matchingPresets.flatMap(pr => pr.preciseInput?.snapToLayers) | ||||
|         for (const layerId of matchingPreset) { | ||||
|             const snapOntoLayer = this._state.layout.getLayer(layerId) | ||||
|             const text = <Translation> t.reasons.reasonSnapTo.PartialSubsTr("name", snapOntoLayer.snapName) | ||||
|             reasons.push({ | ||||
|                 text, | ||||
|                 invitingText: text, | ||||
|                 icon: "snap", | ||||
|                 changesetCommentValue: "snap", | ||||
|                 lockBounds: true, | ||||
|                 includeSearch: false, | ||||
|                 background: "photo", | ||||
|                 startZoom: 19, | ||||
|                 minZoom: 16, | ||||
|                 eraseAddressFields: false, | ||||
|                 snapTo: [snapOntoLayer.id], | ||||
|                 maxSnapDistance: 5, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         return reasons | ||||
|     } | ||||
| 
 | ||||
|     public async moveFeature( | ||||
|         loc: { lon: number; lat: number }, | ||||
|         snappedTo: WayId, | ||||
|         reason: MoveReason, | ||||
|         featureToMove: Feature<Point> | ||||
|         featureToMove: Feature<Point>, | ||||
|     ) { | ||||
|         const state = this._state | ||||
|         if(snappedTo !== undefined){ | ||||
|             this.moveDisallowedReason.set(Translations.t.move.partOfAWay) | ||||
|         } | ||||
|         await state.changes.applyAction( | ||||
|             new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], { | ||||
|                 reason: reason.changesetCommentValue, | ||||
|                 theme: state.layout.id, | ||||
|             }) | ||||
|             new ChangeLocationAction(state, | ||||
|                 featureToMove.properties.id, | ||||
|                 [loc.lon, loc.lat], | ||||
|                 snappedTo, | ||||
|                 { | ||||
|                     reason: reason.changesetCommentValue, | ||||
|                     theme: state.layout.id, | ||||
|                 }), | ||||
|         ) | ||||
|         featureToMove.properties._lat = loc.lat | ||||
|         featureToMove.properties._lon = loc.lon | ||||
|  | @ -104,8 +153,8 @@ export class MoveWizardState { | |||
|                     { | ||||
|                         changeType: "relocated", | ||||
|                         theme: state.layout.id, | ||||
|                     } | ||||
|                 ) | ||||
|                     }, | ||||
|                 ), | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -79,7 +79,6 @@ | |||
|   >([]) | ||||
| 
 | ||||
|   async function calculateQuestions() { | ||||
|     console.log("Applying questions to ask") | ||||
|     const qta = questionsToAsk.data | ||||
|     firstQuestion.setData(undefined) | ||||
|     //allQuestionsToAsk.setData([]) | ||||
|  |  | |||
|  | @ -104,7 +104,6 @@ | |||
|         {state} | ||||
|         {layer} | ||||
|         on:saved={() => (editMode = false)} | ||||
|         allowDeleteOfFreeform={true} | ||||
|       > | ||||
|         <button | ||||
|           slot="cancel" | ||||
|  |  | |||
|  | @ -33,6 +33,9 @@ | |||
|   import Markdown from "../../Base/Markdown.svelte" | ||||
|   import { Utils } from "../../../Utils" | ||||
|   import type { UploadableTag } from "../../../Logic/Tags/TagTypes" | ||||
|   import { Modal } from "flowbite-svelte" | ||||
|   import Popup from "../../Base/Popup.svelte" | ||||
|   import If from "../../Base/If.svelte" | ||||
| 
 | ||||
|   export let config: TagRenderingConfig | ||||
|   export let tags: UIEventSource<Record<string, string>> | ||||
|  | @ -43,13 +46,13 @@ | |||
|   export let selectedTags: UploadableTag = undefined | ||||
|   export let extraTags: UIEventSource<Record<string, string>> = new UIEventSource({}) | ||||
| 
 | ||||
|   export let allowDeleteOfFreeform: boolean = true | ||||
| 
 | ||||
|   export let clss = "interactive border-interactive" | ||||
| 
 | ||||
|   let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) | ||||
| 
 | ||||
|   let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)) | ||||
|   let isKnown = tags.mapD(tags => config.GetRenderValue(tags) !== undefined) | ||||
|   let matchesEmpty = config.GetRenderValue({}) !== undefined | ||||
| 
 | ||||
|   // Will be bound if a freeform is available | ||||
|   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]) | ||||
|  | @ -61,6 +64,12 @@ | |||
|    */ | ||||
|   let checkedMappings: boolean[] | ||||
| 
 | ||||
|   /** | ||||
|    * IF set: we can remove the current answer by deleting all those keys | ||||
|    */ | ||||
|   let settableKeys = tags.mapD(tags => config.removeToSetUnknown(layer, tags)) | ||||
|   let unknownModal = new UIEventSource(false) | ||||
| 
 | ||||
|   let searchTerm: UIEventSource<string> = new UIEventSource("") | ||||
| 
 | ||||
|   let dispatch = createEventDispatcher<{ | ||||
|  | @ -82,7 +91,7 @@ | |||
|       return !m.hideInAnswer.matchesProperties(tgs) | ||||
|     }) | ||||
|     selectedMapping = mappings?.findIndex( | ||||
|       (mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs) | ||||
|       (mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs), | ||||
|     ) | ||||
|     if (selectedMapping < 0) { | ||||
|       selectedMapping = undefined | ||||
|  | @ -144,7 +153,6 @@ | |||
| 
 | ||||
|   let usedKeys: string[] = Utils.Dedup(config.usedTags().flatMap((t) => t.usedKeys())) | ||||
| 
 | ||||
|   let keysToDeleteOnUnknown = config.settableKeys() | ||||
|   /** | ||||
|    * The 'minimalTags' is a subset of the tags of the feature, only containing the values relevant for this object. | ||||
|    * The main goal is to be stable and only 'ping' when an actual change is relevant | ||||
|  | @ -191,13 +199,12 @@ | |||
|       if (freeformValue?.length > 0) { | ||||
|         selectedMapping = config.mappings.length | ||||
|       } | ||||
|     }) | ||||
|     }), | ||||
|   ) | ||||
| 
 | ||||
|   $: { | ||||
|     if ( | ||||
|       config.freeform?.key && | ||||
|       allowDeleteOfFreeform && | ||||
|       !$freeformInput && | ||||
|       !$freeformInputUnvalidated && | ||||
|       !checkedMappings?.some((m) => m) && | ||||
|  | @ -210,7 +217,7 @@ | |||
|           $freeformInput, | ||||
|           selectedMapping, | ||||
|           checkedMappings, | ||||
|           tags.data | ||||
|           tags.data, | ||||
|         ) | ||||
|         if (featureSwitchIsDebugging?.data) { | ||||
|           console.log( | ||||
|  | @ -222,7 +229,7 @@ | |||
|               currentTags: tags.data, | ||||
|             }, | ||||
|             " --> ", | ||||
|             selectedTags | ||||
|             selectedTags, | ||||
|           ) | ||||
|         } | ||||
|       } catch (e) { | ||||
|  | @ -244,7 +251,7 @@ | |||
|         selectedTags = new And([...selectedTags.and, ...extraTagsArray]) | ||||
|       } else { | ||||
|         console.error( | ||||
|           "selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags) | ||||
|           "selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags), | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|  | @ -313,9 +320,24 @@ | |||
|     onDestroy( | ||||
|       state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { | ||||
|         numberOfCs = ud.csCount | ||||
|       }) | ||||
|       }), | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   function clearAnswer() { | ||||
|     const tagsToSet = settableKeys.data.map(k => new Tag(k, "")) | ||||
|     const change = new ChangeTagAction(tags.data.id, new And(tagsToSet), tags.data, { | ||||
|       theme: tags.data["_orig_theme"] ?? state.layout.id, | ||||
|       changeType: "answer", | ||||
|     }) | ||||
|     freeformInput.set(undefined) | ||||
|     selectedMapping = undefined | ||||
|     selectedTags = undefined | ||||
|     change | ||||
|       .CreateChangeDescriptions() | ||||
|       .then((changes) => state.changes.applyChanges(changes)) | ||||
|       .catch(console.error) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| {#if question !== undefined} | ||||
|  | @ -324,7 +346,7 @@ | |||
|       class="relative flex flex-col overflow-y-auto px-2" | ||||
|       style="max-height: 75vh" | ||||
|       on:submit|preventDefault={() => { | ||||
|         /*onSave(); This submit is not needed and triggers to early, causing bugs: see #1808*/ | ||||
|         /*onSave(); This submit is not needed and triggers too early, causing bugs: see #1808*/ | ||||
|       }} | ||||
|     > | ||||
|       <fieldset> | ||||
|  | @ -386,7 +408,7 @@ | |||
|           /> | ||||
|         {:else if config.mappings !== undefined && !config.multiAnswer} | ||||
|           <!-- Simple radiobuttons as mapping --> | ||||
|           <div class="flex flex-col no-bold"> | ||||
|           <div class="no-bold flex flex-col"> | ||||
|             {#each config.mappings as mapping, i (mapping.then)} | ||||
|               <!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices--> | ||||
|               <TagRenderingMappingInput | ||||
|  | @ -401,7 +423,7 @@ | |||
|               > | ||||
|                 <input | ||||
|                   type="radio" | ||||
|                   class="self-center mr-1" | ||||
|                   class="mr-1 self-center" | ||||
|                   bind:group={selectedMapping} | ||||
|                   name={"mappings-radio-" + config.id} | ||||
|                   value={i} | ||||
|  | @ -413,7 +435,7 @@ | |||
|               <label class="flex gap-x-1"> | ||||
|                 <input | ||||
|                   type="radio" | ||||
|                   class="self-center mr-1" | ||||
|                   class="mr-1 self-center" | ||||
|                   bind:group={selectedMapping} | ||||
|                   name={"mappings-radio-" + config.id} | ||||
|                   value={config.mappings?.length} | ||||
|  | @ -436,7 +458,7 @@ | |||
|           </div> | ||||
|         {:else if config.mappings !== undefined && config.multiAnswer} | ||||
|           <!-- Multiple answers can be chosen: checkboxes --> | ||||
|           <div class="flex flex-col no-bold"> | ||||
|           <div class="no-bold flex flex-col"> | ||||
|             {#each config.mappings as mapping, i (mapping.then)} | ||||
|               <TagRenderingMappingInput | ||||
|                 {mapping} | ||||
|  | @ -450,7 +472,7 @@ | |||
|               > | ||||
|                 <input | ||||
|                   type="checkbox" | ||||
|                   class="self-center mr-1" | ||||
|                   class="mr-1 self-center" | ||||
|                   name={"mappings-checkbox-" + config.id + "-" + i} | ||||
|                   bind:checked={checkedMappings[i]} | ||||
|                   on:keypress={(e) => onInputKeypress(e)} | ||||
|  | @ -461,7 +483,7 @@ | |||
|               <label class="flex gap-x-1"> | ||||
|                 <input | ||||
|                   type="checkbox" | ||||
|                   class="self-center mr-1" | ||||
|                   class="mr-1 self-center" | ||||
|                   name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length} | ||||
|                   bind:checked={checkedMappings[config.mappings.length]} | ||||
|                   on:keypress={(e) => onInputKeypress(e)} | ||||
|  | @ -494,36 +516,74 @@ | |||
|               <Tr t={$feedback} /> | ||||
|             </div> | ||||
|           {/if} | ||||
|           <!--{#if keysToDeleteOnUnknown?.some(k => !! $tags[k])} | ||||
|             Mark as unknown (delete {keysToDeleteOnUnknown?.filter(k => !! $tags[k]).join(";")}) | ||||
|             {/if}--> | ||||
| 
 | ||||
| 
 | ||||
|           <Popup shown={unknownModal}> | ||||
|             <h2 slot="header"> | ||||
|               <Tr t={Translations.t.unknown.title} /> | ||||
|             </h2> | ||||
|             <Tr t={Translations.t.unknown.explanation} /> | ||||
|             <If condition={state.userRelatedState.showTags.map(v => v === "yes" || v === "full" || v === "always")}> | ||||
|               <div class="subtle"> | ||||
|                 <Tr t={Translations.t.unknown.removedKeys}/> | ||||
|                 {#each $settableKeys as key} | ||||
|                   <code> | ||||
|                     <del> | ||||
|                       {key} | ||||
|                     </del> | ||||
|                   </code> | ||||
|                 {/each} | ||||
|               </div> | ||||
|             </If> | ||||
|             <div class="flex justify-end w-full" slot="footer"> | ||||
|               <button on:click={() => unknownModal.set(false)}> | ||||
|                 <Tr t={Translations.t.unknown.keep} /> | ||||
|               </button> | ||||
|               <button class="primary" on:click={() => {unknownModal.set(false); clearAnswer()}}> | ||||
|                 <Tr t={Translations.t.unknown.clear} /> | ||||
|               </button> | ||||
|             </div> | ||||
|           </Popup> | ||||
| 
 | ||||
|           <div | ||||
|             class="sticky bottom-0 flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap" | ||||
|             class="sticky bottom-0 flex justify-between flex-wrap" | ||||
|             style="z-index: 11" | ||||
|           > | ||||
|             <!-- TagRenderingQuestion-buttons --> | ||||
|             <slot name="cancel" /> | ||||
|             <slot name="save-button" {selectedTags}> | ||||
|               {#if config.freeform?.key && allowDeleteOfFreeform && !checkedMappings?.some((m) => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]} | ||||
|                 <button | ||||
|                   class="primary flex" | ||||
|                   on:click|stopPropagation|preventDefault={() => onSave()} | ||||
|                 > | ||||
|                   <TrashIcon class="h-6 w-6 text-red-500" /> | ||||
|                   <Tr t={Translations.t.general.eraseValue} /> | ||||
|                 </button> | ||||
|               {:else} | ||||
|                 <button | ||||
|                   on:click={() => onSave()} | ||||
|                   class={twJoin( | ||||
| 
 | ||||
|             {#if $settableKeys && $isKnown && !matchesEmpty } | ||||
|               <button class="as-link small text-sm" on:click={() => unknownModal.set(true)}> | ||||
|                 <Tr t={Translations.t.unknown.markUnknown} /> | ||||
|               </button> | ||||
|             {/if} | ||||
| 
 | ||||
| 
 | ||||
|             <div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap self-end flex-grow"> | ||||
| 
 | ||||
|               <!-- TagRenderingQuestion-buttons --> | ||||
|               <slot name="cancel" /> | ||||
|               <slot name="save-button" {selectedTags}> | ||||
|                 {#if config.freeform?.key && !checkedMappings?.some((m) => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]} | ||||
|                   <button | ||||
|                     class="primary flex" | ||||
|                     on:click|stopPropagation|preventDefault={() => onSave()} | ||||
|                   > | ||||
|                     <TrashIcon class="h-6 w-6 text-red-500" /> | ||||
|                     <Tr t={Translations.t.general.eraseValue} /> | ||||
|                   </button> | ||||
|                 {:else} | ||||
|                   <button | ||||
|                     on:click={() => onSave()} | ||||
|                     class={twJoin( | ||||
|                     selectedTags === undefined ? "disabled" : "button-shadow", | ||||
|                     "primary" | ||||
|                   )} | ||||
|                 > | ||||
|                   <Tr t={Translations.t.general.save} /> | ||||
|                 </button> | ||||
|               {/if} | ||||
|             </slot> | ||||
|                   > | ||||
|                     <Tr t={Translations.t.general.save} /> | ||||
|                   </button> | ||||
|                 {/if} | ||||
|               </slot> | ||||
|             </div> | ||||
| 
 | ||||
|           </div> | ||||
|           {#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging} | ||||
|             <span class="flex flex-wrap justify-between"> | ||||
|  |  | |||
|  | @ -28,8 +28,6 @@ | |||
|   export let selectedTags: UploadableTag = undefined | ||||
|   export let extraTags: UIEventSource<Record<string, string>> = new UIEventSource({}) | ||||
| 
 | ||||
|   export let allowDeleteOfFreeform: boolean = true | ||||
| 
 | ||||
|   let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement) | ||||
| </script> | ||||
| 
 | ||||
|  | @ -40,7 +38,6 @@ | |||
|   {selectedElement} | ||||
|   {layer} | ||||
|   {selectedTags} | ||||
|   {allowDeleteOfFreeform} | ||||
|   {extraTags} | ||||
| > | ||||
|   <slot name="cancel" slot="cancel" /> | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -28,8 +28,8 @@ | |||
|   } | ||||
| 
 | ||||
|   let configJson: Store<QuestionableTagRenderingConfigJson[]> = value.map((x) => { | ||||
|     if(x === undefined){ | ||||
|       console.log("No config found for ",path) | ||||
|     if (x === undefined) { | ||||
|       console.log("No config found for ", path) | ||||
|       return [] | ||||
|     } | ||||
|     if (typeof x === "string") { | ||||
|  |  | |||
|  | @ -1,4 +1,40 @@ | |||
| <script lang="ts"> | ||||
| import OHTable from "./InputElement/Helpers/OpeningHours/OHTable.svelte" | ||||
| import { UIEventSource } from "../Logic/UIEventSource" | ||||
| import type { OpeningHour } from "./OpeningHours/OpeningHours" | ||||
| export let value: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([ | ||||
|   { | ||||
|     weekday: 3, | ||||
|     startMinutes: 0, | ||||
|     endMinutes: 0, | ||||
|     startHour: 12, | ||||
|     endHour: 16 | ||||
|   }, | ||||
|   { | ||||
|     weekday: 0, | ||||
|     startMinutes: 0, | ||||
|     endMinutes: 0, | ||||
|     startHour: 0, | ||||
|     endHour: 24 | ||||
|   }, | ||||
|   { | ||||
|     weekday: 1, | ||||
|     startMinutes: 0, | ||||
|     endMinutes: 0, | ||||
|     startHour: 1, | ||||
|     endHour: 24 | ||||
|   }, | ||||
|   { | ||||
|     weekday: 2, | ||||
|     startMinutes: 0, | ||||
|     endMinutes: 0, | ||||
|     startHour: 12, | ||||
|     endHour: 24 | ||||
|   } | ||||
| ]) | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <main /> | ||||
| <main > | ||||
|   <OHTable {value}/> | ||||
| </main> | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ | |||
|   import FloatOver from "./Base/FloatOver.svelte" | ||||
|   import Constants from "../Models/Constants" | ||||
|   import LoginToggle from "./Base/LoginToggle.svelte" | ||||
|   import ModalRight from "./Base/ModalRight.svelte" | ||||
|   import LevelSelector from "./BigComponents/LevelSelector.svelte" | ||||
|   import type { RasterLayerPolygon } from "../Models/RasterLayers" | ||||
|   import { AvailableRasterLayers } from "../Models/RasterLayers" | ||||
|  | @ -30,7 +29,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" | ||||
|  | @ -49,6 +47,8 @@ | |||
|   import Searchbar from "./Base/Searchbar.svelte" | ||||
|   import ChevronRight from "@babeard/svelte-heroicons/mini/ChevronRight" | ||||
|   import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft" | ||||
|   import { Drawer } from "flowbite-svelte" | ||||
|   import { linear, sineIn } from "svelte/easing" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
| 
 | ||||
|  | @ -77,20 +77,26 @@ | |||
| 
 | ||||
|   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) | ||||
|   }) | ||||
| 
 | ||||
|   state.mapProperties.installCustomKeyboardHandler(viewport) | ||||
|  | @ -220,7 +226,7 @@ | |||
|               {#if $currentZoom < Constants.minZoomLevelToAddNewPoint} | ||||
|                 <Tr t={Translations.t.general.add.zoomInFurther} /> | ||||
|               {:else if state.layout.hasPresets()} | ||||
|                 <Tr t={Translations.t.general.add.title} /> | ||||
|                 ✨ <Tr t={Translations.t.general.add.title} /> | ||||
|               {:else} | ||||
|                 <Tr t={Translations.t.notes.addAComment} /> | ||||
|               {/if} | ||||
|  | @ -240,15 +246,14 @@ | |||
|             </MapControlButton> | ||||
|           </If> | ||||
|           <If condition={state.featureSwitches.featureSwitchBackgroundSelection}> | ||||
|             <OpenBackgroundSelectorButton | ||||
|               hideTooltip={true} | ||||
|               {state} | ||||
|             /> | ||||
|             <OpenBackgroundSelectorButton hideTooltip={true} {state} /> | ||||
|           </If> | ||||
|           <button | ||||
|             class="unstyled bg-black-transparent pointer-events-auto ml-1 h-fit max-h-12 cursor-pointer overflow-hidden rounded-2xl px-1 text-white opacity-50 hover:opacity-100" | ||||
|             style="background: #00000088; padding: 0.25rem; border-radius: 2rem;" | ||||
|             on:click={() => {state.guistate.pageStates.copyright.set(true)}} | ||||
|             on:click={() => { | ||||
|               state.guistate.pageStates.copyright.set(true) | ||||
|             }} | ||||
|           > | ||||
|             © <span class="hidden sm:inline sm:pr-2"> | ||||
|               OpenStreetMap | ||||
|  | @ -435,14 +440,27 @@ | |||
| 
 | ||||
|   {#if $selectedElement !== undefined && $selectedLayer !== undefined && !$selectedLayer.popupInFloatover} | ||||
|     <!-- right modal with the selected element view --> | ||||
|     <ModalRight | ||||
|       on:close={() => { | ||||
|         state.selectedElement.setData(undefined) | ||||
|       }} | ||||
|     <Drawer | ||||
|       placement="right" | ||||
|       transitionType="fly" | ||||
|       activateClickOutside={false} | ||||
|       backdrop={false} | ||||
|       id="drawer-right" | ||||
|       width="w-full md:w-6/12 lg:w-5/12 xl:w-4/12" | ||||
|       rightOffset="inset-y-0 right-0" | ||||
|       transitionParams={ { | ||||
|     x: 640, | ||||
|     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} /> | ||||
|     </ModalRight> | ||||
|     </Drawer> | ||||
|   {/if} | ||||
| 
 | ||||
|   {#if $selectedElement !== undefined && $selectedLayer !== undefined && $selectedLayer.popupInFloatover} | ||||
|  | @ -463,17 +481,13 @@ | |||
|           state.selectedElement.setData(undefined) | ||||
|         }} | ||||
|       > | ||||
|         <SelectedElementView {state} layer={$selectedLayer} selectedElement={$state_selectedElement} /> | ||||
|         <SelectedElementView | ||||
|           {state} | ||||
|           layer={$selectedLayer} | ||||
|           selectedElement={$state_selectedElement} | ||||
|         /> | ||||
|       </FloatOver> | ||||
|     {/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> | ||||
|  |  | |||
|  | @ -417,6 +417,9 @@ export class TypedTranslation<T extends Record<string, any>> extends Translation | |||
|         key: string, | ||||
|         replaceWith: Translation | ||||
|     ): TypedTranslation<Omit<T, K>> { | ||||
|         if(replaceWith === undefined){ | ||||
|             return this | ||||
|         } | ||||
|         const newTranslations: Record<string, string> = {} | ||||
|         const toSearch = "{" + key + "}" | ||||
|         const missingLanguages = new Set<string>(Object.keys(this.translations)) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue