UNPKG

dockview-core

Version:

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

205 lines (204 loc) 10.7 kB
import { getPanelData, LocalSelectionTransfer, PanelTransfer, } from '../../../dnd/dataTransfer'; import { html5Backend, pointerBackend, } from '../../../dnd/backend'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { CompositeDisposable, Disposable, MutableDisposable, } from '../../../lifecycle'; import { quasiPreventDefault, toggleClass } from '../../../dom'; import { resolveDndCapabilities } from '../../dndCapabilities'; // Floating-group redock via touch: require a deliberate long press so the // "move the float around" gesture doesn't double-trigger the redock ghost. // Infinity pressTolerance disables the pre-arm flick override; any motion // during the wait is treated as drag-the-float, not redock intent. const FLOATING_REDOCK_INITIATION_DELAY_MS = 500; export class VoidContainer extends CompositeDisposable { get element() { return this._element; } constructor(accessor, group) { var _a, _b; super(); this.accessor = accessor; this.group = group; this.panelTransfer = LocalSelectionTransfer.getInstance(); this._onDrop = new Emitter(); this.onDrop = this._onDrop.event; this._onDragStart = new Emitter(); this.onDragStart = this._onDragStart.event; const caps = resolveDndCapabilities(this.accessor.options); this._element = document.createElement('div'); this._element.className = 'dv-void-container'; this._element.draggable = caps.html5; toggleClass(this._element, 'dv-draggable', caps.html5 || caps.pointer); this.addDisposables(this._onDrop, this._onDragStart, addDisposableListener(this._element, 'pointerdown', () => { this.accessor.doSetGroupActive(this.group); }), // Shift+pointerdown marks the event so the group's overlay // drag (move-by-floating) sees it was consumed and doesn't // fire alongside the HTML5 drag. quasiPreventDefault sets the // marker without calling preventDefault — that would also // block dragstart, which we need to fire. addDisposableListener(this._element, 'pointerdown', (e) => { if (e.shiftKey) { quasiPreventDefault(e); } }, true)); const canDisplayOverlay = (event, position) => { if (this.group.api.locked) { // Dropping on the void/header space adds the panel // to this group, which `locked` is meant to prevent // (both `true` and `'no-drop-target'`). return false; } const data = getPanelData(); if (data && this.accessor.id === data.viewId) { return true; } return group.model.canDisplayOverlay(event, position, 'header_space'); }; this.dropTarget = html5Backend.createDropTarget(this._element, { acceptedTargetZones: ['center'], canDisplayOverlay, getOverrideTarget: () => { var _a; return (_a = group.model.dropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; }, }); this.pointerDropTarget = pointerBackend.createDropTarget(this._element, { acceptedTargetZones: ['center'], canDisplayOverlay, getOverrideTarget: () => { var _a; return (_a = group.model.dropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; }, }); const buildMultiPanelsGhost = () => { const ghostEl = document.createElement('div'); const style = window.getComputedStyle(this._element); const bgColor = style.getPropertyValue('--dv-activegroup-visiblepanel-tab-background-color'); const color = style.getPropertyValue('--dv-activegroup-visiblepanel-tab-color'); ghostEl.style.backgroundColor = bgColor; ghostEl.style.color = color; ghostEl.style.padding = '2px 8px'; ghostEl.style.height = '24px'; ghostEl.style.fontSize = '11px'; ghostEl.style.lineHeight = '20px'; ghostEl.style.borderRadius = '12px'; ghostEl.style.whiteSpace = 'nowrap'; ghostEl.style.boxSizing = 'border-box'; // HTML5 setDragImage snapshots the element as appended to the // document; a default block-level div would stretch to the // body's width and render as a viewport-wide bar. ghostEl.style.display = 'inline-block'; ghostEl.textContent = `Multiple Panels (${this.group.size})`; return ghostEl; }; const buildGhostSpec = () => { const createGhost = this.accessor.options.createGroupDragGhostComponent; if (createGhost) { const renderer = createGhost(this.group); renderer.init({ group: this.group, api: this.accessor.api, }); return { element: renderer.element, offsetX: 30, offsetY: -10, dispose: renderer.dispose ? () => { var _a; return (_a = renderer.dispose) === null || _a === void 0 ? void 0 : _a.call(renderer); } : undefined, }; } return { element: buildMultiPanelsGhost(), offsetX: 30, offsetY: -10, }; }; const sharedDragOptions = { getData: () => { this.panelTransfer.setData([new PanelTransfer(this.accessor.id, this.group.id, null)], PanelTransfer.prototype); return { dispose: () => { this.panelTransfer.clearData(PanelTransfer.prototype); }, }; }, createGhost: buildGhostSpec, onDragStart: (event) => { this._onDragStart.fire(event); }, }; this.html5DragSource = html5Backend.createDragSource(this._element, Object.assign(Object.assign({}, sharedDragOptions), { disabled: !caps.html5, isCancelled: (event) => { // HTML5: floating groups need shift+drag as the explicit // detach gesture (otherwise click-and-drag conflicts with // moving the floating group itself). if (this.group.api.location.type === 'floating' && !event.shiftKey) { return true; } if (this.group.api.location.type === 'edge' && this.group.size === 0) { return true; } return false; } })); const isFloating = () => { var _a, _b, _c; return ((_c = (_b = (_a = this.group) === null || _a === void 0 ? void 0 : _a.api) === null || _b === void 0 ? void 0 : _b.location) === null || _c === void 0 ? void 0 : _c.type) === 'floating'; }; this.pointerDragSource = pointerBackend.createDragSource(this._element, Object.assign(Object.assign({}, sharedDragOptions), { disabled: !caps.pointer, touchOnly: !caps.pointerHandlesMouse, // Floating groups share this element with the overlay's // move-the-float drag. Without a longer hold + tolerance // override, both gestures commit simultaneously and the // user sees the float follow their finger *and* a ghost. touchInitiationDelay: () => isFloating() ? FLOATING_REDOCK_INITIATION_DELAY_MS : 250, pressTolerance: () => (isFloating() ? Infinity : 8), isCancelled: () => { if (!resolveDndCapabilities(this.accessor.options).pointer) { return true; } // Pointer: long-press IS the deliberate gesture, so // floating groups don't need the shift gate. if (this.group.api.location.type === 'edge' && this.group.size === 0) { return true; } return false; }, onDragStart: (event) => { var _a; // Redock just committed — abort any in-flight overlay // move so the float stops following the finger while // the ghost takes over. (_a = this.getFloatingOverlay()) === null || _a === void 0 ? void 0 : _a.cancelPendingDrag(); this._onDragStart.fire(event); } })); // Mirror direction: once the overlay's move-the-float gesture has // actually moved something, cancel the pending redock arm so the // ghost doesn't appear mid-drag if the user holds past 500ms. const overlayMoveSub = new MutableDisposable(); const refreshOverlayMoveSub = () => { const overlay = this.getFloatingOverlay(); overlayMoveSub.value = overlay ? overlay.onDidStartMoving(() => { this.pointerDragSource.cancelPending(); }) : Disposable.NONE; }; refreshOverlayMoveSub(); this.addDisposables(overlayMoveSub); const locationChange = (_b = (_a = this.group) === null || _a === void 0 ? void 0 : _a.api) === null || _b === void 0 ? void 0 : _b.onDidLocationChange; if (locationChange) { this.addDisposables(locationChange(refreshOverlayMoveSub)); } this.onWillShowOverlay = Event.any(this.dropTarget.onWillShowOverlay, this.pointerDropTarget.onWillShowOverlay); this.addDisposables(this.html5DragSource, this.dropTarget.onDrop((event) => { this._onDrop.fire(event); }), this.pointerDropTarget.onDrop((event) => { this._onDrop.fire(event); }), this.dropTarget, this.pointerDropTarget, this.pointerDragSource); } updateDragAndDropState() { const caps = resolveDndCapabilities(this.accessor.options); this._element.draggable = caps.html5; toggleClass(this._element, 'dv-draggable', caps.html5 || caps.pointer); this.html5DragSource.setDisabled(!caps.html5); this.pointerDragSource.setDisabled(!caps.pointer); this.pointerDragSource.setTouchOnly(!caps.pointerHandlesMouse); } getFloatingOverlay() { var _a, _b; if (!this.group) { return undefined; } return (_b = (_a = this.accessor.floatingGroups) === null || _a === void 0 ? void 0 : _a.find((fg) => fg.group === this.group)) === null || _b === void 0 ? void 0 : _b.overlay; } }