mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
A desktop side panel hides once collapsed, so Escape hands focus to its visible rail item instead of the hidden disclosure. Rail selection reads the panel's collapsed class rather than the store, which follows it a microtask late.
128 lines
4.3 KiB
JavaScript
128 lines
4.3 KiB
JavaScript
import PanelRailView from './PanelRailView.vue';
|
|
import { PANEL_RAIL_HOST_CLASS, PANEL_RAIL_ITEMS } from './panelRailCatalog.js';
|
|
import { usePanelStore } from './stores/panels.js';
|
|
|
|
/**
|
|
* Own the desktop panel rails: publish each side's panels to the panel store
|
|
* and open or close a panel when its rail item is selected. Disclosure itself
|
|
* stays with PanelChrome, which enforces one open panel per side.
|
|
*/
|
|
export class PanelNavigation {
|
|
/**
|
|
* @param {object} options
|
|
* @param {{pinia: object, mount: Function}} options.ui Shell Vue runtime.
|
|
* @param {{left: HTMLElement|null, right: HTMLElement|null}} options.stacks Side panel stacks.
|
|
* @param {Function} options.setPanelCollapsed PanelChrome's disclosure operation.
|
|
*/
|
|
constructor({ ui, stacks, setPanelCollapsed }) {
|
|
this._ui = ui;
|
|
this._stacks = stacks;
|
|
this._setPanelCollapsed = setPanelCollapsed;
|
|
this._store = usePanelStore(ui.pinia);
|
|
this._unmounts = [];
|
|
this._hosts = [];
|
|
this._observer = null;
|
|
this._destroyed = false;
|
|
}
|
|
|
|
init() {
|
|
if (this._destroyed) return;
|
|
for (const side of ['left', 'right']) {
|
|
const stack = this._stacks[side];
|
|
if (!stack) continue;
|
|
// Inside the stack, the rail shows and hides with its panels in every
|
|
// presentation mode (Cockpit, clean view, recording, phone sheets).
|
|
const host = stack.ownerDocument.createElement('div');
|
|
host.className = `${PANEL_RAIL_HOST_CLASS} ${PANEL_RAIL_HOST_CLASS}--${side}`;
|
|
stack.prepend(host);
|
|
this._hosts.push(host);
|
|
this._unmounts.push(
|
|
this._ui.mount(PanelRailView, host, {
|
|
side,
|
|
onSelect: (panelId) => this.select(panelId),
|
|
}),
|
|
);
|
|
}
|
|
if (typeof MutationObserver !== 'undefined') {
|
|
// Only the panels themselves matter: their order, disclosure and
|
|
// availability, not changes inside them or inside the rails.
|
|
this._observer = new MutationObserver((records) => {
|
|
if (records.some((record) => this._isPanelRecord(record))) this.sync();
|
|
});
|
|
for (const stack of Object.values(this._stacks)) {
|
|
if (!stack) continue;
|
|
this._observer.observe(stack, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ['class', 'hidden'],
|
|
});
|
|
}
|
|
}
|
|
this.sync();
|
|
}
|
|
|
|
/** Publish both sides' panels in display order. */
|
|
sync() {
|
|
if (this._destroyed) return;
|
|
for (const side of ['left', 'right']) {
|
|
const stack = this._stacks[side];
|
|
const panels = stack
|
|
? [...stack.children]
|
|
.filter((panel) => panel.matches('[data-panel-id]'))
|
|
.map((panel) => {
|
|
const shownBy = PANEL_RAIL_ITEMS[panel.id]?.shownBy;
|
|
return {
|
|
id: panel.id,
|
|
title:
|
|
panel
|
|
.querySelector('.panel-title, .pp-header-label')
|
|
?.textContent.trim() || panel.id,
|
|
open: !panel.classList.contains('collapsed'),
|
|
available:
|
|
!panel.hidden &&
|
|
(!shownBy || panel.classList.contains(shownBy)),
|
|
};
|
|
})
|
|
: [];
|
|
this._store.setSide(side, panels);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle a panel from its rail item.
|
|
* @param {string} panelId Panel element id.
|
|
*/
|
|
select(panelId) {
|
|
if (this._destroyed) return;
|
|
// The panel itself, not the store: the store follows its class a
|
|
// microtask later, after a same-task setPanelCollapsed.
|
|
const panel = document.getElementById(panelId);
|
|
if (!panel) return;
|
|
this._setPanelCollapsed(panelId, !panel.classList.contains('collapsed'), {
|
|
explicit: true,
|
|
});
|
|
}
|
|
|
|
_isPanelRecord(record) {
|
|
const stacks = Object.values(this._stacks);
|
|
return record.type === 'childList'
|
|
? stacks.includes(record.target) &&
|
|
[...record.addedNodes, ...record.removedNodes].some(
|
|
(node) => !this._hosts.includes(node),
|
|
)
|
|
: stacks.includes(record.target.parentElement);
|
|
}
|
|
|
|
destroy() {
|
|
if (this._destroyed) return;
|
|
this._destroyed = true;
|
|
this._observer?.disconnect();
|
|
this._observer = null;
|
|
for (const unmount of this._unmounts) unmount();
|
|
this._unmounts = [];
|
|
for (const host of this._hosts) host.remove();
|
|
this._hosts = [];
|
|
}
|
|
}
|