@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,78 @@
1
+ /**
2
+ * Keyboard handling for the shared draw mode: cmd/ctrl+z undo, arrow/Enter keyboard
3
+ * drawing, and Escape cancel/reinitialise. Part of createDrawMode.
4
+ *
5
+ * Note the two distinct handlers: onKeyup (lowercase) is a window listener; onKeyUp
6
+ * (capital U) is mapbox-gl-draw's own, registered on the container.
7
+ */
8
+ export const createKeyboardHandlers = ({ ParentMode, getFeature, INTERFACE_KEYS }) => ({
9
+ onKeydown (state, e) {
10
+ if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
11
+ this._handleUndoKeydown(state, e)
12
+ return
13
+ }
14
+ if (document.activeElement !== state.container) {
15
+ return
16
+ }
17
+ if (e.key === 'Escape') {
18
+ e.preventDefault()
19
+ return
20
+ }
21
+ if (e.key === 'Enter') {
22
+ state.isActive = true
23
+ }
24
+ if (!INTERFACE_KEYS.has(e.key)) {
25
+ return
26
+ }
27
+ this._setInterface(state, 'keyboard')
28
+ this.onMove(state, e)
29
+ },
30
+
31
+ onKeyup (state, e) {
32
+ if (e.key === 'Escape') {
33
+ if (state.interfaceType !== 'keyboard') {
34
+ // Mouse/touch: cancel drawing — onKeyUp (capital U) won't fire since container isn't focused
35
+ this.map.fire('draw.cancel')
36
+ }
37
+ // Keyboard: onKeyUp (capital U) handles reinitialize (container is focused, event reaches it)
38
+ return
39
+ }
40
+ if (document.activeElement !== state.container) {
41
+ return
42
+ }
43
+ if (!INTERFACE_KEYS.has(e.key)) {
44
+ return
45
+ }
46
+ this._setInterface(state, 'keyboard')
47
+ this.onMove(state, e)
48
+ if (e.key === 'Enter' && state.isActive) {
49
+ this.doClick(state)
50
+ }
51
+ },
52
+
53
+ // Called by mapbox-gl-draw's event system (capital U — distinct from onKeyup above).
54
+ // Registered on ctx.container, so only fires when the viewport has focus (keyboard drawing).
55
+ // 1. A UI element inside the viewport has focus (e.g. popup menu) → ignore, let React handle
56
+ // 2. Keyboard drawing (container focused, interfaceType === 'keyboard') → Escape restarts
57
+ // 3. Non-keyboard with container focused → skip (already handled by window onKeyup via draw.cancel)
58
+ onKeyUp (state, e) {
59
+ const activeEl = document.activeElement
60
+ if (activeEl && activeEl !== state.container && state.container.contains(activeEl)) {
61
+ return
62
+ }
63
+ if (e.key === 'Escape') {
64
+ if (state.interfaceType === 'keyboard') {
65
+ const undoStack = this.map._undoStack
66
+ if (undoStack) {
67
+ undoStack.clear()
68
+ }
69
+ this._reinitializeFeature(state, getFeature(state))
70
+ }
71
+ // Non-keyboard already handled by onKeyup (window) via draw.cancel
72
+ return
73
+ }
74
+ if (activeEl !== state.container) {
75
+ ParentMode.onKeyUp.call(this, state, e)
76
+ }
77
+ }
78
+ })
@@ -0,0 +1,75 @@
1
+ import { setup, clickAt, firedWith, DrawPolygonMode } from './__helpers__/harness.js'
2
+
3
+ describe('keyboard interface', () => {
4
+ test('arrow keys switch to keyboard interface, show the crosshair and move the rubber band to center', () => {
5
+ const { ctx, state, marker, container } = setup(DrawPolygonMode)
6
+ clickAt(ctx, state, 0, 0)
7
+ container.focus()
8
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }))
9
+ expect(state.interfaceType).toBe('keyboard')
10
+ expect(marker.style.display).toBe('block')
11
+ expect(state.polygon.coordinates[0].at(-1)).toEqual([5, 5])
12
+ expect(firedWith(ctx.map, 'draw.geometrychange').length).toBeGreaterThan(0)
13
+ })
14
+
15
+ test('other keys, unfocused container and Escape do not switch interface', () => {
16
+ const { ctx, state, marker, container } = setup(DrawPolygonMode)
17
+ ctx.keydownHandler(new KeyboardEvent('keydown', { key: 'ArrowRight' })) // container not focused
18
+ container.focus()
19
+ ctx.keydownHandler(new KeyboardEvent('keydown', { key: 'a' }))
20
+ ctx.keydownHandler(new KeyboardEvent('keydown', { key: 'Escape' }))
21
+ ctx.keyupHandler(new KeyboardEvent('keyup', { key: 'a' }))
22
+ expect(state.interfaceType).toBe('mouse')
23
+ expect(marker.style.display).toBe('none')
24
+ })
25
+
26
+ test('keyup without focus is ignored', () => {
27
+ const { ctx, state } = setup(DrawPolygonMode)
28
+ ctx.keyupHandler(new KeyboardEvent('keyup', { key: 'ArrowRight' }))
29
+ expect(state.interfaceType).toBe('mouse')
30
+ })
31
+
32
+ test('Enter down then up places a vertex at center', () => {
33
+ const { ctx, state, container } = setup(DrawPolygonMode)
34
+ container.focus()
35
+ ctx.keydownHandler(new KeyboardEvent('keydown', { key: 'Enter' }))
36
+ ctx.keyupHandler(new KeyboardEvent('keyup', { key: 'Enter' }))
37
+ expect(state.isActive).toBe(true)
38
+ expect(state.polygon.coordinates[0][0]).toEqual([5, 5])
39
+ })
40
+
41
+ test('Escape keyup cancels drawing for mouse/touch but not keyboard', () => {
42
+ const { ctx, state } = setup(DrawPolygonMode)
43
+ ctx.keyupHandler(new KeyboardEvent('keyup', { key: 'Escape' }))
44
+ expect(firedWith(ctx.map, 'draw.cancel')).toHaveLength(1)
45
+
46
+ state.interfaceType = 'keyboard'
47
+ ctx.keyupHandler(new KeyboardEvent('keyup', { key: 'Escape' }))
48
+ expect(firedWith(ctx.map, 'draw.cancel')).toHaveLength(1)
49
+ })
50
+
51
+ test('keyup of a non-Enter interface key while focused switches interface without committing a vertex', () => {
52
+ const { ctx, state, container } = setup(DrawPolygonMode)
53
+ container.focus()
54
+ ctx.keyupHandler(new KeyboardEvent('keyup', { key: 'ArrowRight' }))
55
+ expect(state.interfaceType).toBe('keyboard')
56
+ expect(ctx.map._undoStack.length).toBe(0)
57
+ })
58
+
59
+ test('container keyUp with the container itself focused and a non-Escape key does not defer to the parent', () => {
60
+ const { ctx, state, container } = setup(DrawPolygonMode)
61
+ container.focus()
62
+ ctx.onKeyUp(state, { key: 'a' })
63
+ expect(ctx.changeMode).not.toHaveBeenCalled()
64
+ })
65
+
66
+ test('Escape during keyboard drawing with no undo stack still reinitialises the feature', () => {
67
+ const { ctx, state, container } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
68
+ clickAt(ctx, state, 0, 0)
69
+ container.focus()
70
+ const id = state.polygon.id
71
+ ctx.map._undoStack = null
72
+ ctx.onKeyUp(state, { key: 'Escape' })
73
+ expect(ctx._ctx.store.delete).toHaveBeenCalledWith([id])
74
+ })
75
+ })
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Setup / teardown for the shared draw mode: binds the window/container/map event
3
+ * handlers on entry and removes them on exit. Part of createDrawMode.
4
+ */
5
+ export const createLifecycle = ({ ParentMode, featureProp, excludeFeatureIdFromSetup }) => ({
6
+ onSetup (options) {
7
+ const { map } = this
8
+
9
+ // Some parent modes (DrawLineString) interpret featureId as "continue existing"
10
+ // rather than "use this ID for new feature"
11
+ const parentOptions = excludeFeatureIdFromSetup
12
+ ? { ...options, featureId: null }
13
+ : options
14
+
15
+ const state = {
16
+ ...ParentMode.onSetup.call(this, parentOptions),
17
+ ...options
18
+ }
19
+
20
+ // Add initial props
21
+ state[featureProp].properties = options.properties
22
+
23
+ const { container, vertexMarkerId, getInterfaceType } = state
24
+ const currentInterfaceType = getInterfaceType ? getInterfaceType() : state.interfaceType
25
+ state.interfaceType = currentInterfaceType
26
+ const vertexMarker = container.querySelector(`#${vertexMarkerId}`)
27
+ state.vertexMarker = vertexMarker
28
+ if (['touch', 'keyboard'].includes(currentInterfaceType)) {
29
+ this._showCrossHair(state)
30
+ } else {
31
+ this._hideCrossHair(state)
32
+ }
33
+
34
+ // Bind all handlers once
35
+ const bind = (name, fn) => (this[name] = fn.bind(this, state))
36
+ const handlers = {
37
+ keydownHandler: this.onKeydown,
38
+ keyupHandler: this.onKeyup,
39
+ blurHandler: this.onBlur,
40
+ createHandler: this.onCreate,
41
+ moveHandler: this.onMove,
42
+ pointerdownHandler: this.onPointerdown,
43
+ pointermoveHandler: this.onPointermove,
44
+ pointerupHandler: this.onPointerup,
45
+ vertexButtonClickHandler: this.onVertexButtonClick,
46
+ undoHandler: this.onUndo,
47
+ interfaceTypeChangeHandler: this.onInterfaceTypeChange
48
+ }
49
+ Object.entries(handlers).forEach(([k, fn]) => bind(k, fn))
50
+
51
+ // Register events
52
+ this._listeners = [
53
+ [window, 'keydown', this.keydownHandler],
54
+ [window, 'keyup', this.keyupHandler],
55
+ [window, 'click', this.vertexButtonClickHandler],
56
+ [container, 'blur', this.blurHandler],
57
+ [container, 'pointermove', this.pointermoveHandler],
58
+ [container, 'pointerup', this.pointerupHandler],
59
+ [map, 'pointerdown', this.pointerdownHandler],
60
+ [map, 'draw.create', this.createHandler],
61
+ [map, 'move', this.moveHandler],
62
+ [map, 'draw.undo', this.undoHandler],
63
+ [map, 'draw.interfacetypechange', this.interfaceTypeChangeHandler]
64
+ ]
65
+ this._listeners.forEach(([t, e, h]) => t.addEventListener ? t.addEventListener(e, h) : t.on(e, h))
66
+
67
+ return state
68
+ },
69
+
70
+ onStop (state) {
71
+ ParentMode.onStop.call(this, state)
72
+ this._listeners.forEach(([t, e, h]) => t.removeEventListener ? t.removeEventListener(e, h) : t.off(e, h))
73
+ this._hideCrossHair(state)
74
+ // Sync the final interfaceType from draw mode back to app state so crosshair
75
+ // visibility is correct when exiting draw mode (e.g., if user switched from mouse to keyboard)
76
+ this.map.fire('draw.interfacetypechange', { interfaceType: state.interfaceType })
77
+ }
78
+ })
@@ -0,0 +1,48 @@
1
+ import { setup, firedWith, DrawPolygonMode, DrawLineMode } from './__helpers__/harness.js'
2
+
3
+ describe('setup and crosshair', () => {
4
+ test('mouse interface hides the vertex marker, keyboard shows it', () => {
5
+ const mouse = setup(DrawPolygonMode)
6
+ expect(mouse.marker.style.display).toBe('none')
7
+
8
+ const keyboard = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
9
+ expect(keyboard.marker.style.display).toBe('block')
10
+ })
11
+
12
+ test('getInterfaceType() takes precedence over the static interfaceType option', () => {
13
+ const { state, marker } = setup(DrawPolygonMode, { interfaceType: 'mouse', getInterfaceType: () => 'touch' })
14
+ expect(state.interfaceType).toBe('touch')
15
+ expect(marker.style.display).toBe('block')
16
+ })
17
+
18
+ test('a crossHair object is used instead of the vertex marker when provided', () => {
19
+ const crossHair = { show: jest.fn(), hide: jest.fn() }
20
+ const { ctx, state } = setup(DrawPolygonMode, { interfaceType: 'keyboard', crossHair })
21
+ expect(crossHair.show).toHaveBeenCalled()
22
+ ctx.pointermoveHandler({ pointerType: 'mouse' })
23
+ expect(crossHair.hide).toHaveBeenCalled()
24
+ ctx.onBlur(state, { target: document.body })
25
+ expect(crossHair.hide).toHaveBeenCalledTimes(2)
26
+ })
27
+
28
+ test('line mode creates a fresh feature even when featureId is passed', () => {
29
+ const { state } = setup(DrawLineMode)
30
+ expect(state.line).toBeDefined()
31
+ expect(state.featureId).toBe('shape-1')
32
+ })
33
+ })
34
+
35
+ describe('onStop', () => {
36
+ test('removes listeners, hides the crosshair and reports the final interface type', () => {
37
+ const { ctx, state, marker, container } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
38
+ ctx.onStop(state)
39
+ expect(marker.style.display).toBe('none')
40
+ expect(firedWith(ctx.map, 'draw.interfacetypechange')).toEqual([{ interfaceType: 'keyboard' }])
41
+
42
+ // Window/container/map listeners are gone: keydown no longer shows the crosshair
43
+ container.focus()
44
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }))
45
+ expect(marker.style.display).toBe('none')
46
+ expect(ctx.map.off).toHaveBeenCalledWith('draw.create', ctx.createHandler)
47
+ })
48
+ })
@@ -0,0 +1,103 @@
1
+ import {
2
+ getSnapInstance, isSnapEnabled, getSnapLngLat, triggerSnapAtPoint, triggerSnapAtCenter
3
+ } from '../../utils/snapHelpers.js'
4
+
5
+ /**
6
+ * Pointer / touch handling for the shared draw mode: touch and mouse interface
7
+ * switching, rubber-band movement (with snapping) and blur. Part of createDrawMode.
8
+ */
9
+ export const createPointerHandlers = ({ ParentMode, getFeature, getCoords }) => ({
10
+ onTouchStart (state, e) {
11
+ this._setInterface(state, 'touch')
12
+ this.onMove(state, e)
13
+ },
14
+
15
+ onTouchEnd (state, e) {
16
+ this._setInterface(state, 'touch')
17
+ this.onMove(state, e)
18
+ },
19
+
20
+ // The global interface type (e.g. switching to touch and panning via MoveControl)
21
+ // can change mid-session without any touch/pointer/key event ever landing on the
22
+ // map container, so this can't rely on onTouchStart/onPointerdown alone — refresh
23
+ // the rubber band immediately rather than waiting for the next incidental 'move'.
24
+ // Also fires once on every mode entry (DrawInit.jsx syncs setInterfaceType whenever
25
+ // draw_polygon/draw_line starts), so this must only show the crosshair for
26
+ // touch/keyboard — same as onSetup/onMove — not unconditionally like touch's own
27
+ // onTouchStart/onTouchEnd, or a mouse-driven session would flash it on every entry.
28
+ onInterfaceTypeChange (state, e) {
29
+ this._setInterface(state, e.interfaceType, ['touch', 'keyboard'].includes(e.interfaceType))
30
+ this.onMove(state)
31
+ },
32
+
33
+ onBlur (state, e) {
34
+ if (e.target !== state.container) {
35
+ this._hideCrossHair(state)
36
+ }
37
+ },
38
+
39
+ onMouseMove (state, e) {
40
+ if (isSnapEnabled(state)) {
41
+ const snap = getSnapInstance(this.map)
42
+ triggerSnapAtPoint(snap, this.map, e.point)
43
+
44
+ const snappedLngLat = getSnapLngLat(snap)
45
+ if (snappedLngLat) {
46
+ e = { ...e, lngLat: snappedLngLat }
47
+ }
48
+ }
49
+
50
+ ParentMode.onMouseMove.call(this, state, e)
51
+
52
+ // Fired after the parent updates the rubber band so the payload (and the live
53
+ // invalid-stroke check driven by it) reflects the current cursor position.
54
+ this.map.fire('draw.geometrychange', state.polygon || state.line)
55
+ },
56
+
57
+ onMove (state) {
58
+ if (['touch', 'keyboard'].includes(state.interfaceType)) {
59
+ if (isSnapEnabled(state)) {
60
+ triggerSnapAtCenter(getSnapInstance(this.map), this.map)
61
+ }
62
+
63
+ const snap = getSnapInstance(this.map)
64
+ const snappedLngLat = isSnapEnabled(state) && getSnapLngLat(snap)
65
+
66
+ if (snappedLngLat) {
67
+ const point = this.map.project([snappedLngLat.lng, snappedLngLat.lat])
68
+ ParentMode.onMouseMove.call(this, state, {
69
+ lngLat: snappedLngLat,
70
+ point,
71
+ originalEvent: new MouseEvent('mousemove', {
72
+ clientX: point.x,
73
+ clientY: point.y,
74
+ bubbles: true,
75
+ cancelable: true
76
+ })
77
+ })
78
+ this._ctx.store.render()
79
+ // Parity with _simulateMouse: report the rubber-band move so the live
80
+ // invalid-stroke check sees snapped touch/keyboard moves too.
81
+ this.map.fire('draw.geometrychange', state.polygon || state.line)
82
+ } else {
83
+ this._simulateMouse('mousemove', ParentMode.onMouseMove, state)
84
+ }
85
+ }
86
+ },
87
+
88
+ onPointerdown (state, e) {
89
+ if (e.pointerType !== 'touch') {
90
+ this._setInterface(state, 'mouse', false)
91
+ }
92
+ },
93
+
94
+ onPointermove (state, e) {
95
+ if (e.pointerType !== 'touch') {
96
+ this._hideCrossHair(state)
97
+ }
98
+ },
99
+
100
+ onPointerup (state) {
101
+ this.dispatchVertexChange(getCoords(getFeature(state)))
102
+ }
103
+ })
@@ -0,0 +1,127 @@
1
+ import { setup, clickAt, firedWith, activeSnap, DrawPolygonMode, DrawLineMode, CENTER } from './__helpers__/harness.js'
2
+
3
+ describe('touch and pointer interface', () => {
4
+ test('touch start/end switch to touch interface and show the crosshair', () => {
5
+ const { ctx, state, marker } = setup(DrawPolygonMode)
6
+ ctx.onTouchStart(state, {})
7
+ expect(state.interfaceType).toBe('touch')
8
+ expect(marker.style.display).toBe('block')
9
+ ctx.onTouchEnd(state, {})
10
+ expect(state.interfaceType).toBe('touch')
11
+ })
12
+
13
+ test('non-touch pointerdown switches back to mouse without showing the crosshair; touch does not', () => {
14
+ const { ctx, state, marker } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
15
+ ctx.map.fire('pointerdown', { pointerType: 'mouse' })
16
+ expect(state.interfaceType).toBe('mouse')
17
+ expect(marker.style.display).toBe('block') // unchanged, not re-shown or hidden
18
+
19
+ ctx.map.fire('pointerdown', { pointerType: 'touch' })
20
+ expect(state.interfaceType).toBe('mouse')
21
+ })
22
+
23
+ test('mouse pointermove hides the crosshair; touch pointermove does not', () => {
24
+ const { ctx, marker } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
25
+ ctx.pointermoveHandler({ pointerType: 'touch' })
26
+ expect(marker.style.display).toBe('block')
27
+ ctx.pointermoveHandler({ pointerType: 'mouse' })
28
+ expect(marker.style.display).toBe('none')
29
+ })
30
+
31
+ test('pointerup reports the current vertex count', () => {
32
+ const { ctx, state } = setup(DrawPolygonMode)
33
+ clickAt(ctx, state, 0, 0)
34
+ ctx.map.fire.mockClear()
35
+ ctx.pointerupHandler({})
36
+ expect(firedWith(ctx.map, 'draw.vertexchange')).toEqual([{ numVertecies: 1 }])
37
+ })
38
+
39
+ test('blur away from the container hides the crosshair; blur on the container does not', () => {
40
+ const { ctx, marker, container } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
41
+ ctx.blurHandler({ target: container })
42
+ expect(marker.style.display).toBe('block')
43
+ ctx.blurHandler({ target: document.body })
44
+ expect(marker.style.display).toBe('none')
45
+ })
46
+
47
+ test('draw.interfacetypechange (e.g. switching to touch and panning via MoveControl mid-session) updates the interface and refreshes the rubber band immediately', () => {
48
+ const { ctx, state } = setup(DrawPolygonMode)
49
+ clickAt(ctx, state, 0, 0)
50
+ ctx.map.fire('draw.interfacetypechange', { interfaceType: 'touch' })
51
+ expect(state.interfaceType).toBe('touch')
52
+ expect(state.polygon.coordinates[0].at(-1)).toEqual([CENTER.lng, CENTER.lat])
53
+ })
54
+
55
+ test('draw.interfacetypechange to touch/keyboard shows the crosshair; to mouse it does not', () => {
56
+ const { ctx, marker } = setup(DrawPolygonMode)
57
+ ctx.map.fire('draw.interfacetypechange', { interfaceType: 'touch' })
58
+ expect(marker.style.display).toBe('block')
59
+
60
+ marker.style.display = 'none' // reset, isolate the next assertion
61
+ ctx.map.fire('draw.interfacetypechange', { interfaceType: 'keyboard' })
62
+ expect(marker.style.display).toBe('block')
63
+ })
64
+
65
+ test('draw.interfacetypechange to mouse never shows the crosshair — fires on every mode entry regardless of device', () => {
66
+ const { ctx, marker } = setup(DrawPolygonMode)
67
+ ctx.map.fire('draw.interfacetypechange', { interfaceType: 'mouse' })
68
+ expect(marker.style.display).toBe('none')
69
+ })
70
+ })
71
+
72
+ describe('rubber band and snapping while moving', () => {
73
+ test('onMouseMove moves the rubber band, preferring the snapped position', () => {
74
+ const { ctx, state } = setup(DrawPolygonMode, { getSnapEnabled: () => true })
75
+ clickAt(ctx, state, 0, 0)
76
+ ctx.map._snapInstance = activeSnap()
77
+ ctx.onMouseMove(state, { lngLat: { lng: 3, lat: 3 }, point: { x: 3, y: 3 } })
78
+ expect(ctx.map._snapInstance.snapToClosestPoint).toHaveBeenCalled()
79
+ expect(state.polygon.coordinates[0].at(-1)).toEqual([9, 9])
80
+ expect(firedWith(ctx.map, 'draw.geometrychange').length).toBeGreaterThan(0)
81
+ })
82
+
83
+ test('onMouseMove without snap uses the pointer position', () => {
84
+ const { ctx, state } = setup(DrawPolygonMode)
85
+ clickAt(ctx, state, 0, 0)
86
+ ctx.onMouseMove(state, { lngLat: { lng: 3, lat: 3 }, point: { x: 3, y: 3 } })
87
+ expect(state.polygon.coordinates[0].at(-1)).toEqual([3, 3])
88
+ })
89
+
90
+ test('onMouseMove with snapping enabled but no snap point falls back to the pointer position', () => {
91
+ const { ctx, state } = setup(DrawPolygonMode, { getSnapEnabled: () => true })
92
+ clickAt(ctx, state, 0, 0)
93
+ ctx.map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
94
+ ctx.onMouseMove(state, { lngLat: { lng: 3, lat: 3 }, point: { x: 3, y: 3 } })
95
+ expect(state.polygon.coordinates[0].at(-1)).toEqual([3, 3])
96
+ })
97
+
98
+ test('map move in keyboard interface keeps the rubber band at center, or at the snap point when snapping', () => {
99
+ const { ctx, state } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
100
+ clickAt(ctx, state, 0, 0)
101
+ ctx.map.fire('move')
102
+ expect(state.polygon.coordinates[0].at(-1)).toEqual([CENTER.lng, CENTER.lat])
103
+
104
+ state.getSnapEnabled = () => true
105
+ ctx.map._snapInstance = activeSnap()
106
+ ctx.map.fire('move')
107
+ expect(ctx.map._snapInstance.snapToClosestPoint).toHaveBeenCalled()
108
+ expect(state.polygon.coordinates[0].at(-1)).toEqual([9, 9])
109
+ })
110
+
111
+ test('map move in mouse interface leaves the rubber band alone', () => {
112
+ const { ctx, state } = setup(DrawPolygonMode)
113
+ clickAt(ctx, state, 0, 0)
114
+ const before = [...state.polygon.coordinates[0].map((c) => [...c])]
115
+ ctx.map.fire('move')
116
+ expect(state.polygon.coordinates[0]).toEqual(before)
117
+ })
118
+
119
+ test('map move in keyboard line mode with snap fires geometrychange with state.line', () => {
120
+ const { ctx, state } = setup(DrawLineMode, { interfaceType: 'keyboard', getSnapEnabled: () => true })
121
+ clickAt(ctx, state, 0, 0)
122
+ ctx.map._snapInstance = activeSnap()
123
+ ctx.map.fire.mockClear()
124
+ ctx.map.fire('move')
125
+ expect(firedWith(ctx.map, 'draw.geometrychange').pop()).toBe(state.line)
126
+ })
127
+ })
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Rendering helpers for the shared draw mode: crosshair/vertex-marker visibility,
3
+ * interface switching, simulated mouse events at map centre, and the display-feature
4
+ * pass that adds a marker on every placed vertex. Part of createDrawMode.
5
+ */
6
+ export const createRenderHelpers = ({ ParentMode, geometryType, getFeature, getPlacedCoords }) => ({
7
+ _simulateMouse (type, fn, state) {
8
+ const { map } = this
9
+ const center = map.getCenter()
10
+ const point = map.project(center)
11
+ fn.call(this, state, {
12
+ lngLat: center,
13
+ point,
14
+ originalEvent: new MouseEvent(type, {
15
+ clientX: point.x,
16
+ clientY: point.y,
17
+ bubbles: true,
18
+ cancelable: true
19
+ })
20
+ })
21
+ this._ctx.store.render()
22
+
23
+ this.map.fire('draw.geometrychange', state.polygon || state.line)
24
+ },
25
+
26
+ _showCrossHair (state) {
27
+ if (state.crossHair) { state.crossHair.show() } else { state.vertexMarker.style.display = 'block' }
28
+ },
29
+
30
+ _hideCrossHair (state) {
31
+ if (state.crossHair) { state.crossHair.hide() } else { state.vertexMarker.style.display = 'none' }
32
+ },
33
+
34
+ _setInterface (state, type, show = true) {
35
+ state.interfaceType = type
36
+ if (show) {
37
+ this._showCrossHair(state)
38
+ }
39
+ },
40
+
41
+ toDisplayFeatures (state, geojson, display) {
42
+ ParentMode.toDisplayFeatures.call(this, state, geojson, display)
43
+
44
+ // Display features carry the id in properties.id (no top-level id)
45
+ const feature = getFeature(state)
46
+ if (geojson.geometry.type === geometryType && geojson.properties.id === feature.id) {
47
+ // Parent modes render only some placed vertices (which ones varies by
48
+ // mapbox-gl-draw version) — add a marker on every placed vertex. The
49
+ // 'draw-vertex' meta is display-only: it's not in mapbox-gl-draw's
50
+ // META_TYPES, so featuresAt ignores these markers and the parent's own
51
+ // vertex click targets (click first/last to finish) keep working.
52
+ getPlacedCoords(geojson).forEach((coordinates) => display({
53
+ type: 'Feature',
54
+ properties: { meta: 'draw-vertex', parent: feature.id, active: 'false' },
55
+ geometry: { type: 'Point', coordinates }
56
+ }))
57
+ }
58
+ }
59
+ })
@@ -0,0 +1,34 @@
1
+ import { setup, clickAt, DrawPolygonMode, DrawLineMode } from './__helpers__/harness.js'
2
+
3
+ describe('vertex marker display', () => {
4
+ const displayed = (ctx, state, geometry, id) => {
5
+ const display = jest.fn()
6
+ ctx.toDisplayFeatures(state, { type: 'Feature', properties: { id }, geometry }, display)
7
+ return display.mock.calls.map(([f]) => f).filter((f) => f.properties.meta === 'draw-vertex')
8
+ }
9
+
10
+ test('every placed polygon vertex gets a display-only draw-vertex marker', () => {
11
+ const { ctx, state } = setup(DrawPolygonMode)
12
+ clickAt(ctx, state, 0, 0)
13
+ clickAt(ctx, state, 10, 0)
14
+ clickAt(ctx, state, 10, 10)
15
+ const ring = [[0, 0], [10, 0], [10, 10], [5, 5], [0, 0]] // placed + rubber + closing
16
+ const markers = displayed(ctx, state, { type: 'Polygon', coordinates: [ring] }, state.polygon.id)
17
+ expect(markers.map((m) => m.geometry.coordinates)).toEqual([[0, 0], [10, 0], [10, 10]])
18
+ expect(markers[0].properties).toEqual({ meta: 'draw-vertex', parent: state.polygon.id, active: 'false' })
19
+ })
20
+
21
+ test('every placed line vertex gets a marker', () => {
22
+ const { ctx, state } = setup(DrawLineMode)
23
+ ctx.doClick(state)
24
+ const coords = [[5, 5], [8, 8]] // placed + rubber
25
+ const markers = displayed(ctx, state, { type: 'LineString', coordinates: coords }, state.line.id)
26
+ expect(markers.map((m) => m.geometry.coordinates)).toEqual([[5, 5]])
27
+ })
28
+
29
+ test('other features get no markers', () => {
30
+ const { ctx, state } = setup(DrawPolygonMode)
31
+ const ring = [[0, 0], [10, 0], [10, 10], [0, 0]]
32
+ expect(displayed(ctx, state, { type: 'Polygon', coordinates: [ring] }, 'other-feature')).toEqual([])
33
+ })
34
+ })