@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +506 -2
- package/dist/core/basemap.svelte.d.ts +38 -0
- package/dist/core/basemap.svelte.js +163 -0
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.js +13 -0
- package/dist/core/create-map.svelte.d.ts +207 -0
- package/dist/core/create-map.svelte.js +376 -0
- package/dist/core/gl.d.ts +92 -0
- package/dist/core/gl.js +1 -0
- package/dist/core/interactions.svelte.d.ts +43 -0
- package/dist/core/interactions.svelte.js +230 -0
- package/dist/core/plugin-host.svelte.d.ts +46 -0
- package/dist/core/plugin-host.svelte.js +366 -0
- package/dist/core/plugin.d.ts +155 -0
- package/dist/core/plugin.js +1 -0
- package/dist/core/scoped-context.d.ts +7 -0
- package/dist/core/scoped-context.js +34 -0
- package/dist/data/format.d.ts +10 -0
- package/dist/data/format.js +24 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.js +9 -0
- package/dist/data/messages/en.json +52 -0
- package/dist/data/messages.d.ts +10 -0
- package/dist/data/messages.js +17 -0
- package/dist/data/metadata.d.ts +22 -0
- package/dist/data/metadata.js +110 -0
- package/dist/data/refs.d.ts +18 -0
- package/dist/data/refs.js +61 -0
- package/dist/data/scale-types.d.ts +7 -0
- package/dist/data/scale-types.js +1 -0
- package/dist/data/scale.d.ts +13 -0
- package/dist/data/scale.js +27 -0
- package/dist/data/selection.d.ts +7 -0
- package/dist/data/selection.js +31 -0
- package/dist/data/static-source.d.ts +4 -0
- package/dist/data/static-source.js +3 -0
- package/dist/data/types.d.ts +69 -0
- package/dist/data/types.js +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +41 -0
- package/dist/plugins/icons.d.ts +21 -0
- package/dist/plugins/icons.js +73 -0
- package/dist/plugins/points-clusters.d.ts +60 -0
- package/dist/plugins/points-clusters.js +93 -0
- package/dist/plugins/points.svelte.d.ts +100 -0
- package/dist/plugins/points.svelte.js +501 -0
- package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
- package/dist/plugins/scale-resolution.svelte.js +111 -0
- package/dist/plugins/shapes.svelte.d.ts +29 -0
- package/dist/plugins/shapes.svelte.js +307 -0
- package/dist/plugins/shared.d.ts +6 -0
- package/dist/plugins/shared.js +18 -0
- package/dist/plugins/style.d.ts +57 -0
- package/dist/plugins/style.js +193 -0
- package/dist/presets/CollocationTooltip.svelte +132 -0
- package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
- package/dist/presets/MonitorClusterTooltip.svelte +149 -0
- package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
- package/dist/presets/MonitorReadingBox.svelte +58 -0
- package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
- package/dist/presets/MonitorTooltip.svelte +88 -0
- package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
- package/dist/presets/calheatscore.d.ts +24 -0
- package/dist/presets/calheatscore.js +22 -0
- package/dist/presets/collocation-sites.d.ts +18 -0
- package/dist/presets/collocation-sites.js +67 -0
- package/dist/presets/fire-groups.d.ts +26 -0
- package/dist/presets/fire-groups.js +51 -0
- package/dist/presets/forecast-zones.d.ts +25 -0
- package/dist/presets/forecast-zones.js +23 -0
- package/dist/presets/hms-fire.d.ts +26 -0
- package/dist/presets/hms-fire.js +87 -0
- package/dist/presets/hms-smoke.d.ts +19 -0
- package/dist/presets/hms-smoke.js +20 -0
- package/dist/presets/monitors.d.ts +48 -0
- package/dist/presets/monitors.js +243 -0
- package/dist/presets/reactive-props.svelte.d.ts +4 -0
- package/dist/presets/reactive-props.svelte.js +7 -0
- package/dist/presets/regions.d.ts +24 -0
- package/dist/presets/regions.js +22 -0
- package/dist/stores/abort.d.ts +16 -0
- package/dist/stores/abort.js +52 -0
- package/dist/stores/collocation-store.svelte.d.ts +25 -0
- package/dist/stores/collocation-store.svelte.js +72 -0
- package/dist/stores/hms-stores.svelte.d.ts +35 -0
- package/dist/stores/hms-stores.svelte.js +97 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/load-once.svelte.d.ts +25 -0
- package/dist/stores/load-once.svelte.js +93 -0
- package/dist/stores/monitors-store.svelte.d.ts +85 -0
- package/dist/stores/monitors-store.svelte.js +340 -0
- package/dist/theme.css +34 -0
- package/dist/ui/FeatureList.svelte +505 -0
- package/dist/ui/FeatureList.svelte.d.ts +14 -0
- package/dist/ui/Legend.svelte +213 -0
- package/dist/ui/Legend.svelte.d.ts +22 -0
- package/dist/ui/LegendBar.svelte +161 -0
- package/dist/ui/LegendBar.svelte.d.ts +11 -0
- package/dist/ui/MapControls.svelte +135 -0
- package/dist/ui/MapControls.svelte.d.ts +11 -0
- package/dist/ui/MapTooltip.svelte +158 -0
- package/dist/ui/MapTooltip.svelte.d.ts +28 -0
- package/dist/ui/SelectionBar.svelte +123 -0
- package/dist/ui/SelectionBar.svelte.d.ts +10 -0
- package/dist/ui/TooltipItem.svelte +72 -0
- package/dist/ui/TooltipItem.svelte.d.ts +11 -0
- package/dist/ui/ViewSummary.svelte +97 -0
- package/dist/ui/ViewSummary.svelte.d.ts +7 -0
- package/dist/ui/controls.css +45 -0
- package/dist/ui/data-credits.svelte.d.ts +9 -0
- package/dist/ui/data-credits.svelte.js +52 -0
- package/dist/ui/feature-list.d.ts +38 -0
- package/dist/ui/feature-list.js +95 -0
- package/dist/ui/legend-data.d.ts +41 -0
- package/dist/ui/legend-data.js +40 -0
- package/dist/ui/level-chip.d.ts +11 -0
- package/dist/ui/level-chip.js +15 -0
- package/dist/ui/value-format.d.ts +7 -0
- package/dist/ui/value-format.js +13 -0
- package/dist/ui/view-summary.d.ts +11 -0
- package/dist/ui/view-summary.js +33 -0
- package/dist/view/MapView.svelte +373 -0
- package/dist/view/MapView.svelte.d.ts +43 -0
- package/dist/view/create-gl-map.d.ts +34 -0
- package/dist/view/create-gl-map.js +182 -0
- package/dist/view/list-placement.d.ts +21 -0
- package/dist/view/list-placement.js +13 -0
- package/package.json +103 -5
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, tick, untrack } from "svelte";
|
|
3
|
+
import type { Bounds, MapContext } from "../core/create-map.svelte.js";
|
|
4
|
+
import type { FeatureListItem } from "../core/plugin.js";
|
|
5
|
+
import { selectionKeys } from "../data/selection.js";
|
|
6
|
+
import "./controls.css";
|
|
7
|
+
import {
|
|
8
|
+
MENU_EVENT_WINDOW_MS,
|
|
9
|
+
inView,
|
|
10
|
+
optionIdPart,
|
|
11
|
+
rowLabel,
|
|
12
|
+
levelText,
|
|
13
|
+
shownLevel,
|
|
14
|
+
sortItems,
|
|
15
|
+
typeaheadIndex,
|
|
16
|
+
type SortBy
|
|
17
|
+
} from "./feature-list.js";
|
|
18
|
+
import { formatValue as formatNumber } from "./value-format.js";
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
map: MapContext;
|
|
22
|
+
class?: string;
|
|
23
|
+
/** The root element's id, e.g. for a button's `aria-controls`. */
|
|
24
|
+
id?: string;
|
|
25
|
+
/** The sort order; bind it to keep it while the list is unmounted. */
|
|
26
|
+
sortBy?: SortBy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let { map, class: className, id, sortBy = $bindable("name") }: Props = $props();
|
|
30
|
+
|
|
31
|
+
const baseId = $props.id();
|
|
32
|
+
/** Rows rendered above and below the visible ones. */
|
|
33
|
+
const OVERSCAN = 10;
|
|
34
|
+
const TYPEAHEAD_MS = 500;
|
|
35
|
+
|
|
36
|
+
type Row = FeatureListItem & { pluginId: string; key: string };
|
|
37
|
+
|
|
38
|
+
let listbox: HTMLElement | undefined = $state();
|
|
39
|
+
let scroller: HTMLElement | undefined = $state();
|
|
40
|
+
let scrollTop = $state(0);
|
|
41
|
+
let viewportHeight = $state(0);
|
|
42
|
+
// Measured from a hidden row, so the row height stays in CSS (rem-based).
|
|
43
|
+
let rowHeight = $state(0);
|
|
44
|
+
let activeKey = $state<string | null>(null);
|
|
45
|
+
|
|
46
|
+
let typeahead = "";
|
|
47
|
+
let typeaheadTimer: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
// Shift+F10 or the Menu key already sent a menu event to the active row, so the browser's own
|
|
49
|
+
// event (on keydown or keyup, depending on the platform) is dropped.
|
|
50
|
+
let menuSent = false;
|
|
51
|
+
let menuSentTimer: ReturnType<typeof setTimeout> | undefined;
|
|
52
|
+
// Focus from a pointer: the click picks the row, so focusing doesn't activate the first one.
|
|
53
|
+
let pointerFocus = false;
|
|
54
|
+
// While the listbox has focus it filters by the bounds from when focus arrived: each key press
|
|
55
|
+
// pans the map, and re-filtering on that pan's moveend would reshuffle the rows under the user.
|
|
56
|
+
let frozen = $state.raw<{ bounds: Bounds | null } | null>(null);
|
|
57
|
+
|
|
58
|
+
// Every plugin's features in view, re-computed when the view settles (bounds change on
|
|
59
|
+
// moveend; not while focused, see `frozen`) or a plugin's data changes. A refKey a plugin
|
|
60
|
+
// repeats is listed once, its first item.
|
|
61
|
+
const rows = $derived.by((): Row[] => {
|
|
62
|
+
const bounds = frozen ? frozen.bounds : map.bounds;
|
|
63
|
+
const all = map.plugins.flatMap((plugin) => {
|
|
64
|
+
const items = plugin.features?.() ?? [];
|
|
65
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- local to one computation
|
|
66
|
+
const seen = new Set<string>();
|
|
67
|
+
return (bounds ? inView(items, bounds) : items).flatMap((item) => {
|
|
68
|
+
if (seen.has(item.refKey)) return [];
|
|
69
|
+
seen.add(item.refKey);
|
|
70
|
+
return [{ ...item, pluginId: plugin.id, key: optionIdPart(plugin.id, item.refKey) }];
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return sortItems(all, sortBy) as Row[];
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const selected = $derived(
|
|
77
|
+
new Map(map.plugins.map((p) => [p.id, selectionKeys(map.getSelection(p.id))]))
|
|
78
|
+
);
|
|
79
|
+
const multiselectable = $derived(
|
|
80
|
+
map.plugins.some((p) => map.getSelectionMode(p.id) === "multiple")
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const activeIndex = $derived(
|
|
84
|
+
activeKey === null ? -1 : rows.findIndex((r) => r.key === activeKey)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// The visible rows plus OVERSCAN either side; always including the active row, so
|
|
88
|
+
// `aria-activedescendant` points at an element in the DOM.
|
|
89
|
+
const range = $derived.by(() => {
|
|
90
|
+
const h = rowHeight || 1;
|
|
91
|
+
let start = Math.max(0, Math.floor(scrollTop / h) - OVERSCAN);
|
|
92
|
+
let end = Math.min(rows.length, Math.ceil((scrollTop + viewportHeight) / h) + OVERSCAN);
|
|
93
|
+
if (activeIndex >= 0) {
|
|
94
|
+
start = Math.min(start, activeIndex);
|
|
95
|
+
end = Math.max(end, activeIndex + 1);
|
|
96
|
+
}
|
|
97
|
+
return { start, end };
|
|
98
|
+
});
|
|
99
|
+
const windowRows = $derived(rows.slice(range.start, range.end));
|
|
100
|
+
|
|
101
|
+
// Whatever moved the active row (keys, a data change, a re-filter), keep it in view.
|
|
102
|
+
$effect(() => {
|
|
103
|
+
const index = activeIndex;
|
|
104
|
+
const h = rowHeight;
|
|
105
|
+
const el = scroller;
|
|
106
|
+
if (index < 0 || !h || !el) return;
|
|
107
|
+
untrack(() => {
|
|
108
|
+
const top = index * h;
|
|
109
|
+
if (top < el.scrollTop) el.scrollTop = top;
|
|
110
|
+
else if (top + h > el.scrollTop + el.clientHeight) el.scrollTop = top + h - el.clientHeight;
|
|
111
|
+
scrollTop = el.scrollTop;
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** Stable per row, so a row keeps its id when the list re-sorts or re-filters. */
|
|
116
|
+
function optionId(row: Row): string {
|
|
117
|
+
return `${baseId}-${row.key}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function setSort(by: SortBy) {
|
|
121
|
+
sortBy = by;
|
|
122
|
+
if (frozen) frozen = { bounds: map.bounds };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isSelectable(row: Row): boolean {
|
|
126
|
+
return row.selectable && map.getSelectionMode(row.pluginId) !== "none";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isSelected(row: Row): boolean {
|
|
130
|
+
return selected.get(row.pluginId)?.has(row.refKey) ?? false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Makes row `index` active and focuses (and pans to) its feature. */
|
|
134
|
+
function activate(index: number) {
|
|
135
|
+
const row = rows[index];
|
|
136
|
+
if (!row) return;
|
|
137
|
+
activeKey = row.key;
|
|
138
|
+
map.focusFeature(row.refKey, row.pluginId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pick(row: Row, additive: boolean) {
|
|
142
|
+
map.pickFromList(row.pluginId, row.refKey, additive);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function pageSize(): number {
|
|
146
|
+
return Math.max(1, Math.floor(viewportHeight / (rowHeight || 1)) - 1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function openMenu() {
|
|
150
|
+
const row = rows[activeIndex];
|
|
151
|
+
if (!row) return;
|
|
152
|
+
await tick();
|
|
153
|
+
const el = document.getElementById(optionId(row));
|
|
154
|
+
if (!el) return;
|
|
155
|
+
const box = el.getBoundingClientRect();
|
|
156
|
+
el.dispatchEvent(
|
|
157
|
+
new MouseEvent("contextmenu", {
|
|
158
|
+
bubbles: true,
|
|
159
|
+
cancelable: true,
|
|
160
|
+
composed: true,
|
|
161
|
+
button: 2,
|
|
162
|
+
clientX: box.left + Math.min(box.width / 2, 24),
|
|
163
|
+
clientY: box.top + box.height / 2
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onkeydown(e: KeyboardEvent) {
|
|
169
|
+
const last = rows.length - 1;
|
|
170
|
+
if ((e.key === "F10" && e.shiftKey) || e.key === "ContextMenu") {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
if (activeIndex < 0) return;
|
|
173
|
+
menuSent = true;
|
|
174
|
+
clearTimeout(menuSentTimer);
|
|
175
|
+
menuSentTimer = setTimeout(() => (menuSent = false), MENU_EVENT_WINDOW_MS);
|
|
176
|
+
void openMenu();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
180
|
+
const current = activeIndex;
|
|
181
|
+
let next: number;
|
|
182
|
+
switch (e.key) {
|
|
183
|
+
case "ArrowDown":
|
|
184
|
+
next = Math.min(current + 1, last);
|
|
185
|
+
break;
|
|
186
|
+
case "ArrowUp":
|
|
187
|
+
next = current < 0 ? 0 : Math.max(current - 1, 0);
|
|
188
|
+
break;
|
|
189
|
+
case "Home":
|
|
190
|
+
next = 0;
|
|
191
|
+
break;
|
|
192
|
+
case "End":
|
|
193
|
+
next = last;
|
|
194
|
+
break;
|
|
195
|
+
case "PageDown":
|
|
196
|
+
next = Math.min(Math.max(current, 0) + pageSize(), last);
|
|
197
|
+
break;
|
|
198
|
+
case "PageUp":
|
|
199
|
+
next = Math.max(current - pageSize(), 0);
|
|
200
|
+
break;
|
|
201
|
+
case "Enter":
|
|
202
|
+
case " ":
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
if (current >= 0) pick(rows[current], e.key === " ");
|
|
205
|
+
return;
|
|
206
|
+
default:
|
|
207
|
+
if (e.key.length === 1 && e.key !== " ") {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
findByTyping(e.key);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
if (rows.length > 0) activate(next);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function findByTyping(char: string) {
|
|
218
|
+
clearTimeout(typeaheadTimer);
|
|
219
|
+
typeaheadTimer = setTimeout(() => (typeahead = ""), TYPEAHEAD_MS);
|
|
220
|
+
// A new search starts after the active row; a longer one may stay on it.
|
|
221
|
+
const from = typeahead === "" ? activeIndex + 1 : Math.max(activeIndex, 0);
|
|
222
|
+
typeahead += char;
|
|
223
|
+
const index = typeaheadIndex(rows, typeahead, from);
|
|
224
|
+
if (index >= 0) activate(index);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function onfocus() {
|
|
228
|
+
frozen = { bounds: map.bounds };
|
|
229
|
+
const byPointer = pointerFocus;
|
|
230
|
+
pointerFocus = false;
|
|
231
|
+
if (byPointer || rows.length === 0) return;
|
|
232
|
+
if (activeIndex >= 0) {
|
|
233
|
+
const row = rows[activeIndex];
|
|
234
|
+
map.focusFeature(row.refKey, row.pluginId);
|
|
235
|
+
} else {
|
|
236
|
+
const first = rows.findIndex(isSelected);
|
|
237
|
+
activate(first >= 0 ? first : 0);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Clears the map's focus if it still points at the active row. */
|
|
242
|
+
function releaseFocus() {
|
|
243
|
+
const row = rows[activeIndex];
|
|
244
|
+
if (row && map.focusedKey === row.refKey && map.focusedPluginId === row.pluginId) {
|
|
245
|
+
map.focusFeature(null);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function onblur() {
|
|
250
|
+
pointerFocus = false;
|
|
251
|
+
releaseFocus();
|
|
252
|
+
frozen = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function oncontextmenu(e: MouseEvent) {
|
|
256
|
+
// A row's own event (a right-click) bubbles as is. The browser's keyboard menu event
|
|
257
|
+
// targets the focused listbox instead, so it goes to the active row.
|
|
258
|
+
if (e.target !== listbox || activeIndex < 0) return;
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
if (menuSent) menuSent = false;
|
|
262
|
+
else void openMenu();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function onrowclick(e: MouseEvent, index: number) {
|
|
266
|
+
activate(index);
|
|
267
|
+
pick(rows[index], e.shiftKey || e.metaKey || e.ctrlKey);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function formatValue(row: Row): string {
|
|
271
|
+
if (row.value === null) return "";
|
|
272
|
+
return typeof row.value === "number"
|
|
273
|
+
? formatNumber(row.value, row.precision, map.format)
|
|
274
|
+
: row.value;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
onDestroy(() => {
|
|
278
|
+
clearTimeout(typeaheadTimer);
|
|
279
|
+
clearTimeout(menuSentTimer);
|
|
280
|
+
// Unmounting a focused listbox may not fire blur.
|
|
281
|
+
untrack(releaseFocus);
|
|
282
|
+
});
|
|
283
|
+
</script>
|
|
284
|
+
|
|
285
|
+
<div class={["mapsdk-list text-sm", className]} {id} data-mapsdk-map={map.uid}>
|
|
286
|
+
<div class="mapsdk-list-sort">
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
class="mapsdk-focus mapsdk-motion"
|
|
290
|
+
aria-pressed={sortBy === "name"}
|
|
291
|
+
onclick={() => setSort("name")}
|
|
292
|
+
>
|
|
293
|
+
{map.messages("mapsdk_list_sort_name")}
|
|
294
|
+
</button>
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
class="mapsdk-focus mapsdk-motion"
|
|
298
|
+
aria-pressed={sortBy === "value"}
|
|
299
|
+
onclick={() => setSort("value")}
|
|
300
|
+
>
|
|
301
|
+
{map.messages("mapsdk_list_sort_value")}
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="mapsdk-list-row mapsdk-list-sizer" aria-hidden="true" bind:offsetHeight={rowHeight}>
|
|
305
|
+
|
|
306
|
+
</div>
|
|
307
|
+
<!-- The scroller is separate from the listbox: the listbox's padding stands in for the rows
|
|
308
|
+
that aren't rendered, and a scroll container can't shrink its own padding. -->
|
|
309
|
+
<div
|
|
310
|
+
bind:this={scroller}
|
|
311
|
+
bind:clientHeight={viewportHeight}
|
|
312
|
+
class="mapsdk-list-scroll"
|
|
313
|
+
onscroll={() => (scrollTop = scroller!.scrollTop)}
|
|
314
|
+
>
|
|
315
|
+
<div
|
|
316
|
+
bind:this={listbox}
|
|
317
|
+
class="mapsdk-list-box mapsdk-focus-inset-active"
|
|
318
|
+
role="listbox"
|
|
319
|
+
tabindex="0"
|
|
320
|
+
aria-label={map.messages("mapsdk_list_label")}
|
|
321
|
+
aria-multiselectable={multiselectable || undefined}
|
|
322
|
+
aria-activedescendant={activeIndex >= 0 ? optionId(rows[activeIndex]) : undefined}
|
|
323
|
+
style:padding-top="{range.start * rowHeight}px"
|
|
324
|
+
style:padding-bottom="{(rows.length - range.end) * rowHeight}px"
|
|
325
|
+
{onkeydown}
|
|
326
|
+
{onfocus}
|
|
327
|
+
{onblur}
|
|
328
|
+
{oncontextmenu}
|
|
329
|
+
onpointerdown={() => (pointerFocus = true)}
|
|
330
|
+
>
|
|
331
|
+
{#each windowRows as row, i (row.key)}
|
|
332
|
+
{@const index = range.start + i}
|
|
333
|
+
{@const level = shownLevel(row)}
|
|
334
|
+
{@const levelPlace = levelText(row, map.messages)}
|
|
335
|
+
{@const isOn = isSelected(row)}
|
|
336
|
+
<!-- Options aren't focused: the listbox holds focus and handles keys (aria-activedescendant). -->
|
|
337
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_interactive_supports_focus -->
|
|
338
|
+
<div
|
|
339
|
+
id={optionId(row)}
|
|
340
|
+
class="mapsdk-list-row"
|
|
341
|
+
class:mapsdk-active-option={index === activeIndex}
|
|
342
|
+
class:active={index === activeIndex}
|
|
343
|
+
class:selected={isOn}
|
|
344
|
+
role="option"
|
|
345
|
+
aria-selected={isSelectable(row) ? isOn : undefined}
|
|
346
|
+
aria-setsize={rows.length}
|
|
347
|
+
aria-posinset={index + 1}
|
|
348
|
+
aria-label={rowLabel(row, map.messages, map.format)}
|
|
349
|
+
data-mapsdk-ref={row.refKey}
|
|
350
|
+
data-mapsdk-plugin={row.pluginId}
|
|
351
|
+
onclick={(e) => onrowclick(e, index)}
|
|
352
|
+
>
|
|
353
|
+
<span class="mapsdk-list-check" aria-hidden="true">
|
|
354
|
+
{#if isOn}
|
|
355
|
+
<svg viewBox="0 0 16 16" width="14" height="14">
|
|
356
|
+
<path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" />
|
|
357
|
+
</svg>
|
|
358
|
+
{/if}
|
|
359
|
+
</span>
|
|
360
|
+
<span class="mapsdk-list-name">{row.name}</span>
|
|
361
|
+
<span class="mapsdk-list-value">
|
|
362
|
+
{formatValue(row)}{#if row.units && row.value !== null} {row.units}{/if}
|
|
363
|
+
</span>
|
|
364
|
+
<span class="mapsdk-list-level text-xs">
|
|
365
|
+
{#if levelPlace.kind !== "detail"}
|
|
366
|
+
<span
|
|
367
|
+
class="mapsdk-list-swatch"
|
|
368
|
+
class:neutral={!level?.color}
|
|
369
|
+
style:background={level?.color || undefined}
|
|
370
|
+
></span>
|
|
371
|
+
{/if}
|
|
372
|
+
{levelPlace.text}
|
|
373
|
+
</span>
|
|
374
|
+
</div>
|
|
375
|
+
{/each}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
{#if rows.length === 0}
|
|
379
|
+
<p class="mapsdk-list-empty">{map.messages("mapsdk_list_empty")}</p>
|
|
380
|
+
{/if}
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<style>
|
|
384
|
+
.mapsdk-list {
|
|
385
|
+
--mapsdk-list-row-height: 2.25rem;
|
|
386
|
+
display: flex;
|
|
387
|
+
flex-direction: column;
|
|
388
|
+
position: relative;
|
|
389
|
+
width: 21rem;
|
|
390
|
+
max-width: calc(100vw - 2rem);
|
|
391
|
+
max-height: 100%;
|
|
392
|
+
box-sizing: border-box;
|
|
393
|
+
border: 1px solid var(--card-border, var(--border));
|
|
394
|
+
border-radius: var(--radius-control, var(--radius));
|
|
395
|
+
background: var(--background);
|
|
396
|
+
color: var(--foreground);
|
|
397
|
+
box-shadow: var(--shadow-lifted, var(--shadow-card, none));
|
|
398
|
+
pointer-events: auto;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.mapsdk-list-sort {
|
|
402
|
+
display: flex;
|
|
403
|
+
gap: 0.25rem;
|
|
404
|
+
padding: 0.5rem;
|
|
405
|
+
border-bottom: 1px solid var(--card-border, var(--border));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.mapsdk-list-sort button {
|
|
409
|
+
padding: 0.25rem 0.625rem;
|
|
410
|
+
border: 1px solid var(--border);
|
|
411
|
+
border-radius: var(--radius-control, var(--radius));
|
|
412
|
+
background: var(--background);
|
|
413
|
+
color: var(--foreground);
|
|
414
|
+
font: inherit;
|
|
415
|
+
cursor: pointer;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.mapsdk-list-sort button[aria-pressed="true"] {
|
|
419
|
+
background: var(--highlight-subtle, var(--muted));
|
|
420
|
+
border-color: var(--highlight, var(--primary));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.mapsdk-list-scroll {
|
|
424
|
+
flex: 1 1 auto;
|
|
425
|
+
min-height: 0;
|
|
426
|
+
max-height: calc(var(--mapsdk-list-row-height) * 10);
|
|
427
|
+
overflow-y: auto;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.mapsdk-list-box {
|
|
431
|
+
outline: none;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* With no active row to carry the ring (an empty list), the scroller shows it. */
|
|
435
|
+
.mapsdk-list-scroll:has(.mapsdk-list-box:focus-visible):not(:has(.active)) {
|
|
436
|
+
outline: 2px solid var(--ring);
|
|
437
|
+
outline-offset: -3px;
|
|
438
|
+
box-shadow: inset 0 0 0 4px var(--ring-edge, var(--background));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.mapsdk-list-row {
|
|
442
|
+
display: grid;
|
|
443
|
+
grid-template-columns: 1rem minmax(0, 1fr) auto auto;
|
|
444
|
+
align-items: center;
|
|
445
|
+
gap: 0.5rem;
|
|
446
|
+
height: var(--mapsdk-list-row-height);
|
|
447
|
+
box-sizing: border-box;
|
|
448
|
+
padding: 0 0.75rem;
|
|
449
|
+
cursor: default;
|
|
450
|
+
white-space: nowrap;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.mapsdk-list-sizer {
|
|
454
|
+
position: absolute;
|
|
455
|
+
visibility: hidden;
|
|
456
|
+
pointer-events: none;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.mapsdk-list-row.selected {
|
|
460
|
+
background: var(--highlight-subtle, var(--muted));
|
|
461
|
+
box-shadow: inset 0 0 0 2px var(--highlight, var(--primary));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* Selected and active: the selection ring outside the focus halo; more specific than the
|
|
465
|
+
shared rule, so the order of the stylesheets doesn't matter. */
|
|
466
|
+
.mapsdk-list-box:focus-visible .mapsdk-list-row.selected.mapsdk-active-option {
|
|
467
|
+
box-shadow:
|
|
468
|
+
inset 0 0 0 4px var(--ring-edge, var(--background)),
|
|
469
|
+
inset 0 0 0 6px var(--highlight, var(--primary));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.mapsdk-list-name {
|
|
473
|
+
overflow: hidden;
|
|
474
|
+
text-overflow: ellipsis;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.mapsdk-list-value {
|
|
478
|
+
font-variant-numeric: tabular-nums;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.mapsdk-list-level {
|
|
482
|
+
display: inline-flex;
|
|
483
|
+
align-items: center;
|
|
484
|
+
gap: 0.375rem;
|
|
485
|
+
color: var(--muted-foreground);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.mapsdk-list-swatch {
|
|
489
|
+
width: 0.625rem;
|
|
490
|
+
height: 0.625rem;
|
|
491
|
+
border-radius: 999px;
|
|
492
|
+
outline: var(--swatch-outline-width, 1px) solid var(--swatch-outline-color, var(--foreground));
|
|
493
|
+
outline-offset: 1px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.mapsdk-list-swatch.neutral {
|
|
497
|
+
background: var(--muted);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.mapsdk-list-empty {
|
|
501
|
+
margin: 0;
|
|
502
|
+
padding: 0.75rem;
|
|
503
|
+
color: var(--muted-foreground);
|
|
504
|
+
}
|
|
505
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MapContext } from "../core/create-map.svelte.js";
|
|
2
|
+
import "./controls.css";
|
|
3
|
+
import { type SortBy } from "./feature-list.js";
|
|
4
|
+
interface Props {
|
|
5
|
+
map: MapContext;
|
|
6
|
+
class?: string;
|
|
7
|
+
/** The root element's id, e.g. for a button's `aria-controls`. */
|
|
8
|
+
id?: string;
|
|
9
|
+
/** The sort order; bind it to keep it while the list is unmounted. */
|
|
10
|
+
sortBy?: SortBy;
|
|
11
|
+
}
|
|
12
|
+
declare const FeatureList: import("svelte").Component<Props, {}, "sortBy">;
|
|
13
|
+
type FeatureList = ReturnType<typeof FeatureList>;
|
|
14
|
+
export default FeatureList;
|