@defra/interactive-map 0.0.37-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 (244) 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-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  15. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  17. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  18. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  19. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  20. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  21. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  22. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  23. package/plugins/draw/dist/esm/index.js +1 -0
  24. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  26. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  27. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  28. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  29. package/plugins/draw/dist/umd/index.js +2 -0
  30. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  31. package/plugins/draw/src/DrawInit.jsx +89 -0
  32. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  33. package/plugins/draw/src/adapterEvents.js +127 -0
  34. package/plugins/draw/src/adapterEvents.test.js +20 -0
  35. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  36. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  37. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  38. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  39. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  40. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  41. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  42. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  43. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  44. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  45. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  46. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  47. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  48. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  49. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  86. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  87. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  88. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  89. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  95. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  96. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  97. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  98. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  99. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  100. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  101. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  102. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  103. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  104. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  105. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  106. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  107. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  108. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  109. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  110. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  111. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  112. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  113. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  114. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  115. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  118. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  119. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  120. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  121. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  144. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  145. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  146. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  147. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  148. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  149. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  156. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  157. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  158. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  159. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  163. package/plugins/draw/src/api/addFeature.js +22 -0
  164. package/plugins/draw/src/api/addFeature.test.js +41 -0
  165. package/plugins/draw/src/api/deleteFeature.js +11 -0
  166. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  167. package/plugins/draw/src/api/editFeature.js +54 -0
  168. package/plugins/draw/src/api/editFeature.test.js +134 -0
  169. package/plugins/draw/src/api/merge.js +30 -0
  170. package/plugins/draw/src/api/merge.test.js +57 -0
  171. package/plugins/draw/src/api/newLine.js +41 -0
  172. package/plugins/draw/src/api/newLine.test.js +93 -0
  173. package/plugins/draw/src/api/newPolygon.js +41 -0
  174. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  175. package/plugins/draw/src/api/split.js +117 -0
  176. package/plugins/draw/src/api/split.test.js +204 -0
  177. package/plugins/draw/src/defaults.js +52 -0
  178. package/plugins/draw/src/defaults.test.js +18 -0
  179. package/plugins/draw/src/draw.scss +43 -0
  180. package/plugins/draw/src/events.js +237 -0
  181. package/plugins/draw/src/events.test.js +369 -0
  182. package/plugins/draw/src/index.js +10 -0
  183. package/plugins/draw/src/index.test.js +24 -0
  184. package/plugins/draw/src/manifest.js +170 -0
  185. package/plugins/draw/src/manifest.test.js +147 -0
  186. package/plugins/draw/src/reducer.js +69 -0
  187. package/plugins/draw/src/reducer.test.js +85 -0
  188. package/plugins/draw/src/utils/debounce.js +16 -0
  189. package/plugins/draw/src/utils/debounce.test.js +40 -0
  190. package/plugins/draw/src/utils/eventBus.js +36 -0
  191. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  192. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  193. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  194. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  195. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  196. package/plugins/draw/src/utils/resolveColors.js +39 -0
  197. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  198. package/plugins/draw/src/utils/spatial.js +243 -0
  199. package/plugins/draw/src/utils/spatial.test.js +243 -0
  200. package/plugins/draw/src/utils/touchTarget.js +95 -0
  201. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  202. package/plugins/draw/src/utils/undoStack.js +31 -0
  203. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  204. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  205. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  206. package/plugins/draw/src/validation/liveStroke.js +85 -0
  207. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  208. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  209. package/plugins/draw/src/validation/rules.js +186 -0
  210. package/plugins/draw/src/validation/rules.test.js +120 -0
  211. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  212. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  213. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  214. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  215. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  216. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  217. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  218. package/plugins/search/dist/css/index.css +1 -317
  219. package/rollup.esm.mjs +35 -28
  220. package/sonar-project.properties +2 -2
  221. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  222. package/src/App/components/Hints/Hints.module.scss +5 -4
  223. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  224. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  225. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  226. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  227. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  228. package/src/App/hooks/useHintsAPI.js +67 -0
  229. package/src/App/hooks/useHintsAPI.test.js +145 -0
  230. package/src/App/renderer/PluginInits.jsx +4 -0
  231. package/src/App/renderer/PluginInits.test.jsx +7 -1
  232. package/src/InteractiveMap/InteractiveMap.js +20 -0
  233. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  234. package/src/config/events.js +4 -0
  235. package/src/scss/settings/_dimensions.scss +3 -0
  236. package/src/services/announcer.js +38 -3
  237. package/src/services/announcer.test.js +133 -1
  238. package/src/services/hints.js +3 -0
  239. package/src/services/hints.test.js +19 -0
  240. package/src/types.js +12 -0
  241. package/src/utils/detectInterfaceType.js +5 -1
  242. package/src/utils/isMac.js +15 -0
  243. package/src/utils/isMac.test.js +37 -0
  244. package/webpack.umd.mjs +1 -0
@@ -0,0 +1,369 @@
1
+ import { attachEvents } from './events.js'
2
+
3
+ jest.useFakeTimers()
4
+
5
+ const DRAW_EVENTS = ['create', 'editfinish', 'cancel', 'vertexselection', 'vertexchange', 'undochange', 'update', 'geometrychange', 'placementblocked', 'validitychange', 'canplacechange', 'interfacetypechange']
6
+
7
+ const setup = (overrides = {}) => {
8
+ const draw = {
9
+ getMode: jest.fn(() => 'draw_polygon'),
10
+ done: jest.fn(),
11
+ cancel: jest.fn(),
12
+ add: jest.fn(),
13
+ get: jest.fn(() => ({ id: 'F' })),
14
+ undo: jest.fn(),
15
+ setGeometryValid: jest.fn(),
16
+ setInvalid: jest.fn(),
17
+ deleteVertex: jest.fn(),
18
+ nudgeSelectedVertex: jest.fn(),
19
+ setSnapEnabled: jest.fn(),
20
+ isSnapEnabled: jest.fn(() => false),
21
+ changeMode: jest.fn(),
22
+ on: jest.fn(),
23
+ off: jest.fn()
24
+ }
25
+ const dispatch = jest.fn()
26
+ const pluginState = { dispatch, feature: { id: 'F' }, tempFeature: { id: 'T' }, snap: false, ...overrides.pluginState }
27
+ const mapProvider = { draw }
28
+ const eventBus = { emit: jest.fn() }
29
+ const hints = { show: jest.fn() }
30
+ const buttonConfig = { drawDone: {}, drawCancel: {}, drawUndo: {}, drawDeletePoint: {}, drawSnap: {}, ...overrides.buttonConfig }
31
+ const appState = { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'mouse' }
32
+ const appConfig = { id: 'app' }
33
+ const mapState = { mapSize: 'medium' }
34
+
35
+ const detach = attachEvents({ appState, appConfig, mapState, pluginState, mapProvider, buttonConfig, eventBus, hints })
36
+ return { draw, dispatch, pluginState, mapProvider, eventBus, hints, buttonConfig, detach }
37
+ }
38
+
39
+ const drawHandler = (draw, event) => draw.on.mock.calls.find(([name]) => name === event)[1]
40
+
41
+ beforeEach(() => jest.clearAllTimers())
42
+
43
+ describe('attachEvents – wiring', () => {
44
+ test('assigns onClick to each configured button', () => {
45
+ const { buttonConfig } = setup()
46
+ expect(typeof buttonConfig.drawDone.onClick).toBe('function')
47
+ expect(typeof buttonConfig.drawCancel.onClick).toBe('function')
48
+ expect(typeof buttonConfig.drawUndo.onClick).toBe('function')
49
+ expect(typeof buttonConfig.drawDeletePoint.onClick).toBe('function')
50
+ expect(typeof buttonConfig.drawSnap.onClick).toBe('function')
51
+ })
52
+
53
+ test('subscribes to every draw event', () => {
54
+ const { draw } = setup()
55
+ DRAW_EVENTS.forEach((event) => {
56
+ expect(draw.on).toHaveBeenCalledWith(event, expect.any(Function))
57
+ })
58
+ })
59
+
60
+ test('tolerates missing optional buttons', () => {
61
+ const { buttonConfig, detach } = setup({ buttonConfig: { drawDeletePoint: undefined, drawSnap: undefined } })
62
+ expect(typeof buttonConfig.drawDone.onClick).toBe('function')
63
+ expect(() => detach()).not.toThrow()
64
+ })
65
+ })
66
+
67
+ describe('button handlers', () => {
68
+ test('done finishes without resetting snap', () => {
69
+ const { buttonConfig, draw, dispatch } = setup()
70
+ buttonConfig.drawDone.onClick()
71
+ expect(dispatch).not.toHaveBeenCalledWith({ type: 'SET_SNAP', payload: false })
72
+ expect(draw.setSnapEnabled).not.toHaveBeenCalledWith(false)
73
+ expect(draw.done).toHaveBeenCalled()
74
+ })
75
+
76
+ test('cancel re-adds the feature when cancelling a vertex edit, and releases activeMoveTarget', () => {
77
+ const { buttonConfig, draw, dispatch, eventBus, mapProvider } = setup()
78
+ draw.getMode.mockReturnValue('edit_vertex')
79
+ drawHandler(draw, 'vertexselection')({ index: 1 })
80
+
81
+ buttonConfig.drawCancel.onClick()
82
+
83
+ expect(draw.add).toHaveBeenCalledWith({ id: 'F' })
84
+ expect(draw.cancel).toHaveBeenCalled()
85
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: null })
86
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null } })
87
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:cancelled', { id: 'F' })
88
+ expect(mapProvider.activeMoveTarget).toBeNull()
89
+ })
90
+
91
+ test('cancel does not re-add outside a vertex edit', () => {
92
+ const { buttonConfig, draw } = setup()
93
+ draw.getMode.mockReturnValue('draw_polygon')
94
+ buttonConfig.drawCancel.onClick()
95
+ expect(draw.add).not.toHaveBeenCalled()
96
+ })
97
+
98
+ test('cancel does not re-add a vertex edit without a temp feature', () => {
99
+ const { buttonConfig, draw } = setup({ pluginState: { tempFeature: null } })
100
+ draw.getMode.mockReturnValue('edit_vertex')
101
+ buttonConfig.drawCancel.onClick()
102
+ expect(draw.add).not.toHaveBeenCalled()
103
+ })
104
+
105
+ test('undo and delete-vertex delegate to the adapter', () => {
106
+ const { buttonConfig, draw } = setup()
107
+ buttonConfig.drawUndo.onClick()
108
+ buttonConfig.drawDeletePoint.onClick()
109
+ expect(draw.undo).toHaveBeenCalled()
110
+ expect(draw.deleteVertex).toHaveBeenCalled()
111
+ })
112
+
113
+ test('snap toggles state and syncs the adapter', () => {
114
+ const { buttonConfig, draw, dispatch } = setup({ pluginState: { snap: false } })
115
+ buttonConfig.drawSnap.onClick()
116
+ expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_SNAP' })
117
+ expect(draw.setSnapEnabled).toHaveBeenCalledWith(true)
118
+ })
119
+ })
120
+
121
+ describe('draw event handlers', () => {
122
+ test('create resets state, preserves snap, disables mode asynchronously and emits', () => {
123
+ const { draw, dispatch, eventBus } = setup()
124
+ drawHandler(draw, 'create')({ id: 'new' })
125
+
126
+ expect(dispatch).not.toHaveBeenCalledWith({ type: 'SET_SNAP', payload: false })
127
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: null })
128
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:created', { id: 'new' })
129
+
130
+ jest.runAllTimers()
131
+ expect(draw.changeMode).toHaveBeenCalledWith('disabled')
132
+ })
133
+
134
+ test('editfinish resets state and emits draw:edited', () => {
135
+ const { draw, eventBus } = setup()
136
+ drawHandler(draw, 'editfinish')({ id: 'edited' })
137
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:edited', { id: 'edited' })
138
+ jest.runAllTimers()
139
+ expect(draw.changeMode).toHaveBeenCalledWith('disabled')
140
+ })
141
+
142
+ test('re-opens an invalid finished shape in edit mode instead of creating it', () => {
143
+ const { draw, eventBus, hints } = setup()
144
+ const bowtie = { id: 'bad', type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 1], [1, 0], [0, 1], [0, 0]]] } }
145
+ drawHandler(draw, 'create')(bowtie)
146
+ expect(eventBus.emit).not.toHaveBeenCalledWith('draw:created', bowtie)
147
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:geometryinvalid', expect.objectContaining({ reason: expect.stringMatching(/intersect/i) }))
148
+ expect(hints.show).toHaveBeenCalledWith(expect.stringMatching(/intersect/i))
149
+ jest.runAllTimers()
150
+ const editCall = draw.changeMode.mock.calls.find(([mode]) => mode === 'edit_vertex')
151
+ expect(editCall[1]).toEqual(expect.objectContaining({ featureId: 'bad' }))
152
+ // the wired getSnapEnabled option delegates to the adapter
153
+ editCall[1].getSnapEnabled()
154
+ expect(draw.isSnapEnabled).toHaveBeenCalled()
155
+ })
156
+
157
+ test('edit finish of a drawn-then-fixed shape reports as a creation', () => {
158
+ const { draw, eventBus } = setup()
159
+ // An invalid finished shape is re-opened for fixing (marks it as a pending creation)…
160
+ const bowtie = { id: 'bad', type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 1], [1, 0], [0, 1], [0, 0]]] } }
161
+ drawHandler(draw, 'create')(bowtie)
162
+ jest.runAllTimers()
163
+ // …so when its edit finishes, it reports as a creation, exactly once.
164
+ drawHandler(draw, 'editfinish')({ id: 'bad' })
165
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:created', { id: 'bad' })
166
+ drawHandler(draw, 'editfinish')({ id: 'bad' })
167
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:edited', { id: 'bad' })
168
+ })
169
+
170
+ test('cancel handler is a no-op', () => {
171
+ const { draw } = setup()
172
+ expect(() => drawHandler(draw, 'cancel')()).not.toThrow()
173
+ })
174
+
175
+ test('vertexselection dispatches, emits, and claims mapProvider.activeMoveTarget for MoveControl', () => {
176
+ const { draw, dispatch, eventBus, mapProvider } = setup()
177
+ drawHandler(draw, 'vertexselection')({ index: 2 })
178
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_VERTEX_INDEX', payload: { index: 2 } })
179
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:vertexselection', { index: 2 })
180
+ expect(mapProvider.activeMoveTarget).toMatchObject({ label: 'vertex' })
181
+
182
+ mapProvider.activeMoveTarget.move(1, 0, true)
183
+ expect(draw.nudgeSelectedVertex).toHaveBeenCalledWith(1, 0, true)
184
+ })
185
+
186
+ test('vertexselection releases activeMoveTarget when the index is deselected', () => {
187
+ const { draw, mapProvider } = setup()
188
+ drawHandler(draw, 'vertexselection')({ index: 2 })
189
+ drawHandler(draw, 'vertexselection')({ index: -1 })
190
+ expect(mapProvider.activeMoveTarget).toBeNull()
191
+ })
192
+
193
+ test('vertexchange resets the selected index with the new count and releases activeMoveTarget', () => {
194
+ const { draw, dispatch, mapProvider } = setup()
195
+ drawHandler(draw, 'vertexselection')({ index: 2 })
196
+ drawHandler(draw, 'vertexchange')({ numVertices: 5 })
197
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_VERTEX_INDEX', payload: { index: -1, numVertices: 5 } })
198
+ expect(mapProvider.activeMoveTarget).toBeNull()
199
+ })
200
+
201
+ test('undochange dispatches the stack length', () => {
202
+ const { draw, dispatch } = setup()
203
+ drawHandler(draw, 'undochange')(4)
204
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_UNDO_STACK_LENGTH', payload: 4 })
205
+ })
206
+
207
+ test('update emits draw:updated', () => {
208
+ const { draw, eventBus } = setup()
209
+ drawHandler(draw, 'update')({ id: 'u' })
210
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:updated', { id: 'u' })
211
+ })
212
+ })
213
+
214
+ describe('geometrychange validation', () => {
215
+ const squareFeature = {
216
+ type: 'Feature',
217
+ geometry: { type: 'Polygon', coordinates: [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]] }
218
+ }
219
+ const bowtieFeature = {
220
+ type: 'Feature',
221
+ geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 1], [1, 0], [0, 1], [0, 0]]] }
222
+ }
223
+ const collinearFeature = {
224
+ type: 'Feature',
225
+ geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [2, 0], [0, 0]]] }
226
+ }
227
+ const partDrawnFeature = {
228
+ type: 'Feature',
229
+ geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [0, 0]]] }
230
+ }
231
+
232
+ test('ignores preview payloads that carry no phase', () => {
233
+ const { draw, dispatch } = setup()
234
+ drawHandler(draw, 'geometrychange')({ coordinates: [[0, 0], [1, 1]] })
235
+ expect(dispatch).not.toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: expect.anything() })
236
+ })
237
+
238
+ test('opens the gate for a valid geometry', () => {
239
+ const { draw, dispatch } = setup()
240
+ drawHandler(draw, 'geometrychange')({ feature: squareFeature, phase: 'commit-add', vertexIndex: 3 })
241
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: true })
242
+ })
243
+
244
+ test('gates a self-intersecting shape while drawing', () => {
245
+ const { draw, dispatch, eventBus, hints } = setup()
246
+ drawHandler(draw, 'geometrychange')({ feature: bowtieFeature, phase: 'commit-add', vertexIndex: 3 })
247
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
248
+ expect(draw.setGeometryValid).toHaveBeenCalledWith(false)
249
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:geometryinvalid', expect.objectContaining({ reason: expect.stringMatching(/intersect/i) }))
250
+ expect(hints.show).toHaveBeenCalledWith(expect.stringMatching(/intersect/i))
251
+ })
252
+
253
+ test('gates a part-drawn shape below the minimum vertex count, but skips the hint — an incomplete shape is expected, not a mistake', () => {
254
+ const { draw, dispatch, eventBus, hints } = setup()
255
+ drawHandler(draw, 'geometrychange')({ feature: partDrawnFeature, phase: 'commit-add', vertexIndex: 1 })
256
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
257
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:geometryinvalid', expect.objectContaining({ reason: expect.stringMatching(/at least 3 points/) }))
258
+ expect(hints.show).not.toHaveBeenCalled()
259
+ })
260
+
261
+ test('gates a zero-area shape while drawing', () => {
262
+ const { draw, dispatch } = setup()
263
+ drawHandler(draw, 'geometrychange')({ feature: collinearFeature, phase: 'commit-add', vertexIndex: 2 })
264
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
265
+ })
266
+
267
+ test('gates a self-intersecting move in edit mode (never reverts)', () => {
268
+ const { draw, dispatch, eventBus } = setup()
269
+ draw.getMode.mockReturnValue('edit_vertex')
270
+ drawHandler(draw, 'geometrychange')({ feature: bowtieFeature, phase: 'commit-move', vertexIndex: 2 })
271
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
272
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:geometryinvalid', expect.objectContaining({ reason: expect.stringMatching(/intersect/i) }))
273
+ })
274
+
275
+ test('keeps a valid edit move (gate open)', () => {
276
+ const { draw, dispatch } = setup()
277
+ draw.getMode.mockReturnValue('edit_vertex')
278
+ drawHandler(draw, 'geometrychange')({ feature: squareFeature, phase: 'commit-move', vertexIndex: 1 })
279
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: true })
280
+ })
281
+
282
+ test('applies the per-session user validator as a gate', () => {
283
+ const { draw, dispatch, eventBus, hints } = setup()
284
+ draw._geometryValidator = () => ({ valid: false, reason: 'too big' })
285
+ drawHandler(draw, 'geometrychange')({ feature: squareFeature, phase: 'commit-add', vertexIndex: 3 })
286
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
287
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:geometryinvalid', expect.objectContaining({ reason: 'too big' }))
288
+ expect(hints.show).toHaveBeenCalledWith('too big')
289
+ })
290
+
291
+ test('a user validator that vetoes with no reason (plain `false`) still emits, but skips the hint', () => {
292
+ const { draw, dispatch, eventBus, hints } = setup()
293
+ draw._geometryValidator = () => false
294
+ drawHandler(draw, 'geometrychange')({ feature: squareFeature, phase: 'commit-add', vertexIndex: 3 })
295
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
296
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:geometryinvalid', expect.objectContaining({ reason: null }))
297
+ expect(hints.show).not.toHaveBeenCalled()
298
+ })
299
+
300
+ test('drives the invalid stroke from committed validity in edit mode only', () => {
301
+ const { draw } = setup()
302
+ draw.getMode.mockReturnValue('draw_polygon')
303
+ drawHandler(draw, 'geometrychange')({ feature: bowtieFeature, phase: 'commit-add', vertexIndex: 3 })
304
+ expect(draw.setInvalid).not.toHaveBeenCalled() // draw mode: the adapter's live check owns the stroke
305
+
306
+ draw.getMode.mockReturnValue('edit_vertex')
307
+ drawHandler(draw, 'geometrychange')({ feature: bowtieFeature, phase: 'commit-move', vertexIndex: 2 })
308
+ expect(draw.setInvalid).toHaveBeenCalledWith(true)
309
+ })
310
+
311
+ test('a live validity flip (edit drag) drives the Done gate', () => {
312
+ const { draw, dispatch } = setup()
313
+ drawHandler(draw, 'validitychange')({ valid: false, reason: 'Shape must not intersect itself' })
314
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
315
+ expect(draw.setGeometryValid).toHaveBeenCalledWith(false)
316
+
317
+ drawHandler(draw, 'validitychange')({ valid: true, reason: null })
318
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: true })
319
+ })
320
+
321
+ test('a live placement-veto flip drives the Add point gate', () => {
322
+ const { draw, dispatch } = setup()
323
+ drawHandler(draw, 'canplacechange')({ canPlace: false, reason: 'outside region' })
324
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_CAN_ADD_POINT', payload: false })
325
+
326
+ drawHandler(draw, 'canplacechange')({ canPlace: true, reason: null })
327
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_CAN_ADD_POINT', payload: true })
328
+ })
329
+
330
+ test('relays an interface-type change to the public bus', () => {
331
+ const { draw, eventBus } = setup()
332
+ drawHandler(draw, 'interfacetypechange')({ interfaceType: 'keyboard' })
333
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:interfacetypechange', { interfaceType: 'keyboard' })
334
+ })
335
+
336
+ test('relays a blocked placement to the public bus as draw:geometryinvalid, and as a hint', () => {
337
+ const { draw, eventBus, hints } = setup()
338
+ const blocked = { phase: 'place', mode: 'draw_polygon', vertexIndex: 2, reason: 'outside region', feature: { type: 'Feature' } }
339
+ drawHandler(draw, 'placementblocked')(blocked)
340
+ expect(eventBus.emit).toHaveBeenCalledWith('draw:geometryinvalid', blocked)
341
+ expect(hints.show).toHaveBeenCalledWith('outside region')
342
+ })
343
+
344
+ test('passes the current mode into the validation context', () => {
345
+ const { draw } = setup()
346
+ draw.getMode.mockReturnValue('draw_polygon')
347
+ const validator = jest.fn(() => true)
348
+ draw._geometryValidator = validator
349
+ drawHandler(draw, 'geometrychange')({ feature: squareFeature, phase: 'commit-add', vertexIndex: 3 })
350
+ expect(validator).toHaveBeenCalledWith({ feature: squareFeature, phase: 'commit-add', vertexIndex: 3, mode: 'draw_polygon' })
351
+ })
352
+ })
353
+
354
+ describe('detach', () => {
355
+ test('clears button handlers and unsubscribes from draw events', () => {
356
+ const { detach, buttonConfig, draw } = setup()
357
+
358
+ detach()
359
+
360
+ expect(buttonConfig.drawDone.onClick).toBeNull()
361
+ expect(buttonConfig.drawCancel.onClick).toBeNull()
362
+ expect(buttonConfig.drawUndo.onClick).toBeNull()
363
+ expect(buttonConfig.drawDeletePoint.onClick).toBeNull()
364
+ expect(buttonConfig.drawSnap.onClick).toBeNull()
365
+ DRAW_EVENTS.forEach((event) => {
366
+ expect(draw.off).toHaveBeenCalledWith(event, expect.any(Function))
367
+ })
368
+ })
369
+ })
@@ -0,0 +1,10 @@
1
+ export default function createPlugin (options = {}) {
2
+ return {
3
+ ...options,
4
+ id: 'draw',
5
+ load: async () => {
6
+ const module = (await import(/* webpackChunkName: "im-draw-plugin" */ './manifest.js')).manifest
7
+ return module
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,24 @@
1
+ import createPlugin from './index.js'
2
+ import { manifest } from './manifest.js'
3
+
4
+ jest.mock('./manifest.js', () => ({ manifest: { id: 'draw-manifest' } }))
5
+
6
+ describe('createPlugin', () => {
7
+ test('returns a plugin descriptor with a fixed id and passes options through', () => {
8
+ const plugin = createPlugin({ foo: 'bar' })
9
+ expect(plugin).toMatchObject({ id: 'draw', foo: 'bar' })
10
+ expect(typeof plugin.load).toBe('function')
11
+ })
12
+
13
+ test('the fixed id cannot be overridden by options', () => {
14
+ expect(createPlugin({ id: 'other' }).id).toBe('draw')
15
+ })
16
+
17
+ test('defaults options to an empty object', () => {
18
+ expect(() => createPlugin()).not.toThrow()
19
+ })
20
+
21
+ test('load dynamically imports the manifest', async () => {
22
+ await expect(createPlugin().load()).resolves.toBe(manifest)
23
+ })
24
+ })
@@ -0,0 +1,170 @@
1
+ import { initialState, actions } from './reducer.js'
2
+ import { DrawInit } from './DrawInit.jsx'
3
+ import { newPolygon } from './api/newPolygon.js'
4
+ import { newLine } from './api/newLine.js'
5
+ import { editFeature } from './api/editFeature.js'
6
+ import { addFeature } from './api/addFeature.js'
7
+ import { deleteFeature } from './api/deleteFeature.js'
8
+ import { split } from './api/split.js'
9
+ import { merge } from './api/merge.js'
10
+ import { isMac } from '../../../src/utils/isMac.js'
11
+
12
+ const DRAW_ACTIONS_SLOT = 'top-middle'
13
+
14
+ // Show the platform-appropriate undo modifier (⌘ on macOS, Ctrl elsewhere).
15
+ const undoCommand = isMac() ? '<kbd>Command</kbd> + <kbd>Z</kbd>' : '<kbd>Ctrl</kbd> + <kbd>Z</kbd>'
16
+
17
+ const createButtonSlots = (showLabel) => ({
18
+ mobile: { slot: 'actions', showLabel },
19
+ tablet: { slot: 'actions', showLabel },
20
+ desktop: { slot: 'actions', showLabel }
21
+ })
22
+
23
+ export const manifest = {
24
+ reducer: {
25
+ initialState,
26
+ actions
27
+ },
28
+
29
+ InitComponent: DrawInit,
30
+
31
+ buttons: [
32
+ {
33
+ id: 'drawCancel',
34
+ label: 'Cancel',
35
+ variant: 'tertiary',
36
+ exclusiveSlot: true,
37
+ hiddenWhen: ({ pluginState }) => !pluginState.mode,
38
+ ...createButtonSlots(true)
39
+ },
40
+ {
41
+ id: 'drawAddPoint',
42
+ label: 'Add point',
43
+ variant: 'primary',
44
+ exclusiveSlot: true,
45
+ hiddenWhen: ({ appState, pluginState }) =>
46
+ !['draw_polygon', 'draw_line'].includes(pluginState.mode) || appState.interfaceType !== 'touch',
47
+ // Disabled while placing at the crosshair would be vetoed (validatePlacement) —
48
+ // driven live so the button never looks active when a tap would do nothing.
49
+ enableWhen: ({ pluginState }) => pluginState.canAddPoint,
50
+ ...createButtonSlots(true)
51
+ },
52
+ {
53
+ id: 'drawDone',
54
+ label: 'Done',
55
+ variant: 'primary',
56
+ exclusiveSlot: true,
57
+ hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
58
+ enableWhen: ({ pluginState }) => {
59
+ const { mode, geometryValid } = pluginState
60
+ // Min-vertices, area and self-intersection are all enforced by the validation
61
+ // rules via geometryValid, so the gate is simply "is the geometry valid now".
62
+ return ['draw_polygon', 'draw_line', 'edit_vertex'].includes(mode) && geometryValid
63
+ },
64
+ ...createButtonSlots(true)
65
+ },
66
+ {
67
+ id: 'drawMenu',
68
+ label: ({ pluginState }) => pluginState.mode === 'edit_vertex' ? 'Edit actions' : 'Draw actions',
69
+ iconId: 'menu',
70
+ exclusiveSlot: true,
71
+ hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
72
+ menuItems: [
73
+ {
74
+ id: 'drawUndo',
75
+ label: 'Undo',
76
+ iconId: 'undo',
77
+ hiddenWhen: ({ pluginState }) => !['draw_polygon', 'draw_line', 'edit_vertex'].includes(pluginState.mode),
78
+ enableWhen: ({ pluginState }) => {
79
+ if (['draw_polygon', 'draw_line'].includes(pluginState.mode)) {
80
+ return pluginState.numVertices > 0
81
+ }
82
+ return pluginState.undoStackLength > 0
83
+ }
84
+ },
85
+ {
86
+ id: 'drawSnap',
87
+ label: 'Snap to feature',
88
+ iconId: 'magnet',
89
+ hiddenWhen: ({ pluginState }) => !pluginState.mode || !pluginState.hasSnapLayers,
90
+ pressedWhen: ({ pluginState }) => !!pluginState.snap
91
+ },
92
+ {
93
+ id: 'drawDeletePoint',
94
+ label: 'Delete point',
95
+ iconId: 'trash',
96
+ enableWhen: ({ pluginState }) => {
97
+ if (pluginState.selectedVertexIndex < 0) { return false }
98
+ const isPolygon = pluginState.feature?.geometry?.type === 'Polygon'
99
+ return isPolygon ? pluginState.numVertices > 3 : pluginState.numVertices > 2 // NOSONAR
100
+ },
101
+ hiddenWhen: ({ pluginState }) => pluginState.mode !== 'edit_vertex'
102
+ }
103
+ ],
104
+ mobile: { slot: DRAW_ACTIONS_SLOT },
105
+ tablet: { slot: DRAW_ACTIONS_SLOT },
106
+ desktop: { slot: DRAW_ACTIONS_SLOT }
107
+ }
108
+ ],
109
+
110
+ keyboardShortcuts: [{
111
+ id: 'drawAddPoint',
112
+ group: 'Drawing',
113
+ title: 'Add point (draw)',
114
+ command: '<kbd>Enter</kbd>'
115
+ }, {
116
+ id: 'drawSelectPoint',
117
+ group: 'Drawing',
118
+ title: 'Select nearest point (edit)',
119
+ command: '<kbd>Spacebar</kbd>'
120
+ }, {
121
+ id: 'drawSelectAdjacentPoint',
122
+ group: 'Drawing',
123
+ title: 'Select adjacent point (edit)',
124
+ command: '<kbd>Alt</kbd> + <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> or <kbd>→</kbd>'
125
+ }, {
126
+ id: 'drawMovePoint',
127
+ group: 'Drawing',
128
+ title: 'Move point (edit)',
129
+ command: '<kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> or <kbd>→</kbd>'
130
+ }, {
131
+ id: 'drawNudgePoint',
132
+ group: 'Drawing',
133
+ title: 'Nudge point (edit)',
134
+ command: '<kbd>Shift</kbd> + <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> or <kbd>→</kbd>'
135
+ }, {
136
+ id: 'drawDeletePoint',
137
+ group: 'Drawing',
138
+ title: 'Delete point (edit)',
139
+ command: '<kbd>Delete</kbd>'
140
+ }, {
141
+ id: 'drawUndo',
142
+ group: 'Drawing',
143
+ title: 'Undo',
144
+ command: undoCommand
145
+ }],
146
+
147
+ icons: [{
148
+ id: 'menu',
149
+ svgContent: '<path d="m6 9 6 6 6-6"/>'
150
+ }, {
151
+ id: 'undo',
152
+ svgContent: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/>'
153
+ }, {
154
+ id: 'magnet',
155
+ svgContent: '<path d="m12 15 4 4"/><path d="M2.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.029-6.029a1 1 0 1 1 3 3l-6.029 6.029a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.365-6.367A1 1 0 0 0 8.716 4.282z"/><path d="m5 8 4 4"/>'
156
+ }, {
157
+ id: 'trash',
158
+ svgContent: '<path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>'
159
+ }],
160
+
161
+ api: {
162
+ newPolygon,
163
+ newLine,
164
+ editFeature,
165
+ addFeature,
166
+ deleteFeature,
167
+ split,
168
+ merge
169
+ }
170
+ }