UNPKG

dockview-core

Version:

Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript

186 lines (185 loc) 8.45 kB
import { CompositeDisposable, Disposable, MutableDisposable, } from '../../../lifecycle'; import { Emitter } from '../../../events'; import { trackFocus } from '../../../dom'; import { Droptarget } from '../../../dnd/droptarget'; import { pointerBackend } from '../../../dnd/backend'; import { getPanelData } from '../../../dnd/dataTransfer'; let _contentId = 0; /** Stable DOM id so each tab's `aria-controls` can reference its tabpanel. */ const nextContentId = () => `dv-tabpanel-${_contentId++}`; export class ContentContainer extends CompositeDisposable { get element() { return this._element; } constructor(accessor, group) { var _a, _b, _c, _d, _e, _f, _g; super(); this.accessor = accessor; this.group = group; this.disposable = new MutableDisposable(); this._onDidFocus = new Emitter(); this.onDidFocus = this._onDidFocus.event; this._onDidBlur = new Emitter(); this.onDidBlur = this._onDidBlur.event; this._element = document.createElement('div'); this._element.className = 'dv-content-container'; this._element.tabIndex = -1; // WAI-ARIA Tabs pattern: the single content area per group is the // tabpanel; `aria-labelledby` is pointed at the active tab in // `setLabelledBy` (driven from the group model on activation). this._element.id = nextContentId(); this._element.setAttribute('role', 'tabpanel'); this.addDisposables(this._onDidFocus, this._onDidBlur); // Resolve the override anchor dynamically: a group can be relocated // between roots (grid / floating / popout) after construction, and the // popout anchor in particular lives in another window — a value // captured here would mount overlays in the wrong window. const getOverrideTarget = () => { var _a; return (_a = group.dropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; }; const canDisplayOverlay = (event, position) => { if (this.group.locked === 'no-drop-target' || (this.group.locked && position === 'center')) { return false; } const data = getPanelData(); if (!data && event.shiftKey && this.group.location.type !== 'floating') { return false; } if (data && data.viewId === this.accessor.id) { return true; } return this.group.canDisplayOverlay(event, position, 'content'); }; // `dropTarget` stays the concrete `Droptarget` (not via the backend // factory) because overlayRenderContainer forwards HTML5 drag events // through `dropTarget.dnd` — that field is not part of `IDropTarget`. this.dropTarget = new Droptarget(this.element, { getOverlayOutline: () => { var _a; return ((_a = accessor.options.theme) === null || _a === void 0 ? void 0 : _a.dndPanelOverlay) === 'group' ? this.element.parentElement : null; }, className: 'dv-drop-target-content', acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], canDisplayOverlay, getOverrideTarget, overlayModel: (_b = (_a = this.accessor).resolveDropOverlayModel) === null || _b === void 0 ? void 0 : _b.call(_a, 'content'), }); this.pointerDropTarget = pointerBackend.createDropTarget(this.element, { acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], canDisplayOverlay, getOverlayOutline: () => { var _a; return ((_a = accessor.options.theme) === null || _a === void 0 ? void 0 : _a.dndPanelOverlay) === 'group' ? this.element.parentElement : null; }, className: 'dv-drop-target-content', getOverrideTarget, overlayModel: (_d = (_c = this.accessor).resolveDropOverlayModel) === null || _d === void 0 ? void 0 : _d.call(_c, 'content'), }); this.addDisposables(this.dropTarget, this.pointerDropTarget, // Re-apply the app-supplied overlay model when options change. // `{}` resets to the built-in default (all fields optional). (_g = (_f = (_e = this.accessor).onDidOptionsChange) === null || _f === void 0 ? void 0 : _f.call(_e, () => { var _a, _b, _c; const model = (_c = (_b = (_a = this.accessor).resolveDropOverlayModel) === null || _b === void 0 ? void 0 : _b.call(_a, 'content')) !== null && _c !== void 0 ? _c : {}; this.dropTarget.setOverlayModel(model); this.pointerDropTarget.setOverlayModel(model); })) !== null && _g !== void 0 ? _g : Disposable.NONE); } show() { this.element.style.display = ''; } hide() { this.element.style.display = 'none'; } setLabelledBy(tabElementId) { if (tabElementId) { this._element.setAttribute('aria-labelledby', tabElementId); } else { this._element.removeAttribute('aria-labelledby'); } } renderPanel(panel, options = { asActive: true }) { var _a, _b, _c, _d; const doRender = options.asActive || (this.panel && this.group.isPanelActive(this.panel)); if (this.panel && this.panel.view.content.element.parentElement === this._element) { /** * If the currently attached panel is mounted directly to the content then remove it */ this._element.removeChild(this.panel.view.content.element); (_b = (_a = this.panel.view.content).onHide) === null || _b === void 0 ? void 0 : _b.call(_a); } this.panel = panel; let container; switch (panel.api.renderer) { case 'onlyWhenVisible': this.group.renderContainer.detatch(panel); if (this.panel) { if (doRender) { this._element.appendChild(this.panel.view.content.element); (_d = (_c = this.panel.view.content).onShow) === null || _d === void 0 ? void 0 : _d.call(_c); } } container = this._element; break; case 'always': if (panel.view.content.element.parentElement === this._element) { this._element.removeChild(panel.view.content.element); } container = this.group.renderContainer.attach({ panel, referenceContainer: this, }); break; default: throw new Error(`dockview: invalid renderer type '${panel.api.renderer}'`); } if (doRender) { const focusTracker = trackFocus(container); this.focusTracker = focusTracker; const disposable = new CompositeDisposable(); disposable.addDisposables(focusTracker, focusTracker.onDidFocus(() => this._onDidFocus.fire()), focusTracker.onDidBlur(() => this._onDidBlur.fire())); this.disposable.value = disposable; } } openPanel(panel) { if (this.panel === panel) { return; } this.renderPanel(panel); } layout(_width, _height) { // noop } closePanel() { var _a, _b, _c; if (this.panel) { if (this.panel.api.renderer === 'onlyWhenVisible') { (_a = this.panel.view.content.element.parentElement) === null || _a === void 0 ? void 0 : _a.removeChild(this.panel.view.content.element); (_c = (_b = this.panel.view.content).onHide) === null || _c === void 0 ? void 0 : _c.call(_b); } } this.panel = undefined; } dispose() { this.disposable.dispose(); super.dispose(); } /** * Refresh the focus tracker state to handle cases where focus state * gets out of sync due to programmatic panel activation */ refreshFocusState() { var _a; if ((_a = this.focusTracker) === null || _a === void 0 ? void 0 : _a.refreshState) { this.focusTracker.refreshState(); } } }