@defra/interactive-map 0.0.43-alpha → 0.0.44-alpha

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 (159) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api.md +16 -8
  7. package/docs/plugins/draw.md +1 -1
  8. package/package.json +1 -1
  9. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  10. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  11. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  12. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  13. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  14. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  15. package/plugins/datasets/dist/css/index.css +1 -1
  16. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  17. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  18. package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
  19. package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
  20. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
  21. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  22. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  23. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  24. package/plugins/draw/dist/esm/index.js +1 -1
  25. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
  26. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  27. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  28. package/plugins/draw/dist/umd/index.js +1 -1
  29. package/plugins/draw/src/DrawInit.jsx +31 -26
  30. package/plugins/draw/src/DrawInit.test.jsx +55 -12
  31. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
  32. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
  33. package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
  34. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
  35. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
  36. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
  37. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
  38. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +8 -2
  39. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +35 -0
  40. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
  41. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
  42. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
  43. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
  44. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
  45. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
  46. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
  47. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
  48. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
  49. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
  51. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
  52. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
  53. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
  54. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
  55. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
  56. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
  57. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
  58. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
  59. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
  60. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
  61. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
  62. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
  63. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
  64. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
  65. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
  66. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
  67. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
  68. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
  69. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
  70. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +31 -17
  71. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
  72. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
  73. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
  74. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
  75. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
  76. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
  77. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
  78. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
  79. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
  80. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +32 -57
  81. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +14 -13
  82. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
  83. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
  84. package/plugins/draw/src/events.js +4 -4
  85. package/plugins/draw/src/events.test.js +1 -1
  86. package/plugins/draw/src/hooks/useSpatialList.js +180 -0
  87. package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
  88. package/plugins/draw/src/utils/spatial.js +1 -31
  89. package/plugins/draw/src/utils/spatial.test.js +1 -24
  90. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  91. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  92. package/plugins/interact/dist/umd/index.js +1 -1
  93. package/plugins/interact/src/InteractInit.jsx +2 -2
  94. package/plugins/interact/src/InteractInit.test.js +3 -3
  95. package/plugins/interact/src/events.js +5 -0
  96. package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
  97. package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
  98. package/plugins/interact/src/hooks/useCrossHairVisibility.js +4 -4
  99. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
  100. package/plugins/interact/src/hooks/{useMapItemList.js → useSpatialList.js} +87 -52
  101. package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +152 -71
  102. package/plugins/interact/src/manifest.js +12 -1
  103. package/plugins/interact/src/manifest.test.js +20 -0
  104. package/plugins/interact/src/utils/spatial.js +1 -1
  105. package/providers/beta/openlayers/src/utils/queryFeatures.js +1 -1
  106. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  107. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  108. package/providers/maplibre/dist/umd/index.js +1 -1
  109. package/providers/maplibre/src/utils/labels.js +2 -2
  110. package/providers/maplibre/src/utils/labels.test.js +5 -5
  111. package/providers/maplibre/src/utils/spatial.js +0 -57
  112. package/providers/maplibre/src/utils/spatial.test.js +0 -27
  113. package/src/App/components/CrossHair/CrossHair.jsx +10 -4
  114. package/src/App/components/CrossHair/CrossHair.module.scss +16 -12
  115. package/src/App/components/CrossHair/CrossHair.test.jsx +9 -4
  116. package/src/App/components/Hints/Hints.jsx +3 -3
  117. package/src/App/components/Hints/Hints.test.jsx +3 -3
  118. package/src/App/components/MapButton/MapButton.module.scss +1 -2
  119. package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
  120. package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
  121. package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
  122. package/src/App/components/Markers/Markers.jsx +2 -2
  123. package/src/App/components/Markers/Markers.test.jsx +3 -3
  124. package/src/App/components/Markers/SymbolMarker.test.jsx +2 -2
  125. package/src/App/components/Viewport/MapStatus.jsx +1 -1
  126. package/src/App/components/Viewport/SpatialList.jsx +47 -0
  127. package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +3 -3
  128. package/src/App/components/Viewport/{Features.test.jsx → SpatialList.test.jsx} +60 -43
  129. package/src/App/components/Viewport/Viewport.jsx +20 -10
  130. package/src/App/components/Viewport/Viewport.test.jsx +18 -2
  131. package/src/App/hooks/useCrossHairAPI.js +5 -5
  132. package/src/App/hooks/useCrossHairAPI.test.js +5 -5
  133. package/src/App/hooks/useHintsAPI.js +5 -5
  134. package/src/App/hooks/useSpatialListFocus.js +296 -0
  135. package/src/App/hooks/{useFeatureFocus.test.js → useSpatialListFocus.test.js} +318 -163
  136. package/src/App/hooks/useSpatialListItems.js +51 -0
  137. package/src/App/hooks/useSpatialListItems.test.js +217 -0
  138. package/src/App/initialiseApp.js +6 -3
  139. package/src/App/initialiseApp.test.js +11 -0
  140. package/src/App/registry/spatialListRegistry.js +104 -0
  141. package/src/App/registry/spatialListRegistry.test.js +218 -0
  142. package/src/App/store/AppProvider.jsx +3 -2
  143. package/src/config/appConfig.js +14 -14
  144. package/src/config/appConfig.test.js +19 -19
  145. package/src/config/defaults.js +2 -2
  146. package/src/config/events.js +8 -8
  147. package/src/scss/main.scss +2 -2
  148. package/src/test-utils.js +8 -0
  149. package/src/types.js +11 -9
  150. package/src/utils/findNearestItemInDirection.js +29 -0
  151. package/src/utils/findNearestItemInDirection.test.js +67 -0
  152. package/src/utils/globalAltShortcuts.js +12 -0
  153. package/src/utils/globalAltShortcuts.test.js +27 -0
  154. package/src/utils/spatialNavigate.js +49 -0
  155. package/src/utils/spatialNavigate.test.js +42 -0
  156. package/src/App/components/Viewport/Features.jsx +0 -49
  157. package/src/App/hooks/useFeatureFocus.js +0 -257
  158. package/src/App/hooks/useFeatureItems.js +0 -39
  159. package/src/App/hooks/useFeatureItems.test.js +0 -155
@@ -6,17 +6,28 @@ import { ADAPTER_EVENTS } from '../../adapterEvents.js'
6
6
  import { createLiveStroke } from '../../validation/liveStroke.js'
7
7
  import { createLiveDrawChecks } from '../../validation/liveDrawChecks.js'
8
8
  import { resolvePointSymbol, hasSymbolStyle } from './pointSymbolImages.js'
9
+ import { getCoords, getMidpointCoords } from './modes/editVertexMode/geometryHelpers.js'
9
10
 
10
11
  const polygonFeature = (coordinates) => ({ type: 'Feature', geometry: { type: 'Polygon', coordinates } })
11
12
  const lineFeature = (coordinates) => ({ type: 'Feature', geometry: { type: 'LineString', coordinates } })
12
13
  const pointFeature = (coordinates) => ({ type: 'Feature', geometry: { type: 'Point', coordinates } })
13
14
 
14
- // The displayed feature + placed-vertex count for the live stroke check. MapLibre's
15
- // fire() copies the payload onto an Event whose `type` is the event name, so the
16
- // geometry type is clobbered it comes from the draw mode, or (in edit, where the
17
- // mode covers both shapes) from the coordinate nesting: a polygon's coordinates are
18
- // rings (one level deeper than a line's). Draw-mode coordinates carry a trailing
19
- // rubber-band point; edit-mode coordinates are all committed vertices.
15
+ // mapbox-gl-draw's public get() returns closed rings (first coord repeated as last), but the
16
+ // running mode's own internal indexing has that duplicate stripped getVertexItems() below
17
+ // reads via the public API, so it must correct for this or indexes drift across rings/parts.
18
+ const dedupeClosedRings = (geometry) => {
19
+ if (geometry.type === 'Polygon') {
20
+ return { ...geometry, coordinates: geometry.coordinates.map(ring => ring.slice(0, -1)) }
21
+ }
22
+ if (geometry.type === 'MultiPolygon') {
23
+ return { ...geometry, coordinates: geometry.coordinates.map(poly => poly.map(ring => ring.slice(0, -1))) }
24
+ }
25
+ return geometry
26
+ }
27
+
28
+ // The displayed feature + vertex count for the live stroke check. MapLibre's fire() clobbers
29
+ // the payload's own geometry type, so it's inferred instead from the mode, or (in edit mode,
30
+ // which covers both shapes) from the coordinate nesting.
20
31
  export const displayedShape = (mode, coordinates) => {
21
32
  if (mode === 'draw_polygon') {
22
33
  return { feature: polygonFeature(coordinates), numVertices: (coordinates[0]?.length ?? 1) - 1 }
@@ -25,10 +36,7 @@ export const displayedShape = (mode, coordinates) => {
25
36
  return { feature: lineFeature(coordinates), numVertices: (coordinates?.length ?? 1) - 1 }
26
37
  }
27
38
  if (mode === 'edit_vertex') {
28
- // Read the outer ring once — Array.isArray(ring?.[0]) already proves `ring` itself is
29
- // non-nullish whenever it's true, so the polygon branch can use it directly with no
30
- // second optional-chaining/fallback (which `coordinates[0]` re-read a second time,
31
- // unreachably, used to need).
39
+ // Array.isArray(ring?.[0]) already proves ring is non-nullish, so the polygon branch below reuses it directly.
32
40
  const ring = coordinates[0]
33
41
  return Array.isArray(ring?.[0])
34
42
  ? { feature: polygonFeature(coordinates), numVertices: ring.length }
@@ -53,6 +61,7 @@ export const displayedShape = (mode, coordinates) => {
53
61
  * setInterfaceType(type)
54
62
  * done() / cancel() / undo() / deleteVertex()
55
63
  * nudgeSelectedVertex(dx, dy, isLargeStep)
64
+ * getVertexItems() / selectVertex(index) / insertVertexAtMidpoint(index)
56
65
  * get(id) / add(feature) / setStyle(id, properties) / delete(id) / deleteAll()
57
66
  * setSnapEnabled(bool) / setSnapLayers(layers) / isSnapEnabled()
58
67
  * setFeatureProperty(id, property, value) / setDrawingPreviewProperty(property, value)
@@ -65,10 +74,8 @@ export class MaplibreDrawAdapter {
65
74
  this._map = mapProvider.map
66
75
  this._bus = createEventBus()
67
76
  this._editingFeatureId = null
68
- // Assigned once per feature, on creation only see styles.js's SORT_KEY_PROP. Drives
69
- // fill/line/symbol-sort-key so a later-drawn feature reliably paints above an earlier one
70
- // within the same shared layer, instead of the order being incidental to mapbox-gl-draw's
71
- // internal cold/hot bucket placement.
77
+ // Assigned once per feature, on creation only (see styles.js's SORT_KEY_PROP) — drives
78
+ // paint order so a later-drawn feature reliably layers above an earlier one.
72
79
  this._sortKeyCounter = 0
73
80
 
74
81
  const { draw, remove } = createMapboxDraw({
@@ -83,12 +90,9 @@ export class MaplibreDrawAdapter {
83
90
  this._draw = draw
84
91
  this._cleanupDraw = remove
85
92
 
86
- // Single owner of the dashed-stroke state: live rubber-band / drag moves feed
87
- // update() (default rules sync, user callback throttled) and committed verdicts
88
- // (events.js) land via setInvalid → set(), so the cached state always mirrors
89
- // the rendered layers. onChange does the actual layer toggle; in edit mode the
90
- // displayed shape is exactly what Done finishes, so validity flips also gate
91
- // the Done button (events.js dispatches them).
93
+ // Single owner of the dashed-stroke state live moves feed update(), committed verdicts
94
+ // land via setInvalid(). onChange does the actual layer toggle, and in edit mode also
95
+ // gates the Done button (the displayed shape is exactly what Done finishes).
92
96
  this._liveStroke = createLiveStroke({
93
97
  onChange: (invalid, reason) => {
94
98
  this._applyStrokeInvalid(invalid)
@@ -99,13 +103,10 @@ export class MaplibreDrawAdapter {
99
103
  }
100
104
  })
101
105
 
102
- // Draw-mode-only: computes both the stroke/Done verdict and the Add-point
103
- // verdict from a SINGLE throttled call to the user's callback per rubber-band
104
- // move (see liveDrawChecks.js for why), and is flip-guarded itself. The
105
- // stroke verdict still routes through _liveStroke.set() that instance is
106
- // also driven independently by edit mode and must stay the single source of
107
- // truth for the rendered stroke across mode switches. The Add-point verdict
108
- // has no such cross-mode instance to stay in sync with, so it emits directly.
106
+ // Draw-mode-only: computes both the stroke/Done verdict and the Add-point verdict from one
107
+ // throttled user callback (see liveDrawChecks.js). The stroke verdict still routes through
108
+ // _liveStroke.set() to stay the single source of truth across mode switches; Add-point has
109
+ // no such shared instance, so it emits directly.
109
110
  this._liveDrawChecks = createLiveDrawChecks({
110
111
  onStrokeChange: (invalid, reason) => this._liveStroke.set(invalid, reason),
111
112
  onPlaceChange: (vetoed, reason) => this._bus.emit(ADAPTER_EVENTS.CAN_PLACE_CHANGE, { canPlace: !vetoed, reason })
@@ -114,42 +115,34 @@ export class MaplibreDrawAdapter {
114
115
  // Normalise ML map events → the shared adapter event contract (adapterEvents.js).
115
116
  // The OL adapter emits the same contract directly from OLDrawManager.
116
117
  this._mapHandlers = {
117
- create: (e) => {
118
- const feature = e.features[0]
119
- // Every draw mode's own 'draw.create' listener (registered later, at that mode's
120
- // onSetup, so it runs after this one in the same synchronous dispatch) re-ids a
121
- // freshly-drawn feature to its caller-requested id via a delete+re-add (see
122
- // drawPointMode.js's onCreate / drawMode/clickHandlers.js's reidCreatedFeature) —
123
- // mutating this SAME `feature` object's `id` in place. Deferring a tick lets that
124
- // re-id finish first, so `feature.id` is already final by the time this runs, and
125
- // setFeatureProperty targets the feature that's actually still in the store.
118
+ create: (event) => {
119
+ const feature = event.features[0]
120
+ // Deferred a tick so the mode's own re-id (delete+re-add to the caller-requested id —
121
+ // see drawPointMode.js's onCreate) finishes first, or this targets a stale feature id.
126
122
  setTimeout(() => {
127
123
  this._draw.setFeatureProperty(feature.id, 'sortKey', this._nextSortKey())
128
124
  this._bus.emit(ADAPTER_EVENTS.CREATE, this._draw.get(feature.id))
129
125
  }, 0)
130
126
  },
131
- editfinish: (e) => this._bus.emit(ADAPTER_EVENTS.EDIT_FINISH, e.features[0]),
127
+ editfinish: (event) => this._bus.emit(ADAPTER_EVENTS.EDIT_FINISH, event.features[0]),
132
128
  cancel: () => this._bus.emit(ADAPTER_EVENTS.CANCEL),
133
- // Normalise typo: the ML modes fire numVertecies, the contract uses numVertices
134
- vertexselection: (e) => this._bus.emit(ADAPTER_EVENTS.VERTEX_SELECTION, { ...e, numVertices: e.numVertecies }),
135
- vertexchange: (e) => this._bus.emit(ADAPTER_EVENTS.VERTEX_CHANGE, { ...e, numVertices: e.numVertecies }),
136
- undochange: (e) => this._bus.emit(ADAPTER_EVENTS.UNDO_CHANGE, e.length),
137
- update: (e) => this._bus.emit(ADAPTER_EVENTS.UPDATE, e.features[0]),
138
- geometrychange: (e) => {
139
- // Phase-less events are rubber-band moves carrying the displayed feature
140
- // (placed vertices + cursor) they drive the live invalid stroke, and are
141
- // cached for setDrawingPreviewProperty (the in-progress feature has no
142
- // stable id yet — only assigned once drawing actually finishes — so it
143
- // can't be targeted via setFeatureProperty).
144
- if (!e?.phase) {
145
- this._updateLiveStroke(e)
146
- this._currentDrawEvent = e
129
+ vertexselection: (event) => this._bus.emit(ADAPTER_EVENTS.VERTEX_SELECTION, event),
130
+ vertexchange: (event) => this._bus.emit(ADAPTER_EVENTS.VERTEX_CHANGE, event),
131
+ undochange: (event) => this._bus.emit(ADAPTER_EVENTS.UNDO_CHANGE, event.length),
132
+ update: (event) => this._bus.emit(ADAPTER_EVENTS.UPDATE, event.features[0]),
133
+ geometrychange: (event) => {
134
+ // Phase-less events are rubber-band moves carrying the in-progress feature — they
135
+ // drive the live invalid stroke, and are cached for setDrawingPreviewProperty (which
136
+ // has no stable id yet to target via setFeatureProperty).
137
+ if (!event?.phase) {
138
+ this._updateLiveStroke(event)
139
+ this._currentDrawEvent = event
147
140
  }
148
- this._bus.emit(ADAPTER_EVENTS.GEOMETRY_CHANGE, e)
141
+ this._bus.emit(ADAPTER_EVENTS.GEOMETRY_CHANGE, event)
149
142
  },
150
- placementblocked: (e) => this._bus.emit(ADAPTER_EVENTS.PLACEMENT_BLOCKED, e),
151
- interfacetypechange: (e) => this._bus.emit(ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE, { interfaceType: e.interfaceType }),
152
- modechange: (e) => this._handleModeChange(e),
143
+ placementblocked: (event) => this._bus.emit(ADAPTER_EVENTS.PLACEMENT_BLOCKED, event),
144
+ interfacetypechange: (event) => this._bus.emit(ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE, { interfaceType: event.interfaceType }),
145
+ modechange: (event) => this._handleModeChange(event),
153
146
  styledata: () => this._handleStyleData()
154
147
  }
155
148
 
@@ -177,36 +170,30 @@ export class MaplibreDrawAdapter {
177
170
  this._liveStroke.set(false)
178
171
  this._liveDrawChecks.reset()
179
172
  }
180
- // draw_point has no direct dependency on symbolRegistry/mapProvider the resolver is
181
- // injected here so the mode only ever calls state.resolvePointSymbol(...) and stays
182
- // decoupled from how a symbol image actually gets resolved/registered.
173
+ // The resolver is injected here so draw_point stays decoupled from how a symbol image
174
+ // actually gets resolved/registered.
183
175
  if (name === 'draw_point') {
184
176
  options = { ...options, resolvePointSymbol: (featureId, properties) => this._resolvePointSymbol(featureId, properties) }
185
177
  }
186
178
  this._draw.changeMode(name, options)
187
- // The underlying mapbox-gl-draw control's public changeMode API is silent by
188
- // default (it never fires 'draw.modechange'), so every mode change requested
189
- // through this adapter must drive the same cleanup manually.
179
+ // mapbox-gl-draw's own changeMode never fires 'draw.modechange', so every mode change
180
+ // requested through this adapter must drive the same cleanup manually.
190
181
  this._handleModeChange({ mode: name })
191
182
  }
192
183
 
193
- // Injected into draw_point's changeMode options as state.resolvePointSymbol resolves and
194
- // registers the feature's symbol-config icon, then writes the resolved image id/anchor back
195
- // onto the feature so the data-driven point-symbol layer (styles.js) can render it.
184
+ // Resolves and registers the feature's symbol-config icon, then writes the resolved image
185
+ // id/anchor back so styles.js's point-symbol layer can render it.
196
186
  _resolvePointSymbol (featureId, properties) {
197
187
  return resolvePointSymbol({ draw: this._draw, mapProvider: this._mapProvider, map: this._map, featureId, properties })
198
188
  }
199
189
 
200
- // Live invalid-stroke driver: called on every rubber-band move (draw) and vertex
201
- // drag / nudge (edit) with the displayed feature. Edit mode has no Add-point
202
- // gate, so it goes straight through the live-stroke controller as before. Draw
203
- // mode routes through _liveDrawChecks instead, which computes both the stroke
204
- // and Add-point verdicts from one throttled user-callback call (see
205
- // liveDrawChecks.js).
206
- _updateLiveStroke (e) {
207
- if (!e?.coordinates) { return }
190
+ // Live invalid-stroke driver, called on every rubber-band move (draw) and vertex drag/nudge
191
+ // (edit). Draw mode routes through _liveDrawChecks (also computes the Add-point verdict);
192
+ // edit mode goes straight through the live-stroke controller.
193
+ _updateLiveStroke (event) {
194
+ if (!event?.coordinates) { return }
208
195
  const mode = this._draw.getMode()
209
- const shape = displayedShape(mode, e.coordinates)
196
+ const shape = displayedShape(mode, event.coordinates)
210
197
  if (!shape) { return }
211
198
  if (mode === 'draw_polygon' || mode === 'draw_line') {
212
199
  this._liveDrawChecks.update({ feature: shape.feature, numVertices: shape.numVertices, context: { mode }, onGeometryChange: this._geometryValidator })
@@ -240,13 +227,9 @@ export class MaplibreDrawAdapter {
240
227
  cancel () {
241
228
  this._mapProvider.undoStack?.clear()
242
229
  const mode = this._draw.getMode()
243
- // trash() only belongs to an in-progress, never-committed draw it deletes
244
- // whatever's selected. For edit_vertex, events.js's handleCancel has already
245
- // restored the original feature via draw.add() before this runs; trash()
246
- // here would instead run mapbox-gl-draw's direct_select onTrash on the
247
- // mode's own live state (removing the selected vertex, or — if the
248
- // in-progress edit happened to leave the shape invalid — deleting the
249
- // entire feature), silently discarding the just-restored original.
230
+ // trash() only belongs to an in-progress, uncommitted draw. For edit_vertex, events.js has
231
+ // already restored the original feature trash() here would instead run direct_select's
232
+ // own onTrash and silently discard that restore.
250
233
  if (mode === 'draw_polygon' || mode === 'draw_line') {
251
234
  this._draw.trash()
252
235
  }
@@ -258,14 +241,41 @@ export class MaplibreDrawAdapter {
258
241
  this._map.fire(CUSTOM_DRAW_EVENTS.UNDO)
259
242
  }
260
243
 
261
- // MoveControls' D-pad, routed here via mapProvider.activeMoveTarget (see
262
- // events.js) once a vertex is selected. Bridged into the running edit mode the
263
- // same way setInterfaceType is, since the adapter has no direct reference to the
264
- // mode's live state.
244
+ // MapControls' D-pad, routed here via mapProvider.activeMoveTarget once a vertex is
245
+ // selected bridged into the mode via event, same as setInterfaceType.
265
246
  nudgeSelectedVertex (dx, dy, isLargeStep) {
266
247
  this._map.fire(CUSTOM_DRAW_EVENTS.NUDGE_VERTEX, { dx, dy, isLargeStep })
267
248
  }
268
249
 
250
+ // Read-only vertex/midpoint coordinates for the shared spatial listbox (useSpatialList.js).
251
+ // Reads the committed feature via the public get() — the adapter has no reference into the
252
+ // live mode (see selectVertex below) — flat across all rings/parts, matching the mode's own
253
+ // selectedVertexIndex convention.
254
+ getVertexItems () {
255
+ const empty = { vertices: [], midpoints: [] }
256
+ if (this._draw.getMode() !== 'edit_vertex' || !this._editingFeatureId) {
257
+ return empty
258
+ }
259
+ const feature = this._draw.get(this._editingFeatureId)
260
+ if (!feature) {
261
+ return empty
262
+ }
263
+ const geometry = dedupeClosedRings(feature.geometry)
264
+ return { vertices: getCoords(geometry), midpoints: getMidpointCoords(geometry) }
265
+ }
266
+
267
+ // Moves the live vertex/midpoint cursor to `index` — preview only, no geometry change.
268
+ // Bridged into the mode via a custom event, same pattern as nudgeSelectedVertex.
269
+ selectVertex (index) {
270
+ this._map.fire(CUSTOM_DRAW_EVENTS.SELECT_VERTEX, { index })
271
+ }
272
+
273
+ // Commits a new vertex exactly at midpoint `index` (no directional offset, unlike the
274
+ // keyboard/D-pad path) — the destructive counterpart to selectVertex's preview-only move.
275
+ insertVertexAtMidpoint (index) {
276
+ this._map.fire(CUSTOM_DRAW_EVENTS.INSERT_VERTEX_AT_MIDPOINT, { index })
277
+ }
278
+
269
279
  // Record the current geometry validity so the draw mode can block finish gestures
270
280
  // (double-click / click-to-close) while the in-progress shape is invalid.
271
281
  setGeometryValid (valid) {
@@ -283,10 +293,8 @@ export class MaplibreDrawAdapter {
283
293
  this._liveStroke.set(invalid)
284
294
  }
285
295
 
286
- // Toggle the active shape's stroke between solid (valid) and dashed (invalid) by
287
- // swapping which of the two overlaid stroke layers is visible; the fill is hidden
288
- // while invalid so the shape reads as an outline only. Only the live-stroke
289
- // controller calls this — everything else goes through setInvalid.
296
+ // Swaps which overlaid stroke layer is visible (solid vs dashed); fill is hidden while
297
+ // invalid. Only the live-stroke controller calls this everything else goes through setInvalid.
290
298
  _applyStrokeInvalid (invalid) {
291
299
  this._setLayerVisibility('stroke-active', !invalid)
292
300
  this._setLayerVisibility('stroke-active-invalid', invalid)
@@ -303,12 +311,9 @@ export class MaplibreDrawAdapter {
303
311
  }
304
312
 
305
313
  deleteVertex () {
306
- // Intentionally a no-op on MapLibre. The shared events layer calls draw.deleteVertex()
307
- // from the delete-point button, but the ML edit mode already handles deletion itself:
308
- // via the keyboard (Backspace/Delete) and its own window-click listener matching
309
- // deleteVertexButtonId (see editVertexMode.onButtonClick). Deleting here too would
310
- // double-delete. The OL adapter implements this for real; here it only satisfies the
311
- // shared adapter interface.
314
+ // Intentionally a no-op on MapLibre the ML edit mode already handles vertex deletion
315
+ // itself (keyboard + its own click listener); deleting here too would double-delete.
316
+ // The OL adapter implements this for real; here it only satisfies the shared interface.
312
317
  }
313
318
 
314
319
  get (id) { return this._draw.get(id) }
@@ -318,9 +323,8 @@ export class MaplibreDrawAdapter {
318
323
  return this._sortKeyCounter
319
324
  }
320
325
 
321
- // A directly-added Point (e.g. api/addFeature.js) skips draw_point's own icon-resolving
322
- // drawend handler, so it must be resolved here instead with no fallback, styles.js's
323
- // point-symbol layer would otherwise render nothing at all.
326
+ // A directly-added Point skips draw_point's own icon-resolving handler, so it must be
327
+ // resolved here instead, or styles.js's point-symbol layer renders nothing.
324
328
  add (feature) {
325
329
  // Only a genuinely new feature gets a fresh sort key — setStyle() re-adds an existing one
326
330
  // (via a shallow properties spread) to re-render it, and must not bump it back to the front.
@@ -335,9 +339,8 @@ export class MaplibreDrawAdapter {
335
339
  return ids
336
340
  }
337
341
 
338
- // Patches an existing feature's style properties (stroke/fill/strokeWidth or symbol-family
339
- // keys) and re-renders routed through add() itself so a Point's icon gets re-resolved the
340
- // same way a directly-added one does, for free.
342
+ // Patches an existing feature's style properties and re-renders via add(), so a Point's
343
+ // icon gets re-resolved the same way a directly-added one does.
341
344
  setStyle (id, properties) {
342
345
  const feature = this._draw.get(id)
343
346
  if (!feature) {
@@ -380,17 +383,14 @@ export class MaplibreDrawAdapter {
380
383
  this._draw.setFeatureProperty(id, property, value)
381
384
  }
382
385
 
383
- // Tag the feature currently being drawn (rubber-band, not yet created so it
384
- // has no stable id to target via setFeatureProperty) with a property, and
385
- // re-render so the change is visible immediately. Used for live preview styling
386
- // while drawing, e.g. split's valid/invalid line colour. A no-op once nothing
387
- // has been drawn yet this session, or outside draw_polygon/draw_line.
386
+ // Tags the in-progress rubber-band feature (no stable id yet) with a property and
387
+ // re-renders used for live preview styling, e.g. split's valid/invalid line colour.
388
388
  setDrawingPreviewProperty (property, value) {
389
- const e = this._currentDrawEvent
390
- if (e?.properties) {
391
- e.properties[property] = value
389
+ const event = this._currentDrawEvent
390
+ if (event?.properties) {
391
+ event.properties[property] = value
392
392
  }
393
- e?.ctx?.store?.render()
393
+ event?.ctx?.store?.render()
394
394
  }
395
395
 
396
396
  on (type, handler) {
@@ -401,9 +401,9 @@ export class MaplibreDrawAdapter {
401
401
  this._bus.off(type, handler)
402
402
  }
403
403
 
404
- _handleModeChange (e) {
404
+ _handleModeChange (event) {
405
405
  const DRAW_MODES = new Set(['draw_polygon', 'draw_line', 'draw_point', 'edit_vertex', 'edit_point'])
406
- if (!DRAW_MODES.has(e.mode)) {
406
+ if (!DRAW_MODES.has(event.mode)) {
407
407
  clearSnapIndicator(getSnapInstance(this._map), this._map)
408
408
  }
409
409
  }
@@ -414,9 +414,8 @@ export class MaplibreDrawAdapter {
414
414
  // 'styledata' after the map's been torn down — getStyle() returns undefined then; no-op.
415
415
  const style = this._map.getStyle()
416
416
  if (!style) { return }
417
- // A style reload re-adds the draw layers with their spec-default visibility
418
- // (solid stroke shown, dashed hidden) re-assert the cached stroke state so
419
- // an invalid shape stays dashed across the reload.
417
+ // A style reload resets layers to spec-default visibility — re-assert the cached stroke
418
+ // state so an invalid shape stays dashed.
420
419
  this._liveStroke.refresh()
421
420
  const layers = style.layers || []
422
421
  if (!layers.length || layers[layers.length - 1].source?.startsWith('mapbox-gl-draw')) {
@@ -179,11 +179,11 @@ describe('map event normalisation', () => {
179
179
  expect(bus.emit).toHaveBeenCalledWith('cancel')
180
180
  })
181
181
 
182
- test('vertexselection/vertexchange normalise the numVertecies typo', () => {
182
+ test('vertexselection/vertexchange forward the raw event', () => {
183
183
  const { map, bus } = setup()
184
184
 
185
- onHandler(map, CUSTOM_DRAW_EVENTS.VERTEX_SELECTION)({ numVertecies: 3, index: 1 })
186
- onHandler(map, CUSTOM_DRAW_EVENTS.VERTEX_CHANGE)({ numVertecies: 2 })
185
+ onHandler(map, CUSTOM_DRAW_EVENTS.VERTEX_SELECTION)({ numVertices: 3, index: 1 })
186
+ onHandler(map, CUSTOM_DRAW_EVENTS.VERTEX_CHANGE)({ numVertices: 2 })
187
187
 
188
188
  expect(bus.emit).toHaveBeenCalledWith('vertexselection', expect.objectContaining({ numVertices: 3, index: 1 }))
189
189
  expect(bus.emit).toHaveBeenCalledWith('vertexchange', expect.objectContaining({ numVertices: 2 }))
@@ -197,16 +197,16 @@ describe('map event normalisation', () => {
197
197
 
198
198
  test('geometrychange forwards the raw event', () => {
199
199
  const { map, bus } = setup()
200
- const e = { type: 'Polygon' }
201
- onHandler(map, CUSTOM_DRAW_EVENTS.GEOMETRY_CHANGE)(e)
202
- expect(bus.emit).toHaveBeenCalledWith('geometrychange', e)
200
+ const event = { type: 'Polygon' }
201
+ onHandler(map, CUSTOM_DRAW_EVENTS.GEOMETRY_CHANGE)(event)
202
+ expect(bus.emit).toHaveBeenCalledWith('geometrychange', event)
203
203
  })
204
204
 
205
205
  test('placementblocked forwards the raw event', () => {
206
206
  const { map, bus } = setup()
207
- const e = { phase: 'place', reason: 'outside region' }
208
- onHandler(map, CUSTOM_DRAW_EVENTS.PLACEMENT_BLOCKED)(e)
209
- expect(bus.emit).toHaveBeenCalledWith('placementblocked', e)
207
+ const event = { phase: 'place', reason: 'outside region' }
208
+ onHandler(map, CUSTOM_DRAW_EVENTS.PLACEMENT_BLOCKED)(event)
209
+ expect(bus.emit).toHaveBeenCalledWith('placementblocked', event)
210
210
  })
211
211
  })
212
212
 
@@ -582,6 +582,18 @@ describe('simple delegations', () => {
582
582
  expect(map.fire).toHaveBeenCalledWith(CUSTOM_DRAW_EVENTS.NUDGE_VERTEX, { dx: 1, dy: 0, isLargeStep: true })
583
583
  })
584
584
 
585
+ test('selectVertex fires the select-vertex event with the given index', () => {
586
+ const { adapter, map } = setup()
587
+ adapter.selectVertex(3)
588
+ expect(map.fire).toHaveBeenCalledWith(CUSTOM_DRAW_EVENTS.SELECT_VERTEX, { index: 3 })
589
+ })
590
+
591
+ test('insertVertexAtMidpoint fires the insert-vertex-at-midpoint event with the given index', () => {
592
+ const { adapter, map } = setup()
593
+ adapter.insertVertexAtMidpoint(4)
594
+ expect(map.fire).toHaveBeenCalledWith(CUSTOM_DRAW_EVENTS.INSERT_VERTEX_AT_MIDPOINT, { index: 4 })
595
+ })
596
+
585
597
  test('deleteVertex is a no-op', () => {
586
598
  const { adapter, draw, map } = setup()
587
599
  expect(() => adapter.deleteVertex()).not.toThrow()
@@ -761,6 +773,67 @@ describe('simple delegations', () => {
761
773
  })
762
774
  })
763
775
 
776
+ describe('getVertexItems', () => {
777
+ test('returns empty vertices/midpoints when not in edit_vertex mode', () => {
778
+ const { adapter, draw } = setup()
779
+ draw.getMode.mockReturnValue('draw_polygon')
780
+ expect(adapter.getVertexItems()).toEqual({ vertices: [], midpoints: [] })
781
+ })
782
+
783
+ test('returns empty vertices/midpoints when in edit_vertex but no editing feature id is set', () => {
784
+ const { adapter, draw } = setup()
785
+ draw.getMode.mockReturnValue('edit_vertex')
786
+ expect(adapter.getVertexItems()).toEqual({ vertices: [], midpoints: [] })
787
+ })
788
+
789
+ test('returns empty vertices/midpoints when the editing feature no longer exists', () => {
790
+ const { adapter, draw } = setup()
791
+ adapter.changeMode('edit_vertex', { featureId: 'f1' })
792
+ draw.getMode.mockReturnValue('edit_vertex')
793
+ draw.get.mockReturnValue(undefined)
794
+ expect(adapter.getVertexItems()).toEqual({ vertices: [], midpoints: [] })
795
+ })
796
+
797
+ test('flattens a LineString — no closing duplicate to strip', () => {
798
+ const { adapter, draw } = setup()
799
+ adapter.changeMode('edit_vertex', { featureId: 'f1' })
800
+ draw.getMode.mockReturnValue('edit_vertex')
801
+ draw.get.mockReturnValue({ geometry: { type: 'LineString', coordinates: [[0, 0], [10, 0], [10, 10]] } })
802
+ const result = adapter.getVertexItems()
803
+ expect(result.vertices).toEqual([[0, 0], [10, 0], [10, 10]])
804
+ expect(result.midpoints).toEqual([[5, 0], [10, 5]])
805
+ })
806
+
807
+ test('strips the closing duplicate coordinate for a Polygon ring — index-aligned with the mode\'s own selectedVertexIndex', () => {
808
+ const { adapter, draw } = setup()
809
+ adapter.changeMode('edit_vertex', { featureId: 'f1' })
810
+ draw.getMode.mockReturnValue('edit_vertex')
811
+ draw.get.mockReturnValue({ geometry: { type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 0]]] } })
812
+ const result = adapter.getVertexItems()
813
+ expect(result.vertices).toEqual([[0, 0], [10, 0], [10, 10]])
814
+ })
815
+
816
+ test('strips the closing duplicate for every ring/part of a MultiPolygon, keeping index continuity across parts', () => {
817
+ const { adapter, draw } = setup()
818
+ adapter.changeMode('edit_vertex', { featureId: 'f1' })
819
+ draw.getMode.mockReturnValue('edit_vertex')
820
+ draw.get.mockReturnValue({
821
+ geometry: {
822
+ type: 'MultiPolygon',
823
+ coordinates: [
824
+ [[[0, 0], [10, 0], [10, 10], [0, 0]]],
825
+ [[[20, 20], [30, 20], [30, 30], [20, 20]]]
826
+ ]
827
+ }
828
+ })
829
+ const result = adapter.getVertexItems()
830
+ expect(result.vertices).toEqual([
831
+ [0, 0], [10, 0], [10, 10],
832
+ [20, 20], [30, 20], [30, 30]
833
+ ])
834
+ })
835
+ })
836
+
764
837
  describe('done', () => {
765
838
  test('clears the undo stack and fires editfinish when editing a vertex', () => {
766
839
  const { adapter, map, draw, undoStack } = setup()
@@ -28,7 +28,9 @@ export const CUSTOM_DRAW_EVENTS = {
28
28
  GEOMETRY_CHANGE: 'draw.geometrychange',
29
29
  INTERFACE_TYPE_CHANGE: 'draw.interfacetypechange',
30
30
  PLACEMENT_BLOCKED: 'draw.placementblocked',
31
- NUDGE_VERTEX: 'draw.nudgevertex'
31
+ NUDGE_VERTEX: 'draw.nudgevertex',
32
+ SELECT_VERTEX: 'draw.selectvertex',
33
+ INSERT_VERTEX_AT_MIDPOINT: 'draw.insertvertexatmidpoint'
32
34
  }
33
35
 
34
36
  // Native MapLibre map event (not a draw event) — fires whenever the map style data changes.
@@ -21,7 +21,7 @@ describe('createDrawMode composition', () => {
21
21
  const mode = build()
22
22
  const contributed = [
23
23
  'onSetup', 'onStop', // lifecycle
24
- 'onClick', 'onTap', 'doClick', 'onVertexButtonClick', 'onCreate', // click
24
+ 'onClick', 'onTap', 'doClick', '_placeAtCrossHair', 'onVertexButtonClick', 'onCreate', // click
25
25
  'pushDrawUndo', 'undoVertex', 'onUndo', // undo
26
26
  'onKeydown', 'onKeyup', 'onKeyUp', // keyboard
27
27
  'onMove', 'onMouseMove', 'onTouchStart', 'onBlur', // pointer
@@ -1,8 +1,10 @@
1
1
  import { DrawPolygonMode } from '../../drawPolygonMode.js'
2
2
  import { DrawLineMode } from '../../drawLineMode.js'
3
3
  import { createUndoStack } from '../../../../../utils/undoStack.js'
4
- import PolygonFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/polygon.js'
5
- import LineStringFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/line_string.js'
4
+ // Real mapbox-gl-draw Feature classes, not exported from its package entrypoint — needed here
5
+ // so this harness can construct the actual objects mapbox-gl-draw itself hands to a mode.
6
+ import PolygonFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/polygon.js' // NOSONAR
7
+ import LineStringFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/line_string.js' // NOSONAR
6
8
 
7
9
  /**
8
10
  * Shared test harness for the createDrawMode handler modules. Exercises the real
@@ -12,6 +14,10 @@ import LineStringFeature from '../../../../../../../../node_modules/@mapbox/mapb
12
14
 
13
15
  export { DrawPolygonMode, DrawLineMode }
14
16
  export const CENTER = { lng: 5, lat: 5 }
17
+ // Fixed point a mock-active snap resolves to, distinct from CENTER so tests can tell whether
18
+ // a click landed at the real coordinate or was pulled onto the snap target — the value itself
19
+ // is arbitrary, just needs to differ from CENTER.
20
+ export const SNAP_TARGET = [9, 9] // NOSONAR
15
21
 
16
22
  const createMap = () => {
17
23
  const canvas = document.createElement('canvas')
@@ -25,9 +31,12 @@ const createMap = () => {
25
31
  getCenter: () => ({ ...CENTER }),
26
32
  project: jest.fn(() => ({ x: 50, y: 50 })),
27
33
  unproject: jest.fn(() => ({ ...CENTER })),
28
- fire: jest.fn(function (type, e) { (listeners[type] ?? []).forEach((h) => h(e)) }),
29
- on: jest.fn((type, h) => { (listeners[type] ??= []).push(h) }),
30
- off: jest.fn((type, h) => { listeners[type] = (listeners[type] ?? []).filter((x) => x !== h) })
34
+ fire: jest.fn(function (type, event) { (listeners[type] ?? []).forEach((handler) => handler(event)) }),
35
+ on: jest.fn((type, handler) => {
36
+ listeners[type] ??= []
37
+ listeners[type].push(handler)
38
+ }),
39
+ off: jest.fn((type, handler) => { listeners[type] = (listeners[type] ?? []).filter((x) => x !== handler) })
31
40
  }
32
41
  }
33
42
 
@@ -64,8 +73,8 @@ const createModeContext = (mode) => {
64
73
 
65
74
  // Remove window/container/map listeners registered by onSetup so tests don't leak into each other
66
75
  const contexts = []
67
- const removeListeners = (ctx) => ctx._listeners?.forEach(([t, e, h]) =>
68
- t.removeEventListener ? t.removeEventListener(e, h) : t.off(e, h))
76
+ const removeListeners = (ctx) => ctx._listeners?.forEach(([target, eventName, handler, opts]) =>
77
+ target.removeEventListener ? target.removeEventListener(eventName, handler, opts) : target.off(eventName, handler))
69
78
 
70
79
  const createContainer = () => {
71
80
  const container = document.createElement('div')
@@ -104,9 +113,9 @@ export const clickEvent = (ctx, lng, lat, overrides = {}) => ({
104
113
 
105
114
  export const clickAt = (ctx, state, lng, lat) => ctx.onClick(state, clickEvent(ctx, lng, lat))
106
115
 
107
- export const firedWith = (map, type) => map.fire.mock.calls.filter(([t]) => t === type).map(([, e]) => e)
116
+ export const firedWith = (map, type) => map.fire.mock.calls.filter(([eventType]) => eventType === type).map(([, event]) => event)
108
117
 
109
- export const activeSnap = () => ({ status: true, snapStatus: true, snapCoords: [9, 9], snapToClosestPoint: jest.fn() })
118
+ export const activeSnap = () => ({ status: true, snapStatus: true, snapCoords: SNAP_TARGET, snapToClosestPoint: jest.fn() })
110
119
 
111
120
  afterEach(() => {
112
121
  contexts.splice(0).forEach(removeListeners)
@@ -60,7 +60,7 @@ const createClickHelpers = ({ geometryType, getFeature, getCoords }) => ({
60
60
  dispatchVertexChange (coords) {
61
61
  // Both polygon ring and LineString store [v0...vN, rubber_band] during drawing — subtract 1 to get placed vertex count
62
62
  this.map.fire('draw.vertexchange', {
63
- numVertecies: Math.max(0, coords.length - 1)
63
+ numVertices: Math.max(0, coords.length - 1)
64
64
  })
65
65
  },
66
66
 
@@ -179,6 +179,14 @@ const createClickActions = ({ ParentMode, getFeature, getCoords, validateClick,
179
179
  this.pushDrawUndo(state)
180
180
  this.dispatchVertexChange(newCoords)
181
181
  this.emitDrawValidation(state)
182
+ },
183
+
184
+ // Alias for lifecycle.js's shared crosshair.activate wiring (createLifecycle calls
185
+ // this._placeAtCrossHair?.(state)) — drawPointMode.js defines its own _placeAtCrossHair
186
+ // directly, but polygon/line's equivalent "commit at crosshair" action is doClick above
187
+ // (also used by the touch add-vertex button), just under a different name.
188
+ _placeAtCrossHair (state) {
189
+ this.doClick(state)
182
190
  }
183
191
  }
184
192
  }