From 7aecea002585a7849864f2aca399e64f8e17dd97 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 23 Mar 2026 20:37:40 +0100 Subject: [PATCH 01/18] initial version --- src/sidebar/search/AddressInput.tsx | 70 +++++++++++++++-- .../AddressInputAutocomplete.module.css | 23 ++++++ .../search/AddressInputAutocomplete.tsx | 74 ++++++++++++++++-- src/sidebar/search/RecentLocations.ts | 75 +++++++++++++++++++ 4 files changed, 231 insertions(+), 11 deletions(-) create mode 100644 src/sidebar/search/RecentLocations.ts diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index d87b385e..922c53ed 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -1,7 +1,8 @@ import { JSX, ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { QueryPoint, QueryPointType } from '@/stores/QueryStore' import { Bbox, GeocodingHit, ReverseGeocodingHit } from '@/api/graphhopper' -import Autocomplete, { AutocompleteItem, GeocodingItem, POIQueryItem } from '@/sidebar/search/AddressInputAutocomplete' +import Autocomplete, { AutocompleteItem, GeocodingItem, POIQueryItem, RecentLocationItem } from '@/sidebar/search/AddressInputAutocomplete' +import { clearRecentLocations, getRecentLocations, saveRecentLocation } from '@/sidebar/search/RecentLocations' import ArrowBack from './arrow_back.svg' import Cross from '@/sidebar/times-solid-thin.svg' import CurrentLocationIcon from './current-location.svg' @@ -73,13 +74,23 @@ export default function AddressInput(props: AddressInputProps) { const [poiSearch] = useState(new ReverseGeocoder(getApi(), props.point, AddressParseResult.handleGeocodingResponse)) // if item is selected we need to clear the autocompletion list - useEffect(() => setAutocompleteItems([]), [props.point]) + useEffect(() => { + if (pendingItemsRef.current) { + setAutocompleteItems(pendingItemsRef.current) + pendingItemsRef.current = null + } else { + setAutocompleteItems([]) + } + }, [props.point]) // highlighted result of geocoding results. Keep track which index is highlighted and change things on ArrowUp and Down // on Enter select highlighted result or the 0th if nothing is highlighted const [highlightedResult, setHighlightedResult] = useState(-1) useEffect(() => setHighlightedResult(-1), [autocompleteItems]) + // items to restore after the props.point-change effect clears autocomplete + const pendingItemsRef = useRef(null) + // for positioning of the autocomplete we need: const searchInputContainer = useRef(null) @@ -106,7 +117,7 @@ export default function AddressInput(props: AddressInputProps) { setText(origText) } else if (nextIndex >= 0) { const item = autocompleteItems[nextIndex] - if (item instanceof GeocodingItem) setText(item.mainText) + if (item instanceof GeocodingItem || item instanceof RecentLocationItem) setText(item.mainText) else setText(origText) } } @@ -127,6 +138,8 @@ export default function AddressInput(props: AddressInputProps) { if (item instanceof POIQueryItem) { handlePoiSearch(poiSearch, item.result, props.map) props.onAddressSelected(item.result.text(item.result.poi), undefined) + } else if (item instanceof RecentLocationItem) { + props.onAddressSelected(item.toText(), item.point) } else if (highlightedResult < 0 && !props.point.isInitialized) { // by default use the first result, otherwise the highlighted one getApi() @@ -136,12 +149,15 @@ export default function AddressInput(props: AddressInputProps) { const hit: GeocodingHit = result.hits[0] const res = nominatimHitToItem(hit) props.onAddressSelected(res.mainText + ', ' + res.secondText, hit.point) + saveRecentLocation(res.mainText, res.secondText, hit.point) } else if (item instanceof GeocodingItem) { props.onAddressSelected(item.toText(), item.point) + saveRecentLocation(item.mainText, item.secondText, item.point) } }) } else if (item instanceof GeocodingItem) { props.onAddressSelected(item.toText(), item.point) + saveRecentLocation(item.mainText, item.secondText, item.point) } } // Enter: focus next address input, or blur if last @@ -201,8 +217,21 @@ export default function AddressInput(props: AddressInputProps) { onChange={e => { const query = e.target.value setText(query) - const coordinate = textToCoordinate(query) - if (!coordinate) geocoder.request(e.target.value, biasCoord, getMap().getView().getZoom()) + if (query === '') { + geocoder.cancel() + const recents = buildRecentItems(undefined, 5) + pendingItemsRef.current = recents.length > 0 ? recents : null + if (recents.length > 0) setAutocompleteItems(recents) + else setAutocompleteItems([]) + } else { + const coordinate = textToCoordinate(query) + if (!coordinate) { + const recents = buildRecentItems(query) + pendingItemsRef.current = recents.length > 0 ? recents : null + if (recents.length > 0) setAutocompleteItems(recents) + geocoder.request(query, biasCoord, getMap().getView().getZoom()) + } + } props.onChange(query) }} onKeyDown={onKeypress} @@ -210,6 +239,10 @@ export default function AddressInput(props: AddressInputProps) { setHasFocus(true) props.clearDragDrop() if (origAutocompleteItems.length > 0) setAutocompleteItems(origAutocompleteItems) + else if (text === '') { + const recents = buildRecentItems(undefined, 5) + if (recents.length > 0) setAutocompleteItems(recents) + } }} onBlur={() => { setHasFocus(false) @@ -233,6 +266,9 @@ export default function AddressInput(props: AddressInputProps) { onClick={e => { setText('') props.onChange('') + const recents = buildRecentItems(undefined, 5) + if (recents.length > 0) setAutocompleteItems(recents) + else setAutocompleteItems([]) // if we clear the text without focus then explicitly request it to improve usability: searchInput.current!.focus() }} @@ -268,12 +304,19 @@ export default function AddressInput(props: AddressInputProps) { onSelect={item => { if (item instanceof GeocodingItem) { props.onAddressSelected(item.toText(), item.point) + saveRecentLocation(item.mainText, item.secondText, item.point) + } else if (item instanceof RecentLocationItem) { + props.onAddressSelected(item.toText(), item.point) } else if (item instanceof POIQueryItem) { handlePoiSearch(poiSearch, item.result, props.map) setText(item.result.text(item.result.poi)) } searchInput.current!.blur() // see also AutocompleteEntry->onMouseDown }} + onClearRecents={() => { + clearRecentLocations() + setAutocompleteItems([]) + }} /> )} @@ -282,6 +325,23 @@ export default function AddressInput(props: AddressInputProps) { ) } +function buildRecentItems(filter?: string, limit?: number): RecentLocationItem[] { + let recents = getRecentLocations(1) + if (filter) { + const lower = filter.toLowerCase() + recents = recents.filter( + e => + e.mainText.toLowerCase().startsWith(lower) || + e.secondText.toLowerCase().split(/[\s,]+/).some(word => word.startsWith(lower)), + ) + } + if (limit) recents = recents.slice(0, limit) + return recents.map( + e => + new RecentLocationItem(e.mainText, e.secondText, { lat: e.lat, lng: e.lng }, getBBoxFromCoord({ lat: e.lat, lng: e.lng })), + ) +} + function handlePoiSearch(poiSearch: ReverseGeocoder, result: AddressParseResult, map: Map) { if (!result.hasPOIs()) return diff --git a/src/sidebar/search/AddressInputAutocomplete.module.css b/src/sidebar/search/AddressInputAutocomplete.module.css index 6fa88ed9..2b69511a 100644 --- a/src/sidebar/search/AddressInputAutocomplete.module.css +++ b/src/sidebar/search/AddressInputAutocomplete.module.css @@ -61,3 +61,26 @@ font-size: small; color: #5b616a; } + +.recentHeader { + display: flex; + justify-content: space-between; + align-items: center; + font-size: small; + font-weight: bold; + color: #5b616a; + padding: 0.3rem 0.5rem 0; +} + +.clearRecentsButton { + border: none; + background: none; + color: #5b616a; + font-size: small; + cursor: pointer; + padding: 0; +} + +.clearRecentsButton:hover { + color: #333; +} diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index 292a8cf7..2ba2bebe 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -22,6 +22,24 @@ export class GeocodingItem implements AutocompleteItem { } } +export class RecentLocationItem implements AutocompleteItem { + mainText: string + secondText: string + point: { lat: number; lng: number } + bbox: Bbox + + constructor(mainText: string, secondText: string, point: { lat: number; lng: number }, bbox: Bbox) { + this.mainText = mainText + this.secondText = secondText + this.point = point + this.bbox = bbox + } + + toText() { + return this.mainText + ', ' + this.secondText + } +} + export class POIQueryItem implements AutocompleteItem { result: AddressParseResult @@ -34,16 +52,39 @@ export interface AutocompleteProps { items: AutocompleteItem[] highlightedItem: AutocompleteItem onSelect: (hit: AutocompleteItem) => void + onClearRecents?: () => void } -export default function Autocomplete({ items, highlightedItem, onSelect }: AutocompleteProps) { +export default function Autocomplete({ items, highlightedItem, onSelect, onClearRecents }: AutocompleteProps) { + let recentHeaderShown = false return (
    - {items.map((item, i) => ( -
  • - {mapToComponent(item, highlightedItem === item, onSelect)} -
  • - ))} + {items.map((item, i) => { + let header = null + if (item instanceof RecentLocationItem && !recentHeaderShown) { + recentHeaderShown = true + header = ( +
    + Recent + {onClearRecents && ( + + )} +
    + ) + } + return ( +
  • + {header} + {mapToComponent(item, highlightedItem === item, onSelect)} +
  • + ) + })}
) } @@ -51,6 +92,8 @@ export default function Autocomplete({ items, highlightedItem, onSelect }: Autoc function mapToComponent(item: AutocompleteItem, isHighlighted: boolean, onSelect: (hit: AutocompleteItem) => void) { if (item instanceof GeocodingItem) return + else if (item instanceof RecentLocationItem) + return else if (item instanceof POIQueryItem) return else throw Error('Unsupported item type: ' + typeof item) @@ -95,6 +138,25 @@ function GeocodingEntry({ ) } +function RecentLocationEntry({ + item, + isHighlighted, + onSelect, +}: { + item: RecentLocationItem + isHighlighted: boolean + onSelect: (item: RecentLocationItem) => void +}) { + return ( + onSelect(item)}> +
+ {item.mainText} + {item.secondText} +
+
+ ) +} + function AutocompleteEntry({ isHighlighted, children, diff --git a/src/sidebar/search/RecentLocations.ts b/src/sidebar/search/RecentLocations.ts new file mode 100644 index 00000000..fb184ed7 --- /dev/null +++ b/src/sidebar/search/RecentLocations.ts @@ -0,0 +1,75 @@ +import { calcDist, Coordinate } from '@/utils' +import { tr } from '@/translation/Translation' +import { textToCoordinate } from '@/Converters' + +const STORAGE_KEY = 'recentLocations' +const MAX_ENTRIES = 20 +const DEDUP_DISTANCE_METERS = 100 + +export interface RecentLocation { + mainText: string + secondText: string + lat: number + lng: number + timestamp: number + count: number +} + +export function getRecentLocations(minCount: number = 0): RecentLocation[] { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + const parsed = JSON.parse(raw) + if (!Array.isArray(parsed)) return [] + return parsed + .filter( + (e: any) => + typeof e.mainText === 'string' && + typeof e.secondText === 'string' && + typeof e.lat === 'number' && + typeof e.lng === 'number' && + typeof e.timestamp === 'number', + ) + .map((e: any) => ({ ...e, count: typeof e.count === 'number' ? e.count : 1 })) + .filter((e: RecentLocation) => e.count > minCount) + .sort((a: RecentLocation, b: RecentLocation) => b.timestamp - a.timestamp) + } catch { + return [] + } +} + +export function clearRecentLocations(): void { + try { + localStorage.removeItem(STORAGE_KEY) + } catch { + // localStorage unavailable + } +} + +export function saveRecentLocation(mainText: string, secondText: string, coordinate: Coordinate): void { + if (mainText === tr('current_location')) return + if (textToCoordinate(mainText)) return + + try { + const all = getRecentLocations() + const existing = all.find( + e => calcDist({ lat: e.lat, lng: e.lng }, coordinate) <= DEDUP_DISTANCE_METERS, + ) + const prevCount = existing ? existing.count : 0 + const filtered = all.filter( + e => calcDist({ lat: e.lat, lng: e.lng }, coordinate) > DEDUP_DISTANCE_METERS, + ) + + filtered.unshift({ + mainText, + secondText, + lat: coordinate.lat, + lng: coordinate.lng, + timestamp: Date.now(), + count: prevCount + 1, + }) + localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered.slice(0, MAX_ENTRIES))) + } catch { + // localStorage unavailable (private browsing, quota exceeded) + } +} From 86256101327a70372d734825123702ec2ca05ca9 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 23 Mar 2026 20:43:54 +0100 Subject: [PATCH 02/18] simpler --- src/sidebar/search/AddressInput.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 922c53ed..09caa960 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -75,12 +75,7 @@ export default function AddressInput(props: AddressInputProps) { // if item is selected we need to clear the autocompletion list useEffect(() => { - if (pendingItemsRef.current) { - setAutocompleteItems(pendingItemsRef.current) - pendingItemsRef.current = null - } else { - setAutocompleteItems([]) - } + if (props.point.isInitialized) setAutocompleteItems([]) }, [props.point]) // highlighted result of geocoding results. Keep track which index is highlighted and change things on ArrowUp and Down @@ -88,9 +83,6 @@ export default function AddressInput(props: AddressInputProps) { const [highlightedResult, setHighlightedResult] = useState(-1) useEffect(() => setHighlightedResult(-1), [autocompleteItems]) - // items to restore after the props.point-change effect clears autocomplete - const pendingItemsRef = useRef(null) - // for positioning of the autocomplete we need: const searchInputContainer = useRef(null) @@ -220,14 +212,12 @@ export default function AddressInput(props: AddressInputProps) { if (query === '') { geocoder.cancel() const recents = buildRecentItems(undefined, 5) - pendingItemsRef.current = recents.length > 0 ? recents : null if (recents.length > 0) setAutocompleteItems(recents) else setAutocompleteItems([]) } else { const coordinate = textToCoordinate(query) if (!coordinate) { const recents = buildRecentItems(query) - pendingItemsRef.current = recents.length > 0 ? recents : null if (recents.length > 0) setAutocompleteItems(recents) geocoder.request(query, biasCoord, getMap().getView().getZoom()) } From 8a0645bd6b2b0641de077c9d20f9f3b0dd480e73 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 23 Mar 2026 20:54:49 +0100 Subject: [PATCH 03/18] minor fixes --- src/sidebar/search/AddressInput.tsx | 11 +++++----- .../search/AddressInputAutocomplete.tsx | 21 ++++++++++++++----- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 09caa960..b86717a1 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -217,8 +217,10 @@ export default function AddressInput(props: AddressInputProps) { } else { const coordinate = textToCoordinate(query) if (!coordinate) { - const recents = buildRecentItems(query) - if (recents.length > 0) setAutocompleteItems(recents) + if (query.length < 2) { + const recents = buildRecentItems(query) + if (recents.length > 0) setAutocompleteItems(recents) + } geocoder.request(query, biasCoord, getMap().getView().getZoom()) } } @@ -326,10 +328,7 @@ function buildRecentItems(filter?: string, limit?: number): RecentLocationItem[] ) } if (limit) recents = recents.slice(0, limit) - return recents.map( - e => - new RecentLocationItem(e.mainText, e.secondText, { lat: e.lat, lng: e.lng }, getBBoxFromCoord({ lat: e.lat, lng: e.lng })), - ) + return recents.map(e => new RecentLocationItem(e.mainText, e.secondText, { lat: e.lat, lng: e.lng })) } function handlePoiSearch(poiSearch: ReverseGeocoder, result: AddressParseResult, map: Map) { diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index 2ba2bebe..88c026e0 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -26,13 +26,11 @@ export class RecentLocationItem implements AutocompleteItem { mainText: string secondText: string point: { lat: number; lng: number } - bbox: Bbox - constructor(mainText: string, secondText: string, point: { lat: number; lng: number }, bbox: Bbox) { + constructor(mainText: string, secondText: string, point: { lat: number; lng: number }) { this.mainText = mainText this.secondText = secondText this.point = point - this.bbox = bbox } toText() { @@ -65,14 +63,27 @@ export default function Autocomplete({ items, highlightedItem, onSelect, onClear recentHeaderShown = true header = (
- Recent + + + + + {onClearRecents && ( )}
From bacbd8952f31ec34338492c4cee4113b7452cd28 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 23 Mar 2026 21:00:10 +0100 Subject: [PATCH 04/18] sort favs first by count --- src/sidebar/search/AddressInputAutocomplete.tsx | 2 +- src/sidebar/search/RecentLocations.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index 88c026e0..de2cf185 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -63,7 +63,7 @@ export default function Autocomplete({ items, highlightedItem, onSelect, onClear recentHeaderShown = true header = (
- + ({ ...e, count: typeof e.count === 'number' ? e.count : 1 })) .filter((e: RecentLocation) => e.count > minCount) - .sort((a: RecentLocation, b: RecentLocation) => b.timestamp - a.timestamp) + .sort((a: RecentLocation, b: RecentLocation) => b.count - a.count || b.timestamp - a.timestamp) } catch { return [] } From 844c5fd17f85862ab2523738d3f8cca0c44ff839 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 23 Mar 2026 21:17:35 +0100 Subject: [PATCH 05/18] limit to 5 --- src/sidebar/search/AddressInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index b86717a1..12a55081 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -218,7 +218,7 @@ export default function AddressInput(props: AddressInputProps) { const coordinate = textToCoordinate(query) if (!coordinate) { if (query.length < 2) { - const recents = buildRecentItems(query) + const recents = buildRecentItems(query, 5) if (recents.length > 0) setAutocompleteItems(recents) } geocoder.request(query, biasCoord, getMap().getView().getZoom()) From 77604d5bb85ac9b83ed64fedf2840d8faf7e9900 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 26 Mar 2026 15:09:24 +0100 Subject: [PATCH 06/18] move clock to right + every item --- .../AddressInputAutocomplete.module.css | 26 +++++-- .../search/AddressInputAutocomplete.tsx | 69 +++++++++---------- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/src/sidebar/search/AddressInputAutocomplete.module.css b/src/sidebar/search/AddressInputAutocomplete.module.css index 2b69511a..b8e24fe1 100644 --- a/src/sidebar/search/AddressInputAutocomplete.module.css +++ b/src/sidebar/search/AddressInputAutocomplete.module.css @@ -62,14 +62,30 @@ color: #5b616a; } -.recentHeader { +.recentEntry { display: flex; - justify-content: space-between; align-items: center; - font-size: small; - font-weight: bold; + text-align: start; + margin: 0.4rem 0.5rem; +} + +.recentEntryText { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.recentIcon { + flex-shrink: 0; color: #5b616a; - padding: 0.3rem 0.5rem 0; + margin-left: 0.5rem; +} + +.recentFooter { + display: flex; + justify-content: flex-end; + padding: 0.2rem 0.5rem 0.3rem; } .clearRecentsButton { diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index de2cf185..059bbe5e 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -54,45 +54,34 @@ export interface AutocompleteProps { } export default function Autocomplete({ items, highlightedItem, onSelect, onClearRecents }: AutocompleteProps) { - let recentHeaderShown = false + let lastRecentIndex = -1 + for (let j = items.length - 1; j >= 0; j--) { + if (items[j] instanceof RecentLocationItem) { lastRecentIndex = j; break } + } return (
    {items.map((item, i) => { - let header = null - if (item instanceof RecentLocationItem && !recentHeaderShown) { - recentHeaderShown = true - header = ( -
    - - - - - - {onClearRecents && ( - - )} -
    - ) - } + const footer = i === lastRecentIndex && onClearRecents ? ( +
    + +
    + ) : null return (
  • - {header} {mapToComponent(item, highlightedItem === item, onSelect)} + {footer}
  • ) })} @@ -160,9 +149,17 @@ function RecentLocationEntry({ }) { return ( onSelect(item)}> -
    - {item.mainText} - {item.secondText} +
    +
    + {item.mainText} + {item.secondText} +
    + + +
    ) From cef73b9138408b9bca372bdb4619dea72200e22c Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 26 Mar 2026 15:28:55 +0100 Subject: [PATCH 07/18] recent via text --- .../AddressInputAutocomplete.module.css | 39 ++++++------ .../search/AddressInputAutocomplete.tsx | 62 +++++++++---------- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/src/sidebar/search/AddressInputAutocomplete.module.css b/src/sidebar/search/AddressInputAutocomplete.module.css index b8e24fe1..7949d19f 100644 --- a/src/sidebar/search/AddressInputAutocomplete.module.css +++ b/src/sidebar/search/AddressInputAutocomplete.module.css @@ -62,39 +62,38 @@ color: #5b616a; } -.recentEntry { +.recentLocationEntry { display: flex; - align-items: center; + flex-direction: column; text-align: start; - margin: 0.4rem 0.5rem; + margin: 0.2rem 0.5rem; } -.recentEntryText { +.recentHeader { display: flex; - flex-direction: column; - flex: 1; - min-width: 0; -} - -.recentIcon { - flex-shrink: 0; - color: #5b616a; - margin-left: 0.5rem; + justify-content: space-between; + align-items: center; + padding: 0.1rem 0.5rem 0 0.5rem; + margin-bottom: -0.3rem; + padding-right: 0.1rem; } -.recentFooter { - display: flex; - justify-content: flex-end; - padding: 0.2rem 0.5rem 0.3rem; +.recentHeaderText { + font-size: small; + color: #999; } .clearRecentsButton { border: none; background: none; - color: #5b616a; - font-size: small; + color: currentColor; cursor: pointer; - padding: 0; + padding: 0.3rem; + min-width: 1.5rem; + min-height: 1.5rem; + display: flex; + align-items: center; + justify-content: center; } .clearRecentsButton:hover { diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index 059bbe5e..36d267a8 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -54,34 +54,38 @@ export interface AutocompleteProps { } export default function Autocomplete({ items, highlightedItem, onSelect, onClearRecents }: AutocompleteProps) { - let lastRecentIndex = -1 - for (let j = items.length - 1; j >= 0; j--) { - if (items[j] instanceof RecentLocationItem) { lastRecentIndex = j; break } - } + let recentHeaderShown = false return (
      {items.map((item, i) => { - const footer = i === lastRecentIndex && onClearRecents ? ( -
      - -
      - ) : null + let header = null + if (item instanceof RecentLocationItem && !recentHeaderShown) { + recentHeaderShown = true + header = ( +
      + Recent + {onClearRecents && ( + + )} +
      + ) + } return (
    • + {header} {mapToComponent(item, highlightedItem === item, onSelect)} - {footer}
    • ) })} @@ -149,17 +153,9 @@ function RecentLocationEntry({ }) { return ( onSelect(item)}> -
      -
      - {item.mainText} - {item.secondText} -
      - - - +
      + {item.mainText} + {item.secondText}
      ) From fbf340ebcab1f4b636450fc4eaa17db2f918d607 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 26 Mar 2026 15:38:37 +0100 Subject: [PATCH 08/18] fix clear button color --- src/sidebar/search/AddressInputAutocomplete.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sidebar/search/AddressInputAutocomplete.module.css b/src/sidebar/search/AddressInputAutocomplete.module.css index 7949d19f..019b3d0a 100644 --- a/src/sidebar/search/AddressInputAutocomplete.module.css +++ b/src/sidebar/search/AddressInputAutocomplete.module.css @@ -86,7 +86,7 @@ .clearRecentsButton { border: none; background: none; - color: currentColor; + color: #5b616a; cursor: pointer; padding: 0.3rem; min-width: 1.5rem; From 446daccf589c837933819ea5736fe588a5066a9b Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 28 Mar 2026 13:21:59 +0100 Subject: [PATCH 09/18] bug fix and simplify --- src/sidebar/search/RecentLocations.ts | 22 +++++++------- test/RecentLocations.test.ts | 44 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 test/RecentLocations.test.ts diff --git a/src/sidebar/search/RecentLocations.ts b/src/sidebar/search/RecentLocations.ts index b5a084eb..77167522 100644 --- a/src/sidebar/search/RecentLocations.ts +++ b/src/sidebar/search/RecentLocations.ts @@ -3,7 +3,7 @@ import { tr } from '@/translation/Translation' import { textToCoordinate } from '@/Converters' const STORAGE_KEY = 'recentLocations' -const MAX_ENTRIES = 20 +export const MAX_ENTRIES = 15 const DEDUP_DISTANCE_METERS = 100 export interface RecentLocation { @@ -46,28 +46,28 @@ export function clearRecentLocations(): void { } } -export function saveRecentLocation(mainText: string, secondText: string, coordinate: Coordinate): void { +export function saveRecentLocation(mainText: string, secondText: string, coordinate: Coordinate, now: number = Date.now()): void { if (mainText === tr('current_location')) return if (textToCoordinate(mainText)) return try { const all = getRecentLocations() - const existing = all.find( - e => calcDist({ lat: e.lat, lng: e.lng }, coordinate) <= DEDUP_DISTANCE_METERS, - ) - const prevCount = existing ? existing.count : 0 - const filtered = all.filter( - e => calcDist({ lat: e.lat, lng: e.lng }, coordinate) > DEDUP_DISTANCE_METERS, - ) + let prevCount = 0 + const filtered = all.filter(e => { + const isDuplicate = calcDist({ lat: e.lat, lng: e.lng }, coordinate) <= DEDUP_DISTANCE_METERS + if (isDuplicate) prevCount = e.count + return !isDuplicate + }) - filtered.unshift({ + filtered.push({ mainText, secondText, lat: coordinate.lat, lng: coordinate.lng, - timestamp: Date.now(), + timestamp: now, count: prevCount + 1, }) + filtered.sort((a, b) => b.timestamp - a.timestamp) localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered.slice(0, MAX_ENTRIES))) } catch { // localStorage unavailable (private browsing, quota exceeded) diff --git a/test/RecentLocations.test.ts b/test/RecentLocations.test.ts new file mode 100644 index 00000000..d6a59e0e --- /dev/null +++ b/test/RecentLocations.test.ts @@ -0,0 +1,44 @@ +import { getRecentLocations, saveRecentLocation, MAX_ENTRIES } from '@/sidebar/search/RecentLocations' + +jest.mock('@/translation/Translation', () => ({ tr: (key: string) => key })) +jest.mock('@/Converters', () => ({ textToCoordinate: () => null })) + +beforeEach(() => localStorage.clear()) + +function save(name: string, lat: number, timestamp: number) { + saveRecentLocation(name, '', { lat, lng: 0 }, timestamp) +} + +function names() { + return getRecentLocations().map(l => l.mainText) +} + +describe('RecentLocations', () => { + it('should dedup nearby locations and increment count', () => { + save('A', 1, 1000) + save('A2', 1, 2000) + const locs = getRecentLocations() + expect(locs).toHaveLength(1) + expect(locs[0]).toMatchObject({ mainText: 'A2', count: 2, timestamp: 2000 }) + }) + + it('should evict oldest entry when MAX is reached, not a recent one', () => { + for (let i = 0; i < MAX_ENTRIES; i++) save(`Old${i}`, 10 + i, 1000 + i) + + save('Recent', 90, 100_000) + + expect(names()).toContain('Recent') + expect(names()).not.toContain('Old0') // oldest timestamp evicted + expect(getRecentLocations()).toHaveLength(MAX_ENTRIES) + }) + + it('should keep a re-used location from being evicted', () => { + for (let i = 0; i < MAX_ENTRIES; i++) save(`P${i}`, 10 + i, 1000 + i) + + save('P0', 10, 200_000) // re-use refreshes timestamp + save('New', 90, 200_001) // triggers eviction + + expect(names()).toContain('P0') + expect(names()).not.toContain('P1') // now the oldest + }) +}) From 61e0425d3e2ee60b237da969b64bb02f55e6b05a Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 28 Mar 2026 13:31:06 +0100 Subject: [PATCH 10/18] as we now show all locations (min=0 instead 1) the previous input shouldn't be shown --- src/sidebar/search/AddressInput.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 43386e3c..f6820b33 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -18,7 +18,7 @@ import { toLonLat, transformExtent } from 'ol/proj' import { Map } from 'ol' import { AddressParseResult } from '@/pois/AddressParseResult' import { getMap } from '@/map/map' -import { Coordinate, getBBoxFromCoord } from '@/utils' +import { calcDist, Coordinate, getBBoxFromCoord } from '@/utils' export interface AddressInputProps { point: QueryPoint @@ -42,6 +42,8 @@ export default function AddressInput(props: AddressInputProps) { // keep track of focus and toggle fullscreen display on small screens const [hasFocus, setHasFocus] = useState(false) const isSmallScreen = useMediaQuery({ query: '(max-width: 44rem)' }) + const prevPoint = props.index > 0 ? props.points[props.index - 1] : undefined + const excludeCoord = prevPoint?.isInitialized ? prevPoint.coordinate : undefined // container for geocoding results which gets set by the geocoder class and set to empty if the underlying query // point gets changed from outside also gets filled with an item to select the current location as input if input @@ -211,14 +213,14 @@ export default function AddressInput(props: AddressInputProps) { setText(query) if (query === '') { geocoder.cancel() - const recents = buildRecentItems(undefined, 5) + const recents = buildRecentItems(undefined, 5, excludeCoord) if (recents.length > 0) setAutocompleteItems(recents) else setAutocompleteItems([]) } else { const coordinate = textToCoordinate(query) if (!coordinate) { if (query.length < 2) { - const recents = buildRecentItems(query, 5) + const recents = buildRecentItems(query, 5, excludeCoord) if (recents.length > 0) setAutocompleteItems(recents) } geocoder.request(query, biasCoord, getMap().getView().getZoom()) @@ -231,7 +233,7 @@ export default function AddressInput(props: AddressInputProps) { setHasFocus(true) props.clearDragDrop() if (text === '') { - const recents = buildRecentItems(undefined, 5) + const recents = buildRecentItems(undefined, 5, excludeCoord) if (recents.length > 0) setAutocompleteItems(recents) } }} @@ -256,7 +258,7 @@ export default function AddressInput(props: AddressInputProps) { onClick={e => { setText('') props.onChange('') - const recents = buildRecentItems(undefined, 5) + const recents = buildRecentItems(undefined, 5, excludeCoord) if (recents.length > 0) setAutocompleteItems(recents) else setAutocompleteItems([]) // if we clear the text without focus then explicitly request it to improve usability: @@ -315,8 +317,10 @@ export default function AddressInput(props: AddressInputProps) { ) } -function buildRecentItems(filter?: string, limit?: number): RecentLocationItem[] { - let recents = getRecentLocations(1) +function buildRecentItems(filter?: string, limit?: number, excludeCoord?: Coordinate): RecentLocationItem[] { + let recents = getRecentLocations(0) + if (excludeCoord) + recents = recents.filter(e => calcDist({ lat: e.lat, lng: e.lng }, excludeCoord) > 0) if (filter) { const lower = filter.toLowerCase() recents = recents.filter( From e2bb29e9c1586446e0e758c453ee147320a7ae1f Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 28 Mar 2026 13:34:36 +0100 Subject: [PATCH 11/18] make recent list disappear even if we click into an (empty) input that shows a new recent list --- src/sidebar/search/AddressInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index f6820b33..4e56aaef 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -284,7 +284,7 @@ export default function AddressInput(props: AddressInputProps) { - {autocompleteItems.length > 0 && ( + {hasFocus && autocompleteItems.length > 0 && ( Date: Sat, 28 Mar 2026 13:58:22 +0100 Subject: [PATCH 12/18] fix like we did for normal entries also needed for recent (c7629bd) --- src/sidebar/search/AddressInput.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 83b73388..43988893 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -177,6 +177,7 @@ export default function AddressInput(props: AddressInputProps) { // do not focus on mobile as we would hide the map with the "input"-view const focusFirstInput = props.index == 0 && !isSmallScreen + const isInitialFocus = useRef(focusFirstInput) return (
      @@ -232,7 +233,9 @@ export default function AddressInput(props: AddressInputProps) { onFocus={() => { setHasFocus(true) props.clearDragDrop() - if (text === '') { + if (isInitialFocus.current) { + isInitialFocus.current = false + } else if (text === '') { const recents = buildRecentItems(undefined, 5, excludeCoord) if (recents.length > 0) setAutocompleteItems(recents) } @@ -299,6 +302,7 @@ export default function AddressInput(props: AddressInputProps) { props.onAddressSelected(item.toText(), item.point) saveRecentLocation(item.mainText, item.secondText, item.point) } else if (item instanceof RecentLocationItem) { + setText(item.toText()) props.onAddressSelected(item.toText(), item.point) } else if (item instanceof POIQueryItem) { handlePoiSearch(poiSearch, item.result, props.map) From f6c99413988350d558daa131dc96d49737f8bd75 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 28 Mar 2026 14:17:21 +0100 Subject: [PATCH 13/18] populate recents via useEffect so excludeCoord is fresh after re-render Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sidebar/search/AddressInput.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 48e1d602..f9420c11 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -79,6 +79,14 @@ export default function AddressInput(props: AddressInputProps) { if (props.point.isInitialized) setAutocompleteItems([]) }, [props.point]) + useEffect(() => { + if (hasFocus && !isInitialFocus.current && text === '') { + const recents = buildRecentItems(undefined, 5, excludeCoord) + if (recents.length > 0) setAutocompleteItems(recents) + } + isInitialFocus.current = false + }, [hasFocus, excludeCoord]) + // highlighted result of geocoding results. Keep track which index is highlighted and change things on ArrowUp and Down // on Enter select highlighted result or the 0th if nothing is highlighted const [highlightedResult, setHighlightedResult] = useState(-1) @@ -234,12 +242,6 @@ export default function AddressInput(props: AddressInputProps) { onFocus={() => { setHasFocus(true) props.clearDragDrop() - if (isInitialFocus.current) { - isInitialFocus.current = false - } else if (text === '') { - const recents = buildRecentItems(undefined, 5, excludeCoord) - if (recents.length > 0) setAutocompleteItems(recents) - } }} onBlur={() => { setHasFocus(false) From 2ff79e2ee5b9c00374370db1d3f940fb8212cf91 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 28 Mar 2026 23:21:30 +0100 Subject: [PATCH 14/18] use clock on left and settings --- package-lock.json | 21 +--- src/sidebar/SettingsBox.module.css | 23 +++++ src/sidebar/SettingsBox.tsx | 9 ++ src/sidebar/search/AddressInput.tsx | 6 +- .../AddressInputAutocomplete.module.css | 38 ++----- .../search/AddressInputAutocomplete.tsx | 55 ++++------- src/translation/tr.json | 98 +++++++++++++++++++ 7 files changed, 159 insertions(+), 91 deletions(-) diff --git a/package-lock.json b/package-lock.json index 453766ef..551ebdc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,7 +95,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2043,7 +2042,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2067,7 +2065,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3416,7 +3413,6 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3813,7 +3809,6 @@ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4457,7 +4452,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4494,7 +4488,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4927,7 +4920,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6478,7 +6470,6 @@ "integrity": "sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "escape-html": "^1.0.3", "sharp": "^0.33.1", @@ -7697,7 +7688,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -8346,7 +8336,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -9031,7 +9020,6 @@ "resolved": "https://registry.npmjs.org/ol/-/ol-10.6.1.tgz", "integrity": "sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@types/rbush": "4.0.0", "earcut": "^3.0.0", @@ -9473,7 +9461,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9797,7 +9784,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11329,7 +11315,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11564,8 +11549,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/type-detect": { "version": "4.0.8", @@ -11610,7 +11594,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11888,7 +11871,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -11938,7 +11920,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", diff --git a/src/sidebar/SettingsBox.module.css b/src/sidebar/SettingsBox.module.css index ddedbe7a..e06fb241 100644 --- a/src/sidebar/SettingsBox.module.css +++ b/src/sidebar/SettingsBox.module.css @@ -88,6 +88,29 @@ input { font-weight: bold; } +.clearRecentsButton { + border: 1px solid lightgray; + border-radius: 8px; + background: none; + color: inherit; + padding: 3px 5px; + cursor: pointer; + justify-self: center; + display: flex; + align-items: center; + justify-content: center; +} + +.clearRecentsButton svg { + scale: 1; + margin: 0; +} + +.clearRecentsButton:hover { + background: #ececec; + color: #333; +} + .title, .infoLine, .settingsTable, diff --git a/src/sidebar/SettingsBox.tsx b/src/sidebar/SettingsBox.tsx index eb36ae57..fa44def4 100644 --- a/src/sidebar/SettingsBox.tsx +++ b/src/sidebar/SettingsBox.tsx @@ -10,6 +10,7 @@ import { SettingsContext } from '@/contexts/SettingsContext' import { RoutingProfile } from '@/api/graphhopper' import * as config from 'config' import { ProfileGroupMap } from '@/utils' +import { clearRecentLocations } from '@/sidebar/search/RecentLocations' export default function SettingsBox({ profile }: { profile: RoutingProfile }) { const settings = useContext(SettingsContext) @@ -51,6 +52,14 @@ export default function SettingsBox({ profile }: { profile: RoutingProfile }) { Dispatcher.dispatch(new UpdateSettings({ showDistanceInMiles: !settings.showDistanceInMiles })) } /> +
      + +
      {tr('clear_recent_locations')}
      +
      {tr('settings_gpx_export')}
      diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index f9420c11..b0f294cc 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -2,7 +2,7 @@ import { JSX, ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { QueryPoint, QueryPointType } from '@/stores/QueryStore' import { Bbox, GeocodingHit, ReverseGeocodingHit } from '@/api/graphhopper' import Autocomplete, { AutocompleteItem, GeocodingItem, POIQueryItem, RecentLocationItem } from '@/sidebar/search/AddressInputAutocomplete' -import { clearRecentLocations, getRecentLocations, saveRecentLocation } from '@/sidebar/search/RecentLocations' +import { getRecentLocations, saveRecentLocation } from '@/sidebar/search/RecentLocations' import ArrowBack from './arrow_back.svg' import Cross from '@/sidebar/times-solid-thin.svg' import CurrentLocationIcon from './current-location.svg' @@ -313,10 +313,6 @@ export default function AddressInput(props: AddressInputProps) { } focusNextOrBlur() }} - onClearRecents={() => { - clearRecentLocations() - setAutocompleteItems([]) - }} /> )} diff --git a/src/sidebar/search/AddressInputAutocomplete.module.css b/src/sidebar/search/AddressInputAutocomplete.module.css index 019b3d0a..8fd0577f 100644 --- a/src/sidebar/search/AddressInputAutocomplete.module.css +++ b/src/sidebar/search/AddressInputAutocomplete.module.css @@ -62,40 +62,22 @@ color: #5b616a; } -.recentLocationEntry { +.recentEntry { display: flex; - flex-direction: column; + align-items: center; text-align: start; - margin: 0.2rem 0.5rem; + margin: 0.4rem 0.5rem; } -.recentHeader { +.recentEntryText { display: flex; - justify-content: space-between; - align-items: center; - padding: 0.1rem 0.5rem 0 0.5rem; - margin-bottom: -0.3rem; - padding-right: 0.1rem; -} - -.recentHeaderText { - font-size: small; - color: #999; + flex-direction: column; + flex: 1; + min-width: 0; } -.clearRecentsButton { - border: none; - background: none; +.recentIcon { + flex-shrink: 0; color: #5b616a; - cursor: pointer; - padding: 0.3rem; - min-width: 1.5rem; - min-height: 1.5rem; - display: flex; - align-items: center; - justify-content: center; -} - -.clearRecentsButton:hover { - color: #333; + margin-right: 0.5rem; } diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index 36d267a8..0f9a0087 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -50,45 +50,16 @@ export interface AutocompleteProps { items: AutocompleteItem[] highlightedItem: AutocompleteItem onSelect: (hit: AutocompleteItem) => void - onClearRecents?: () => void } -export default function Autocomplete({ items, highlightedItem, onSelect, onClearRecents }: AutocompleteProps) { - let recentHeaderShown = false +export default function Autocomplete({ items, highlightedItem, onSelect }: AutocompleteProps) { return (
        - {items.map((item, i) => { - let header = null - if (item instanceof RecentLocationItem && !recentHeaderShown) { - recentHeaderShown = true - header = ( -
        - Recent - {onClearRecents && ( - - )} -
        - ) - } - return ( -
      • - {header} - {mapToComponent(item, highlightedItem === item, onSelect)} -
      • - ) - })} + {items.map((item, i) => ( +
      • + {mapToComponent(item, highlightedItem === item, onSelect)} +
      • + ))}
      ) } @@ -153,9 +124,17 @@ function RecentLocationEntry({ }) { return ( onSelect(item)}> -
      - {item.mainText} - {item.secondText} +
      + + + +
      + {item.mainText} + {item.secondText} +
      ) diff --git a/src/translation/tr.json b/src/translation/tr.json index d905530e..b57ed532 100644 --- a/src/translation/tr.json +++ b/src/translation/tr.json @@ -131,6 +131,8 @@ "back":"Back", "as_start":"As start", "as_destination":"As destination", +"clear_button":"Clear", +"clear_recent_locations":"Recent Locations", "route_stats_bike_network":"Bike Network", "route_stats_foot_network":"Foot Network", "route_stats_incline":"Incline", @@ -362,6 +364,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -593,6 +597,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -824,6 +830,8 @@ "back":"Geri", "as_start":"Başlanğıc nöqtəsi kimi", "as_destination":"Təyinat nöqtəsi kimi", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1055,6 +1063,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1286,6 +1296,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1517,6 +1529,8 @@ "back":"Enrera", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1748,6 +1762,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1979,6 +1995,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -2210,6 +2228,8 @@ "back":"Zurück", "as_start":"Als Start", "as_destination":"Als Ziel", +"clear_button":"Löschen", +"clear_recent_locations":"Letzte Orte", "route_stats_bike_network":"Radnetz", "route_stats_foot_network":"Wanderwege", "route_stats_incline":"Anstieg", @@ -2441,6 +2461,8 @@ "back":"Πίσω", "as_start":"Ως αφετηρία", "as_destination":"Ως προορισμό", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -2672,6 +2694,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -2903,6 +2927,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -3134,6 +3160,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -3365,6 +3393,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -3596,6 +3626,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -3827,6 +3859,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4058,6 +4092,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4289,6 +4325,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4520,6 +4558,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4751,6 +4791,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4982,6 +5024,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -5213,6 +5257,8 @@ "back":"Vissza", "as_start":"Kiindulási pontnak", "as_destination":"Célpontnak", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"Kerékpárút-hálózat", "route_stats_foot_network":"Turistaút-hálózat", "route_stats_incline":"Lejtés", @@ -5444,6 +5490,8 @@ "back":"Sebelumnya", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -5675,6 +5723,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -5906,6 +5956,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -6137,6 +6189,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -6368,6 +6422,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -6599,6 +6655,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -6830,6 +6888,8 @@ "back":"Буцах", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7061,6 +7121,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7292,6 +7354,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7523,6 +7587,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7754,6 +7820,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7985,6 +8053,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8216,6 +8286,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8447,6 +8519,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8678,6 +8752,8 @@ "back":"Назад", "as_start":"Как пункт отправления", "as_destination":"Как пункт назначения", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8909,6 +8985,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -9140,6 +9218,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -9371,6 +9451,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -9602,6 +9684,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -9833,6 +9917,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10064,6 +10150,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10295,6 +10383,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10526,6 +10616,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10757,6 +10849,8 @@ "back":"返回", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10988,6 +11082,8 @@ "back":"返回", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -11219,6 +11315,8 @@ "back":"", "as_start":"", "as_destination":"", +"clear_button":"", +"clear_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", From 563ed54f6a9aa94de224466ce6d4436ba8794431 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 28 Mar 2026 23:30:22 +0100 Subject: [PATCH 15/18] use switch for save and clear if off --- src/sidebar/SettingsBox.module.css | 23 ---- src/sidebar/SettingsBox.tsx | 16 +-- src/sidebar/search/RecentLocations.ts | 2 + src/stores/SettingsStore.ts | 2 + src/translation/tr.json | 147 +++++++++----------------- 5 files changed, 61 insertions(+), 129 deletions(-) diff --git a/src/sidebar/SettingsBox.module.css b/src/sidebar/SettingsBox.module.css index e06fb241..ddedbe7a 100644 --- a/src/sidebar/SettingsBox.module.css +++ b/src/sidebar/SettingsBox.module.css @@ -88,29 +88,6 @@ input { font-weight: bold; } -.clearRecentsButton { - border: 1px solid lightgray; - border-radius: 8px; - background: none; - color: inherit; - padding: 3px 5px; - cursor: pointer; - justify-self: center; - display: flex; - align-items: center; - justify-content: center; -} - -.clearRecentsButton svg { - scale: 1; - margin: 0; -} - -.clearRecentsButton:hover { - background: #ececec; - color: #333; -} - .title, .infoLine, .settingsTable, diff --git a/src/sidebar/SettingsBox.tsx b/src/sidebar/SettingsBox.tsx index fa44def4..d5305a2d 100644 --- a/src/sidebar/SettingsBox.tsx +++ b/src/sidebar/SettingsBox.tsx @@ -52,14 +52,14 @@ export default function SettingsBox({ profile }: { profile: RoutingProfile }) { Dispatcher.dispatch(new UpdateSettings({ showDistanceInMiles: !settings.showDistanceInMiles })) } /> -
      - -
      {tr('clear_recent_locations')}
      -
      + { + if (settings.saveRecentLocations) clearRecentLocations() + Dispatcher.dispatch(new UpdateSettings({ saveRecentLocations: !settings.saveRecentLocations })) + }} + />
      {tr('settings_gpx_export')}
      diff --git a/src/sidebar/search/RecentLocations.ts b/src/sidebar/search/RecentLocations.ts index 77167522..ef47a86c 100644 --- a/src/sidebar/search/RecentLocations.ts +++ b/src/sidebar/search/RecentLocations.ts @@ -1,6 +1,7 @@ import { calcDist, Coordinate } from '@/utils' import { tr } from '@/translation/Translation' import { textToCoordinate } from '@/Converters' +import { getSettingsStore } from '@/stores/Stores' const STORAGE_KEY = 'recentLocations' export const MAX_ENTRIES = 15 @@ -47,6 +48,7 @@ export function clearRecentLocations(): void { } export function saveRecentLocation(mainText: string, secondText: string, coordinate: Coordinate, now: number = Date.now()): void { + if (!getSettingsStore().state.saveRecentLocations) return if (mainText === tr('current_location')) return if (textToCoordinate(mainText)) return diff --git a/src/stores/SettingsStore.ts b/src/stores/SettingsStore.ts index 406c0bb9..84e88e4c 100644 --- a/src/stores/SettingsStore.ts +++ b/src/stores/SettingsStore.ts @@ -5,6 +5,7 @@ import { SetCustomModelEnabled, UpdateSettings } from '@/actions/Actions' export interface Settings { showDistanceInMiles: boolean drawAreasEnabled: boolean + saveRecentLocations: boolean gpxExportRte: boolean gpxExportWpt: boolean gpxExportTrk: boolean @@ -13,6 +14,7 @@ export interface Settings { export const defaultSettings: Settings = { showDistanceInMiles: false, drawAreasEnabled: false, + saveRecentLocations: true, gpxExportRte: false, gpxExportWpt: false, gpxExportTrk: true, diff --git a/src/translation/tr.json b/src/translation/tr.json index b57ed532..e69fdea1 100644 --- a/src/translation/tr.json +++ b/src/translation/tr.json @@ -131,8 +131,7 @@ "back":"Back", "as_start":"As start", "as_destination":"As destination", -"clear_button":"Clear", -"clear_recent_locations":"Recent Locations", +"save_recent_locations":"Save recent locations", "route_stats_bike_network":"Bike Network", "route_stats_foot_network":"Foot Network", "route_stats_incline":"Incline", @@ -364,8 +363,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -597,8 +595,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -830,8 +827,7 @@ "back":"Geri", "as_start":"Başlanğıc nöqtəsi kimi", "as_destination":"Təyinat nöqtəsi kimi", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1063,8 +1059,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1296,8 +1291,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1529,8 +1523,7 @@ "back":"Enrera", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1762,8 +1755,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -1995,8 +1987,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -2228,8 +2219,7 @@ "back":"Zurück", "as_start":"Als Start", "as_destination":"Als Ziel", -"clear_button":"Löschen", -"clear_recent_locations":"Letzte Orte", +"save_recent_locations":"Letzte Orte speichern", "route_stats_bike_network":"Radnetz", "route_stats_foot_network":"Wanderwege", "route_stats_incline":"Anstieg", @@ -2461,8 +2451,7 @@ "back":"Πίσω", "as_start":"Ως αφετηρία", "as_destination":"Ως προορισμό", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -2694,8 +2683,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -2927,8 +2915,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -3160,8 +3147,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -3393,8 +3379,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -3626,8 +3611,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -3859,8 +3843,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4092,8 +4075,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4325,8 +4307,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4558,8 +4539,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -4791,8 +4771,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -5024,8 +5003,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -5257,8 +5235,7 @@ "back":"Vissza", "as_start":"Kiindulási pontnak", "as_destination":"Célpontnak", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"Kerékpárút-hálózat", "route_stats_foot_network":"Turistaút-hálózat", "route_stats_incline":"Lejtés", @@ -5490,8 +5467,7 @@ "back":"Sebelumnya", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -5723,8 +5699,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -5956,8 +5931,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -6189,8 +6163,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -6422,8 +6395,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -6655,8 +6627,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -6888,8 +6859,7 @@ "back":"Буцах", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7121,8 +7091,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7354,8 +7323,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7587,8 +7555,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -7820,8 +7787,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8053,8 +8019,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8286,8 +8251,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8519,8 +8483,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8752,8 +8715,7 @@ "back":"Назад", "as_start":"Как пункт отправления", "as_destination":"Как пункт назначения", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -8985,8 +8947,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -9218,8 +9179,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -9451,8 +9411,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -9684,8 +9643,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -9917,8 +9875,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10150,8 +10107,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10383,8 +10339,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10616,8 +10571,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -10849,8 +10803,7 @@ "back":"返回", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -11082,8 +11035,7 @@ "back":"返回", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", @@ -11315,8 +11267,7 @@ "back":"", "as_start":"", "as_destination":"", -"clear_button":"", -"clear_recent_locations":"", +"save_recent_locations":"", "route_stats_bike_network":"", "route_stats_foot_network":"", "route_stats_incline":"", From c1fbb3bbcfbf9bab59e6a6463a3a8be401669f5c Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 28 Mar 2026 23:37:31 +0100 Subject: [PATCH 16/18] cleanup --- src/sidebar/search/AddressInput.tsx | 30 ++++++++++++------- .../search/AddressInputAutocomplete.tsx | 8 ++++- src/sidebar/search/RecentLocations.ts | 9 ++++-- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index b0f294cc..3edac99c 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -1,8 +1,14 @@ -import { JSX, ReactNode, useCallback, useEffect, useRef, useState } from 'react' +import { JSX, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react' import { QueryPoint, QueryPointType } from '@/stores/QueryStore' import { Bbox, GeocodingHit, ReverseGeocodingHit } from '@/api/graphhopper' -import Autocomplete, { AutocompleteItem, GeocodingItem, POIQueryItem, RecentLocationItem } from '@/sidebar/search/AddressInputAutocomplete' +import Autocomplete, { + AutocompleteItem, + GeocodingItem, + POIQueryItem, + RecentLocationItem, +} from '@/sidebar/search/AddressInputAutocomplete' import { getRecentLocations, saveRecentLocation } from '@/sidebar/search/RecentLocations' +import { SettingsContext } from '@/contexts/SettingsContext' import ArrowBack from './arrow_back.svg' import Cross from '@/sidebar/times-solid-thin.svg' import CurrentLocationIcon from './current-location.svg' @@ -34,6 +40,7 @@ export interface AddressInputProps { } export default function AddressInput(props: AddressInputProps) { + const saveRecent = useContext(SettingsContext).saveRecentLocations const [origText, setOrigText] = useState(props.point.queryText) // controlled component pattern with initial value set from props const [text, setText] = useState(props.point.queryText) @@ -118,7 +125,8 @@ export default function AddressInput(props: AddressInputProps) { setText(origText) } else if (nextIndex >= 0) { const item = autocompleteItems[nextIndex] - if (item instanceof GeocodingItem || item instanceof RecentLocationItem) setText(item.mainText) + if (item instanceof GeocodingItem || item instanceof RecentLocationItem) + setText(item.mainText) else setText(origText) } } @@ -150,15 +158,15 @@ export default function AddressInput(props: AddressInputProps) { const hit: GeocodingHit = result.hits[0] const res = nominatimHitToItem(hit) props.onAddressSelected(res.mainText + ', ' + res.secondText, hit.point) - saveRecentLocation(res.mainText, res.secondText, hit.point) + if (saveRecent) saveRecentLocation(res.mainText, res.secondText, hit.point) } else if (item instanceof GeocodingItem) { props.onAddressSelected(item.toText(), item.point) - saveRecentLocation(item.mainText, item.secondText, item.point) + if (saveRecent) saveRecentLocation(item.mainText, item.secondText, item.point) } }) } else if (item instanceof GeocodingItem) { props.onAddressSelected(item.toText(), item.point) - saveRecentLocation(item.mainText, item.secondText, item.point) + if (saveRecent) saveRecentLocation(item.mainText, item.secondText, item.point) } } if (event.key === 'Enter') focusNextOrBlur() @@ -303,7 +311,7 @@ export default function AddressInput(props: AddressInputProps) { if (item instanceof GeocodingItem) { setText(item.toText()) props.onAddressSelected(item.toText(), item.point) - saveRecentLocation(item.mainText, item.secondText, item.point) + if (saveRecent) saveRecentLocation(item.mainText, item.secondText, item.point) } else if (item instanceof RecentLocationItem) { setText(item.toText()) props.onAddressSelected(item.toText(), item.point) @@ -323,14 +331,16 @@ export default function AddressInput(props: AddressInputProps) { function buildRecentItems(filter?: string, limit?: number, excludeCoord?: Coordinate): RecentLocationItem[] { let recents = getRecentLocations(0) - if (excludeCoord) - recents = recents.filter(e => calcDist({ lat: e.lat, lng: e.lng }, excludeCoord) > 0) + if (excludeCoord) recents = recents.filter(e => calcDist({ lat: e.lat, lng: e.lng }, excludeCoord) > 0) if (filter) { const lower = filter.toLowerCase() recents = recents.filter( e => e.mainText.toLowerCase().startsWith(lower) || - e.secondText.toLowerCase().split(/[\s,]+/).some(word => word.startsWith(lower)), + e.secondText + .toLowerCase() + .split(/[\s,]+/) + .some(word => word.startsWith(lower)), ) } if (limit) recents = recents.slice(0, limit) diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index 0f9a0087..21f3d6ff 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -125,7 +125,13 @@ function RecentLocationEntry({ return ( onSelect(item)}>
      - + Date: Sat, 28 Mar 2026 23:54:32 +0100 Subject: [PATCH 17/18] combine entry selection with save --- src/sidebar/search/AddressInput.tsx | 30 +++++++++++---------------- src/sidebar/search/RecentLocations.ts | 2 +- src/sidebar/search/Search.tsx | 9 +++++++- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 3edac99c..de8969e8 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -1,4 +1,4 @@ -import { JSX, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react' +import { JSX, ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { QueryPoint, QueryPointType } from '@/stores/QueryStore' import { Bbox, GeocodingHit, ReverseGeocodingHit } from '@/api/graphhopper' import Autocomplete, { @@ -7,8 +7,7 @@ import Autocomplete, { POIQueryItem, RecentLocationItem, } from '@/sidebar/search/AddressInputAutocomplete' -import { getRecentLocations, saveRecentLocation } from '@/sidebar/search/RecentLocations' -import { SettingsContext } from '@/contexts/SettingsContext' +import { getRecentLocations } from '@/sidebar/search/RecentLocations' import ArrowBack from './arrow_back.svg' import Cross from '@/sidebar/times-solid-thin.svg' import CurrentLocationIcon from './current-location.svg' @@ -30,7 +29,7 @@ export interface AddressInputProps { point: QueryPoint points: QueryPoint[] onCancel: () => void - onAddressSelected: (queryText: string, coord: Coordinate | undefined) => void + onLocationSelected: (mainText: string, secondText: string | undefined, coord: Coordinate | undefined) => void onChange: (value: string) => void clearDragDrop: () => void moveStartIndex: number @@ -40,7 +39,6 @@ export interface AddressInputProps { } export default function AddressInput(props: AddressInputProps) { - const saveRecent = useContext(SettingsContext).saveRecentLocations const [origText, setOrigText] = useState(props.point.queryText) // controlled component pattern with initial value set from props const [text, setText] = useState(props.point.queryText) @@ -140,15 +138,15 @@ export default function AddressInput(props: AddressInputProps) { // try to parse input as coordinate. Otherwise query nominatim const coordinate = textToCoordinate(text) if (coordinate) { - props.onAddressSelected(text, coordinate) + props.onLocationSelected(text, undefined, coordinate) } else if (autocompleteItems.length > 0) { const index = highlightedResult >= 0 ? highlightedResult : 0 const item = autocompleteItems[index] if (item instanceof POIQueryItem) { handlePoiSearch(poiSearch, item.result, props.map) - props.onAddressSelected(item.result.text(item.result.poi), undefined) + props.onLocationSelected(item.result.text(item.result.poi), undefined, undefined) } else if (item instanceof RecentLocationItem) { - props.onAddressSelected(item.toText(), item.point) + props.onLocationSelected(item.mainText, item.secondText, item.point) } else if (highlightedResult < 0 && !props.point.isInitialized) { // by default use the first result, otherwise the highlighted one getApi() @@ -157,16 +155,13 @@ export default function AddressInput(props: AddressInputProps) { if (result && result.hits.length > 0) { const hit: GeocodingHit = result.hits[0] const res = nominatimHitToItem(hit) - props.onAddressSelected(res.mainText + ', ' + res.secondText, hit.point) - if (saveRecent) saveRecentLocation(res.mainText, res.secondText, hit.point) + props.onLocationSelected(res.mainText, res.secondText, hit.point) } else if (item instanceof GeocodingItem) { - props.onAddressSelected(item.toText(), item.point) - if (saveRecent) saveRecentLocation(item.mainText, item.secondText, item.point) + props.onLocationSelected(item.mainText, item.secondText, item.point) } }) } else if (item instanceof GeocodingItem) { - props.onAddressSelected(item.toText(), item.point) - if (saveRecent) saveRecentLocation(item.mainText, item.secondText, item.point) + props.onLocationSelected(item.mainText, item.secondText, item.point) } } if (event.key === 'Enter') focusNextOrBlur() @@ -290,7 +285,7 @@ export default function AddressInput(props: AddressInputProps) { e => e.preventDefault() // prevents that input->onBlur is called when clicking the button (would hide this button and prevent onClick) } onClick={() => { - onCurrentLocationSelected(props.onAddressSelected) + onCurrentLocationSelected((text, coord) => props.onLocationSelected(text, undefined, coord)) // but when clicked => we want to lose the focus e.g. to close mobile-input view searchInput.current!.blur() }} @@ -310,11 +305,10 @@ export default function AddressInput(props: AddressInputProps) { onSelect={item => { if (item instanceof GeocodingItem) { setText(item.toText()) - props.onAddressSelected(item.toText(), item.point) - if (saveRecent) saveRecentLocation(item.mainText, item.secondText, item.point) + props.onLocationSelected(item.mainText, item.secondText, item.point) } else if (item instanceof RecentLocationItem) { setText(item.toText()) - props.onAddressSelected(item.toText(), item.point) + props.onLocationSelected(item.mainText, item.secondText, item.point) } else if (item instanceof POIQueryItem) { handlePoiSearch(poiSearch, item.result, props.map) setText(item.result.text(item.result.poi)) diff --git a/src/sidebar/search/RecentLocations.ts b/src/sidebar/search/RecentLocations.ts index da504132..8b9a79f1 100644 --- a/src/sidebar/search/RecentLocations.ts +++ b/src/sidebar/search/RecentLocations.ts @@ -4,7 +4,7 @@ import { textToCoordinate } from '@/Converters' const STORAGE_KEY = 'recentLocations' export const MAX_ENTRIES = 15 -const DEDUP_DISTANCE_METERS = 100 +const DEDUP_DISTANCE_METERS = 5 export interface RecentLocation { mainText: string diff --git a/src/sidebar/search/Search.tsx b/src/sidebar/search/Search.tsx index 40d1fa12..72a0318e 100644 --- a/src/sidebar/search/Search.tsx +++ b/src/sidebar/search/Search.tsx @@ -24,6 +24,9 @@ import { tr } from '@/translation/Translation' import SettingsBox from '@/sidebar/SettingsBox' import { RoutingProfile } from '@/api/graphhopper' import { getBBoxFromCoord } from '@/utils' +import { saveRecentLocation } from '@/sidebar/search/RecentLocations' +import { useContext } from 'react' +import { SettingsContext } from '@/contexts/SettingsContext' export default function Search({ points, profile, map }: { points: QueryPoint[]; profile: RoutingProfile; map: Map }) { const [showSettings, setShowSettings] = useState(false) @@ -103,6 +106,7 @@ const SearchBox = ({ map: Map }) => { const point = points[index] + const saveRecent = useContext(SettingsContext).saveRecentLocations function onClickOrDrop() { onDropPreviewSelect(-1) @@ -176,7 +180,10 @@ const SearchBox = ({ point={point} points={points} onCancel={() => console.log('cancel')} - onAddressSelected={(queryText, coordinate) => { + onLocationSelected={(mainText, secondText, coordinate) => { + const queryText = secondText ? mainText + ', ' + secondText : mainText + if (secondText && coordinate && saveRecent) saveRecentLocation(mainText, secondText, coordinate) + const initCount = points.filter(p => p.isInitialized).length if (coordinate && initCount != points.length) Dispatcher.dispatch(new SetBBox(getBBoxFromCoord(coordinate))) From ae0a8ba15843661b1a9ab70c03faa93dd3eac99b Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 29 Mar 2026 18:55:32 +0200 Subject: [PATCH 18/18] again minor bug necessary: don't show list for first focus (ie. initial load) --- src/sidebar/search/AddressInput.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index de8969e8..dc38e29c 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -85,11 +85,15 @@ export default function AddressInput(props: AddressInputProps) { }, [props.point]) useEffect(() => { - if (hasFocus && !isInitialFocus.current && text === '') { + if (!hasFocus) return + if (isInitialFocus.current) { + isInitialFocus.current = false + return + } + if (text === '') { const recents = buildRecentItems(undefined, 5, excludeCoord) if (recents.length > 0) setAutocompleteItems(recents) } - isInitialFocus.current = false }, [hasFocus, excludeCoord]) // highlighted result of geocoding results. Keep track which index is highlighted and change things on ArrowUp and Down