@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
|
@@ -35,11 +35,12 @@ export const createKeyboardActions = (mapProvider, announce, {
|
|
|
35
35
|
|
|
36
36
|
zoomOut: (e) => mapProvider.zoomOut(getZoom(e.shiftKey)),
|
|
37
37
|
|
|
38
|
-
getInfo: async (
|
|
38
|
+
getInfo: async (_e) => {
|
|
39
39
|
const coord = mapProvider.getCenter()
|
|
40
40
|
const place = await reverseGeocode(mapProvider.getZoom(), coord)
|
|
41
41
|
const area = mapProvider.getAreaDimensions?.()
|
|
42
|
-
|
|
42
|
+
const message = area ? `${place}. Covering ${area}.` : `${place}.`
|
|
43
|
+
announce(message, 'core')
|
|
43
44
|
},
|
|
44
45
|
|
|
45
46
|
highlightNextLabel: (e) => {
|
|
@@ -48,13 +48,15 @@ export const coreShortcuts = [
|
|
|
48
48
|
group: 'Labels',
|
|
49
49
|
title: 'Highlight label at centre',
|
|
50
50
|
command: '<kbd>Alt</kbd> + <kbd>Enter</kbd>',
|
|
51
|
-
enabled: false
|
|
51
|
+
enabled: false,
|
|
52
|
+
requiredConfig: ['readMapText']
|
|
52
53
|
},
|
|
53
54
|
{
|
|
54
55
|
id: 'highlightNextLabel',
|
|
55
56
|
group: 'Labels',
|
|
56
57
|
title: 'Highlight nearby label',
|
|
57
58
|
command: '<kbd>Alt</kbd> + <kbd>→</kbd>, <kbd>←</kbd>, <kbd>↑</kbd> or <kbd>↓</kbd>',
|
|
58
|
-
enabled: false
|
|
59
|
+
enabled: false,
|
|
60
|
+
requiredConfig: ['readMapText']
|
|
59
61
|
}
|
|
60
62
|
]
|
|
@@ -4,6 +4,52 @@ import { useApp } from '../store/appContext.js'
|
|
|
4
4
|
import { useConfig } from '../store/configContext.js'
|
|
5
5
|
import { PluginContext } from '../store/PluginProvider.jsx'
|
|
6
6
|
|
|
7
|
+
function evaluateButtonState ({ btn, pluginId, appState, dispatch, evaluateProp }) {
|
|
8
|
+
const checks = [{
|
|
9
|
+
prop: 'enableWhen',
|
|
10
|
+
state: appState.disabledButtons,
|
|
11
|
+
action: 'TOGGLE_BUTTON_DISABLED',
|
|
12
|
+
invert: true,
|
|
13
|
+
key: 'isDisabled'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
prop: 'hiddenWhen',
|
|
17
|
+
state: appState.hiddenButtons,
|
|
18
|
+
action: 'TOGGLE_BUTTON_HIDDEN',
|
|
19
|
+
key: 'isHidden'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
prop: 'pressedWhen',
|
|
23
|
+
state: appState.pressedButtons,
|
|
24
|
+
action: 'TOGGLE_BUTTON_PRESSED',
|
|
25
|
+
key: 'isPressed'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
prop: 'expandedWhen',
|
|
29
|
+
state: appState.expandedButtons,
|
|
30
|
+
action: 'TOGGLE_BUTTON_EXPANDED',
|
|
31
|
+
key: 'isExpanded'
|
|
32
|
+
}]
|
|
33
|
+
|
|
34
|
+
checks.forEach(({ prop, state, action, key, invert }) => {
|
|
35
|
+
if (typeof btn[prop] !== 'function') {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = evaluateProp(btn[prop], pluginId)
|
|
41
|
+
const next = invert ? !result : result
|
|
42
|
+
const current = state.has(btn.id)
|
|
43
|
+
|
|
44
|
+
if (current !== next) {
|
|
45
|
+
dispatch({ type: action, payload: { id: btn.id, [key]: next } })
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn(`${prop} error for button ${btn.id}:`, err)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
7
53
|
export function useButtonStateEvaluator (evaluateProp) {
|
|
8
54
|
const appState = useApp()
|
|
9
55
|
const { pluginRegistry } = useConfig()
|
|
@@ -13,54 +59,21 @@ export function useButtonStateEvaluator (evaluateProp) {
|
|
|
13
59
|
if (!appState?.dispatch || !pluginContext) {
|
|
14
60
|
return
|
|
15
61
|
}
|
|
16
|
-
const { dispatch } = appState
|
|
17
62
|
|
|
18
|
-
|
|
19
|
-
const buttons = plugin?.manifest?.buttons || []
|
|
20
|
-
|
|
21
|
-
buttons.forEach((btn) => {
|
|
22
|
-
const pluginId = plugin.id
|
|
23
|
-
|
|
24
|
-
// EnableWhen
|
|
25
|
-
if (typeof btn.enableWhen === 'function') {
|
|
26
|
-
try {
|
|
27
|
-
const shouldBeEnabled = evaluateProp(btn.enableWhen, pluginId)
|
|
28
|
-
const currentlyDisabled = appState.disabledButtons.has(btn.id)
|
|
29
|
-
const isDisabled = !shouldBeEnabled
|
|
30
|
-
if (currentlyDisabled !== isDisabled) {
|
|
31
|
-
dispatch({ type: 'TOGGLE_BUTTON_DISABLED', payload: { id: btn.id, isDisabled } })
|
|
32
|
-
}
|
|
33
|
-
} catch (err) {
|
|
34
|
-
console.warn(`enableWhen error for button ${btn.id}:`, err)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
63
|
+
const { dispatch } = appState
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const shouldBeHidden = evaluateProp(btn.hiddenWhen, pluginId)
|
|
42
|
-
const currentlyHidden = appState.hiddenButtons.has(btn.id)
|
|
43
|
-
if (currentlyHidden !== shouldBeHidden) {
|
|
44
|
-
dispatch({ type: 'TOGGLE_BUTTON_HIDDEN', payload: { id: btn.id, isHidden: shouldBeHidden } })
|
|
45
|
-
}
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.warn(`hiddenWhen error for button ${btn.id}:`, err)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
65
|
+
pluginRegistry.registeredPlugins.forEach(plugin => {
|
|
66
|
+
const buttons = (plugin?.manifest?.buttons ?? []).flatMap(b => [b, ...(b.menuItems ?? [])])
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
console.warn(`pressedWhen error for button ${btn.id}:`, err)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
})
|
|
68
|
+
buttons.forEach(btn =>
|
|
69
|
+
evaluateButtonState({
|
|
70
|
+
btn,
|
|
71
|
+
pluginId: plugin.id,
|
|
72
|
+
appState,
|
|
73
|
+
dispatch,
|
|
74
|
+
evaluateProp
|
|
75
|
+
})
|
|
76
|
+
)
|
|
64
77
|
})
|
|
65
78
|
}, [appState, pluginContext, evaluateProp])
|
|
66
79
|
}
|
|
@@ -21,6 +21,7 @@ describe('useButtonStateEvaluator', () => {
|
|
|
21
21
|
disabledButtons: new Set(),
|
|
22
22
|
hiddenButtons: new Set(),
|
|
23
23
|
pressedButtons: new Set(),
|
|
24
|
+
expandedButtons: new Set(),
|
|
24
25
|
dispatch: mockDispatch
|
|
25
26
|
}
|
|
26
27
|
useApp.mockReturnValue(mockAppState)
|
|
@@ -89,15 +90,30 @@ describe('useButtonStateEvaluator', () => {
|
|
|
89
90
|
})
|
|
90
91
|
})
|
|
91
92
|
|
|
93
|
+
it('toggles button expanded state based on expandedWhen', () => {
|
|
94
|
+
mockPluginRegistry.registeredPlugins = [{
|
|
95
|
+
id: 'p1',
|
|
96
|
+
manifest: { buttons: [{ id: 'btn1', expandedWhen: () => true }] }
|
|
97
|
+
}]
|
|
98
|
+
|
|
99
|
+
renderHook(() => useButtonStateEvaluator((fn) => fn()))
|
|
100
|
+
expect(mockDispatch).toHaveBeenCalledWith({
|
|
101
|
+
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
102
|
+
payload: { id: 'btn1', isExpanded: true }
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
92
106
|
it('does not dispatch if state already matches', () => {
|
|
93
107
|
mockAppState.disabledButtons.add('btn1')
|
|
94
108
|
mockAppState.hiddenButtons.add('btn2')
|
|
95
109
|
mockAppState.pressedButtons.add('btn3')
|
|
110
|
+
mockAppState.expandedButtons.add('btn4')
|
|
96
111
|
|
|
97
112
|
mockPluginRegistry.registeredPlugins = [
|
|
98
113
|
{ id: 'p1', manifest: { buttons: [{ id: 'btn1', enableWhen: () => false }] } },
|
|
99
114
|
{ id: 'p2', manifest: { buttons: [{ id: 'btn2', hiddenWhen: () => true }] } },
|
|
100
|
-
{ id: 'p3', manifest: { buttons: [{ id: 'btn3', pressedWhen: () => true }] } }
|
|
115
|
+
{ id: 'p3', manifest: { buttons: [{ id: 'btn3', pressedWhen: () => true }] } },
|
|
116
|
+
{ id: 'p4', manifest: { buttons: [{ id: 'btn4', expandedWhen: () => true }] } }
|
|
101
117
|
]
|
|
102
118
|
|
|
103
119
|
renderHook(() => useButtonStateEvaluator((fn) => fn()))
|
|
@@ -112,13 +128,14 @@ describe('useButtonStateEvaluator', () => {
|
|
|
112
128
|
buttons: [
|
|
113
129
|
{ id: 'btn1', enableWhen: failingFn },
|
|
114
130
|
{ id: 'btn2', hiddenWhen: failingFn },
|
|
115
|
-
{ id: 'btn3', pressedWhen: failingFn }
|
|
131
|
+
{ id: 'btn3', pressedWhen: failingFn },
|
|
132
|
+
{ id: 'btn4', expandedWhen: failingFn }
|
|
116
133
|
]
|
|
117
134
|
}
|
|
118
135
|
}]
|
|
119
136
|
|
|
120
137
|
renderHook(() => useButtonStateEvaluator((fn, pluginId) => fn(pluginId)))
|
|
121
|
-
expect(console.warn).toHaveBeenCalledTimes(
|
|
138
|
+
expect(console.warn).toHaveBeenCalledTimes(4)
|
|
122
139
|
expect(mockDispatch).not.toHaveBeenCalled()
|
|
123
140
|
})
|
|
124
141
|
|
|
@@ -130,4 +147,20 @@ describe('useButtonStateEvaluator', () => {
|
|
|
130
147
|
renderHook(() => useButtonStateEvaluator((fn) => fn({ pluginState: {} })))
|
|
131
148
|
expect(enableWhen).toHaveBeenCalled()
|
|
132
149
|
})
|
|
150
|
+
|
|
151
|
+
it('covers fallback to empty array when manifest or buttons is missing', () => {
|
|
152
|
+
// Branch 1: Plugin exists but manifest is missing
|
|
153
|
+
// Branch 2: Manifest exists but buttons is missing
|
|
154
|
+
mockPluginRegistry.registeredPlugins = [
|
|
155
|
+
{ id: 'p1' },
|
|
156
|
+
{ id: 'p2', manifest: {} },
|
|
157
|
+
{ id: 'p3', manifest: { buttons: null } }
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
renderHook(() => useButtonStateEvaluator((fn) => fn()))
|
|
161
|
+
|
|
162
|
+
// If the fallback (|| []) works, the code continues to the next plugin
|
|
163
|
+
// without throwing a "cannot read property forEach of undefined" error.
|
|
164
|
+
expect(mockDispatch).not.toHaveBeenCalled()
|
|
165
|
+
})
|
|
133
166
|
})
|
|
@@ -6,6 +6,53 @@ import { useService } from '../store/serviceContext.js'
|
|
|
6
6
|
import { scaleFactor } from '../../config/appConfig.js'
|
|
7
7
|
import { EVENTS as events } from '../../config/events.js'
|
|
8
8
|
|
|
9
|
+
const assignCrossHairAPI = (crossHair, el, mapProvider, dispatch, updatePosition) => {
|
|
10
|
+
crossHair.pinToMap = (coords, state) => {
|
|
11
|
+
const { x, y } = mapProvider.mapToScreen(coords)
|
|
12
|
+
crossHair.coords = coords
|
|
13
|
+
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isPinnedToMap: true, isVisible: true, coords, state } })
|
|
14
|
+
updatePosition(el, x, y)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
crossHair.fixAtCenter = () => {
|
|
18
|
+
el.style.left = '50%'
|
|
19
|
+
el.style.top = '50%'
|
|
20
|
+
el.style.transform = 'translate(0,0)'
|
|
21
|
+
el.style.display = 'block'
|
|
22
|
+
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isPinnedToMap: false, isVisible: true } })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
crossHair.remove = () => {
|
|
26
|
+
el.style.display = 'none'
|
|
27
|
+
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isPinnedToMap: false, isVisible: false } })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
crossHair.show = () => {
|
|
31
|
+
el.style.display = 'block'
|
|
32
|
+
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isVisible: true } })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
crossHair.hide = () => {
|
|
36
|
+
el.style.display = 'none'
|
|
37
|
+
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isVisible: false } })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
crossHair.setStyle = (state) => {
|
|
41
|
+
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { state } })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
crossHair.getDetail = () => {
|
|
45
|
+
const coords = crossHair.isPinnedToMap ? crossHair.coords : mapProvider.getCenter()
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
state: crossHair.state,
|
|
49
|
+
point: mapProvider.mapToScreen(coords),
|
|
50
|
+
zoom: mapProvider.getZoom(),
|
|
51
|
+
coords
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
9
56
|
export const useCrossHair = () => {
|
|
10
57
|
const { mapProvider } = useConfig()
|
|
11
58
|
const { safeZoneInset } = useApp()
|
|
@@ -25,55 +72,10 @@ export const useCrossHair = () => {
|
|
|
25
72
|
|
|
26
73
|
const crossHairRef = useCallback(el => {
|
|
27
74
|
if (!el) {
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// --- API ---
|
|
32
|
-
|
|
33
|
-
crossHair.pinToMap = (coords, state) => {
|
|
34
|
-
const { x, y } = mapProvider.mapToScreen(coords)
|
|
35
|
-
crossHair.coords = coords
|
|
36
|
-
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isPinnedToMap: true, isVisible: true, coords, state } })
|
|
37
|
-
updatePosition(el, x, y)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
crossHair.fixAtCenter = () => {
|
|
41
|
-
el.style.left = '50%'
|
|
42
|
-
el.style.top = '50%'
|
|
43
|
-
el.style.transform = 'translate(0,0)'
|
|
44
|
-
el.style.display = 'block'
|
|
45
|
-
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isPinnedToMap: false, isVisible: true } })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
crossHair.remove = () => {
|
|
49
|
-
el.style.display = 'none'
|
|
50
|
-
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isPinnedToMap: false, isVisible: false } })
|
|
75
|
+
return undefined
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
crossHair
|
|
54
|
-
el.style.display = 'block'
|
|
55
|
-
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isVisible: true } })
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
crossHair.hide = () => {
|
|
59
|
-
el.style.display = 'none'
|
|
60
|
-
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { isVisible: false } })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
crossHair.setStyle = (state) => {
|
|
64
|
-
dispatch({ type: 'UPDATE_CROSS_HAIR', payload: { state } })
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
crossHair.getDetail = () => {
|
|
68
|
-
const coords = crossHair.isPinnedToMap ? crossHair.coords : mapProvider.getCenter()
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
state: crossHair.state,
|
|
72
|
-
point: mapProvider.mapToScreen(coords),
|
|
73
|
-
zoom: mapProvider.getZoom(),
|
|
74
|
-
coords
|
|
75
|
-
}
|
|
76
|
-
}
|
|
78
|
+
assignCrossHairAPI(crossHair, el, mapProvider, dispatch, updatePosition)
|
|
77
79
|
|
|
78
80
|
const handleRender = () => {
|
|
79
81
|
if (crossHair.coords && crossHair.isPinnedToMap) {
|
|
@@ -11,7 +11,7 @@ export function useFocusVisible () {
|
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
const scope = layoutRefs.appContainerRef.current
|
|
13
13
|
if (!scope) {
|
|
14
|
-
return
|
|
14
|
+
return undefined
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function handleFocusIn (e) {
|
|
@@ -22,7 +22,7 @@ export function useFocusVisible () {
|
|
|
22
22
|
delete e.target.dataset.focusVisible
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function handlePointerdown (
|
|
25
|
+
function handlePointerdown () {
|
|
26
26
|
delete document.activeElement.dataset.focusVisible
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -5,12 +5,19 @@ import { useApp } from '../store/appContext.js'
|
|
|
5
5
|
import { useService } from '../store/serviceContext.js'
|
|
6
6
|
|
|
7
7
|
export const useInterfaceAPI = () => {
|
|
8
|
-
const { dispatch, hiddenButtons, pressedButtons,
|
|
8
|
+
const { dispatch, hiddenButtons, disabledButtons, pressedButtons, expandedButtons } = useApp()
|
|
9
9
|
const { eventBus } = useService()
|
|
10
10
|
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
const handleAddButton = ({ id, config }) => {
|
|
13
|
+
// Add the button
|
|
13
14
|
dispatch({ type: 'ADD_BUTTON', payload: { id, config } })
|
|
15
|
+
// Add all optional menu items as individual buttons
|
|
16
|
+
if (Array.isArray(config.menuItems)) {
|
|
17
|
+
config.menuItems.forEach(item => {
|
|
18
|
+
dispatch({ type: 'ADD_BUTTON', payload: { id: item.id, config: item } })
|
|
19
|
+
})
|
|
20
|
+
}
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
const handleToggleButtonState = ({ id, prop, value }) => {
|
|
@@ -20,14 +27,19 @@ export const useInterfaceAPI = () => {
|
|
|
20
27
|
dispatch({ type: 'TOGGLE_BUTTON_HIDDEN', payload: { id, isHidden } })
|
|
21
28
|
break
|
|
22
29
|
}
|
|
30
|
+
case 'disabled': {
|
|
31
|
+
const isDisabled = typeof value === 'boolean' ? value : !disabledButtons.has(id)
|
|
32
|
+
dispatch({ type: 'TOGGLE_BUTTON_DISABLED', payload: { id, isDisabled } })
|
|
33
|
+
break
|
|
34
|
+
}
|
|
23
35
|
case 'pressed': {
|
|
24
36
|
const isPressed = typeof value === 'boolean' ? value : !pressedButtons.has(id)
|
|
25
37
|
dispatch({ type: 'TOGGLE_BUTTON_PRESSED', payload: { id, isPressed } })
|
|
26
38
|
break
|
|
27
39
|
}
|
|
28
|
-
case '
|
|
29
|
-
const
|
|
30
|
-
dispatch({ type: '
|
|
40
|
+
case 'expanded': {
|
|
41
|
+
const isExpanded = typeof value === 'boolean' ? value : !expandedButtons.has(id)
|
|
42
|
+
dispatch({ type: 'TOGGLE_BUTTON_EXPANDED', payload: { id, isExpanded } })
|
|
31
43
|
break
|
|
32
44
|
}
|
|
33
45
|
default:
|
|
@@ -13,7 +13,7 @@ export function useKeyboardShortcuts (containerRef) {
|
|
|
13
13
|
useEffect(() => {
|
|
14
14
|
const el = containerRef.current
|
|
15
15
|
if (!el || interfaceType !== 'keyboard') {
|
|
16
|
-
return
|
|
16
|
+
return undefined
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const actions = createKeyboardActions(mapProvider, announce, {
|
|
@@ -31,7 +31,7 @@ export function useKeyboardShortcuts (containerRef) {
|
|
|
31
31
|
|
|
32
32
|
// Use e.code for letters to avoid 'dead' keys with Alt/AltGr
|
|
33
33
|
if (/^Key[A-Z]$/.test(e.code)) {
|
|
34
|
-
key = e.code.slice(3) // "KeyI" -> "I"
|
|
34
|
+
key = e.code.slice(3) // NOSONAR: strip "Key" prefix, e.g. "KeyI" -> "I"
|
|
35
35
|
} else {
|
|
36
36
|
key = e.key // works for arrows, numpad, punctuation
|
|
37
37
|
}
|
|
@@ -41,6 +41,8 @@ export function useKeyboardShortcuts (containerRef) {
|
|
|
41
41
|
key = '+'
|
|
42
42
|
} else if (key === 'Subtract' || key === 'NumpadSubtract') {
|
|
43
43
|
key = '-'
|
|
44
|
+
} else {
|
|
45
|
+
// No action
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
return e.altKey ? `Alt+${key}` : key
|
|
@@ -5,6 +5,39 @@ import { useService } from '../store/serviceContext.js'
|
|
|
5
5
|
import { getMapStatusMessage } from '../../utils/getMapStatusMessage.js'
|
|
6
6
|
import { EVENTS as events } from '../../config/events.js'
|
|
7
7
|
|
|
8
|
+
const resolveMessage = (previous, current, mapProvider) => {
|
|
9
|
+
const zoomChanged = previous.zoom !== current.zoom
|
|
10
|
+
const centerChanged =
|
|
11
|
+
previous.center[0] !== current.center[0] ||
|
|
12
|
+
previous.center[1] !== current.center[1]
|
|
13
|
+
|
|
14
|
+
const areaDimensions = mapProvider.getAreaDimensions()
|
|
15
|
+
|
|
16
|
+
// Panned only
|
|
17
|
+
if (centerChanged && !zoomChanged) {
|
|
18
|
+
const direction = mapProvider.getCardinalMove(previous.center, current.center)
|
|
19
|
+
return getMapStatusMessage.moved({ direction, areaDimensions })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Zoomed only
|
|
23
|
+
if (!centerChanged && zoomChanged) {
|
|
24
|
+
return getMapStatusMessage.zoomed({
|
|
25
|
+
...current,
|
|
26
|
+
from: previous.zoom,
|
|
27
|
+
to: current.zoom,
|
|
28
|
+
areaDimensions
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// No change
|
|
33
|
+
if (!centerChanged && !zoomChanged) {
|
|
34
|
+
return getMapStatusMessage.noChange({ ...current })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Panned and zoomed
|
|
38
|
+
return getMapStatusMessage.newArea({ ...current, areaDimensions })
|
|
39
|
+
}
|
|
40
|
+
|
|
8
41
|
export function useMapAnnouncements () {
|
|
9
42
|
const { mapProvider } = useConfig()
|
|
10
43
|
const { eventBus, announce } = useService()
|
|
@@ -15,38 +48,7 @@ export function useMapAnnouncements () {
|
|
|
15
48
|
return
|
|
16
49
|
}
|
|
17
50
|
|
|
18
|
-
const
|
|
19
|
-
const centerChanged =
|
|
20
|
-
previous.center[0] !== current.center[0] ||
|
|
21
|
-
previous.center[1] !== current.center[1]
|
|
22
|
-
|
|
23
|
-
const areaDimensions = mapProvider.getAreaDimensions()
|
|
24
|
-
let message
|
|
25
|
-
|
|
26
|
-
if (centerChanged && !zoomChanged) {
|
|
27
|
-
const direction = mapProvider.getCardinalMove(previous.center, current.center)
|
|
28
|
-
message = getMapStatusMessage.moved({
|
|
29
|
-
direction,
|
|
30
|
-
areaDimensions
|
|
31
|
-
})
|
|
32
|
-
} else if (!centerChanged && zoomChanged) {
|
|
33
|
-
message = getMapStatusMessage.zoomed({
|
|
34
|
-
...current,
|
|
35
|
-
from: previous.zoom,
|
|
36
|
-
to: current.zoom,
|
|
37
|
-
areaDimensions
|
|
38
|
-
})
|
|
39
|
-
} else if (!centerChanged && !zoomChanged) {
|
|
40
|
-
message = getMapStatusMessage.noChange({
|
|
41
|
-
...current
|
|
42
|
-
})
|
|
43
|
-
} else {
|
|
44
|
-
message = getMapStatusMessage.newArea({
|
|
45
|
-
...current,
|
|
46
|
-
areaDimensions
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
51
|
+
const message = resolveMessage(previous, current, mapProvider)
|
|
50
52
|
if (message) {
|
|
51
53
|
announce(message, 'core')
|
|
52
54
|
}
|
|
@@ -2,13 +2,24 @@ import { useEffect } from 'react'
|
|
|
2
2
|
import { useConfig } from '../store/configContext.js'
|
|
3
3
|
import { useService } from '../store/serviceContext.js'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Subscribes to map events via the shared event bus for the lifetime of
|
|
7
|
+
* the consuming component. Handlers are automatically unsubscribed on
|
|
8
|
+
* unmount or when the eventMap reference changes.
|
|
9
|
+
*
|
|
10
|
+
* Supported events include clicks, pans, zooms, style changes and more
|
|
11
|
+
* — see `EVENTS` in `config/events.js` for the full list.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object<string, Function>} [eventMap={}] - A map of event names to
|
|
14
|
+
* callback functions.
|
|
15
|
+
*/
|
|
5
16
|
export function useMapEvents (eventMap = {}) {
|
|
6
17
|
const { mapProvider } = useConfig()
|
|
7
18
|
const { eventBus } = useService()
|
|
8
19
|
|
|
9
20
|
useEffect(() => {
|
|
10
21
|
if (!mapProvider) {
|
|
11
|
-
return
|
|
22
|
+
return undefined
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
const handlers = {}
|
|
@@ -32,14 +32,14 @@ export const useMapProviderOverrides = () => {
|
|
|
32
32
|
|
|
33
33
|
useEffect(() => {
|
|
34
34
|
if (!mapProvider) {
|
|
35
|
-
return
|
|
35
|
+
return undefined
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const originalFitToBounds = mapProvider.fitToBounds
|
|
39
39
|
|
|
40
40
|
mapProvider.fitToBounds = (bounds, skipPaddingCalc = false) => {
|
|
41
41
|
if (!bounds) {
|
|
42
|
-
return
|
|
42
|
+
return undefined
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Calculate and set safe zone padding unless explicitly skipped
|
|
@@ -54,7 +54,7 @@ export const useMapProviderOverrides = () => {
|
|
|
54
54
|
|
|
55
55
|
mapProvider.setView = ({ center, zoom }) => {
|
|
56
56
|
if (!center) {
|
|
57
|
-
return
|
|
57
|
+
return undefined
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
updatePadding()
|
|
@@ -4,6 +4,12 @@ import { useMap } from '../store/mapContext.js'
|
|
|
4
4
|
import { useService } from '../store/serviceContext.js'
|
|
5
5
|
import { EVENTS as events } from '../../config/events.js'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Keeps React state in sync with the map provider by listening to move,
|
|
9
|
+
* move-end and first-idle events. On each move-end it also emits a
|
|
10
|
+
* MAP_STATE_UPDATED event carrying both the previous and current state,
|
|
11
|
+
* which other hooks (e.g. useMapAnnouncements) use to determine what changed.
|
|
12
|
+
*/
|
|
7
13
|
export function useMapStateSync () {
|
|
8
14
|
const { mapProvider } = useConfig()
|
|
9
15
|
const { dispatch } = useMap()
|
|
@@ -14,7 +20,7 @@ export function useMapStateSync () {
|
|
|
14
20
|
|
|
15
21
|
useEffect(() => {
|
|
16
22
|
if (!mapProvider) {
|
|
17
|
-
return
|
|
23
|
+
return undefined
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
// Handle map move
|
|
@@ -4,13 +4,18 @@ import { useConfig } from '../store/configContext.js'
|
|
|
4
4
|
import { useService } from '../store/serviceContext.js'
|
|
5
5
|
import { EVENTS as events } from '../../config/events.js'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Persists the current map center and zoom into the page URL whenever the
|
|
9
|
+
* map state changes, allowing the view to be restored on page reload or
|
|
10
|
+
* shared via a link.
|
|
11
|
+
*/
|
|
7
12
|
export function useMapURLSync () {
|
|
8
13
|
const { id } = useConfig()
|
|
9
14
|
const { eventBus } = useService()
|
|
10
15
|
|
|
11
16
|
useEffect(() => {
|
|
12
17
|
if (!id) {
|
|
13
|
-
return
|
|
18
|
+
return undefined
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
const handleStateUpdate = ({ current }) => {
|