Files
SnowLuma/.pnpm-store/v11/files/d1/c07072f932f55d5944d4f6e060a2c5c11170cba45e76b107636a187fa2453ae24ac5574a6ccf1cbf45162f0b481b6d63d1b08bb1a7397e58fc5125cae5d590
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

462 lines
21 KiB
Plaintext

/**
* dd-draggable.ts 12.6.0
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
*/
import { DDManager } from './dd-manager';
import { Utils } from './utils';
import { DDBaseImplement } from './dd-base-impl';
import { isTouch, touchend, touchmove, touchstart, pointerdown, DDTouch } from './dd-touch';
// make sure we are not clicking on known object that handles mouseDown
const skipMouseDown = 'input,textarea,button,select,option,[contenteditable="true"],.ui-resizable-handle';
// let count = 0; // TEST
class DDDraggable extends DDBaseImplement {
constructor(el, option = {}) {
super();
this.el = el;
this.option = option;
/** @internal */
this.dragTransform = {
xScale: 1,
yScale: 1,
xOffset: 0,
yOffset: 0
};
/** @internal single tick of the auto-scroll animation loop */
this._autoScrollTick = () => {
const el = this.helper;
const scrollCont = this._autoScrollContainer;
if (!el || !scrollCont) {
this._stopScrolling();
return;
}
const clipping = this._getClipping(el, scrollCont);
if (clipping === 0) {
this._stopScrolling();
return;
}
if (!this._autoScrollMaxSpeed) {
const viewportH = window.innerHeight || document.documentElement.clientHeight;
this._autoScrollMaxSpeed = Math.max(viewportH / 150, 4);
}
const absPx = Math.abs(clipping);
const speed = Math.min(absPx * 0.5, this._autoScrollMaxSpeed);
const scrollAmount = clipping > 0 ? speed : -speed;
const prevScroll = scrollCont.scrollTop;
scrollCont.scrollTop += scrollAmount;
if (scrollCont.scrollTop === prevScroll) {
this._stopScrolling();
return;
}
if (this.dragging && this.lastDrag) {
this._dragFollow(this.lastDrag);
this._callDrag(this.lastDrag);
}
this._autoScrollAnimId = requestAnimationFrame(this._autoScrollTick);
};
// get the element that is actually supposed to be dragged by
const handleName = option?.handle?.substring(1);
const n = el.gridstackNode;
this.dragEls = !handleName || el.classList.contains(handleName) ? [el] : (n?.subGrid ? [el.querySelector(option.handle) || el] : this.getAllHandles());
if (this.dragEls.length === 0) {
this.dragEls = [el];
}
// create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions)
this._mouseDown = this._mouseDown.bind(this);
this._mouseMove = this._mouseMove.bind(this);
this._mouseUp = this._mouseUp.bind(this);
this._keyEvent = this._keyEvent.bind(this);
this.enable();
}
/** return all handles omitting other nested `.grid-stack-item` children (in case node.subGrid isn't set for some reason) */
getAllHandles() {
return Array.from(this.el.querySelectorAll(this.option.handle)).filter((node) => {
if (!(node instanceof HTMLElement))
return false;
const owner = node.closest('.grid-stack-item');
return owner === this.el || !owner;
});
}
on(event, callback) {
super.on(event, callback);
}
off(event) {
super.off(event);
}
enable() {
if (this.disabled === false)
return;
super.enable();
this.dragEls.forEach(dragEl => {
dragEl.addEventListener('mousedown', this._mouseDown);
if (isTouch) {
dragEl.addEventListener('touchstart', touchstart);
dragEl.addEventListener('pointerdown', pointerdown);
// dragEl.style.touchAction = 'none'; // not needed unlike pointerdown doc comment
}
});
this.el.classList.remove('ui-draggable-disabled');
}
disable(forDestroy = false) {
if (this.disabled === true)
return;
super.disable();
this.dragEls.forEach(dragEl => {
dragEl.removeEventListener('mousedown', this._mouseDown);
if (isTouch) {
dragEl.removeEventListener('touchstart', touchstart);
dragEl.removeEventListener('pointerdown', pointerdown);
}
});
if (!forDestroy)
this.el.classList.add('ui-draggable-disabled');
}
destroy() {
if (this.dragTimeout)
window.clearTimeout(this.dragTimeout);
delete this.dragTimeout;
if (this.mouseDownEvent)
this._mouseUp(this.mouseDownEvent);
this.disable(true);
delete this.el;
delete this.helper;
delete this.option;
super.destroy();
}
updateOption(opts) {
Object.keys(opts).forEach(key => this.option[key] = opts[key]);
return this;
}
/** @internal call when mouse goes down before a dragstart happens */
_mouseDown(e) {
// if real brower event (trusted:true vs false for our simulated ones) and we didn't correctly clear the last touch event, clear things up
if (DDTouch.touchHandled && e.isTrusted)
DDTouch.touchHandled = false;
// don't let more than one widget handle mouseStart
if (DDManager.mouseHandled)
return;
if (e.button !== 0)
return true; // only left click
// make sure we are not clicking on known object that handles mouseDown, or ones supplied by the user
if (!this.dragEls.find(el => el === e.target) && e.target.closest(skipMouseDown))
return true;
if (this.option.cancel) {
if (e.target.closest(this.option.cancel))
return true;
}
this.mouseDownEvent = e;
delete this.dragging;
delete DDManager.dragElement;
delete DDManager.dropElement;
delete this._autoScrollMaxSpeed;
delete this._autoScrollContainer;
// document handler so we can continue receiving moves as the item is 'fixed' position, and capture=true so WE get a first crack
document.addEventListener('mousemove', this._mouseMove, { capture: true, passive: true }); // true=capture, not bubble
document.addEventListener('mouseup', this._mouseUp, true);
if (isTouch) {
e.currentTarget.addEventListener('touchmove', touchmove);
e.currentTarget.addEventListener('touchend', touchend);
}
e.preventDefault();
// preventDefault() prevents blur event which occurs just after mousedown event.
// if an editable content has focus, then blur must be call
if (document.activeElement)
document.activeElement.blur();
DDManager.mouseHandled = true;
return true;
}
/** @internal method to call actual drag event */
_callDrag(e) {
if (!this.dragging)
return;
const ev = Utils.initEvent(e, { target: this.el, type: 'drag' });
if (this.option.drag) {
this.option.drag(ev, this.ui());
}
this.triggerEvent('drag', ev);
}
/** @internal called when the main page (after successful mousedown) receives a move event to drag the item around the screen */
_mouseMove(e) {
// console.log(`${count++} move ${e.x},${e.y}`)
const s = this.mouseDownEvent;
this.lastDrag = e;
if (this.dragging) {
this._dragFollow(e);
// delay actual grid handling drag until we pause for a while if set
if (DDManager.pauseDrag) {
const pause = Number.isInteger(DDManager.pauseDrag) ? DDManager.pauseDrag : 100;
if (this.dragTimeout)
window.clearTimeout(this.dragTimeout);
this.dragTimeout = window.setTimeout(() => this._callDrag(e), pause);
}
else {
this._callDrag(e);
}
}
else if (Math.abs(e.x - s.x) + Math.abs(e.y - s.y) > 3) {
/**
* don't start unless we've moved at least 3 pixels
*/
this.dragging = true;
DDManager.dragElement = this;
// if we're dragging an actual grid item, set the current drop as the grid (to detect enter/leave)
const grid = this.el.gridstackNode?.grid;
if (grid) {
DDManager.dropElement = grid.el.ddElement.ddDroppable;
}
else {
delete DDManager.dropElement;
}
this.helper = this._createHelper();
this._setupHelperContainmentStyle();
this.dragTransform = Utils.getValuesFromTransformedElement(this.helperContainment);
this.dragOffset = this._getDragOffset(e, this.el, this.helperContainment);
this._setupHelperStyle(e);
const ev = Utils.initEvent(e, { target: this.el, type: 'dragstart' });
if (this.option.start) {
this.option.start(ev, this.ui());
}
this.triggerEvent('dragstart', ev);
// now track keyboard events to cancel or rotate
document.addEventListener('keydown', this._keyEvent);
}
// e.preventDefault(); // passive = true. OLD: was needed otherwise we get text sweep text selection as we drag around
return true;
}
/** @internal call when the mouse gets released to drop the item at current location */
_mouseUp(e) {
this._stopScrolling();
document.removeEventListener('mousemove', this._mouseMove, true);
document.removeEventListener('mouseup', this._mouseUp, true);
if (isTouch && e.currentTarget) { // destroy() during nested grid call us again wit fake _mouseUp
e.currentTarget.removeEventListener('touchmove', touchmove, true);
e.currentTarget.removeEventListener('touchend', touchend, true);
}
if (this.dragging) {
delete this.dragging;
delete this.el.gridstackNode?._origRotate;
document.removeEventListener('keydown', this._keyEvent);
// reset the drop target if dragging over ourself (already parented, just moving during stop callback below)
if (DDManager.dropElement?.el === this.el.parentElement) {
delete DDManager.dropElement;
}
this.helperContainment.style.position = this.parentOriginStylePosition || null;
if (this.helper !== this.el)
this.helper.remove(); // hide now
this._removeHelperStyle();
const ev = Utils.initEvent(e, { target: this.el, type: 'dragstop' });
if (this.option.stop) {
this.option.stop(ev); // NOTE: destroy() will be called when removing item, so expect NULL ptr after!
}
this.triggerEvent('dragstop', ev);
// call the droppable method to receive the item
if (DDManager.dropElement) {
DDManager.dropElement.drop(e);
}
}
delete this.helper;
delete this.mouseDownEvent;
delete DDManager.dragElement;
delete DDManager.dropElement;
delete DDManager.mouseHandled;
e.preventDefault();
}
/** @internal call when keys are being pressed - use Esc to cancel, R to rotate */
_keyEvent(e) {
const n = this.el.gridstackNode;
const grid = n?.grid || DDManager.dropElement?.el?.gridstack;
if (e.key === 'Escape') {
if (n && n._origRotate) {
n._orig = n._origRotate;
delete n._origRotate;
}
grid?.cancelDrag();
this._mouseUp(this.mouseDownEvent);
}
else if (n && grid && (e.key === 'r' || e.key === 'R')) {
if (!Utils.canBeRotated(n))
return;
n._origRotate = n._origRotate || { ...n._orig }; // store the real orig size in case we Esc after doing rotation
delete n._moving; // force rotate to happen (move waits for >50% coverage otherwise)
grid.setAnimation(false) // immediate rotate so _getDragOffset() gets the right dom size below
.rotate(n.el, {
top: -this.dragOffset.offsetTop,
left: -this.dragOffset.offsetX
})
.setAnimation();
n._moving = true;
this.dragOffset = this._getDragOffset(this.lastDrag, n.el, this.helperContainment);
this.helper.style.width = this.dragOffset.width + 'px';
this.helper.style.height = this.dragOffset.height + 'px';
Utils.swap(n._orig, 'w', 'h');
delete n._rect;
this._mouseMove(this.lastDrag);
}
}
/** @internal create a clone copy (or user defined method) of the original drag item if set */
_createHelper() {
let helper = this.el;
if (typeof this.option.helper === 'function') {
helper = this.option.helper(this.el);
}
else if (this.option.helper === 'clone') {
helper = Utils.cloneNode(this.el);
}
if (!helper.parentElement) {
Utils.appendTo(helper, this.option.appendTo === 'parent' ? this.el.parentElement : this.option.appendTo);
}
this.dragElementOriginStyle = DDDraggable.originStyleProp.map(prop => this.el.style[prop]);
return helper;
}
/** @internal set the fix position of the dragged item */
_setupHelperStyle(e) {
this.helper.classList.add('ui-draggable-dragging');
this.el.gridstackNode?.grid?.el.classList.add('grid-stack-dragging');
// TODO: set all at once with style.cssText += ... ? https://stackoverflow.com/questions/3968593
const style = this.helper.style;
style.pointerEvents = 'none'; // needed for over items to get enter/leave
// style.cursor = 'move'; // TODO: can't set with pointerEvents=none ! (no longer in CSS either as no-op)
style.width = this.dragOffset.width + 'px';
style.height = this.dragOffset.height + 'px';
style.willChange = 'left, right, top';
style.position = 'fixed'; // let us drag between grids by not clipping as parent .grid-stack is position: 'relative'
this._dragFollow(e); // now position it
style.transition = 'none'; // show up instantly
setTimeout(() => {
if (this.helper) {
style.transition = null; // recover animation
}
}, 0);
return this;
}
/** @internal restore back the original style before dragging */
_removeHelperStyle() {
this.helper.classList.remove('ui-draggable-dragging');
this.el.gridstackNode?.grid?.el.classList.remove('grid-stack-dragging');
const node = this.helper?.gridstackNode;
// don't bother restoring styles if we're gonna remove anyway...
if (!node?._isAboutToRemove && this.dragElementOriginStyle) {
const helper = this.helper;
// don't animate, otherwise we animate offseted when switching back to 'absolute' from 'fixed'.
// TODO: this also removes resizing animation which doesn't have this issue, but others.
// Ideally both would animate ('move' would immediately restore 'absolute' and adjust coordinate to match,
// then trigger a delay (repaint) to restore to final dest with animate) but then we need to make sure 'resizestop'
// is called AFTER 'transitionend' event is received (see https://github.com/gridstack/gridstack.js/issues/2033)
const transition = this.dragElementOriginStyle['transition'] || null;
helper.style.transition = this.dragElementOriginStyle['transition'] = 'none'; // can't be NULL #1973
DDDraggable.originStyleProp.forEach(prop => helper.style[prop] = this.dragElementOriginStyle[prop] || null);
setTimeout(() => helper.style.transition = transition, 50); // recover animation from saved vars after a pause (0 isn't enough #1973)
}
delete this.dragElementOriginStyle;
return this;
}
/** @internal updates the top/left position to follow the mouse */
_dragFollow(e) {
const style = this.helper.style;
const offset = this.dragOffset;
if (this.option.rtl) {
style.right = ((window.innerWidth - e.clientX) + offset.offsetX) * this.dragTransform.xScale + 'px';
if (style.left)
style.left = '';
}
else {
style.left = (e.clientX + offset.offsetX) * this.dragTransform.xScale + 'px';
if (style.right)
style.right = '';
}
style.top = (e.clientY + offset.offsetTop) * this.dragTransform.yScale + 'px';
}
/** @internal */
_setupHelperContainmentStyle() {
this.helperContainment = this.helper.parentElement;
if (this.helper.style.position !== 'fixed') {
this.parentOriginStylePosition = this.helperContainment.style.position;
if (getComputedStyle(this.helperContainment).position.match(/static/)) {
this.helperContainment.style.position = 'relative';
}
}
return this;
}
/** @internal */
_getDragOffset(event, el, parent) {
// in case ancestor has transform/perspective css properties that change the viewpoint
let xformOffsetX = 0;
let xformOffsetY = 0;
if (parent) {
xformOffsetX = this.dragTransform.xOffset;
xformOffsetY = this.dragTransform.yOffset;
}
const targetOffset = el.getBoundingClientRect();
let x = this.option.rtl ? targetOffset.right : targetOffset.left;
let offsetX = this.option.rtl
? (event.clientX - targetOffset.right + xformOffsetX)
: (-event.clientX + targetOffset.left - xformOffsetX);
return {
x,
top: targetOffset.top,
offsetX,
offsetTop: -event.clientY + targetOffset.top - xformOffsetY,
width: targetOffset.width * this.dragTransform.xScale,
height: targetOffset.height * this.dragTransform.yScale
};
}
/** @internal starts or continues auto-scroll when the dragged helper is clipped by the scroll container.
* Takes the grid's own element to find the scroll container so external/sidebar drags work too (#2074). */
updateScrollPosition(gridEl) {
this._autoScrollContainer = Utils.getScrollElement(gridEl); // always use latest active grid
const clipping = this._getClipping(this.helper, this._autoScrollContainer);
if (clipping === 0) {
this._stopScrolling();
}
else if (!this._autoScrollAnimId) {
this._autoScrollAnimId = requestAnimationFrame(this._autoScrollTick);
}
}
/** @internal compute how many pixels the element is clipped: negative = above, positive = below, 0 = fully inside OR outside (stop scrolling) */
_getClipping(el, scrollEl) {
const elRect = el.getBoundingClientRect();
const scrollRect = scrollEl.getBoundingClientRect();
const viewportH = window.innerHeight || document.documentElement.clientHeight;
if (elRect.bottom < scrollRect.top || elRect.top > scrollRect.bottom)
return 0; // fully outside
const clippedBelow = elRect.bottom - Math.min(scrollRect.bottom, viewportH);
const clippedAbove = elRect.top - Math.max(scrollRect.top, 0);
if (clippedAbove < 0)
return clippedAbove;
if (clippedBelow > 0)
return clippedBelow;
return 0;
}
/** @internal stop any active auto-scroll animation */
_stopScrolling() {
if (this._autoScrollAnimId) {
cancelAnimationFrame(this._autoScrollAnimId);
delete this._autoScrollAnimId;
}
}
/** @internal TODO: set to public as called by DDDroppable! */
ui() {
const containmentEl = this.el.parentElement;
const containmentRect = containmentEl.getBoundingClientRect();
const offset = this.helper.getBoundingClientRect();
// RTL: GridStack measures column positions from the right side of the container,
// so we report `left` as the distance between the helper's right edge and the
// container's right edge (both in viewport-left coordinates via getBoundingClientRect).
const leftPos = this.option.rtl
? (containmentRect.right - offset.right) * this.dragTransform.xScale
: (offset.left - containmentRect.left) * this.dragTransform.xScale;
return {
position: {
top: (offset.top - containmentRect.top) * this.dragTransform.yScale,
left: leftPos
}
/* not used by GridStack for now...
helper: [this.helper], //The object arr representing the helper that's being dragged.
offset: { top: offset.top, left: offset.left } // Current offset position of the helper as { top, left } object.
*/
};
}
}
/** @internal properties we change during dragging, and restore back */
DDDraggable.originStyleProp = ['width', 'height', 'transform', 'transform-origin', 'transition', 'pointerEvents', 'position', 'left', 'right', 'top', 'minWidth', 'willChange'];
export { DDDraggable };
//# sourceMappingURL=dd-draggable.js.map