@defra/interactive-map 0.0.4-alpha → 0.0.5-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 +95 -0
- package/plugins/interact/src/utils/spatial.test.js +93 -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 +3 -0
- package/src/App/store/AppProvider.test.jsx +40 -0
- package/src/App/store/PluginProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +25 -1
- package/src/App/store/appActionsMap.test.js +21 -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/_dimensions.scss +6 -2
- 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 +4 -4
- 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
|
@@ -36,6 +36,7 @@ describe('mapButtons module', () => {
|
|
|
36
36
|
disabledButtons: new Set(),
|
|
37
37
|
hiddenButtons: new Set(),
|
|
38
38
|
pressedButtons: new Set(),
|
|
39
|
+
expandedButtons: new Set(),
|
|
39
40
|
buttonConfig: {},
|
|
40
41
|
panelConfig: {}
|
|
41
42
|
}
|
|
@@ -137,15 +138,16 @@ describe('mapButtons module', () => {
|
|
|
137
138
|
})
|
|
138
139
|
})
|
|
139
140
|
|
|
140
|
-
it('renders correct state flags for disabled, hidden, and
|
|
141
|
+
it('renders correct state flags for disabled, hidden, pressed and expanded buttons', () => {
|
|
141
142
|
const state = {
|
|
142
143
|
...appState,
|
|
143
144
|
disabledButtons: new Set(['id']),
|
|
144
145
|
hiddenButtons: new Set(['id']),
|
|
145
|
-
pressedButtons: new Set(['id'])
|
|
146
|
+
pressedButtons: new Set(['id']),
|
|
147
|
+
expandedButtons: new Set(['id'])
|
|
146
148
|
}
|
|
147
|
-
const result = render({ ...baseBtn, pressedWhen: jest.fn() }, state)
|
|
148
|
-
expect(result.props).toMatchObject({ isDisabled: true, isHidden: true, isPressed: true })
|
|
149
|
+
const result = render({ ...baseBtn, pressedWhen: jest.fn(), expandedWhen: jest.fn() }, state)
|
|
150
|
+
expect(result.props).toMatchObject({ isDisabled: true, isHidden: true, isPressed: true, isExpanded: true })
|
|
149
151
|
})
|
|
150
152
|
|
|
151
153
|
it('uses empty object fallback for missing breakpoint config', () => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { withPluginContexts } from './pluginWrapper.js'
|
|
4
4
|
import { allowedSlots } from './slots.js'
|
|
5
|
+
import { isConsumerHtml } from './slotHelpers.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Map controls for a given slot and app state.
|
|
@@ -12,6 +13,11 @@ export function mapControls ({ slot, appState, evaluateProp }) {
|
|
|
12
13
|
|
|
13
14
|
return Object.values(controlConfig)
|
|
14
15
|
.filter(control => {
|
|
16
|
+
// Consumer HTML controls are managed by HtmlElementHost
|
|
17
|
+
if (isConsumerHtml(control)) {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
const bpConfig = control[breakpoint]
|
|
16
22
|
if (!bpConfig) {
|
|
17
23
|
return false
|
|
@@ -90,14 +90,22 @@ describe('mapControls', () => {
|
|
|
90
90
|
expect(result[0].order).toBe(0)
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
it('renders HTML controls with dangerouslySetInnerHTML', () => {
|
|
93
|
+
it('renders plugin HTML controls with dangerouslySetInnerHTML', () => {
|
|
94
94
|
defaultAppState.controlConfig = ({
|
|
95
|
-
ctrlHtml: { id: 'ctrlHtml', desktop: { slot: 'header' }, html: '<p>Hi</p>', includeModes: ['view'] }
|
|
95
|
+
ctrlHtml: { id: 'ctrlHtml', pluginId: 'plugin1', desktop: { slot: 'header' }, html: '<p>Hi</p>', includeModes: ['view'] }
|
|
96
96
|
})
|
|
97
97
|
const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
|
|
98
98
|
expect(result[0].element.props.dangerouslySetInnerHTML).toEqual({ __html: '<p>Hi</p>' })
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
+
it('filters out consumer HTML controls (handled by HtmlElementHost)', () => {
|
|
102
|
+
defaultAppState.controlConfig = ({
|
|
103
|
+
ctrlHtml: { id: 'ctrlHtml', desktop: { slot: 'header' }, html: '<p>Hi</p>', includeModes: ['view'] }
|
|
104
|
+
})
|
|
105
|
+
const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
|
|
106
|
+
expect(result).toEqual([])
|
|
107
|
+
})
|
|
108
|
+
|
|
101
109
|
it('handles plugin-less controls gracefully', () => {
|
|
102
110
|
defaultAppState.controlConfig = ({
|
|
103
111
|
ctrl2: { id: 'ctrl2', desktop: { slot: 'header' }, render: () => <div />, includeModes: ['view'] }
|
|
@@ -4,7 +4,38 @@ import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
|
4
4
|
import { withPluginContexts } from './pluginWrapper.js'
|
|
5
5
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
6
6
|
import { allowedSlots } from './slots.js'
|
|
7
|
+
import { resolveTargetSlot, isModeAllowed, isConsumerHtml } from './slotHelpers.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Determines whether a panel should be rendered in the given slot.
|
|
11
|
+
* Checks slot eligibility, mode restrictions, inline/fullscreen constraints,
|
|
12
|
+
* and ensures only the topmost modal panel is shown.
|
|
13
|
+
*/
|
|
14
|
+
const isPanelVisible = (panelId, config, bpConfig, { targetSlot, slot, mode, isFullscreen, allowedModalPanelId }) => {
|
|
15
|
+
const isNextToButton = `${stringToKebab(panelId)}-button` === targetSlot
|
|
16
|
+
if (!allowedSlots.panel.includes(targetSlot) && !isNextToButton) {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
if (!isModeAllowed(config, mode)) {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
if (config.inline === false && !isFullscreen) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
if (targetSlot !== slot) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
if (bpConfig.modal && panelId !== allowedModalPanelId) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
7
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Maps open panels to renderable entries for a given layout slot.
|
|
36
|
+
* Filters panels by slot, breakpoint, mode, and modal state, then wraps
|
|
37
|
+
* each panel's render function with the appropriate plugin contexts.
|
|
38
|
+
*/
|
|
8
39
|
export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
9
40
|
const { breakpoint, pluginRegistry, panelConfig, mode, openPanels } = appState
|
|
10
41
|
|
|
@@ -18,43 +49,25 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
18
49
|
|
|
19
50
|
return openPanelEntries.map(([panelId, { props }]) => {
|
|
20
51
|
const config = panelConfig[panelId]
|
|
21
|
-
|
|
22
52
|
if (!config) {
|
|
23
53
|
return null
|
|
24
54
|
}
|
|
25
55
|
|
|
26
|
-
|
|
27
|
-
if (
|
|
56
|
+
// Consumer HTML panels are managed by HtmlElementHost
|
|
57
|
+
if (isConsumerHtml(config)) {
|
|
28
58
|
return null
|
|
29
59
|
}
|
|
30
60
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Slot constraint: bottom slot only permitted for mobile, revert to inset
|
|
35
|
-
if (targetSlot === 'bottom' && ['tablet', 'desktop'].includes(breakpoint)) {
|
|
36
|
-
targetSlot = 'inset'
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const isNextToButton = `${stringToKebab(panelId)}-button` === targetSlot
|
|
40
|
-
const slotAllowed = allowedSlots.panel.includes(targetSlot) || isNextToButton
|
|
41
|
-
const inModeWhitelist = config.includeModes?.includes(mode) ?? true
|
|
42
|
-
const inExcludeModes = config.excludeModes?.includes(mode) ?? false
|
|
43
|
-
|
|
44
|
-
if (!slotAllowed || !inModeWhitelist || inExcludeModes) {
|
|
45
|
-
return null
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Skip panels marked as inline:false when not in fullscreen mode
|
|
49
|
-
if (config.inline === false && !appState.isFullscreen) {
|
|
61
|
+
const bpConfig = config[breakpoint]
|
|
62
|
+
if (!bpConfig) {
|
|
50
63
|
return null
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
66
|
+
const targetSlot = resolveTargetSlot(bpConfig, breakpoint)
|
|
56
67
|
|
|
57
|
-
if (
|
|
68
|
+
if (!isPanelVisible(panelId, config, bpConfig, {
|
|
69
|
+
targetSlot, slot, mode, isFullscreen: appState.isFullscreen, allowedModalPanelId
|
|
70
|
+
})) {
|
|
58
71
|
return null
|
|
59
72
|
}
|
|
60
73
|
|
|
@@ -65,6 +65,28 @@ describe('mapPanels', () => {
|
|
|
65
65
|
expect(map()).toEqual([])
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
+
it('skips panel if mode is not allowed (isModeAllowed returns false)', () => {
|
|
69
|
+
// 1. Define config that only allows 'edit' mode
|
|
70
|
+
const panelConfig = {
|
|
71
|
+
p1: {
|
|
72
|
+
desktop: { slot: 'header' },
|
|
73
|
+
includeModes: ['edit']
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Mock appState with 'view' mode
|
|
78
|
+
const state = {
|
|
79
|
+
...defaultAppState,
|
|
80
|
+
mode: 'view',
|
|
81
|
+
panelConfig,
|
|
82
|
+
openPanels: { p1: { props: {} } }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Verify it's filtered out even though it's the right slot
|
|
86
|
+
const result = map(state, 'header')
|
|
87
|
+
expect(result).toEqual([])
|
|
88
|
+
})
|
|
89
|
+
|
|
68
90
|
it('only allows last opened modal panel', () => {
|
|
69
91
|
defaultAppState.panelConfig = ({
|
|
70
92
|
p1: { desktop: { modal: true }, includeModes: ['view'] },
|
|
@@ -159,4 +181,11 @@ describe('mapPanels', () => {
|
|
|
159
181
|
})
|
|
160
182
|
expect(map().map(p => p.id)).toEqual(['p1'])
|
|
161
183
|
})
|
|
184
|
+
|
|
185
|
+
it('filters out consumer HTML panels (handled by HtmlElementHost)', () => {
|
|
186
|
+
defaultAppState.panelConfig = ({
|
|
187
|
+
p1: { desktop: { slot: 'header' }, html: '<p>Hi</p>', includeModes: ['view'] }
|
|
188
|
+
})
|
|
189
|
+
expect(map()).toEqual([])
|
|
190
|
+
})
|
|
162
191
|
})
|
|
@@ -44,4 +44,32 @@ describe('withPluginContexts', () => {
|
|
|
44
44
|
|
|
45
45
|
expect(Wrapped2).toBe(Wrapped1)
|
|
46
46
|
})
|
|
47
|
+
|
|
48
|
+
it('filters buttonConfig by pluginId', () => {
|
|
49
|
+
const Inner = jest.fn(() => <div>Inner</div>)
|
|
50
|
+
|
|
51
|
+
// Provide a buttonConfig with buttons for multiple plugins
|
|
52
|
+
const appStateMock = {
|
|
53
|
+
user: 'testUser',
|
|
54
|
+
buttonConfig: {
|
|
55
|
+
btn1: { label: 'A', pluginId: 'plugin1' },
|
|
56
|
+
btn2: { label: 'B', pluginId: 'plugin2' }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Override the useApp hook for this test
|
|
61
|
+
const { useApp } = require('../store/appContext.js')
|
|
62
|
+
useApp.mockImplementation(() => appStateMock)
|
|
63
|
+
|
|
64
|
+
const Wrapped = withPluginContexts(Inner, { pluginId: 'plugin1', pluginConfig: { foo: 'bar' } })
|
|
65
|
+
|
|
66
|
+
render(<Wrapped customProp='xyz' />)
|
|
67
|
+
|
|
68
|
+
const props = Inner.mock.calls[0][0]
|
|
69
|
+
|
|
70
|
+
// buttonConfig should include only buttons belonging to plugin1
|
|
71
|
+
expect(props.buttonConfig).toEqual({
|
|
72
|
+
btn1: { label: 'A', pluginId: 'plugin1' }
|
|
73
|
+
})
|
|
74
|
+
})
|
|
47
75
|
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/App/renderer/slotHelpers.js
|
|
2
|
+
import { allowedSlots } from './slots.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the target slot for a panel based on its breakpoint config.
|
|
6
|
+
* Modal panels always render in the 'modal' slot, and the bottom slot
|
|
7
|
+
* is only available on mobile — tablet and desktop fall back to 'inset'.
|
|
8
|
+
*/
|
|
9
|
+
export const resolveTargetSlot = (bpConfig, breakpoint) => {
|
|
10
|
+
if (bpConfig.modal) {
|
|
11
|
+
return 'modal'
|
|
12
|
+
}
|
|
13
|
+
if (bpConfig.slot === 'bottom' && ['tablet', 'desktop'].includes(breakpoint)) {
|
|
14
|
+
return 'inset'
|
|
15
|
+
}
|
|
16
|
+
return bpConfig.slot
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks whether the current application mode permits an item to be shown,
|
|
21
|
+
* based on its includeModes and excludModes configuration.
|
|
22
|
+
*/
|
|
23
|
+
export const isModeAllowed = (config, mode) => {
|
|
24
|
+
if (config.includeModes && !config.includeModes.includes(mode)) {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
if (config.excludeModes?.includes(mode)) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks whether a control should be visible based on breakpoint,
|
|
35
|
+
* mode, fullscreen, and slot constraints.
|
|
36
|
+
*/
|
|
37
|
+
export const isControlVisible = (control, { breakpoint, mode, isFullscreen }) => {
|
|
38
|
+
const bpConfig = control[breakpoint]
|
|
39
|
+
if (!bpConfig) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
if (!allowedSlots.control.includes(bpConfig.slot)) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
if (!isModeAllowed(control, mode)) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
if (control.inline === false && !isFullscreen) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true if a panel/control was added via the consumer API with static HTML
|
|
56
|
+
* (i.e. not a plugin component).
|
|
57
|
+
*/
|
|
58
|
+
export const isConsumerHtml = (config) => {
|
|
59
|
+
return typeof config.html === 'string' && !config.pluginId
|
|
60
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { resolveTargetSlot, isModeAllowed, isControlVisible, isConsumerHtml } from './slotHelpers.js'
|
|
2
|
+
|
|
3
|
+
jest.mock('./slots.js', () => ({ allowedSlots: { control: ['inset', 'banner', 'actions'] } }))
|
|
4
|
+
|
|
5
|
+
describe('resolveTargetSlot', () => {
|
|
6
|
+
it('returns modal for modal panels', () => {
|
|
7
|
+
expect(resolveTargetSlot({ modal: true, slot: 'side' }, 'desktop')).toBe('modal')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('replaces bottom with inset on tablet and desktop', () => {
|
|
11
|
+
expect(resolveTargetSlot({ slot: 'bottom' }, 'tablet')).toBe('inset')
|
|
12
|
+
expect(resolveTargetSlot({ slot: 'bottom' }, 'desktop')).toBe('inset')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('keeps bottom on mobile', () => {
|
|
16
|
+
expect(resolveTargetSlot({ slot: 'bottom' }, 'mobile')).toBe('bottom')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('returns slot as-is otherwise', () => {
|
|
20
|
+
expect(resolveTargetSlot({ slot: 'side' }, 'desktop')).toBe('side')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('isModeAllowed', () => {
|
|
25
|
+
it('returns true when no mode restrictions', () => {
|
|
26
|
+
expect(isModeAllowed({}, 'view')).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('rejects when mode not in includeModes', () => {
|
|
30
|
+
expect(isModeAllowed({ includeModes: ['edit'] }, 'view')).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('rejects when mode in excludeModes', () => {
|
|
34
|
+
expect(isModeAllowed({ excludeModes: ['view'] }, 'view')).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('allows when mode matches includeModes', () => {
|
|
38
|
+
expect(isModeAllowed({ includeModes: ['view'] }, 'view')).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('isControlVisible', () => {
|
|
43
|
+
const base = { desktop: { slot: 'inset' } }
|
|
44
|
+
|
|
45
|
+
it('returns true for valid control', () => {
|
|
46
|
+
expect(isControlVisible(base, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns false when breakpoint config missing', () => {
|
|
50
|
+
expect(isControlVisible(base, { breakpoint: 'mobile', mode: 'view', isFullscreen: false })).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('returns false when slot not allowed', () => {
|
|
54
|
+
expect(isControlVisible({ desktop: { slot: 'invalid' } }, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns false when mode not allowed', () => {
|
|
58
|
+
expect(isControlVisible({ ...base, includeModes: ['edit'] }, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns false when inline:false and not fullscreen', () => {
|
|
62
|
+
expect(isControlVisible({ ...base, inline: false }, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('returns true when inline:false and fullscreen', () => {
|
|
66
|
+
expect(isControlVisible({ ...base, inline: false }, { breakpoint: 'desktop', mode: 'view', isFullscreen: true })).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('isConsumerHtml', () => {
|
|
71
|
+
it('returns true for consumer HTML config', () => {
|
|
72
|
+
expect(isConsumerHtml({ html: '<p>Hi</p>' })).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns false when pluginId present', () => {
|
|
76
|
+
expect(isConsumerHtml({ html: '<p>Hi</p>', pluginId: 'p1' })).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns false when no html', () => {
|
|
80
|
+
expect(isConsumerHtml({ render: () => {} })).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -21,9 +21,12 @@ export const AppProvider = ({ options, children }) => {
|
|
|
21
21
|
topRightColRef: useRef(null),
|
|
22
22
|
insetRef: useRef(null),
|
|
23
23
|
rightRef: useRef(null),
|
|
24
|
+
middleRef: useRef(null),
|
|
25
|
+
bottomRef: useRef(null),
|
|
24
26
|
footerRef: useRef(null),
|
|
25
27
|
actionsRef: useRef(null),
|
|
26
28
|
bannerRef: useRef(null),
|
|
29
|
+
modalRef: useRef(null),
|
|
27
30
|
viewportRef: useRef(null)
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -4,6 +4,7 @@ import { AppProvider, AppContext } from './AppProvider.jsx'
|
|
|
4
4
|
import { createMockRegistries } from '../../test-utils.js'
|
|
5
5
|
import * as mediaHook from '../hooks/useMediaQueryDispatch.js'
|
|
6
6
|
import * as detectInterface from '../../utils/detectInterfaceType.js'
|
|
7
|
+
import * as appReducerModule from './appReducer.js'
|
|
7
8
|
|
|
8
9
|
jest.mock('../hooks/useMediaQueryDispatch.js')
|
|
9
10
|
jest.mock('../../utils/detectInterfaceType.js')
|
|
@@ -111,4 +112,43 @@ describe('AppProvider', () => {
|
|
|
111
112
|
expect(contextValue.layoutRefs).toHaveProperty('mainRef')
|
|
112
113
|
expect(contextValue.layoutRefs).toHaveProperty('footerRef')
|
|
113
114
|
})
|
|
115
|
+
|
|
116
|
+
test('dispatch fallback uses options.panelRegistry.getPanelConfig() when state.panelConfig missing', () => {
|
|
117
|
+
const getPanelConfigMock = jest.fn(() => ({ panel1: {} }))
|
|
118
|
+
|
|
119
|
+
// Mock initialState to return state without panelConfig but with panelRegistry
|
|
120
|
+
jest.spyOn(appReducerModule, 'initialState').mockImplementation(() => ({
|
|
121
|
+
mode: 'view',
|
|
122
|
+
previousMode: 'edit',
|
|
123
|
+
openPanels: {},
|
|
124
|
+
previousOpenPanels: {},
|
|
125
|
+
interfaceType: 'default',
|
|
126
|
+
isFullscreen: false,
|
|
127
|
+
hasExclusiveControl: false,
|
|
128
|
+
panelRegistry: { getPanelConfig: getPanelConfigMock } // <-- provide it here!
|
|
129
|
+
}))
|
|
130
|
+
|
|
131
|
+
const mockEventBus = { on: jest.fn(), off: jest.fn() }
|
|
132
|
+
const mockBreakpointDetector = { subscribe: jest.fn(() => jest.fn()) }
|
|
133
|
+
const mockOptions = {
|
|
134
|
+
...createMockRegistries({ panelConfig: undefined }),
|
|
135
|
+
eventBus: mockEventBus,
|
|
136
|
+
breakpointDetector: mockBreakpointDetector
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
render(
|
|
140
|
+
<AppProvider options={mockOptions}>
|
|
141
|
+
<div>Child</div>
|
|
142
|
+
</AppProvider>
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Trigger a dispatch via eventBus to hit the dispatch wrapper
|
|
146
|
+
act(() => {
|
|
147
|
+
mockEventBus.on.mock.calls.forEach(([event, handler]) => {
|
|
148
|
+
if (event === 'app:setmode') handler('newMode')
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(getPanelConfigMock).toHaveBeenCalled()
|
|
153
|
+
})
|
|
114
154
|
})
|
|
@@ -27,19 +27,21 @@ export const PluginProvider = ({ children }) => {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// Initialize ref registry for each plugin
|
|
30
|
-
if (!refs.current[plugin.id])
|
|
30
|
+
if (!refs.current[plugin.id]) {
|
|
31
|
+
refs.current[plugin.id] = {}
|
|
32
|
+
}
|
|
31
33
|
})
|
|
32
34
|
|
|
33
35
|
// Combined reducer
|
|
34
|
-
const combinedReducer = (
|
|
36
|
+
const combinedReducer = (pluginState, action) => {
|
|
35
37
|
const { pluginId } = action
|
|
36
38
|
if (pluginId && pluginReducers[pluginId]) {
|
|
37
39
|
return {
|
|
38
|
-
...
|
|
39
|
-
[pluginId]: pluginReducers[pluginId](
|
|
40
|
+
...pluginState,
|
|
41
|
+
[pluginId]: pluginReducers[pluginId](pluginState[pluginId], action)
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
|
-
return
|
|
44
|
+
return pluginState
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
const [state, dispatch] = useReducer(combinedReducer, initialState)
|
|
@@ -200,6 +200,22 @@ const toggleButtonPressed = (state, payload) => {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
const toggleButtonExpanded = (state, payload) => {
|
|
204
|
+
const { id, isExpanded } = payload
|
|
205
|
+
const updated = new Set(state.expandedButtons)
|
|
206
|
+
|
|
207
|
+
if (isExpanded) {
|
|
208
|
+
updated.add(id)
|
|
209
|
+
} else {
|
|
210
|
+
updated.delete(id)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
...state,
|
|
215
|
+
expandedButtons: updated
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
203
219
|
// Registry mutation actions
|
|
204
220
|
const registerButton = (state, payload) => {
|
|
205
221
|
return {
|
|
@@ -230,6 +246,12 @@ const addButton = (state, payload) => {
|
|
|
230
246
|
pressedButtons.add(id)
|
|
231
247
|
}
|
|
232
248
|
|
|
249
|
+
// Set expanded state
|
|
250
|
+
const expandedButtons = new Set(state.expandedButtons)
|
|
251
|
+
if (config.isExpanded) {
|
|
252
|
+
expandedButtons.add(id)
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
// Also update the registry instance for persistence across app lifecycle
|
|
234
256
|
if (state.buttonRegistry?.addButton) {
|
|
235
257
|
state.buttonRegistry.addButton(id, config)
|
|
@@ -240,7 +262,8 @@ const addButton = (state, payload) => {
|
|
|
240
262
|
buttonConfig: newButtonConfig,
|
|
241
263
|
hiddenButtons,
|
|
242
264
|
disabledButtons,
|
|
243
|
-
pressedButtons
|
|
265
|
+
pressedButtons,
|
|
266
|
+
expandedButtons
|
|
244
267
|
}
|
|
245
268
|
}
|
|
246
269
|
|
|
@@ -334,6 +357,7 @@ export const actionsMap = {
|
|
|
334
357
|
TOGGLE_BUTTON_DISABLED: toggleButtonDisabled,
|
|
335
358
|
TOGGLE_BUTTON_HIDDEN: toggleButtonHidden,
|
|
336
359
|
TOGGLE_BUTTON_PRESSED: toggleButtonPressed,
|
|
360
|
+
TOGGLE_BUTTON_EXPANDED: toggleButtonExpanded,
|
|
337
361
|
// Registry actions
|
|
338
362
|
REGISTER_BUTTON: registerButton,
|
|
339
363
|
ADD_BUTTON: addButton,
|
|
@@ -30,6 +30,7 @@ describe('actionsMap full coverage', () => {
|
|
|
30
30
|
disabledButtons: new Set(['btn1']),
|
|
31
31
|
hiddenButtons: new Set(['btn3']),
|
|
32
32
|
pressedButtons: new Set(['btn5']),
|
|
33
|
+
expandedButtons: new Set(['btn7']),
|
|
33
34
|
panelConfig: mockPanelConfig,
|
|
34
35
|
panelRegistry: { addPanel: jest.fn(), removePanel: jest.fn(), getPanelConfig: jest.fn(() => mockPanelConfig) },
|
|
35
36
|
buttonConfig: {},
|
|
@@ -152,6 +153,13 @@ describe('actionsMap full coverage', () => {
|
|
|
152
153
|
expect(r2.pressedButtons.has('btn5')).toBe(false)
|
|
153
154
|
})
|
|
154
155
|
|
|
156
|
+
test('TOGGLE_BUTTON_EXPANDED adds/removes button', () => {
|
|
157
|
+
const r1 = actionsMap.TOGGLE_BUTTON_EXPANDED(state, { id: 'btn8', isExpanded: true })
|
|
158
|
+
expect(r1.expandedButtons.has('btn8')).toBe(true)
|
|
159
|
+
const r2 = actionsMap.TOGGLE_BUTTON_EXPANDED(state, { id: 'btn7', isExpanded: false })
|
|
160
|
+
expect(r2.expandedButtons.has('btn7')).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
|
|
155
163
|
test('REGISTER_PANEL updates panelConfig', () => {
|
|
156
164
|
const payload = { id: 'panelX', config: { desktop: { slot: 'side' } } }
|
|
157
165
|
const result = actionsMap.REGISTER_PANEL(state, payload)
|
|
@@ -204,6 +212,19 @@ describe('actionsMap full coverage', () => {
|
|
|
204
212
|
expect(state.buttonRegistry.addButton).toHaveBeenCalled()
|
|
205
213
|
})
|
|
206
214
|
|
|
215
|
+
test('ADD_BUTTON sets hidden, disabled, pressed and expanded state when config flags are true', () => {
|
|
216
|
+
const payload = {
|
|
217
|
+
id: 'btnSpecial',
|
|
218
|
+
config: { isHidden: true, isDisabled: true, isPressed: true, isExpanded: true }
|
|
219
|
+
}
|
|
220
|
+
const result = actionsMap.ADD_BUTTON(state, payload)
|
|
221
|
+
expect(result.buttonConfig.btnSpecial).toBeDefined()
|
|
222
|
+
expect(result.hiddenButtons.has('btnSpecial')).toBe(true)
|
|
223
|
+
expect(result.disabledButtons.has('btnSpecial')).toBe(true)
|
|
224
|
+
expect(result.pressedButtons.has('btnSpecial')).toBe(true)
|
|
225
|
+
expect(result.expandedButtons.has('btnSpecial')).toBe(true)
|
|
226
|
+
})
|
|
227
|
+
|
|
207
228
|
// ---------------------- FALLBACK / OPTIONAL BRANCHES ----------------------
|
|
208
229
|
test('SET_MODE uses panelRegistry.getPanelConfig() when panelConfig missing', () => {
|
|
209
230
|
const tmp = { ...state, panelConfig: undefined }
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic reducer action that shallow-merges the payload into state.
|
|
3
|
+
* Shared by MAP_MOVE, MAP_MOVE_END and MAP_FIRST_IDLE.
|
|
4
|
+
*/
|
|
5
|
+
const mergePayload = (state, payload) => ({
|
|
6
|
+
...state,
|
|
7
|
+
...payload
|
|
8
|
+
})
|
|
9
|
+
|
|
1
10
|
const setMapReady = (state) => {
|
|
2
11
|
return {
|
|
3
12
|
...state,
|
|
@@ -5,27 +14,6 @@ const setMapReady = (state) => {
|
|
|
5
14
|
}
|
|
6
15
|
}
|
|
7
16
|
|
|
8
|
-
const mapMove = (state, payload) => {
|
|
9
|
-
return {
|
|
10
|
-
...state,
|
|
11
|
-
...payload
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const mapMoveEnd = (state, payload) => {
|
|
16
|
-
return {
|
|
17
|
-
...state,
|
|
18
|
-
...payload
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const mapFirstIdle = (state, payload) => {
|
|
23
|
-
return {
|
|
24
|
-
...state,
|
|
25
|
-
...payload
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
17
|
const setMapStyle = (state, payload) => {
|
|
30
18
|
return {
|
|
31
19
|
...state,
|
|
@@ -78,9 +66,9 @@ const removeMarker = (state, payload) => {
|
|
|
78
66
|
|
|
79
67
|
export const actionsMap = {
|
|
80
68
|
SET_MAP_READY: setMapReady,
|
|
81
|
-
MAP_MOVE:
|
|
82
|
-
MAP_MOVE_END:
|
|
83
|
-
MAP_FIRST_IDLE:
|
|
69
|
+
MAP_MOVE: mergePayload,
|
|
70
|
+
MAP_MOVE_END: mergePayload,
|
|
71
|
+
MAP_FIRST_IDLE: mergePayload,
|
|
84
72
|
SET_MAP_STYLE: setMapStyle,
|
|
85
73
|
SET_MAP_SIZE: setMapSize,
|
|
86
74
|
UPDATE_CROSS_HAIR: updateCrossHair,
|