@defra/interactive-map 0.0.18-alpha → 0.0.19-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/assets/css/docusaurus.css +58 -34
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/panel-definition.md +16 -0
- package/docs/api.md +28 -4
- package/docs/assets/basic-map.jpg +0 -0
- package/docs/assets/button-first.jpg +0 -0
- package/docs/assets/maker-panel.jpg +0 -0
- package/docs/examples/add-marker-with-panel.mdx +59 -0
- package/docs/examples/basic-map.mdx +24 -0
- package/docs/examples/button-map.mdx +24 -0
- package/docs/examples/index.mdx +49 -0
- package/docs/index.mdx +1 -1
- package/docs/plugins/interact.md +32 -1
- package/docs/plugins.md +1 -1
- package/docusaurus.config.cjs +9 -1
- package/package.json +1 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/css/index.css +2 -19
- 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/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/src/scaleBar.scss +1 -0
- 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 +5 -3
- package/plugins/interact/src/api/clear.js +1 -1
- package/plugins/interact/src/api/selectMarker.js +14 -0
- package/plugins/interact/src/api/selectMarker.test.js +25 -0
- package/plugins/interact/src/api/unselectMarker.js +14 -0
- package/plugins/interact/src/api/unselectMarker.test.js +14 -0
- package/plugins/interact/src/events.js +18 -30
- package/plugins/interact/src/events.test.js +113 -108
- package/plugins/interact/src/manifest.js +10 -2
- package/plugins/interact/src/reducer.js +36 -1
- package/plugins/interact/src/reducer.test.js +40 -1
- package/plugins/interact/src/utils/interactionModes.js +12 -0
- package/src/App/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +37 -0
- package/src/App/components/Viewport/Viewport.jsx +5 -15
- package/src/App/components/Viewport/Viewport.module.scss +2 -0
- package/src/App/components/Viewport/Viewport.test.jsx +16 -33
- package/src/App/hooks/useInterfaceAPI.js +7 -7
- package/src/App/hooks/useInterfaceAPI.test.js +15 -9
- package/src/App/hooks/useLayoutMeasurements.js +64 -72
- package/src/App/layout/Layout.jsx +1 -1
- package/src/App/layout/layout.module.scss +1 -8
- package/src/App/renderer/HtmlElementHost.jsx +10 -5
- package/src/App/renderer/mapPanels.js +2 -1
- package/src/App/store/appActionsMap.js +4 -4
- package/src/App/store/appActionsMap.test.js +10 -0
- package/src/InteractiveMap/InteractiveMap.js +59 -11
- package/src/InteractiveMap/InteractiveMap.test.js +126 -4
- package/src/InteractiveMap/domStateManager.js +18 -6
- package/src/InteractiveMap/domStateManager.test.js +21 -0
- package/src/InteractiveMap/historyManager.js +28 -16
- package/src/InteractiveMap/historyManager.test.js +17 -0
- package/src/config/appConfig.js +2 -1
- package/src/config/appConfig.test.js +3 -13
- package/src/config/defaults.js +2 -1
- package/src/config/events.js +20 -21
- package/src/services/closeApp.js +1 -10
- package/src/services/closeApp.test.js +3 -43
- package/src/types.js +6 -1
- package/src/utils/mapStateSync.js +48 -10
- package/src/utils/mapStateSync.test.js +29 -9
- package/docs/examples.mdx +0 -70
|
@@ -117,7 +117,7 @@ describe('InteractiveMap Core Functionality', () => {
|
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
it('open button click calls _handleButtonClick / loadApp', async () => {
|
|
120
|
-
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
120
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
|
|
121
121
|
const loadSpy = jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
122
122
|
const pushStateSpy = jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
123
123
|
const fakeEvent = { currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } }
|
|
@@ -195,7 +195,7 @@ describe('InteractiveMap Core Functionality', () => {
|
|
|
195
195
|
expect(map.unmount).toHaveBeenCalled()
|
|
196
196
|
expect(mockButtonInstance.removeAttribute).toHaveBeenCalledWith('style')
|
|
197
197
|
expect(mockButtonInstance.focus).toHaveBeenCalled()
|
|
198
|
-
expect(updateDOMState).toHaveBeenCalledWith(map)
|
|
198
|
+
expect(updateDOMState).toHaveBeenCalledWith(map, { isFullscreen: false })
|
|
199
199
|
})
|
|
200
200
|
|
|
201
201
|
it('skips unmount if _root is falsy or unmount is not a function', () => {
|
|
@@ -275,6 +275,7 @@ describe('InteractiveMap Core Functionality', () => {
|
|
|
275
275
|
behaviour: 'buttonFirst',
|
|
276
276
|
mapProvider: mapProviderMock,
|
|
277
277
|
mapViewParamKey: 'mv',
|
|
278
|
+
manageHistoryState: true,
|
|
278
279
|
preserveStateOnClose: false
|
|
279
280
|
})
|
|
280
281
|
|
|
@@ -303,6 +304,7 @@ describe('InteractiveMap Core Functionality', () => {
|
|
|
303
304
|
behaviour: 'buttonFirst',
|
|
304
305
|
mapProvider: mapProviderMock,
|
|
305
306
|
mapViewParamKey: 'mv',
|
|
307
|
+
manageHistoryState: true,
|
|
306
308
|
preserveStateOnClose: true
|
|
307
309
|
})
|
|
308
310
|
|
|
@@ -321,7 +323,7 @@ describe('InteractiveMap Core Functionality', () => {
|
|
|
321
323
|
})
|
|
322
324
|
|
|
323
325
|
it('_handleButtonClick calls showApp when map is hidden', async () => {
|
|
324
|
-
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
326
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
|
|
325
327
|
map._isHidden = true
|
|
326
328
|
const showAppSpy = jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
327
329
|
const loadAppSpy = jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
@@ -339,6 +341,126 @@ describe('InteractiveMap Core Functionality', () => {
|
|
|
339
341
|
pushStateSpy.mockRestore()
|
|
340
342
|
})
|
|
341
343
|
|
|
344
|
+
it('_handleButtonClick skips pushState when manageHistoryState is false', async () => {
|
|
345
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: false, mapProvider: mapProviderMock })
|
|
346
|
+
expect(map.config.manageHistoryState).toBe(false)
|
|
347
|
+
const pushStateSpy = jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
348
|
+
const fakeEvent = { currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } }
|
|
349
|
+
|
|
350
|
+
await openButtonCallback(fakeEvent)
|
|
351
|
+
|
|
352
|
+
expect(pushStateSpy).not.toHaveBeenCalled()
|
|
353
|
+
pushStateSpy.mockRestore()
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('_handleExitClick calls history.back() when history.state.isBack is true', () => {
|
|
357
|
+
const backSpy = jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
358
|
+
Object.defineProperty(history, 'state', { value: { isBack: true }, writable: true, configurable: true })
|
|
359
|
+
|
|
360
|
+
const map = new InteractiveMap('map', {
|
|
361
|
+
behaviour: 'buttonFirst',
|
|
362
|
+
mapProvider: mapProviderMock,
|
|
363
|
+
mapViewParamKey: 'mv',
|
|
364
|
+
manageHistoryState: true,
|
|
365
|
+
preserveStateOnClose: false
|
|
366
|
+
})
|
|
367
|
+
jest.spyOn(map, 'removeApp').mockImplementation(() => {})
|
|
368
|
+
|
|
369
|
+
map._handleExitClick()
|
|
370
|
+
|
|
371
|
+
expect(backSpy).toHaveBeenCalled()
|
|
372
|
+
Object.defineProperty(history, 'state', { value: null, writable: true, configurable: true })
|
|
373
|
+
backSpy.mockRestore()
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('_handleExitClick skips history when manageHistoryState is false', () => {
|
|
377
|
+
const backSpy = jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
378
|
+
const replaceStateSpy = jest.spyOn(history, 'replaceState').mockImplementation(() => {})
|
|
379
|
+
|
|
380
|
+
const map = new InteractiveMap('map', {
|
|
381
|
+
behaviour: 'buttonFirst',
|
|
382
|
+
mapProvider: mapProviderMock,
|
|
383
|
+
mapViewParamKey: 'mv',
|
|
384
|
+
manageHistoryState: false,
|
|
385
|
+
preserveStateOnClose: false
|
|
386
|
+
})
|
|
387
|
+
jest.spyOn(map, 'removeApp').mockImplementation(() => {})
|
|
388
|
+
|
|
389
|
+
map._handleExitClick()
|
|
390
|
+
|
|
391
|
+
expect(backSpy).not.toHaveBeenCalled()
|
|
392
|
+
expect(replaceStateSpy).not.toHaveBeenCalled()
|
|
393
|
+
backSpy.mockRestore()
|
|
394
|
+
replaceStateSpy.mockRestore()
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it('loadApp emits APP_OPENED with statePreserved: false', async () => {
|
|
398
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
399
|
+
await map.loadApp()
|
|
400
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: false })
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('removeApp emits APP_CLOSED with statePreserved: false', () => {
|
|
404
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
405
|
+
map._root = {}
|
|
406
|
+
map.unmount = jest.fn()
|
|
407
|
+
map.removeApp()
|
|
408
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:closed', { statePreserved: false })
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('hideApp emits APP_CLOSED with statePreserved: true', () => {
|
|
412
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
413
|
+
map.hideApp()
|
|
414
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:closed', { statePreserved: true })
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('showApp emits APP_OPENED with statePreserved: true', () => {
|
|
418
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
419
|
+
map._isHidden = true
|
|
420
|
+
map.showApp()
|
|
421
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: true })
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('open() shows hidden map', () => {
|
|
425
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
426
|
+
const showSpy = jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
427
|
+
map._isHidden = true
|
|
428
|
+
map.open()
|
|
429
|
+
expect(showSpy).toHaveBeenCalled()
|
|
430
|
+
showSpy.mockRestore()
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('open() loads map when not yet initialised', () => {
|
|
434
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
435
|
+
const loadSpy = jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
436
|
+
map._root = null
|
|
437
|
+
map._isHidden = false
|
|
438
|
+
map.open()
|
|
439
|
+
expect(loadSpy).toHaveBeenCalled()
|
|
440
|
+
loadSpy.mockRestore()
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('open() is a no-op when already open', () => {
|
|
444
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
445
|
+
const loadSpy = jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
446
|
+
const showSpy = jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
447
|
+
map._root = {}
|
|
448
|
+
map._isHidden = false
|
|
449
|
+
map.open()
|
|
450
|
+
expect(loadSpy).not.toHaveBeenCalled()
|
|
451
|
+
expect(showSpy).not.toHaveBeenCalled()
|
|
452
|
+
loadSpy.mockRestore()
|
|
453
|
+
showSpy.mockRestore()
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('close() delegates to _handleExitClick', () => {
|
|
457
|
+
const map = new InteractiveMap('map', { mapProvider: mapProviderMock })
|
|
458
|
+
const exitSpy = jest.spyOn(map, '_handleExitClick').mockImplementation(() => {})
|
|
459
|
+
map.close()
|
|
460
|
+
expect(exitSpy).toHaveBeenCalled()
|
|
461
|
+
exitSpy.mockRestore()
|
|
462
|
+
})
|
|
463
|
+
|
|
342
464
|
it('hideApp sets _isHidden and hides element', () => {
|
|
343
465
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
344
466
|
map._openButton = mockButtonInstance
|
|
@@ -497,7 +619,7 @@ describe('InteractiveMap Public API Methods', () => {
|
|
|
497
619
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:removepanel', 'panel1')
|
|
498
620
|
|
|
499
621
|
// New assertions for coverage
|
|
500
|
-
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', 'panel2')
|
|
622
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel2', focus: true })
|
|
501
623
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:hidepanel', 'panel3')
|
|
502
624
|
})
|
|
503
625
|
|
|
@@ -14,11 +14,21 @@ function updatePageTitle ({ pageTitle, isFullscreen }) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function getIsFullscreen (config) {
|
|
17
|
-
const { id, behaviour } = config
|
|
18
|
-
const hasViewParam = getQueryParam(defaults.mapViewParamKey) === id
|
|
17
|
+
const { id, behaviour, manageHistoryState } = config
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
if (behaviour === 'mapOnly') {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (behaviour === 'buttonFirst') {
|
|
24
|
+
// When the SPA manages history, the app is always fullscreen when loaded
|
|
25
|
+
if (manageHistoryState === false) {
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
return getQueryParam(defaults.mapViewParamKey) === id
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return isHybridFullscreen(config) && getQueryParam(defaults.mapViewParamKey) === id
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
// -----------------------------------------------------------------------------
|
|
@@ -36,12 +46,14 @@ function getIsFullscreen (config) {
|
|
|
36
46
|
* @param {string} mapInstance.config.behaviour - Behaviour mode ("mapOnly", "buttonFirst", "hybrid").
|
|
37
47
|
* @param {string|number} mapInstance.config.containerHeight - Height to use when not fullscreen.
|
|
38
48
|
* @param {HTMLElement} mapInstance.rootEl - Root element of the app.
|
|
49
|
+
* @param {Object} [options]
|
|
50
|
+
* @param {boolean} [options.isFullscreen] - Override the computed fullscreen state.
|
|
39
51
|
* @returns {void}
|
|
40
52
|
*/
|
|
41
|
-
function updateDOMState (mapInstance) {
|
|
53
|
+
function updateDOMState (mapInstance, { isFullscreen: isFullscreenOverride } = {}) {
|
|
42
54
|
const { config, rootEl } = mapInstance
|
|
43
55
|
const { pageTitle, behaviour, containerHeight } = config
|
|
44
|
-
const isFullscreen = getIsFullscreen(config)
|
|
56
|
+
const isFullscreen = isFullscreenOverride ?? getIsFullscreen(config)
|
|
45
57
|
|
|
46
58
|
if (['mapOnly', 'buttonFirst', 'hybrid'].includes(behaviour)) {
|
|
47
59
|
toggleInertElements({ containerEl: rootEl, isFullscreen })
|
|
@@ -69,6 +69,27 @@ describe('updateDOMState', () => {
|
|
|
69
69
|
}
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
it('isFullscreen override forces non-fullscreen even when URL param matches', () => {
|
|
73
|
+
mapInstance.config.behaviour = 'buttonFirst'
|
|
74
|
+
queryString.getQueryParam.mockReturnValue('map') // URL says fullscreen
|
|
75
|
+
|
|
76
|
+
updateDOMState(mapInstance, { isFullscreen: false }) // but override says no
|
|
77
|
+
|
|
78
|
+
expect(document.documentElement.classList.contains('im-is-fullscreen')).toBe(false)
|
|
79
|
+
expect(rootEl.style.height).toBe('auto')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('buttonFirst with manageHistoryState false is always fullscreen regardless of URL', () => {
|
|
83
|
+
mapInstance.config.behaviour = 'buttonFirst'
|
|
84
|
+
mapInstance.config.manageHistoryState = false
|
|
85
|
+
queryString.getQueryParam.mockReturnValue(null) // no mv param in URL
|
|
86
|
+
|
|
87
|
+
updateDOMState(mapInstance)
|
|
88
|
+
|
|
89
|
+
expect(document.documentElement.classList.contains('im-is-fullscreen')).toBe(true)
|
|
90
|
+
expect(mapInstance.rootEl.style.height).toBe('100%')
|
|
91
|
+
})
|
|
92
|
+
|
|
72
93
|
describe('hybrid behaviour', () => {
|
|
73
94
|
beforeEach(() => {
|
|
74
95
|
mapInstance.config.behaviour = 'hybrid'
|
|
@@ -40,6 +40,32 @@ function closeMap (mapInstance) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Syncs a single map instance against the current URL view parameter.
|
|
45
|
+
*
|
|
46
|
+
* @param {MapInstance} mapInstance
|
|
47
|
+
* @param {string|null} viewId - The current `mv` query param value.
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
function syncMapInstance (mapInstance, viewId) {
|
|
51
|
+
if (mapInstance.config.manageHistoryState === false) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const shouldBeOpen = mapInstance.id === viewId
|
|
56
|
+
const isHybridVisible = mapInstance.config.behaviour === 'hybrid' && !isHybridFullscreen(mapInstance.config)
|
|
57
|
+
const isOpen = mapInstance.rootEl?.children.length
|
|
58
|
+
|
|
59
|
+
if (shouldBeOpen && (!isOpen || mapInstance._isHidden)) {
|
|
60
|
+
openMap(mapInstance)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!shouldBeOpen && isOpen && !isHybridVisible) {
|
|
65
|
+
closeMap(mapInstance)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
/**
|
|
44
70
|
* Handles the `popstate` event triggered by browser back/forward navigation.
|
|
45
71
|
*
|
|
@@ -56,21 +82,7 @@ function closeMap (mapInstance) {
|
|
|
56
82
|
*/
|
|
57
83
|
function handlePopstate () {
|
|
58
84
|
const viewId = getQueryParam(defaults.mapViewParamKey)
|
|
59
|
-
|
|
60
|
-
for (const mapInstance of components.values()) {
|
|
61
|
-
const shouldBeOpen = mapInstance.id === viewId
|
|
62
|
-
const isHybridVisible = mapInstance.config.behaviour === 'hybrid' && !isHybridFullscreen(mapInstance.config)
|
|
63
|
-
const isOpen = mapInstance.rootEl?.children.length
|
|
64
|
-
|
|
65
|
-
if (shouldBeOpen && (!isOpen || mapInstance._isHidden)) {
|
|
66
|
-
openMap(mapInstance)
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!shouldBeOpen && isOpen && !isHybridVisible) {
|
|
71
|
-
closeMap(mapInstance)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
85
|
+
components.forEach(mapInstance => syncMapInstance(mapInstance, viewId))
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
// -----------------------------------------------------------------------------
|
|
@@ -105,7 +117,7 @@ let initialized = false
|
|
|
105
117
|
*/
|
|
106
118
|
function register (component) {
|
|
107
119
|
if (!initialized) {
|
|
108
|
-
|
|
120
|
+
globalThis.addEventListener('popstate', handlePopstate)
|
|
109
121
|
initialized = true
|
|
110
122
|
}
|
|
111
123
|
|
|
@@ -141,6 +141,23 @@ describe('historyManager', () => {
|
|
|
141
141
|
expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 768px)')
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
it('skips component when manageHistoryState is false', () => {
|
|
145
|
+
const managedComponent = {
|
|
146
|
+
id: 'managed',
|
|
147
|
+
config: { behaviour: 'buttonFirst', hybridWidth: null, maxMobileWidth: 640, manageHistoryState: false },
|
|
148
|
+
rootEl: document.createElement('div'),
|
|
149
|
+
loadApp: jest.fn(),
|
|
150
|
+
removeApp: jest.fn(),
|
|
151
|
+
_isHidden: false
|
|
152
|
+
}
|
|
153
|
+
historyManager.register(managedComponent)
|
|
154
|
+
queryString.getQueryParam.mockReturnValue('managed')
|
|
155
|
+
|
|
156
|
+
window.dispatchEvent(popstateEvent)
|
|
157
|
+
|
|
158
|
+
expect(managedComponent.loadApp).not.toHaveBeenCalled()
|
|
159
|
+
})
|
|
160
|
+
|
|
144
161
|
it('unregisters component', () => {
|
|
145
162
|
historyManager.register(component1)
|
|
146
163
|
component1.rootEl.appendChild(document.createElement('div'))
|
package/src/config/appConfig.js
CHANGED
|
@@ -24,7 +24,7 @@ export const defaultAppConfig = {
|
|
|
24
24
|
label: 'Exit',
|
|
25
25
|
iconId: 'close',
|
|
26
26
|
onClick: (_e, { services }) => services.closeApp(),
|
|
27
|
-
excludeWhen: ({ appConfig, appState }) => !appConfig.hasExitButton || !
|
|
27
|
+
excludeWhen: ({ appConfig, appState }) => !appConfig.hasExitButton || !appState.isFullscreen,
|
|
28
28
|
mobile: exitButtonSlots,
|
|
29
29
|
tablet: exitButtonSlots,
|
|
30
30
|
desktop: exitButtonSlots
|
|
@@ -115,6 +115,7 @@ export const defaultButtonConfig = {
|
|
|
115
115
|
// Used by addPanel
|
|
116
116
|
export const defaultPanelConfig = {
|
|
117
117
|
label: 'Panel',
|
|
118
|
+
focus: true,
|
|
118
119
|
mobile: {
|
|
119
120
|
slot: 'drawer',
|
|
120
121
|
open: true,
|
|
@@ -23,28 +23,18 @@ describe('defaultAppConfig', () => {
|
|
|
23
23
|
|
|
24
24
|
// --- EXIT BUTTON (Line 27 Coverage) ---
|
|
25
25
|
it('covers all branches of exitBtn excludeWhen', () => {
|
|
26
|
-
const config = { hasExitButton: true, mapViewParamKey: 'view' }
|
|
27
|
-
|
|
28
26
|
expect(exitBtn.excludeWhen({
|
|
29
|
-
appConfig: {
|
|
27
|
+
appConfig: { hasExitButton: false },
|
|
30
28
|
appState: { isFullscreen: true }
|
|
31
29
|
})).toBe(true)
|
|
32
30
|
|
|
33
|
-
window.history.pushState({}, '', '?view=map')
|
|
34
31
|
expect(exitBtn.excludeWhen({
|
|
35
|
-
appConfig:
|
|
32
|
+
appConfig: { hasExitButton: true },
|
|
36
33
|
appState: { isFullscreen: false }
|
|
37
34
|
})).toBe(true)
|
|
38
35
|
|
|
39
|
-
window.history.pushState({}, '', '?wrong=param')
|
|
40
|
-
expect(exitBtn.excludeWhen({
|
|
41
|
-
appConfig: config,
|
|
42
|
-
appState: { isFullscreen: true }
|
|
43
|
-
})).toBe(true)
|
|
44
|
-
|
|
45
|
-
window.history.pushState({}, '', '?view=map')
|
|
46
36
|
expect(exitBtn.excludeWhen({
|
|
47
|
-
appConfig:
|
|
37
|
+
appConfig: { hasExitButton: true },
|
|
48
38
|
appState: { isFullscreen: true }
|
|
49
39
|
})).toBe(false)
|
|
50
40
|
})
|
package/src/config/defaults.js
CHANGED
|
@@ -16,7 +16,7 @@ const defaults = {
|
|
|
16
16
|
containerHeight: '600px',
|
|
17
17
|
deviceNotSupportedText: 'Your device is not supported. A map is available with a more up-to-date browser or device.',
|
|
18
18
|
enableFullscreen: false,
|
|
19
|
-
enableZoomControls:
|
|
19
|
+
enableZoomControls: true,
|
|
20
20
|
genericErrorText: 'There was a problem loading the map. Please try again later.',
|
|
21
21
|
hasExitButton: false,
|
|
22
22
|
hybridWidth: null, // Defaults to maxMobileWidth if not set
|
|
@@ -24,6 +24,7 @@ const defaults = {
|
|
|
24
24
|
mapLabel: 'Interactive map',
|
|
25
25
|
mapProvider: null,
|
|
26
26
|
mapSize: 'small',
|
|
27
|
+
manageHistoryState: true,
|
|
27
28
|
mapViewParamKey: 'mv',
|
|
28
29
|
maxMobileWidth: 640,
|
|
29
30
|
minDesktopWidth: 835,
|
package/src/config/events.js
CHANGED
|
@@ -62,32 +62,34 @@ export const EVENTS = {
|
|
|
62
62
|
APP_READY: 'app:ready',
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* Emitted when the map application
|
|
65
|
+
* Emitted when the map application has opened and is visible.
|
|
66
66
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* (e.g. resizing to mobile and opening the map).
|
|
67
|
+
* Fired after initial load (`loadApp`) and when the app is shown again after being hidden (`showApp`).
|
|
68
|
+
* Subscribe to this event to react whenever the map becomes visible to the user.
|
|
70
69
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* Payload: `{ statePreserved: boolean }` — `true` if the map state was preserved from a previous session.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* map.on(EVENTS.APP_OPENED, ({ statePreserved }) => {
|
|
74
|
+
* console.log('Map opened, state preserved:', statePreserved)
|
|
75
|
+
* })
|
|
75
76
|
*/
|
|
76
|
-
|
|
77
|
+
APP_OPENED: 'app:opened',
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* Emitted when the map application
|
|
80
|
+
* Emitted when the map application has closed and is no longer visible.
|
|
81
|
+
*
|
|
82
|
+
* Fired when the app is hidden (`hideApp`) or removed (`removeApp`).
|
|
83
|
+
* Subscribe to this event to react whenever the map is closed.
|
|
80
84
|
*
|
|
81
|
-
*
|
|
82
|
-
* (e.g. visible on desktop) but then becomes hidden
|
|
83
|
-
* (e.g. resizing to mobile or closing the map view).
|
|
85
|
+
* Payload: `{ statePreserved: boolean }` — `true` if the map state was preserved (i.e. can be restored).
|
|
84
86
|
*
|
|
85
|
-
* @
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
87
|
+
* @example
|
|
88
|
+
* map.on(EVENTS.APP_CLOSED, ({ statePreserved }) => {
|
|
89
|
+
* console.log('Map closed, state preserved:', statePreserved)
|
|
90
|
+
* })
|
|
89
91
|
*/
|
|
90
|
-
|
|
92
|
+
APP_CLOSED: 'app:closed',
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
95
|
* Emitted when a panel is opened.
|
|
@@ -202,9 +204,6 @@ export const EVENTS = {
|
|
|
202
204
|
*/
|
|
203
205
|
MAP_CLICK: 'map:click',
|
|
204
206
|
|
|
205
|
-
/** Emitted when the user exits the map (e.g., via the close button). */
|
|
206
|
-
MAP_EXIT: 'map:exit',
|
|
207
|
-
|
|
208
207
|
/** Emitted when the map is destroyed. Payload: { mapId: string } */
|
|
209
208
|
MAP_DESTROY: 'map:destroy'
|
|
210
209
|
}
|
package/src/services/closeApp.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
// src/services/closeApp.js
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export function closeApp (mapId, handleExitClick, eventBus) {
|
|
5
|
-
eventBus.emit(events.MAP_EXIT, { mapId })
|
|
6
|
-
|
|
7
|
-
if (history.state?.isBack) {
|
|
8
|
-
history.back()
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
|
|
2
|
+
export function closeApp (_mapId, handleExitClick) {
|
|
12
3
|
handleExitClick()
|
|
13
4
|
}
|
|
@@ -1,49 +1,9 @@
|
|
|
1
1
|
import { closeApp } from './closeApp'
|
|
2
2
|
|
|
3
3
|
describe('closeApp', () => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
handleExitClickMock = jest.fn()
|
|
9
|
-
mockEventBus = {
|
|
10
|
-
emit: jest.fn(),
|
|
11
|
-
on: jest.fn(),
|
|
12
|
-
off: jest.fn()
|
|
13
|
-
}
|
|
14
|
-
jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
jest.restoreAllMocks()
|
|
19
|
-
jest.clearAllMocks()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('calls history.back() when history.state.isBack is true', () => {
|
|
23
|
-
Object.defineProperty(history, 'state', {
|
|
24
|
-
value: { isBack: true },
|
|
25
|
-
writable: true,
|
|
26
|
-
configurable: true
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
closeApp('map-123', handleExitClickMock, mockEventBus)
|
|
30
|
-
|
|
31
|
-
expect(mockEventBus.emit).toHaveBeenCalledWith('map:exit', { mapId: 'map-123' })
|
|
32
|
-
expect(history.back).toHaveBeenCalled()
|
|
33
|
-
expect(handleExitClickMock).not.toHaveBeenCalled()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('calls handleExitClick when history.state.isBack is not true', () => {
|
|
37
|
-
Object.defineProperty(history, 'state', {
|
|
38
|
-
value: null,
|
|
39
|
-
writable: true,
|
|
40
|
-
configurable: true
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
closeApp('map-123', handleExitClickMock, mockEventBus)
|
|
44
|
-
|
|
45
|
-
expect(mockEventBus.emit).toHaveBeenCalledWith('map:exit', { mapId: 'map-123' })
|
|
46
|
-
expect(history.back).not.toHaveBeenCalled()
|
|
4
|
+
it('calls handleExitClick', () => {
|
|
5
|
+
const handleExitClickMock = jest.fn()
|
|
6
|
+
closeApp('map-123', handleExitClickMock)
|
|
47
7
|
expect(handleExitClickMock).toHaveBeenCalled()
|
|
48
8
|
})
|
|
49
9
|
})
|
package/src/types.js
CHANGED
|
@@ -591,7 +591,7 @@
|
|
|
591
591
|
* @property {boolean} [enableFullscreen=false]
|
|
592
592
|
* Whether a toggle fullscreen button is displayed.
|
|
593
593
|
*
|
|
594
|
-
* @property {boolean} [enableZoomControls=
|
|
594
|
+
* @property {boolean} [enableZoomControls=true]
|
|
595
595
|
* Whether zoom control buttons are displayed.
|
|
596
596
|
*
|
|
597
597
|
* @property {[number, number, number, number]} [extent]
|
|
@@ -661,6 +661,11 @@
|
|
|
661
661
|
* @property {PluginDescriptor[]} [plugins]
|
|
662
662
|
* Plugins to load.
|
|
663
663
|
*
|
|
664
|
+
* @property {boolean} [manageHistoryState=true]
|
|
665
|
+
* Whether the library should manage browser history state (pushState/replaceState) when opening and closing the map.
|
|
666
|
+
* Set to `false` in SPA frameworks (e.g. React Router, Docusaurus) that intercept history API calls.
|
|
667
|
+
* When `false`, listen to `APP_OPENED` and `APP_CLOSED` events and manage navigation in your router instead.
|
|
668
|
+
*
|
|
664
669
|
* @property {boolean} [preserveStateOnClose=false]
|
|
665
670
|
* Whether to preserve the map state when closed via back button or exit button.
|
|
666
671
|
* When true, the map is hidden but not destroyed, preserving markers, zoom, etc.
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Reads center and zoom for a given map ID from a URL search string.
|
|
3
|
+
* The `search` parameter is accepted explicitly to keep this function pure and testable.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} id - Map instance ID.
|
|
6
|
+
* @param {string} search - URL search string (e.g. `window.location.search`).
|
|
7
|
+
* @returns {{ center: [number, number], zoom: number } | null}
|
|
8
|
+
*/
|
|
2
9
|
const getMapStateFromURL = (id, search) => {
|
|
3
10
|
const params = new URLSearchParams(search)
|
|
4
11
|
const centerStr = params.get(`${id}:center`)
|
|
@@ -11,30 +18,61 @@ const getMapStateFromURL = (id, search) => {
|
|
|
11
18
|
return { center: [lng, lat], zoom }
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Persists map center and zoom into the page URL without triggering navigation.
|
|
23
|
+
*
|
|
24
|
+
* Existing query parameters are preserved. Parameters for this map ID are
|
|
25
|
+
* replaced if already present. Builds the URL manually to avoid percent-encoding
|
|
26
|
+
* colons and commas that URLSearchParams would otherwise encode.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} id - Map instance ID, used as a namespace prefix for the params.
|
|
29
|
+
* @param {{ center?: [number, number], zoom?: number }} state - Map state to write.
|
|
30
|
+
* @param {string} [currentHref] - URL to update. Defaults to `window.location.href`.
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
14
33
|
const setMapStateInURL = (id, state, currentHref = window.location.href) => {
|
|
15
|
-
// Use the passed href or the global one
|
|
16
34
|
const url = new URL(currentHref || 'http://localhost')
|
|
17
|
-
|
|
35
|
+
|
|
36
|
+
const newKeys = new Set()
|
|
18
37
|
const newParams = []
|
|
19
38
|
|
|
20
39
|
if (state.center) {
|
|
21
|
-
|
|
40
|
+
const key = `${id}:center`
|
|
41
|
+
newKeys.add(key)
|
|
42
|
+
newParams.push(`${key}=${state.center[0]},${state.center[1]}`)
|
|
22
43
|
}
|
|
23
44
|
if (state.zoom != null) {
|
|
24
|
-
|
|
45
|
+
const key = `${id}:zoom`
|
|
46
|
+
newKeys.add(key)
|
|
47
|
+
newParams.push(`${key}=${state.zoom}`)
|
|
25
48
|
}
|
|
26
49
|
|
|
27
|
-
const
|
|
28
|
-
|
|
50
|
+
const existingParams = []
|
|
51
|
+
url.searchParams.forEach((value, key) => {
|
|
52
|
+
if (!newKeys.has(key)) {
|
|
53
|
+
existingParams.push(`${key}=${value}`)
|
|
54
|
+
}
|
|
29
55
|
})
|
|
30
56
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
57
|
+
const allParams = [...existingParams, ...newParams].join('&')
|
|
58
|
+
const search = allParams ? '?' + allParams : ''
|
|
59
|
+
const newUrl = `${url.origin}${url.pathname}${search}${url.hash}`
|
|
33
60
|
window.history.replaceState(window.history.state, '', newUrl)
|
|
34
61
|
}
|
|
35
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Returns the initial map view state, preferring any saved state from the URL.
|
|
65
|
+
*
|
|
66
|
+
* Resolution order:
|
|
67
|
+
* 1. Center/zoom encoded in the URL search string for this map ID.
|
|
68
|
+
* 2. A `bounds` value if provided (used to fit the view on load).
|
|
69
|
+
* 3. The configured `center` and `zoom` defaults.
|
|
70
|
+
*
|
|
71
|
+
* @param {{ id: string, center: [number, number], zoom: number, bounds?: any }} config - Map config.
|
|
72
|
+
* @param {string} [search] - URL search string. Defaults to `window.location.search`.
|
|
73
|
+
* @returns {{ center: [number, number], zoom: number } | { bounds: any }}
|
|
74
|
+
*/
|
|
36
75
|
const getInitialMapState = ({ id, center, zoom, bounds }, search = window.location.search) => {
|
|
37
|
-
// Pass search string down to the internal function
|
|
38
76
|
const savedState = getMapStateFromURL(id, search)
|
|
39
77
|
if (savedState) {
|
|
40
78
|
return {
|