@defra/interactive-map 0.0.4-alpha → 0.0.6-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/index.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api.md +8 -0
- package/package.json +3 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +2 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt +1 -0
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js.LICENSE.txt +1 -0
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/datasets.js +55 -2
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +206 -0
- package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +38 -0
- package/plugins/beta/datasets/src/handleSetMapStyle.js +8 -1
- package/plugins/beta/datasets/src/mapLayers.js +41 -3
- package/plugins/beta/datasets/src/utils/bbox.js +113 -2
- package/plugins/beta/draw-ml/dist/css/index.css +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-ml/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/api/addFeature.js +15 -3
- package/plugins/beta/draw-ml/src/api/editFeature.js +16 -5
- package/plugins/beta/draw-ml/src/api/newLine.js +32 -7
- package/plugins/beta/draw-ml/src/api/newPolygon.js +33 -8
- package/plugins/beta/draw-ml/src/defaults.js +1 -0
- package/plugins/beta/draw-ml/src/draw.scss +23 -0
- package/plugins/beta/draw-ml/src/events.js +55 -113
- package/plugins/beta/draw-ml/src/manifest.js +64 -38
- package/plugins/beta/draw-ml/src/mapboxSnap.js +16 -12
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +4 -3
- package/plugins/beta/draw-ml/src/modes/editVertex/geometryHelpers.js +135 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/helpers.js +2 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +134 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +133 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +141 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +121 -0
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +51 -406
- package/plugins/beta/draw-ml/src/utils/flattenStyleProperties.js +49 -0
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/src/Frame.jsx +1 -1
- package/plugins/beta/map-styles/dist/css/index.css +1 -1
- package/plugins/beta/map-styles/dist/esm/index.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/beta/map-styles/src/mapStyles.scss +4 -1
- package/plugins/interact/dist/css/index.css +1 -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/InteractInit.jsx +1 -1
- package/plugins/interact/src/defaults.js +1 -0
- package/plugins/interact/src/events.js +37 -52
- package/plugins/interact/src/events.test.js +35 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +80 -48
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +64 -8
- package/plugins/interact/src/interact.scss +6 -0
- package/plugins/interact/src/reducer.js +25 -27
- package/plugins/interact/src/reducer.test.js +19 -6
- package/plugins/interact/src/utils/featureQueries.js +2 -2
- package/plugins/interact/src/utils/spatial.js +99 -0
- package/plugins/interact/src/utils/spatial.test.js +100 -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/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +2 -2
- package/plugins/search/src/components/Form/Form.jsx +1 -1
- package/plugins/search/src/components/Suggestions/Suggestions.jsx +5 -5
- package/plugins/search/src/datasets.js +1 -1
- package/plugins/search/src/defaults.js +3 -0
- package/plugins/search/src/events/fetchSuggestions.js +48 -53
- package/plugins/search/src/events/formHandlers.js +3 -2
- package/plugins/search/src/events/index.js +1 -1
- package/plugins/search/src/events/inputHandlers.js +3 -1
- package/plugins/search/src/events/suggestionHandlers.js +12 -3
- package/plugins/search/src/search.scss +4 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +12 -12
- package/providers/beta/esri/src/esriProvider.js +1 -1
- package/providers/beta/esri/src/index.js +1 -1
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.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/defaults.js +3 -2
- package/providers/maplibre/src/index.js +22 -19
- package/providers/maplibre/src/maplibreProvider.js +13 -11
- package/providers/maplibre/src/utils/detectWebgl.js +1 -2
- package/providers/maplibre/src/utils/highlightFeatures.js +78 -67
- package/providers/maplibre/src/utils/labels.js +174 -120
- package/providers/maplibre/src/utils/maplibreFixes.js +2 -2
- package/providers/maplibre/src/utils/queryFeatures.js +146 -0
- package/providers/maplibre/src/utils/spatial.js +14 -17
- package/sonar-project.properties +39 -2
- package/src/App/components/Actions/Actions.jsx +2 -2
- package/src/App/components/Actions/Actions.module.scss +2 -2
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +3 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +9 -0
- package/src/App/components/Logo/Logo.jsx +1 -1
- package/src/App/components/MapButton/MapButton.jsx +217 -41
- package/src/App/components/MapButton/MapButton.module.scss +32 -21
- package/src/App/components/MapButton/MapButton.test.jsx +128 -84
- package/src/App/components/Markers/Markers.jsx +1 -1
- package/src/App/components/Markers/Markers.module.scss +0 -5
- package/src/App/components/Panel/Panel.jsx +75 -45
- package/src/App/components/Panel/Panel.module.scss +18 -24
- package/src/App/components/Panel/Panel.test.jsx +18 -0
- package/src/App/components/PopupMenu/PopupMenu.jsx +246 -0
- package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +304 -0
- package/src/App/components/Tooltip/Tooltip.jsx +4 -2
- package/src/App/components/Tooltip/getTooltipPosition.js +1 -1
- package/src/App/components/Viewport/MapController.jsx +1 -1
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/Viewport.jsx +2 -2
- package/src/App/components/Viewport/Viewport.module.scss +2 -2
- package/src/App/controls/keyboardActions.js +3 -2
- package/src/App/controls/keyboardShortcuts.js +4 -2
- package/src/App/hooks/useButtonStateEvaluator.js +58 -45
- package/src/App/hooks/useButtonStateEvaluator.test.js +36 -3
- package/src/App/hooks/useCrossHairAPI.js +49 -47
- package/src/App/hooks/useFocusVisible.js +2 -2
- package/src/App/hooks/useInterfaceAPI.js +16 -4
- package/src/App/hooks/useKeyboardHint.js +1 -1
- package/src/App/hooks/useKeyboardShortcuts.js +4 -2
- package/src/App/hooks/useMapAnnouncements.js +34 -32
- package/src/App/hooks/useMapEvents.js +12 -1
- package/src/App/hooks/useMapProviderOverrides.js +3 -3
- package/src/App/hooks/useMapStateSync.js +7 -1
- package/src/App/hooks/useMapURLSync.js +6 -1
- package/src/App/hooks/useMarkersAPI.js +88 -50
- package/src/App/hooks/useMediaQueryDispatch.test.js +48 -0
- package/src/App/hooks/useModalPanelBehaviour.js +46 -40
- package/src/App/hooks/useResizeObserver.js +2 -2
- package/src/App/initialiseApp.js +45 -38
- package/src/App/layout/Layout.jsx +6 -4
- package/src/App/layout/layout.module.scss +13 -17
- package/src/App/registry/keyboardShortcutRegistry.js +12 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +28 -0
- package/src/App/registry/mergeManifests.js +1 -1
- package/src/App/registry/panelRegistry.js +1 -1
- package/src/App/registry/panelRegistry.test.js +30 -1
- package/src/App/registry/pluginRegistry.js +14 -51
- package/src/App/registry/pluginRegistry.test.js +54 -6
- package/src/App/renderer/HtmlElementHost.jsx +186 -0
- package/src/App/renderer/HtmlElementHost.test.jsx +231 -0
- package/src/App/renderer/mapButtons.js +11 -8
- package/src/App/renderer/mapButtons.test.js +6 -4
- package/src/App/renderer/mapControls.js +6 -0
- package/src/App/renderer/mapControls.test.js +10 -2
- package/src/App/renderer/mapPanels.js +39 -26
- package/src/App/renderer/mapPanels.test.js +29 -0
- package/src/App/renderer/pluginWrapper.test.js +28 -0
- package/src/App/renderer/slotHelpers.js +60 -0
- package/src/App/renderer/slotHelpers.test.js +82 -0
- package/src/App/store/AppProvider.jsx +21 -18
- package/src/App/store/AppProvider.test.jsx +40 -0
- package/src/App/store/PluginProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +28 -1
- package/src/App/store/appActionsMap.test.js +26 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +12 -24
- package/src/App/store/mapReducer.js +1 -1
- package/src/InteractiveMap/InteractiveMap.js +79 -6
- package/src/InteractiveMap/InteractiveMap.test.js +164 -2
- package/src/InteractiveMap/behaviourController.js +12 -2
- package/src/InteractiveMap/behaviourController.test.js +82 -1
- package/src/InteractiveMap/deviceChecker.js +1 -1
- package/src/InteractiveMap/deviceChecker.test.js +2 -2
- package/src/InteractiveMap/historyManager.js +41 -5
- package/src/InteractiveMap/historyManager.test.js +37 -6
- package/src/InteractiveMap/polyfills.js +39 -0
- package/src/InteractiveMap/polyfills.test.js +127 -0
- package/src/config/appConfig.js +5 -5
- package/src/config/appConfig.test.js +107 -42
- package/src/config/defaults.js +1 -0
- package/src/index.umd.js +1 -1
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_colors.scss +1 -1
- package/src/scss/settings/_dimensions.scss +4 -0
- package/src/services/eventBus.test.js +24 -1
- package/src/test-utils.js +1 -0
- package/src/types.js +5 -0
- package/src/utils/detectBreakpoint.js +78 -77
- package/src/utils/detectBreakpoint.test.js +50 -4
- package/src/utils/detectInterfaceType.js +6 -5
- package/src/utils/getIsFullscreen.js +3 -1
- package/src/utils/getSafeZoneInset.js +23 -7
- package/src/utils/getSafeZoneInset.test.js +18 -0
- package/src/utils/mapStateSync.js +3 -3
- package/src/utils/queryString.js +1 -1
- package/src/utils/stringToKebab.js +1 -1
- package/src/utils/toggleInertElements.js +37 -12
- package/webpack.dev.mjs +3 -4
- package/plugins/interact/src/utils/turfHelpers.js +0 -30
- package/plugins/interact/src/utils/turfHelpers.test.js +0 -30
- package/plugins/search/src/utils/fetchDataset.js +0 -23
- package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
- package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { registerPanel, addPanel, removePanel, getPanelConfig } from './panelRegistry.js'
|
|
1
|
+
import { createPanelRegistry, registerPanel, addPanel, removePanel, getPanelConfig } from './panelRegistry.js'
|
|
2
2
|
import { defaultPanelConfig } from '../../config/appConfig.js'
|
|
3
3
|
|
|
4
4
|
describe('panelRegistry', () => {
|
|
@@ -88,4 +88,33 @@ describe('panelRegistry', () => {
|
|
|
88
88
|
expect(updatedConfig.panel1).toBeUndefined()
|
|
89
89
|
expect(updatedConfig).not.toBe(config) // Immutable
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
describe('createPanelRegistry (Factory)', () => {
|
|
93
|
+
let registry
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
registry = createPanelRegistry()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('should manage state internally via all methods', () => {
|
|
100
|
+
// Test registerPanel state
|
|
101
|
+
registry.registerPanel({ p1: { title: 'P1' } })
|
|
102
|
+
expect(registry.getPanelConfig()).toHaveProperty('p1')
|
|
103
|
+
expect(registry.getPanelConfig().p1.showLabel).toBe(true)
|
|
104
|
+
|
|
105
|
+
// Test addPanel state and return value
|
|
106
|
+
const added = registry.addPanel('p2', { title: 'P2' })
|
|
107
|
+
expect(added.title).toBe('P2')
|
|
108
|
+
expect(registry.getPanelConfig()).toHaveProperty('p2')
|
|
109
|
+
|
|
110
|
+
// Test removePanel state
|
|
111
|
+
registry.removePanel('p1')
|
|
112
|
+
expect(registry.getPanelConfig()).not.toHaveProperty('p1')
|
|
113
|
+
expect(registry.getPanelConfig()).toHaveProperty('p2')
|
|
114
|
+
|
|
115
|
+
// Test clear state
|
|
116
|
+
registry.clear()
|
|
117
|
+
expect(registry.getPanelConfig()).toEqual({})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
91
120
|
})
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { registerIcon } from './iconRegistry.js'
|
|
3
3
|
import { registerKeyboardShortcut } from './keyboardShortcutRegistry.js'
|
|
4
4
|
|
|
5
|
+
const asArray = (value) => Array.isArray(value) ? value : [value]
|
|
6
|
+
|
|
5
7
|
export function createPluginRegistry ({ registerButton, registerPanel, registerControl }) {
|
|
6
8
|
const registeredPlugins = []
|
|
7
9
|
|
|
@@ -14,76 +16,37 @@ export function createPluginRegistry ({ registerButton, registerPanel, registerC
|
|
|
14
16
|
excludeModes: plugin.config?.excludeModes
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
// --- Register buttons ---
|
|
18
19
|
if (manifest.buttons) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
registerButton({
|
|
25
|
-
[button.id]: {
|
|
26
|
-
...pluginConfig,
|
|
27
|
-
...button
|
|
28
|
-
}
|
|
20
|
+
asArray(manifest.buttons).forEach(button => {
|
|
21
|
+
registerButton({ [button.id]: { ...pluginConfig, ...button } })
|
|
22
|
+
// Flat button registry including anu menu items
|
|
23
|
+
button?.menuItems?.forEach(menuItem => {
|
|
24
|
+
registerButton({ [menuItem.id]: { ...pluginConfig, ...menuItem } })
|
|
29
25
|
})
|
|
30
26
|
})
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
// --- Register panels ---
|
|
34
29
|
if (manifest.panels) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
: [manifest.panels]
|
|
38
|
-
|
|
39
|
-
panels.forEach(panel => {
|
|
40
|
-
registerPanel({
|
|
41
|
-
[panel.id]: {
|
|
42
|
-
...pluginConfig,
|
|
43
|
-
...panel
|
|
44
|
-
}
|
|
45
|
-
})
|
|
30
|
+
asArray(manifest.panels).forEach(panel => {
|
|
31
|
+
registerPanel({ [panel.id]: { ...pluginConfig, ...panel } })
|
|
46
32
|
})
|
|
47
33
|
}
|
|
48
34
|
|
|
49
|
-
// --- Register controls ---
|
|
50
35
|
if (manifest.controls) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
: [manifest.controls]
|
|
54
|
-
|
|
55
|
-
controls.forEach(control => {
|
|
56
|
-
registerControl({
|
|
57
|
-
[control.id]: {
|
|
58
|
-
...pluginConfig,
|
|
59
|
-
...control
|
|
60
|
-
}
|
|
61
|
-
})
|
|
36
|
+
asArray(manifest.controls).forEach(control => {
|
|
37
|
+
registerControl({ [control.id]: { ...pluginConfig, ...control } })
|
|
62
38
|
})
|
|
63
39
|
}
|
|
64
40
|
|
|
65
|
-
// --- Register icons ---
|
|
66
41
|
if (manifest.icons) {
|
|
67
|
-
|
|
68
|
-
? manifest.icons
|
|
69
|
-
: [manifest.icons]
|
|
70
|
-
|
|
71
|
-
icons.forEach(icon =>
|
|
42
|
+
asArray(manifest.icons).forEach(icon =>
|
|
72
43
|
registerIcon({ [icon.id]: icon.svgContent })
|
|
73
44
|
)
|
|
74
45
|
}
|
|
75
46
|
|
|
76
|
-
// --- Register keyboard shortcuts ---
|
|
77
47
|
if (manifest.keyboardShortcuts) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
: [manifest.keyboardShortcuts]
|
|
81
|
-
|
|
82
|
-
shortcuts.forEach(shortcut =>
|
|
83
|
-
registerKeyboardShortcut({
|
|
84
|
-
...pluginConfig,
|
|
85
|
-
shortcut
|
|
86
|
-
})
|
|
48
|
+
asArray(manifest.keyboardShortcuts).forEach(shortcut =>
|
|
49
|
+
registerKeyboardShortcut({ ...pluginConfig, shortcut })
|
|
87
50
|
)
|
|
88
51
|
}
|
|
89
52
|
|
|
@@ -41,7 +41,7 @@ describe('pluginRegistry', () => {
|
|
|
41
41
|
buttons: { id: 'btn1' },
|
|
42
42
|
panels: [{ id: 'panel1' }],
|
|
43
43
|
controls: { id: 'ctrl1' },
|
|
44
|
-
icons: { id: 'icon1', svgContent: 'Comp' },
|
|
44
|
+
icons: { id: 'icon1', svgContent: 'Comp' },
|
|
45
45
|
keyboardShortcuts: { key: 'K' }
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -65,15 +65,50 @@ describe('pluginRegistry', () => {
|
|
|
65
65
|
})
|
|
66
66
|
expect(registerIcon).toHaveBeenCalledWith({ icon1: 'Comp' })
|
|
67
67
|
expect(registerKeyboardShortcut).toHaveBeenCalledWith(
|
|
68
|
-
expect.objectContaining({
|
|
69
|
-
...expectedPluginConfig,
|
|
70
|
-
shortcut: { key: 'K' }
|
|
71
|
-
})
|
|
68
|
+
expect.objectContaining({ ...expectedPluginConfig, shortcut: { key: 'K' } })
|
|
72
69
|
)
|
|
73
|
-
|
|
74
70
|
expect(pluginRegistry.registeredPlugins).toContain(plugin)
|
|
75
71
|
})
|
|
76
72
|
|
|
73
|
+
it('registers nested menuItems for buttons', () => {
|
|
74
|
+
const plugin = {
|
|
75
|
+
id: 'plugin-menu',
|
|
76
|
+
config: { includeModes: ['mode1'], excludeModes: [] },
|
|
77
|
+
manifest: {
|
|
78
|
+
buttons: [
|
|
79
|
+
{
|
|
80
|
+
id: 'parentBtn',
|
|
81
|
+
menuItems: [
|
|
82
|
+
{ id: 'childBtn1', label: 'Child 1' },
|
|
83
|
+
{ id: 'childBtn2', label: 'Child 2' }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pluginRegistry.registerPlugin(plugin)
|
|
91
|
+
|
|
92
|
+
const expectedPluginConfig = {
|
|
93
|
+
pluginId: 'plugin-menu',
|
|
94
|
+
includeModes: ['mode1'],
|
|
95
|
+
excludeModes: []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Parent button
|
|
99
|
+
expect(registerButton).toHaveBeenCalledWith({
|
|
100
|
+
parentBtn: expect.objectContaining(expectedPluginConfig)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Each menu item
|
|
104
|
+
expect(registerButton).toHaveBeenCalledWith({
|
|
105
|
+
childBtn1: expect.objectContaining(expectedPluginConfig)
|
|
106
|
+
})
|
|
107
|
+
expect(registerButton).toHaveBeenCalledWith({
|
|
108
|
+
childBtn2: expect.objectContaining(expectedPluginConfig)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
77
112
|
it('handles single vs array manifests correctly', () => {
|
|
78
113
|
const plugin = {
|
|
79
114
|
id: 'plugin3',
|
|
@@ -103,4 +138,17 @@ describe('pluginRegistry', () => {
|
|
|
103
138
|
pluginRegistry.registerPlugin(pluginB)
|
|
104
139
|
expect(pluginRegistry.registeredPlugins).toEqual([pluginA, pluginB])
|
|
105
140
|
})
|
|
141
|
+
|
|
142
|
+
it('clears all registered plugins', () => {
|
|
143
|
+
const pluginA = { id: 'A', config: {}, manifest: {} }
|
|
144
|
+
const pluginB = { id: 'B', config: {}, manifest: {} }
|
|
145
|
+
|
|
146
|
+
pluginRegistry.registerPlugin(pluginA)
|
|
147
|
+
pluginRegistry.registerPlugin(pluginB)
|
|
148
|
+
expect(pluginRegistry.registeredPlugins.length).toBe(2)
|
|
149
|
+
|
|
150
|
+
pluginRegistry.clear()
|
|
151
|
+
expect(pluginRegistry.registeredPlugins.length).toBe(0)
|
|
152
|
+
expect(pluginRegistry.registeredPlugins).toEqual([])
|
|
153
|
+
})
|
|
106
154
|
})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// src/App/renderer/HtmlElementHost.jsx
|
|
2
|
+
import React, { useRef, useEffect, useMemo } from 'react'
|
|
3
|
+
import { useApp } from '../store/appContext.js'
|
|
4
|
+
import { Panel } from '../components/Panel/Panel.jsx'
|
|
5
|
+
import { resolveTargetSlot, isModeAllowed, isControlVisible, isConsumerHtml } from './slotHelpers.js'
|
|
6
|
+
import { allowedSlots } from './slots.js'
|
|
7
|
+
import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Maps slot names to their corresponding layout refs.
|
|
11
|
+
*/
|
|
12
|
+
export const getSlotRef = (slot, layoutRefs) => {
|
|
13
|
+
const slotRefMap = {
|
|
14
|
+
side: layoutRefs.sideRef,
|
|
15
|
+
banner: layoutRefs.bannerRef,
|
|
16
|
+
'top-left': layoutRefs.topLeftColRef,
|
|
17
|
+
'top-right': layoutRefs.topRightColRef,
|
|
18
|
+
inset: layoutRefs.insetRef,
|
|
19
|
+
middle: layoutRefs.middleRef,
|
|
20
|
+
bottom: layoutRefs.bottomRef,
|
|
21
|
+
actions: layoutRefs.actionsRef,
|
|
22
|
+
modal: layoutRefs.modalRef
|
|
23
|
+
}
|
|
24
|
+
return slotRefMap[slot] || null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Manages DOM projection for a single persistent element.
|
|
29
|
+
* Moves the wrapper into the target slot when visible, hides it otherwise.
|
|
30
|
+
* Depends on breakpoint to handle conditionally rendered slot containers
|
|
31
|
+
* (e.g. the banner slot swaps DOM nodes between mobile and desktop).
|
|
32
|
+
*/
|
|
33
|
+
export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint) => {
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const wrapper = wrapperRef.current
|
|
36
|
+
|
|
37
|
+
if (isVisible) {
|
|
38
|
+
const slotRef = getSlotRef(targetSlot, layoutRefs)
|
|
39
|
+
if (slotRef?.current) {
|
|
40
|
+
slotRef.current.appendChild(wrapper)
|
|
41
|
+
wrapper.style.display = ''
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
wrapper.style.display = 'none'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
wrapper.style.display = 'none'
|
|
49
|
+
}
|
|
50
|
+
}, [isVisible, targetSlot, layoutRefs, breakpoint, wrapperRef])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Persistent wrapper for a consumer HTML panel.
|
|
55
|
+
* The Panel component stays mounted for the lifetime of the registration.
|
|
56
|
+
* DOM projection moves it between slots; CSS hides it when closed.
|
|
57
|
+
*/
|
|
58
|
+
const PersistentPanel = ({ panelId, config, isOpen, openPanelProps, allowedModalPanelId, appState }) => {
|
|
59
|
+
const panelRootRef = useRef(null)
|
|
60
|
+
const { breakpoint, mode, isFullscreen, layoutRefs } = appState
|
|
61
|
+
|
|
62
|
+
const bpConfig = config[breakpoint]
|
|
63
|
+
const targetSlot = bpConfig ? resolveTargetSlot(bpConfig, breakpoint) : null
|
|
64
|
+
|
|
65
|
+
// Determine visibility using the same logic as mapPanels
|
|
66
|
+
const isVisible = (() => {
|
|
67
|
+
// 1. Initial Guard: Basic requirements
|
|
68
|
+
if (!isOpen || !bpConfig || !targetSlot) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Slot Validation
|
|
73
|
+
const isNextToButton = `${stringToKebab(panelId)}-button` === targetSlot
|
|
74
|
+
const isSlotAllowed = allowedSlots.panel.includes(targetSlot) || isNextToButton
|
|
75
|
+
|
|
76
|
+
if (!isSlotAllowed) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Business Logic: Combine remaining conditions into a single "fail" check
|
|
81
|
+
const isForbiddenModal = bpConfig.modal && panelId !== allowedModalPanelId
|
|
82
|
+
const isForbiddenInline = config.inline === false && !isFullscreen
|
|
83
|
+
const isInvalidMode = !isModeAllowed(config, mode)
|
|
84
|
+
|
|
85
|
+
return !(isForbiddenModal || isForbiddenInline || isInvalidMode)
|
|
86
|
+
})()
|
|
87
|
+
|
|
88
|
+
useDomProjection(panelRootRef, targetSlot, isVisible, layoutRefs, breakpoint)
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Panel
|
|
92
|
+
panelId={panelId}
|
|
93
|
+
panelConfig={config}
|
|
94
|
+
props={openPanelProps}
|
|
95
|
+
html={config.html}
|
|
96
|
+
label={config.label}
|
|
97
|
+
isOpen={isOpen}
|
|
98
|
+
rootRef={panelRootRef}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Persistent wrapper for a consumer HTML control.
|
|
105
|
+
* The control stays mounted for the lifetime of the registration.
|
|
106
|
+
*/
|
|
107
|
+
const PersistentControl = ({ control, appState }) => {
|
|
108
|
+
const wrapperRef = useRef(null)
|
|
109
|
+
const { breakpoint, mode, isFullscreen, layoutRefs } = appState
|
|
110
|
+
|
|
111
|
+
const bpConfig = control[breakpoint]
|
|
112
|
+
const isVisible = isControlVisible(control, { breakpoint, mode, isFullscreen })
|
|
113
|
+
const targetSlot = bpConfig?.slot || null
|
|
114
|
+
|
|
115
|
+
useDomProjection(wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint)
|
|
116
|
+
|
|
117
|
+
const innerHtml = useMemo(() => ({ __html: control.html }), [control.html])
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
ref={wrapperRef}
|
|
122
|
+
className='im-c-control'
|
|
123
|
+
style={{ display: 'none' }}
|
|
124
|
+
dangerouslySetInnerHTML={innerHtml}
|
|
125
|
+
/>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Renders all consumer HTML panels and controls persistently.
|
|
131
|
+
* Items mount once on registration and only unmount on deregistration.
|
|
132
|
+
* Visibility and slot placement are handled by DOM projection, not React mount/unmount.
|
|
133
|
+
*/
|
|
134
|
+
export const HtmlElementHost = () => {
|
|
135
|
+
const appState = useApp()
|
|
136
|
+
const { panelConfig = {}, controlConfig = {}, openPanels = {}, breakpoint } = appState
|
|
137
|
+
|
|
138
|
+
// Find consumer HTML panels
|
|
139
|
+
const htmlPanels = useMemo(() =>
|
|
140
|
+
Object.entries(panelConfig).filter(([_, config]) => isConsumerHtml(config)),
|
|
141
|
+
[panelConfig]
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Find consumer HTML controls
|
|
145
|
+
const htmlControls = useMemo(() =>
|
|
146
|
+
Object.values(controlConfig).filter(control => isConsumerHtml(control)),
|
|
147
|
+
[controlConfig]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
// Determine which modal panel is allowed (topmost open modal)
|
|
151
|
+
const allowedModalPanelId = useMemo(() => {
|
|
152
|
+
const openPanelEntries = Object.entries(openPanels)
|
|
153
|
+
const modalPanels = openPanelEntries.filter(([panelId]) => {
|
|
154
|
+
const cfg = panelConfig[panelId]?.[breakpoint]
|
|
155
|
+
return cfg?.modal
|
|
156
|
+
})
|
|
157
|
+
return modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null
|
|
158
|
+
}, [openPanels, panelConfig, breakpoint])
|
|
159
|
+
|
|
160
|
+
if (!htmlPanels.length && !htmlControls.length) {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
{htmlPanels.map(([panelId, config]) => (
|
|
167
|
+
<PersistentPanel
|
|
168
|
+
key={panelId}
|
|
169
|
+
panelId={panelId}
|
|
170
|
+
config={config}
|
|
171
|
+
isOpen={!!openPanels[panelId]}
|
|
172
|
+
openPanelProps={openPanels[panelId]?.props}
|
|
173
|
+
allowedModalPanelId={allowedModalPanelId}
|
|
174
|
+
appState={appState}
|
|
175
|
+
/>
|
|
176
|
+
))}
|
|
177
|
+
{htmlControls.map(control => (
|
|
178
|
+
<PersistentControl
|
|
179
|
+
key={control.id}
|
|
180
|
+
control={control}
|
|
181
|
+
appState={appState}
|
|
182
|
+
/>
|
|
183
|
+
))}
|
|
184
|
+
</>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render } from '@testing-library/react'
|
|
3
|
+
import { HtmlElementHost, getSlotRef } from './HtmlElementHost.jsx'
|
|
4
|
+
import { useApp } from '../store/appContext'
|
|
5
|
+
|
|
6
|
+
jest.mock('../store/appContext', () => ({ useApp: jest.fn() }))
|
|
7
|
+
jest.mock('../store/configContext', () => ({ useConfig: jest.fn(() => ({ id: 'test' })) }))
|
|
8
|
+
jest.mock('../components/Panel/Panel.jsx', () => ({
|
|
9
|
+
Panel: ({ panelId, html, isOpen, rootRef }) => (
|
|
10
|
+
<div ref={rootRef} data-testid={`panel-${panelId}`} data-open={isOpen}>
|
|
11
|
+
{html}
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}))
|
|
15
|
+
jest.mock('./slots.js', () => ({
|
|
16
|
+
allowedSlots: {
|
|
17
|
+
panel: ['inset', 'side', 'modal', 'bottom'],
|
|
18
|
+
control: ['inset', 'banner', 'bottom', 'actions']
|
|
19
|
+
}
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wrapper that provides real DOM slot containers as refs.
|
|
24
|
+
* This keeps projected nodes inside React's render tree so cleanup works.
|
|
25
|
+
*/
|
|
26
|
+
const SlotHarness = ({ layoutRefs, children }) => (
|
|
27
|
+
<div>
|
|
28
|
+
<div ref={layoutRefs.insetRef} data-slot='inset' />
|
|
29
|
+
<div ref={layoutRefs.sideRef} data-slot='side' />
|
|
30
|
+
<div ref={layoutRefs.modalRef} data-slot='modal' />
|
|
31
|
+
<div ref={layoutRefs.bottomRef} data-slot='bottom' />
|
|
32
|
+
<div ref={layoutRefs.bannerRef} data-slot='banner' />
|
|
33
|
+
<div ref={layoutRefs.actionsRef} data-slot='actions' />
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
describe('HtmlElementHost', () => {
|
|
39
|
+
let layoutRefs
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks()
|
|
43
|
+
layoutRefs = {
|
|
44
|
+
sideRef: { current: null },
|
|
45
|
+
bannerRef: { current: null },
|
|
46
|
+
topLeftColRef: { current: null },
|
|
47
|
+
topRightColRef: { current: null },
|
|
48
|
+
insetRef: { current: null },
|
|
49
|
+
middleRef: { current: null },
|
|
50
|
+
bottomRef: { current: null },
|
|
51
|
+
actionsRef: { current: null },
|
|
52
|
+
modalRef: { current: null },
|
|
53
|
+
viewportRef: { current: null },
|
|
54
|
+
mainRef: { current: null }
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const mockApp = (overrides = {}) => {
|
|
59
|
+
const state = {
|
|
60
|
+
breakpoint: 'desktop',
|
|
61
|
+
mode: 'view',
|
|
62
|
+
isFullscreen: false,
|
|
63
|
+
panelConfig: {},
|
|
64
|
+
controlConfig: {},
|
|
65
|
+
openPanels: {},
|
|
66
|
+
layoutRefs,
|
|
67
|
+
dispatch: jest.fn(),
|
|
68
|
+
...overrides
|
|
69
|
+
}
|
|
70
|
+
useApp.mockReturnValue(state)
|
|
71
|
+
return state
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const renderWithSlots = (appOverrides = {}) => {
|
|
75
|
+
mockApp(appOverrides)
|
|
76
|
+
return render(
|
|
77
|
+
<SlotHarness layoutRefs={layoutRefs}>
|
|
78
|
+
<HtmlElementHost />
|
|
79
|
+
</SlotHarness>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
it('renders nothing when no consumer HTML panels or controls exist', () => {
|
|
84
|
+
mockApp()
|
|
85
|
+
const { container } = render(<HtmlElementHost />)
|
|
86
|
+
expect(container.firstChild).toBeNull()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('ignores plugin panels (with pluginId)', () => {
|
|
90
|
+
mockApp({
|
|
91
|
+
panelConfig: { p1: { pluginId: 'plug1', html: '<p>Hi</p>', desktop: { slot: 'inset' } } },
|
|
92
|
+
openPanels: { p1: { props: {} } }
|
|
93
|
+
})
|
|
94
|
+
const { container } = render(<HtmlElementHost />)
|
|
95
|
+
expect(container.firstChild).toBeNull()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('ignores plugin controls (with pluginId)', () => {
|
|
99
|
+
mockApp({
|
|
100
|
+
controlConfig: { c1: { id: 'c1', pluginId: 'plug1', html: '<p>Hi</p>', desktop: { slot: 'inset' } } }
|
|
101
|
+
})
|
|
102
|
+
const { container } = render(<HtmlElementHost />)
|
|
103
|
+
expect(container.firstChild).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('projects open panel into correct slot', () => {
|
|
107
|
+
const { container } = renderWithSlots({
|
|
108
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'inset' } } },
|
|
109
|
+
openPanels: { p1: { props: {} } }
|
|
110
|
+
})
|
|
111
|
+
expect(container.querySelector('[data-slot="inset"] [data-testid="panel-p1"]')).toBeTruthy()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('hides panel when closed and passes isOpen=false', () => {
|
|
115
|
+
const { getByTestId } = renderWithSlots({
|
|
116
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'inset' } } },
|
|
117
|
+
openPanels: {}
|
|
118
|
+
})
|
|
119
|
+
expect(getByTestId('panel-p1').style.display).toBe('none')
|
|
120
|
+
expect(getByTestId('panel-p1').dataset.open).toBe('false')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('hides panel when mode is not allowed', () => {
|
|
124
|
+
const { getByTestId } = renderWithSlots({
|
|
125
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'inset' }, includeModes: ['edit'] } },
|
|
126
|
+
openPanels: { p1: { props: {} } }
|
|
127
|
+
})
|
|
128
|
+
expect(getByTestId('panel-p1').style.display).toBe('none')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('hides panel with inline:false when not fullscreen', () => {
|
|
132
|
+
const { getByTestId } = renderWithSlots({
|
|
133
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'inset' }, inline: false } },
|
|
134
|
+
openPanels: { p1: { props: {} } },
|
|
135
|
+
isFullscreen: false
|
|
136
|
+
})
|
|
137
|
+
expect(getByTestId('panel-p1').style.display).toBe('none')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('resolves bottom slot to inset on desktop', () => {
|
|
141
|
+
const { container } = renderWithSlots({
|
|
142
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'bottom' } } },
|
|
143
|
+
openPanels: { p1: { props: {} } }
|
|
144
|
+
})
|
|
145
|
+
expect(container.querySelector('[data-slot="inset"] [data-testid="panel-p1"]')).toBeTruthy()
|
|
146
|
+
expect(container.querySelector('[data-slot="bottom"] [data-testid="panel-p1"]')).toBeNull()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('only shows topmost modal panel', () => {
|
|
150
|
+
const { getByTestId, container } = renderWithSlots({
|
|
151
|
+
panelConfig: {
|
|
152
|
+
p1: { html: '<p>1</p>', label: 'M1', desktop: { slot: 'side', modal: true } },
|
|
153
|
+
p2: { html: '<p>2</p>', label: 'M2', desktop: { slot: 'side', modal: true } }
|
|
154
|
+
},
|
|
155
|
+
openPanels: { p1: { props: {} }, p2: { props: {} } }
|
|
156
|
+
})
|
|
157
|
+
expect(getByTestId('panel-p1').style.display).toBe('none')
|
|
158
|
+
expect(container.querySelector('[data-slot="modal"] [data-testid="panel-p2"]')).toBeTruthy()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('hides panel when breakpoint config is missing', () => {
|
|
162
|
+
const { getByTestId } = renderWithSlots({
|
|
163
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', mobile: { slot: 'inset' } } },
|
|
164
|
+
openPanels: { p1: { props: {} } },
|
|
165
|
+
breakpoint: 'desktop'
|
|
166
|
+
})
|
|
167
|
+
expect(getByTestId('panel-p1').style.display).toBe('none')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('projects visible control into correct slot', () => {
|
|
171
|
+
const { container } = renderWithSlots({
|
|
172
|
+
controlConfig: { c1: { id: 'c1', html: '<input type="checkbox">', desktop: { slot: 'inset' } } }
|
|
173
|
+
})
|
|
174
|
+
const control = container.querySelector('[data-slot="inset"] .im-c-control')
|
|
175
|
+
expect(control).toBeTruthy()
|
|
176
|
+
expect(control.innerHTML).toBe('<input type="checkbox">')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('hides panel when slot is not allowed', () => {
|
|
180
|
+
const { getByTestId } = renderWithSlots({
|
|
181
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'invalid' } } },
|
|
182
|
+
openPanels: { p1: { props: {} } }
|
|
183
|
+
})
|
|
184
|
+
expect(getByTestId('panel-p1').style.display).toBe('none')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('hides control when breakpoint config is missing', () => {
|
|
188
|
+
const { container } = renderWithSlots({
|
|
189
|
+
controlConfig: { c1: { id: 'c1', html: '<p>Hi</p>', mobile: { slot: 'inset' } } },
|
|
190
|
+
breakpoint: 'desktop'
|
|
191
|
+
})
|
|
192
|
+
const control = container.querySelector('.im-c-control')
|
|
193
|
+
expect(control.style.display).toBe('none')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('uses default empty objects when appState is missing configs', () => {
|
|
197
|
+
useApp.mockReturnValue({}) // no panelConfig/controlConfig/openPanels/breakpoint
|
|
198
|
+
const { container } = render(<HtmlElementHost />)
|
|
199
|
+
expect(container.firstChild).toBeNull() // still renders safely
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('getSlotRef returns null for unknown slot', () => {
|
|
203
|
+
expect(getSlotRef('unknown-slot', {})).toBeNull()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('does not append child if slotRef exists but current is null', () => {
|
|
207
|
+
// 1. Setup refs where the slot exists in the map but the DOM node (current) is null
|
|
208
|
+
const incompleteRefs = {
|
|
209
|
+
...layoutRefs,
|
|
210
|
+
sideRef: { current: null }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 2. Mock the app so the panel IS visible, but the slot it wants is the broken one
|
|
214
|
+
mockApp({
|
|
215
|
+
panelConfig: { p1: { html: '<b>Hi</b>', desktop: { slot: 'side' } } },
|
|
216
|
+
openPanels: { p1: { props: {} } },
|
|
217
|
+
layoutRefs: incompleteRefs
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const { getByTestId, container } = render(<HtmlElementHost />)
|
|
221
|
+
|
|
222
|
+
const panel = getByTestId('panel-p1')
|
|
223
|
+
|
|
224
|
+
// 3. Verify that the panel was NOT moved into a slot container
|
|
225
|
+
// Since sideRef.current is null, it should still be sitting in the root of the Host
|
|
226
|
+
expect(container.querySelector('[data-slot="side"] [data-testid="panel-p1"]')).toBeNull()
|
|
227
|
+
|
|
228
|
+
// The panel exists in the DOM but hasn't been "projected"
|
|
229
|
+
expect(panel).toBeTruthy()
|
|
230
|
+
})
|
|
231
|
+
})
|
|
@@ -36,18 +36,19 @@ function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
|
|
|
36
36
|
|
|
37
37
|
function createButtonClickHandler (btn, appState, evaluateProp) {
|
|
38
38
|
const [, config] = btn
|
|
39
|
-
const
|
|
39
|
+
const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
|
|
40
40
|
|
|
41
41
|
return (e) => {
|
|
42
42
|
if (typeof config.onClick === 'function') {
|
|
43
|
-
|
|
43
|
+
config.onClick(e, evaluateProp(ctx => ctx, config.pluginId))
|
|
44
|
+
return
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
if (config.panelId) {
|
|
47
48
|
const triggeringElement = e.currentTarget
|
|
48
49
|
appState.dispatch({
|
|
49
|
-
type:
|
|
50
|
-
payload:
|
|
50
|
+
type: isPanelOpen ? 'CLOSE_PANEL' : 'OPEN_PANEL',
|
|
51
|
+
payload: isPanelOpen
|
|
51
52
|
? config.panelId
|
|
52
53
|
: { panelId: config.panelId, props: { triggeringElement } }
|
|
53
54
|
})
|
|
@@ -59,7 +60,7 @@ function renderButton ({ btn, appState, appConfig, evaluateProp, groupStart, gro
|
|
|
59
60
|
const [buttonId, config] = btn
|
|
60
61
|
const bpConfig = config[appState.breakpoint] ?? {}
|
|
61
62
|
const handleClick = createButtonClickHandler(btn, appState, evaluateProp)
|
|
62
|
-
const
|
|
63
|
+
const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
|
|
63
64
|
|
|
64
65
|
return (
|
|
65
66
|
<MapButton
|
|
@@ -74,9 +75,11 @@ function renderButton ({ btn, appState, appConfig, evaluateProp, groupStart, gro
|
|
|
74
75
|
isDisabled={appState.disabledButtons.has(buttonId)}
|
|
75
76
|
isHidden={appState.hiddenButtons.has(buttonId)}
|
|
76
77
|
isPressed={(config.isPressed !== undefined || config.pressedWhen) ? appState.pressedButtons.has(buttonId) : undefined}
|
|
77
|
-
|
|
78
|
+
isExpanded={(config.isExpanded !== undefined || config.expandedWhen) ? appState.expandedButtons.has(buttonId) : undefined}
|
|
79
|
+
isPanelOpen={isPanelOpen}
|
|
78
80
|
onClick={handleClick}
|
|
79
81
|
panelId={config.panelId}
|
|
82
|
+
menuItems={config.menuItems}
|
|
80
83
|
idPrefix={appConfig.id}
|
|
81
84
|
groupStart={groupStart}
|
|
82
85
|
groupMiddle={groupMiddle}
|
|
@@ -115,10 +118,10 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
|
|
|
115
118
|
return matching.map((btn, idx) => {
|
|
116
119
|
const [buttonId, config] = btn
|
|
117
120
|
const key = config.group
|
|
118
|
-
const indices = key
|
|
121
|
+
const indices = key == null ? null : groupMap.get(key)
|
|
119
122
|
const groupStart = indices ? idx === indices[0] : false
|
|
120
123
|
const groupEnd = indices ? idx === indices[indices.length - 1] : false
|
|
121
|
-
const groupMiddle = indices && indices.length >= 3 && !groupStart && !groupEnd
|
|
124
|
+
const groupMiddle = indices && indices.length >= 3 && !groupStart && !groupEnd // NOSONAR: 3 = minimum for a start/middle/end group
|
|
122
125
|
const order = config[breakpoint]?.order ?? 0
|
|
123
126
|
|
|
124
127
|
return {
|