mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Carry reviewed #816 onto current main without conflicts; preserve i18n updates, replace obsolete changelog edit with a fragment, and add a constructor-wiring regression alongside the owner and rail tests. Credit: raysung-2026 (report), asturksever (review), quanticsoul4772 (Windows diagnosis).
679 lines
22 KiB
JavaScript
679 lines
22 KiB
JavaScript
import { UiLifetime } from './uiLifetime.js';
|
||
import { t, msg } from '../i18n/index.js';
|
||
import { displayPanelScroller } from './displayPanelScroll.js';
|
||
import { PanelPositionControls } from './panelPositionControls.js';
|
||
import { PanelLayoutController } from './panelLayoutController.js';
|
||
import {
|
||
bindPanelDisclosure,
|
||
collapsePanelOnEscape,
|
||
createHoverDisclosure,
|
||
} from './panelDisclosure.js';
|
||
const SHARE_PANEL_STATE_SPECS = Object.freeze([
|
||
{ id: 'control-panel', pinnable: true },
|
||
{ id: 'location-bar', pinnable: true },
|
||
{ id: 'data-panel' },
|
||
{ id: 'cctv-panel' },
|
||
{ id: 'weather-panel' },
|
||
{ id: 'recent-imagery-panel' },
|
||
{ id: 'street-level-panel' },
|
||
{ id: 'radio-panel' },
|
||
{ id: 'scene-panel' },
|
||
{ id: 'global-context-panel' },
|
||
{ id: 'pp-toggles' },
|
||
{ id: 'param-slider-panel' },
|
||
]);
|
||
/** Standard map-view panels cleared out of the way on a fresh Cockpit entry. */
|
||
const COCKPIT_ENTRY_COLLAPSE_PANEL_IDS = Object.freeze([
|
||
'data-panel',
|
||
'cctv-panel',
|
||
'weather-panel',
|
||
'recent-imagery-panel',
|
||
'street-level-panel',
|
||
'scene-panel',
|
||
'pp-toggles',
|
||
'global-context-panel',
|
||
'radio-panel',
|
||
]);
|
||
|
||
/** Own panel disclosure, docking, persistence and Cockpit rail restoration. */
|
||
export class PanelChrome {
|
||
constructor({
|
||
elements,
|
||
operations,
|
||
readHud,
|
||
readCockpit,
|
||
readShareLinks,
|
||
readInitialShare,
|
||
readScrollRestoreOwner,
|
||
readDisplayScrollTop,
|
||
}) {
|
||
Object.assign(this, elements, operations, {
|
||
readHud,
|
||
readCockpit,
|
||
readShareLinks,
|
||
readInitialShare,
|
||
readScrollRestoreOwner,
|
||
readDisplayScrollTop,
|
||
});
|
||
this._disposed = false;
|
||
this._lifetime = new UiLifetime();
|
||
this._cockpitPanelRestore = null;
|
||
this._cockpitContextCollapsedForDataPanel = false;
|
||
this._panelPosition = new PanelPositionControls({
|
||
syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel),
|
||
layoutRightPanels: () => this._layoutRightPanels(),
|
||
syncCctvPanelViewport: () => this._syncCctvPanelViewport(),
|
||
showToast: (message) => this._showToast(message),
|
||
onPanelResized: (panelId) => this._onPanelResized(panelId),
|
||
});
|
||
this._panelLayout = new PanelLayoutController({
|
||
readHud: () => ({
|
||
visible: this.hud.visible,
|
||
variant: this.hud.getVariant(),
|
||
}),
|
||
scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(),
|
||
syncPanelCollapseButton: (panel) =>
|
||
this._syncRailPanelCollapseButton(panel),
|
||
readDisplayScrollTop: () =>
|
||
this._displayPortalScrollRestoreOwner === 'standard'
|
||
? this._standardDisplayScrollTop
|
||
: displayPanelScroller(this._ppToggles)?.scrollTop || 0,
|
||
});
|
||
}
|
||
get hud() {
|
||
return this.readHud();
|
||
}
|
||
get cockpitView() {
|
||
return this.readCockpit();
|
||
}
|
||
get shareLinkManager() {
|
||
return this.readShareLinks();
|
||
}
|
||
get _initialShareState() {
|
||
return this.readInitialShare();
|
||
}
|
||
get _displayPortalScrollRestoreOwner() {
|
||
return this.readScrollRestoreOwner();
|
||
}
|
||
get _standardDisplayScrollTop() {
|
||
return this.readDisplayScrollTop();
|
||
}
|
||
|
||
_initPanelChrome() {
|
||
for (const control of this._panelDisclosureControls || [])
|
||
control.destroy();
|
||
this._panelDisclosureControls = [];
|
||
const targets = new Map();
|
||
document
|
||
.querySelectorAll('.panel-collapse-btn[data-collapse-target]')
|
||
.forEach((button) => {
|
||
const targetId = button.dataset.collapseTarget;
|
||
if (!targetId) return;
|
||
if (!targets.has(targetId)) targets.set(targetId, []);
|
||
targets.get(targetId).push(button);
|
||
});
|
||
for (const [targetId, buttons] of targets) {
|
||
const panel = document.getElementById(targetId);
|
||
if (!panel) continue;
|
||
this._panelDisclosureControls.push(
|
||
bindPanelDisclosure({
|
||
panel,
|
||
buttons,
|
||
onChange: (collapsed, options) =>
|
||
this.setPanelCollapsed(targetId, collapsed, options),
|
||
onEscape: (event) => this._collapsePanelOnEscape(event, targetId),
|
||
}),
|
||
);
|
||
this._restorePanelCollapsedState(targetId, {
|
||
allowStored: !this._initialShareState,
|
||
});
|
||
}
|
||
// The command dock always starts compact; either wing reveals on hover,
|
||
// focus, or click and collapses again after the interaction moves away.
|
||
this.setPanelCollapsed('control-panel', true, {
|
||
syncShare: false,
|
||
persist: false,
|
||
});
|
||
this.setPanelCollapsed('location-bar', true, {
|
||
syncShare: false,
|
||
persist: false,
|
||
});
|
||
this._initAutoHoverPanel('control-panel', {
|
||
openDelayMs: 140,
|
||
closeDelayMs: 420,
|
||
});
|
||
this._initAutoHoverPanel('location-bar', {
|
||
openDelayMs: 140,
|
||
closeDelayMs: 420,
|
||
});
|
||
this._initCommandDockPins();
|
||
this._initCommandDockTrayMetrics();
|
||
this._maybeNotifyLayoutReset();
|
||
this._panelPosition._initPanelDrag();
|
||
}
|
||
|
||
_collapsePanelOnEscape(event, panelId) {
|
||
return collapsePanelOnEscape(event, {
|
||
panel: document.getElementById(panelId),
|
||
onChange: (collapsed, options) =>
|
||
this.setPanelCollapsed(panelId, collapsed, options),
|
||
beforeCollapse: () => {
|
||
if (panelId !== 'location-bar' || !this._locationSearch) return;
|
||
this._locationSearch.classList.remove('expanded');
|
||
this._locationSearch.value = '';
|
||
this._locationSearch.blur();
|
||
},
|
||
});
|
||
}
|
||
|
||
_initCommandDockPins() {
|
||
document
|
||
.querySelectorAll('.dock-pin-btn[data-pin-target]')
|
||
.forEach((button) => {
|
||
this._lifetime.listen(button, 'click', (event) => {
|
||
event.stopPropagation();
|
||
const panelId = button.dataset.pinTarget;
|
||
this._setCommandDockPanelPinState(panelId);
|
||
});
|
||
});
|
||
}
|
||
|
||
_setCommandDockPanelPinState(
|
||
panelId,
|
||
pin,
|
||
{ restore = false, persist = true, syncShare = true } = {},
|
||
) {
|
||
const panelEl = document.getElementById(panelId);
|
||
const button = document.querySelector(
|
||
`.dock-pin-btn[data-pin-target="${panelId}"]`,
|
||
);
|
||
if (!panelEl || !button) return undefined;
|
||
const shouldPin =
|
||
typeof pin === 'boolean'
|
||
? pin
|
||
: !panelEl.classList.contains('dock-pinned');
|
||
panelEl.classList.toggle('dock-pinned', shouldPin);
|
||
button.setAttribute('aria-pressed', String(shouldPin));
|
||
document
|
||
.querySelectorAll('#command-dock .dock-pinned-top')
|
||
.forEach((pinnedPanel) => {
|
||
pinnedPanel.classList.remove('dock-pinned-top');
|
||
});
|
||
if (shouldPin) {
|
||
panelEl.classList.add('dock-pinned-top');
|
||
this.setPanelCollapsed(panelId, false, {
|
||
explicit: !restore,
|
||
restore,
|
||
persist,
|
||
syncShare: false,
|
||
});
|
||
} else {
|
||
const remainingPinnedPanel = document.querySelector(
|
||
'#command-dock .dock-pinned',
|
||
);
|
||
remainingPinnedPanel?.classList.add('dock-pinned-top');
|
||
if (!restore && !panelEl.matches(':hover')) {
|
||
this.setPanelCollapsed(panelId, true, {
|
||
explicit: true,
|
||
persist,
|
||
syncShare: false,
|
||
});
|
||
}
|
||
}
|
||
this._updateCommandDockTrayStack();
|
||
if (syncShare) {
|
||
if (!restore) this.shareLinkManager?.claimRestoreLane?.('panel', panelId);
|
||
this.shareLinkManager?.onPanelStateChange?.();
|
||
}
|
||
return shouldPin;
|
||
}
|
||
|
||
_initCommandDockTrayMetrics() {
|
||
return this._panelLayout._initCommandDockTrayMetrics();
|
||
}
|
||
|
||
_updateCommandDockTrayStack() {
|
||
return this._panelLayout._updateCommandDockTrayStack();
|
||
}
|
||
|
||
_maybeNotifyLayoutReset() {
|
||
return this._panelPosition._maybeNotifyLayoutReset();
|
||
}
|
||
|
||
_initAutoHoverPanel(
|
||
panelId,
|
||
{ openDelayMs = 850, closeDelayMs = 1000 } = {},
|
||
) {
|
||
const panel = document.getElementById(panelId);
|
||
if (!panel) return;
|
||
this._hoverPanelControls ??= new Map();
|
||
this._hoverPanelControls.get(panelId)?.destroy();
|
||
const controller = createHoverDisclosure({
|
||
panel,
|
||
documentRef: document,
|
||
disclosure: panel.querySelector(`[data-dock-toggle-target="${panelId}"]`),
|
||
openDelayMs,
|
||
closeDelayMs,
|
||
isActive: () => !this._disposed,
|
||
onChange: (collapsed, options) =>
|
||
this.setPanelCollapsed(panelId, collapsed, options),
|
||
onEscape: (event) => this._collapsePanelOnEscape(event, panelId),
|
||
focusTarget:
|
||
panelId === 'control-panel'
|
||
? () =>
|
||
panel.querySelector('.map-stack-chip.active') ||
|
||
panel.querySelector('.map-stack-chip')
|
||
: null,
|
||
});
|
||
this._hoverPanelControls.set(panelId, controller);
|
||
if (panelId === 'control-panel') {
|
||
this._cancelMapSourceFocus?.();
|
||
this._cancelMapSourceFocus = controller.cancelPendingFocus;
|
||
}
|
||
}
|
||
|
||
_restorePanelCollapsedState(panelId, options1) {
|
||
return this._panelPosition._restorePanelCollapsedState(panelId, options1);
|
||
}
|
||
|
||
_savePanelCollapsedState(panelId, collapsed) {
|
||
return this._panelPosition._savePanelCollapsedState(panelId, collapsed);
|
||
}
|
||
|
||
_scheduleRightPanelLayout(options0) {
|
||
return this._panelLayout._scheduleRightPanelLayout(options0);
|
||
}
|
||
|
||
_scheduleLeftPanelLayout(options0) {
|
||
return this._panelLayout._scheduleLeftPanelLayout(options0);
|
||
}
|
||
|
||
_syncRailPanelCollapseButton(panelEl) {
|
||
// Adaptive rail layout may directly restore an auto-collapsed Context
|
||
// without invoking setPanelCollapsed(). Enforce Radio ownership there too.
|
||
if (
|
||
panelEl?.id === 'global-context-panel' &&
|
||
!panelEl.classList.contains('collapsed') &&
|
||
this._contextRadioDock?.classList.contains('disclosure-open')
|
||
) {
|
||
this._setRadioDisclosure?.(false);
|
||
}
|
||
this._syncPanelCollapseButton(panelEl);
|
||
}
|
||
|
||
_syncPanelCollapseButton(panelEl) {
|
||
const isRightRail = [
|
||
'pp-toggles',
|
||
'cctv-panel',
|
||
'weather-panel',
|
||
'recent-imagery-panel',
|
||
'street-level-panel',
|
||
'global-context-panel',
|
||
].includes(panelEl?.id);
|
||
const collapsed = panelEl.classList.contains('collapsed');
|
||
panelEl
|
||
.querySelectorAll('.panel-collapse-btn[data-collapse-target]')
|
||
.forEach((btn) => {
|
||
const owner = btn.closest('[data-panel-id], #param-slider-panel');
|
||
if (owner !== panelEl) return;
|
||
if (isRightRail) {
|
||
btn.textContent = collapsed ? '◀' : '▶';
|
||
} else {
|
||
btn.textContent = collapsed ? '+' : '−';
|
||
}
|
||
btn.setAttribute('aria-expanded', String(!collapsed));
|
||
const panelName =
|
||
panelEl
|
||
.querySelector('.panel-title, .pp-header-label')
|
||
?.textContent?.trim() || t('panel');
|
||
const titleKey = collapsed
|
||
? msg('Expand {name}')
|
||
: msg('Collapse {name}');
|
||
btn.title = t(titleKey, { name: panelName });
|
||
btn.setAttribute('aria-label', t(titleKey, { name: panelName }));
|
||
if (panelEl.id === 'radio-panel') {
|
||
// Radio keeps the wording of its static button (context.html).
|
||
btn.title = t(
|
||
collapsed ? msg('Expand Radio') : msg('Collapse Radio'),
|
||
);
|
||
btn.setAttribute(
|
||
'aria-label',
|
||
t(
|
||
collapsed
|
||
? msg('Expand Radio section')
|
||
: msg('Collapse Radio section'),
|
||
),
|
||
);
|
||
}
|
||
});
|
||
const dockToggle = panelEl.querySelector(
|
||
`[data-dock-toggle-target="${panelEl.id}"]`,
|
||
);
|
||
if (dockToggle) {
|
||
const panelName =
|
||
panelEl
|
||
.querySelector('.panel-title, .location-toolbar-label')
|
||
?.textContent?.trim() || t('panel');
|
||
const titleKey = collapsed
|
||
? msg('Expand {name}')
|
||
: msg('Collapse {name}');
|
||
dockToggle.setAttribute('aria-expanded', String(!collapsed));
|
||
dockToggle.setAttribute('aria-label', t(titleKey, { name: panelName }));
|
||
dockToggle.title = t(titleKey, { name: panelName });
|
||
}
|
||
if (panelEl.id === 'radio-panel' && this._contextRadioDetailsBtn) {
|
||
this._contextRadioDetailsBtn.setAttribute(
|
||
'aria-expanded',
|
||
String(!collapsed),
|
||
);
|
||
}
|
||
if (panelEl.id === 'radio-panel' || panelEl.id === 'global-context-panel') {
|
||
this._syncContextRadioLauncherState();
|
||
}
|
||
}
|
||
|
||
_buildSharePanelState() {
|
||
const specs = [];
|
||
for (const spec of SHARE_PANEL_STATE_SPECS) {
|
||
const panelEl = document.getElementById(spec.id);
|
||
if (!panelEl) continue;
|
||
// Responsive auto-collapse is presentation only; the recipient should
|
||
// restore the user's explicit expanded preference at its own viewport.
|
||
const collapsed =
|
||
panelEl.classList.contains('layout-auto-collapsed') ||
|
||
panelEl.classList.contains('cyber-accordion-collapsed')
|
||
? false
|
||
: panelEl.classList.contains('collapsed');
|
||
const entry = { id: spec.id, collapsed };
|
||
if (spec.pinnable)
|
||
entry.pinned = panelEl.classList.contains('dock-pinned');
|
||
specs.push(entry);
|
||
}
|
||
return specs.length ? { specs } : null;
|
||
}
|
||
|
||
_restorePanelState(panelState) {
|
||
if (!panelState || !Array.isArray(panelState.specs)) return;
|
||
const specsById = new Map(panelState.specs.map((spec) => [spec.id, spec]));
|
||
for (const spec of SHARE_PANEL_STATE_SPECS) {
|
||
const state = specsById.get(spec.id);
|
||
if (!state || typeof state.collapsed !== 'boolean') continue;
|
||
if (spec.pinnable && typeof state.pinned === 'boolean') {
|
||
this._setCommandDockPanelPinState(spec.id, state.pinned, {
|
||
restore: true,
|
||
persist: false,
|
||
syncShare: false,
|
||
});
|
||
}
|
||
const nextCollapsed =
|
||
state.pinned && spec.pinnable ? false : state.collapsed;
|
||
this.setPanelCollapsed(spec.id, nextCollapsed, {
|
||
restore: true,
|
||
persist: false,
|
||
syncShare: false,
|
||
});
|
||
}
|
||
this.shareLinkManager?.onPanelStateChange?.();
|
||
}
|
||
|
||
/** @returns {boolean} Whether the panel was floating and is now docked. */
|
||
dockPanel(panelId) {
|
||
return this._panelPosition?.dockPanel?.(panelId) === true;
|
||
}
|
||
|
||
setPanelCollapsed(
|
||
panelId,
|
||
collapsed,
|
||
{
|
||
explicit = false,
|
||
restore = false,
|
||
persist = true,
|
||
syncShare = true,
|
||
} = {},
|
||
) {
|
||
if (panelId === 'control-panel' && collapsed)
|
||
this._cancelMapSourceFocus?.();
|
||
const panelEl = document.getElementById(panelId);
|
||
if (!panelEl) return;
|
||
if (explicit && !restore)
|
||
this.shareLinkManager?.claimRestoreLane?.('panel', panelId);
|
||
const nextCollapsed = Boolean(collapsed);
|
||
const wasAutoCollapsed =
|
||
panelEl.classList.contains('layout-auto-collapsed') ||
|
||
panelEl.classList.contains('cyber-accordion-collapsed');
|
||
// A user opening a Cyber rail panel owns the whole accordion, including
|
||
// peers that were only presentation-collapsed during a saved-state restore.
|
||
// A floating window is outside the accordion both ways.
|
||
if (
|
||
explicit &&
|
||
!restore &&
|
||
!nextCollapsed &&
|
||
document.documentElement?.dataset.uiTheme === 'cyber' &&
|
||
panelEl.parentElement === this._rightPanelStack &&
|
||
!panelEl.classList.contains('panel-floating')
|
||
) {
|
||
for (const peer of this._rightPanelStack.children) {
|
||
if (
|
||
peer !== panelEl &&
|
||
peer.matches('[data-panel-id]') &&
|
||
!peer.classList.contains('panel-floating') &&
|
||
!peer.hidden
|
||
) {
|
||
this.setPanelCollapsed(peer.id, true, {
|
||
explicit,
|
||
persist,
|
||
syncShare: false,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
const leftOwnerPanel = this._leftPanelStack?.contains(panelEl)
|
||
? panelEl
|
||
: null;
|
||
const rightOwnerPanel =
|
||
panelId === 'radio-panel'
|
||
? document.getElementById('global-context-panel')
|
||
: this._rightPanelStack?.contains(panelEl)
|
||
? panelEl
|
||
: null;
|
||
const priorLeftOwner = this._panelLayout._leftStackPreferredPanelId;
|
||
const priorRightOwner = this._panelLayout._rightStackPreferredPanelId;
|
||
if (explicit && !restore && !nextCollapsed && leftOwnerPanel) {
|
||
this._panelLayout._leftStackPreferredPanelId = leftOwnerPanel.id;
|
||
} else if (
|
||
explicit &&
|
||
!restore &&
|
||
nextCollapsed &&
|
||
leftOwnerPanel?.id === this._panelLayout._leftStackPreferredPanelId
|
||
) {
|
||
this._panelLayout._leftStackPreferredPanelId = null;
|
||
}
|
||
if (explicit && !restore && !nextCollapsed && rightOwnerPanel) {
|
||
this._panelLayout._rightStackPreferredPanelId = rightOwnerPanel.id;
|
||
} else if (
|
||
explicit &&
|
||
!restore &&
|
||
nextCollapsed &&
|
||
rightOwnerPanel?.id === this._panelLayout._rightStackPreferredPanelId
|
||
) {
|
||
this._panelLayout._rightStackPreferredPanelId = null;
|
||
}
|
||
// Reveal Radio's owner before the no-op check: the nested section may
|
||
// already be expanded while its Context parent is closed.
|
||
if (
|
||
document.documentElement?.dataset.uiTheme === 'cyber' &&
|
||
!nextCollapsed &&
|
||
panelId === 'radio-panel' &&
|
||
document
|
||
.getElementById('global-context-panel')
|
||
?.classList.contains('collapsed')
|
||
) {
|
||
this.setPanelCollapsed('global-context-panel', false, {
|
||
explicit,
|
||
restore,
|
||
persist,
|
||
syncShare,
|
||
});
|
||
}
|
||
// A repeated Context-open request must clear a stale compact Radio mini
|
||
// even when no collapsed class changes.
|
||
if (
|
||
!nextCollapsed &&
|
||
panelId === 'global-context-panel' &&
|
||
this._contextRadioDock?.classList.contains('disclosure-open')
|
||
) {
|
||
this._setRadioDisclosure?.(false);
|
||
}
|
||
if (
|
||
panelEl.classList.contains('collapsed') === nextCollapsed &&
|
||
!wasAutoCollapsed
|
||
) {
|
||
this._syncPanelCollapseButton(panelEl);
|
||
if (priorLeftOwner !== this._panelLayout._leftStackPreferredPanelId) {
|
||
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
|
||
}
|
||
if (priorRightOwner !== this._panelLayout._rightStackPreferredPanelId) {
|
||
this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true });
|
||
}
|
||
return;
|
||
}
|
||
panelEl.classList.remove(
|
||
'layout-auto-collapsed',
|
||
'cyber-accordion-collapsed',
|
||
);
|
||
if (
|
||
!nextCollapsed &&
|
||
this.cockpitView?.active &&
|
||
panelId === 'data-panel'
|
||
) {
|
||
this._cockpitContextCollapsedForDataPanel =
|
||
!this.cockpitView.contextCollapsed;
|
||
if (this._cockpitContextCollapsedForDataPanel) {
|
||
this.cockpitView.setContextCollapsed(true);
|
||
}
|
||
}
|
||
if (
|
||
!nextCollapsed &&
|
||
panelId === 'global-context-panel' &&
|
||
this._contextRadioDock?.classList.contains('disclosure-open')
|
||
) {
|
||
this._setRadioDisclosure?.(false);
|
||
}
|
||
if (
|
||
!nextCollapsed &&
|
||
panelId === 'radio-panel' &&
|
||
document.documentElement?.dataset.uiTheme !== 'cyber' &&
|
||
document
|
||
.getElementById('global-context-panel')
|
||
?.classList.contains('collapsed')
|
||
) {
|
||
this.setPanelCollapsed('global-context-panel', false, {
|
||
restore,
|
||
persist,
|
||
syncShare,
|
||
});
|
||
}
|
||
if (!nextCollapsed && !restore && panelId === 'location-bar') {
|
||
const otherPanel = document.getElementById('control-panel');
|
||
if (otherPanel && !otherPanel.classList.contains('dock-pinned')) {
|
||
this.setPanelCollapsed('control-panel', true, {
|
||
restore,
|
||
persist,
|
||
syncShare,
|
||
});
|
||
}
|
||
} else if (!nextCollapsed && !restore && panelId === 'control-panel') {
|
||
const otherPanel = document.getElementById('location-bar');
|
||
if (otherPanel && !otherPanel.classList.contains('dock-pinned')) {
|
||
this.setPanelCollapsed('location-bar', true, {
|
||
restore,
|
||
persist,
|
||
syncShare,
|
||
});
|
||
}
|
||
}
|
||
panelEl.classList.toggle('collapsed', nextCollapsed);
|
||
// Only a user's own collapse docks a floating window; restores, cockpit
|
||
// entry and accordion peers keep its place.
|
||
if (explicit && !restore)
|
||
this._panelPosition?.onPanelCollapsed?.(panelId, nextCollapsed);
|
||
if (
|
||
nextCollapsed &&
|
||
this.cockpitView?.active &&
|
||
panelId === 'data-panel' &&
|
||
this._cockpitContextCollapsedForDataPanel
|
||
) {
|
||
this._cockpitContextCollapsedForDataPanel = false;
|
||
this.cockpitView.setContextCollapsed(false);
|
||
}
|
||
this._syncPanelCollapseButton(panelEl);
|
||
if (persist !== false)
|
||
this._savePanelCollapsedState(panelId, nextCollapsed);
|
||
if (panelId === 'pp-toggles') {
|
||
this._layoutRightPanels();
|
||
}
|
||
if (this._rightPanelStack?.contains(panelEl)) {
|
||
this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true });
|
||
}
|
||
if (panelId === 'cctv-panel') {
|
||
this._syncCctvPanelViewport();
|
||
}
|
||
this._lifetime.frame(() => this._updateCommandDockTrayStack());
|
||
this._scheduleLeftPanelLayout({
|
||
reconsiderAutoCollapse: this._leftPanelStack?.contains(panelEl) === true,
|
||
});
|
||
if (syncShare) this.shareLinkManager?.onPanelStateChange?.();
|
||
}
|
||
|
||
_layoutRightPanels() {
|
||
this._scheduleRightPanelLayout();
|
||
}
|
||
enterCockpit() {
|
||
// A new Cockpit session owns both side rails. Clear standard map-view
|
||
// panels once on entry; NEXT/PREVIOUS never reaches this callback, so
|
||
// panels the operator opens while already inside remain untouched.
|
||
this._cockpitPanelRestore = new Map();
|
||
this._cockpitContextCollapsedForDataPanel = false;
|
||
for (const panelId of COCKPIT_ENTRY_COLLAPSE_PANEL_IDS) {
|
||
const panel = document.getElementById(panelId);
|
||
if (panel) {
|
||
this._cockpitPanelRestore.set(
|
||
panelId,
|
||
panel.classList.contains('collapsed'),
|
||
);
|
||
}
|
||
this.setPanelCollapsed(panelId, true, {
|
||
persist: false,
|
||
syncShare: false,
|
||
});
|
||
}
|
||
this.cockpitView?.setContextCollapsed(false);
|
||
this.cockpitView?.setSignalCollapsed(false, { user: true });
|
||
}
|
||
exitCockpit() {
|
||
const restore = this._cockpitPanelRestore;
|
||
this._cockpitPanelRestore = null;
|
||
this._cockpitContextCollapsedForDataPanel = false;
|
||
if (!restore) return;
|
||
for (const [panelId, wasCollapsed] of restore) {
|
||
this.setPanelCollapsed(panelId, wasCollapsed, {
|
||
persist: false,
|
||
syncShare: false,
|
||
});
|
||
}
|
||
}
|
||
destroy() {
|
||
if (this._disposed) return;
|
||
this._disposed = true;
|
||
this._lifetime.destroy();
|
||
this._panelPosition.destroy();
|
||
this._panelLayout.destroy();
|
||
for (const control of this._panelDisclosureControls || [])
|
||
control.destroy();
|
||
this._panelDisclosureControls = [];
|
||
this._hoverPanelControls?.forEach((control) => control.destroy());
|
||
this._hoverPanelControls?.clear();
|
||
this._cancelMapSourceFocus?.();
|
||
}
|
||
}
|