UNPKG

dockview-core

Version:

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

215 lines (214 loc) 10.2 kB
import { addDisposableListener, Emitter, Event } from '../../../events'; import { CompositeDisposable } from '../../../lifecycle'; import { getPanelData, LocalSelectionTransfer, PanelTransfer, } from '../../../dnd/dataTransfer'; import { toggleClass } from '../../../dom'; import { html5Backend, pointerBackend, } from '../../../dnd/backend'; import { LongPressDetector } from '../../../dnd/pointer/longPress'; import { resolveDndCapabilities } from '../../dndCapabilities'; export class Tab extends CompositeDisposable { get element() { return this._element; } constructor(panel, accessor, group) { super(); this.panel = panel; this.accessor = accessor; this.group = group; this.content = undefined; this.panelTransfer = LocalSelectionTransfer.getInstance(); this._direction = 'horizontal'; this._onPointDown = new Emitter(); this.onPointerDown = this._onPointDown.event; this._onTabClick = new Emitter(); this.onTabClick = this._onTabClick.event; this._onDropped = new Emitter(); this.onDrop = this._onDropped.event; this._onDragStart = new Emitter(); this.onDragStart = this._onDragStart.event; this._onDragEnd = new Emitter(); this.onDragEnd = this._onDragEnd.event; const caps = resolveDndCapabilities(this.accessor.options); this._element = document.createElement('div'); this._element.className = 'dv-tab'; this._element.tabIndex = 0; this._element.draggable = caps.html5; toggleClass(this.element, 'dv-inactive-tab', true); const canDisplayOverlay = (event, position) => { var _a; if (this.group.locked) { return false; } const data = getPanelData(); if (data && this.accessor.id === data.viewId) { // Smooth-reorder takes over the in-flight visual when active, // so individual tab overlays are suppressed for internal drags. if (((_a = this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) === 'smooth') { return false; } return true; } return this.group.model.canDisplayOverlay(event, position, 'tab'); }; this.dropTarget = html5Backend.createDropTarget(this._element, { acceptedTargetZones: ['left', 'right'], overlayModel: this._buildOverlayModel(), canDisplayOverlay, getOverrideTarget: () => { var _a; return (_a = group.model.dropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; }, }); this.pointerDropTarget = pointerBackend.createDropTarget(this._element, { acceptedTargetZones: ['left', 'right'], overlayModel: this._buildOverlayModel(), canDisplayOverlay, getOverrideTarget: () => { var _a; return (_a = group.model.dropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; }, }); const sharedDragOptions = { getData: () => { this.panelTransfer.setData([ new PanelTransfer(this.accessor.id, this.group.id, this.panel.id), ], PanelTransfer.prototype); return { dispose: () => { this.panelTransfer.clearData(PanelTransfer.prototype); }, }; }, // 30/-10 matches the HTML5 setDragImage offset that has been // shipped for years; pointer backend wraps in PointerGhost, // HTML5 backend feeds into setDragImage. createGhost: () => ({ element: this._buildGhostElement(), offsetX: 30, offsetY: -10, }), onDragStart: (event) => { var _a; this._onDragStart.fire(event); if (!(event instanceof PointerEvent) && ((_a = this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) === 'smooth') { // Delay collapse to next frame so the browser // captures the full drag image first. requestAnimationFrame(() => { toggleClass(this.element, 'dv-tab--dragging', true); }); } }, onDragEnd: (event) => { this._onDragEnd.fire(event); }, }; this.html5DragSource = html5Backend.createDragSource(this._element, Object.assign(Object.assign({}, sharedDragOptions), { disabled: !caps.html5 })); this.pointerDragSource = pointerBackend.createDragSource(this._element, Object.assign(Object.assign({}, sharedDragOptions), { disabled: !caps.pointer, touchOnly: !caps.pointerHandlesMouse, isCancelled: () => !resolveDndCapabilities(this.accessor.options).pointer })); // Both droptargets feed the same downstream stream; consumers don't // need to know which path produced the overlay. this.onWillShowOverlay = Event.any(this.dropTarget.onWillShowOverlay, this.pointerDropTarget.onWillShowOverlay); this.addDisposables(this._onPointDown, this._onTabClick, this._onDropped, this._onDragStart, this._onDragEnd, this.accessor.onDidOptionsChange(() => { const model = this._buildOverlayModel(); this.dropTarget.setOverlayModel(model); this.pointerDropTarget.setOverlayModel(model); }), addDisposableListener(this._element, 'dragend', () => { // The shared onDragEnd handler already fires _onDragEnd via // the HTML5 backend; just strip the dragging class here. toggleClass(this.element, 'dv-tab--dragging', false); }), this.html5DragSource, addDisposableListener(this._element, 'pointerdown', (event) => { this._onPointDown.fire(event); }), addDisposableListener(this._element, 'click', (event) => { this._onTabClick.fire(event); }), addDisposableListener(this._element, 'contextmenu', (event) => { this.accessor.contextMenuController.show(this.panel, this.group, event); }), new LongPressDetector(this._element, { onLongPress: (event) => { // Don't let a subsequent finger move arm a drag on top // of the just-opened menu. this.pointerDragSource.cancelPending(); this.accessor.contextMenuController.show(this.panel, this.group, event); }, }), this.dropTarget.onDrop((event) => { this._onDropped.fire(event); }), this.pointerDropTarget.onDrop((event) => { this._onDropped.fire(event); }), this.dropTarget, this.pointerDropTarget, this.pointerDragSource); } setActive(isActive) { toggleClass(this.element, 'dv-active-tab', isActive); toggleClass(this.element, 'dv-inactive-tab', !isActive); } setContent(part) { if (this.content) { this._element.removeChild(this.content.element); } this.content = part; this._element.appendChild(this.content.element); } _buildOverlayModel() { var _a; // 'line' themes render a 4px insertion strip at the tab edge via the // anchor container's small-boundary path. 'fill' themes render a // half-width highlighted area, so we disable the small-boundary path // entirely (boundary = 0 ⟹ isSmall always false). const smallBoundary = ((_a = this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.dndTabIndicator) === 'line' ? Number.POSITIVE_INFINITY : 0; return { activationSize: { value: 50, type: 'percentage' }, smallWidthBoundary: smallBoundary, smallHeightBoundary: smallBoundary, }; } setDirection(direction) { this._direction = direction; const zones = direction === 'vertical' ? ['top', 'bottom'] : ['left', 'right']; this.dropTarget.setTargetZones(zones); this.pointerDropTarget.setTargetZones(zones); } updateDragAndDropState() { const caps = resolveDndCapabilities(this.accessor.options); this._element.draggable = caps.html5; this.html5DragSource.setDisabled(!caps.html5); this.pointerDragSource.setDisabled(!caps.pointer); this.pointerDragSource.setTouchOnly(!caps.pointerHandlesMouse); } /** * Vertical tabs are flipped to horizontal so the ghost stays readable * during the drag rather than appearing sideways-rotated. */ _buildGhostElement() { const style = getComputedStyle(this.element); const newNode = this.element.cloneNode(true); const isVertical = this._direction === 'vertical'; const verticalSkip = new Set([ 'writing-mode', 'inline-size', 'block-size', 'min-inline-size', 'min-block-size', 'max-inline-size', 'max-block-size', 'margin-inline', 'margin-inline-start', 'margin-inline-end', 'margin-block', 'margin-block-start', 'margin-block-end', 'padding-inline', 'padding-inline-start', 'padding-inline-end', 'padding-block', 'padding-block-start', 'padding-block-end', ]); Array.from(style).forEach((key) => { if (isVertical && verticalSkip.has(key)) { return; } newNode.style.setProperty(key, style.getPropertyValue(key), style.getPropertyPriority(key)); }); if (isVertical) { newNode.style.setProperty('writing-mode', 'horizontal-tb'); newNode.style.setProperty('width', style.height); newNode.style.setProperty('height', style.width); } newNode.style.position = 'absolute'; newNode.classList.add('dv-tab-ghost-drag'); return newNode; } }