Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-09-05 17:34:13 +02:00
commit 423618847b
334 changed files with 9307 additions and 6025 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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

View file

@ -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
View 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>

View file

@ -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>

View file

@ -14,7 +14,6 @@
const license: SmallLicense = licenses[key]
allLicenses[license.path] = license
}
</script>
{#each iconAttributions as iconAttribution}

View file

@ -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>

View file

View 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>

View file

@ -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>

View file

@ -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)
}
}

View file

@ -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}

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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}

View file

@ -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} />

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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
}
}

View 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} `}
/>

View 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>

View file

@ -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} />

View file

@ -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"}

View file

@ -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")

View file

@ -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://") &&

View file

@ -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}

View file

@ -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")}

View file

@ -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: {

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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()
}
}

View file

@ -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"]'
}

View file

@ -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}

View file

@ -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,
}
)
},
),
)
}

View file

@ -79,7 +79,6 @@
>([])
async function calculateQuestions() {
console.log("Applying questions to ask")
const qta = questionsToAsk.data
firstQuestion.setData(undefined)
//allQuestionsToAsk.setData([])

View file

@ -104,7 +104,6 @@
{state}
{layer}
on:saved={() => (editMode = false)}
allowDeleteOfFreeform={true}
>
<button
slot="cancel"

View file

@ -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">

View file

@ -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

View file

@ -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") {

View file

@ -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>

View file

@ -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>

View file

@ -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))