@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
@@ -0,0 +1,83 @@
1
+ import { createHarness } from './__helpers__/harness.js'
2
+
3
+ describe('keyboardHandlers', () => {
4
+ const keydown = (ctx, state, key, extra = {}) => ctx.onKeydown(state, { key, preventDefault: jest.fn(), stopPropagation: jest.fn(), ...extra })
5
+ const keyup = (ctx, state, key) => ctx.onKeyup(state, { key, stopPropagation: jest.fn() })
6
+
7
+ // isInteractiveElementFocused's own branches are covered by utils/keyboardShortcuts.test.js
8
+ // — this just checks onKeydown/onKeyup actually consult it.
9
+ test('shortcuts are ignored while a form control outside the viewport has focus', () => {
10
+ const { ctx, state } = createHarness()
11
+ const input = document.createElement('input')
12
+ document.body.appendChild(input)
13
+ input.focus()
14
+ keydown(ctx, state, 'ArrowRight')
15
+ keyup(ctx, state, 'ArrowRight')
16
+ expect(state.interfaceType).not.toBe('keyboard')
17
+ })
18
+
19
+ test('space prevents the page scrolling — there is nothing to select, the point already is', () => {
20
+ const { ctx, state } = createHarness()
21
+ const e = { key: ' ', preventDefault: jest.fn(), stopPropagation: jest.fn() }
22
+ ctx.onKeydown(state, e)
23
+ expect(e.preventDefault).toHaveBeenCalled()
24
+ expect(state.interfaceType).toBe('keyboard')
25
+ })
26
+
27
+ // resolveSnapTarget's own snap/break-out-of-snap branches are covered by
28
+ // utils/snapMovement.test.js — this just checks onKeydown wires an arrow key through to it,
29
+ // records the starting position for undo on the first move, and leaves it fixed for the
30
+ // rest of a held-key sequence.
31
+ test('an arrow key moves the point and starts a keyboard-move undo sequence', () => {
32
+ const { ctx, state } = createHarness()
33
+ keydown(ctx, state, 'ArrowRight')
34
+ expect(state._keyboardMoveStartPosition).toEqual([5, 5])
35
+ expect(state.feature.coordinates).not.toEqual([5, 5])
36
+
37
+ keydown(ctx, state, 'ArrowRight') // still held → start position isn't reset
38
+ expect(state._keyboardMoveStartPosition).toEqual([5, 5])
39
+ })
40
+
41
+ test('arrow key guards a missing feature', () => {
42
+ const { ctx, state, map } = createHarness()
43
+ keydown(ctx, { ...state, featureId: 'missing' }, 'ArrowRight')
44
+ expect(map._undoStack).toHaveLength(0)
45
+ })
46
+
47
+ // isUndoShortcut/handleUndoShortcut's own edge cases (shift, wrong key, text-field focus)
48
+ // are covered by utils/keyboardShortcuts.test.js — this just checks onKeydown wires
49
+ // Cmd/Ctrl+Z through to handleUndo, and that Alt+arrow and Escape are left unbound here.
50
+ test('Cmd/Ctrl+Z undoes; Alt+arrow and Escape are left unbound', () => {
51
+ const { ctx, state } = createHarness()
52
+ const moveSpy = jest.spyOn(ctx, 'movePointByKey').mockImplementation(() => {})
53
+ keydown(ctx, state, 'ArrowRight', { altKey: true })
54
+ expect(moveSpy).toHaveBeenCalled() // Alt is not special-cased — nothing to navigate to
55
+
56
+ keydown(ctx, state, 'Escape')
57
+ expect(state.interfaceType).toBe('keyboard') // Escape does nothing but set the interface type
58
+
59
+ const undoSpy = jest.spyOn(ctx, 'handleUndo').mockImplementation(() => {})
60
+ keydown(ctx, state, 'z', { metaKey: true })
61
+ expect(undoSpy).toHaveBeenCalledTimes(1)
62
+ })
63
+
64
+ test('onKeyup pushes a move undo after a held-arrow sequence, no-ops otherwise, and allows viewport focus', () => {
65
+ const { ctx, state, map } = createHarness()
66
+ state._keyboardMoveStartPosition = [10, 0]
67
+ keyup(ctx, state, 'ArrowRight')
68
+ expect(map._undoStack.pop()).toMatchObject({ type: 'move_point', vertexIndex: 0, previousPosition: [10, 0] })
69
+
70
+ keyup(ctx, state, 'ArrowRight') // no active sequence → no undo
71
+ expect(map._undoStack).toHaveLength(0)
72
+
73
+ const child = document.createElement('button') // focus inside the viewport is non-blocking
74
+ state.container.appendChild(child)
75
+ child.focus()
76
+ keyup(ctx, state, 'ArrowRight')
77
+ expect(state.interfaceType).toBe('keyboard')
78
+
79
+ // A non-arrow key just tracks the interface type, nothing else
80
+ keyup(ctx, state, 'z')
81
+ expect(map._undoStack).toHaveLength(0)
82
+ })
83
+ })
@@ -0,0 +1,55 @@
1
+ import { sharedSnapMovement } from '../../utils/snapMovement.js'
2
+
3
+ // A Point has a single coordinate, always selected — no ring/segment/index bookkeeping the
4
+ // way editVertexMode's vertexOperations.js needs. getOffset/getOffsetByDelta/resolveSnapTarget
5
+ // come from the shared mixin (utils/snapMovement.js) — they're already pure single-coordinate
6
+ // math with no ring dependency; movePoint/nudgePointByDelta replace moveVertex/
7
+ // nudgeVertexByDelta with the direct Point.prototype.updateCoordinate('', lng, lat) mutation
8
+ // mapbox-gl-draw's own Point feature model already provides, instead of the toGeoJSON-mutate-
9
+ // api.add() ring round-trip.
10
+ export const pointOperations = {
11
+ ...sharedSnapMovement,
12
+
13
+ // The feature's own current coordinate — mapbox-gl-draw's Point model stores it flat as
14
+ // `.coordinates`, no getCoords()/index lookup needed the way a ring feature requires.
15
+ getPointCoord (state) {
16
+ return this.getFeature(state.featureId)?.coordinates
17
+ },
18
+
19
+ getNewCoord (state, e) {
20
+ return this.getOffset(this.getPointCoord(state), e)
21
+ },
22
+
23
+ // Point.prototype.updateCoordinate ignores its first (path) argument when called with
24
+ // three arguments — mapbox-gl-draw's own Point model has nothing path-shaped to address —
25
+ // and mutates + calls changed() in place, so unlike moveVertex there's no
26
+ // toGeoJSON-mutate-api.add() round-trip needed for the data itself. But that round-trip's
27
+ // api.add() call is also mapbox-gl-draw's own render trigger — updateCoordinate's changed()
28
+ // only marks the feature dirty and fires an internal event, it never repaints (see
29
+ // store.js's featureChanged/render). Mouse drag/click get a repaint for free regardless,
30
+ // since mapbox-gl-draw's own dispatch (lib/mode_handler.js) auto-renders after every
31
+ // onDrag/onClick/onMouseUp it calls — but keyboard, touch and the D-pad all move a point
32
+ // through our own custom addEventListener-bound handlers, entirely outside that dispatch,
33
+ // so without this explicit call the coordinate was updating with nothing on screen ever
34
+ // reflecting it.
35
+ movePoint (state, coord) {
36
+ state.feature.updateCoordinate('', coord.lng, coord.lat)
37
+ this._ctx.store.render()
38
+ this.map.fire('draw.geometrychange', state.feature)
39
+ },
40
+
41
+ // Moves the point by an explicit (dx, dy) unit direction — the entry point for
42
+ // MoveControls' D-pad (see mapProvider.activeMoveTarget in events.js). Each call is one
43
+ // complete, undoable action (no held-key sequencing — a button click has no "held" state
44
+ // the way arrow keys do), honouring snap via the shared resolveSnapTarget above.
45
+ nudgePointByDelta (state, dx, dy, isLargeStep) {
46
+ const currentCoord = this.getPointCoord(state)
47
+ if (!currentCoord) {
48
+ return
49
+ }
50
+ const previousPosition = [...currentCoord]
51
+ const target = this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getOffsetByDelta(currentCoord, dx, dy, isLargeStep))
52
+ this.movePoint(state, target)
53
+ this.pushUndo({ type: 'move_point', featureId: state.featureId, vertexIndex: 0, previousPosition })
54
+ }
55
+ }
@@ -0,0 +1,62 @@
1
+ import { createHarness } from './__helpers__/harness.js'
2
+
3
+ // getOffset/getOffsetByDelta/resolveSnapTarget are the shared mixin from
4
+ // utils/snapMovement.js — their own behaviour is covered by utils/snapMovement.test.js.
5
+ // This file only tests what's specific to a single-coordinate Point.
6
+ describe('pointOperations', () => {
7
+ test('getPointCoord reads the feature\'s own coordinate, no path/index lookup needed', () => {
8
+ const { ctx, state } = createHarness()
9
+ expect(ctx.getPointCoord(state)).toEqual([5, 5])
10
+ expect(ctx.getPointCoord({ ...state, featureId: 'missing' })).toBeUndefined()
11
+ })
12
+
13
+ test('getNewCoord offsets the point\'s current coordinate', () => {
14
+ const { ctx, state } = createHarness()
15
+ expect(ctx.getNewCoord(state, { key: 'ArrowRight', shiftKey: false })).toHaveProperty('lng')
16
+ })
17
+
18
+ test('movePoint mutates the coordinate directly via updateCoordinate and fires geometrychange', () => {
19
+ const { ctx, state, map } = createHarness()
20
+ ctx.movePoint(state, { lng: 1, lat: 2 })
21
+ expect(state.feature.coordinates).toEqual([1, 2])
22
+ expect(map.fire).toHaveBeenCalledWith('draw.geometrychange', state.feature)
23
+ })
24
+
25
+ // Regression: updateCoordinate's own changed() call only marks the feature dirty and fires
26
+ // an internal event — it never repaints (store.js's featureChanged/render). Mouse drag/click
27
+ // get a repaint for free via mapbox-gl-draw's own dispatch (lib/mode_handler.js auto-renders
28
+ // after every onDrag/onClick/onMouseUp it calls), but keyboard, touch and the D-pad all move
29
+ // a point through our own addEventListener-bound handlers, entirely outside that dispatch —
30
+ // without this, the coordinate updated with nothing on screen ever reflecting it.
31
+ test('movePoint explicitly triggers a render — keyboard/touch/D-pad have no other path to one', () => {
32
+ const { ctx, state } = createHarness()
33
+ ctx.movePoint(state, { lng: 1, lat: 2 })
34
+ expect(ctx._ctx.store.render).toHaveBeenCalled()
35
+ })
36
+
37
+ test('nudgePointByDelta moves the point and pushes a single undo entry', () => {
38
+ const { ctx, state, map } = createHarness()
39
+ const before = [...state.feature.coordinates]
40
+ ctx.nudgePointByDelta(state, 1, 0, true)
41
+ expect(state.feature.coordinates).not.toEqual(before)
42
+ expect(map._undoStack.pop()).toMatchObject({ type: 'move_point', vertexIndex: 0, previousPosition: before })
43
+ })
44
+
45
+ test('nudgePointByDelta no-ops when the feature is missing', () => {
46
+ const { ctx, state, map } = createHarness()
47
+ ctx.nudgePointByDelta({ ...state, featureId: 'missing' }, 1, 0, true)
48
+ expect(map._undoStack).toHaveLength(0)
49
+ })
50
+
51
+ // resolveSnapTarget's own snap/break-out-of-snap branches are covered by
52
+ // utils/snapMovement.test.js — this just checks nudgePointByDelta wires a snap hit through
53
+ // to the moved coordinate.
54
+ test('nudgePointByDelta honours an active snap', () => {
55
+ const { ctx, state, map } = createHarness()
56
+ state.getSnapEnabled = () => true
57
+ map._snapInstance = { status: true, snapStatus: true, snapCoords: [7, 8], snapToClosestPoint: jest.fn() }
58
+ ctx.nudgePointByDelta(state, 0, -1, true)
59
+ expect(state._isSnapped).toBe(true)
60
+ expect(state.feature.coordinates).toEqual([7, 8])
61
+ })
62
+ })
@@ -0,0 +1,138 @@
1
+ import DirectSelect from '../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/modes/direct_select.js' // NOSONAR
2
+ import { isActiveFeature } from '../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/lib/common_selectors.js' // NOSONAR
3
+ import {
4
+ getSnapInstance, isSnapEnabled, getSnapLngLat, triggerSnapAtPoint, clearSnapState, setAutoSnapSuspended
5
+ } from '../../utils/snapHelpers.js'
6
+ import { validatePlacement } from '../../../../validation/validateGeometry.js'
7
+
8
+ const MODE = 'edit_point'
9
+
10
+ // A point has no ring/rubber-band to build a candidate from — just the coordinate about to
11
+ // be moved to. Mirrors drawPointMode.js's own canPlace, which mirrors validateGeometry.js's
12
+ // attemptPlacement — that helper hardcodes Polygon/LineString via MODE_BY_GEOMETRY, so a
13
+ // Point candidate is built and validated directly instead.
14
+ const canPlaceAt = (map, coord) => {
15
+ const candidate = { type: 'Feature', geometry: { type: 'Point', coordinates: coord }, properties: {} }
16
+ const { valid, reason } = validatePlacement(
17
+ candidate,
18
+ { mode: MODE, vertexIndex: 0 },
19
+ { onGeometryChange: map._drawGeometryValidator }
20
+ )
21
+ if (!valid) {
22
+ map.fire('draw.placementblocked', { feature: candidate, reason: reason ?? null, phase: 'place', mode: MODE, vertexIndex: 0 })
23
+ }
24
+ return valid
25
+ }
26
+
27
+ /**
28
+ * Mouse/pointer interaction for the point-edit mode: drag (with snapping), click-elsewhere
29
+ * to relocate, and mouse-up undo bookkeeping. Wraps the parent DirectSelect handlers — with
30
+ * state.selectedCoordPaths built as [] (see editPointMode.js), DirectSelect's own onMouseDown
31
+ * (== onTouchStart, left unoverridden here — only touchHandlers.js's touch stubs neutralise
32
+ * the touch half) already routes a click on the point to onFeature/startDragging and its own
33
+ * onDrag/dragFeature already moves it via a whole-feature delta — this only adds the
34
+ * undo/snap/relocate behaviour stock DirectSelect has no concept of. Mixed into EditPointMode.
35
+ */
36
+ export const pointerHandlers = {
37
+ onFeature (state, e) {
38
+ if (!state.dragMoving) {
39
+ state._moveStartPosition = [...state.feature.coordinates]
40
+ // Fixed lng/lat offset between the click and the anchor, captured once — onDrag adds it
41
+ // to the pointer's current position on every move so a click away from the anchor keeps
42
+ // its grab point for the whole drag.
43
+ state._grabOffset = [state.feature.coordinates[0] - e.lngLat.lng, state.feature.coordinates[1] - e.lngLat.lat]
44
+ // Silence the snap library's own auto-query for the drag — see setAutoSnapSuspended.
45
+ setAutoSnapSuspended(getSnapInstance(this.map), true)
46
+ }
47
+ DirectSelect.onFeature.call(this, state, e)
48
+ },
49
+
50
+ // Click-elsewhere relocates the point — the WCAG 2.5.7 single-pointer alternative to
51
+ // dragging. Clicking the point itself is a no-op (unlike DirectSelect.clickActiveFeature,
52
+ // which would clear the selection — the point stays selected for the whole edit session).
53
+ onClick (state, e) {
54
+ if (isActiveFeature(e)) {
55
+ return
56
+ }
57
+ if (!canPlaceAt(this.map, [e.lngLat.lng, e.lngLat.lat])) {
58
+ return
59
+ }
60
+
61
+ const previousPosition = [...state.feature.coordinates]
62
+ const snap = getSnapInstance(this.map)
63
+ let target = e.lngLat
64
+ if (isSnapEnabled(state) && snap) {
65
+ triggerSnapAtPoint(snap, this.map, e.point)
66
+ target = getSnapLngLat(snap) || e.lngLat
67
+ }
68
+ this.movePoint(state, target)
69
+ this.pushUndo({ type: 'move_point', featureId: state.featureId, vertexIndex: 0, previousPosition })
70
+ },
71
+
72
+ onMouseUp (state, e) {
73
+ const snap = getSnapInstance(this.map)
74
+ clearSnapState(snap)
75
+ setAutoSnapSuspended(snap, false)
76
+ if (this._didPointMove(state)) {
77
+ this.pushUndo({ type: 'move_point', featureId: state.featureId, vertexIndex: 0, previousPosition: state._moveStartPosition })
78
+ }
79
+ state._moveStartPosition = null
80
+ DirectSelect.onMouseUp.call(this, state, e)
81
+ },
82
+
83
+ // Did the point actually change position during this interaction? Reads the live
84
+ // feature coordinate directly — a Point has no ring index to look up.
85
+ _didPointMove (state) {
86
+ if (!state._moveStartPosition) {
87
+ return false
88
+ }
89
+ const current = state.feature.coordinates
90
+ return current[0] !== state._moveStartPosition[0] || current[1] !== state._moveStartPosition[1]
91
+ },
92
+
93
+ onDrag (state, e) {
94
+ if (state.interfaceType === 'touch') {
95
+ return
96
+ }
97
+
98
+ this.map.fire('draw.geometrychange', state.feature)
99
+
100
+ const snap = getSnapInstance(this.map)
101
+ if (snap) {
102
+ snap.snapStatus = false
103
+ snap.snapCoords = null
104
+ }
105
+
106
+ if (!isSnapEnabled(state) || !snap?.status) {
107
+ // selectedCoordPaths is [] for a point (see editPointMode.js) — DirectSelect.onDrag's
108
+ // own selectedCoordPaths.length > 0 check routes this to dragFeature, its built-in
109
+ // whole-feature delta move, which already handles a Point feature correctly.
110
+ DirectSelect.onDrag.call(this, state, e)
111
+ return
112
+ }
113
+
114
+ if (!state.canDragMove) {
115
+ return
116
+ }
117
+
118
+ state.dragMoving = true
119
+ e.originalEvent.stopPropagation()
120
+
121
+ // Candidate = pointer's current absolute position + the fixed grab offset — not an
122
+ // incremental delta, which would compound onto an already-snapped position and leave the
123
+ // pin stuck on the same target no matter how far the mouse then moves.
124
+ const candidateLngLat = {
125
+ lng: e.lngLat.lng + state._grabOffset[0],
126
+ lat: e.lngLat.lat + state._grabOffset[1]
127
+ }
128
+ triggerSnapAtPoint(snap, this.map, this.map.project([candidateLngLat.lng, candidateLngLat.lat]))
129
+
130
+ const finalLngLat = getSnapLngLat(snap) || candidateLngLat
131
+ state.feature.updateCoordinate('', finalLngLat.lng, finalLngLat.lat)
132
+ state.dragMoveLocation = e.lngLat
133
+ },
134
+
135
+ // Whole-feature deletion is deleteFeature()'s job, not this mode's — deleting a Point's
136
+ // one coordinate isn't a "delete vertex" operation the way it is for a ring.
137
+ onTrash () {}
138
+ }
@@ -0,0 +1,197 @@
1
+ import { createHarness, dragEvt } from './__helpers__/harness.js'
2
+
3
+ const activeFeatureTarget = { properties: { active: 'true', meta: 'feature' } }
4
+
5
+ describe('pointerHandlers', () => {
6
+ test('onFeature snapshots the move start and starts dragging via stock DirectSelect (selectedCoordPaths: [])', () => {
7
+ const { ctx, state } = createHarness()
8
+ ctx.onFeature(state, { featureTarget: activeFeatureTarget, lngLat: { lng: 5, lat: 5 } })
9
+ expect(state._moveStartPosition).toEqual([5, 5])
10
+ expect(state.canDragMove).toBe(true)
11
+ expect(state.dragMoveLocation).toEqual({ lng: 5, lat: 5 })
12
+ })
13
+
14
+ // Feature anchor is [5, 5] (createHarness's default) — clicking at [2, 2] is 3 units away,
15
+ // e.g. near the top of a pin icon rather than its tip.
16
+ test('onFeature captures the click\'s fixed offset from the anchor', () => {
17
+ const { ctx, state } = createHarness()
18
+ ctx.onFeature(state, { featureTarget: activeFeatureTarget, lngLat: { lng: 2, lat: 2 } })
19
+ expect(state._grabOffset).toEqual([3, 3])
20
+ })
21
+
22
+ test('onFeature does not re-snapshot mid-drag', () => {
23
+ const { ctx, state } = createHarness()
24
+ state.dragMoving = true
25
+ state._moveStartPosition = [1, 1]
26
+ state._grabOffset = [0, 0]
27
+ ctx.onFeature(state, { featureTarget: activeFeatureTarget, lngLat: { lng: 9, lat: 9 } })
28
+ expect(state._moveStartPosition).toEqual([1, 1])
29
+ expect(state._grabOffset).toEqual([0, 0])
30
+ })
31
+
32
+ // Silencing the snap library's own raw-cursor auto-query for the whole drag is what keeps
33
+ // the indicator and the dragged feature from falling out of sync — see
34
+ // snapHelpers.js's setAutoSnapSuspended for the full mechanism.
35
+ test('onFeature suspends the snap library\'s own auto-query at drag start; onMouseUp resumes it', () => {
36
+ const { ctx, state, map } = createHarness()
37
+ map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
38
+
39
+ ctx.onFeature(state, { featureTarget: activeFeatureTarget, lngLat: { lng: 5, lat: 5 } })
40
+ expect(map._snapInstance._autoSnapSuspended).toBe(true)
41
+
42
+ ctx.onMouseUp(state, {})
43
+ expect(map._snapInstance._autoSnapSuspended).toBe(false)
44
+ })
45
+
46
+ test('onFeature suspending auto-snap is a harmless no-op when there is no snap instance', () => {
47
+ const { ctx, state } = createHarness()
48
+ expect(() => ctx.onFeature(state, { featureTarget: activeFeatureTarget, lngLat: { lng: 5, lat: 5 } })).not.toThrow()
49
+ })
50
+
51
+ test('onClick on the active feature is a no-op — the point stays selected for the whole session', () => {
52
+ const { ctx, state, map } = createHarness()
53
+ const before = [...state.feature.coordinates]
54
+ ctx.onClick(state, { featureTarget: activeFeatureTarget })
55
+ expect(state.feature.coordinates).toEqual(before)
56
+ expect(map._undoStack).toHaveLength(0)
57
+ })
58
+
59
+ test('onClick elsewhere relocates the point (WCAG 2.5.7 single-pointer alternative to drag), pushing an undo entry', () => {
60
+ const { ctx, state, map } = createHarness()
61
+ ctx.onClick(state, { featureTarget: undefined, lngLat: { lng: 9, lat: 9 }, point: { x: 90, y: 90 } })
62
+ expect(state.feature.coordinates).toEqual([9, 9])
63
+ expect(map._undoStack.pop()).toMatchObject({ type: 'move_point', vertexIndex: 0, previousPosition: [5, 5] })
64
+ })
65
+
66
+ test('onClick honours snap when enabled and active, falling back to the raw target when inactive', () => {
67
+ const { ctx, state, map } = createHarness()
68
+ state.getSnapEnabled = () => true
69
+ map._snapInstance = { status: true, snapStatus: true, snapCoords: [7, 8], snapToClosestPoint: jest.fn() }
70
+ ctx.onClick(state, { featureTarget: undefined, lngLat: { lng: 9, lat: 9 }, point: { x: 90, y: 90 } })
71
+ expect(state.feature.coordinates).toEqual([7, 8])
72
+
73
+ map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
74
+ ctx.onClick(state, { featureTarget: undefined, lngLat: { lng: 3, lat: 3 }, point: { x: 30, y: 30 } })
75
+ expect(state.feature.coordinates).toEqual([3, 3])
76
+ })
77
+
78
+ test('onClick is vetoed by a rejecting geometry validator, firing draw.placementblocked with a fallback null reason, and leaving the point untouched', () => {
79
+ const { ctx, state, map } = createHarness()
80
+ map._drawGeometryValidator = () => false // no {reason} shape
81
+ ctx.onClick(state, { featureTarget: undefined, lngLat: { lng: 9, lat: 9 }, point: { x: 90, y: 90 } })
82
+ expect(state.feature.coordinates).toEqual([5, 5])
83
+ expect(map.fire).toHaveBeenCalledWith('draw.placementblocked', expect.objectContaining({ reason: null }))
84
+ expect(map._undoStack).toHaveLength(0)
85
+ })
86
+
87
+ test('onMouseUp clears snap state, records a move undo only if the point actually moved, and delegates to stock DirectSelect', () => {
88
+ const { ctx, state, map } = createHarness()
89
+ ctx.onMouseUp(state, {}) // no prior snapshot → nothing recorded
90
+ expect(map._undoStack).toHaveLength(0)
91
+
92
+ state._moveStartPosition = [5, 5]
93
+ ctx.movePoint(state, { lng: 9, lat: 9 })
94
+ ctx.onMouseUp(state, {})
95
+ expect(map._undoStack.pop()).toMatchObject({ type: 'move_point', vertexIndex: 0, previousPosition: [5, 5] })
96
+ expect(state._moveStartPosition).toBeNull()
97
+
98
+ // Snapshot present but the point never actually moved → no undo entry
99
+ state._moveStartPosition = [9, 9]
100
+ ctx.onMouseUp(state, {})
101
+ expect(map._undoStack).toHaveLength(0)
102
+ })
103
+
104
+ test('onDrag skips touch, delegates without snap, and snaps when enabled', () => {
105
+ const { ctx, state, map } = createHarness()
106
+ ctx.onDrag({ ...state, interfaceType: 'touch' }, dragEvt(1, 1))
107
+ expect(state.feature.coordinates).toEqual([5, 5]) // untouched — bailed early for touch
108
+
109
+ // no snap → stock DirectSelect.onDrag/dragFeature moves the point via moveFeatures
110
+ Object.assign(state, { canDragMove: true, dragMoveLocation: { lng: 0, lat: 0 } })
111
+ ctx.onDrag(state, dragEvt(2, 2))
112
+ expect(map.fire).toHaveBeenCalledWith('draw.geometrychange', expect.anything())
113
+ expect(state.feature.coordinates).toEqual([7, 7])
114
+
115
+ // snap enabled but inactive → resolves via dragging still (canDragMove true)
116
+ state.getSnapEnabled = () => true
117
+ map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
118
+ Object.assign(state, { dragMoveLocation: { lng: 2, lat: 2 }, _grabOffset: [0, 0] })
119
+ ctx.onDrag(state, dragEvt(3, 3))
120
+ expect(state.dragMoving).toBe(true)
121
+ })
122
+
123
+ test('onDrag with snap active writes the snapped coordinate directly', () => {
124
+ const { ctx, state, map } = createHarness() // feature anchor at [5, 5]
125
+ state.getSnapEnabled = () => true
126
+ ctx.onFeature(state, { featureTarget: activeFeatureTarget, lngLat: { lng: 0, lat: 0 } }) // grab offset [5,5]
127
+ map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn(() => { map._snapInstance.snapStatus = true; map._snapInstance.snapCoords = [7, 8] }) }
128
+ ctx.onDrag(state, dragEvt(2, 2)) // candidate = [2,2] + grab offset [5,5] = [7,7], queried and snapped
129
+ expect(state.feature.coordinates).toEqual([7, 8])
130
+ })
131
+
132
+ // Regression: the candidate used to be built by accumulating the pointer's frame-to-frame
133
+ // delta onto the feature's own (possibly already-snapped) coordinate — so once a snap moved
134
+ // the feature onto a target, the next frame's small pointer movement produced a candidate
135
+ // still within that same target's radius and got pulled straight back onto it, reading as
136
+ // permanently stuck regardless of how far the mouse then moved. The candidate must instead
137
+ // always be the pointer's current absolute position plus the fixed grab offset from drag
138
+ // start, independent of wherever a previous frame's snap left the feature.
139
+ test('onDrag recovers on the next frame instead of staying stuck at a target it just snapped to', () => {
140
+ const { ctx, state, map } = createHarness() // feature anchor at [5, 5]
141
+ ctx.onFeature(state, { featureTarget: activeFeatureTarget, lngLat: { lng: 0, lat: 0 } }) // grab offset [5,5]
142
+ state.getSnapEnabled = () => true
143
+ map._snapInstance = {
144
+ status: true,
145
+ snapStatus: false,
146
+ snapCoords: null,
147
+ snapToClosestPoint: jest.fn(() => { map._snapInstance.snapStatus = true; map._snapInstance.snapCoords = [7, 8] })
148
+ }
149
+
150
+ // Frame 1: pointer at [2,2] → candidate [7,7], found and snapped onto the target [7,8].
151
+ ctx.onDrag(state, dragEvt(2, 2))
152
+ expect(state.feature.coordinates).toEqual([7, 8])
153
+
154
+ // Frame 2: pointer moves on by just one unit, to [3,2]. The fresh candidate must be built
155
+ // from the pointer's own absolute position ([3,2] + [5,5] = [8,7]), not from last frame's
156
+ // already-snapped [7,8] plus this frame's tiny delta (which would still read as sitting on
157
+ // the target).
158
+ map._snapInstance.snapToClosestPoint = jest.fn(() => { map._snapInstance.snapStatus = false; map._snapInstance.snapCoords = null })
159
+ ctx.onDrag(state, dragEvt(3, 2))
160
+ expect(map.project).toHaveBeenCalledWith([8, 7])
161
+ expect(state.feature.coordinates).toEqual([8, 7])
162
+ })
163
+
164
+ // The click that starts a drag can land anywhere on the icon, not just on top of its anchor
165
+ // coordinate — the anchor must move by the same delta as the pointer and snap must be queried
166
+ // at that offset-corrected candidate, not at the raw pointer (which would snap to whatever's
167
+ // near the cursor and jump the anchor under it the moment a candidate is found). Mirrors the
168
+ // OL adapter's point/pointDragInteraction.js regression test for the identical bug.
169
+ test('onDrag preserves the click\'s offset from the anchor and queries snap at the anchor\'s own candidate, not the raw pointer', () => {
170
+ const { ctx, state, map } = createHarness() // feature anchor at [5, 5]
171
+ // Mousedown at [2, 2] — 3 units away from the anchor, e.g. near the top of a pin icon.
172
+ ctx.onFeature(state, { featureTarget: activeFeatureTarget, lngLat: { lng: 2, lat: 2 } })
173
+ state.getSnapEnabled = () => true
174
+ map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
175
+
176
+ // Pointer moves from [2,2] to [4,2] — a (+2, 0) delta — no snap candidate found.
177
+ ctx.onDrag(state, dragEvt(4, 2))
178
+ expect(state.feature.coordinates).toEqual([7, 5]) // anchor [5,5] + the pointer's own delta
179
+ expect(map.project).toHaveBeenCalledWith([7, 5]) // queried at the anchor's candidate…
180
+ expect(map.project).not.toHaveBeenCalledWith([4, 2]) // …never at the raw pointer position
181
+ })
182
+
183
+ test('onDrag with snap enabled but not canDragMove bails out', () => {
184
+ const { ctx, state } = createHarness()
185
+ state.getSnapEnabled = () => true
186
+ state.canDragMove = false
187
+ ctx.map._snapInstance = { status: true, snapStatus: true, snapCoords: [1, 1], snapToClosestPoint: jest.fn() }
188
+ ctx.onDrag(state, dragEvt(2, 2))
189
+ expect(state.feature.coordinates).toEqual([5, 5])
190
+ })
191
+
192
+ test('onTrash is a no-op — whole-feature deletion is deleteFeature\'s job, not this mode\'s', () => {
193
+ const { ctx, state } = createHarness()
194
+ expect(() => ctx.onTrash(state)).not.toThrow()
195
+ expect(state.feature.coordinates).toEqual([5, 5])
196
+ })
197
+ })
@@ -0,0 +1,92 @@
1
+ import { getSnapInstance, clearSnapState } from '../../utils/snapHelpers.js'
2
+ import { isOnSVG, scalePoint } from '../editVertexMode/helpers.js'
3
+ import { createTouchTarget } from '../../../../utils/touchTarget.js'
4
+ // Reused, not redefined — mapboxDraw.js's style-reload path already imports this same symbol
5
+ // and keys off map._drawEditContainer, which edit_point also sets, so the recolour-on-
6
+ // style-change path works for the point's touch target with zero changes elsewhere.
7
+ import { applyTouchVertexColors } from '../editVertexMode/touchHandlers.js'
8
+ import { getTouchPoint, computeTouchDragAnchors, resolveTouchDragCoord } from '../../utils/touchDragMath.js'
9
+
10
+ // The offset-drag delta math here (utils/touchDragMath.js) is shared with editVertexMode/
11
+ // touchHandlers.js's onTouchstart/onTouchmove — it's already single-coordinate, not
12
+ // ring-specific — just sourced from the point's own coordinate instead of a vertex list
13
+ // index. onTap does NOT relocate the point (a stray tap during a pan-ish gesture shouldn't
14
+ // silently move it — the offset target drag and the D-pad are touch's two ways to move it).
15
+ export const touchHandlers = {
16
+ addTouchPointTarget (state) {
17
+ state.touchPointTarget = createTouchTarget(state.container)
18
+ applyTouchVertexColors(state.touchPointTarget, this.map._drawCurrentMapStyle, this.map._drawPluginConfig)
19
+ },
20
+
21
+ updateTouchPointTarget (state, point) {
22
+ if (point && state.interfaceType === 'touch') {
23
+ Object.assign(state.touchPointTarget.style, { display: 'block', top: `${point.y}px`, left: `${point.x}px` })
24
+ } else {
25
+ state.touchPointTarget.style.display = 'none'
26
+ }
27
+ },
28
+
29
+ hideTouchPointIndicator (state) {
30
+ state.touchPointTarget.style.display = 'none'
31
+ },
32
+
33
+ // Point is always selected, so — unlike editVertexMode's onPointerevent — there's nothing
34
+ // to deselect on a non-touch pointermove; just track the interface type.
35
+ onPointerevent (state, e) {
36
+ state.interfaceType = e.pointerType === 'touch' ? 'touch' : 'mouse'
37
+ state.isPanEnabled = true
38
+ },
39
+
40
+ // Empty stubs required by DirectSelect
41
+ onTouchStart () {},
42
+ onTouchMove () {},
43
+ onTouchEnd () {},
44
+
45
+ onTouchend (state) {
46
+ clearSnapState(getSnapInstance(this.map))
47
+ if (state._touchMoved && state._moveStartPosition) {
48
+ this.pushUndo({ type: 'move_point', featureId: state.featureId, vertexIndex: 0, previousPosition: state._moveStartPosition })
49
+ }
50
+ state._moveStartPosition = null
51
+ state._touchMoved = false
52
+
53
+ // Re-sync the target to the point's actual final coordinate. Mid-drag it tracks the raw
54
+ // finger 1:1 from its touchstart-time offset (see onTouchmove) — a mid-drag snap moves the
55
+ // point without moving the target by the same amount, leaving them out of alignment. Left
56
+ // uncorrected, the next onTouchstart's delta math re-anchors off that drifted position,
57
+ // compounding a little further with every snapped drag.
58
+ if (state.feature) {
59
+ this.updateTouchPointTarget(state, scalePoint(this.map.project(state.feature.coordinates), state.scale))
60
+ }
61
+ },
62
+
63
+ // Touch tap on the point does nothing — relocating it is the offset target drag or the
64
+ // D-pad's job, not a raw tap.
65
+ onTap () {},
66
+
67
+ onTouchstart (state, e) {
68
+ clearSnapState(getSnapInstance(this.map))
69
+ const vertex = this.getPointCoord(state)
70
+ if (!vertex || !isOnSVG(e.target.parentNode)) {
71
+ return
72
+ }
73
+
74
+ state._moveStartPosition = [...vertex]
75
+ state._touchMoved = false
76
+
77
+ const touch = getTouchPoint(e)
78
+ Object.assign(state, computeTouchDragAnchors(this.map, state.touchPointTarget, touch, vertex, state.scale))
79
+ },
80
+
81
+ onTouchmove (state, e) {
82
+ if (!isOnSVG(e.target.parentNode)) {
83
+ return
84
+ }
85
+
86
+ state._touchMoved = true
87
+
88
+ const touch = getTouchPoint(e)
89
+ this.movePoint(state, resolveTouchDragCoord(this.map, state, touch))
90
+ this.updateTouchPointTarget(state, { x: touch.x - state.deltaTarget.x, y: touch.y - state.deltaTarget.y })
91
+ }
92
+ }