forked from MapComplete/MapComplete
More search functionality
This commit is contained in:
parent
5d0de8520b
commit
1c46a65c84
25 changed files with 962 additions and 846 deletions
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue