@versatiles/svelte 2.0.1 → 2.1.0

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.
Files changed (83) hide show
  1. package/dist/components/BBoxMap/BBoxMap.svelte +23 -12
  2. package/dist/components/BBoxMap/lib/bbox.d.ts +1 -1
  3. package/dist/components/BBoxMap/lib/bbox.js +19 -17
  4. package/dist/components/BasicMap/BasicMap.svelte +22 -9
  5. package/dist/components/BasicMap/BasicMap.svelte.d.ts +2 -1
  6. package/dist/components/MapEditor/MapEditor.svelte +51 -20
  7. package/dist/components/MapEditor/components/Dialog.svelte +92 -0
  8. package/dist/components/MapEditor/components/Dialog.svelte.d.ts +17 -0
  9. package/dist/components/MapEditor/components/DialogFile.svelte +112 -0
  10. package/dist/components/MapEditor/components/DialogFile.svelte.d.ts +6 -0
  11. package/dist/components/MapEditor/components/DialogShare.svelte +216 -0
  12. package/dist/components/MapEditor/components/DialogShare.svelte.d.ts +10 -0
  13. package/dist/components/MapEditor/components/Editor.svelte +6 -14
  14. package/dist/components/MapEditor/components/EditorFill.svelte +3 -3
  15. package/dist/components/MapEditor/components/EditorStroke.svelte +3 -3
  16. package/dist/components/MapEditor/components/EditorSymbol.svelte +9 -9
  17. package/dist/components/MapEditor/components/InputRow.svelte +2 -3
  18. package/dist/components/MapEditor/components/PanelFile.svelte +73 -0
  19. package/dist/components/MapEditor/components/PanelFile.svelte.d.ts +7 -0
  20. package/dist/components/MapEditor/components/PanelSymbolSelector.svelte +82 -0
  21. package/dist/components/MapEditor/components/PanelSymbolSelector.svelte.d.ts +8 -0
  22. package/dist/components/MapEditor/components/Sidebar.svelte +51 -98
  23. package/dist/components/MapEditor/components/Sidebar.svelte.d.ts +3 -3
  24. package/dist/components/MapEditor/components/SidebarPanel.svelte +13 -5
  25. package/dist/components/MapEditor/lib/element/abstract.d.ts +8 -4
  26. package/dist/components/MapEditor/lib/element/abstract.js +10 -1
  27. package/dist/components/MapEditor/lib/element/abstract_path.d.ts +3 -2
  28. package/dist/components/MapEditor/lib/element/abstract_path.js +6 -3
  29. package/dist/components/MapEditor/lib/element/circle.d.ts +25 -0
  30. package/dist/components/MapEditor/lib/element/circle.js +118 -0
  31. package/dist/components/MapEditor/lib/element/line.d.ts +2 -2
  32. package/dist/components/MapEditor/lib/element/line.js +1 -1
  33. package/dist/components/MapEditor/lib/element/marker.d.ts +4 -3
  34. package/dist/components/MapEditor/lib/element/marker.js +2 -2
  35. package/dist/components/MapEditor/lib/element/polygon.d.ts +2 -2
  36. package/dist/components/MapEditor/lib/element/polygon.js +2 -2
  37. package/dist/components/MapEditor/lib/element/types.d.ts +2 -3
  38. package/dist/components/MapEditor/lib/geometry_manager.d.ts +12 -29
  39. package/dist/components/MapEditor/lib/geometry_manager.js +44 -160
  40. package/dist/components/MapEditor/lib/geometry_manager_interactive.d.ts +33 -0
  41. package/dist/components/MapEditor/lib/geometry_manager_interactive.js +102 -0
  42. package/dist/components/MapEditor/lib/map_layer/abstract.d.ts +2 -1
  43. package/dist/components/MapEditor/lib/map_layer/abstract.js +25 -22
  44. package/dist/components/MapEditor/lib/map_layer/fill.js +2 -4
  45. package/dist/components/MapEditor/lib/map_layer/line.js +1 -1
  46. package/dist/components/MapEditor/lib/map_layer/symbol.js +1 -1
  47. package/dist/components/MapEditor/lib/selection.d.ts +11 -0
  48. package/dist/components/MapEditor/lib/selection.js +70 -0
  49. package/dist/components/MapEditor/lib/state/constants.js +5 -6
  50. package/dist/components/MapEditor/lib/state/history.d.ts +14 -0
  51. package/dist/components/MapEditor/lib/state/history.js +53 -0
  52. package/dist/components/MapEditor/lib/state/manager.d.ts +7 -10
  53. package/dist/components/MapEditor/lib/state/manager.js +19 -54
  54. package/dist/components/MapEditor/lib/state/reader.d.ts +6 -4
  55. package/dist/components/MapEditor/lib/state/reader.js +70 -18
  56. package/dist/components/MapEditor/lib/state/types.d.ts +19 -2
  57. package/dist/components/MapEditor/lib/state/utils.d.ts +2 -0
  58. package/dist/components/MapEditor/lib/state/utils.js +12 -0
  59. package/dist/components/MapEditor/lib/state/writer.d.ts +6 -4
  60. package/dist/components/MapEditor/lib/state/writer.js +59 -19
  61. package/dist/components/MapEditor/lib/symbols.d.ts +1 -1
  62. package/dist/components/MapEditor/lib/symbols.js +47 -28
  63. package/dist/components/MapEditor/lib/utils/event_handler.d.ts +10 -0
  64. package/dist/components/MapEditor/lib/utils/event_handler.js +39 -0
  65. package/dist/components/MapEditor/lib/utils/geometry.d.ts +12 -0
  66. package/dist/components/MapEditor/lib/utils/geometry.js +87 -0
  67. package/dist/components/MapEditor/lib/utils/types.d.ts +2 -0
  68. package/dist/components/MapEditor/lib/utils/types.js +1 -0
  69. package/dist/components/MapEditor/style/button.scss +115 -0
  70. package/dist/components/MapEditor/style/index.scss +3 -0
  71. package/dist/components/MapEditor/style/layout.scss +20 -0
  72. package/dist/components/MapEditor/style/other.scss +10 -0
  73. package/dist/utils/location.d.ts +1 -0
  74. package/dist/utils/location.js +181 -0
  75. package/dist/utils/map_style.d.ts +2 -2
  76. package/dist/utils/map_style.js +2 -2
  77. package/package.json +29 -29
  78. package/dist/components/MapEditor/components/SymbolSelector.svelte +0 -110
  79. package/dist/components/MapEditor/components/SymbolSelector.svelte.d.ts +0 -8
  80. package/dist/components/MapEditor/lib/utils.d.ts +0 -6
  81. package/dist/components/MapEditor/lib/utils.js +0 -23
  82. /package/dist/components/MapEditor/lib/{geocoder.d.ts → utils/geocoder.d.ts} +0 -0
  83. /package/dist/components/MapEditor/lib/{geocoder.js → utils/geocoder.js} +0 -0
@@ -15,19 +15,15 @@
15
15
  let bboxes: { key: string; value: BBox }[] | undefined = $state();
16
16
  let mapContainer: HTMLElement;
17
17
 
18
- async function onMapLoad(_map: MaplibreMapType) {
18
+ async function onMapInit(_map: MaplibreMapType) {
19
19
  map = _map;
20
20
  mapContainer = map.getContainer();
21
- map.setPadding({ top: 31 + 5, right: 5, bottom: 5, left: 5 });
22
- bboxDrawer = new BBoxDrawer(
23
- map!,
24
- [-180, -86, 180, 86],
25
- isDarkMode(mapContainer) ? '#FFFFFF' : '#000000'
26
- );
21
+ map.setPadding({ top: 42, right: 10, bottom: 15, left: 10 });
22
+ bboxDrawer = new BBoxDrawer(map!, [-180, -85, 180, 85], isDarkMode(mapContainer) ? '#FFFFFF' : '#000000');
27
23
  bboxes = await loadBBoxes();
28
- bboxDrawer.bbox.subscribe((bbox) => {
29
- selectedBBox = bbox;
30
- });
24
+ // If an initial bbox is already provided by the parent, display it instead of guessing the user's country
25
+ if (selectedBBox) flyToBBox(selectedBBox);
26
+ bboxDrawer.bbox.subscribe((bbox) => (selectedBBox = bbox));
31
27
  }
32
28
 
33
29
  function flyToBBox(bbox: BBox) {
@@ -47,6 +43,21 @@
47
43
  map.flyTo({ ...transform, essential: true, speed: 5 });
48
44
  }
49
45
  }
46
+
47
+ function getInitialInputText() {
48
+ // When an initial bbox is supplied, we skip the country pre‑fill
49
+ if (selectedBBox) {
50
+ const area = (selectedBBox[2] - selectedBBox[0]) * (selectedBBox[3] - selectedBBox[1]);
51
+ if (area < 60000) return '';
52
+ }
53
+ let query = getCountryName() ?? '';
54
+ switch (query) {
55
+ case 'France':
56
+ query = 'France, métropolitaine';
57
+ break;
58
+ }
59
+ return query;
60
+ }
50
61
  </script>
51
62
 
52
63
  <div class="container">
@@ -56,11 +67,11 @@
56
67
  items={bboxes}
57
68
  placeholder="Find country, region or city …"
58
69
  change={(bbox) => flyToBBox(bbox)}
59
- initialInputText={getCountryName() ?? ''}
70
+ initialInputText={getInitialInputText()}
60
71
  />
61
72
  </div>
62
73
  {/if}
63
- <BasicMap {onMapLoad}></BasicMap>
74
+ <BasicMap {onMapInit} emptyStyle={true}></BasicMap>
64
75
  </div>
65
76
 
66
77
  <style>
@@ -9,7 +9,7 @@ export declare const DragPointMap: Map<DragPoint, {
9
9
  }>;
10
10
  export type BBox = [number, number, number, number];
11
11
  export declare class BBoxDrawer {
12
- private source?;
12
+ private sourceId;
13
13
  private dragPoint;
14
14
  private isDragging;
15
15
  private map;
@@ -1,6 +1,6 @@
1
1
  import { get, writable } from 'svelte/store';
2
2
  import maplibregl from 'maplibre-gl';
3
- const { LngLatBounds } = maplibregl;
3
+ import { getMapStyle } from '../../../utils/map_style.js';
4
4
  // prettier-ignore
5
5
  export const DragPointMap = new Map([
6
6
  ['n', { cursor: 'ns-resize', flipH: 'n', flipV: 's' }],
@@ -15,7 +15,7 @@ export const DragPointMap = new Map([
15
15
  ]);
16
16
  const worldBBox = [-180, -85, 180, 85];
17
17
  export class BBoxDrawer {
18
- source;
18
+ sourceId;
19
19
  dragPoint = false;
20
20
  isDragging = false;
21
21
  map;
@@ -26,27 +26,27 @@ export class BBoxDrawer {
26
26
  this.bbox = writable(bbox);
27
27
  this.inverted = inverted ?? true;
28
28
  this.map = map;
29
- const sourceId = 'bbox_' + Math.random().toString(36).slice(2);
30
- if (this.source)
31
- throw new Error('BBoxDrawer already added to map');
32
- map.addSource(sourceId, { type: 'geojson', data: this.getAsFeatureCollection() });
33
- map.addLayer({
29
+ this.sourceId = 'bbox_' + Math.random().toString(36).slice(2);
30
+ const style = getMapStyle();
31
+ style.transition = { duration: 0, delay: 0 };
32
+ style.sources[this.sourceId] = { type: 'geojson', data: this.getAsFeatureCollection() };
33
+ style.layers.push({
34
34
  id: 'bbox-line_' + Math.random().toString(36).slice(2),
35
35
  type: 'line',
36
- source: sourceId,
36
+ source: this.sourceId,
37
37
  filter: ['==', '$type', 'LineString'],
38
38
  layout: { 'line-cap': 'round', 'line-join': 'round' },
39
39
  paint: { 'line-color': color }
40
40
  });
41
- map.addLayer({
41
+ style.layers.push({
42
42
  id: 'bbox-fill_' + Math.random().toString(36).slice(2),
43
43
  type: 'fill',
44
- source: sourceId,
44
+ source: this.sourceId,
45
45
  filter: ['==', '$type', 'Polygon'],
46
46
  layout: {},
47
47
  paint: { 'fill-color': color, 'fill-opacity': 0.2 }
48
48
  });
49
- this.source = map.getSource(sourceId);
49
+ map.setStyle(style);
50
50
  this.canvas = map.getCanvasContainer();
51
51
  map.on('mousemove', (e) => {
52
52
  if (e.originalEvent.buttons % 2 === 0)
@@ -85,10 +85,7 @@ export class BBoxDrawer {
85
85
  const ring = getRing(get(this.bbox));
86
86
  return {
87
87
  type: 'FeatureCollection',
88
- features: [
89
- this.inverted ? polygon(getRing(worldBBox), ring) : polygon(ring),
90
- linestring(ring)
91
- ]
88
+ features: [this.inverted ? polygon(getRing(worldBBox), ring) : polygon(ring), linestring(ring)]
92
89
  };
93
90
  function getRing(bbox) {
94
91
  const x0 = Math.min(bbox[0], bbox[2]);
@@ -110,10 +107,15 @@ export class BBoxDrawer {
110
107
  this.redraw();
111
108
  }
112
109
  getBounds() {
113
- return new LngLatBounds(get(this.bbox));
110
+ return new maplibregl.LngLatBounds(get(this.bbox));
114
111
  }
115
112
  redraw() {
116
- this.source?.setData(this.getAsFeatureCollection());
113
+ const source = this.map.getSource(this.sourceId);
114
+ if (!source)
115
+ return;
116
+ if (source instanceof maplibregl.GeoJSONSource) {
117
+ source.setData(this.getAsFeatureCollection());
118
+ }
117
119
  }
118
120
  getAsPixel() {
119
121
  const bbox = get(this.bbox);
@@ -10,18 +10,21 @@
10
10
  styleOptions = { transitionDuration: 0 },
11
11
  mapOptions = {},
12
12
  map = $bindable(),
13
+ emptyStyle = false,
13
14
  onMapInit,
14
15
  onMapLoad
15
16
  }: {
16
17
  style?: string;
17
- styleOptions?: Parameters<typeof getMapStyle>[1];
18
+ styleOptions?: Parameters<typeof getMapStyle>[0];
18
19
  mapOptions?: Partial<MapOptions>;
19
20
  map?: maplibre.Map;
21
+ emptyStyle?: boolean;
20
22
  onMapInit?: (map: maplibre.Map, maplibre: typeof import('maplibre-gl')) => void;
21
23
  onMapLoad?: (map: maplibre.Map, maplibre: typeof import('maplibre-gl')) => void;
22
24
  } = $props();
23
25
 
24
26
  let container: HTMLDivElement;
27
+ let triggeredMapReady = false;
25
28
 
26
29
  $effect(() => {
27
30
  if (container) init();
@@ -30,14 +33,18 @@
30
33
  async function init(): Promise<void> {
31
34
  if (map) return;
32
35
 
33
- if (!container) throw Error();
36
+ if (styleOptions.darkMode == null) styleOptions.darkMode = isDarkMode(container);
34
37
 
35
- const darkMode = isDarkMode(container);
36
- container.style.setProperty('--bg-color', darkMode ? '#000' : '#fff');
37
- container.style.setProperty('--fg-color', darkMode ? '#fff' : '#000');
38
+ container.style.setProperty('--bg-color', styleOptions.darkMode ? '#000' : '#fff');
39
+ container.style.setProperty('--fg-color', styleOptions.darkMode ? '#fff' : '#000');
40
+
41
+ let style = undefined;
42
+
43
+ if (!emptyStyle) {
44
+ style = getMapStyle(styleOptions);
45
+ style.transition = { duration: 0, delay: 0 };
46
+ }
38
47
 
39
- const style = getMapStyle(darkMode, styleOptions);
40
- style.transition = { duration: 0, delay: 0 };
41
48
  map = new maplibre.Map({
42
49
  container,
43
50
  style,
@@ -50,9 +57,15 @@
50
57
 
51
58
  if (onMapInit) onMapInit(map, maplibre);
52
59
 
53
- map.on('load', () => {
60
+ map.on('idle', checkMapReady);
61
+
62
+ function checkMapReady() {
63
+ if (triggeredMapReady) return;
64
+ if (!map!.loaded()) return;
65
+ triggeredMapReady = true;
54
66
  if (onMapLoad) onMapLoad(map!, maplibre);
55
- });
67
+ setTimeout(() => console.log('map_ready'), 100);
68
+ }
56
69
  }
57
70
  </script>
58
71
 
@@ -4,9 +4,10 @@ import maplibre from 'maplibre-gl';
4
4
  import type { MapOptions } from 'maplibre-gl';
5
5
  type $$ComponentProps = {
6
6
  style?: string;
7
- styleOptions?: Parameters<typeof getMapStyle>[1];
7
+ styleOptions?: Parameters<typeof getMapStyle>[0];
8
8
  mapOptions?: Partial<MapOptions>;
9
9
  map?: maplibre.Map;
10
+ emptyStyle?: boolean;
10
11
  onMapInit?: (map: maplibre.Map, maplibre: typeof import('maplibre-gl')) => void;
11
12
  onMapLoad?: (map: maplibre.Map, maplibre: typeof import('maplibre-gl')) => void;
12
13
  };
@@ -1,31 +1,50 @@
1
1
  <script lang="ts">
2
2
  import type { Map as MaplibreMapType } from 'maplibre-gl';
3
3
  import BasicMap from '../BasicMap/BasicMap.svelte';
4
- import { onMount } from 'svelte';
5
4
  import Sidebar from './components/Sidebar.svelte';
6
- import type { GeometryManager } from './lib/geometry_manager.js';
5
+ import { getCountryBoundingBox } from '../../utils/location.js';
6
+ import { GeometryManager } from './lib/geometry_manager.js';
7
+ import { GeometryManagerInteractive } from './lib/geometry_manager_interactive.js';
8
+ import { StateReader } from './lib/state/reader.js';
7
9
 
8
10
  let showSidebar = $state(false);
11
+ let geometryManager: GeometryManager | GeometryManagerInteractive | undefined = $state();
9
12
 
10
- onMount(() => {
11
- const inIframe = window.self !== window.top;
12
- if (!inIframe) showSidebar = true;
13
- });
13
+ function onMapInit(map: MaplibreMapType, maplibre: typeof import('maplibre-gl')) {
14
+ showSidebar = window.self === window.top;
14
15
 
15
- let map: MaplibreMapType | undefined = $state();
16
- let geometryManager: GeometryManager | undefined = $state();
16
+ const padding = 10;
17
+ map.setPadding({
18
+ top: padding,
19
+ right: padding + (showSidebar ? 250 : 0),
20
+ bottom: padding,
21
+ left: padding
22
+ });
17
23
 
18
- function onMapInit(_map: MaplibreMapType, maplibre: typeof import('maplibre-gl')) {
19
- map = _map;
20
24
  map.addControl(new maplibre.AttributionControl({ compact: true }), 'bottom-left');
21
25
 
22
- map.on('load', async () => {
23
- const { GeometryManager } = await import('./lib/geometry_manager.js');
24
- geometryManager = new GeometryManager(map!);
25
- const hash = location.hash.slice(1);
26
- if (hash) geometryManager.state.setHash(hash);
27
- addEventListener('hashchange', () => geometryManager!.state.setHash(location.hash.slice(1)));
28
- });
26
+ if (showSidebar) {
27
+ geometryManager = new GeometryManagerInteractive(map);
28
+ } else {
29
+ geometryManager = new GeometryManager(map);
30
+ }
31
+
32
+ let hash = location.hash.slice(1);
33
+ if (!hash) hash = window.frameElement?.getAttribute('data') ?? '';
34
+ if (hash) {
35
+ readHash(hash);
36
+ } else {
37
+ const bbox = getCountryBoundingBox();
38
+ if (bbox) map.fitBounds(bbox, { animate: false });
39
+ }
40
+
41
+ addEventListener('hashchange', () => readHash(location.hash.slice(1)));
42
+
43
+ function readHash(hash: string) {
44
+ if (!geometryManager) return;
45
+ const stateReader = StateReader.fromBase64(hash);
46
+ geometryManager.loadState(stateReader.readRoot());
47
+ }
29
48
  }
30
49
  </script>
31
50
 
@@ -33,12 +52,13 @@
33
52
  <div class="container">
34
53
  <BasicMap
35
54
  {onMapInit}
36
- styleOptions={{ disableDarkMode: true }}
55
+ emptyStyle={true}
37
56
  mapOptions={{ attributionControl: false }}
57
+ styleOptions={{ darkMode: false }}
38
58
  ></BasicMap>
39
59
  </div>
40
- {#if showSidebar && geometryManager}
41
- <Sidebar {geometryManager} width={200} />
60
+ {#if showSidebar && geometryManager && geometryManager.isInteractive()}
61
+ <Sidebar {geometryManager} />
42
62
 
43
63
  <style>
44
64
  .page .container {
@@ -52,6 +72,16 @@
52
72
  </div>
53
73
 
54
74
  <style>
75
+ .page {
76
+ --color-blue: #158;
77
+ --color-green: #1a1;
78
+ --color-bg: #fff;
79
+ --color-text: #000;
80
+ --btn-gap: 5px;
81
+ --gap: 10px;
82
+ --border-radius: 1em;
83
+ }
84
+
55
85
  .page,
56
86
  .container {
57
87
  width: 100%;
@@ -59,6 +89,7 @@
59
89
  position: relative;
60
90
  min-height: 6em;
61
91
  }
92
+
62
93
  :global(.maplibregl-ctrl-attrib) {
63
94
  display: flex;
64
95
  align-items: center;
@@ -0,0 +1,92 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { EventHandler } from '../lib/utils/event_handler.js';
4
+
5
+ let {
6
+ children = $bindable(),
7
+ size,
8
+ onopen,
9
+ onclose
10
+ }: {
11
+ children?: Snippet;
12
+ size?: 'big' | 'fullscreen' | 'small';
13
+ onopen?: () => void;
14
+ onclose?: () => void;
15
+ } = $props();
16
+
17
+ let dialog: HTMLDialogElement | null = null;
18
+ export const eventHandler = new EventHandler();
19
+
20
+ export function getNode(): HTMLDialogElement {
21
+ return dialog!;
22
+ }
23
+
24
+ export function open() {
25
+ dialog?.showModal();
26
+ if (onopen) onopen();
27
+ eventHandler.emit('open');
28
+ }
29
+
30
+ export function close() {
31
+ dialog?.close();
32
+ if (onclose) onclose();
33
+ eventHandler.emit('close');
34
+ }
35
+
36
+ export function isOpen(): boolean {
37
+ return dialog?.open ?? false;
38
+ }
39
+ </script>
40
+
41
+ <dialog bind:this={dialog} class={size}>
42
+ <button onclick={close}>&#x2715;</button>
43
+ {@render children?.()}
44
+ </dialog>
45
+
46
+ <style>
47
+ dialog {
48
+ max-width: 100vw;
49
+ max-height: 100vh;
50
+ min-width: 300px;
51
+ min-height: 100px;
52
+ width: 80vw;
53
+ height: 80vh;
54
+
55
+ background-color: rgba(255, 255, 255, 0.8);
56
+ backdrop-filter: blur(10px);
57
+ box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
58
+ z-index: 10000;
59
+ border: 0.5px solid rgba(0, 0, 0, 0.3);
60
+ border-radius: 10px;
61
+ box-sizing: border-box;
62
+ padding: 20px;
63
+
64
+ &.fullscreen {
65
+ width: calc(100vw - 20px);
66
+ height: calc(100vh - 20px);
67
+ }
68
+
69
+ &.small {
70
+ width: fit-content;
71
+ height: fit-content;
72
+ }
73
+ }
74
+
75
+ dialog::backdrop {
76
+ backdrop-filter: blur(2px) brightness(0.9);
77
+ }
78
+
79
+ button {
80
+ position: absolute;
81
+ top: 5px;
82
+ right: 5px;
83
+ font-size: 20px;
84
+ cursor: pointer;
85
+ background: none;
86
+ border: none;
87
+ width: 25px;
88
+ height: 25px;
89
+ text-align: center;
90
+ padding: 0;
91
+ }
92
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { EventHandler } from '../lib/utils/event_handler.js';
3
+ type $$ComponentProps = {
4
+ children?: Snippet;
5
+ size?: 'big' | 'fullscreen' | 'small';
6
+ onopen?: () => void;
7
+ onclose?: () => void;
8
+ };
9
+ declare const Dialog: import("svelte").Component<$$ComponentProps, {
10
+ eventHandler: EventHandler;
11
+ getNode: () => HTMLDialogElement;
12
+ open: () => void;
13
+ close: () => void;
14
+ isOpen: () => boolean;
15
+ }, "children">;
16
+ type Dialog = ReturnType<typeof Dialog>;
17
+ export default Dialog;
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import Dialog from './Dialog.svelte';
4
+ import { EventHandler } from '../lib/utils/event_handler.js';
5
+
6
+ type Mode = 'download' | 'new' | null;
7
+ let mode: Mode = $state(null);
8
+ let dialog: Dialog | null = null;
9
+ let input: HTMLInputElement | null = $state(null);
10
+ let eventHandler = new EventHandler();
11
+
12
+ async function openDialog(newMode: Mode) {
13
+ if (!dialog) return;
14
+ mode = newMode;
15
+
16
+ dialog.eventHandler.clear();
17
+ eventHandler.clear();
18
+
19
+ dialog.open();
20
+ await tick();
21
+ dialog.getNode()?.querySelector<HTMLButtonElement>('button[data-focus]')?.focus();
22
+ return;
23
+ }
24
+
25
+ async function closeDialog() {
26
+ dialog?.close();
27
+ dialog?.eventHandler.clear();
28
+ eventHandler.clear();
29
+ mode = null;
30
+ return tick();
31
+ }
32
+
33
+ export async function askDownloadFilename(initialFilename: string): Promise<string | null> {
34
+ if (!dialog) return null;
35
+ await openDialog('download');
36
+ initInput(initialFilename);
37
+ const { response, value } = await getResponse(true);
38
+ return (response && value?.trim()) || null;
39
+ }
40
+
41
+ export async function askCreateNew(): Promise<boolean> {
42
+ if (!dialog) return false;
43
+ await openDialog('new');
44
+ const { response } = await getResponse(false);
45
+ return response;
46
+ }
47
+
48
+ async function getResponse(defaultValue: boolean): Promise<{ response: boolean; value: string | null }> {
49
+ const response = await new Promise<boolean>((resolve) => {
50
+ if (!dialog) return resolve(false);
51
+ dialog!.eventHandler.on('close', () => resolve(false));
52
+ eventHandler.on('A', () => resolve(!defaultValue));
53
+ eventHandler.on('B', () => resolve(defaultValue));
54
+ });
55
+ const value = input?.value ?? null;
56
+ await closeDialog();
57
+ return { response, value };
58
+ }
59
+
60
+ function emitA() {
61
+ eventHandler.emit('A');
62
+ }
63
+
64
+ function emitB() {
65
+ eventHandler.emit('B');
66
+ }
67
+
68
+ function initInput(value: string) {
69
+ if (!input) return;
70
+ input.value = value;
71
+ input.addEventListener('keypress', (e) => {
72
+ if (e.key === 'Enter') eventHandler.emit('B');
73
+ });
74
+ }
75
+ </script>
76
+
77
+ <Dialog bind:this={dialog} size="small">
78
+ {#if mode == 'download'}
79
+ <h2>Download File</h2>
80
+ <label>
81
+ File name:
82
+ <input type="text" bind:this={input} spellcheck="false" />
83
+ </label>
84
+ <div class="grid2">
85
+ <button class="btn" onclick={emitA}>Cancel</button>
86
+ <button class="btn" onclick={emitB} data-focus>Download</button>
87
+ </div>
88
+ {/if}
89
+ {#if mode == 'new'}
90
+ <h2>New Map</h2>
91
+ <p>Do you want to create a new map?</p>
92
+ <div class="grid2">
93
+ <button class="btn" onclick={emitA}>OK</button>
94
+ <button class="btn" onclick={emitB} data-focus>Cancel</button>
95
+ </div>
96
+ {/if}
97
+ </Dialog>
98
+
99
+ <style>
100
+ h2 {
101
+ text-align: center;
102
+ margin-top: 0;
103
+ }
104
+ .grid2 {
105
+ margin: 0;
106
+ }
107
+ label,
108
+ p {
109
+ display: block;
110
+ margin-bottom: 10px;
111
+ }
112
+ </style>
@@ -0,0 +1,6 @@
1
+ declare const DialogFile: import("svelte").Component<Record<string, never>, {
2
+ askDownloadFilename: (initialFilename: string) => Promise<string | null>;
3
+ askCreateNew: () => Promise<boolean>;
4
+ }, "">;
5
+ type DialogFile = ReturnType<typeof DialogFile>;
6
+ export default DialogFile;