@defra/interactive-map 0.0.40-alpha → 0.0.41-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.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/control-definition.md +9 -1
- package/docs/api/slots.md +26 -5
- package/docs/assets/images/slot-map.svg +61 -262
- package/docs/examples/draw-tools.mdx +3 -3
- package/docs/plugins/draw.md +79 -16
- package/docs/plugins/search.md +1 -1
- package/govuk-prototype-kit.config.json +2 -0
- package/jest.setup.js +5 -0
- package/package.json +9 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/datasets/dist/css/index.css +1 -1
- package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/index.js +1 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
- package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
- package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
- package/plugins/datasets/src/api/setOpacity.test.js +9 -0
- package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
- package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
- package/plugins/datasets/src/datasets.scss +0 -1
- package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
- package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
- package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
- package/plugins/datasets/src/index.test.js +36 -0
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
- package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
- package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
- package/plugins/datasets/src/manifest.js +0 -38
- package/plugins/datasets/src/manifest.test.js +49 -0
- package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
- package/plugins/datasets/src/reducers/pluginState.js +3 -16
- package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
- package/plugins/datasets/src/registry/dataset.js +13 -1
- package/plugins/datasets/src/registry/dataset.test.js +61 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
- package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
- package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
- package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
- package/plugins/datasets/src/utils/bbox.test.js +125 -0
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/esm/index.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/index.js +1 -1
- package/plugins/draw/src/DrawInit.jsx +10 -2
- package/plugins/draw/src/DrawInit.test.jsx +51 -10
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
- package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
- package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
- package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
- package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
- package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
- package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
- package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
- package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
- package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
- package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
- package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
- package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
- package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
- package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
- package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
- package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
- package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
- package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
- package/plugins/draw/src/api/addFeature.js +16 -1
- package/plugins/draw/src/api/addFeature.test.js +62 -5
- package/plugins/draw/src/api/editFeature.js +7 -4
- package/plugins/draw/src/api/editFeature.test.js +16 -0
- package/plugins/draw/src/api/newPoint.js +42 -0
- package/plugins/draw/src/api/newPoint.test.js +117 -0
- package/plugins/draw/src/api/setStyle.js +43 -0
- package/plugins/draw/src/api/setStyle.test.js +75 -0
- package/plugins/draw/src/events.js +17 -10
- package/plugins/draw/src/events.test.js +14 -0
- package/plugins/draw/src/manifest.js +18 -6
- package/plugins/draw/src/manifest.test.js +32 -1
- package/plugins/draw/src/reducer.test.js +7 -0
- package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
- package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
- package/plugins/draw/src/utils/symbolKeys.js +5 -0
- package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
- package/plugins/draw/src/validation/liveStroke.test.js +64 -1
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
- package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
- package/plugins/interact/src/utils/featureQueries.js +18 -0
- package/plugins/interact/src/utils/featureQueries.test.js +15 -0
- package/plugins/interact/src/utils/spatial.js +9 -3
- package/plugins/interact/src/utils/spatial.test.js +12 -1
- package/plugins/map-key/dist/css/index.css +62 -0
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
- package/plugins/map-key/dist/esm/index.js +1 -0
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
- package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
- package/plugins/map-key/dist/umd/index.js +2 -0
- package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/map-key/package.json +28 -0
- package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
- package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/Key.jsx +41 -0
- package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
- package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
- package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
- package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
- package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
- package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
- package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
- package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
- package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
- package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
- package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
- package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
- package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
- package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
- package/plugins/map-key/src/index.js +13 -0
- package/plugins/map-key/src/index.test.js +31 -0
- package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
- package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
- package/plugins/map-key/src/manifest.js +30 -0
- package/plugins/map-key/src/manifest.test.js +63 -0
- package/plugins/map-key/src/mapKey.scss +4 -0
- package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
- package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
- package/plugins/map-key/src/registry/index.js +3 -0
- package/plugins/map-key/src/registry/index.test.js +15 -0
- package/plugins/menu/dist/css/index.css +80 -0
- package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
- package/plugins/menu/dist/esm/index.js +1 -0
- package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
- package/plugins/menu/dist/umd/index.js +2 -0
- package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/menu/package.json +28 -0
- package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
- package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
- package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
- package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
- package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
- package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
- package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
- package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
- package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
- package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
- package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
- package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
- package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
- package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
- package/plugins/menu/src/index.js +13 -0
- package/plugins/menu/src/index.test.js +31 -0
- package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
- package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
- package/plugins/menu/src/initialise/createMenu.js +10 -0
- package/plugins/menu/src/initialise/createMenu.test.js +37 -0
- package/plugins/menu/src/manifest.js +35 -0
- package/plugins/menu/src/manifest.test.js +63 -0
- package/plugins/menu/src/menu.scss +4 -0
- package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
- package/plugins/menu/src/reducers/pluginState.js +35 -0
- package/plugins/menu/src/reducers/pluginState.test.js +57 -0
- package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
- package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
- package/plugins/menu/src/registry/index.js +3 -0
- package/plugins/menu/src/registry/index.test.js +15 -0
- package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
- package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +2 -3
- package/plugins/search/src/components/Form/Form.module.scss +9 -20
- package/plugins/search/src/index.js +0 -1
- package/plugins/search/src/index.test.js +2 -4
- package/plugins/search/src/manifest.js +1 -1
- package/plugins/search/src/search.scss +17 -16
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
- package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
- package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
- package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
- package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
- package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
- package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
- package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
- package/providers/maplibre/src/utils/hoverCursor.js +30 -14
- package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
- package/providers/maplibre/src/utils/patternImages.js +1 -1
- package/providers/maplibre/src/utils/symbolImages.js +41 -3
- package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
- package/rollup.esm.mjs +11 -1
- package/src/App/components/Actions/Actions.test.jsx +47 -0
- package/src/App/components/Hints/Hints.test.jsx +13 -0
- package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
- package/src/App/components/Panel/Panel.jsx +18 -3
- package/src/App/components/Panel/Panel.module.scss +18 -8
- package/src/App/components/Panel/Panel.test.jsx +34 -0
- package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
- package/src/App/hooks/useFeatureItems.js +16 -7
- package/src/App/hooks/useFeatureItems.test.js +57 -0
- package/src/App/hooks/useLayoutMeasurements.js +95 -84
- package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
- package/src/App/layout/Layout.jsx +9 -10
- package/src/App/layout/Layout.test.jsx +2 -0
- package/src/App/layout/layout.module.scss +31 -17
- package/src/App/registry/pluginRegistry.js +1 -1
- package/src/App/registry/pluginRegistry.test.js +12 -0
- package/src/App/renderer/HtmlElementHost.jsx +18 -5
- package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
- package/src/App/renderer/mapButtons.js +28 -12
- package/src/App/renderer/mapButtons.test.js +17 -4
- package/src/App/renderer/mapControls.js +3 -1
- package/src/App/renderer/mapControls.test.js +16 -0
- package/src/App/renderer/mapPanels.js +44 -9
- package/src/App/renderer/mapPanels.test.js +51 -2
- package/src/App/renderer/orderItems.js +28 -0
- package/src/App/renderer/orderItems.test.js +50 -0
- package/src/App/renderer/slotAggregator.js +2 -18
- package/src/App/renderer/slotHelpers.js +3 -1
- package/src/App/renderer/slotHelpers.test.js +5 -0
- package/src/App/renderer/slots.js +3 -0
- package/src/App/store/AppProvider.jsx +1 -0
- package/src/App/store/appDispatchMiddleware.js +1 -1
- package/src/App/store/appDispatchMiddleware.test.js +8 -0
- package/src/config/events.js +2 -0
- package/src/scss/settings/_colors.scss +1 -0
- package/src/scss/settings/_dimensions.scss +2 -3
- package/src/scss/settings/_transition.scss +1 -1
- package/src/services/eventBus.js +44 -0
- package/src/services/eventBus.test.js +83 -0
- package/src/services/symbolRegistry.js +1 -1
- package/src/types.js +11 -1
- package/webpack.umd.mjs +2 -0
- package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
- package/plugins/datasets/src/components/Key/Key.jsx +0 -50
- package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
- package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
- package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
- package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
- package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
- package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
- package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
- package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
- /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
- /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
- /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
|
@@ -49,8 +49,7 @@ function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
|
|
|
49
49
|
})
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function createButtonClickHandler (
|
|
53
|
-
const [, config] = btn
|
|
52
|
+
function createButtonClickHandler (config, appState, evaluateProp) {
|
|
54
53
|
const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
|
|
55
54
|
const isToggle = config.isPressed !== undefined || !!config.pressedWhen
|
|
56
55
|
|
|
@@ -134,15 +133,33 @@ function applySlotExclusivity (matching, appState) {
|
|
|
134
133
|
return matching.filter(([_, config]) => config.pluginId === exclusivePluginId)
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Builds the props for a <SlotButton>. isHidden/variant are included here (not just derived
|
|
138
|
+
* inside SlotButton) because Actions.jsx also reads them directly off the 'actions' slot's
|
|
139
|
+
* immediate children via React.Children.toArray — they need to be top-level props on whatever
|
|
140
|
+
* element ends up there, not just used internally to build MapButton.
|
|
141
|
+
*/
|
|
142
|
+
const slotButtonProps = ({ buttonId, config, appState, appConfig, evaluateProp }) => ({
|
|
143
|
+
buttonId,
|
|
144
|
+
config,
|
|
145
|
+
appState,
|
|
146
|
+
appConfig,
|
|
147
|
+
evaluateProp,
|
|
148
|
+
isHidden: appState.hiddenButtons.has(buttonId),
|
|
149
|
+
variant: config.variant
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Renders a single button's MapButton element for its slot, computing its click handler
|
|
154
|
+
* and derived state (panel-open, showLabel fallback, etc.) from its config and appState.
|
|
155
|
+
*/
|
|
156
|
+
function SlotButton ({ buttonId, config, appState, appConfig, evaluateProp }) {
|
|
139
157
|
const bpConfig = config[appState.breakpoint] ?? {}
|
|
140
|
-
const handleClick = createButtonClickHandler(
|
|
158
|
+
const handleClick = createButtonClickHandler(config, appState, evaluateProp)
|
|
141
159
|
const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
|
|
142
160
|
|
|
143
161
|
return (
|
|
144
162
|
<MapButton
|
|
145
|
-
key={buttonId}
|
|
146
163
|
buttonId={buttonId}
|
|
147
164
|
iconId={evaluateProp(config.iconId, config.pluginId)}
|
|
148
165
|
iconSvgContent={evaluateProp(config.iconSvgContent, config.pluginId)}
|
|
@@ -219,21 +236,20 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
|
|
|
219
236
|
id: buttonId,
|
|
220
237
|
type: 'button',
|
|
221
238
|
order,
|
|
222
|
-
element:
|
|
239
|
+
element: <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />
|
|
223
240
|
})
|
|
224
241
|
}
|
|
225
242
|
|
|
226
243
|
for (const [groupName, { label, order: groupOrder, members }] of groupMap) {
|
|
227
244
|
if (members.length < 2) {
|
|
228
245
|
// Singleton group: degrade to a regular button using the group's slot order
|
|
229
|
-
const
|
|
230
|
-
const [buttonId, config] = btn
|
|
246
|
+
const [buttonId, config] = members[0]
|
|
231
247
|
const order = groupOrder || config[breakpoint]?.order || 0
|
|
232
248
|
result.push({
|
|
233
249
|
id: buttonId,
|
|
234
250
|
type: 'button',
|
|
235
251
|
order,
|
|
236
|
-
element:
|
|
252
|
+
element: <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />
|
|
237
253
|
})
|
|
238
254
|
continue
|
|
239
255
|
}
|
|
@@ -251,7 +267,7 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
|
|
|
251
267
|
order: groupOrder,
|
|
252
268
|
element: (
|
|
253
269
|
<div key={`group-${groupName}`} role='group' aria-label={label} className='im-c-button-group'>{/* NOSONAR - div with role="group" is correct for a button group */}
|
|
254
|
-
{sorted.map(
|
|
270
|
+
{sorted.map(([buttonId, config]) => <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />)}
|
|
255
271
|
</div>
|
|
256
272
|
)
|
|
257
273
|
})
|
|
@@ -264,7 +280,7 @@ export {
|
|
|
264
280
|
mapButtons,
|
|
265
281
|
getMatchingButtons,
|
|
266
282
|
applySlotExclusivity,
|
|
267
|
-
|
|
283
|
+
SlotButton,
|
|
268
284
|
resolveGroupName,
|
|
269
285
|
resolveGroupLabel,
|
|
270
286
|
resolveGroupOrder
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { mapButtons, getMatchingButtons, applySlotExclusivity,
|
|
2
|
+
import { mapButtons, getMatchingButtons, applySlotExclusivity, SlotButton, resolveGroupName, resolveGroupLabel, resolveGroupOrder } from './mapButtons.js'
|
|
3
3
|
import { logger } from '../../services/logger.js'
|
|
4
4
|
import { getPanelConfig } from '../registry/panelRegistry.js'
|
|
5
5
|
|
|
@@ -156,11 +156,11 @@ describe('mapButtons module', () => {
|
|
|
156
156
|
})
|
|
157
157
|
|
|
158
158
|
// -------------------------
|
|
159
|
-
//
|
|
159
|
+
// SlotButton tests
|
|
160
160
|
// -------------------------
|
|
161
|
-
describe('
|
|
161
|
+
describe('SlotButton', () => {
|
|
162
162
|
const render = (config, state = appState) =>
|
|
163
|
-
|
|
163
|
+
SlotButton({ buttonId: 'id', config, appState: state, appConfig, evaluateProp })
|
|
164
164
|
|
|
165
165
|
it('renders a MapButton with correct basic props', () => {
|
|
166
166
|
const result = render(baseBtn)
|
|
@@ -404,5 +404,18 @@ describe('mapButtons module', () => {
|
|
|
404
404
|
expect(result).toHaveLength(1)
|
|
405
405
|
expect(result[0].id).toBe('parent')
|
|
406
406
|
})
|
|
407
|
+
|
|
408
|
+
// Actions.jsx (the 'actions' slot wrapper) reads isHidden/variant directly off each slot
|
|
409
|
+
// item's top-level element props via React.Children.toArray — these two guard that contract.
|
|
410
|
+
it('carries isHidden as a top-level prop on the slot element, not just inside MapButton', () => {
|
|
411
|
+
appState.buttonConfig = ({ b1: baseBtn })
|
|
412
|
+
appState.hiddenButtons = new Set(['b1'])
|
|
413
|
+
expect(map()[0].element.props.isHidden).toBe(true)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('carries variant as a top-level prop on the slot element, not just inside MapButton', () => {
|
|
417
|
+
appState.buttonConfig = ({ b1: { ...baseBtn, variant: 'touch' } })
|
|
418
|
+
expect(map()[0].element.props.variant).toBe('touch')
|
|
419
|
+
})
|
|
407
420
|
})
|
|
408
421
|
})
|
|
@@ -28,7 +28,9 @@ export function mapControls ({ slot, appState, evaluateProp }) {
|
|
|
28
28
|
return false
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// A control may also target a panel's body directly via the `<panelId>-panel`
|
|
32
|
+
// slot convention (mirrors the `<buttonId>-button` convention panels already use).
|
|
33
|
+
const slotAllowed = allowedSlots.control.includes(bpConfig.slot) || bpConfig.slot?.endsWith('-panel')
|
|
32
34
|
const inModeWhitelist = control.includeModes?.includes(mode) ?? true
|
|
33
35
|
const inExcludeModes = control.excludeModes?.includes(mode) ?? false
|
|
34
36
|
|
|
@@ -164,4 +164,20 @@ describe('mapControls', () => {
|
|
|
164
164
|
const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
|
|
165
165
|
expect(result.map(c => c.id)).toEqual(['ctrl1'])
|
|
166
166
|
})
|
|
167
|
+
|
|
168
|
+
it('matches a control targeting a panel-body slot via the <panelId>-panel convention', () => {
|
|
169
|
+
defaultAppState.controlConfig = ({
|
|
170
|
+
ctrl1: { id: 'ctrl1', desktop: { slot: 'map-styles-panel', order: 1 }, includeModes: ['view'] }
|
|
171
|
+
})
|
|
172
|
+
const result = mapControls({ slot: 'map-styles-panel', appState: defaultAppState, evaluateProp: (p) => p })
|
|
173
|
+
expect(result.map(c => c.id)).toEqual(['ctrl1'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('does not match a panel-body-targeting control against an unrelated slot', () => {
|
|
177
|
+
defaultAppState.controlConfig = ({
|
|
178
|
+
ctrl1: { id: 'ctrl1', desktop: { slot: 'map-styles-panel', order: 1 }, includeModes: ['view'] }
|
|
179
|
+
})
|
|
180
|
+
const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
|
|
181
|
+
expect(result).toEqual([])
|
|
182
|
+
})
|
|
167
183
|
})
|
|
@@ -4,6 +4,10 @@ import { withPluginContexts } from './pluginWrapper.js'
|
|
|
4
4
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
5
5
|
import { allowedSlots } from './slots.js'
|
|
6
6
|
import { resolveTargetSlot, isModeAllowed, isConsumerHtml } from './slotHelpers.js'
|
|
7
|
+
import { mapControls } from './mapControls.js'
|
|
8
|
+
import { orderItems } from './orderItems.js'
|
|
9
|
+
import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
10
|
+
import { logger } from '../../services/logger.js'
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Determines whether a panel should be rendered in the given slot.
|
|
@@ -73,13 +77,7 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
73
77
|
const plugin = pluginRegistry.registeredPlugins.find(p => p.id === config.pluginId)
|
|
74
78
|
const pluginId = plugin?.id
|
|
75
79
|
|
|
76
|
-
const
|
|
77
|
-
? withPluginContexts(config.render, {
|
|
78
|
-
...props,
|
|
79
|
-
pluginId,
|
|
80
|
-
pluginConfig: plugin?.config
|
|
81
|
-
})
|
|
82
|
-
: null
|
|
80
|
+
const html = pluginId ? evaluateProp(config.html, pluginId) : config.html
|
|
83
81
|
|
|
84
82
|
return {
|
|
85
83
|
id: panelId,
|
|
@@ -92,12 +90,49 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
92
90
|
panelConfig={config}
|
|
93
91
|
props={props}
|
|
94
92
|
focusOnOpen={focusOnOpen}
|
|
95
|
-
|
|
93
|
+
items={buildPanelBodyItems({ panelId, config, props, plugin, pluginId, html, appState, evaluateProp })}
|
|
96
94
|
label={evaluateProp(config.label, pluginId)}
|
|
97
|
-
html={
|
|
95
|
+
html={html}
|
|
98
96
|
/>
|
|
99
97
|
)
|
|
100
98
|
}
|
|
101
99
|
})
|
|
102
100
|
.filter(Boolean)
|
|
103
101
|
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builds the ordered list of body items for a panel: its own render content (if any) plus
|
|
105
|
+
* any controls registered against its `<panelId>-panel` slot by other plugins. Static-html
|
|
106
|
+
* panels don't build an items list — dangerouslySetInnerHTML can't host injected controls,
|
|
107
|
+
* so any controls targeting one are silently skipped (with a dev warning).
|
|
108
|
+
*/
|
|
109
|
+
function buildPanelBodyItems ({ panelId, config, props, plugin, pluginId, html, appState, evaluateProp }) {
|
|
110
|
+
const injectedItems = mapControls({
|
|
111
|
+
slot: `${stringToKebab(panelId)}-panel`,
|
|
112
|
+
appState,
|
|
113
|
+
evaluateProp
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (html) {
|
|
117
|
+
/* istanbul ignore next */
|
|
118
|
+
if (process.env.NODE_ENV !== 'production' && injectedItems.length > 0) {
|
|
119
|
+
logger.warn(`Panel "${panelId}" uses static html — controls targeting its slot are not rendered.`)
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!config.render) {
|
|
125
|
+
return injectedItems
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const WrappedChild = withPluginContexts(config.render, {
|
|
129
|
+
...props,
|
|
130
|
+
pluginId,
|
|
131
|
+
pluginConfig: plugin?.config
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return orderItems([
|
|
135
|
+
{ id: panelId, order: 0, element: <WrappedChild {...props} /> },
|
|
136
|
+
...injectedItems
|
|
137
|
+
])
|
|
138
|
+
}
|
|
@@ -2,10 +2,14 @@ import React from 'react'
|
|
|
2
2
|
import { mapPanels } from './mapPanels.js'
|
|
3
3
|
import { registeredPlugins } from '../registry/pluginRegistry.js'
|
|
4
4
|
import { withPluginContexts } from './pluginWrapper.js'
|
|
5
|
+
import { mapControls } from './mapControls.js'
|
|
6
|
+
import { logger } from '../../services/logger.js'
|
|
5
7
|
|
|
8
|
+
jest.mock('../../services/logger.js')
|
|
6
9
|
jest.mock('../registry/panelRegistry.js')
|
|
7
10
|
jest.mock('../registry/pluginRegistry.js', () => ({ registeredPlugins: [] }))
|
|
8
11
|
jest.mock('./pluginWrapper.js', () => ({ withPluginContexts: jest.fn((c) => c) }))
|
|
12
|
+
jest.mock('./mapControls.js', () => ({ mapControls: jest.fn(() => []) }))
|
|
9
13
|
jest.mock('../components/Panel/Panel.jsx', () => ({ Panel: (props) => <div data-testid='panel' {...props} /> }))
|
|
10
14
|
jest.mock('./slots.js', () => ({ allowedSlots: { panel: ['header', 'modal', 'left-top'] } }))
|
|
11
15
|
|
|
@@ -23,6 +27,7 @@ describe('mapPanels', () => {
|
|
|
23
27
|
|
|
24
28
|
beforeEach(() => {
|
|
25
29
|
jest.clearAllMocks()
|
|
30
|
+
mapControls.mockReturnValue([])
|
|
26
31
|
registeredPlugins.length = 0
|
|
27
32
|
defaultAppState = {
|
|
28
33
|
breakpoint: 'desktop',
|
|
@@ -116,9 +121,53 @@ describe('mapPanels', () => {
|
|
|
116
121
|
)
|
|
117
122
|
})
|
|
118
123
|
|
|
119
|
-
it('
|
|
124
|
+
it('builds an empty items list if there is no render function and no injected controls', () => {
|
|
120
125
|
const result = map()
|
|
121
|
-
expect(result[0].element.props.
|
|
126
|
+
expect(result[0].element.props.items).toEqual([])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('builds a single-item items list for a panel with its own render content', () => {
|
|
130
|
+
const renderFn = () => <div>child</div>
|
|
131
|
+
defaultAppState.panelConfig = ({ p1: { ...baseConfig, render: renderFn } })
|
|
132
|
+
const result = map()
|
|
133
|
+
expect(result[0].element.props.items).toEqual([
|
|
134
|
+
{ id: 'p1', order: 0, element: expect.anything() }
|
|
135
|
+
])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('requests injected controls using the <panelId>-panel slot convention', () => {
|
|
139
|
+
map()
|
|
140
|
+
expect(mapControls).toHaveBeenCalledWith(expect.objectContaining({ slot: 'p1-panel' }))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('merges and orders the panel\'s own content with controls injected via mapControls', () => {
|
|
144
|
+
const renderFn = () => <div>child</div>
|
|
145
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 1, element: <span>injected</span> }])
|
|
146
|
+
defaultAppState.panelConfig = ({ p1: { ...baseConfig, render: renderFn } })
|
|
147
|
+
const result = map()
|
|
148
|
+
// injected1 has order 1, so it's spliced ahead of the panel's own (unordered) content
|
|
149
|
+
expect(result[0].element.props.items.map(i => i.id)).toEqual(['injected1', 'p1'])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('returns just the injected controls when the panel has neither render nor html', () => {
|
|
153
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 0, element: <span>injected</span> }])
|
|
154
|
+
defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'] } })
|
|
155
|
+
const result = map()
|
|
156
|
+
expect(result[0].element.props.items.map(i => i.id)).toEqual(['injected1'])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('does not build an items list for a static-html panel (dangerouslySetInnerHTML can\'t host injected controls)', () => {
|
|
160
|
+
defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'], pluginId: 'plug1', html: '<p>Hi</p>' } })
|
|
161
|
+
const result = map()
|
|
162
|
+
expect(result[0].element.props.items).toBeUndefined()
|
|
163
|
+
expect(result[0].element.props.html).toBe('<p>Hi</p>')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('warns in dev when controls target a static-html panel', () => {
|
|
167
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 0, element: <span>injected</span> }])
|
|
168
|
+
defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'], pluginId: 'plug1', html: '<p>Hi</p>' } })
|
|
169
|
+
map()
|
|
170
|
+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('p1'))
|
|
122
171
|
})
|
|
123
172
|
|
|
124
173
|
it('returns correct structure and defaults', () => {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/core/renderers/orderItems.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Orders a list of `{ id, order, element }` items for a single render target
|
|
5
|
+
* (a layout slot, or a panel body).
|
|
6
|
+
*
|
|
7
|
+
* Items without an explicit `order` (or `order === 0`) keep their natural/registration
|
|
8
|
+
* sequence. Items with an explicit `order` are spliced into that 1-based position among
|
|
9
|
+
* the unordered items, clamped to the valid range.
|
|
10
|
+
*/
|
|
11
|
+
export function orderItems (items) {
|
|
12
|
+
// Start with items that have no explicit order (in their natural sequence)
|
|
13
|
+
const withoutOrder = items.filter(item => item.order == null || item.order === 0)
|
|
14
|
+
const withOrder = items.filter(item => item.order != null && item.order !== 0)
|
|
15
|
+
|
|
16
|
+
// Sort items with order by their order value (stable sort preserves ties)
|
|
17
|
+
withOrder.sort((a, b) => a.order - b.order)
|
|
18
|
+
|
|
19
|
+
// Insert each ordered item at its specified position
|
|
20
|
+
const result = [...withoutOrder]
|
|
21
|
+
for (const item of withOrder) {
|
|
22
|
+
// Clamp position to valid range (1-based, so subtract 1 for array index)
|
|
23
|
+
const pos = Math.min(Math.max(item.order - 1, 0), result.length)
|
|
24
|
+
result.splice(pos, 0, item)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { orderItems } from './orderItems.js'
|
|
2
|
+
|
|
3
|
+
describe('orderItems', () => {
|
|
4
|
+
it('keeps unordered items in their natural sequence', () => {
|
|
5
|
+
const items = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]
|
|
6
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b', 'c'])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('treats order 0 the same as no order', () => {
|
|
10
|
+
const items = [{ id: 'a', order: 0 }, { id: 'b' }]
|
|
11
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b'])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('inserts an ordered item at its 1-based position among unordered items', () => {
|
|
15
|
+
const items = [{ id: 'a' }, { id: 'b' }, { id: 'c', order: 1 }]
|
|
16
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['c', 'a', 'b'])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('splices ordered items in ascending order, each relative to the growing result', () => {
|
|
20
|
+
const items = [
|
|
21
|
+
{ id: 'a' },
|
|
22
|
+
{ id: 'b', order: 2 },
|
|
23
|
+
{ id: 'c', order: 1 }
|
|
24
|
+
]
|
|
25
|
+
// c (order 1) is spliced in first, ahead of 'a': [c, a]
|
|
26
|
+
// b (order 2) is then spliced at index 1 of that 2-item result: [c, b, a]
|
|
27
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['c', 'b', 'a'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('clamps an order larger than the list to the end', () => {
|
|
31
|
+
const items = [{ id: 'a' }, { id: 'b' }, { id: 'c', order: 99 }]
|
|
32
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b', 'c'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('splices equal-order items each at position 0, reversing their relative order', () => {
|
|
36
|
+
// The stable sort keeps a before b, but with no unordered items to anchor against,
|
|
37
|
+
// each splice(0, 0, item) inserts ahead of the previous one.
|
|
38
|
+
const items = [{ id: 'a', order: 1 }, { id: 'b', order: 1 }]
|
|
39
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['b', 'a'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('handles an all-ordered list', () => {
|
|
43
|
+
const items = [{ id: 'a', order: 2 }, { id: 'b', order: 1 }]
|
|
44
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['b', 'a'])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('handles an empty list', () => {
|
|
48
|
+
expect(orderItems([])).toEqual([])
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -2,28 +2,12 @@
|
|
|
2
2
|
import { mapControls } from './mapControls.js'
|
|
3
3
|
import { mapPanels } from './mapPanels.js'
|
|
4
4
|
import { mapButtons } from './mapButtons.js'
|
|
5
|
+
import { orderItems } from './orderItems.js'
|
|
5
6
|
|
|
6
7
|
export function getSlotItems ({ evaluateProp, ...args }) {
|
|
7
8
|
const controls = mapControls({ evaluateProp, ...args })
|
|
8
9
|
const panels = mapPanels({ evaluateProp, ...args })
|
|
9
10
|
const buttons = mapButtons({ evaluateProp, ...args })
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// Start with items that have no explicit order (in their natural sequence)
|
|
14
|
-
const withoutOrder = items.filter(item => item.order == null || item.order === 0)
|
|
15
|
-
const withOrder = items.filter(item => item.order != null && item.order !== 0)
|
|
16
|
-
|
|
17
|
-
// Sort items with order by their order value (stable sort preserves ties)
|
|
18
|
-
withOrder.sort((a, b) => a.order - b.order)
|
|
19
|
-
|
|
20
|
-
// Insert each ordered item at its specified position
|
|
21
|
-
const result = [...withoutOrder]
|
|
22
|
-
for (const item of withOrder) {
|
|
23
|
-
// Clamp position to valid range (1-based, so subtract 1 for array index)
|
|
24
|
-
const pos = Math.min(Math.max(item.order - 1, 0), result.length)
|
|
25
|
-
result.splice(pos, 0, item)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return result
|
|
12
|
+
return orderItems([...controls, ...panels, ...buttons])
|
|
29
13
|
}
|
|
@@ -39,7 +39,9 @@ export const isControlVisible = (control, { breakpoint, mode, isFullscreen }) =>
|
|
|
39
39
|
if (!bpConfig) {
|
|
40
40
|
return false
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
// A control may also target a panel's body directly via the `<panelId>-panel`
|
|
43
|
+
// slot convention (mirrors the `<buttonId>-button` convention panels already use).
|
|
44
|
+
if (!allowedSlots.control.includes(bpConfig.slot) && !bpConfig.slot?.endsWith('-panel')) {
|
|
43
45
|
return false
|
|
44
46
|
}
|
|
45
47
|
if (!isModeAllowed(control, mode)) {
|
|
@@ -65,6 +65,11 @@ describe('isControlVisible', () => {
|
|
|
65
65
|
it('returns true when inline:false and fullscreen', () => {
|
|
66
66
|
expect(isControlVisible({ ...base, inline: false }, { breakpoint: 'desktop', mode: 'view', isFullscreen: true })).toBe(true)
|
|
67
67
|
})
|
|
68
|
+
|
|
69
|
+
it('returns true for a control targeting a panel-body slot via the <panelId>-panel convention', () => {
|
|
70
|
+
const panelTargeting = { desktop: { slot: 'map-styles-panel' } }
|
|
71
|
+
expect(isControlVisible(panelTargeting, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(true)
|
|
72
|
+
})
|
|
68
73
|
})
|
|
69
74
|
|
|
70
75
|
describe('isConsumerHtml', () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/core/renderers/slots.js
|
|
2
2
|
export const layoutSlots = Object.freeze({
|
|
3
3
|
SIDE: 'side',
|
|
4
|
+
HEADER: 'header',
|
|
4
5
|
BANNER: 'banner',
|
|
5
6
|
TOP_LEFT: 'top-left',
|
|
6
7
|
TOP_MIDDLE: 'top-middle',
|
|
@@ -19,6 +20,7 @@ export const layoutSlots = Object.freeze({
|
|
|
19
20
|
|
|
20
21
|
export const allowedSlots = Object.freeze({
|
|
21
22
|
control: [
|
|
23
|
+
layoutSlots.HEADER,
|
|
22
24
|
layoutSlots.BANNER,
|
|
23
25
|
layoutSlots.TOP_LEFT,
|
|
24
26
|
layoutSlots.TOP_RIGHT,
|
|
@@ -31,6 +33,7 @@ export const allowedSlots = Object.freeze({
|
|
|
31
33
|
],
|
|
32
34
|
panel: [
|
|
33
35
|
layoutSlots.SIDE,
|
|
36
|
+
layoutSlots.HEADER,
|
|
34
37
|
layoutSlots.BANNER,
|
|
35
38
|
layoutSlots.LEFT_TOP,
|
|
36
39
|
layoutSlots.LEFT_BOTTOM,
|
|
@@ -101,7 +101,7 @@ export function handleActionSideEffects (action, previousState, panelConfig, eve
|
|
|
101
101
|
const mergedConfig = deepMerge(defaultControlConfig, config)
|
|
102
102
|
BREAKPOINTS.forEach(bp => {
|
|
103
103
|
const slot = mergedConfig[bp]?.slot
|
|
104
|
-
if (slot && !allowedSlots.control.includes(slot)) {
|
|
104
|
+
if (slot && !allowedSlots.control.includes(slot) && !slot.endsWith('-panel')) {
|
|
105
105
|
logger.warn(`control "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.control.join(', ')}.`)
|
|
106
106
|
}
|
|
107
107
|
})
|
|
@@ -185,6 +185,14 @@ describe('ADD_CONTROL', () => {
|
|
|
185
185
|
)
|
|
186
186
|
expect(console.warn).not.toHaveBeenCalled()
|
|
187
187
|
})
|
|
188
|
+
|
|
189
|
+
it('does not warn for a control targeting a panel-body slot', () => {
|
|
190
|
+
run(
|
|
191
|
+
{ type: 'ADD_CONTROL', payload: { id: 'myCtrl', config: { desktop: { slot: 'map-styles-panel' } } } },
|
|
192
|
+
{ breakpoint: 'desktop' }
|
|
193
|
+
)
|
|
194
|
+
expect(console.warn).not.toHaveBeenCalled()
|
|
195
|
+
})
|
|
188
196
|
})
|
|
189
197
|
|
|
190
198
|
describe('ADD_PANEL', () => {
|
package/src/config/events.js
CHANGED
|
@@ -147,6 +147,8 @@ export const EVENTS = {
|
|
|
147
147
|
MAP_SET_SIZE: 'map:setsize',
|
|
148
148
|
/** @internal Set the accessible features list. Payload: { items: { id: string, label: string }[] } */
|
|
149
149
|
MAP_SET_FEATURES: 'map:setfeatures',
|
|
150
|
+
/** @internal Suppress/restore the accessible features list. Payload: { suppressed: boolean } */
|
|
151
|
+
MAP_SET_FEATURES_SUPPRESSED: 'map:setfeaturessuppressed',
|
|
150
152
|
/** @internal Set the active feature in the accessible features list. Payload: { id: string | null } */
|
|
151
153
|
MAP_SET_ACTIVE_FEATURE: 'map:setactivefeature',
|
|
152
154
|
/** @internal Select the active listbox feature as the real selection (Enter key). Payload: none */
|
|
@@ -39,9 +39,8 @@
|
|
|
39
39
|
--panel-close-button-gap: 4px;
|
|
40
40
|
--panel-heading-margin-top: 12px;
|
|
41
41
|
--panel-margin: 15px;
|
|
42
|
-
--banner-
|
|
43
|
-
--banner-
|
|
44
|
-
--banner-collapse-margin: 14px;
|
|
42
|
+
--banner-preferred-width: 280px; // Docked max-width, and the gutter threshold JS docks/stacks against
|
|
43
|
+
--banner-border-radius: 0px; // Styled like a hint, not a panel
|
|
45
44
|
|
|
46
45
|
// Popup menu
|
|
47
46
|
--popup-menu-min-width: 200px;
|
package/src/services/eventBus.js
CHANGED
|
@@ -108,6 +108,50 @@ class EventBus {
|
|
|
108
108
|
return this.emit(eventName, ...args)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Like `emit`, but also registers a listener for the corresponding `Required` event.
|
|
113
|
+
*
|
|
114
|
+
* Use this for events that may be requested before or after the emitter side is ready
|
|
115
|
+
* e.g. called from plugins that require a state reference from a different plugin like this:
|
|
116
|
+
* const removeRegistryListener = eventBus.emitWhenRequested('datasets:registry', datasetRegistry)
|
|
117
|
+
*
|
|
118
|
+
* @param {string} eventName
|
|
119
|
+
* @param {...*} args
|
|
120
|
+
* @returns {Function} A function to remove the listener for the `Required` event.
|
|
121
|
+
*/
|
|
122
|
+
emitWhenRequested (eventName, ...args) {
|
|
123
|
+
const eventRequestedName = `${eventName}Requested`
|
|
124
|
+
const eventReadyName = `${eventName}Ready`
|
|
125
|
+
const handleOnRequired = () => this.emit(eventReadyName, ...args)
|
|
126
|
+
// Add a listener that will emit the event whenever requested
|
|
127
|
+
this.on(eventRequestedName, handleOnRequired)
|
|
128
|
+
// Also emit the event immediately, in case a listener is already waiting for it
|
|
129
|
+
handleOnRequired()
|
|
130
|
+
return () => this.off(eventRequestedName, handleOnRequired)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Like `emit`, but appends `Requested` to the event.
|
|
135
|
+
* used for events that require a response from another plugin,
|
|
136
|
+
* e.g. `datasets:registryRequested` will be emitted by the menu plugin,
|
|
137
|
+
* via `eventBus.requestOnce('datasets:registry')`, and the datasets plugin will respond with `datasets:registryReady`.
|
|
138
|
+
* and the datasets plugin will respond with `datasets:registryReady`.
|
|
139
|
+
* via `eventBus.emitWhenRequested('datasets:registry', datasetRegistry)`.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} eventName
|
|
142
|
+
* @param {Function} eventHandler
|
|
143
|
+
* @returns {this}
|
|
144
|
+
*/
|
|
145
|
+
requestOnce (eventName, eventHandler) {
|
|
146
|
+
const eventRequestedName = `${eventName}Requested`
|
|
147
|
+
const eventReadyName = `${eventName}Ready`
|
|
148
|
+
// Note - must register the once listener before emitting the requested event,
|
|
149
|
+
// otherwise the event won't emit, as there are no listeners yet.
|
|
150
|
+
this.once(eventReadyName, eventHandler)
|
|
151
|
+
this.emit(eventRequestedName, eventName)
|
|
152
|
+
return this
|
|
153
|
+
}
|
|
154
|
+
|
|
111
155
|
/**
|
|
112
156
|
* Remove all handlers and clear any queued events.
|
|
113
157
|
*/
|