aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/js/searchbox.js
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
commit3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch)
tree4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/js/searchbox.js
downloadworldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz
worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/js/searchbox.js')
-rw-r--r--viewer/js/searchbox.js59
1 files changed, 59 insertions, 0 deletions
diff --git a/viewer/js/searchbox.js b/viewer/js/searchbox.js
new file mode 100644
index 0000000..c073b65
--- /dev/null
+++ b/viewer/js/searchbox.js
@@ -0,0 +1,59 @@
+// searchbox.js — the search field: suggestions as you type, ↑/↓/Enter/Esc, fly to the choice
+import { el } from './ui.js';
+import { suggest } from './search.js';
+
+export class SearchBox {
+ constructor(input, list, { pins, locate, go }) {
+ Object.assign(this, { input, list, pins, locate, go });
+ this.items = [];
+ this.sel = -1;
+ input.addEventListener('input', () => this.update());
+ input.addEventListener('keydown', e => this.key(e));
+ input.addEventListener('blur', () => setTimeout(() => this.close(), 150)); // let a click on an item land first
+ }
+
+ update() {
+ this.items = suggest(this.input.value, this.pins());
+ this.sel = this.items.length ? 0 : -1;
+ this.render();
+ }
+
+ render() {
+ this.list.hidden = !this.items.length;
+ this.list.replaceChildren(...this.items.map((it, i) => el('div',
+ { class: `item${i === this.sel ? ' sel' : ''}`, onmousedown: e => { e.preventDefault(); this.choose(i); } },
+ it.label, it.sub ? el('span', { class: 'sub' }, it.sub) : null)));
+ }
+
+ key(e) {
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
+ e.preventDefault();
+ if (!this.items.length) return;
+ this.sel = (this.sel + (e.key === 'ArrowDown' ? 1 : -1) + this.items.length) % this.items.length;
+ this.render();
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ if (this.sel >= 0) this.choose(this.sel);
+ } else if (e.key === 'Escape') {
+ this.close();
+ this.input.blur();
+ }
+ }
+
+ close() {
+ this.items = [];
+ this.sel = -1;
+ this.render();
+ }
+
+ async choose(i) {
+ const it = this.items[i];
+ this.close();
+ try {
+ const at = it.kind === 'cell' ? await this.locate(it.cell) : { lat: it.lat, lon: it.lon };
+ this.go({ lat: at.lat, lon: at.lon, label: it.label, marker: it.kind !== 'pin' });
+ } catch (e) {
+ this.go({ error: e.message });
+ }
+ }
+}