@defra/interactive-map 0.0.40-alpha → 0.0.42-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 (374) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/button-definition.md +7 -8
  7. package/docs/api/control-definition.md +17 -1
  8. package/docs/api/panel-definition.md +5 -0
  9. package/docs/api/slots.md +50 -6
  10. package/docs/assets/images/slot-map.svg +61 -262
  11. package/docs/examples/add-polygons.mdx +8 -2
  12. package/docs/examples/add-symbols.mdx +8 -2
  13. package/docs/examples/draw-tools.mdx +3 -3
  14. package/docs/plugins/datasets.md +23 -15
  15. package/docs/plugins/draw.md +79 -16
  16. package/docs/plugins/map-key.md +147 -0
  17. package/docs/plugins/search.md +1 -1
  18. package/docs/plugins.md +5 -1
  19. package/govuk-prototype-kit.config.json +2 -0
  20. package/jest.setup.js +5 -0
  21. package/package.json +9 -1
  22. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  23. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  24. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  25. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  26. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  27. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  28. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  29. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  30. package/plugins/beta/use-location/src/manifest.js +1 -1
  31. package/plugins/datasets/dist/css/index.css +1 -1
  32. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  33. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  34. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  35. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  36. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  37. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  38. package/plugins/datasets/dist/umd/index.js +1 -1
  39. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  40. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  41. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  42. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  43. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  44. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  45. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  46. package/plugins/datasets/src/datasets.scss +0 -1
  47. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  48. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  49. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  50. package/plugins/datasets/src/index.test.js +36 -0
  51. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  52. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  53. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  54. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  55. package/plugins/datasets/src/manifest.js +0 -38
  56. package/plugins/datasets/src/manifest.test.js +49 -0
  57. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  58. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  59. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  60. package/plugins/datasets/src/registry/dataset.js +13 -1
  61. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  62. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  63. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  64. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  65. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  66. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  67. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  68. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  69. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  70. package/plugins/draw/dist/esm/index.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  72. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  73. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  74. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  75. package/plugins/draw/dist/umd/index.js +1 -1
  76. package/plugins/draw/src/DrawInit.jsx +10 -2
  77. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  78. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  79. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  80. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  81. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  110. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  112. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  114. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  116. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  117. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  118. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  130. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  132. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  133. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  135. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  137. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  140. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  142. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  144. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  147. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  148. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  149. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  150. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  151. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  152. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  153. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  154. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  155. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  156. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  162. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  163. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  164. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  165. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  166. package/plugins/draw/src/api/addFeature.js +16 -1
  167. package/plugins/draw/src/api/addFeature.test.js +62 -5
  168. package/plugins/draw/src/api/createNewShape.js +41 -0
  169. package/plugins/draw/src/api/createNewShape.test.js +63 -0
  170. package/plugins/draw/src/api/editFeature.js +7 -4
  171. package/plugins/draw/src/api/editFeature.test.js +16 -0
  172. package/plugins/draw/src/api/newLine.js +2 -40
  173. package/plugins/draw/src/api/newPoint.js +42 -0
  174. package/plugins/draw/src/api/newPoint.test.js +117 -0
  175. package/plugins/draw/src/api/newPolygon.js +2 -40
  176. package/plugins/draw/src/api/setStyle.js +43 -0
  177. package/plugins/draw/src/api/setStyle.test.js +75 -0
  178. package/plugins/draw/src/events.js +17 -10
  179. package/plugins/draw/src/events.test.js +14 -0
  180. package/plugins/draw/src/manifest.js +18 -6
  181. package/plugins/draw/src/manifest.test.js +32 -1
  182. package/plugins/draw/src/reducer.test.js +7 -0
  183. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  184. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  185. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  186. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  187. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  188. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  189. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  190. package/plugins/interact/dist/umd/index.js +1 -1
  191. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  192. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  193. package/plugins/interact/src/manifest.js +1 -1
  194. package/plugins/interact/src/utils/featureQueries.js +18 -0
  195. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  196. package/plugins/interact/src/utils/spatial.js +9 -3
  197. package/plugins/interact/src/utils/spatial.test.js +12 -1
  198. package/plugins/map-key/dist/css/index.css +68 -0
  199. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  200. package/plugins/map-key/dist/esm/index.js +1 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  202. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  203. package/plugins/map-key/dist/umd/index.js +2 -0
  204. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  205. package/plugins/map-key/package.json +28 -0
  206. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  208. package/plugins/map-key/src/components/Key/Key.jsx +43 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +21 -14
  210. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  212. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.jsx +26 -0
  214. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  215. package/plugins/map-key/src/components/Key/KeySvg.jsx +41 -0
  216. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  217. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +22 -0
  218. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.jsx +7 -2
  220. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +8 -2
  221. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +26 -0
  222. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.jsx +8 -3
  224. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +17 -6
  225. package/plugins/map-key/src/components/Key/MapKey.jsx +53 -0
  226. package/plugins/map-key/src/components/Key/MapKey.test.jsx +147 -0
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  228. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  229. package/plugins/map-key/src/index.js +13 -0
  230. package/plugins/map-key/src/index.test.js +31 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  232. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  233. package/plugins/map-key/src/manifest.js +30 -0
  234. package/plugins/map-key/src/manifest.test.js +63 -0
  235. package/plugins/map-key/src/mapKey.scss +4 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  237. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  238. package/plugins/map-key/src/registry/index.js +3 -0
  239. package/plugins/map-key/src/registry/index.test.js +15 -0
  240. package/plugins/menu/dist/css/index.css +80 -0
  241. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  242. package/plugins/menu/dist/esm/index.js +1 -0
  243. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  244. package/plugins/menu/dist/umd/index.js +2 -0
  245. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  246. package/plugins/menu/package.json +28 -0
  247. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  249. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  250. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  251. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  252. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  254. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  255. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  257. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  258. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  259. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  260. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  261. package/plugins/menu/src/index.js +13 -0
  262. package/plugins/menu/src/index.test.js +31 -0
  263. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  264. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  265. package/plugins/menu/src/initialise/createMenu.js +10 -0
  266. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  267. package/plugins/menu/src/manifest.js +35 -0
  268. package/plugins/menu/src/manifest.test.js +63 -0
  269. package/plugins/menu/src/menu.scss +4 -0
  270. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  271. package/plugins/menu/src/reducers/pluginState.js +35 -0
  272. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  274. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  275. package/plugins/menu/src/registry/index.js +3 -0
  276. package/plugins/menu/src/registry/index.test.js +15 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  278. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  279. package/plugins/search/dist/css/index.css +1 -1
  280. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  281. package/plugins/search/dist/esm/index.js +1 -1
  282. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  283. package/plugins/search/dist/umd/index.js +1 -1
  284. package/plugins/search/src/Search.jsx +2 -3
  285. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  286. package/plugins/search/src/index.js +0 -1
  287. package/plugins/search/src/index.test.js +2 -4
  288. package/plugins/search/src/manifest.js +1 -1
  289. package/plugins/search/src/search.scss +17 -16
  290. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  292. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  294. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  296. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  298. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  299. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  301. package/providers/maplibre/dist/umd/index.js +1 -1
  302. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  303. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  304. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  305. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  306. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  307. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  308. package/providers/maplibre/src/utils/patternImages.js +1 -1
  309. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  310. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  311. package/rollup.esm.mjs +11 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Hints/Hints.test.jsx +13 -0
  314. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
  315. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
  316. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  317. package/src/App/components/Panel/Panel.jsx +65 -8
  318. package/src/App/components/Panel/Panel.module.scss +49 -15
  319. package/src/App/components/Panel/Panel.test.jsx +114 -1
  320. package/src/App/components/Tabs/Tabs.jsx +43 -17
  321. package/src/App/components/Tabs/Tabs.module.scss +32 -5
  322. package/src/App/components/Tabs/Tabs.test.jsx +40 -10
  323. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  324. package/src/App/hooks/useFeatureItems.js +16 -7
  325. package/src/App/hooks/useFeatureItems.test.js +57 -0
  326. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  327. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  328. package/src/App/layout/Layout.jsx +9 -10
  329. package/src/App/layout/Layout.test.jsx +2 -0
  330. package/src/App/layout/layout.module.scss +31 -17
  331. package/src/App/registry/pluginRegistry.js +1 -1
  332. package/src/App/registry/pluginRegistry.test.js +12 -0
  333. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  334. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  335. package/src/App/renderer/groupByKey.js +28 -0
  336. package/src/App/renderer/groupByKey.test.js +39 -0
  337. package/src/App/renderer/groupIntoTabs.js +42 -0
  338. package/src/App/renderer/groupIntoTabs.test.js +95 -0
  339. package/src/App/renderer/mapButtons.js +97 -129
  340. package/src/App/renderer/mapButtons.test.js +67 -83
  341. package/src/App/renderer/mapControls.js +4 -1
  342. package/src/App/renderer/mapControls.test.js +32 -0
  343. package/src/App/renderer/mapPanels.js +53 -11
  344. package/src/App/renderer/mapPanels.test.js +95 -2
  345. package/src/App/renderer/orderItems.js +28 -0
  346. package/src/App/renderer/orderItems.test.js +50 -0
  347. package/src/App/renderer/slotAggregator.js +2 -18
  348. package/src/App/renderer/slotHelpers.js +3 -1
  349. package/src/App/renderer/slotHelpers.test.js +5 -0
  350. package/src/App/renderer/slots.js +3 -0
  351. package/src/App/store/AppProvider.jsx +1 -0
  352. package/src/App/store/appDispatchMiddleware.js +1 -1
  353. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  354. package/src/config/appConfig.js +2 -2
  355. package/src/config/events.js +2 -0
  356. package/src/scss/settings/_colors.scss +1 -0
  357. package/src/scss/settings/_dimensions.scss +2 -3
  358. package/src/scss/settings/_transition.scss +1 -1
  359. package/src/services/eventBus.js +44 -0
  360. package/src/services/eventBus.test.js +83 -0
  361. package/src/services/symbolRegistry.js +1 -1
  362. package/src/types.js +31 -3
  363. package/webpack.umd.mjs +2 -0
  364. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  365. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  366. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  367. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  368. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  369. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  370. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  371. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  372. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  373. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  374. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -2,10 +2,7 @@
2
2
  import { SIZES } from './defaults.js'
3
3
  import { resolveColors } from '../../utils/resolveColors.js'
4
4
 
5
- // `defaultValue` is the already-resolved fallback (colors[defaultsKey] the
6
- // plugin-config override if one was set, else the built-in default), not the
7
- // raw COLORS constant, so a shapeStroke/shapeFill override applies to every
8
- // feature that doesn't set its own stroke/fill property.
5
+ // `defaultValue` is the already-resolved fallback (plugin-config override or built-in default), so a shapeStroke/shapeFill override applies to every feature without its own.
9
6
  const getUserProp = (mapStyle, prop, defaultValue) => [
10
7
  'coalesce',
11
8
  ['get', `user_${prop}${mapStyle.id.charAt(0).toUpperCase() + mapStyle.id.slice(1)}`],
@@ -13,11 +10,18 @@ const getUserProp = (mapStyle, prop, defaultValue) => [
13
10
  defaultValue
14
11
  ]
15
12
 
13
+ // Every feature gets an incrementing user_sortKey property when it's created (see
14
+ // MaplibreDrawAdapter.js's _nextSortKey) — later-drawn features paint on top of earlier ones
15
+ // within the same shared layer, instead of the order being incidental to which of mapbox-gl-
16
+ // draw's cold/hot buckets a feature happens to be in.
17
+ const SORT_KEY_PROP = ['get', 'user_sortKey']
18
+
16
19
  // Inactive lines and fills
17
20
  const fillInactive = (mapStyle, colors) => ({
18
21
  id: 'fill-inactive',
19
22
  type: 'fill',
20
23
  filter: ['all', ['==', '$type', 'Polygon'], ['==', 'active', 'false']],
24
+ layout: { 'fill-sort-key': SORT_KEY_PROP },
21
25
  paint: { 'fill-color': getUserProp(mapStyle, 'fill', colors.shapeFill) }
22
26
  })
23
27
 
@@ -25,7 +29,7 @@ const strokeInactive = (mapStyle, colors) => ({
25
29
  id: 'stroke-inactive',
26
30
  type: 'line',
27
31
  filter: ['all', ['any', ['==', '$type', 'Polygon'], ['==', '$type', 'LineString']], ['==', 'active', 'false'], ['!has', 'user_splitter']],
28
- layout: { 'line-cap': 'round', 'line-join': 'round' },
32
+ layout: { 'line-cap': 'round', 'line-join': 'round', 'line-sort-key': SORT_KEY_PROP },
29
33
  paint: {
30
34
  'line-color': getUserProp(mapStyle, 'stroke', colors.shapeStroke),
31
35
  'line-width': colors.strokeWidth
@@ -48,9 +52,7 @@ const strokeActive = (editStrokeColor) => ({
48
52
  paint: { 'line-color': editStrokeColor, 'line-width': 2, 'line-opacity': 1 }
49
53
  })
50
54
 
51
- // Dashed stroke shown in place of stroke-active while the shape is invalid. Same
52
- // filter as stroke-active; the adapter's setInvalid() toggles the two layers'
53
- // visibility (line-dasharray can't be data-driven per feature in MapLibre).
55
+ // Dashed stroke shown in place of stroke-active while invalid — the adapter's setInvalid() toggles the two layers' visibility (line-dasharray can't be data-driven per feature).
54
56
  const strokeActiveInvalid = (invalidStrokeColor) => ({
55
57
  id: 'stroke-active-invalid',
56
58
  type: 'line',
@@ -150,6 +152,32 @@ const circle = (editStrokeColor) => ({
150
152
  paint: { 'line-color': editStrokeColor, 'line-width': 2, 'line-opacity': 0.8 }
151
153
  })
152
154
 
155
+ // A committed point renders its resolved symbol-config icon (pointSymbolImages.js writes user_symbolImageId/user_symbolIconAnchor onto each feature). While a point is being edited (active:'true'), icon-image swaps to its precomputed "selected" variant — the same one the interact plugin's own highlight uses — so the look persists for the whole edit session instead of disappearing when that highlight is cleared on entering edit mode.
156
+ // icon-offset is deliberately NOT a per-feature `get` here — MapLibre's GeoJSON sources
157
+ // silently JSON.stringify array properties, so it would read back a string, not an array.
158
+ // pointSymbolImages.js's registerSymbolIconOffset overwrites this default via
159
+ // setLayoutProperty with a `match` expression once a point needs a non-zero offset.
160
+ const pointSymbol = () => ({
161
+ id: 'point-symbol',
162
+ type: 'symbol',
163
+ filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'feature']],
164
+ layout: {
165
+ 'icon-image': [
166
+ 'case',
167
+ ['==', ['get', 'active'], 'true'],
168
+ ['coalesce', ['get', 'user_symbolSelectedImageId'], ['get', 'user_symbolImageId']],
169
+ ['get', 'user_symbolImageId']
170
+ ],
171
+ 'icon-anchor': ['get', 'user_symbolIconAnchor'],
172
+ 'icon-offset': ['literal', [0, 0]],
173
+ 'icon-allow-overlap': true,
174
+ 'icon-ignore-placement': true,
175
+ 'symbol-sort-key': SORT_KEY_PROP
176
+ },
177
+ // Empty but required — updateDrawStyles() below iterates every layer's paint object unconditionally.
178
+ paint: {}
179
+ })
180
+
153
181
  const touchVertexIndicator = () => ({
154
182
  id: 'touch-vertex-indicator',
155
183
  type: 'circle',
@@ -157,9 +185,7 @@ const touchVertexIndicator = () => ({
157
185
  paint: { 'circle-radius': 30, 'circle-color': '#3bb2d0', 'circle-stroke-width': 3, 'circle-stroke-color': '#ffffff', 'circle-opacity': 0.9 }
158
186
  })
159
187
 
160
- // `pluginConfig` lets callers override any of these colours/strokeWidth see
161
- // resolveColors() and the draw plugin's own options docs. Unaffected keys
162
- // still resolve to the built-in COLORS/SIZES defaults.
188
+ // `pluginConfig` lets callers override any of these colours/strokeWidth (see resolveColors()); unaffected keys fall back to the built-in defaults.
163
189
  const createDrawStyles = (mapStyle, pluginConfig = {}) => {
164
190
  const colors = resolveColors(mapStyle, pluginConfig)
165
191
  const { vertexRadius, midpointRadius, vertexHaloRadius, midpointHaloRadius } = SIZES
@@ -170,6 +196,7 @@ const createDrawStyles = (mapStyle, pluginConfig = {}) => {
170
196
  strokeActive(colors.editStroke),
171
197
  strokeActiveInvalid(colors.invalidStroke),
172
198
  strokeInactive(mapStyle, colors),
199
+ pointSymbol(),
173
200
  drawInvalidSplitter(colors.splitInvalid),
174
201
  drawValidSplitter(colors.splitValid),
175
202
  drawPreviewLine(colors.editStroke),
@@ -11,12 +11,51 @@ describe('createDrawStyles', () => {
11
11
  const layers = createDrawStyles(mapStyle)
12
12
  expect(layers.map((l) => l.id)).toEqual([
13
13
  'fill-inactive', 'fill-active', 'stroke-active', 'stroke-active-invalid', 'stroke-inactive',
14
+ 'point-symbol',
14
15
  'stroke-invalid-splitter', 'stroke-valid-splitter', 'stroke-preview-line',
15
16
  'midpoint', 'midpoint-halo', 'midpoint-active',
16
17
  'vertex', 'vertex-halo', 'vertex-active', 'circle', 'touch-vertex-indicator'
17
18
  ])
18
19
  })
19
20
 
21
+ test('the point-symbol layer is data-driven per feature and only matches real committed point features', () => {
22
+ const layers = createDrawStyles(mapStyle)
23
+ const point = findLayer(layers, 'point-symbol')
24
+ expect(point.filter).toEqual(['all', ['==', '$type', 'Point'], ['==', 'meta', 'feature']])
25
+ expect(point.layout['icon-anchor']).toEqual(['get', 'user_symbolIconAnchor'])
26
+ // Static default — pointSymbolImages.js's registerSymbolIconOffset overwrites this via
27
+ // setLayoutProperty with a match expression once a point needs a non-zero offset (a raw
28
+ // per-feature `get` here would read back a JSON.stringify'd string, not the real array —
29
+ // see that function's comment).
30
+ expect(point.layout['icon-offset']).toEqual(['literal', [0, 0]])
31
+ expect(point.layout['icon-allow-overlap']).toBe(true)
32
+ })
33
+
34
+ // Drives predictable draw-order stacking within each shared layer (a later-drawn feature
35
+ // paints above an earlier one) — see MaplibreDrawAdapter.js's _nextSortKey/sortKey property.
36
+ test('fill/line/symbol layers sort by the per-feature user_sortKey property', () => {
37
+ const layers = createDrawStyles(mapStyle)
38
+ expect(findLayer(layers, 'fill-inactive').layout['fill-sort-key']).toEqual(['get', 'user_sortKey'])
39
+ expect(findLayer(layers, 'stroke-inactive').layout['line-sort-key']).toEqual(['get', 'user_sortKey'])
40
+ expect(findLayer(layers, 'point-symbol').layout['symbol-sort-key']).toEqual(['get', 'user_sortKey'])
41
+ })
42
+
43
+ // While a point is the one being edited (edit_point mode's toDisplayFeatures sets
44
+ // active:'true' just for it), icon-image swaps to the same precomputed "selected" (black
45
+ // ring) variant the interact plugin's own selection highlight already uses — see
46
+ // pointSymbol()'s own comment — so the look persists for the whole edit session instead of
47
+ // disappearing when the interact plugin's selection gets cleared on entering edit mode.
48
+ test('the point-symbol layer swaps to the selected-variant icon while active, falling back to the normal one if no variant was resolved', () => {
49
+ const layers = createDrawStyles(mapStyle)
50
+ const point = findLayer(layers, 'point-symbol')
51
+ expect(point.layout['icon-image']).toEqual([
52
+ 'case',
53
+ ['==', ['get', 'active'], 'true'],
54
+ ['coalesce', ['get', 'user_symbolSelectedImageId'], ['get', 'user_symbolImageId']],
55
+ ['get', 'user_symbolImageId']
56
+ ])
57
+ })
58
+
20
59
  test('the invalid stroke layer is a hidden dashed line matching the active shape', () => {
21
60
  const layers = createDrawStyles(mapStyle)
22
61
  const invalid = findLayer(layers, 'stroke-active-invalid')
@@ -0,0 +1,79 @@
1
+ import { CUSTOM_DRAW_EVENTS } from '../drawEvents.js'
2
+ import { getSnapInstance, clearSnapIndicator } from './snapHelpers.js'
3
+
4
+ // Builds the `handlers` map both modes' setupEventListeners bind to bindEditModeListeners —
5
+ // the base set of window/container/scale/move/interface-type-change handlers is identical
6
+ // in both, bound to whichever mode instance is passed in; `extra` supplies the handlers that
7
+ // differ per mode (nudge, plus edit_vertex's own selectionchange/update).
8
+ export const buildEditModeHandlers = (mode, state, extra = {}) => {
9
+ const bind = (handler) => (event) => handler.call(mode, state, event)
10
+ const handlers = {
11
+ keydown: bind(mode.onKeydown),
12
+ keyup: bind(mode.onKeyup),
13
+ pointerdown: bind(mode.onPointerevent),
14
+ pointermove: bind(mode.onPointerevent),
15
+ pointerup: bind(mode.onPointerevent),
16
+ click: bind(mode.onButtonClick),
17
+ touchstart: bind(mode.onTouchstart),
18
+ touchmove: bind(mode.onTouchmove),
19
+ touchend: bind(mode.onTouchend),
20
+ scalechange: bind(mode.onScaleChange),
21
+ move: bind(mode.onMove),
22
+ interfacetypechange: bind(mode.onInterfaceTypeChange)
23
+ }
24
+ for (const [name, handler] of Object.entries(extra)) {
25
+ handlers[name] = bind(handler)
26
+ }
27
+ return handlers
28
+ }
29
+
30
+ // Shared DOM/map event wiring for edit_point and edit_vertex — both modes' setupEventListeners
31
+ // bind the exact same base set of window/container listeners plus scale/move/interface-type-
32
+ // change/nudge map events; edit_vertex additionally wires its own selectionchange/update map
33
+ // events on top of this. Each mode still builds its own `handlers` map (bound to its own onXxx
34
+ // methods) — this just wires/unwires it, keyed on the handler names both modes share (the
35
+ // nudge entry is named `nudge` in both, even though what it does — nudgePointByDelta vs
36
+ // nudgeVertexByDelta — differs per mode).
37
+ export const bindEditModeListeners = (state, map, handlers) => {
38
+ window.addEventListener('keydown', handlers.keydown, { capture: true })
39
+ window.addEventListener('keyup', handlers.keyup, { capture: true })
40
+ window.addEventListener('click', handlers.click)
41
+ state.container.addEventListener('pointerdown', handlers.pointerdown)
42
+ state.container.addEventListener('pointermove', handlers.pointermove)
43
+ state.container.addEventListener('pointerup', handlers.pointerup)
44
+ state.container.addEventListener('touchstart', handlers.touchstart, { passive: false })
45
+ state.container.addEventListener('touchmove', handlers.touchmove, { passive: false })
46
+ state.container.addEventListener('touchend', handlers.touchend, { passive: false })
47
+ map.on('draw.scalechange', handlers.scalechange)
48
+ map.on('move', handlers.move)
49
+ map.on(CUSTOM_DRAW_EVENTS.INTERFACE_TYPE_CHANGE, handlers.interfacetypechange)
50
+ map.on(CUSTOM_DRAW_EVENTS.NUDGE_VERTEX, handlers.nudge)
51
+ }
52
+
53
+ // Mirrors bindEditModeListeners, plus the dragPan re-enable both modes' onStop does right
54
+ // alongside it (dragPan is disabled for the duration of the edit session elsewhere).
55
+ export const unbindEditModeListeners = (state, map, handlers) => {
56
+ state.container.removeEventListener('pointerdown', handlers.pointerdown)
57
+ state.container.removeEventListener('pointermove', handlers.pointermove)
58
+ state.container.removeEventListener('pointerup', handlers.pointerup)
59
+ state.container.removeEventListener('touchstart', handlers.touchstart)
60
+ state.container.removeEventListener('touchmove', handlers.touchmove)
61
+ state.container.removeEventListener('touchend', handlers.touchend)
62
+ map.off('draw.scalechange', handlers.scalechange)
63
+ map.off('move', handlers.move)
64
+ map.off(CUSTOM_DRAW_EVENTS.INTERFACE_TYPE_CHANGE, handlers.interfacetypechange)
65
+ map.off(CUSTOM_DRAW_EVENTS.NUDGE_VERTEX, handlers.nudge)
66
+ map.dragPan.enable()
67
+ window.removeEventListener('click', handlers.click)
68
+ window.removeEventListener('keydown', handlers.keydown, { capture: true })
69
+ window.removeEventListener('keyup', handlers.keyup, { capture: true })
70
+ }
71
+
72
+ // Clear any snap indicator left over from a previous interaction — both edit modes do this
73
+ // once, at onSetup.
74
+ export const clearActiveSnapIndicator = (map) => {
75
+ const snap = getSnapInstance(map)
76
+ if (snap) {
77
+ clearSnapIndicator(snap, map)
78
+ }
79
+ }
@@ -0,0 +1,135 @@
1
+ import { bindEditModeListeners, unbindEditModeListeners, clearActiveSnapIndicator, buildEditModeHandlers } from './editModeEvents.js'
2
+ import { CUSTOM_DRAW_EVENTS } from '../drawEvents.js'
3
+
4
+ const makeHandlers = () => ({
5
+ keydown: jest.fn(),
6
+ keyup: jest.fn(),
7
+ click: jest.fn(),
8
+ pointerdown: jest.fn(),
9
+ pointermove: jest.fn(),
10
+ pointerup: jest.fn(),
11
+ touchstart: jest.fn(),
12
+ touchmove: jest.fn(),
13
+ touchend: jest.fn(),
14
+ scalechange: jest.fn(),
15
+ move: jest.fn(),
16
+ interfacetypechange: jest.fn(),
17
+ nudge: jest.fn()
18
+ })
19
+
20
+ const makeMap = () => ({ on: jest.fn(), off: jest.fn(), dragPan: { enable: jest.fn() } })
21
+
22
+ describe('bindEditModeListeners', () => {
23
+ test('wires the shared window/container listeners and map events', () => {
24
+ const state = { container: document.createElement('div') }
25
+ jest.spyOn(window, 'addEventListener')
26
+ jest.spyOn(state.container, 'addEventListener')
27
+ const map = makeMap()
28
+ const handlers = makeHandlers()
29
+
30
+ bindEditModeListeners(state, map, handlers)
31
+
32
+ expect(window.addEventListener).toHaveBeenCalledWith('keydown', handlers.keydown, { capture: true })
33
+ expect(window.addEventListener).toHaveBeenCalledWith('keyup', handlers.keyup, { capture: true })
34
+ expect(window.addEventListener).toHaveBeenCalledWith('click', handlers.click)
35
+ expect(state.container.addEventListener).toHaveBeenCalledWith('pointerdown', handlers.pointerdown)
36
+ expect(state.container.addEventListener).toHaveBeenCalledWith('pointermove', handlers.pointermove)
37
+ expect(state.container.addEventListener).toHaveBeenCalledWith('pointerup', handlers.pointerup)
38
+ expect(state.container.addEventListener).toHaveBeenCalledWith('touchstart', handlers.touchstart, { passive: false })
39
+ expect(state.container.addEventListener).toHaveBeenCalledWith('touchmove', handlers.touchmove, { passive: false })
40
+ expect(state.container.addEventListener).toHaveBeenCalledWith('touchend', handlers.touchend, { passive: false })
41
+ expect(map.on).toHaveBeenCalledWith('draw.scalechange', handlers.scalechange)
42
+ expect(map.on).toHaveBeenCalledWith('move', handlers.move)
43
+ expect(map.on).toHaveBeenCalledWith(CUSTOM_DRAW_EVENTS.INTERFACE_TYPE_CHANGE, handlers.interfacetypechange)
44
+ expect(map.on).toHaveBeenCalledWith(CUSTOM_DRAW_EVENTS.NUDGE_VERTEX, handlers.nudge)
45
+
46
+ window.addEventListener.mockRestore()
47
+ })
48
+ })
49
+
50
+ describe('unbindEditModeListeners', () => {
51
+ test('tears down the shared window/container listeners and map events, and re-enables dragPan', () => {
52
+ const state = { container: document.createElement('div') }
53
+ jest.spyOn(window, 'removeEventListener')
54
+ jest.spyOn(state.container, 'removeEventListener')
55
+ const map = makeMap()
56
+ const handlers = makeHandlers()
57
+
58
+ unbindEditModeListeners(state, map, handlers)
59
+
60
+ expect(state.container.removeEventListener).toHaveBeenCalledWith('pointerdown', handlers.pointerdown)
61
+ expect(state.container.removeEventListener).toHaveBeenCalledWith('pointermove', handlers.pointermove)
62
+ expect(state.container.removeEventListener).toHaveBeenCalledWith('pointerup', handlers.pointerup)
63
+ expect(state.container.removeEventListener).toHaveBeenCalledWith('touchstart', handlers.touchstart)
64
+ expect(state.container.removeEventListener).toHaveBeenCalledWith('touchmove', handlers.touchmove)
65
+ expect(state.container.removeEventListener).toHaveBeenCalledWith('touchend', handlers.touchend)
66
+ expect(map.off).toHaveBeenCalledWith('draw.scalechange', handlers.scalechange)
67
+ expect(map.off).toHaveBeenCalledWith('move', handlers.move)
68
+ expect(map.off).toHaveBeenCalledWith(CUSTOM_DRAW_EVENTS.INTERFACE_TYPE_CHANGE, handlers.interfacetypechange)
69
+ expect(map.off).toHaveBeenCalledWith(CUSTOM_DRAW_EVENTS.NUDGE_VERTEX, handlers.nudge)
70
+ expect(map.dragPan.enable).toHaveBeenCalled()
71
+ expect(window.removeEventListener).toHaveBeenCalledWith('click', handlers.click)
72
+ expect(window.removeEventListener).toHaveBeenCalledWith('keydown', handlers.keydown, { capture: true })
73
+ expect(window.removeEventListener).toHaveBeenCalledWith('keyup', handlers.keyup, { capture: true })
74
+
75
+ window.removeEventListener.mockRestore()
76
+ })
77
+ })
78
+
79
+ describe('clearActiveSnapIndicator', () => {
80
+ test('clears the indicator when a snap instance is present', () => {
81
+ const snap = { snapStatus: true, snapCoords: [1, 2] }
82
+ const map = { _snapInstance: snap, getLayer: jest.fn(() => null), setLayoutProperty: jest.fn() }
83
+
84
+ clearActiveSnapIndicator(map)
85
+
86
+ expect(snap.snapStatus).toBe(false)
87
+ expect(snap.snapCoords).toBeNull()
88
+ })
89
+
90
+ test('is a no-op without a snap instance', () => {
91
+ expect(() => clearActiveSnapIndicator({})).not.toThrow()
92
+ })
93
+ })
94
+
95
+ describe('buildEditModeHandlers', () => {
96
+ test('builds the shared base handlers bound to the given mode and state', () => {
97
+ const mode = {
98
+ onKeydown: jest.fn(),
99
+ onKeyup: jest.fn(),
100
+ onPointerevent: jest.fn(),
101
+ onButtonClick: jest.fn(),
102
+ onTouchstart: jest.fn(),
103
+ onTouchmove: jest.fn(),
104
+ onTouchend: jest.fn(),
105
+ onScaleChange: jest.fn(),
106
+ onMove: jest.fn(),
107
+ onInterfaceTypeChange: jest.fn()
108
+ }
109
+ const state = {}
110
+ const handlers = buildEditModeHandlers(mode, state)
111
+
112
+ expect(Object.keys(handlers).sort()).toEqual([
113
+ 'click', 'interfacetypechange', 'keydown', 'keyup', 'move', 'pointerdown',
114
+ 'pointermove', 'pointerup', 'scalechange', 'touchend', 'touchmove', 'touchstart'
115
+ ])
116
+ handlers.keydown('event')
117
+ handlers.pointerdown('down-event')
118
+ handlers.pointermove('move-event')
119
+ expect(mode.onKeydown).toHaveBeenCalledWith(state, 'event')
120
+ // pointerdown/pointermove/pointerup all route through the single onPointerevent handler
121
+ expect(mode.onPointerevent).toHaveBeenCalledWith(state, 'down-event')
122
+ expect(mode.onPointerevent).toHaveBeenCalledWith(state, 'move-event')
123
+ })
124
+
125
+ test('binds extra per-mode handlers (e.g. nudge, selectionchange, update) alongside the base set', () => {
126
+ const mode = { onNudgeVertex: jest.fn(), onSelectionChange: jest.fn() }
127
+ const state = {}
128
+ const handlers = buildEditModeHandlers(mode, state, { nudge: mode.onNudgeVertex, selectionchange: mode.onSelectionChange })
129
+
130
+ handlers.nudge('e1')
131
+ handlers.selectionchange('e2')
132
+ expect(mode.onNudgeVertex).toHaveBeenCalledWith(state, 'e1')
133
+ expect(mode.onSelectionChange).toHaveBeenCalledWith(state, 'e2')
134
+ })
135
+ })
@@ -0,0 +1,42 @@
1
+ // Shared by editPointMode/undoHandlers.js and editVertexMode/undoHandlers.js — recording an
2
+ // undo op and feeding it to commit-level validation is identical plumbing for both modes; only
3
+ // the op types themselves differ (a Point only ever moves, a ring vertex can also be inserted
4
+ // or deleted), which each mode still declares locally via its own UNDO_OP_PHASE/
5
+ // UNDO_INVERSE_PHASE maps (read here off `this`, since pushUndo is mixed into both modes).
6
+ export const sharedUndoHandlers = {
7
+ // Fire geometry change event (for external listeners)
8
+ fireGeometryChange (state) {
9
+ const feature = this.getFeature(state.featureId)
10
+ if (feature) {
11
+ this.map.fire('draw.update', {
12
+ features: [feature.toGeoJSON()],
13
+ action: 'change_coordinates'
14
+ })
15
+ }
16
+ },
17
+
18
+ // Emit a commit-level geometrychange (feature + change phase + vertex index) so the
19
+ // validation layer can accept or reject the change. Deferred a tick to avoid
20
+ // re-entrancy: rejection calls draw.undo(), which must run after the current
21
+ // mutation (and its undo bookkeeping) has fully settled.
22
+ emitGeometryValidation (phase, vertexIndex, featureId) {
23
+ if (!phase) { return }
24
+ setTimeout(() => {
25
+ const feature = this.getFeature(featureId)
26
+ if (!feature) { return }
27
+ this.map.fire('draw.geometrychange', { feature: feature.toGeoJSON(), phase, vertexIndex })
28
+ }, 0)
29
+ },
30
+
31
+ // Undo support
32
+ pushUndo (operation) {
33
+ const undoStack = this.map._undoStack
34
+ if (!undoStack) {
35
+ return
36
+ }
37
+ undoStack.push(operation)
38
+ // Every edit commit (move/insert/delete, via mouse/touch/keyboard/D-pad) records an undo
39
+ // op here, so this is the single point that feeds commit-level validation.
40
+ this.emitGeometryValidation(this.UNDO_OP_PHASE[operation.type], operation.vertexIndex, operation.featureId)
41
+ }
42
+ }
@@ -0,0 +1,85 @@
1
+ import { sharedUndoHandlers } from './geometryValidation.js'
2
+ import { createUndoStack } from '../../../utils/undoStack.js'
3
+
4
+ const makeCtx = (overrides = {}) => ({
5
+ ...sharedUndoHandlers,
6
+ UNDO_OP_PHASE: { move: 'commit-move' },
7
+ map: { fire: jest.fn(), _undoStack: createUndoStack(() => {}) },
8
+ getFeature: jest.fn((id) => (id === 'missing' ? undefined : { toGeoJSON: () => ({ type: 'Feature', id }) })),
9
+ ...overrides
10
+ })
11
+
12
+ describe('fireGeometryChange', () => {
13
+ test('fires draw.update only when the feature exists', () => {
14
+ const ctx = makeCtx()
15
+ ctx.fireGeometryChange({ featureId: 'feat-1' })
16
+ expect(ctx.map.fire).toHaveBeenCalledWith('draw.update', expect.objectContaining({ action: 'change_coordinates' }))
17
+
18
+ ctx.map.fire.mockClear()
19
+ ctx.fireGeometryChange({ featureId: 'missing' })
20
+ expect(ctx.map.fire).not.toHaveBeenCalled()
21
+ })
22
+ })
23
+
24
+ describe('emitGeometryValidation', () => {
25
+ test('does nothing without a phase', () => {
26
+ jest.useFakeTimers()
27
+ const ctx = makeCtx()
28
+ ctx.emitGeometryValidation(undefined, 0, 'feat-1')
29
+ jest.runAllTimers()
30
+ expect(ctx.map.fire).not.toHaveBeenCalled()
31
+ jest.useRealTimers()
32
+ })
33
+
34
+ test('fires a deferred draw.geometrychange with the phase, vertexIndex and feature', () => {
35
+ jest.useFakeTimers()
36
+ const ctx = makeCtx()
37
+ ctx.emitGeometryValidation('commit-move', 2, 'feat-1')
38
+ // Deferred a tick to avoid re-entrancy — nothing fired synchronously.
39
+ expect(ctx.map.fire).not.toHaveBeenCalled()
40
+
41
+ jest.runAllTimers()
42
+ expect(ctx.map.fire).toHaveBeenCalledWith('draw.geometrychange', expect.objectContaining({
43
+ phase: 'commit-move',
44
+ vertexIndex: 2,
45
+ feature: expect.any(Object)
46
+ }))
47
+ jest.useRealTimers()
48
+ })
49
+
50
+ test('does not fire once the feature is gone', () => {
51
+ jest.useFakeTimers()
52
+ const ctx = makeCtx()
53
+ ctx.emitGeometryValidation('commit-move', 0, 'missing')
54
+ jest.runAllTimers()
55
+ expect(ctx.map.fire).not.toHaveBeenCalled()
56
+ jest.useRealTimers()
57
+ })
58
+ })
59
+
60
+ describe('pushUndo', () => {
61
+ test('is a no-op without a stack', () => {
62
+ const ctx = makeCtx({ map: { fire: jest.fn(), _undoStack: null } })
63
+ expect(() => ctx.pushUndo({ type: 'move' })).not.toThrow()
64
+ })
65
+
66
+ test('pushes the operation and emits validation for a mapped op type', () => {
67
+ jest.useFakeTimers()
68
+ const ctx = makeCtx()
69
+ ctx.pushUndo({ type: 'move', featureId: 'feat-1', vertexIndex: 0 })
70
+ expect(ctx.map._undoStack.length).toBe(1)
71
+
72
+ jest.runAllTimers()
73
+ expect(ctx.map.fire).toHaveBeenCalledWith('draw.geometrychange', expect.objectContaining({ phase: 'commit-move' }))
74
+ jest.useRealTimers()
75
+ })
76
+
77
+ test('does not emit validation for an unmapped op type', () => {
78
+ jest.useFakeTimers()
79
+ const ctx = makeCtx()
80
+ ctx.pushUndo({ type: 'unmapped', featureId: 'feat-1' })
81
+ jest.runAllTimers()
82
+ expect(ctx.map.fire).not.toHaveBeenCalled()
83
+ jest.useRealTimers()
84
+ })
85
+ })
@@ -0,0 +1,34 @@
1
+ // Shared by editPointMode/keyboardHandlers.js and editVertexMode/keyboardHandlers.js — both
2
+ // modes gate their shortcuts on the same "is focus somewhere that should eat the keystroke"
3
+ // and "is this the undo chord" questions, independent of the ring-vs-single-coordinate
4
+ // bookkeeping the rest of each mode's keyboard handling differs on.
5
+ export const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])
6
+ export const ARROW_OFFSETS = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }
7
+
8
+ const INTERACTIVE_TAGS = new Set(['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'A'])
9
+
10
+ // Keyboard shortcuts are ignored while a form control outside the map viewport has
11
+ // focus, but still work for elements inside the viewport (e.g. draw toolbar buttons).
12
+ export const isInteractiveElementFocused = (state) => {
13
+ const el = document.activeElement
14
+ if (!el || el === document.body) { return false }
15
+ if (state.container?.contains(el)) { return false }
16
+ return INTERACTIVE_TAGS.has(el.tagName) || el.isContentEditable || el.hasAttribute('tabindex')
17
+ }
18
+
19
+ export const isUndoShortcut = (e) => e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey
20
+
21
+ // Mixin method — spread into both modes' keyboardHandlers so `this.handleUndo` resolves to
22
+ // whichever mode it's mixed into.
23
+ export const sharedKeyboardHandlers = {
24
+ // Cmd/Ctrl+Z: undo the last edit, unless the user is typing in a text field.
25
+ handleUndoShortcut (state, e) {
26
+ const tag = document.activeElement?.tagName
27
+ if (tag === 'INPUT' || tag === 'TEXTAREA') {
28
+ return
29
+ }
30
+ e.preventDefault()
31
+ e.stopPropagation()
32
+ this.handleUndo(state)
33
+ }
34
+ }
@@ -0,0 +1,78 @@
1
+ import { isInteractiveElementFocused, isUndoShortcut, sharedKeyboardHandlers } from './keyboardShortcuts.js'
2
+
3
+ afterEach(() => {
4
+ document.body.innerHTML = ''
5
+ })
6
+
7
+ describe('isInteractiveElementFocused', () => {
8
+ test('returns false when nothing has been focused (activeElement defaults to body)', () => {
9
+ expect(document.activeElement).toBe(document.body)
10
+ expect(isInteractiveElementFocused({ container: document.createElement('div') })).toBe(false)
11
+ })
12
+
13
+ test('returns false for focus inside the container', () => {
14
+ const container = document.createElement('div')
15
+ const button = document.createElement('button')
16
+ container.appendChild(button)
17
+ document.body.appendChild(container)
18
+ button.focus()
19
+ expect(isInteractiveElementFocused({ container })).toBe(false)
20
+ })
21
+
22
+ test('returns true for an interactive tag focused outside the container', () => {
23
+ const input = document.createElement('input')
24
+ document.body.appendChild(input)
25
+ input.focus()
26
+ expect(isInteractiveElementFocused({ container: document.createElement('div') })).toBe(true)
27
+ })
28
+
29
+ test('returns true for a tabindex/contentEditable element focused outside the container', () => {
30
+ const div = document.createElement('div')
31
+ div.tabIndex = 0
32
+ document.body.appendChild(div)
33
+ div.focus()
34
+ expect(isInteractiveElementFocused({ container: document.createElement('div') })).toBe(true)
35
+ })
36
+
37
+ test('a missing container is treated as outside — falls through to the tag/attribute check', () => {
38
+ const input = document.createElement('input')
39
+ document.body.appendChild(input)
40
+ input.focus()
41
+ expect(isInteractiveElementFocused({ container: undefined })).toBe(true)
42
+ })
43
+ })
44
+
45
+ describe('isUndoShortcut', () => {
46
+ test('true for Cmd+Z or Ctrl+Z without shift', () => {
47
+ expect(isUndoShortcut({ key: 'z', metaKey: true })).toBe(true)
48
+ expect(isUndoShortcut({ key: 'z', ctrlKey: true })).toBe(true)
49
+ })
50
+
51
+ test('falsy when shift is also held, the key is wrong, or no modifier is held', () => {
52
+ expect(isUndoShortcut({ key: 'z', metaKey: true, shiftKey: true })).toBe(false)
53
+ expect(isUndoShortcut({ key: 'y', metaKey: true })).toBe(false)
54
+ expect(isUndoShortcut({ key: 'z' })).toBeFalsy()
55
+ })
56
+ })
57
+
58
+ describe('sharedKeyboardHandlers.handleUndoShortcut', () => {
59
+ test('swallows the default/propagation and delegates to handleUndo', () => {
60
+ const handleUndo = jest.fn()
61
+ const e = { preventDefault: jest.fn(), stopPropagation: jest.fn() }
62
+ sharedKeyboardHandlers.handleUndoShortcut.call({ handleUndo }, {}, e)
63
+ expect(e.preventDefault).toHaveBeenCalled()
64
+ expect(e.stopPropagation).toHaveBeenCalled()
65
+ expect(handleUndo).toHaveBeenCalled()
66
+ })
67
+
68
+ test('is ignored while typing in an input/textarea', () => {
69
+ const input = document.createElement('input')
70
+ document.body.appendChild(input)
71
+ input.focus()
72
+ const handleUndo = jest.fn()
73
+ const e = { preventDefault: jest.fn(), stopPropagation: jest.fn() }
74
+ sharedKeyboardHandlers.handleUndoShortcut.call({ handleUndo }, {}, e)
75
+ expect(handleUndo).not.toHaveBeenCalled()
76
+ expect(e.preventDefault).not.toHaveBeenCalled()
77
+ })
78
+ })
@@ -52,7 +52,8 @@ export function getSnapCoords (snap) {
52
52
  }
53
53
 
54
54
  /**
55
- * Trigger snap detection at a given point
55
+ * Trigger snap detection at a given point. Marked `_explicit` so it still runs even while
56
+ * setAutoSnapSuspended (below) has silenced the library's own auto-query.
56
57
  * @param {MapboxSnap} snap - Snap instance
57
58
  * @param {maplibregl.Map} map - Map instance
58
59
  * @param {{ x: number, y: number }} point - Screen point
@@ -64,10 +65,22 @@ export function triggerSnapAtPoint (snap, map, point) {
64
65
  }
65
66
 
66
67
  const lngLat = map.unproject(point)
67
- snap.snapToClosestPoint({ point, lngLat })
68
+ snap.snapToClosestPoint({ point, lngLat, _explicit: true })
68
69
  return true
69
70
  }
70
71
 
72
+ /**
73
+ * Silences the snap library's own mousemove-driven auto-query (see prototypePatches.js), which
74
+ * otherwise re-snaps at the raw cursor and fights an edit mode's own offset-aware drag query.
75
+ * @param {MapboxSnap} snap - Snap instance
76
+ * @param {boolean} suspended
77
+ */
78
+ export function setAutoSnapSuspended (snap, suspended) {
79
+ if (snap) {
80
+ snap._autoSnapSuspended = suspended
81
+ }
82
+ }
83
+
71
84
  /**
72
85
  * Trigger snap detection at map center (for touch/keyboard modes)
73
86
  * @param {MapboxSnap} snap - Snap instance
@@ -82,7 +95,7 @@ export function triggerSnapAtCenter (snap, map) {
82
95
 
83
96
  const center = map.getCenter()
84
97
  const point = map.project(center)
85
- snap.snapToClosestPoint({ point, lngLat: center })
98
+ snap.snapToClosestPoint({ point, lngLat: center, _explicit: true })
86
99
  return true
87
100
  }
88
101