mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
refactor(panels): read panel state from the panel chrome store
PanelChrome, its collapse-preference restore and the phone shell read a panel's disclosure state back from the gev-panel-chrome store (_shownPanel), falling back to the markup's classes before the panel is first presented, instead of reading the classes v-panel-state draws. The share link and the phone shell share one rule for a panel the person keeps closed (prefersCollapsed). The rails, the accordion and the hover disclosure still read the classes, which the directive draws in the same task as their layout.
This commit is contained in:
1 parent
4c92f517db
commit
9ed9098593
12 files changed
+117
-49
No files matched your search
@@ -124,7 +124,13 @@ a feature supplying only its accent.
|
|||||||
`accordion-collapsed`): PanelChrome, the rails and the accordion present
|
`accordion-collapsed`): PanelChrome, the rails and the accordion present
|
||||||
it rather than writing classes. The directive applies it in a synchronous
|
it rather than writing classes. The directive applies it in a synchronous
|
||||||
watcher, not on Vue's next render, because the rails measure a panel in
|
watcher, not on Vue's next render, because the rails measure a panel in
|
||||||
the same task that opens or closes it. Done.
|
the same task that opens or closes it. PanelChrome and its preference
|
||||||
|
restore, and the phone shell, read a panel's state back from the store
|
||||||
|
(`_shownPanel`; the markup's classes before its first presentation), so
|
||||||
|
once a panel is presented a class written outside the store no longer
|
||||||
|
counts. The rails, the accordion and the hover disclosure
|
||||||
|
(`panelDisclosure.js`), which act within the same task as a layout, read
|
||||||
|
the classes the directive has just drawn. Done.
|
||||||
3. **Panels.** Each template and its wiring become a component, keeping ids,
|
3. **Panels.** Each template and its wiring become a component, keeping ids,
|
||||||
classes and CSS; text a template renders with `t()` drops its `data-i18n`
|
classes and CSS; text a template renders with `t()` drops its `data-i18n`
|
||||||
marker. The page places each view with a
|
marker. The page places each view with a
|
||||||
|
|||||||
@@ -696,7 +696,7 @@ test('fresh Cockpit entry temporarily collapses map panels and exit restores the
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
callback[1],
|
callback[1],
|
||||||
/_cockpitPanelRestore\.set\(\s*panelId,\s*panel\.classList\.contains\('collapsed'\),?\s*\)/,
|
/_cockpitPanelRestore\.set\(\s*panelId,\s*this\._shownPanel\(panel\)\.collapsed,?\s*\)/,
|
||||||
"entry must remember each panel's exact collapsed state before hiding it",
|
"entry must remember each panel's exact collapsed state before hiding it",
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
@@ -757,7 +757,7 @@ test('real disclosure changes reconsider only their own temporary panel lane', (
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
collapse,
|
collapse,
|
||||||
/classList\.contains\('collapsed'\) === nextCollapsed\s*&&\s*!wasAutoCollapsed[\s\S]*?return;/,
|
/this\._shownPanel\(panelEl\)\.collapsed === nextCollapsed\s*&&\s*!wasAutoCollapsed[\s\S]*?return;/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
collapse,
|
collapse,
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function harness({ hidden = false, selected = true, noChips = false } = {}) {
|
|||||||
window.performance = { now: () => now };
|
window.performance = { now: () => now };
|
||||||
document.defaultView = window;
|
document.defaultView = window;
|
||||||
const methods = new Function('createHoverDisclosure', 'collapsePanelOnEscape', 'document', 'window', 'clearTimeout', 'performance', 'requestAnimationFrame', 't', 'msg', 'readPanelState',
|
const methods = new Function('createHoverDisclosure', 'collapsePanelOnEscape', 'document', 'window', 'clearTimeout', 'performance', 'requestAnimationFrame', 't', 'msg', 'readPanelState',
|
||||||
`return ({${shellMethod('_initAutoHoverPanel').toString()},\n${shellMethod('_collapsePanelOnEscape').toString()},\n${shellMethod('setPanelCollapsed').toString()},\n${shellMethod('_presentPanel').toString()}});`)(
|
`return ({${shellMethod('_initAutoHoverPanel').toString()},\n${shellMethod('_collapsePanelOnEscape').toString()},\n${shellMethod('setPanelCollapsed').toString()},\n${shellMethod('_presentPanel').toString()},\n${shellMethod('_shownPanel').toString()}});`)(
|
||||||
createHoverDisclosure, collapsePanelOnEscape, document, window, (id) => timers.delete(id), { now: () => now }, () => {}, t, msg, readPanelState,
|
createHoverDisclosure, collapsePanelOnEscape, document, window, (id) => timers.delete(id), { now: () => now }, () => {}, t, msg, readPanelState,
|
||||||
);
|
);
|
||||||
const saves = [];
|
const saves = [];
|
||||||
@@ -248,7 +248,7 @@ test('opening the unpinned Location sibling cancels the Map Source request', ()
|
|||||||
assert.equal(h.timers.size, 0); h.drain(); assert.equal(h.calls(), 0);
|
assert.equal(h.timers.size, 0); h.drain(); assert.equal(h.calls(), 0);
|
||||||
});
|
});
|
||||||
test('a pinned Map Source tray retains its own request when the sibling opens', () => {
|
test('a pinned Map Source tray retains its own request when the sibling opens', () => {
|
||||||
const h = harness(); h.panel.classes.add('dock-pinned'); h.key();
|
const h = harness(); h.manager._presentPanel(h.panel, { pinned: true }); h.key();
|
||||||
h.manager.setPanelCollapsed('location-bar', false); h.tick();
|
h.manager.setPanelCollapsed('location-bar', false); h.tick();
|
||||||
assert.equal(h.panel.classList.contains('collapsed'), false);
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
||||||
assert.equal(h.document.activeElement, h.active);
|
assert.equal(h.document.activeElement, h.active);
|
||||||
@@ -357,12 +357,13 @@ test('Location keyboard opening cancels the unpinned sibling and cannot revive i
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Location keyboard opening preserves a pinned sibling and a pinned Location survives sibling opening', () => {
|
test('Location keyboard opening preserves a pinned sibling and a pinned Location survives sibling opening', () => {
|
||||||
const h = harness(); h.panel.classes.add('dock-pinned'); h.key(); h.tick();
|
// Pins go through the store, as the dock's pin button does.
|
||||||
|
const h = harness(); h.manager._presentPanel(h.panel, { pinned: true }); h.key(); h.tick();
|
||||||
h.focus(h.locationDisclosure); h.locationKey(); h.drain();
|
h.focus(h.locationDisclosure); h.locationKey(); h.drain();
|
||||||
assert.equal(h.panel.classList.contains('collapsed'), false);
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
||||||
assert.equal(h.location.classList.contains('collapsed'), false);
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
||||||
assert.equal(h.document.activeElement, h.locationDisclosure);
|
assert.equal(h.document.activeElement, h.locationDisclosure);
|
||||||
h.location.classes.add('dock-pinned');
|
h.manager._presentPanel(h.location, { pinned: true });
|
||||||
h.manager.setPanelCollapsed('control-panel', true);
|
h.manager.setPanelCollapsed('control-panel', true);
|
||||||
h.focus(h.disclosure); h.key(); h.tick();
|
h.focus(h.disclosure); h.key(); h.tick();
|
||||||
assert.equal(h.location.classList.contains('collapsed'), false);
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ test('share-panel state excludes responsive collapse and preserves recipient pre
|
|||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
ui,
|
||||||
/const collapsed =[\s\S]*?panelEl\.classList\.contains\('layout-auto-collapsed'\)[\s\S]*?panelEl\.classList\.contains\('accordion-collapsed'\)[\s\S]*?\? false\s*: panelEl\.classList\.contains\('collapsed'\);/,
|
/const shown = this\._shownPanel\(panelEl\);\s*const collapsed = prefersCollapsed\(shown\);/,
|
||||||
'responsive auto-collapse must serialize the explicit expanded preference',
|
'responsive auto-collapse must serialize the explicit expanded preference',
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
|
|||||||
const { PanelPositionControls } =
|
const { PanelPositionControls } =
|
||||||
await import('../ui/panelPositionControls.js');
|
await import('../ui/panelPositionControls.js');
|
||||||
const { presentPanelLocally } = await import('../testSupport/panelState.mjs');
|
const { presentPanelLocally } = await import('../testSupport/panelState.mjs');
|
||||||
|
const { readPanelState } = await import('../ui/rail/panelState.js');
|
||||||
const html = await readFile(
|
const html = await readFile(
|
||||||
path.join(root, 'src/ui/views/panels/CctvPanel.vue'),
|
path.join(root, 'src/ui/views/panels/CctvPanel.vue'),
|
||||||
'utf8',
|
'utf8',
|
||||||
@@ -112,6 +113,7 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
|
|||||||
try {
|
try {
|
||||||
const controls = new PanelPositionControls({
|
const controls = new PanelPositionControls({
|
||||||
presentPanel: presentPanelLocally,
|
presentPanel: presentPanelLocally,
|
||||||
|
shownPanel: readPanelState,
|
||||||
layoutRightPanels: () => {},
|
layoutRightPanels: () => {},
|
||||||
syncCctvPanelViewport: () => {},
|
syncCctvPanelViewport: () => {},
|
||||||
showToast: () => {},
|
showToast: () => {},
|
||||||
|
|||||||
@@ -858,6 +858,7 @@ export class StyleManager extends ShellFacade {
|
|||||||
this._mobileShell = initMobileShell({
|
this._mobileShell = initMobileShell({
|
||||||
setPanelCollapsed: (panelId, collapsed, options) =>
|
setPanelCollapsed: (panelId, collapsed, options) =>
|
||||||
this._panelChrome.setPanelCollapsed(panelId, collapsed, options),
|
this._panelChrome.setPanelCollapsed(panelId, collapsed, options),
|
||||||
|
readPanel: (panel) => this._panelChrome._shownPanel(panel),
|
||||||
onSheetsClosed: () => this._syncShareState(),
|
onSheetsClosed: () => this._syncShareState(),
|
||||||
onTabOpened: (sheet) => {
|
onTabOpened: (sheet) => {
|
||||||
void this._onMobileTabOpened(sheet);
|
void this._onMobileTabOpened(sheet);
|
||||||
|
|||||||
+14
-11
@@ -13,6 +13,10 @@
|
|||||||
|
|
||||||
import { EXCLUSIVE_SURFACE_CLASSES } from '../exclusiveSurfaces.js';
|
import { EXCLUSIVE_SURFACE_CLASSES } from '../exclusiveSurfaces.js';
|
||||||
import { msg } from '../i18n/index.js';
|
import { msg } from '../i18n/index.js';
|
||||||
|
import {
|
||||||
|
prefersCollapsed as prefersStateCollapsed,
|
||||||
|
readPanelState,
|
||||||
|
} from './rail/panelState.js';
|
||||||
|
|
||||||
export const MOBILE_SHELL_MEDIA_QUERY = '(max-width: 720px)';
|
export const MOBILE_SHELL_MEDIA_QUERY = '(max-width: 720px)';
|
||||||
export const MOBILE_SHELL_CLASS = 'mobile-shell';
|
export const MOBILE_SHELL_CLASS = 'mobile-shell';
|
||||||
@@ -97,6 +101,8 @@ function currentSheetOf(body) {
|
|||||||
* @param {(query: string) => MediaQueryList} [input.matchMedia]
|
* @param {(query: string) => MediaQueryList} [input.matchMedia]
|
||||||
* @param {typeof MutationObserver} [input.MutationObserverImpl]
|
* @param {typeof MutationObserver} [input.MutationObserverImpl]
|
||||||
* @param {typeof ResizeObserver} [input.ResizeObserverImpl]
|
* @param {typeof ResizeObserver} [input.ResizeObserverImpl]
|
||||||
|
* @param {(panel: Element) => object} [input.readPanel] A panel's state as its
|
||||||
|
* owner presented it; its classes by default.
|
||||||
* @param {(callback: () => void) => number} [input.requestFrame]
|
* @param {(callback: () => void) => number} [input.requestFrame]
|
||||||
* @param {() => void} [input.onSheetsClosed] Runs after the last sheet closes
|
* @param {() => void} [input.onSheetsClosed] Runs after the last sheet closes
|
||||||
* and its panels are put back (e.g. to refresh the share link).
|
* and its panels are put back (e.g. to refresh the share link).
|
||||||
@@ -112,6 +118,7 @@ function currentSheetOf(body) {
|
|||||||
*/
|
*/
|
||||||
export function initMobileShell({
|
export function initMobileShell({
|
||||||
setPanelCollapsed,
|
setPanelCollapsed,
|
||||||
|
readPanel = readPanelState,
|
||||||
documentRef = globalThis.document,
|
documentRef = globalThis.document,
|
||||||
matchMedia = globalThis.matchMedia?.bind(globalThis),
|
matchMedia = globalThis.matchMedia?.bind(globalThis),
|
||||||
MutationObserverImpl = globalThis.MutationObserver,
|
MutationObserverImpl = globalThis.MutationObserver,
|
||||||
@@ -182,18 +189,14 @@ export function initMobileShell({
|
|||||||
: null;
|
: null;
|
||||||
removers.push(() => resize?.disconnect());
|
removers.push(() => resize?.disconnect());
|
||||||
|
|
||||||
const isCollapsed = (panelId) =>
|
const isCollapsed = (panelId) => {
|
||||||
element(panelId)?.classList.contains('collapsed') ?? true;
|
const panel = element(panelId);
|
||||||
// The user's preference, as share links record it: a collapse that layout
|
return panel ? readPanel(panel).collapsed : true;
|
||||||
// applies only for space (Tactical, Cyber) still counts as expanded.
|
};
|
||||||
|
// The user's preference, as share links record it.
|
||||||
const prefersCollapsed = (panelId) => {
|
const prefersCollapsed = (panelId) => {
|
||||||
const classes = element(panelId)?.classList;
|
const panel = element(panelId);
|
||||||
if (!classes) return true;
|
return panel ? prefersStateCollapsed(readPanel(panel)) : true;
|
||||||
return (
|
|
||||||
classes.contains('collapsed') &&
|
|
||||||
!classes.contains('layout-auto-collapsed') &&
|
|
||||||
!classes.contains('accordion-collapsed')
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelStates = () =>
|
const panelStates = () =>
|
||||||
|
|||||||
+27
-26
@@ -1,5 +1,5 @@
|
|||||||
import { UiLifetime } from './uiLifetime.js';
|
import { UiLifetime } from './uiLifetime.js';
|
||||||
import { readPanelState } from './rail/panelState.js';
|
import { prefersCollapsed, readPanelState } from './rail/panelState.js';
|
||||||
import { usePanelChromeStore } from './stores/panelChrome.js';
|
import { usePanelChromeStore } from './stores/panelChrome.js';
|
||||||
import { applicationPinia } from './vueRuntime.js';
|
import { applicationPinia } from './vueRuntime.js';
|
||||||
import { displayPanelScroller } from './displayPanelScroll.js';
|
import { displayPanelScroller } from './displayPanelScroll.js';
|
||||||
@@ -83,6 +83,7 @@ export class PanelChrome {
|
|||||||
this._cockpitContextCollapsedForDataPanel = false;
|
this._cockpitContextCollapsedForDataPanel = false;
|
||||||
this._panelPosition = new PanelPositionControls({
|
this._panelPosition = new PanelPositionControls({
|
||||||
presentPanel: (panel, patch) => this._presentPanel(panel, patch),
|
presentPanel: (panel, patch) => this._presentPanel(panel, patch),
|
||||||
|
shownPanel: (panel) => this._shownPanel(panel),
|
||||||
showToast: (message) => this._showToast(message),
|
showToast: (message) => this._showToast(message),
|
||||||
});
|
});
|
||||||
this._panelLayout = new PanelLayoutController({
|
this._panelLayout = new PanelLayoutController({
|
||||||
@@ -202,7 +203,7 @@ export class PanelChrome {
|
|||||||
if (this._disposed || !this._disclosurePanelIds?.has(panelId)) return;
|
if (this._disposed || !this._disclosurePanelIds?.has(panelId)) return;
|
||||||
const panel = document.getElementById(panelId);
|
const panel = document.getElementById(panelId);
|
||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
this.setPanelCollapsed(panelId, !panel.classList.contains('collapsed'), {
|
this.setPanelCollapsed(panelId, !this._shownPanel(panel).collapsed, {
|
||||||
explicit: true,
|
explicit: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -216,9 +217,7 @@ export class PanelChrome {
|
|||||||
const panelEl = document.getElementById(panelId);
|
const panelEl = document.getElementById(panelId);
|
||||||
if (!panelEl) return undefined;
|
if (!panelEl) return undefined;
|
||||||
const shouldPin =
|
const shouldPin =
|
||||||
typeof pin === 'boolean'
|
typeof pin === 'boolean' ? pin : !this._shownPanel(panelEl).pinned;
|
||||||
? pin
|
|
||||||
: !panelEl.classList.contains('dock-pinned');
|
|
||||||
// The tray pinned last draws on top; unpinning hands the top to the
|
// The tray pinned last draws on top; unpinning hands the top to the
|
||||||
// other tray if it is pinned.
|
// other tray if it is pinned.
|
||||||
this._presentPanel(panelEl, { pinned: shouldPin, pinnedTop: shouldPin });
|
this._presentPanel(panelEl, { pinned: shouldPin, pinnedTop: shouldPin });
|
||||||
@@ -227,7 +226,7 @@ export class PanelChrome {
|
|||||||
const other = document.getElementById(otherId);
|
const other = document.getElementById(otherId);
|
||||||
if (!other) continue;
|
if (!other) continue;
|
||||||
this._presentPanel(other, {
|
this._presentPanel(other, {
|
||||||
pinnedTop: !shouldPin && other.classList.contains('dock-pinned'),
|
pinnedTop: !shouldPin && this._shownPanel(other).pinned,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (shouldPin) {
|
if (shouldPin) {
|
||||||
@@ -329,14 +328,22 @@ export class PanelChrome {
|
|||||||
? document.getElementById(panelOrId)
|
? document.getElementById(panelOrId)
|
||||||
: panelOrId;
|
: panelOrId;
|
||||||
if (!panelEl?.id) return;
|
if (!panelEl?.id) return;
|
||||||
const shown =
|
|
||||||
this._chromeStore.panels[panelEl.id] ?? readPanelState(panelEl);
|
|
||||||
this._chromeStore.publish(this._chromeToken, panelEl.id, {
|
this._chromeStore.publish(this._chromeToken, panelEl.id, {
|
||||||
...shown,
|
...this._shownPanel(panelEl),
|
||||||
...patch,
|
...patch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A panel's state as PanelChrome presented it, or its markup's before the
|
||||||
|
* first presentation.
|
||||||
|
* @param {Element} panelEl
|
||||||
|
* @returns {import('./stores/panelChrome.js').PanelState}
|
||||||
|
*/
|
||||||
|
_shownPanel(panelEl) {
|
||||||
|
return this._chromeStore.panels[panelEl.id] ?? readPanelState(panelEl);
|
||||||
|
}
|
||||||
|
|
||||||
_buildSharePanelState() {
|
_buildSharePanelState() {
|
||||||
const specs = [];
|
const specs = [];
|
||||||
for (const spec of SHARE_PANEL_STATE_SPECS) {
|
for (const spec of SHARE_PANEL_STATE_SPECS) {
|
||||||
@@ -344,18 +351,14 @@ export class PanelChrome {
|
|||||||
if (!panelEl) continue;
|
if (!panelEl) continue;
|
||||||
// Responsive auto-collapse is presentation only; the recipient should
|
// Responsive auto-collapse is presentation only; the recipient should
|
||||||
// restore the user's explicit expanded preference at its own viewport.
|
// restore the user's explicit expanded preference at its own viewport.
|
||||||
const collapsed =
|
const shown = this._shownPanel(panelEl);
|
||||||
panelEl.classList.contains('layout-auto-collapsed') ||
|
const collapsed = prefersCollapsed(shown);
|
||||||
panelEl.classList.contains('accordion-collapsed')
|
|
||||||
? false
|
|
||||||
: panelEl.classList.contains('collapsed');
|
|
||||||
// A panel the phone shell only borrowed for a sheet shares its own state.
|
// A panel the phone shell only borrowed for a sheet shares its own state.
|
||||||
const entry = {
|
const entry = {
|
||||||
id: spec.id,
|
id: spec.id,
|
||||||
collapsed: this.readSharedCollapsed(spec.id) ?? collapsed,
|
collapsed: this.readSharedCollapsed(spec.id) ?? collapsed,
|
||||||
};
|
};
|
||||||
if (spec.pinnable)
|
if (spec.pinnable) entry.pinned = shown.pinned;
|
||||||
entry.pinned = panelEl.classList.contains('dock-pinned');
|
|
||||||
specs.push(entry);
|
specs.push(entry);
|
||||||
}
|
}
|
||||||
return specs.length ? { specs } : null;
|
return specs.length ? { specs } : null;
|
||||||
@@ -402,9 +405,7 @@ export class PanelChrome {
|
|||||||
if (explicit && !restore)
|
if (explicit && !restore)
|
||||||
this.shareLinkManager?.claimRestoreLane?.('panel', panelId);
|
this.shareLinkManager?.claimRestoreLane?.('panel', panelId);
|
||||||
const nextCollapsed = Boolean(collapsed);
|
const nextCollapsed = Boolean(collapsed);
|
||||||
const wasAutoCollapsed =
|
const wasAutoCollapsed = Boolean(this._shownPanel(panelEl).auto);
|
||||||
panelEl.classList.contains('layout-auto-collapsed') ||
|
|
||||||
panelEl.classList.contains('accordion-collapsed');
|
|
||||||
// One panel is open per desktop side: opening a side panel, whether the
|
// One panel is open per desktop side: opening a side panel, whether the
|
||||||
// user asked or a feature revealed it (Street Level's viewer, a selected
|
// user asked or a feature revealed it (Street Level's viewer, a selected
|
||||||
// radio station), owns the whole accordion, including peers that were
|
// radio station), owns the whole accordion, including peers that were
|
||||||
@@ -466,12 +467,12 @@ export class PanelChrome {
|
|||||||
}
|
}
|
||||||
// Reveal Radio's owner before the no-op check: the nested section may
|
// Reveal Radio's owner before the no-op check: the nested section may
|
||||||
// already be expanded while its Context parent is closed.
|
// already be expanded while its Context parent is closed.
|
||||||
|
const contextPanel = document.getElementById('global-context-panel');
|
||||||
if (
|
if (
|
||||||
!nextCollapsed &&
|
!nextCollapsed &&
|
||||||
panelId === 'radio-panel' &&
|
panelId === 'radio-panel' &&
|
||||||
document
|
contextPanel &&
|
||||||
.getElementById('global-context-panel')
|
this._shownPanel(contextPanel).collapsed
|
||||||
?.classList.contains('collapsed')
|
|
||||||
) {
|
) {
|
||||||
this.setPanelCollapsed('global-context-panel', false, {
|
this.setPanelCollapsed('global-context-panel', false, {
|
||||||
explicit,
|
explicit,
|
||||||
@@ -481,7 +482,7 @@ export class PanelChrome {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
panelEl.classList.contains('collapsed') === nextCollapsed &&
|
this._shownPanel(panelEl).collapsed === nextCollapsed &&
|
||||||
!wasAutoCollapsed
|
!wasAutoCollapsed
|
||||||
) {
|
) {
|
||||||
if (priorLeftOwner !== this._panelLayout._leftStackPreferredPanelId) {
|
if (priorLeftOwner !== this._panelLayout._leftStackPreferredPanelId) {
|
||||||
@@ -505,7 +506,7 @@ export class PanelChrome {
|
|||||||
}
|
}
|
||||||
if (!nextCollapsed && !restore && panelId === 'location-bar') {
|
if (!nextCollapsed && !restore && panelId === 'location-bar') {
|
||||||
const otherPanel = document.getElementById('control-panel');
|
const otherPanel = document.getElementById('control-panel');
|
||||||
if (otherPanel && !otherPanel.classList.contains('dock-pinned')) {
|
if (otherPanel && !this._shownPanel(otherPanel).pinned) {
|
||||||
this.setPanelCollapsed('control-panel', true, {
|
this.setPanelCollapsed('control-panel', true, {
|
||||||
restore,
|
restore,
|
||||||
persist,
|
persist,
|
||||||
@@ -514,7 +515,7 @@ export class PanelChrome {
|
|||||||
}
|
}
|
||||||
} else if (!nextCollapsed && !restore && panelId === 'control-panel') {
|
} else if (!nextCollapsed && !restore && panelId === 'control-panel') {
|
||||||
const otherPanel = document.getElementById('location-bar');
|
const otherPanel = document.getElementById('location-bar');
|
||||||
if (otherPanel && !otherPanel.classList.contains('dock-pinned')) {
|
if (otherPanel && !this._shownPanel(otherPanel).pinned) {
|
||||||
this.setPanelCollapsed('location-bar', true, {
|
this.setPanelCollapsed('location-bar', true, {
|
||||||
restore,
|
restore,
|
||||||
persist,
|
persist,
|
||||||
@@ -564,7 +565,7 @@ export class PanelChrome {
|
|||||||
if (panel) {
|
if (panel) {
|
||||||
this._cockpitPanelRestore.set(
|
this._cockpitPanelRestore.set(
|
||||||
panelId,
|
panelId,
|
||||||
panel.classList.contains('collapsed'),
|
this._shownPanel(panel).collapsed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setPanelCollapsed(panelId, true, {
|
this.setPanelCollapsed(panelId, true, {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** Own panel collapsed-state preferences and the one-time layout-reset notice. */
|
/** Own panel collapsed-state preferences and the one-time layout-reset notice. */
|
||||||
import { t } from '../i18n/index.js';
|
import { t } from '../i18n/index.js';
|
||||||
|
import { readPanelState } from './rail/panelState.js';
|
||||||
/** Versioned localStorage namespace prefix to invalidate stale panel layouts. */
|
/** Versioned localStorage namespace prefix to invalidate stale panel layouts. */
|
||||||
const PANEL_LAYOUT_STORAGE_VERSION = 'v6';
|
const PANEL_LAYOUT_STORAGE_VERSION = 'v6';
|
||||||
/**
|
/**
|
||||||
@@ -14,8 +15,9 @@ const RETIRED_PANEL_KEY_PREFIXES = [
|
|||||||
`godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelFloatHintShown`,
|
`godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelFloatHintShown`,
|
||||||
];
|
];
|
||||||
export class PanelPositionControls {
|
export class PanelPositionControls {
|
||||||
constructor({ presentPanel, showToast }) {
|
constructor({ presentPanel, shownPanel = readPanelState, showToast }) {
|
||||||
this._presentPanel = presentPanel;
|
this._presentPanel = presentPanel;
|
||||||
|
this._shownPanel = shownPanel;
|
||||||
this._showToast = showToast;
|
this._showToast = showToast;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ export class PanelPositionControls {
|
|||||||
_restorePanelCollapsedState(panelId, { allowStored = true } = {}) {
|
_restorePanelCollapsedState(panelId, { allowStored = true } = {}) {
|
||||||
const panelEl = document.getElementById(panelId);
|
const panelEl = document.getElementById(panelId);
|
||||||
if (!panelEl) return;
|
if (!panelEl) return;
|
||||||
let collapsed = panelEl.classList.contains('collapsed');
|
let collapsed = this._shownPanel(panelEl).collapsed;
|
||||||
let stored = null;
|
let stored = null;
|
||||||
if (allowStored) {
|
if (allowStored) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ export const vPanelState = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the person keeps a panel closed, as share links record it: a
|
||||||
|
* collapse a layout applied only for space still counts as open.
|
||||||
|
* @param {import('../stores/panelChrome.js').PanelState} state
|
||||||
|
*/
|
||||||
|
export function prefersCollapsed(state) {
|
||||||
|
return state.collapsed && !state.auto;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A panel's state as its classes draw it: the markup's state, before its
|
* A panel's state as its classes draw it: the markup's state, before its
|
||||||
* owner first presents the panel.
|
* owner first presents the panel.
|
||||||
|
|||||||
@@ -301,7 +301,11 @@ test('weather rail orders between CCTV and Context, observes and presents chrome
|
|||||||
const weather = f.nodes.get('weather-panel');
|
const weather = f.nodes.get('weather-panel');
|
||||||
stack.children = [weather, f.nodes.get('global-context-panel')];
|
stack.children = [weather, f.nodes.get('global-context-panel')];
|
||||||
const store = usePanelChromeStore(createPinia());
|
const store = usePanelChromeStore(createPinia());
|
||||||
const chrome = { _chromeStore: store, _chromeToken: store.connect({}) };
|
const chrome = {
|
||||||
|
_chromeStore: store,
|
||||||
|
_chromeToken: store.connect({}),
|
||||||
|
_shownPanel: PanelChrome.prototype._shownPanel,
|
||||||
|
};
|
||||||
const presented = [];
|
const presented = [];
|
||||||
const owner = new PanelLayoutController({
|
const owner = new PanelLayoutController({
|
||||||
readHud: () => ({}),
|
readHud: () => ({}),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { PanelChrome } from './panelChrome.js';
|
import { PanelChrome } from './panelChrome.js';
|
||||||
import { presentPanelLocally } from '../testSupport/panelState.mjs';
|
import { presentPanelLocally } from '../testSupport/panelState.mjs';
|
||||||
|
import { readPanelState } from './rail/panelState.js';
|
||||||
|
|
||||||
function fixture(theme = 'cyber') {
|
function fixture(theme = 'cyber') {
|
||||||
const nodes = new Map();
|
const nodes = new Map();
|
||||||
@@ -36,7 +37,7 @@ function fixture(theme = 'cyber') {
|
|||||||
_rightPanelStack: rail,
|
_rightPanelStack: rail,
|
||||||
_panelLayout: {},
|
_panelLayout: {},
|
||||||
_lifetime: { frame() {} },
|
_lifetime: { frame() {} },
|
||||||
_presentPanel: presentPanelLocally, _scheduleRightPanelLayout() {},
|
_presentPanel: presentPanelLocally, _shownPanel: readPanelState, _scheduleRightPanelLayout() {},
|
||||||
_scheduleLeftPanelLayout() {}, _layoutRightPanels() {}, _syncCctvPanelViewport() {},
|
_scheduleLeftPanelLayout() {}, _layoutRightPanels() {}, _syncCctvPanelViewport() {},
|
||||||
_savePanelCollapsedState: (...args) => saved.push(args),
|
_savePanelCollapsedState: (...args) => saved.push(args),
|
||||||
shareLinkManager: { claimRestoreLane: (...args) => claimed.push(args), onPanelStateChange() {} },
|
shareLinkManager: { claimRestoreLane: (...args) => claimed.push(args), onPanelStateChange() {} },
|
||||||
@@ -115,3 +116,41 @@ test('a feature revealing a side panel makes it the side owner', () => {
|
|||||||
assert.deepEqual(f.claimed, []);
|
assert.deepEqual(f.claimed, []);
|
||||||
} finally { globalThis.document = prior; }
|
} finally { globalThis.document = prior; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('PanelChrome reads a panel’s state from its store, not classes written outside it', async () => {
|
||||||
|
const { createPinia } = await import('pinia');
|
||||||
|
const { usePanelChromeStore } = await import('./stores/panelChrome.js');
|
||||||
|
const store = usePanelChromeStore(createPinia());
|
||||||
|
const owner = {
|
||||||
|
_chromeStore: store,
|
||||||
|
_chromeToken: store.connect({}),
|
||||||
|
readSharedCollapsed: () => undefined,
|
||||||
|
};
|
||||||
|
const classes = new Set(['collapsed']);
|
||||||
|
const panel = {
|
||||||
|
id: 'data-panel',
|
||||||
|
classList: { contains: (name) => classes.has(name) },
|
||||||
|
};
|
||||||
|
// Before the first presentation the markup's classes stand.
|
||||||
|
assert.deepEqual(PanelChrome.prototype._shownPanel.call(owner, panel), {
|
||||||
|
collapsed: true, pinned: false, pinnedTop: false, auto: null,
|
||||||
|
});
|
||||||
|
// The accordion closed it; a class written outside the store doesn't count.
|
||||||
|
store.publish(owner._chromeToken, 'data-panel', {
|
||||||
|
collapsed: true, pinned: false, pinnedTop: false, auto: 'accordion',
|
||||||
|
});
|
||||||
|
classes.add('dock-pinned');
|
||||||
|
const shown = PanelChrome.prototype._shownPanel.call(owner, panel);
|
||||||
|
assert.equal(shown.auto, 'accordion');
|
||||||
|
assert.equal(shown.pinned, false);
|
||||||
|
// A share link records the accordion's collapse as the person's open panel.
|
||||||
|
const prior = globalThis.document;
|
||||||
|
globalThis.document = { getElementById: (id) => (id === 'data-panel' ? panel : null) };
|
||||||
|
try {
|
||||||
|
owner._shownPanel = PanelChrome.prototype._shownPanel;
|
||||||
|
const state = PanelChrome.prototype._buildSharePanelState.call(owner);
|
||||||
|
assert.deepEqual(state.specs, [{ id: 'data-panel', collapsed: false }]);
|
||||||
|
} finally {
|
||||||
|
globalThis.document = prior;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user