UNPKG

dockview-core

Version:

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

229 lines (228 loc) 8.92 kB
import { findRelativeZIndexParent } from '../dom'; function popoverZIndexFor(target) { if (!(target instanceof HTMLElement)) { return undefined; } // Floating overlays live in the shell as siblings of the popover anchor // and the AriaLevelTracker sets their inline z-index. Without this, a // popover opened from inside a floating group would render behind it // because they share the shell stacking context. const relativeParent = findRelativeZIndexParent(target); return (relativeParent === null || relativeParent === void 0 ? void 0 : relativeParent.style.zIndex) ? `calc(${relativeParent.style.zIndex} * 2)` : undefined; } let _nextId = 0; const nextContextMenuItemId = () => `dv-ctx-menu-item-${_nextId++}`; function isItemConfig(item) { return typeof item === 'object'; } function buildItem(label, close, action, disabled) { const el = document.createElement('div'); el.className = 'dv-context-menu-item'; el.setAttribute('role', 'menuitem'); if (disabled) { el.classList.add('dv-context-menu-item--disabled'); el.setAttribute('aria-disabled', 'true'); } el.textContent = label; if (!disabled) { el.addEventListener('click', () => { action(); close(); }); } return el; } function buildSeparator() { const el = document.createElement('div'); el.className = 'dv-context-menu-separator'; el.setAttribute('role', 'separator'); return el; } function isCoarsePrimaryInput() { if (typeof window === 'undefined' || !window.matchMedia) { return false; } const coarse = window.matchMedia('(pointer: coarse)').matches; const fine = window.matchMedia('(pointer: fine)').matches; return coarse && !fine; } function buildRenameInput(tabGroup) { const wrapper = document.createElement('div'); wrapper.className = 'dv-context-menu-rename'; const input = document.createElement('input'); input.className = 'dv-context-menu-rename-input'; input.type = 'text'; input.placeholder = 'Name This Group'; input.value = tabGroup.label; input.addEventListener('input', () => { tabGroup.setLabel(input.value); }); input.addEventListener('keydown', (e) => { if (e.key !== 'Escape' && e.key !== 'Enter') { e.stopPropagation(); } }); input.addEventListener('click', (e) => { e.stopPropagation(); }); wrapper.appendChild(input); // Skip auto-focus on touch-primary devices: focusing the input pops the // on-screen keyboard, which fires `window resize`, which `PopupService` // listens to and uses to dismiss the popover — so the menu opens, the // keyboard appears, and the menu immediately closes before the user can // type. The user can still tap the input to focus it intentionally. if (!isCoarsePrimaryInput()) { requestAnimationFrame(() => { input.focus(); input.select(); }); } return wrapper; } function buildColorPicker(tabGroup, palette) { const wrapper = document.createElement('div'); wrapper.className = 'dv-context-menu-color-picker'; if (!palette.enabled) { // Opt-out: render no swatches. Returning a wrapper rather than null // keeps the call site simple; the wrapper is empty and visually inert. return wrapper; } for (const entry of palette.entries()) { const swatch = document.createElement('div'); swatch.className = 'dv-context-menu-color-swatch'; // Use a CSS custom property rather than setting `backgroundColor` // directly: the IDL setter validates the value against a color // grammar and rejects `var(...)` references in some environments // (notably jsdom; some browsers have historically had similar // quirks). The matching SCSS rule reads the var at use time. swatch.style.setProperty('--dv-tab-group-color', entry.value); if (entry.label) { swatch.title = entry.label; } if (tabGroup.color === entry.id) { swatch.classList.add('dv-context-menu-color-swatch--selected'); } swatch.addEventListener('click', () => { tabGroup.setColor(entry.id); }); wrapper.appendChild(swatch); } return wrapper; } export class ContextMenuController { constructor(accessor) { this.accessor = accessor; } show(panel, group, event) { var _a, _b; if (!this.accessor.options.getTabContextMenuItems) { return; } const items = this.accessor.options.getTabContextMenuItems({ panel, group, api: this.accessor.api, event, }); if (items.length === 0) { return; } event.preventDefault(); const popupService = this.accessor.getPopupServiceForGroup(group); const close = () => popupService.close(); const menuEl = document.createElement('div'); menuEl.className = 'dv-context-menu'; menuEl.setAttribute('role', 'menu'); for (const item of items) { if (item === 'separator') { menuEl.appendChild(buildSeparator()); } else if (item === 'close') { menuEl.appendChild(buildItem('Close', close, () => panel.api.close())); } else if (item === 'closeOthers') { menuEl.appendChild(buildItem('Close Others', close, () => { group.panels .filter((p) => p !== panel) .forEach((p) => p.api.close()); })); } else if (item === 'closeAll') { menuEl.appendChild(buildItem('Close All', close, () => { [...group.panels].forEach((p) => p.api.close()); })); } else if (isItemConfig(item) && item.element) { menuEl.appendChild(item.element); } else if (isItemConfig(item) && item.component) { const renderer = (_b = (_a = this.accessor.options).createContextMenuItemComponent) === null || _b === void 0 ? void 0 : _b.call(_a, { id: nextContextMenuItemId(), component: item.component, }); if (renderer) { renderer.init({ panel, group, api: this.accessor.api, close, componentProps: item.componentProps, }); menuEl.appendChild(renderer.element); } } else if (isItemConfig(item) && item.label) { menuEl.appendChild(buildItem(item.label, close, () => { var _a; return (_a = item.action) === null || _a === void 0 ? void 0 : _a.call(item); }, item.disabled)); } } popupService.openPopover(menuEl, { x: event.clientX, y: event.clientY, zIndex: popoverZIndexFor(event.target), }); } showForChip(tabGroup, group, event) { if (!this.accessor.options.getTabGroupChipContextMenuItems) { return; } const items = this.accessor.options.getTabGroupChipContextMenuItems({ tabGroup, group, api: this.accessor.api, event, }); if (items.length === 0) { return; } event.preventDefault(); const popupService = this.accessor.getPopupServiceForGroup(group); const close = () => popupService.close(); const menuEl = document.createElement('div'); menuEl.className = 'dv-context-menu'; menuEl.setAttribute('role', 'menu'); for (const item of items) { if (item === 'separator') { menuEl.appendChild(buildSeparator()); } else if (item === 'rename') { menuEl.appendChild(buildRenameInput(tabGroup)); } else if (item === 'colorPicker') { menuEl.appendChild(buildColorPicker(tabGroup, this.accessor.tabGroupColorPalette)); } else if (isItemConfig(item) && item.element) { menuEl.appendChild(item.element); } else if (isItemConfig(item) && item.label) { menuEl.appendChild(buildItem(item.label, close, () => { var _a; return (_a = item.action) === null || _a === void 0 ? void 0 : _a.call(item); }, item.disabled)); } } popupService.openPopover(menuEl, { x: event.clientX, y: event.clientY, zIndex: popoverZIndexFor(event.target), }); } }