@zag-js/splitter
Version:
Core logic for the splitter widget implemented as a state machine
207 lines (205 loc) • 8.27 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/utils/registry.ts
var registry_exports = {};
__export(registry_exports, {
SplitterRegistry: () => SplitterRegistry,
registry: () => registry
});
module.exports = __toCommonJS(registry_exports);
var import_dom_query = require("@zag-js/dom-query");
var import_intersects = require("./intersects.js");
var import_stacking_order = require("./stacking-order.js");
var SplitterRegistry = class {
constructor(options = {}) {
__publicField(this, "handles", /* @__PURE__ */ new Map());
__publicField(this, "state", {
activeHandleIds: /* @__PURE__ */ new Set(),
isPointerDown: false
});
__publicField(this, "listenerAttached", false);
__publicField(this, "options");
__publicField(this, "handlePointerMove", (event) => {
if (this.state.isPointerDown) return;
const pointerType = this.getPointerType(event);
const intersecting = this.findHitHandles(event.clientX, event.clientY, pointerType);
const newActiveIds = new Set(intersecting.map((h) => h.id));
const changed = newActiveIds.size !== this.state.activeHandleIds.size || [...newActiveIds].some((id) => !this.state.activeHandleIds.has(id));
if (changed) {
this.state.activeHandleIds = newActiveIds;
this.updateCursor(intersecting);
}
});
__publicField(this, "handlePointerDown", (event) => {
const pointerType = this.getPointerType(event);
const intersecting = this.findIntersectingHandles(event.clientX, event.clientY, pointerType, event.target);
if (intersecting.length > 0) {
this.state.isPointerDown = true;
this.state.activeHandleIds = new Set(intersecting.map((h) => h.id));
const point = { x: event.clientX, y: event.clientY };
intersecting.forEach((handle) => {
handle.onActivate(point);
});
this.updateCursor(intersecting);
}
});
__publicField(this, "handlePointerUp", (_event) => {
if (this.state.isPointerDown) {
this.state.isPointerDown = false;
this.handles.forEach((handle) => {
if (this.state.activeHandleIds.has(handle.id)) {
handle.onDeactivate();
}
});
this.state.activeHandleIds.clear();
this.clearGlobalCursor();
}
});
__publicField(this, "globalCursorId", "splitter-registry-cursor");
this.options = {
nonce: options.nonce ?? "",
hitAreaMargins: {
coarse: options.hitAreaMargins?.coarse ?? 15,
fine: options.hitAreaMargins?.fine ?? 5
}
};
}
register(data) {
this.handles.set(data.id, data);
this.attachGlobalListeners();
return () => {
this.handles.delete(data.id);
this.state.activeHandleIds.delete(data.id);
if (this.handles.size === 0) {
this.detachGlobalListeners();
}
};
}
attachGlobalListeners() {
if (this.listenerAttached) return;
this.doc.addEventListener("pointermove", this.handlePointerMove, true);
this.doc.addEventListener("pointerdown", this.handlePointerDown, true);
this.doc.addEventListener("pointerup", this.handlePointerUp, true);
this.listenerAttached = true;
}
detachGlobalListeners() {
if (!this.listenerAttached) return;
this.doc.removeEventListener("pointermove", this.handlePointerMove, true);
this.doc.removeEventListener("pointerdown", this.handlePointerDown, true);
this.doc.removeEventListener("pointerup", this.handlePointerUp, true);
this.listenerAttached = false;
}
getPointerType(event) {
return event.pointerType === "touch" || event.pointerType === "pen" ? "coarse" : "fine";
}
get doc() {
const firstHandle = this.handles.values().next().value;
return (0, import_dom_query.getDocument)(firstHandle?.element);
}
/**
* Fast hit-test: only checks pointer proximity to handles (no stacking order).
* Used for pointermove cursor feedback.
*/
findHitHandles(x, y, pointerType) {
const intersecting = [];
const margin = this.options.hitAreaMargins[pointerType];
this.handles.forEach((handle) => {
const rect = handle.element.getBoundingClientRect();
const hit = x >= rect.left - margin && x <= rect.right + margin && y >= rect.top - margin && y <= rect.bottom + margin;
if (hit) intersecting.push(handle);
});
return intersecting;
}
/**
* Full intersection check: hit-test + stacking order verification.
* Used for pointerdown activation where correctness matters.
*/
findIntersectingHandles(x, y, pointerType, eventTarget) {
const hits = this.findHitHandles(x, y, pointerType);
const targetElement = (0, import_dom_query.isElement)(eventTarget) ? eventTarget : null;
if (!targetElement || !(0, import_dom_query.contains)(this.doc, targetElement)) return hits;
return hits.filter((handle) => {
const dragHandleElement = handle.element;
if (targetElement === dragHandleElement || (0, import_dom_query.contains)(dragHandleElement, targetElement) || (0, import_dom_query.contains)(targetElement, dragHandleElement)) {
return true;
}
try {
if ((0, import_stacking_order.compareStackingOrder)(targetElement, dragHandleElement) > 0) {
const dragHandleRect = dragHandleElement.getBoundingClientRect();
let currentElement = targetElement;
while (currentElement) {
if (currentElement.contains(dragHandleElement)) break;
const currentRect = currentElement.getBoundingClientRect();
if ((0, import_intersects.intersects)((0, import_intersects.toRect)(currentRect), (0, import_intersects.toRect)(dragHandleRect), true)) {
return false;
}
currentElement = (0, import_dom_query.getParentElement)(currentElement);
}
}
} catch {
}
return true;
});
}
updateCursor(intersecting) {
if (intersecting.length === 0) {
this.clearGlobalCursor();
return;
}
const hasHorizontal = intersecting.some((h) => h.orientation === "horizontal");
const hasVertical = intersecting.some((h) => h.orientation === "vertical");
let cursor = "default";
if (hasHorizontal && hasVertical) {
cursor = "move";
} else if (hasHorizontal) {
cursor = "ew-resize";
} else if (hasVertical) {
cursor = "ns-resize";
}
this.setGlobalCursor(cursor);
}
setGlobalCursor(cursor) {
const doc = this.doc;
let styleEl = doc.getElementById(this.globalCursorId);
const textContent = `* { cursor: ${cursor} !important; }`;
if (styleEl) {
styleEl.textContent = textContent;
} else {
styleEl = doc.createElement("style");
styleEl.id = this.globalCursorId;
styleEl.textContent = textContent;
if (this.options.nonce) {
styleEl.nonce = this.options.nonce;
}
doc.head.appendChild(styleEl);
}
}
clearGlobalCursor() {
const styleEl = this.doc.getElementById(this.globalCursorId);
styleEl?.remove();
}
};
var registry = (opts = {}) => new SplitterRegistry(opts);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SplitterRegistry,
registry
});