More search functionality

This commit is contained in:
Pieter Vander Vennet 2024-08-22 22:50:37 +02:00
parent 5d0de8520b
commit 1c46a65c84
25 changed files with 962 additions and 846 deletions

View file

@ -13,7 +13,7 @@
autofocus
class="normal-background absolute top-0 right-0 flex h-screen w-full flex-col overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
role="dialog"
style="max-width: 100vw; max-height: 100vh"
style="max-width: 100vw; max-height: 100vh; z-index: 11"
tabindex="-1"
id="modal-right"
>

View file

@ -12,11 +12,15 @@
import { ariaLabel } from "../../Utils/ariaLabel"
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider"
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider"
import SearchResults from "./SearchResults.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import MoreScreen from "./MoreScreen"
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { focusWithArrows } from "../../Utils/focusWithArrows"
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
export let bounds: UIEventSource<BBox>
@ -82,13 +86,27 @@
return
}
const poi = result[0]
const [lat0, lat1, lon0, lon1] = poi.boundingbox
bounds.set(
new BBox([
[lon0, lat0],
[lon1, lat1]
]).pad(0.01)
)
if (poi.payload !== undefined) {
// This is a theme
const theme = <MinimalLayoutInformation>poi.payload
const url = MoreScreen.createUrlFor(theme, false)
console.log("Found a theme, going to", url)
// @ts-ignore
window.location = url
return
}
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() ?? [])
@ -107,6 +125,7 @@
}
dispatch("searchIsValid", false)
dispatch("searchCompleted")
isFocused.setData(false)
} catch (e) {
console.error(e)
feedback = Translations.t.general.search.error.txt
@ -116,45 +135,73 @@
}
}
let suggestions: Store<{success: GeoCodeResult[]} | {error}> = searchContents.stabilized(250).bindD(search =>
UIEventSource.FromPromiseWithErr(searcher.suggest(search))
let suggestions: Store<GeoCodeResult[]> = searchContents.stabilized(250).bindD(search => {
if (search.length === 0) {
return undefined
}
return searcher.suggest(search, { bbox: bounds.data })
}
)
let geosearch: HTMLDivElement
function checkFocus() {
window.requestAnimationFrame(() => {
if (geosearch.contains(document.activeElement)) {
return
}
isFocused.setData(false)
})
}
document.addEventListener("focus",() => {
checkFocus()
}, true /* use 'capturing' instead of bubbling, needed for focus-events*/)
</script>
<div class="normal-background flex justify-between rounded-full pl-2 w-full">
<form class="flex w-full flex-wrap">
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else}
<input
type="search"
class="w-full outline-none"
bind:this={inputElement}
on:keypress={(keypr) => {
<div bind:this={geosearch} use:focusWithArrows={"searchresult"}>
<div class="normal-background flex justify-between rounded-full pl-2 w-full">
<form class="flex w-full flex-wrap">
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else}
<input
type="search"
class="w-full outline-none"
bind:this={inputElement}
on:keypress={(keypr) => {
feedback = undefined
return keypr.key === "Enter" ? performSearch() : undefined
if(keypr.key === "Enter"){
performSearch()
keypr.preventDefault()
}
return undefined
}}
on:focus={() => {isFocused.setData(true)}}
on:blur={() => {isFocused.setData(false)}}
bind:value={$searchContents}
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>
on:focus={() => {isFocused.setData(true)}}
on:blur={() => {checkFocus()}}
bind:value={$searchContents}
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}
{/if}
</form>
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} />
</div>
</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" style="width: 25rem; max-width: 98vw">
<SearchResults {isFocused} {state} results={$suggestions} searchTerm={searchContents} on:select={() => {searchContents.set("")}}/>
</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>
</div>
</div>

View file

@ -16,14 +16,17 @@
import Tr from "../Base/Tr.svelte"
import { Translation } from "../i18n/Translation"
import MoreScreen from "./MoreScreen"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
export let entry: GeoCodeResult
export let state: SpecialVisualizationState
let layer: LayerConfig
let tags: UIEventSource<Record<string, string>>
let descriptionTr: TagRenderingConfig = undefined
if (entry.feature?.properties?.id) {
layer = state.layout.getMatchingLayer(entry.feature.properties)
tags = state.featureProperties.getStore(entry.feature.properties.id)
descriptionTr = layer.tagRenderings.find(tr => tr.labels.indexOf("description") >= 0)
}
let dispatch = createEventDispatcher<{ select }>()
@ -35,7 +38,6 @@
let otherTheme: MinimalLayoutInformation | undefined = <MinimalLayoutInformation>entry.payload
function select() {
console.log("Selected search entry", entry)
if (entry.boundingbox) {
const [lat0, lat1, lon0, lon1] = entry.boundingbox
state.mapProperties.bounds.set(
@ -56,7 +58,8 @@
</script>
{#if otherTheme}
<a href={ MoreScreen.createUrlFor(otherTheme, false)} class="flex items-center p-2 w-full gap-y-2 rounded-xl" >
<a href={ MoreScreen.createUrlFor(otherTheme, false)}
class="flex items-center p-2 w-full gap-y-2 rounded-xl searchresult">
<Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" />
<div class="flex flex-col">
@ -68,7 +71,7 @@
</a>
{:else}
<button class="unstyled w-full link-no-underline" on:click={() => select() }>
<button class="unstyled w-full link-no-underline searchresult" on:click={() => select() }>
<div class="p-2 flex items-center w-full gap-y-2">
{#if layer}
<ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} />
@ -84,20 +87,32 @@
{entry.display_name ?? entry.osm_id}
{/if}
</b>
<div class="flex gap-x-1 items-center">
{#if $bearing && !$inView}
<ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} />
{/if}
{#if $distance}
{GeoOperations.distanceToHuman($distance)}
{/if}
</div>
{#if $distance > 50}
<div class="flex gap-x-1 items-center">
{#if $bearing && !$inView}
<ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} />
{/if}
{#if $distance}
{GeoOperations.distanceToHuman($distance)}
{/if}
</div>
{/if}
</div>
<div class="flex flex-wrap gap-x-2">
{#if descriptionTr}
<TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state}
selectedElement={entry.feature} {layer} />
{/if}
{#if descriptionTr && entry.description}
{/if}
{#if entry.description}
<div class="subtle flex justify-between w-full">
{entry.description}
</div>
{/if}
</div>
{#if entry.description}
<div class="subtle flex justify-between w-full">
{entry.description}
</div>
{/if}
</div>
</div>

View file

@ -2,15 +2,15 @@
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
import SearchResult from "./SearchResult.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { XMarkIcon } from "@babeard/svelte-heroicons/solid"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Loading from "../Base/Loading.svelte"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
import MoreScreen from "./MoreScreen"
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
export let state: SpecialVisualizationState
export let results: { success: GeoCodeResult[] } | { error }
export let results: GeoCodeResult[]
export let searchTerm: Store<string>
export let isFocused: UIEventSource<boolean>
@ -19,63 +19,59 @@
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
</script>
<div class="w-full collapsable" style="height: 50rem;" class:collapsed={!$isFocused}>
{#if results?.["error"] !== undefined}
<div class="searchbox normal-background items-center">
An error occured
</div>
<div class="relative w-full h-full collapsable " class:collapsed={!$isFocused}>
<button class="absolute right-0 top-0 border-none p-0" on:click={() => isFocused.setData(false)} tabindex="-1">
<XCircleIcon class="w-6 h-6" />
</button>
{:else if $searchTerm.length > 0 && results === undefined}
<div class="searchbox normal-background items-center">
<Loading />
</div>
{:else if results?.["success"]?.length > 0}
<div class="relative w-full h-full">
<div class="absolute top-0 right-0 searchbox normal-background"
style="width: 25rem">
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto">
<div class="searchbox normal-background">
{#each results["success"] as entry (entry)}
<SearchResult on:select {entry} {state} />
{/each}
</div>
{#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">
<div class="absolute top-2 right-2 cursor-pointer" on:click={() => isFocused.setData(false)}>
<XMarkIcon class="w-4 h-4 hover:bg-stone-200 rounded-full" />
{#each results as entry (entry)}
<SearchResult on:select {entry} {state} />
{/each}
</div>
</div>
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
{: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}
<div class="searchbox normal-background overflow-y-auto h-full">
{#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}
<SearchResult {entry} {state} on:select />
{/each}
</div>
{/if}
{#if $recentlySeen?.length > 0}
<h3 class="mx-2">
<Tr t={Translations.t.general.search.recents} />
</h3>
{#each $recentlySeen as entry}
<SearchResult {entry} {state} on:select />
{/each}
{/if}
{#if $recentThemes?.length > 0 && $allowOtherThemes}
<h3 class="mx-2">
<Tr t={Translations.t.general.search.recentThemes} />
</h3>
{#each $recentThemes as themeId (themeId)}
<SearchResult
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}} {state}
on:select />
{/each}
{/if}
</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)}
<SearchResult
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}}
{state}
on:select />
{/each}
</div>
{/if}
</div>
{/if}
</div>
</div>
<style>
@ -91,7 +87,7 @@
.collapsable {
max-height: 50vh;
transition: max-height 400ms linear;
transition-delay: 500ms;
transition-delay: 100ms;
overflow: hidden;
padding: 0 !important;
}

View file

@ -16,6 +16,8 @@
export let layer: LayerConfig
export let config: TagRenderingConfig
export let extraClasses: string | undefined = undefined
export let defaultSize = "w-full"
export let noIcons = false
export let id: string = undefined
@ -28,7 +30,7 @@
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
<div {id} class={twMerge("link-underline flex h-full w-full flex-col", extraClasses)}>
<div {id} class={twMerge("link-underline flex h-full flex-col", defaultSize, extraClasses)}>
{#if $trs.length === 1}
<TagRenderingMapping
mapping={$trs[0]}
@ -37,13 +39,14 @@
{selectedElement}
{layer}
clss={config?.classes?.join(" ") ?? ""}
{noIcons}
/>
{/if}
{#if $trs.length > 1}
<ul>
{#each $trs as mapping}
<li>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} />
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} {noIcons}/>
</li>
{/each}
</ul>

View file

@ -12,6 +12,7 @@
export let tags: UIEventSource<Record<string, string>>
export let state: SpecialVisualizationState
export let layer: LayerConfig
export let noIcons = false
/**
* Css classes to apply
@ -32,7 +33,7 @@
}
</script>
{#if mapping.icon !== undefined}
{#if mapping.icon !== undefined && !noIcons}
<div class="inline-flex items-center">
<Marker
icons={mapping.icon}