@defra/interactive-map 0.0.36-alpha → 0.0.38-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 (248) hide show
  1. package/assets/templates/draw-tools.njk +4 -3
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api.md +32 -0
  8. package/docs/examples/draw-tools.mdx +0 -4
  9. package/docs/plugins/draw.md +482 -0
  10. package/docs/plugins.md +2 -6
  11. package/govuk-prototype-kit.config.json +3 -2
  12. package/package.json +2 -4
  13. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  14. package/plugins/beta/draw-es/src/events.js +1 -1
  15. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  18. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  19. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  20. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  21. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  22. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  23. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +13 -0
  24. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  26. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  27. package/plugins/draw/dist/esm/index.js +1 -0
  28. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  29. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  30. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  31. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  32. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  33. package/plugins/draw/dist/umd/index.js +2 -0
  34. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  35. package/plugins/draw/src/DrawInit.jsx +89 -0
  36. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  37. package/plugins/draw/src/adapterEvents.js +127 -0
  38. package/plugins/draw/src/adapterEvents.test.js +20 -0
  39. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  40. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  41. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  42. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  43. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  44. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  45. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  46. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  47. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  48. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  49. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  95. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  96. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  97. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  98. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  99. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  100. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  101. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  102. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  103. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  104. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  105. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  106. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  107. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  108. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  109. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  110. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  111. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  112. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  113. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  114. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  115. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  118. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  119. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  120. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  121. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  144. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  146. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  148. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  149. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  156. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  157. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  158. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  159. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  163. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  164. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  165. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  166. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  167. package/plugins/draw/src/api/addFeature.js +22 -0
  168. package/plugins/draw/src/api/addFeature.test.js +41 -0
  169. package/plugins/draw/src/api/deleteFeature.js +11 -0
  170. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  171. package/plugins/draw/src/api/editFeature.js +54 -0
  172. package/plugins/draw/src/api/editFeature.test.js +134 -0
  173. package/plugins/draw/src/api/merge.js +30 -0
  174. package/plugins/draw/src/api/merge.test.js +57 -0
  175. package/plugins/draw/src/api/newLine.js +41 -0
  176. package/plugins/draw/src/api/newLine.test.js +93 -0
  177. package/plugins/draw/src/api/newPolygon.js +41 -0
  178. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  179. package/plugins/draw/src/api/split.js +117 -0
  180. package/plugins/draw/src/api/split.test.js +204 -0
  181. package/plugins/draw/src/defaults.js +52 -0
  182. package/plugins/draw/src/defaults.test.js +18 -0
  183. package/plugins/draw/src/draw.scss +43 -0
  184. package/plugins/draw/src/events.js +237 -0
  185. package/plugins/draw/src/events.test.js +369 -0
  186. package/plugins/draw/src/index.js +10 -0
  187. package/plugins/draw/src/index.test.js +24 -0
  188. package/plugins/draw/src/manifest.js +170 -0
  189. package/plugins/draw/src/manifest.test.js +147 -0
  190. package/plugins/draw/src/reducer.js +69 -0
  191. package/plugins/draw/src/reducer.test.js +85 -0
  192. package/plugins/draw/src/utils/debounce.js +16 -0
  193. package/plugins/draw/src/utils/debounce.test.js +40 -0
  194. package/plugins/draw/src/utils/eventBus.js +36 -0
  195. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  196. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  197. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  198. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  199. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  200. package/plugins/draw/src/utils/resolveColors.js +39 -0
  201. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  202. package/plugins/draw/src/utils/spatial.js +243 -0
  203. package/plugins/draw/src/utils/spatial.test.js +243 -0
  204. package/plugins/draw/src/utils/touchTarget.js +95 -0
  205. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  206. package/plugins/draw/src/utils/undoStack.js +31 -0
  207. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  208. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  209. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  210. package/plugins/draw/src/validation/liveStroke.js +85 -0
  211. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  212. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  213. package/plugins/draw/src/validation/rules.js +186 -0
  214. package/plugins/draw/src/validation/rules.test.js +120 -0
  215. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  216. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  217. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  218. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  219. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  220. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  221. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  222. package/plugins/search/dist/css/index.css +1 -317
  223. package/rollup.esm.mjs +35 -28
  224. package/sonar-project.properties +2 -2
  225. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  226. package/src/App/components/Hints/Hints.module.scss +5 -4
  227. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  228. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  229. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  230. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  231. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  232. package/src/App/hooks/useHintsAPI.js +67 -0
  233. package/src/App/hooks/useHintsAPI.test.js +145 -0
  234. package/src/App/renderer/PluginInits.jsx +4 -0
  235. package/src/App/renderer/PluginInits.test.jsx +7 -1
  236. package/src/InteractiveMap/InteractiveMap.js +20 -0
  237. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  238. package/src/config/events.js +4 -0
  239. package/src/scss/settings/_dimensions.scss +3 -0
  240. package/src/services/announcer.js +38 -3
  241. package/src/services/announcer.test.js +133 -1
  242. package/src/services/hints.js +3 -0
  243. package/src/services/hints.test.js +19 -0
  244. package/src/types.js +12 -0
  245. package/src/utils/detectInterfaceType.js +5 -1
  246. package/src/utils/isMac.js +15 -0
  247. package/src/utils/isMac.test.js +37 -0
  248. package/webpack.umd.mjs +1 -0
@@ -0,0 +1 @@
1
+ export { COLORS, SIZES, TOLERANCES, KEYBOARD, MAP_SIZE_SCALES } from '../../defaults.js'
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Event names used by the MapLibre draw adapter, grouped by origin.
3
+ *
4
+ * - MAPBOX_DRAW_EVENTS: stock events fired by @mapbox/mapbox-gl-draw itself.
5
+ * - CUSTOM_DRAW_EVENTS: events this codebase's custom modes / adapter dispatch on the map
6
+ * (also under the `draw.*` namespace).
7
+ * - STYLE_DATA_EVENT: the one native MapLibre map event the adapter listens to (to keep draw
8
+ * layers on top after a style reload).
9
+ *
10
+ * The adapter normalises all of these onto the shared, framework-agnostic event bus.
11
+ */
12
+
13
+ // Stock @mapbox/mapbox-gl-draw events, fired by the library itself.
14
+ export const MAPBOX_DRAW_EVENTS = {
15
+ CREATE: 'draw.create',
16
+ UPDATE: 'draw.update',
17
+ MODE_CHANGE: 'draw.modechange'
18
+ }
19
+
20
+ // Custom draw events dispatched by this codebase's modes / adapter.
21
+ export const CUSTOM_DRAW_EVENTS = {
22
+ EDIT_FINISH: 'draw.editfinish',
23
+ CANCEL: 'draw.cancel',
24
+ VERTEX_SELECTION: 'draw.vertexselection',
25
+ VERTEX_CHANGE: 'draw.vertexchange',
26
+ UNDO_CHANGE: 'draw.undochange',
27
+ UNDO: 'draw.undo',
28
+ GEOMETRY_CHANGE: 'draw.geometrychange',
29
+ INTERFACE_TYPE_CHANGE: 'draw.interfacetypechange',
30
+ PLACEMENT_BLOCKED: 'draw.placementblocked',
31
+ NUDGE_VERTEX: 'draw.nudgevertex'
32
+ }
33
+
34
+ // Native MapLibre map event (not a draw event) — fires whenever the map style data changes.
35
+ export const STYLE_DATA_EVENT = 'styledata'
@@ -0,0 +1,126 @@
1
+ import MapboxDraw from '@mapbox/mapbox-gl-draw'
2
+ import { DisabledMode } from './modes/disabledMode.js'
3
+ import { EditVertexMode } from './modes/editVertexMode.js'
4
+ import { DrawPolygonMode } from './modes/drawPolygonMode.js'
5
+ import { DrawLineMode } from './modes/drawLineMode.js'
6
+ import { createDrawStyles, updateDrawStyles } from './styles.js'
7
+ import { initMapLibreSnap } from './mapboxSnap.js'
8
+ import { createUndoStack } from '../../utils/undoStack.js'
9
+ import { setupTouchClickWorkaround } from './utils/touchClickWorkaround.js'
10
+ import { applyTouchVertexColors } from './modes/editVertexMode/touchHandlers.js'
11
+ import { resolveColors } from '../../utils/resolveColors.js'
12
+ import { TOLERANCES, MAP_SIZE_SCALES } from './defaults.js'
13
+
14
+ /**
15
+ * Creates and manages a MapLibre/Mapbox Draw control instance configured for polygon editing.
16
+ * Returns an object with a `.remove()` cleanup function that removes all listeners
17
+ * and safely disposes of the Draw control.
18
+ *
19
+ * Features:
20
+ * - Custom modes for editing and drawing vertices
21
+ * - Dynamic runtime style updates on `events.MAP_SET_STYLE` event
22
+ * - Safe reapplication of styles if map.setStyle is called
23
+ *
24
+ * @param {string} options.mapStyle - Map style object
25
+ * @param {Object} options.mapProvider - Object containing the map instance
26
+ * @param {Object} options.eventBus - Event bus for app-level events
27
+ * @param {Object} [options.pluginConfig] - Plugin-level colour/size overrides — see resolveColors()
28
+ * @returns {{ draw: MapboxDraw, remove: Function }} draw instance and cleanup function
29
+ */
30
+ export const createMapboxDraw = ({ mapStyle, mapProvider, events, eventBus, snapLayers, pluginConfig = {} }) => {
31
+ const { map } = mapProvider
32
+
33
+ // --- Configure MapLibre GL Draw CSS classes ---
34
+ MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl'
35
+ MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-'
36
+ MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group'
37
+
38
+ // --- Register custom modes ---
39
+ const modes = {
40
+ ...MapboxDraw.modes,
41
+ disabled: DisabledMode,
42
+ edit_vertex: EditVertexMode,
43
+ draw_polygon: DrawPolygonMode,
44
+ draw_line: DrawLineMode
45
+ }
46
+
47
+ // --- Create or reuse MapLibre Draw instance ---
48
+ let draw = mapProvider._mapboxDrawInstance
49
+ if (draw) {
50
+ // Update modes on existing draw instance when adapter is recreated
51
+ Object.assign(draw.modes, modes)
52
+ } else {
53
+ draw = new MapboxDraw({
54
+ modes,
55
+ styles: createDrawStyles(mapStyle, pluginConfig),
56
+ displayControlsDefault: false,
57
+ userProperties: true,
58
+ defaultMode: 'disabled'
59
+ })
60
+ map.addControl(draw)
61
+ mapProvider._mapboxDrawInstance = draw
62
+ }
63
+
64
+ // mapbox-gl-draw swallows tap clicks in disabled mode — synthesize them
65
+ const touchClickWorkaround = setupTouchClickWorkaround(map, draw)
66
+
67
+ // We need a reference to this
68
+ mapProvider.draw = draw
69
+ map._drawCurrentMapStyle = mapStyle
70
+ // Stashed on the map (alongside _drawCurrentMapStyle) so mode code that only
71
+ // has `this.map` — e.g. touchHandlers.js's addTouchVertexTarget — can still
72
+ // resolve colour overrides without pluginConfig being threaded through every
73
+ // mode's option object.
74
+ map._drawPluginConfig = pluginConfig
75
+ // Initialize snap as disabled (matches initialState.snap = false)
76
+ mapProvider.snapEnabled = false
77
+ // Initialize undo stack (reuse if already exists)
78
+ let undoStack = mapProvider.undoStack
79
+ if (!undoStack) {
80
+ undoStack = createUndoStack((length) => map.fire('draw.undochange', { length }))
81
+ mapProvider.undoStack = undoStack
82
+ }
83
+ map._undoStack = undoStack
84
+
85
+ // --- Initialize MapboxSnap using external module ---
86
+ // Start with status: false to match initial snap disabled state
87
+ const snapColors = resolveColors(mapStyle, pluginConfig)
88
+ initMapLibreSnap(map, draw, {
89
+ layers: snapLayers,
90
+ radius: pluginConfig.snapRadius ?? TOLERANCES.snapRadius,
91
+ rules: ['vertex', 'edge'],
92
+ colors: { vertex: snapColors.snapVertex, edge: snapColors.snapEdge }
93
+ })
94
+
95
+ // --- Update colour scheme ---
96
+ const handleSetMapStyle = (e) => {
97
+ map._drawCurrentMapStyle = e
98
+ map.once('idle', () => {
99
+ updateDrawStyles(map, e, pluginConfig)
100
+ const svg = map._drawEditContainer?.querySelector('[data-im-draw-touch-target]')
101
+ applyTouchVertexColors(svg, e, pluginConfig)
102
+ })
103
+ }
104
+ eventBus.on(events.MAP_SET_STYLE, handleSetMapStyle)
105
+
106
+ // --- Update map scale ---
107
+ const handleSetMapSize = (e) => {
108
+ map.fire('draw.scalechange', { scale: MAP_SIZE_SCALES[e] })
109
+ }
110
+ eventBus.on(events.MAP_SET_SIZE, handleSetMapSize)
111
+
112
+ // --- Return instance and cleanup function ---
113
+ return {
114
+ draw,
115
+ remove () {
116
+ touchClickWorkaround.remove()
117
+ // Remove event listeners
118
+ eventBus.off(events.MAP_SET_STYLE, handleSetMapStyle)
119
+ eventBus.off(events.MAP_SET_SIZE, handleSetMapSize)
120
+ // Disable draw mode but keep control on map for reuse
121
+ draw.changeMode('disabled')
122
+ // Clear adapter reference (but not _mapboxDrawInstance so it persists)
123
+ mapProvider.draw = null
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,268 @@
1
+ import MapboxDraw from '@mapbox/mapbox-gl-draw'
2
+ import { createDrawStyles, updateDrawStyles } from './styles.js'
3
+ import { initMapLibreSnap } from './mapboxSnap.js'
4
+ import { createUndoStack } from '../../utils/undoStack.js'
5
+ import { setupTouchClickWorkaround } from './utils/touchClickWorkaround.js'
6
+ import { applyTouchVertexColors } from './modes/editVertexMode/touchHandlers.js'
7
+ import { resolveColors } from '../../utils/resolveColors.js'
8
+ import { TOLERANCES, MAP_SIZE_SCALES } from './defaults.js'
9
+ import { createMapboxDraw } from './mapboxDraw.js'
10
+
11
+ jest.mock('@mapbox/mapbox-gl-draw', () => {
12
+ const MockDraw = jest.fn(function () {
13
+ this.modes = {}
14
+ this.changeMode = jest.fn()
15
+ })
16
+ MockDraw.constants = { classes: {} }
17
+ MockDraw.modes = { existing_mode: { id: 'existing' } }
18
+ return { __esModule: true, default: MockDraw }
19
+ })
20
+
21
+ jest.mock('./modes/disabledMode.js', () => ({ DisabledMode: { id: 'disabled' } }))
22
+ jest.mock('./modes/editVertexMode.js', () => ({ EditVertexMode: { id: 'edit_vertex' } }))
23
+ jest.mock('./modes/drawPolygonMode.js', () => ({ DrawPolygonMode: { id: 'draw_polygon' } }))
24
+ jest.mock('./modes/drawLineMode.js', () => ({ DrawLineMode: { id: 'draw_line' } }))
25
+ jest.mock('./styles.js', () => ({
26
+ createDrawStyles: jest.fn(() => ['style']),
27
+ updateDrawStyles: jest.fn()
28
+ }))
29
+ jest.mock('./mapboxSnap.js', () => ({ initMapLibreSnap: jest.fn() }))
30
+ jest.mock('../../utils/undoStack.js', () => ({ createUndoStack: jest.fn(() => ({ id: 'undo-stack' })) }))
31
+ jest.mock('./utils/touchClickWorkaround.js', () => ({ setupTouchClickWorkaround: jest.fn() }))
32
+ jest.mock('./modes/editVertexMode/touchHandlers.js', () => ({ applyTouchVertexColors: jest.fn() }))
33
+ jest.mock('../../utils/resolveColors.js', () => ({
34
+ resolveColors: jest.fn(() => ({ snapVertex: 'resolved-vertex', snapEdge: 'resolved-edge' }))
35
+ }))
36
+ jest.mock('./defaults.js', () => ({
37
+ TOLERANCES: { snapRadius: 12 },
38
+ MAP_SIZE_SCALES: { medium: 1.5 }
39
+ }))
40
+
41
+ const EVENTS = { MAP_SET_STYLE: 'map:setstyle', MAP_SET_SIZE: 'map:setsize' }
42
+
43
+ const handlerFor = (mockFn, eventName) =>
44
+ mockFn.mock.calls.find(([name]) => name === eventName)?.[1]
45
+
46
+ const createMap = () => ({
47
+ addControl: jest.fn(),
48
+ once: jest.fn(),
49
+ on: jest.fn(),
50
+ off: jest.fn(),
51
+ fire: jest.fn()
52
+ })
53
+
54
+ const setup = ({ existingDraw, existingUndoStack, pluginConfig } = {}) => {
55
+ const map = createMap()
56
+ const mapProvider = {
57
+ map,
58
+ _mapboxDrawInstance: existingDraw,
59
+ undoStack: existingUndoStack
60
+ }
61
+ const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
62
+ const removeWorkaround = jest.fn()
63
+ setupTouchClickWorkaround.mockReturnValue({ remove: removeWorkaround })
64
+
65
+ const result = createMapboxDraw({
66
+ mapStyle: 'light',
67
+ mapProvider,
68
+ events: EVENTS,
69
+ eventBus,
70
+ snapLayers: ['layer-a'],
71
+ ...(pluginConfig !== undefined ? { pluginConfig } : {})
72
+ })
73
+
74
+ return { map, mapProvider, eventBus, removeWorkaround, result }
75
+ }
76
+
77
+ beforeEach(() => {
78
+ jest.clearAllMocks()
79
+ })
80
+
81
+ describe('createMapboxDraw – instance creation', () => {
82
+ test('configures the MapLibre control CSS class constants', () => {
83
+ setup()
84
+ expect(MapboxDraw.constants.classes).toMatchObject({
85
+ CONTROL_BASE: 'maplibregl-ctrl',
86
+ CONTROL_PREFIX: 'maplibregl-ctrl-',
87
+ CONTROL_GROUP: 'maplibregl-ctrl-group'
88
+ })
89
+ })
90
+
91
+ test('creates a new draw instance with the custom modes and adds it to the map', () => {
92
+ const { map, mapProvider, result } = setup()
93
+
94
+ expect(MapboxDraw).toHaveBeenCalledTimes(1)
95
+ const options = MapboxDraw.mock.calls[0][0]
96
+ expect(options).toMatchObject({
97
+ styles: ['style'],
98
+ displayControlsDefault: false,
99
+ userProperties: true,
100
+ defaultMode: 'disabled'
101
+ })
102
+ expect(options.modes).toMatchObject({
103
+ existing_mode: { id: 'existing' },
104
+ disabled: { id: 'disabled' },
105
+ edit_vertex: { id: 'edit_vertex' },
106
+ draw_polygon: { id: 'draw_polygon' },
107
+ draw_line: { id: 'draw_line' }
108
+ })
109
+ expect(createDrawStyles).toHaveBeenCalledWith('light', {})
110
+
111
+ const draw = MapboxDraw.mock.instances[0]
112
+ expect(map.addControl).toHaveBeenCalledWith(draw)
113
+ expect(mapProvider._mapboxDrawInstance).toBe(draw)
114
+ expect(mapProvider.draw).toBe(draw)
115
+ expect(result.draw).toBe(draw)
116
+ })
117
+
118
+ test('reuses an existing draw instance and merges modes instead of creating a new one', () => {
119
+ const existingDraw = { modes: { old: {} }, changeMode: jest.fn() }
120
+ const { map, mapProvider } = setup({ existingDraw })
121
+
122
+ expect(MapboxDraw).not.toHaveBeenCalled()
123
+ expect(map.addControl).not.toHaveBeenCalled()
124
+ expect(existingDraw.modes).toMatchObject({
125
+ old: {},
126
+ disabled: { id: 'disabled' },
127
+ edit_vertex: { id: 'edit_vertex' }
128
+ })
129
+ expect(mapProvider.draw).toBe(existingDraw)
130
+ })
131
+ })
132
+
133
+ describe('createMapboxDraw – setup side effects', () => {
134
+ test('sets up the touch-click workaround and records provider state', () => {
135
+ const { map, mapProvider } = setup()
136
+
137
+ expect(setupTouchClickWorkaround).toHaveBeenCalledWith(map, mapProvider.draw)
138
+ expect(map._drawCurrentMapStyle).toBe('light')
139
+ expect(mapProvider.snapEnabled).toBe(false)
140
+ })
141
+
142
+ test('stashes pluginConfig on the map for mode code that only has `this.map`', () => {
143
+ const pluginConfig = { shapeStroke: '#custom' }
144
+ const { map } = setup({ pluginConfig })
145
+
146
+ expect(map._drawPluginConfig).toBe(pluginConfig)
147
+ })
148
+
149
+ test('defaults pluginConfig to {} when not provided', () => {
150
+ const { map } = setup()
151
+
152
+ expect(map._drawPluginConfig).toEqual({})
153
+ })
154
+
155
+ test('creates an undo stack when none exists and wires it to the map', () => {
156
+ const { map, mapProvider } = setup()
157
+
158
+ expect(createUndoStack).toHaveBeenCalledTimes(1)
159
+ expect(mapProvider.undoStack).toEqual({ id: 'undo-stack' })
160
+ expect(map._undoStack).toBe(mapProvider.undoStack)
161
+
162
+ // The callback passed to createUndoStack fires a draw.undochange event
163
+ const undoCallback = createUndoStack.mock.calls[0][0]
164
+ undoCallback(3)
165
+ expect(map.fire).toHaveBeenCalledWith('draw.undochange', { length: 3 })
166
+ })
167
+
168
+ test('reuses an existing undo stack', () => {
169
+ const existingUndoStack = { id: 'existing-stack' }
170
+ const { map } = setup({ existingUndoStack })
171
+
172
+ expect(createUndoStack).not.toHaveBeenCalled()
173
+ expect(map._undoStack).toBe(existingUndoStack)
174
+ })
175
+
176
+ test('initializes snapping with the default radius, rules, and resolved colours', () => {
177
+ const { map, mapProvider } = setup()
178
+
179
+ expect(initMapLibreSnap).toHaveBeenCalledWith(map, mapProvider.draw, {
180
+ layers: ['layer-a'],
181
+ radius: TOLERANCES.snapRadius,
182
+ rules: ['vertex', 'edge'],
183
+ colors: { vertex: 'resolved-vertex', edge: 'resolved-edge' }
184
+ })
185
+ })
186
+
187
+ test('a pluginConfig.snapRadius overrides the default snap radius', () => {
188
+ const { map, mapProvider } = setup({ pluginConfig: { snapRadius: 20 } })
189
+
190
+ expect(initMapLibreSnap).toHaveBeenCalledWith(map, mapProvider.draw, expect.objectContaining({
191
+ radius: 20
192
+ }))
193
+ })
194
+
195
+ test('resolves snap colours from mapStyle + pluginConfig', () => {
196
+ const pluginConfig = { snapVertex: '#custom' }
197
+ setup({ pluginConfig })
198
+
199
+ expect(resolveColors).toHaveBeenCalledWith('light', pluginConfig)
200
+ })
201
+ })
202
+
203
+ describe('createMapboxDraw – event handlers', () => {
204
+ test('MAP_SET_STYLE updates the current style and restyles on idle', () => {
205
+ const { map, eventBus } = setup()
206
+
207
+ const styleHandler = handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)
208
+ map._drawEditContainer = { querySelector: jest.fn(() => 'svg-el') }
209
+ styleHandler('dark')
210
+
211
+ expect(map._drawCurrentMapStyle).toBe('dark')
212
+
213
+ const idleCallback = handlerFor(map.once, 'idle')
214
+ idleCallback()
215
+ expect(updateDrawStyles).toHaveBeenCalledWith(map, 'dark', {})
216
+ expect(map._drawEditContainer.querySelector).toHaveBeenCalledWith('[data-im-draw-touch-target]')
217
+ expect(applyTouchVertexColors).toHaveBeenCalledWith('svg-el', 'dark', {})
218
+ })
219
+
220
+ test('MAP_SET_STYLE idle handler tolerates a missing edit container', () => {
221
+ const { map, eventBus } = setup()
222
+
223
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
224
+ handlerFor(map.once, 'idle')()
225
+
226
+ expect(applyTouchVertexColors).toHaveBeenCalledWith(undefined, 'dark', {})
227
+ })
228
+
229
+ test('MAP_SET_STYLE passes pluginConfig through to restyling and touch colours', () => {
230
+ const pluginConfig = { editStroke: '#custom' }
231
+ const { map, eventBus } = setup({ pluginConfig })
232
+
233
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
234
+ handlerFor(map.once, 'idle')()
235
+
236
+ expect(updateDrawStyles).toHaveBeenCalledWith(map, 'dark', pluginConfig)
237
+ expect(applyTouchVertexColors).toHaveBeenCalledWith(undefined, 'dark', pluginConfig)
238
+ })
239
+
240
+ test('draw.interfacetypechange is not handled here — the adapter normalises it onto the bus', () => {
241
+ const { map } = setup()
242
+ expect(map.on).not.toHaveBeenCalledWith('draw.interfacetypechange', expect.any(Function))
243
+ })
244
+
245
+ test('MAP_SET_SIZE fires a scale change using the size lookup', () => {
246
+ const { map, eventBus } = setup()
247
+
248
+ handlerFor(eventBus.on, EVENTS.MAP_SET_SIZE)('medium')
249
+
250
+ expect(map.fire).toHaveBeenCalledWith('draw.scalechange', { scale: MAP_SIZE_SCALES.medium })
251
+ })
252
+ })
253
+
254
+ describe('createMapboxDraw – cleanup', () => {
255
+ test('remove() detaches listeners, disables draw and clears the adapter reference', () => {
256
+ const { mapProvider, eventBus, removeWorkaround, result } = setup()
257
+ const draw = mapProvider.draw
258
+
259
+ result.remove()
260
+
261
+ expect(removeWorkaround).toHaveBeenCalledTimes(1)
262
+ expect(eventBus.off).toHaveBeenCalledWith(EVENTS.MAP_SET_STYLE, expect.any(Function))
263
+ expect(eventBus.off).toHaveBeenCalledWith(EVENTS.MAP_SET_SIZE, expect.any(Function))
264
+ expect(draw.changeMode).toHaveBeenCalledWith('disabled')
265
+ expect(mapProvider.draw).toBeNull()
266
+ expect(mapProvider._mapboxDrawInstance).toBe(draw)
267
+ })
268
+ })
@@ -0,0 +1,39 @@
1
+ import { COLORS, TOLERANCES } from './defaults.js'
2
+ import { applyMapboxSnapPatches } from './snap/prototypePatches.js'
3
+ import { pollUntil } from './snap/sourceData.js'
4
+ import { createSnapInstance } from './snap/snapInstance.js'
5
+ import { registerStyleLoadHandler, registerZoomHandlers } from './snap/mapHandlers.js'
6
+ import { DRAW_HOT_SOURCE } from './snap/constants.js'
7
+
8
+ /** Initialize MapboxSnap with MapLibre + MapboxDraw */
9
+ export function initMapLibreSnap (map, draw, snapOptions = {}) {
10
+ // Prevent multiple initializations (causes event listener duplication)
11
+ if (map._snapInitialized) {
12
+ return map._snapInstance
13
+ }
14
+ map._snapInitialized = true
15
+
16
+ const {
17
+ layers = [],
18
+ radius = TOLERANCES.snapRadius,
19
+ rules = ['vertex', 'midpoint', 'edge'],
20
+ status = false,
21
+ onSnapped = () => {},
22
+ colors = {}
23
+ } = snapOptions
24
+ const config = { layers, radius, rules, status, onSnapped }
25
+
26
+ // Apply global patches to the MapboxSnap prototype
27
+ applyMapboxSnapPatches({ vertex: COLORS.snapVertex, midpoint: COLORS.snapMidpoint, edge: COLORS.snapEdge, ...colors })
28
+
29
+ registerStyleLoadHandler(map, draw, config)
30
+ registerZoomHandlers(map)
31
+
32
+ // Initial setup - poll until the draw source exists
33
+ pollUntil(
34
+ () => map._removed ? null : map.getSource(DRAW_HOT_SOURCE),
35
+ (source) => createSnapInstance(map, draw, source, config)
36
+ )
37
+
38
+ return map._snapInstance
39
+ }
@@ -0,0 +1,91 @@
1
+ import { applyMapboxSnapPatches } from './snap/prototypePatches.js'
2
+ import { registerStyleLoadHandler, registerZoomHandlers } from './snap/mapHandlers.js'
3
+ import { createSnapInstance } from './snap/snapInstance.js'
4
+ import { initMapLibreSnap } from './mapboxSnap.js'
5
+
6
+ const DRAW_SOURCE = 'mapbox-gl-draw-hot'
7
+
8
+ jest.mock('./snap/prototypePatches.js', () => ({ applyMapboxSnapPatches: jest.fn() }))
9
+ jest.mock('./snap/mapHandlers.js', () => ({
10
+ registerStyleLoadHandler: jest.fn(),
11
+ registerZoomHandlers: jest.fn()
12
+ }))
13
+ jest.mock('./snap/snapInstance.js', () => ({
14
+ createSnapInstance: jest.fn((map) => {
15
+ map._snapInstance = { id: 'snap' }
16
+ return map._snapInstance
17
+ })
18
+ }))
19
+ jest.mock('./snap/sourceData.js', () => ({
20
+ pollUntil: jest.fn((checkFn, onSuccess) => {
21
+ const result = checkFn()
22
+ if (result) {
23
+ onSuccess(result)
24
+ }
25
+ })
26
+ }))
27
+ jest.mock('./defaults.js', () => ({
28
+ COLORS: { snapVertex: 'v', snapMidpoint: 'm', snapEdge: 'e' },
29
+ TOLERANCES: { snapRadius: 12 }
30
+ }))
31
+
32
+ const makeMap = (overrides = {}) => ({
33
+ getSource: jest.fn(() => ({ id: 'hot' })),
34
+ ...overrides
35
+ })
36
+
37
+ beforeEach(() => {
38
+ jest.clearAllMocks()
39
+ })
40
+
41
+ describe('initMapLibreSnap', () => {
42
+ test('returns the existing instance and skips setup when already initialized', () => {
43
+ const map = { _snapInitialized: true, _snapInstance: { id: 'existing' } }
44
+
45
+ expect(initMapLibreSnap(map, {}, {})).toEqual({ id: 'existing' })
46
+ expect(applyMapboxSnapPatches).not.toHaveBeenCalled()
47
+ expect(registerStyleLoadHandler).not.toHaveBeenCalled()
48
+ })
49
+
50
+ test('applies patches, registers handlers and creates the instance', () => {
51
+ const map = makeMap()
52
+ const draw = { id: 'draw' }
53
+
54
+ const result = initMapLibreSnap(map, draw, { layers: ['a'] })
55
+
56
+ expect(applyMapboxSnapPatches).toHaveBeenCalledWith({ vertex: 'v', midpoint: 'm', edge: 'e' })
57
+ expect(registerStyleLoadHandler).toHaveBeenCalledWith(map, draw, expect.objectContaining({ layers: ['a'], radius: 12 }))
58
+ expect(registerZoomHandlers).toHaveBeenCalledWith(map)
59
+ expect(createSnapInstance).toHaveBeenCalledWith(map, draw, { id: 'hot' }, expect.any(Object))
60
+ expect(map.getSource).toHaveBeenCalledWith(DRAW_SOURCE)
61
+ expect(result).toEqual({ id: 'snap' })
62
+ })
63
+
64
+ test('merges custom snap colours over the defaults', () => {
65
+ initMapLibreSnap(makeMap(), {}, { colors: { vertex: 'custom' } })
66
+ expect(applyMapboxSnapPatches).toHaveBeenCalledWith({ vertex: 'custom', midpoint: 'm', edge: 'e' })
67
+ })
68
+
69
+ test('uses the configured default radius when none is supplied', () => {
70
+ initMapLibreSnap(makeMap(), {}, {})
71
+ expect(registerStyleLoadHandler.mock.calls[0][2].radius).toBe(12)
72
+ })
73
+
74
+ test('defaults snapOptions to an empty object when omitted', () => {
75
+ const map = makeMap()
76
+ expect(() => initMapLibreSnap(map, {})).not.toThrow()
77
+ expect(createSnapInstance).toHaveBeenCalled()
78
+ })
79
+
80
+ test('provides a no-op default onSnapped handler', () => {
81
+ initMapLibreSnap(makeMap(), {}, {})
82
+ const passedConfig = registerStyleLoadHandler.mock.calls[0][2]
83
+ expect(passedConfig.onSnapped()).toBeUndefined()
84
+ })
85
+
86
+ test('does not create an instance when the map has already been removed', () => {
87
+ const map = makeMap({ _removed: true })
88
+ initMapLibreSnap(map, {}, {})
89
+ expect(createSnapInstance).not.toHaveBeenCalled()
90
+ })
91
+ })
@@ -0,0 +1,58 @@
1
+ import { createLifecycle } from './drawMode/lifecycle.js'
2
+ import { createClickHandlers } from './drawMode/clickHandlers.js'
3
+ import { createUndoHandlers } from './drawMode/undoHandlers.js'
4
+ import { createKeyboardHandlers } from './drawMode/keyboardHandlers.js'
5
+ import { createPointerHandlers } from './drawMode/pointerHandlers.js'
6
+ import { createRenderHelpers } from './drawMode/renderHelpers.js'
7
+
8
+ /**
9
+ * Factory function to create a draw mode for either polygons or lines.
10
+ * Shared behaviour (event handling, snap detection, undo, rubber band, keyboard
11
+ * drawing) is split across ./drawMode/* handler modules and composed here.
12
+ *
13
+ * @param {Object} ParentMode - DrawPolygon or DrawLineString from mapbox-gl-draw
14
+ * @param {Object} config - Configuration for the mode
15
+ * @param {string} config.featureProp - Property name on state ('polygon' or 'line')
16
+ * @param {string} config.geometryType - 'Polygon' or 'LineString'
17
+ * @param {Function} config.getCoords - Function to get coordinates from feature
18
+ * @param {Function} config.validateClick - Validation function for clicks
19
+ * @param {Function} config.getPlacedCoords - Function to get placed vertex coordinates from a display geojson
20
+ */
21
+ export const createDrawMode = (ParentMode, config) => {
22
+ const {
23
+ featureProp,
24
+ geometryType,
25
+ getCoords,
26
+ validateClick,
27
+ getPlacedCoords,
28
+ excludeFeatureIdFromSetup = false,
29
+ finishOnInvalidClick = false // For lines: finish when clicking same spot (like double-click)
30
+ } = config
31
+
32
+ const deps = {
33
+ ParentMode,
34
+ featureProp,
35
+ geometryType,
36
+ getCoords,
37
+ validateClick,
38
+ getPlacedCoords,
39
+ excludeFeatureIdFromSetup,
40
+ finishOnInvalidClick,
41
+ getFeature: (state) => state[featureProp],
42
+ // ring is [...placed, last_placed, rubber_band]; splice(-2,1) removes last_placed
43
+ RUBBER_BAND_OFFSET: 2,
44
+ // Only these keys signal a switch to keyboard drawing (matches the OL adapter's
45
+ // drawInput) — modifier keys and shortcut chords like cmd+z must not show the crosshair
46
+ INTERFACE_KEYS: new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter'])
47
+ }
48
+
49
+ return {
50
+ ...ParentMode,
51
+ ...createLifecycle(deps),
52
+ ...createClickHandlers(deps),
53
+ ...createUndoHandlers(deps),
54
+ ...createKeyboardHandlers(deps),
55
+ ...createPointerHandlers(deps),
56
+ ...createRenderHelpers(deps)
57
+ }
58
+ }
@@ -0,0 +1,53 @@
1
+ import { createDrawMode } from './createDrawMode.js'
2
+
3
+ /**
4
+ * Tests for the factory's own job: composing ParentMode with every handler group into a
5
+ * single mode object. The behaviour of each handler group is covered in its colocated
6
+ * test file under drawMode/ (lifecycle, click, keyboard, undo, pointer, render).
7
+ */
8
+
9
+ const POLYGON_CONFIG = {
10
+ featureProp: 'polygon',
11
+ geometryType: 'Polygon',
12
+ getCoords: (f) => f.coordinates[0],
13
+ validateClick: () => true,
14
+ getPlacedCoords: () => []
15
+ }
16
+
17
+ const build = (parent = {}, config = POLYGON_CONFIG) => createDrawMode(parent, config)
18
+
19
+ describe('createDrawMode composition', () => {
20
+ test('assembles a mode object with methods from every handler group', () => {
21
+ const mode = build()
22
+ const contributed = [
23
+ 'onSetup', 'onStop', // lifecycle
24
+ 'onClick', 'onTap', 'doClick', 'onVertexButtonClick', 'onCreate', // click
25
+ 'pushDrawUndo', 'undoVertex', 'onUndo', // undo
26
+ 'onKeydown', 'onKeyup', 'onKeyUp', // keyboard
27
+ 'onMove', 'onMouseMove', 'onTouchStart', 'onBlur', // pointer
28
+ 'toDisplayFeatures', '_showCrossHair', '_simulateMouse' // render
29
+ ]
30
+ for (const method of contributed) {
31
+ expect(typeof mode[method]).toBe('function')
32
+ }
33
+ })
34
+
35
+ test('preserves non-overridden ParentMode members and overrides shared ones', () => {
36
+ const parent = { parentOnly () {}, onClick: 'parent-onclick' }
37
+ const mode = build(parent)
38
+ expect(mode.parentOnly).toBe(parent.parentOnly) // passed through
39
+ expect(typeof mode.onClick).toBe('function') // overridden by the click handlers
40
+ expect(mode.onClick).not.toBe(parent.onClick)
41
+ })
42
+
43
+ test('passes config through so the getFeature accessor reads the configured feature prop', () => {
44
+ const polygonMode = build()
45
+ const lineMode = build({}, { ...POLYGON_CONFIG, featureProp: 'line' })
46
+ // dispatchVertexChange is config-agnostic, but pushDrawUndo reads getFeature(state).id
47
+ const map = { _undoStack: { push: jest.fn() }, _undoInProgress: false }
48
+ polygonMode.pushDrawUndo.call({ map }, { polygon: { id: 'p1' } })
49
+ lineMode.pushDrawUndo.call({ map }, { line: { id: 'l1' } })
50
+ expect(map._undoStack.push).toHaveBeenNthCalledWith(1, expect.objectContaining({ featureId: 'p1' }))
51
+ expect(map._undoStack.push).toHaveBeenNthCalledWith(2, expect.objectContaining({ featureId: 'l1' }))
52
+ })
53
+ })