Improve search UI

This commit is contained in:
Pieter Vander Vennet 2024-08-30 02:18:29 +02:00
parent 3be286c2b1
commit 93f03ddbaf
22 changed files with 564 additions and 499 deletions

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { Drawer } from "flowbite-svelte"
import { sineIn } from "svelte/easing"
import { Store } from "../../Logic/UIEventSource.js"
import { onMount } from "svelte"
export let shown: Store<boolean>
let transitionParams = {
x: 640,
duration: 200,
easing: sineIn
}
let hidden = !shown.data
shown.addCallback(sh => {
hidden = !sh
})
let height = 0
onMount(() => {
let topbar = document.getElementById("top-bar")
height = topbar.clientHeight
})
</script>
<Drawer placement="right"
transitionType="fly" {transitionParams}
activateClickOutside={false}
divClass="overflow-y-auto"
backdrop={false}
id="drawer-right"
width="w-full sm:w-80 md:w-96"
rightOffset="inset-y-0 right-0"
bind:hidden={hidden}>
<div class="normal-background h-screen">
<div class="h-full" style={`padding-top: ${height}px`}>
<div class="flex flex-col h-full overflow-y-auto">
<slot />
</div>
</div>
</div>
</Drawer>

View file

@ -2,17 +2,12 @@
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import NextButton from "../Base/NextButton.svelte"
import Geosearch from "../Search/Geosearch.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twJoin } from "tailwind-merge"
import { Utils } from "../../Utils"
import { Store } from "../../Logic/UIEventSource"
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState"
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
import If from "../Base/If.svelte"
import { ExclamationTriangleIcon } from "@babeard/svelte-heroicons/mini"
import ChevronDoubleLeft from "@babeard/svelte-heroicons/solid/ChevronDoubleLeft"
import GeolocationIndicator from "./GeolocationIndicator.svelte"
/**
@ -20,10 +15,6 @@
*/
export let state: ThemeViewState
let layout = state.layout
let selectedElement = state.selectedElement
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
let searchEnabled = false
let geolocation = state.geolocation.geolocationState
let geopermission: Store<GeolocationPermissionState> = geolocation.permission
@ -35,7 +26,7 @@
state.geolocationControl.handleClick()
const glstate = state.geolocation.geolocationState
if (glstate.currentGPSLocation.data !== undefined) {
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
const c = glstate.currentGPSLocation.data
state.guistate.pageStates.about_theme.setData(false)
const coor = { lon: c.longitude, lat: c.latitude }
state.mapProperties.location.setData(coor)
@ -86,38 +77,6 @@
<Tr t={$gpsExplanation} />
</button>
</If>
<If condition={state.featureSwitches.featureSwitchSearch}>
<div
class=".button low-interaction m-1 flex h-fit w-full flex-wrap items-center justify-end gap-x-2 gap-y-2 rounded border p-1"
>
<div style="min-width: 16rem; " class="grow">
<Geosearch
bounds={state.mapProperties.bounds}
on:searchCompleted={() => state.guistate.pageStates.about_theme.setData(false)}
on:searchIsValid={(event) => {
searchEnabled = event.detail
}}
perLayer={state.perLayer}
{selectedElement}
{triggerSearch}
geolocationState={state.geolocation.geolocationState}
searcher={state.geosearch}
{state}
/>
</div>
<button
class={twJoin(
"small flex w-fit shrink-0 items-center justify-between gap-x-2",
!searchEnabled && "disabled"
)}
on:click={() => triggerSearch.ping()}
>
<Tr t={Translations.t.general.search.searchShort} />
<SearchIcon class="h-6 w-6" />
</button>
</div>
</If>
</div>
{#if $currentGPSLocation === undefined && $geopermission === "requested" && GeoLocationState.isSafari()}

View file

@ -104,8 +104,7 @@
</div>
{#if $reason.includeSearch}
searcher={state.geosearch}
<Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} searcher={state.geosearch} {state}/>
<Geosearch {state}/>
{/if}
<div class="flex flex-wrap">

View file

@ -1,20 +1,30 @@
<script lang="ts">
import type { ActiveFilter } from "../../Logic/State/LayerState"
import Tr from "../Base/Tr.svelte"
import Icon from "../Map/Icon.svelte"
import { Badge } from "flowbite-svelte"
import FilterOption from "./FilterOption.svelte"
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
import Loading from "../Base/Loading.svelte"
export let activeFilter: ActiveFilter
let { control, layer, filter } = activeFilter
let { control, filter } = activeFilter
let option = control.map(c => filter.options[c] ?? filter.options[0])
</script>
<div class="badge">
<FilterOption option={$option} />
<button on:click={() => control.setData(undefined)}>
let loading = false
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
</button>
</div>
function clear() {
loading = true
requestIdleCallback(() => {
control.setData(undefined)
loading = false
})
}
</script>
{#if loading}
<Loading />
{:else }
<div class="badge">
<FilterOption option={$option} />
<button on:click={() => clear()}>
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
</button>
</div>
{/if}

View file

@ -1,24 +1,37 @@
<script lang="ts">
import { default as ActiveFilterSvelte } from "./ActiveFilter.svelte"
import type { ActiveFilter } from "../../Logic/State/LayerState"
import Loading from "../Base/Loading.svelte"
export let activeFilters: ActiveFilter[]
let loading = false
function clear() {
for (const activeFilter of activeFilters) {
activeFilter.control.setData(undefined)
}
loading = true
requestIdleCallback(() => {
for (const activeFilter of activeFilters) {
activeFilter.control.setData(undefined)
}
loading = false
})
}
</script>
{#if activeFilters.length > 0}
<div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled">
{#each activeFilters as activeFilter (activeFilter)}
<ActiveFilterSvelte {activeFilter} />
{/each}
<h3>Active filters</h3>
<button class="as-link subtle" on:click={() => clear()}>
Clear filters
</button>
{#if loading}
<Loading />
{:else}
{#each activeFilters as activeFilter (activeFilter)}
<ActiveFilterSvelte {activeFilter} />
{/each}
<button class="as-link subtle" on:click={() => clear()}>
Clear filters
</button>
{/if}
</div>
{/if}

View file

@ -4,19 +4,15 @@
import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
import { createEventDispatcher } from "svelte"
import Icon from "../Map/Icon.svelte"
import SearchResultUtils from "./SearchResultUtils"
export let entry: {
category: "filter",
payload: FilterPayload
}
let { option, filter, layer, index } = entry.payload
export let entry: FilterPayload
let { option } = entry
export let state: SpecialVisualizationState
let dispatch = createEventDispatcher<{ select }>()
function apply() {
SearchResultUtils.apply(entry.payload, state)
state.searchState.apply(entry)
dispatch("select")
}
</script>

View file

@ -1,53 +1,28 @@
<script lang="ts">
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import Loading from "../Base/Loading.svelte"
import Hotkeys from "../Base/Hotkeys"
import { BBox } from "../../Logic/BBox"
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
import { createEventDispatcher, onDestroy } from "svelte"
import { placeholder } from "../../Utils/placeholder"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { ariaLabel } from "../../Utils/ariaLabel"
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider"
import SearchResults from "./SearchResults.svelte"
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { focusWithArrows } from "../../Utils/focusWithArrows"
import ShowDataLayer from "../Map/ShowDataLayer"
import ThemeViewState from "../../Models/ThemeViewState"
import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource"
import MoreScreen from "../BigComponents/MoreScreen"
import SearchResultUtils from "./SearchResultUtils"
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
export let bounds: UIEventSource<BBox>
export let selectedElement: UIEventSource<Feature> | undefined = undefined
export let geolocationState: GeoLocationState | undefined = undefined
export let clearAfterView: boolean = true
export let searcher: GeocodingProvider = new NominatimGeocoding()
export let state: ThemeViewState
let searchContents: UIEventSource<string> = new UIEventSource<string>("")
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
onDestroy(
triggerSearch.addCallback(() => {
performSearch()
})
)
export let searchContents: UIEventSource<string> = new UIEventSource<string>("")
let isRunning: boolean = false
function performSearch() {
state.searchState.performSearch()
}
let isRunning = state.searchState.isSearching
let inputElement: HTMLInputElement
let feedback: string = undefined
let isFocused = new UIEventSource(false)
export let isFocused = new UIEventSource(false)
function focusOnSearch() {
requestAnimationFrame(() => {
@ -57,7 +32,7 @@
}
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
feedback = undefined
state.searchState.feedback.set(undefined)
focusOnSearch()
})
@ -70,95 +45,6 @@
}
}
async function performSearch() {
try {
isRunning = true
geolocationState?.allowMoving.setData(true)
geolocationState?.requestMoment.setData(undefined) // If the GPS is still searching for a fix, we say that we don't want tozoom to it anymore
const searchContentsData = $searchContents?.trim() ?? ""
if (searchContentsData === "") {
return
}
const result = await searcher.search(searchContentsData, { bbox: bounds.data, limit: 10 })
if (result.length == 0) {
feedback = Translations.t.general.search.nothing.txt
focusOnSearch()
return
}
const poi = result[0]
if (poi.category === "theme") {
const theme = <MinimalLayoutInformation>poi.payload
const url = MoreScreen.createUrlFor(theme, false)
// @ts-ignore
window.location = url
return
}
if(poi.category === "filter"){
SearchResultUtils.apply(poi.payload, state)
}
if(poi.category === "filter"){
return // Should not happen
}
if (poi.boundingbox) {
const [lat0, lat1, lon0, lon1] = poi.boundingbox
bounds.set(
new BBox([
[lon0, lat0],
[lon1, lat1]
]).pad(0.01)
)
} else if (poi.lon && poi.lat) {
state.mapProperties.flyTo(poi.lon, poi.lat, GeocodingUtils.categoryToZoomLevel[poi.category] ?? 16)
}
if (perLayer !== undefined) {
const id = poi.osm_type + "/" + poi.osm_id
const layers = Array.from(perLayer?.values() ?? [])
for (const layer of layers) {
const found = layer.features.data.find((f) => f.properties.id === id)
if (found === undefined) {
continue
}
selectedElement?.setData(found)
break
}
}
if (clearAfterView) {
searchContents.setData("")
}
dispatch("searchIsValid", false)
dispatch("searchCompleted")
isFocused.setData(false)
} catch (e) {
console.error(e)
feedback = Translations.t.general.search.error.txt
focusOnSearch()
} finally {
isRunning = false
}
}
let suggestions: Store<SearchResult[]> = searchContents.stabilized(250).bindD(search => {
if (search.length === 0) {
return undefined
}
return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 })))
}
)
let geocededFeatures= new GeocodingFeatureSource(suggestions.stabilized(250))
state.featureProperties.trackFeatureSource(geocededFeatures)
new ShowDataLayer(
state.map,
{
layer: GeocodingUtils.searchLayer,
features: geocededFeatures,
selectedElement: state.selectedElement
}
)
let geosearch: HTMLDivElement
function checkFocus() {
@ -181,7 +67,7 @@
<div class="normal-background flex justify-between rounded-full pl-2 w-full">
<form class="flex w-full flex-wrap">
{#if isRunning}
{#if $isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else}
<input
@ -189,12 +75,11 @@
class="w-full outline-none"
bind:this={inputElement}
on:keypress={(keypr) => {
feedback = undefined
if(keypr.key === "Enter"){
performSearch()
keypr.preventDefault()
}
return undefined
if(keypr.key === "Enter"){
performSearch()
keypr.preventDefault()
}
return undefined
}}
on:focus={() => {isFocused.setData(true)}}
on:blur={() => {checkFocus()}}
@ -202,21 +87,9 @@
use:placeholder={Translations.t.general.search.search}
use:ariaLabel={Translations.t.general.search.search}
/>
{#if feedback !== undefined}
<!-- The feedback is _always_ shown for screenreaders and to make sure that the searchfield can still be selected by tabbing-->
<div class="alert" role="alert" aria-live="assertive">
{feedback}
</div>
{/if}
{/if}
</form>
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} />
</div>
<div class="relative h-0" style="z-index: 10">
<div class="absolute right-0 w-full sm:w-96 h-fit max-h-96">
<SearchResults {isFocused} {state} results={$suggestions} searchTerm={searchContents}
on:select={() => {searchContents.set(""); isFocused.setData(false)}} />
</div>
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={() => performSearch()} />
</div>
</div>

View file

@ -11,9 +11,9 @@
</script>
{#if entry.category === "theme"}
<ThemeResult {entry} on:select />
<ThemeResult entry={entry.payload} on:select />
{:else if entry.category === "filter"}
<FilterResult {entry} {state} on:select />
<FilterResult entry={entry.payload} {state} on:select />
{:else}
<GeocodeResult {entry} {state} on:select />
{/if}

View file

@ -1,25 +0,0 @@
import { SpecialVisualizationState } from "../SpecialVisualization"
import { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
export default class SearchResultUtils {
static apply(payload: FilterPayload, state: SpecialVisualizationState) {
const { layer, filter, index, option } = payload
let flayer = state.layerState.filteredLayers.get(layer.id)
let filtercontrol = flayer.appliedFilters.get(filter.id)
for (const [name, otherLayer] of state.layerState.filteredLayers) {
if (name === layer.id) {
otherLayer.isDisplayed.setData(true)
continue
}
otherLayer.isDisplayed.setData(false)
}
if (filtercontrol.data === index) {
filtercontrol.setData(undefined)
} else {
filtercontrol.setData(index)
}
}
}

View file

@ -1,104 +1,99 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Store } from "../../Logic/UIEventSource"
import Loading from "../Base/Loading.svelte"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
import { default as SearchResultSvelte } from "./SearchResult.svelte"
import MoreScreen from "../BigComponents/MoreScreen"
import type { GeocodeResult, SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider"
import ActiveFilters from "./ActiveFilters.svelte"
import Constants from "../../Models/Constants"
import type { ActiveFilter } from "../../Logic/State/LayerState"
import ThemeViewState from "../../Models/ThemeViewState"
import FilterResult from "./FilterResult.svelte"
import ThemeResult from "./ThemeResult.svelte"
export let state: SpecialVisualizationState
export let results: SearchResult[]
export let searchTerm: Store<string>
export let isFocused: UIEventSource<boolean>
export let state: ThemeViewState
let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0))
let hasActiveFilters = activeFilters.map(afs => afs.length > 0)
let recentlySeen: Store<GeocodeResult[]> = state.recentlySearched.seenThisSession
let recentlySeen: Store<GeocodeResult[]> = state.searchState.recentlySearched.seenThisSession
let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3))
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
let searchTerm = state.searchState.searchTerm
let results = state.searchState.suggestions
let filterResults = state.searchState.filterSuggestions
let themeResults = state.searchState.themeSuggestions
</script>
<div class="p-4">
<div class="relative w-full h-full collapsable " class:collapsed={!$isFocused && !$hasActiveFilters}>
<div class="searchbox normal-background">
<ActiveFilters activeFilters={$activeFilters} />
{#if $isFocused}
{#if $searchTerm.length > 0 && results === undefined}
<div class="flex justify-center m-4 my-8">
<Loading />
</div>
{:else if results?.length > 0}
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2" tabindex="-1">
<ActiveFilters activeFilters={$activeFilters} />
{#each results as entry (entry)}
<SearchResultSvelte on:select {entry} {state} />
{#if $filterResults.length > 0}
<h3>Pick a filter below</h3>
<div class="flex flex-wrap">
{#each $filterResults as filterResult (filterResult)}
<FilterResult {state} entry={filterResult} />
{/each}
</div>
{/if}
{#if $searchTerm.length > 0}
<h3>Locations</h3>
{/if}
{#if $searchTerm.length > 0 && $results === undefined}
<div class="flex justify-center m-4 my-8">
<Loading />
</div>
{:else if $results?.length > 0}
{#each $results as entry (entry)}
<SearchResultSvelte on:select {entry} {state} />
{/each}
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
<div class="flex flex-col gap-y-8"
tabindex="-1">
{#if $searchTerm.length > 0}
<b class="flex justify-center p-4">
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} />
</b>
{/if}
{#if $recentlySeen?.length > 0}
<div>
<h3 class="m-2">
<Tr t={Translations.t.general.search.recents} />
</h3>
{#each $recentlySeen as entry}
<SearchResultSvelte {entry} {state} on:select />
{/each}
</div>
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2 flex flex-col gap-y-8"
tabindex="-1">
{#if $searchTerm.length > 0}
<b class="flex justify-center p-4">
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} />
</b>
{/if}
{/if}
{#if $recentlySeen?.length > 0}
<div>
<h3 class="m-2">
<Tr t={Translations.t.general.search.recents} />
</h3>
{#each $recentlySeen as entry}
<SearchResultSvelte {entry} {state} on:select />
{/each}
</div>
{/if}
{#if $recentThemes?.length > 0 && $allowOtherThemes}
<div>
<h3 class="m-2">
<Tr t={Translations.t.general.search.recentThemes} />
</h3>
{#each $recentThemes as themeId (themeId)}
<SearchResultSvelte
entry={{payload: MoreScreen.officialThemesById.get(themeId), osm_id: themeId, category: "theme"}}
{state}
on:select />
{/each}
</div>
{/if}
{#if $recentThemes?.length > 0 && $allowOtherThemes}
<div>
<h3 class="m-2">
<Tr t={Translations.t.general.search.recentThemes} />
</h3>
{#each $recentThemes as themeId (themeId)}
<SearchResultSvelte
entry={{payload: MoreScreen.officialThemesById.get(themeId), osm_id: themeId, category: "theme"}}
{state}
on:select />
{/each}
</div>
{/if}
{/if}
</div>
</div>
{/if}
{#if $themeResults.length > 0}
<h3>
Other maps
</h3>
{#each $themeResults as entry}
<ThemeResult {state} {entry} />
{/each}
{/if}
</div>
<style>
.searchbox {
display: flex;
flex-direction: column;
row-gap: 0.5rem;
padding: 0.5rem;
border: 1px solid black;
border-radius: 0.5rem;
}
.collapsable {
max-height: 50vh;
transition: max-height 400ms linear;
transition-delay: 100ms;
overflow: hidden;
padding: 0 !important;
}
.collapsed {
padding-top: 0 !important;
padding-bottom: 0 !important;
max-height: 0 !important;
}
</style>

View file

@ -5,8 +5,8 @@
import Icon from "../Map/Icon.svelte"
import Tr from "../Base/Tr.svelte"
export let entry: { category: "theme", payload: MinimalLayoutInformation }
let otherTheme = entry.payload
export let entry: MinimalLayoutInformation
let otherTheme = entry
</script>
<a href={MoreScreen.createUrlFor(otherTheme, false)}

View file

@ -29,7 +29,7 @@ import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import { Map as MlMap } from "maplibre-gl"
import ShowDataLayer from "./Map/ShowDataLayer"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
import { RecentSearch } from "../Logic/Geocoding/RecentSearch"
import SearchState from "../Logic/State/SearchState"
/**
* The state needed to render a special Visualisation.
@ -95,7 +95,7 @@ export interface SpecialVisualizationState {
readonly previewedImage: UIEventSource<ProvidedImage>
readonly nearbyImageSearcher: CombinedFetcher
readonly geolocation: GeoLocationHandler
readonly recentlySearched: RecentSearch
readonly searchState: SearchState
getMatchingLayer(properties: Record<string, string>);

View file

@ -13,9 +13,7 @@
import type { MapProperties } from "../Models/MapProperties"
import Geosearch from "./Search/Geosearch.svelte"
import Translations from "./i18n/Translations"
import {
MenuIcon
} from "@rgossiaux/svelte-heroicons/solid"
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "./Base/Tr.svelte"
import FloatOver from "./Base/FloatOver.svelte"
import Constants from "../Models/Constants"
@ -41,13 +39,13 @@
import ReverseGeocoding from "./BigComponents/ReverseGeocoding.svelte"
import { BBox } from "../Logic/BBox"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton.svelte"
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
import Marker from "./Map/Marker.svelte"
import SelectedElementPanel from "./Base/SelectedElementPanel.svelte"
import MenuDrawer from "./BigComponents/MenuDrawer.svelte"
import DrawerLeft from "./Base/DrawerLeft.svelte"
import type { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
import { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider"
import DrawerRight from "./Base/DrawerRight.svelte"
import SearchResults from "./Search/SearchResults.svelte"
import { CloseButton } from "flowbite-svelte"
export let state: ThemeViewState
let layout = state.layout
@ -174,107 +172,6 @@
/>
</div>
{/if}
<div class="pointer-events-none absolute top-0 left-0 w-full">
<!-- Top components -->
<div
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse">
<!-- Top bar with tools -->
<div class="flex items-center">
<MapControlButton
cls="m-0.5 p-0.5 sm:p-1"
arialabel={Translations.t.general.labels.menu}
on:click={() => {console.log("Opening...."); state.guistate.menuIsOpened.setData(true)}}
on:keydown={forwardEventToMap}
>
<MenuIcon class="h-6 w-6 cursor-pointer" />
</MapControlButton>
<MapControlButton
on:click={() => state.guistate.pageStates.about_theme.set(true)}
on:keydown={forwardEventToMap}
>
<div
class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2"
>
<Marker icons={layout.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" />
<b class="mr-1">
<Tr t={layout.title} />
</b>
</div>
</MapControlButton>
</div>
<If condition={state.featureSwitches.featureSwitchSearch}>
<div class="w-full sm:w-64 my-2 sm:mt-0">
<Geosearch
bounds={state.mapProperties.bounds}
on:searchCompleted={() => {
state.map?.data?.getCanvas()?.focus()
}}
perLayer={state.perLayer}
selectedElement={state.selectedElement}
geolocationState={state.geolocation.geolocationState}
searcher={state.geosearch}
{state}
/>
</div>
</If>
</div>
<div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2">
<If condition={state.visualFeedback}>
{#if $selectedElement === undefined}
<div class="w-fit">
<VisualFeedbackPanel {state} />
</div>
{/if}
</If>
</div>
<div class="float-left m-1 flex flex-col sm:mt-2">
<If condition={state.featureSwitches.featureSwitchWelcomeMessage}>
</If>
{#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()}
<MapControlButton
on:click={() => {
state.selectCurrentView()
}}
on:keydown={forwardEventToMap}
>
<div class="h-8 w-8 cursor-pointer">
<ToSvelte construct={() => currentViewLayer.defaultIcon()} />
</div>
</MapControlButton>
{/if}
<ExtraLinkButton {state} />
<UploadingImageCounter featureId="*" showThankYou={false} {state} />
<PendingChangesIndicator {state} />
<If condition={state.featureSwitchIsTesting}>
<div class="alert w-fit">Testmode</div>
</If>
{#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")}
<div class="thanks">Testserver</div>
{/if}
<If condition={state.featureSwitches.featureSwitchFakeUser}>
<div class="alert w-fit">Faking a user (Testmode)</div>
</If>
</div>
<div class="flex w-full flex-col items-center justify-center">
<!-- Flex and w-full are needed for the positioning -->
<!-- Centermessage -->
<StateIndicator {state} />
<ReverseGeocoding {state} />
</div>
</div>
<div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen">
<!-- bottom controls -->
<div class="flex w-full items-end justify-between px-4">
@ -380,8 +277,110 @@
</If>
</div>
</div>
</div>
<DrawerRight shown={state.searchState.showSearchDrawer} }>
<div class="relative">
<div class="absolute right-0 top-0 ">
<div class="mr-4 mt-4">
<CloseButton on:click={() => state.searchState.showSearchDrawer.set(false)} />
</div>
</div>
<SearchResults {state} />
</div>
</DrawerRight>
<div class="pointer-events-none absolute top-0 left-0 w-full">
<!-- Top components -->
<div
id="top-bar"
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse">
<!-- Top bar with tools -->
<div class="flex items-center">
<MapControlButton
cls="m-0.5 p-0.5 sm:p-1"
arialabel={Translations.t.general.labels.menu}
on:click={() => {state.guistate.menuIsOpened.setData(true)}}
on:keydown={forwardEventToMap}
>
<MenuIcon class="h-6 w-6 cursor-pointer" />
</MapControlButton>
<MapControlButton
on:click={() => state.guistate.pageStates.about_theme.set(true)}
on:keydown={forwardEventToMap}
>
<div
class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2"
>
<Marker icons={layout.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" />
<b class="mr-1">
<Tr t={layout.title} />
</b>
</div>
</MapControlButton>
</div>
<If condition={state.featureSwitches.featureSwitchSearch}>
<div class="w-full sm:w-80 md:w-96 my-2 sm:mt-0">
<Geosearch {state} isFocused={state.searchState.searchIsFocused}
searchContents={state.searchState.searchTerm} />
</div>
</If>
</div>
<div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2">
<If condition={state.visualFeedback}>
{#if $selectedElement === undefined}
<div class="w-fit">
<VisualFeedbackPanel {state} />
</div>
{/if}
</If>
</div>
<div class="float-left m-1 flex flex-col sm:mt-2">
{#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()}
<MapControlButton
on:click={() => {
state.selectCurrentView()
}}
on:keydown={forwardEventToMap}
>
<div class="h-8 w-8 cursor-pointer">
<ToSvelte construct={() => currentViewLayer.defaultIcon()} />
</div>
</MapControlButton>
{/if}
<ExtraLinkButton {state} />
<UploadingImageCounter featureId="*" showThankYou={false} {state} />
<PendingChangesIndicator {state} />
<If condition={state.featureSwitchIsTesting}>
<div class="alert w-fit">Testmode</div>
</If>
{#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")}
<div class="thanks">Testserver</div>
{/if}
<If condition={state.featureSwitches.featureSwitchFakeUser}>
<div class="alert w-fit">Faking a user (Testmode)</div>
</If>
</div>
<div class="flex w-full flex-col items-center justify-center">
<!-- Flex and w-full are needed for the positioning -->
<!-- Centermessage -->
<StateIndicator {state} />
<ReverseGeocoding {state} />
</div>
</div>
<LoginToggle ignoreLoading={true} {state}>
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback}
<!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit -->