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