mertani-web-toolkit 0.1.63 → 0.1.65
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/dist/components/inputs/SelectInput/SelectInput.css +27 -6
- package/dist/components/inputs/SelectInput/SelectInput.svelte +40 -15
- package/dist/components/inputs/SelectInput/SelectInput.svelte.d.ts +2 -0
- package/dist/components/map/Map.d.ts +1 -0
- package/dist/components/map/Map.js +1 -0
- package/dist/components/map/Map.svelte +284 -0
- package/dist/components/map/Map.svelte.d.ts +44 -0
- package/dist/components/map/MapGeoJSON.svelte +60 -0
- package/dist/components/map/MapGeoJSON.svelte.d.ts +8 -0
- package/dist/components/map/MapMarker.svelte +424 -0
- package/dist/components/map/MapMarker.svelte.d.ts +25 -0
- package/dist/components/map/MapMarkerGroup.svelte +213 -0
- package/dist/components/map/MapMarkerGroup.svelte.d.ts +31 -0
- package/dist/components/map/MapTopoJSON.svelte +70 -0
- package/dist/components/map/MapTopoJSON.svelte.d.ts +9 -0
- package/dist/components/map/MapVelocity.svelte +152 -0
- package/dist/components/map/MapVelocity.svelte.d.ts +37 -0
- package/dist/components/map/PopupWrapper.svelte +8 -0
- package/dist/components/map/PopupWrapper.svelte.d.ts +7 -0
- package/dist/components/map/map.css +176 -0
- package/dist/icons/BsSliders.svelte +21 -21
- package/dist/icons/Crosshair.svelte +12 -0
- package/dist/icons/Crosshair.svelte.d.ts +26 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +6 -0
- package/dist/utils/mapContext.d.ts +27 -0
- package/dist/utils/mapContext.js +52 -0
- package/package.json +9 -2
|
@@ -59,35 +59,56 @@
|
|
|
59
59
|
box-shadow 0.2s;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
.select-trigger:not(
|
|
62
|
+
.select-trigger:not(.disabled) {
|
|
63
63
|
background: var(--color-bg-surface);
|
|
64
64
|
cursor: pointer;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
.select-trigger:not(
|
|
67
|
+
.select-trigger:not(.disabled):hover {
|
|
68
68
|
border-color: var(--color-border-form);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
.select-trigger:not(
|
|
71
|
+
.select-trigger:not(.disabled):focus {
|
|
72
72
|
outline: none;
|
|
73
73
|
border-color: var(--color-bg-act-primary);
|
|
74
74
|
box-shadow: 0 0 0 2px var(--color-bg-act-primary) 40;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
.select-trigger
|
|
77
|
+
.select-trigger.disabled {
|
|
78
78
|
cursor: not-allowed;
|
|
79
79
|
background: var(--color-bg-disabled);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
.select-trigger.error:not(
|
|
82
|
+
.select-trigger.error:not(.disabled) {
|
|
83
83
|
border-color: var(--color-text-error-ti);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
.select-trigger.error:not(
|
|
86
|
+
.select-trigger.error:not(.disabled):focus {
|
|
87
87
|
border-color: var(--color-text-error-ti);
|
|
88
88
|
box-shadow: 0 0 0 2px var(--color-text-error-ti) 40;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
.select-clear-button {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
padding: 4px;
|
|
96
|
+
border-radius: 4px;
|
|
97
|
+
color: var(--color-text-tertiary);
|
|
98
|
+
background: transparent;
|
|
99
|
+
border: none;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
transition:
|
|
102
|
+
color 0.2s,
|
|
103
|
+
background-color 0.2s;
|
|
104
|
+
line-height: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.select-clear-button:hover {
|
|
108
|
+
color: var(--color-text-primary);
|
|
109
|
+
background: var(--color-bg-disabled);
|
|
110
|
+
}
|
|
111
|
+
|
|
91
112
|
.select-menu {
|
|
92
113
|
position: absolute;
|
|
93
114
|
right: 0;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount, onDestroy, tick, getContext } from 'svelte';
|
|
3
3
|
import type { IFieldOption } from './SelectInput.js';
|
|
4
|
-
import { Icon } from '
|
|
4
|
+
import { Icon } from '../../../index.js';
|
|
5
5
|
import './SelectInput.css';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
onOpen?: () => void;
|
|
42
42
|
/** Dipanggil saat teks search berubah (debounce 300ms), hanya jika remoteSearch. */
|
|
43
43
|
onSearchQueryChange?: (query: string) => void;
|
|
44
|
+
onClear?: () => void;
|
|
44
45
|
|
|
45
46
|
// Validation
|
|
46
47
|
isMandatory?: boolean;
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
class?: string;
|
|
57
58
|
style?: string;
|
|
58
59
|
isShowChevron?: boolean;
|
|
60
|
+
isClearable?: boolean;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
const {
|
|
@@ -92,6 +94,7 @@
|
|
|
92
94
|
onSelect,
|
|
93
95
|
onOpen,
|
|
94
96
|
onSearchQueryChange,
|
|
97
|
+
onClear,
|
|
95
98
|
|
|
96
99
|
// Validation
|
|
97
100
|
isMandatory = false,
|
|
@@ -105,7 +108,8 @@
|
|
|
105
108
|
|
|
106
109
|
class: className = '',
|
|
107
110
|
style: customStyle = '',
|
|
108
|
-
isShowChevron = true
|
|
111
|
+
isShowChevron = true,
|
|
112
|
+
isClearable = false
|
|
109
113
|
}: Props = $props();
|
|
110
114
|
|
|
111
115
|
// ===== Constants =====
|
|
@@ -123,7 +127,7 @@
|
|
|
123
127
|
let open = $state(false);
|
|
124
128
|
let search = $state('');
|
|
125
129
|
let placement: 'bottom' | 'top' = $state('bottom');
|
|
126
|
-
let triggerEl:
|
|
130
|
+
let triggerEl: HTMLDivElement | null = $state(null);
|
|
127
131
|
let menuEl: HTMLDivElement | null = $state(null);
|
|
128
132
|
let resizeObs: ResizeObserver | null = $state(null);
|
|
129
133
|
let unregisterDropdown: (() => void) | null = $state(null);
|
|
@@ -306,6 +310,13 @@
|
|
|
306
310
|
close();
|
|
307
311
|
}
|
|
308
312
|
|
|
313
|
+
function handleClear(e: MouseEvent) {
|
|
314
|
+
e.stopPropagation();
|
|
315
|
+
onSelect?.('');
|
|
316
|
+
onClear?.();
|
|
317
|
+
error = validateInput('');
|
|
318
|
+
}
|
|
319
|
+
|
|
309
320
|
function validateInput(val: string): string {
|
|
310
321
|
if (customValidation) {
|
|
311
322
|
const customError = customValidation(val);
|
|
@@ -403,28 +414,42 @@
|
|
|
403
414
|
</label>
|
|
404
415
|
{/if}
|
|
405
416
|
<div class="relative flex-1">
|
|
406
|
-
<
|
|
417
|
+
<div
|
|
407
418
|
bind:this={triggerEl}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
style={triggerStyles()}
|
|
419
|
+
role="combobox"
|
|
420
|
+
tabindex="0"
|
|
411
421
|
aria-haspopup="listbox"
|
|
412
422
|
aria-expanded={open}
|
|
423
|
+
aria-controls={open ? 'select-menu' : undefined}
|
|
424
|
+
class="select-trigger {className} {error ? 'error' : ''}"
|
|
425
|
+
style={triggerStyles()}
|
|
413
426
|
onclick={toggle}
|
|
414
427
|
onkeydown={handleKeydown}
|
|
415
428
|
onfocus={handleFocus}
|
|
416
429
|
onblur={handleBlur}
|
|
417
|
-
disabled={disabled || isLoading}
|
|
418
430
|
title={tooltip || ''}
|
|
431
|
+
class:disabled={disabled || isLoading}
|
|
419
432
|
>
|
|
420
433
|
<span class="truncate">{labelOf(value) || placeholder}</span>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
434
|
+
<div class="flex items-center gap-1">
|
|
435
|
+
{#if isClearable && value && !disabled && !isLoading}
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
class="select-clear-button"
|
|
439
|
+
onclick={handleClear}
|
|
440
|
+
aria-label="Clear selection"
|
|
441
|
+
>
|
|
442
|
+
<Icon name="bs-x-lg" />
|
|
443
|
+
</button>
|
|
444
|
+
{/if}
|
|
445
|
+
{#if isShowChevron}
|
|
446
|
+
<Icon
|
|
447
|
+
name="bs-chevron-down"
|
|
448
|
+
style="transform-origin: center; transition: transform 0.25s; transform: rotate({chevronRotation});"
|
|
449
|
+
/>
|
|
450
|
+
{/if}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
428
453
|
|
|
429
454
|
{#if open}
|
|
430
455
|
<div
|
|
@@ -28,6 +28,7 @@ interface Props {
|
|
|
28
28
|
onOpen?: () => void;
|
|
29
29
|
/** Dipanggil saat teks search berubah (debounce 300ms), hanya jika remoteSearch. */
|
|
30
30
|
onSearchQueryChange?: (query: string) => void;
|
|
31
|
+
onClear?: () => void;
|
|
31
32
|
isMandatory?: boolean;
|
|
32
33
|
customValidation?: (value: string) => string | null;
|
|
33
34
|
isLoading?: boolean;
|
|
@@ -37,6 +38,7 @@ interface Props {
|
|
|
37
38
|
class?: string;
|
|
38
39
|
style?: string;
|
|
39
40
|
isShowChevron?: boolean;
|
|
41
|
+
isClearable?: boolean;
|
|
40
42
|
}
|
|
41
43
|
declare const SelectInput: import("svelte").Component<Props, {}, "">;
|
|
42
44
|
type SelectInput = ReturnType<typeof SelectInput>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, onMount } from 'svelte';
|
|
3
|
+
import type { Map as LMap, LatLngTuple, LatLngBounds, TileLayer, Marker, Layer } from 'leaflet';
|
|
4
|
+
import 'leaflet/dist/leaflet.css';
|
|
5
|
+
import { createMapContext } from '../../utils/mapContext.js';
|
|
6
|
+
import './map.css';
|
|
7
|
+
import Icon from '../Icon/Icon.svelte';
|
|
8
|
+
|
|
9
|
+
type LeafletNamespace = typeof import('leaflet');
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
center?: LatLngTuple;
|
|
13
|
+
zoom?: number;
|
|
14
|
+
minZoom?: number;
|
|
15
|
+
maxZoom?: number;
|
|
16
|
+
tileUrl?: string;
|
|
17
|
+
tileAttribution?: string;
|
|
18
|
+
scrollWheelZoom?: boolean;
|
|
19
|
+
zoomControl?: boolean;
|
|
20
|
+
autoZoom?: boolean;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
center,
|
|
26
|
+
zoom = 5,
|
|
27
|
+
minZoom,
|
|
28
|
+
maxZoom,
|
|
29
|
+
tileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
30
|
+
tileAttribution = '©2025 Developed by <b>Mertani</b>',
|
|
31
|
+
scrollWheelZoom = true,
|
|
32
|
+
autoZoom = true,
|
|
33
|
+
class: className = ''
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
let el: HTMLDivElement | null = $state(null);
|
|
37
|
+
let map: LMap | null = null;
|
|
38
|
+
let tile: TileLayer | null = null;
|
|
39
|
+
let L: LeafletNamespace | null = null;
|
|
40
|
+
const mapStore = createMapContext();
|
|
41
|
+
|
|
42
|
+
function calculateBounds(): LatLngBounds | null {
|
|
43
|
+
if (!map || !L) return null;
|
|
44
|
+
|
|
45
|
+
const bounds = L.latLngBounds([]);
|
|
46
|
+
let hasBounds = false;
|
|
47
|
+
|
|
48
|
+
map.eachLayer((layer: Layer) => {
|
|
49
|
+
if (!L) return;
|
|
50
|
+
|
|
51
|
+
// Skip tile layers
|
|
52
|
+
if (layer instanceof L.TileLayer) return;
|
|
53
|
+
|
|
54
|
+
// Handle MarkerClusterGroup - get all child markers
|
|
55
|
+
if (
|
|
56
|
+
(layer as any).getAllChildMarkers &&
|
|
57
|
+
typeof (layer as any).getAllChildMarkers === 'function'
|
|
58
|
+
) {
|
|
59
|
+
const clusterGroup = layer as any;
|
|
60
|
+
const markers = clusterGroup.getAllChildMarkers() as Marker[];
|
|
61
|
+
markers.forEach((marker: Marker) => {
|
|
62
|
+
const latlng = marker.getLatLng();
|
|
63
|
+
if (latlng) {
|
|
64
|
+
bounds.extend(latlng);
|
|
65
|
+
hasBounds = true;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Handle individual markers
|
|
70
|
+
else if ('getLatLng' in layer && typeof layer.getLatLng === 'function') {
|
|
71
|
+
const latlng = (layer as Marker).getLatLng();
|
|
72
|
+
if (latlng) {
|
|
73
|
+
bounds.extend(latlng);
|
|
74
|
+
hasBounds = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Handle layers with bounds (GeoJSON, TopoJSON, etc.)
|
|
78
|
+
else if ('getBounds' in layer && typeof layer.getBounds === 'function') {
|
|
79
|
+
const layerBounds = layer.getBounds() as LatLngBounds;
|
|
80
|
+
if (layerBounds && layerBounds.isValid()) {
|
|
81
|
+
bounds.extend(layerBounds);
|
|
82
|
+
hasBounds = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return hasBounds && bounds.isValid() ? bounds : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function autoZoomToMarkers() {
|
|
91
|
+
if (!map || !L || !autoZoom) return;
|
|
92
|
+
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
if (!map || !L) return;
|
|
95
|
+
|
|
96
|
+
const bounds = calculateBounds();
|
|
97
|
+
if (bounds) {
|
|
98
|
+
map.fitBounds(bounds, { padding: [30, 30] });
|
|
99
|
+
} else if (center) {
|
|
100
|
+
map.setView(center, zoom);
|
|
101
|
+
} else {
|
|
102
|
+
map.setView([-2.5489, 118.0149] as LatLngTuple, zoom);
|
|
103
|
+
}
|
|
104
|
+
}, 100);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function init() {
|
|
108
|
+
if (!el) return;
|
|
109
|
+
|
|
110
|
+
const leafletMod = await import('leaflet');
|
|
111
|
+
L = (leafletMod as any).default ?? leafletMod;
|
|
112
|
+
|
|
113
|
+
if (!L) return;
|
|
114
|
+
|
|
115
|
+
map = L.map(el, {
|
|
116
|
+
zoomControl: false,
|
|
117
|
+
zoomAnimation: true,
|
|
118
|
+
fadeAnimation: true,
|
|
119
|
+
markerZoomAnimation: true,
|
|
120
|
+
scrollWheelZoom,
|
|
121
|
+
minZoom,
|
|
122
|
+
maxZoom
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
|
126
|
+
tile = L.tileLayer(tileUrl, { attribution: tileAttribution });
|
|
127
|
+
tile.addTo(map);
|
|
128
|
+
|
|
129
|
+
if (center) {
|
|
130
|
+
map.setView(center, zoom);
|
|
131
|
+
} else {
|
|
132
|
+
map.setView([-2.5489, 118.0149] as LatLngTuple, zoom);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
mapStore.set(map as any);
|
|
136
|
+
|
|
137
|
+
requestAnimationFrame(() => map?.invalidateSize());
|
|
138
|
+
setTimeout(() => map?.invalidateSize(), 50);
|
|
139
|
+
|
|
140
|
+
if (autoZoom) {
|
|
141
|
+
setTimeout(() => autoZoomToMarkers(), 200);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function updateTileLayer() {
|
|
146
|
+
if (!map || !L) return;
|
|
147
|
+
|
|
148
|
+
if (tile) {
|
|
149
|
+
map.removeLayer(tile);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
tile = L.tileLayer(tileUrl, { attribution: tileAttribution });
|
|
153
|
+
tile.addTo(map);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
$effect(() => {
|
|
157
|
+
if (autoZoom && map && L) {
|
|
158
|
+
autoZoomToMarkers();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export function zoomToAll() {
|
|
163
|
+
if (!map || !L) return;
|
|
164
|
+
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
if (!map || !L) return;
|
|
167
|
+
|
|
168
|
+
const bounds = calculateBounds();
|
|
169
|
+
|
|
170
|
+
if (bounds && bounds.isValid()) {
|
|
171
|
+
map.flyToBounds(bounds, {
|
|
172
|
+
padding: [50, 50],
|
|
173
|
+
maxZoom: 18
|
|
174
|
+
});
|
|
175
|
+
} else if (center) {
|
|
176
|
+
map.flyTo(center, zoom);
|
|
177
|
+
} else {
|
|
178
|
+
map.flyTo([-2.5489, 118.0149] as LatLngTuple, zoom);
|
|
179
|
+
}
|
|
180
|
+
}, 100);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
$effect(() => {
|
|
184
|
+
if (!map || autoZoom) return;
|
|
185
|
+
if (center) {
|
|
186
|
+
map.setView(center, zoom, { animate: false });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
$effect(() => {
|
|
191
|
+
tileUrl;
|
|
192
|
+
tileAttribution;
|
|
193
|
+
updateTileLayer();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
$effect(() => {
|
|
197
|
+
if (!map || !L) return;
|
|
198
|
+
updateTileLayer();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
onMount(() => {
|
|
202
|
+
init();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
onDestroy(() => {
|
|
206
|
+
try {
|
|
207
|
+
mapStore.set(null);
|
|
208
|
+
tile?.remove?.();
|
|
209
|
+
map?.remove?.();
|
|
210
|
+
} finally {
|
|
211
|
+
tile = null;
|
|
212
|
+
map = null;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<div class={`mertani-map ${className}`} bind:this={el} role="application" aria-label="Map">
|
|
218
|
+
<slot />
|
|
219
|
+
<button class="crosshair-btn" onclick={zoomToAll} type="button" aria-label="Zoom to all">
|
|
220
|
+
<div class="info-icon">
|
|
221
|
+
<Icon name="bs-crosshair" />
|
|
222
|
+
</div>
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<style>
|
|
227
|
+
.mertani-map {
|
|
228
|
+
position: relative;
|
|
229
|
+
width: 100%;
|
|
230
|
+
height: 100%;
|
|
231
|
+
min-height: 280px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.crosshair-btn {
|
|
235
|
+
position: absolute;
|
|
236
|
+
bottom: 24px;
|
|
237
|
+
left: 24px;
|
|
238
|
+
z-index: 1001;
|
|
239
|
+
background-color: color-mix(in srgb, var(--color-bg-surface), transparent 20%) !important;
|
|
240
|
+
backdrop-filter: blur(2px);
|
|
241
|
+
padding: 4px;
|
|
242
|
+
border-radius: 8px;
|
|
243
|
+
border: none;
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
transition: background-color 0.2s;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.crosshair-btn:hover {
|
|
249
|
+
background-color: color-mix(in srgb, var(--color-bg-surface), transparent 5%) !important;
|
|
250
|
+
backdrop-filter: blur(2px);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.crosshair-btn .info-icon {
|
|
254
|
+
padding: 8px;
|
|
255
|
+
border-radius: 6px;
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
color: var(--color-bg-act-primary);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.crosshair-btn .info-icon :global(svg) {
|
|
263
|
+
width: 24px;
|
|
264
|
+
height: 24px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@media (max-width: 768px) {
|
|
268
|
+
.crosshair-btn {
|
|
269
|
+
bottom: 12px;
|
|
270
|
+
left: 12px;
|
|
271
|
+
padding: 3px;
|
|
272
|
+
border-radius: 6px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.crosshair-btn .info-icon {
|
|
276
|
+
padding: 6px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.crosshair-btn :global(svg) {
|
|
280
|
+
width: 20px !important;
|
|
281
|
+
height: 20px !important;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { LatLngTuple } from 'leaflet';
|
|
2
|
+
import 'leaflet/dist/leaflet.css';
|
|
3
|
+
import './map.css';
|
|
4
|
+
interface Props {
|
|
5
|
+
center?: LatLngTuple;
|
|
6
|
+
zoom?: number;
|
|
7
|
+
minZoom?: number;
|
|
8
|
+
maxZoom?: number;
|
|
9
|
+
tileUrl?: string;
|
|
10
|
+
tileAttribution?: string;
|
|
11
|
+
scrollWheelZoom?: boolean;
|
|
12
|
+
zoomControl?: boolean;
|
|
13
|
+
autoZoom?: boolean;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
17
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
18
|
+
$$bindings?: Bindings;
|
|
19
|
+
} & Exports;
|
|
20
|
+
(internal: unknown, props: Props & {
|
|
21
|
+
$$events?: Events;
|
|
22
|
+
$$slots?: Slots;
|
|
23
|
+
}): Exports & {
|
|
24
|
+
$set?: any;
|
|
25
|
+
$on?: any;
|
|
26
|
+
};
|
|
27
|
+
z_$$bindings?: Bindings;
|
|
28
|
+
}
|
|
29
|
+
type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
|
|
30
|
+
default: any;
|
|
31
|
+
} ? Props extends Record<string, never> ? any : {
|
|
32
|
+
children?: any;
|
|
33
|
+
} : {});
|
|
34
|
+
declare const Map: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<Props, {
|
|
35
|
+
default: {};
|
|
36
|
+
}>, {
|
|
37
|
+
[evt: string]: CustomEvent<any>;
|
|
38
|
+
}, {
|
|
39
|
+
default: {};
|
|
40
|
+
}, {
|
|
41
|
+
zoomToAll: () => void;
|
|
42
|
+
}, "">;
|
|
43
|
+
type Map = InstanceType<typeof Map>;
|
|
44
|
+
export default Map;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { useMapContext } from '../../utils/mapContext.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
data,
|
|
7
|
+
style,
|
|
8
|
+
onEachFeature
|
|
9
|
+
}: {
|
|
10
|
+
data: any; // GeoJSON Feature/FeatureCollection
|
|
11
|
+
style?: any;
|
|
12
|
+
onEachFeature?: (feature: any, layer: any) => void;
|
|
13
|
+
} = $props();
|
|
14
|
+
|
|
15
|
+
const mapStore = useMapContext();
|
|
16
|
+
let map: any = null;
|
|
17
|
+
let layer: any = null;
|
|
18
|
+
let unsub: (() => void) | null = null;
|
|
19
|
+
|
|
20
|
+
async function rebuild() {
|
|
21
|
+
if (!map) return;
|
|
22
|
+
if (layer) {
|
|
23
|
+
layer.remove?.();
|
|
24
|
+
layer = null;
|
|
25
|
+
}
|
|
26
|
+
if (!data) return;
|
|
27
|
+
|
|
28
|
+
const leafletMod = await import('leaflet');
|
|
29
|
+
const L = (leafletMod as any).default ?? leafletMod;
|
|
30
|
+
|
|
31
|
+
layer = L.geoJSON(data, { style, onEachFeature });
|
|
32
|
+
layer.addTo(map);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$effect(() => {
|
|
36
|
+
data;
|
|
37
|
+
style;
|
|
38
|
+
onEachFeature;
|
|
39
|
+
rebuild();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
unsub = (mapStore as any).subscribe((m: any) => {
|
|
43
|
+
map = m;
|
|
44
|
+
if (!map && layer) {
|
|
45
|
+
layer.remove?.();
|
|
46
|
+
layer = null;
|
|
47
|
+
}
|
|
48
|
+
if (map) rebuild();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
onDestroy(() => {
|
|
52
|
+
try {
|
|
53
|
+
unsub?.();
|
|
54
|
+
} finally {
|
|
55
|
+
unsub = null;
|
|
56
|
+
layer?.remove?.();
|
|
57
|
+
layer = null;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
data: any;
|
|
3
|
+
style?: any;
|
|
4
|
+
onEachFeature?: (feature: any, layer: any) => void;
|
|
5
|
+
};
|
|
6
|
+
declare const MapGeoJSON: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type MapGeoJSON = ReturnType<typeof MapGeoJSON>;
|
|
8
|
+
export default MapGeoJSON;
|