@fusion.dev/helix-map 0.115.0 → 0.117.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.
@@ -42,6 +42,15 @@ export interface MapDriver {
42
42
  * `:state(ready)`.
43
43
  */
44
44
  ready?(done: () => void): () => void;
45
+ /**
46
+ * Which gestures the renderer takes from the page, as the frame's
47
+ * `gestures` decides. `free` is what the renderer did before it was told
48
+ * otherwise. `cooperative` leaves the wheel and one finger to the page and
49
+ * takes the wheel with Ctrl or ⌘, and two fingers. `off` takes none, while a
50
+ * map waiting to be pressed has not been. The frame tells the reader what
51
+ * passed to the page, so hide the renderer's own message where it has one.
52
+ */
53
+ gestures?(mode: 'cooperative' | 'free' | 'off'): void;
45
54
  /**
46
55
  * The element the renderer made focusable, which the connector takes out of
47
56
  * the tab order while connected so the frame's region is the one stop.
@@ -792,8 +792,21 @@ export const connectMap = (frame, driver, options = {}) => {
792
792
  queueMicrotask(flush);
793
793
  }
794
794
  };
795
+ let gestured = 'free';
796
+ const tellGestures = () => {
797
+ const mode = frame.gestureMode ?? 'free';
798
+ if (mode !== gestured) {
799
+ gestured = mode;
800
+ driver.gestures?.(mode);
801
+ }
802
+ };
803
+ tellGestures();
795
804
  const handlePartChange = (event) => {
796
805
  const { kind } = event.detail;
806
+ if (kind === 'gestures') {
807
+ tellGestures();
808
+ return;
809
+ }
797
810
  if (kind === 'menu') {
798
811
  drawPreviews();
799
812
  return;
@@ -905,6 +918,9 @@ export const connectMap = (frame, driver, options = {}) => {
905
918
  clearTimeout(looking);
906
919
  stopWaiting?.();
907
920
  frame.drawn?.(false);
921
+ if (gestured !== 'free') {
922
+ driver.gestures?.('free');
923
+ }
908
924
  stopTheming?.();
909
925
  styled.disconnect();
910
926
  children.disconnect();
@@ -21,6 +21,12 @@ export interface LeafletAttribution {
21
21
  };
22
22
  remove(): unknown;
23
23
  }
24
+ /** One of Leaflet's gesture handlers, as far as the driver switches it. */
25
+ export interface LeafletHandler {
26
+ disable(): unknown;
27
+ enable(): unknown;
28
+ enabled(): boolean;
29
+ }
24
30
  /**
25
31
  * The part of a Leaflet `Map` the driver uses, so this package imports no
26
32
  * renderer and a page brings whichever version it has.
@@ -58,6 +64,22 @@ export interface LeafletMap {
58
64
  } | undefined;
59
65
  /** How far the view is zoomed in. */
60
66
  getZoom?(): number;
67
+ /** The wheel's zoom, one of the handlers a frame's `gestures` switches. */
68
+ scrollWheelZoom?: LeafletHandler | undefined;
69
+ /** Dragging the map, by a mouse or one finger. */
70
+ dragging?: LeafletHandler | undefined;
71
+ /** Two fingers zooming the map. */
72
+ touchZoom?: LeafletHandler | undefined;
73
+ /** The place under a pointer, which a zoom with the wheel is made round. */
74
+ mouseEventToLatLng?(event: MouseEvent): {
75
+ lat: number;
76
+ lng: number;
77
+ };
78
+ /** Zooms keeping a place where it is on screen. */
79
+ setZoomAround?(latlng: {
80
+ lat: number;
81
+ lng: number;
82
+ }, zoom: number): unknown;
61
83
  zoomIn(delta?: number, options?: {
62
84
  animate?: boolean;
63
85
  }): unknown;
@@ -1,4 +1,5 @@
1
1
  const animation = (motion) => motion ? { animate: true, duration: motion.duration / 1000 } : { animate: false };
2
+ const gestureState = new WeakMap();
2
3
  export const leafletDriver = (map, shapes) => ({
3
4
  credits(changed) {
4
5
  const control = map.attributionControl;
@@ -38,6 +39,53 @@ export const leafletDriver = (map, shapes) => ({
38
39
  });
39
40
  },
40
41
  focusTarget: () => map.getContainer(),
42
+ gestures: (mode) => {
43
+ const handlers = { drag: map.dragging, scroll: map.scrollWheelZoom, touch: map.touchZoom };
44
+ const state = gestureState.get(map) ?? {
45
+ had: {
46
+ drag: handlers.drag?.enabled() ?? false,
47
+ scroll: handlers.scroll?.enabled() ?? false,
48
+ touch: handlers.touch?.enabled() ?? false,
49
+ },
50
+ };
51
+ gestureState.set(map, state);
52
+ const finger = typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches;
53
+ const want = mode === 'free'
54
+ ? state.had
55
+ : mode === 'cooperative'
56
+ ? { drag: !finger, scroll: false, touch: true }
57
+ : { drag: false, scroll: false, touch: false };
58
+ for (const name of ['drag', 'scroll', 'touch']) {
59
+ const handler = handlers[name];
60
+ if (handler && handler.enabled() !== want[name]) {
61
+ if (want[name]) {
62
+ handler.enable();
63
+ }
64
+ else {
65
+ handler.disable();
66
+ }
67
+ }
68
+ }
69
+ const box = map.getContainer();
70
+ if (state.wheel) {
71
+ box.removeEventListener('wheel', state.wheel);
72
+ state.wheel = undefined;
73
+ }
74
+ if (mode === 'cooperative' && map.mouseEventToLatLng && map.setZoomAround && map.getZoom) {
75
+ state.wheel = (event) => {
76
+ if (!(event.ctrlKey || event.metaKey)) {
77
+ return;
78
+ }
79
+ event.preventDefault();
80
+ const at = map.mouseEventToLatLng?.(event);
81
+ const zoom = map.getZoom?.();
82
+ if (at && zoom !== undefined) {
83
+ map.setZoomAround?.(at, zoom - Math.sign(event.deltaY) * 0.5);
84
+ }
85
+ };
86
+ box.addEventListener('wheel', state.wheel, { passive: false });
87
+ }
88
+ },
41
89
  onView: (moved) => {
42
90
  map.on('move zoom resize', moved);
43
91
  return () => {
@@ -105,6 +105,12 @@ interface MapLibreAnimation {
105
105
  duration?: number;
106
106
  easing?: (t: number) => number;
107
107
  }
108
+ /** One of MapLibre's gesture handlers, as far as the driver switches it. */
109
+ export interface MapLibreHandler {
110
+ disable(): unknown;
111
+ enable(): unknown;
112
+ isEnabled(): boolean;
113
+ }
108
114
  /**
109
115
  * The part of a MapLibre `Map` the driver uses, so this package imports no
110
116
  * renderer and a page brings whichever version it has.
@@ -123,6 +129,14 @@ export interface MapLibreMap {
123
129
  isSourceLoaded?(id: string): boolean;
124
130
  /** The canvas, which MapLibre makes focusable. */
125
131
  getCanvas(): HTMLElement;
132
+ /** The wheel's zoom, one of the handlers a frame's `gestures` switches. */
133
+ scrollZoom?: MapLibreHandler;
134
+ /** Dragging the map, by a mouse or one finger. */
135
+ dragPan?: MapLibreHandler;
136
+ /** Two fingers zooming and turning the map. */
137
+ touchZoomRotate?: MapLibreHandler;
138
+ /** MapLibre's own cooperative gestures: Ctrl or ⌘ with the wheel, and two fingers. */
139
+ cooperativeGestures?: MapLibreHandler;
126
140
  /** The middle of the view, which a map-type picture is drawn around. */
127
141
  getCenter?(): {
128
142
  lat: number;
@@ -287,6 +287,7 @@ const answerImages = (holder) => {
287
287
  return () => holder.off?.('styleimagemissing', missing);
288
288
  };
289
289
  const PICTURE = { height: 90, width: 120 };
290
+ const originalGestures = new WeakMap();
290
291
  export const mapLibreDriver = (map) => ({
291
292
  bearing: () => map.getBearing(),
292
293
  behind: ({ lat, lng }) => (map.transform ?? map._camera?.transform)?.isLocationOccluded?.({ lat, lng }) === true,
@@ -323,6 +324,38 @@ export const mapLibreDriver = (map) => ({
323
324
  ], { ...animation(motion), padding });
324
325
  },
325
326
  focusTarget: () => map.getCanvas(),
327
+ gestures: (mode) => {
328
+ const handlers = {
329
+ cooperative: map.cooperativeGestures,
330
+ drag: map.dragPan,
331
+ scroll: map.scrollZoom,
332
+ touch: map.touchZoomRotate,
333
+ };
334
+ const had = (originalGestures.get(map) ??
335
+ Object.fromEntries(Object.entries(handlers).map(([name, handler]) => [name, handler?.isEnabled() ?? false])));
336
+ originalGestures.set(map, had);
337
+ const want = mode === 'free'
338
+ ? had
339
+ : mode === 'cooperative'
340
+ ? { cooperative: true, drag: true, scroll: true, touch: true }
341
+ : { cooperative: false, drag: false, scroll: false, touch: false };
342
+ for (const [name, handler] of Object.entries(handlers)) {
343
+ if (handler && handler.isEnabled() !== want[name]) {
344
+ if (want[name]) {
345
+ handler.enable();
346
+ }
347
+ else {
348
+ handler.disable();
349
+ }
350
+ }
351
+ }
352
+ const screen = map
353
+ .getContainer()
354
+ .querySelector('.maplibregl-cooperative-gesture-screen');
355
+ if (screen) {
356
+ screen.style.display = 'none';
357
+ }
358
+ },
326
359
  labels: (shown) => {
327
360
  const heard = bared.has(map);
328
361
  bared.set(map, !shown);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-map",
3
3
  "description": "Connects the Helix Map frame to a map renderer: MapLibre, Leaflet, or a driver of your own.",
4
- "version": "0.115.0",
4
+ "version": "0.117.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -43,14 +43,14 @@
43
43
  },
44
44
  "bugs": "https://github.com/project-fusion/fusion/issues",
45
45
  "dependencies": {
46
- "@fusion.dev/helix-core": "0.115.0",
47
- "@fusion.dev/helix-icons": "0.115.0",
46
+ "@fusion.dev/helix-core": "0.117.0",
47
+ "@fusion.dev/helix-icons": "0.117.0",
48
48
  "@mapbox/vector-tile": "^3.0.0",
49
49
  "pbf": "^5.1.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@fusion.dev/helix-headless": "0.115.0",
53
- "@fusion.dev/helix-testing": "0.115.0",
52
+ "@fusion.dev/helix-headless": "0.117.0",
53
+ "@fusion.dev/helix-testing": "0.117.0",
54
54
  "@maplibre/maplibre-gl-style-spec": "^26.4.4",
55
55
  "@types/bun": "^1.3.0",
56
56
  "@types/leaflet": "^1.9.20",