dockview-core
Version:
Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript
242 lines (241 loc) • 10.9 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PointerDragController = void 0;
var dom_1 = require("../../dom");
var events_1 = require("../../events");
var lifecycle_1 = require("../../lifecycle");
/**
* Singleton — only one pointer-driven drag active at a time.
*
* State is shared across every Dockview instance on the page. Targets
* from instance B receive hit-tests from drags originating in instance A;
* that's intentional for cross-instance drops since `LocalSelectionTransfer`
* is also process-wide. The corollary is that every Tabs subscriber to
* `onDragMove` fires for every pointer drag globally — each subscriber
* hit-tests against its own DOM, so this is O(N) per pointermove where N
* is the number of registered listeners across all instances.
*/
var PointerDragController = /** @class */ (function (_super) {
__extends(PointerDragController, _super);
function PointerDragController() {
var _this = _super.call(this) || this;
_this._targets = new Set();
/** Kept in sync with `_targets` so hit-testing is allocation-free. */
_this._targetByElement = new Map();
_this._onDragStart = new events_1.Emitter();
_this.onDragStart = _this._onDragStart.event;
_this._onDragMove = new events_1.Emitter();
_this.onDragMove = _this._onDragMove.event;
_this._onDragEnd = new events_1.Emitter();
_this.onDragEnd = _this._onDragEnd.event;
_this.addDisposables(_this._onDragStart, _this._onDragMove, _this._onDragEnd);
return _this;
}
PointerDragController.getInstance = function () {
if (!PointerDragController._instance) {
PointerDragController._instance = new PointerDragController();
}
return PointerDragController._instance;
};
Object.defineProperty(PointerDragController.prototype, "active", {
get: function () {
return this._active;
},
enumerable: false,
configurable: true
});
PointerDragController.prototype.registerTarget = function (target) {
var _this = this;
this._targets.add(target);
this._targetByElement.set(target.element, target);
return {
dispose: function () {
_this._targets.delete(target);
if (_this._targetByElement.get(target.element) === target) {
_this._targetByElement.delete(target.element);
}
if (_this._currentTarget === target) {
_this._currentTarget = undefined;
}
},
};
};
PointerDragController.prototype.beginDrag = function (args) {
var _this = this;
var _a, _b, _c;
if (this._active) {
this.cancel();
}
var pointerEvent = args.pointerEvent, source = args.source;
// Call `getData()` before mutating controller state — a throw
// here would otherwise leave `_active` populated with no window
// listeners installed, blocking every subsequent drag.
var dataDisposable = args.getData();
this._active = {
pointerId: pointerEvent.pointerId,
startX: pointerEvent.clientX,
startY: pointerEvent.clientY,
source: source,
};
this._onDragMoveCallback = args.onDragMove;
this._onDragEndCallback = args.onDragEnd;
this._dataDisposable = dataDisposable;
this._ghost = args.ghost;
// Iframes capture pointermove once the cursor crosses into them,
// which would freeze the drag from the parent window's POV.
this._iframeShield = (0, dom_1.disableIframePointEvents)((_a = source.ownerDocument) !== null && _a !== void 0 ? _a : document);
var startEvent = {
clientX: pointerEvent.clientX,
clientY: pointerEvent.clientY,
pointerEvent: pointerEvent,
};
this._onDragStart.fire(startEvent);
// Source's owning window — popout drags fire on their own window,
// not the main one.
var targetWindow = (_c = (_b = source.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView) !== null && _c !== void 0 ? _c : window;
this._moveListener = (0, events_1.addDisposableListener)(targetWindow, 'pointermove', function (e) {
if (!_this._active || e.pointerId !== _this._active.pointerId) {
return;
}
_this._handleMove(e);
});
this._upListener = (0, events_1.addDisposableListener)(targetWindow, 'pointerup', function (e) {
if (!_this._active || e.pointerId !== _this._active.pointerId) {
return;
}
_this._handleEnd(e, true);
});
this._cancelListener = (0, events_1.addDisposableListener)(targetWindow, 'pointercancel', function (e) {
if (!_this._active || e.pointerId !== _this._active.pointerId) {
return;
}
_this._handleEnd(e, false);
});
};
PointerDragController.prototype.cancel = function () {
var _a, _b;
if (!this._active) {
return;
}
(_a = this._currentTarget) === null || _a === void 0 ? void 0 : _a.handleDragLeave();
this._teardown();
(_b = this._dataDisposable) === null || _b === void 0 ? void 0 : _b.dispose();
this._dataDisposable = undefined;
};
PointerDragController.prototype._findTargetUnder = function (x, y) {
var e_1, _a;
var _b, _c;
// `elementsFromPoint` is topmost-first; walk up to find the closest
// registered ancestor (so a tab beats the layout-root that contains it).
// Use the source's owning document so popout drags hit their own targets.
var sourceDoc = (_c = (_b = this._active) === null || _b === void 0 ? void 0 : _b.source.ownerDocument) !== null && _c !== void 0 ? _c : document;
var elements = sourceDoc.elementsFromPoint(x, y);
try {
for (var elements_1 = __values(elements), elements_1_1 = elements_1.next(); !elements_1_1.done; elements_1_1 = elements_1.next()) {
var el = elements_1_1.value;
var current = el;
while (current) {
var target = this._targetByElement.get(current);
if (target) {
return target;
}
current = current.parentElement;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (elements_1_1 && !elements_1_1.done && (_a = elements_1.return)) _a.call(elements_1);
}
finally { if (e_1) throw e_1.error; }
}
return undefined;
};
PointerDragController.prototype._handleMove = function (e) {
var _a, _b, _c;
(_a = this._ghost) === null || _a === void 0 ? void 0 : _a.update(e.clientX, e.clientY);
var dragEvent = {
clientX: e.clientX,
clientY: e.clientY,
pointerEvent: e,
};
var newTarget = this._findTargetUnder(e.clientX, e.clientY);
if (newTarget !== this._currentTarget) {
(_b = this._currentTarget) === null || _b === void 0 ? void 0 : _b.handleDragLeave();
this._currentTarget = newTarget;
}
if (newTarget) {
newTarget.handleDragOver(dragEvent);
}
(_c = this._onDragMoveCallback) === null || _c === void 0 ? void 0 : _c.call(this, dragEvent);
this._onDragMove.fire(dragEvent);
};
PointerDragController.prototype._handleEnd = function (e, dropped) {
var _a;
var dragEvent = {
clientX: e.clientX,
clientY: e.clientY,
pointerEvent: e,
};
if (dropped && this._currentTarget) {
this._currentTarget.handleDrop(dragEvent);
}
else {
(_a = this._currentTarget) === null || _a === void 0 ? void 0 : _a.handleDragLeave();
}
var onEnd = this._onDragEndCallback;
var dataDisposable = this._dataDisposable;
this._teardown();
this._dataDisposable = undefined;
// Defer disposal so drop handlers can still read the transfer data.
setTimeout(function () { return dataDisposable === null || dataDisposable === void 0 ? void 0 : dataDisposable.dispose(); }, 0);
onEnd === null || onEnd === void 0 ? void 0 : onEnd(dragEvent, dropped);
this._onDragEnd.fire(dragEvent);
};
PointerDragController.prototype._teardown = function () {
var _a, _b, _c, _d, _e;
this._currentTarget = undefined;
this._active = undefined;
this._onDragMoveCallback = undefined;
this._onDragEndCallback = undefined;
(_a = this._ghost) === null || _a === void 0 ? void 0 : _a.dispose();
this._ghost = undefined;
(_b = this._iframeShield) === null || _b === void 0 ? void 0 : _b.release();
this._iframeShield = undefined;
(_c = this._moveListener) === null || _c === void 0 ? void 0 : _c.dispose();
(_d = this._upListener) === null || _d === void 0 ? void 0 : _d.dispose();
(_e = this._cancelListener) === null || _e === void 0 ? void 0 : _e.dispose();
this._moveListener = undefined;
this._upListener = undefined;
this._cancelListener = undefined;
};
return PointerDragController;
}(lifecycle_1.CompositeDisposable));
exports.PointerDragController = PointerDragController;