dockview-core
Version:
Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript
215 lines (214 loc) • 10.2 kB
JavaScript
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;
}
}