gridstack
Version:
TypeScript/JS lib for dashboard layout and creation, responsive, mobile support, no external dependencies, with many wrappers (React, Angular, Vue, Ember, knockout...)
796 lines • 30.7 kB
JavaScript
/**
* utils.ts 12.4.1
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
*/
// /**
// * Checks for obsolete method names and provides deprecation warnings.
// * Creates a wrapper function that logs a deprecation warning when called.
// *
// * @param self the object context to apply the function to
// * @param f the new function to call
// * @param oldName the deprecated method name
// * @param newName the new method name to use instead
// * @param rev the version when the deprecation was introduced
// * @returns a wrapper function that warns about deprecation
// */
// eslint-disable-next-line
// export function obsolete(self, f, oldName: string, newName: string, rev: string): (...args: any[]) => any {
// const wrapper = (...args) => {
// console.warn('gridstack.js: Function `' + oldName + '` is deprecated in ' + rev + ' and has been replaced ' +
// 'with `' + newName + '`. It will be **removed** in a future release');
// return f.apply(self, args);
// }
// wrapper.prototype = f.prototype;
// return wrapper;
// }
// /**
// * Checks for obsolete grid options and migrates them to new names.
// * Automatically copies old option values to new option names and shows deprecation warnings.
// *
// * @param opts the options object to check and migrate
// * @param oldName the deprecated option name
// * @param newName the new option name to use instead
// * @param rev the version when the deprecation was introduced
// */
// export function obsoleteOpts(opts: GridStackOptions, oldName: string, newName: string, rev: string): void {
// if (opts[oldName] !== undefined) {
// opts[newName] = opts[oldName];
// console.warn('gridstack.js: Option `' + oldName + '` is deprecated in ' + rev + ' and has been replaced with `' +
// newName + '`. It will be **removed** in a future release');
// }
// }
// /**
// * Checks for obsolete grid options that have been completely removed.
// * Shows deprecation warnings for options that are no longer supported.
// *
// * @param opts the options object to check
// * @param oldName the removed option name
// * @param rev the version when the option was removed
// * @param info additional information about the removal
// */
// export function obsoleteOptsDel(opts: GridStackOptions, oldName: string, rev: string, info: string): void {
// if (opts[oldName] !== undefined) {
// console.warn('gridstack.js: Option `' + oldName + '` is deprecated in ' + rev + info);
// }
// }
// /**
// * Checks for obsolete HTML element attributes and migrates them.
// * Automatically copies old attribute values to new attribute names and shows deprecation warnings.
// *
// * @param el the HTML element to check and migrate
// * @param oldName the deprecated attribute name
// * @param newName the new attribute name to use instead
// * @param rev the version when the deprecation was introduced
// */
// export function obsoleteAttr(el: HTMLElement, oldName: string, newName: string, rev: string): void {
// const oldAttr = el.getAttribute(oldName);
// if (oldAttr !== null) {
// el.setAttribute(newName, oldAttr);
// console.warn('gridstack.js: attribute `' + oldName + '`=' + oldAttr + ' is deprecated on this object in ' + rev + ' and has been replaced with `' +
// newName + '`. It will be **removed** in a future release');
// }
// }
/**
* Collection of utility methods used throughout GridStack.
* These are general-purpose helper functions for DOM manipulation,
* positioning calculations, object operations, and more.
*/
export class Utils {
/**
* Convert a potential selector into an actual list of HTML elements.
* Supports CSS selectors, element references, and special ID handling.
*
* @param els selector string, HTMLElement, or array of elements
* @param root optional root element to search within (defaults to document, useful for shadow DOM)
* @returns array of HTML elements matching the selector
*
* @example
* const elements = Utils.getElements('.grid-item');
* const byId = Utils.getElements('#myWidget');
* const fromShadow = Utils.getElements('.item', shadowRoot);
*/
static getElements(els, root = document) {
if (typeof els === 'string') {
const doc = ('getElementById' in root) ? root : undefined;
// Note: very common for people use to id='1,2,3' which is only legal as HTML5 id, but not CSS selectors
// so if we start with a number, assume it's an id and just return that one item...
// see https://github.com/gridstack/gridstack.js/issues/2234#issuecomment-1523796562
if (doc && !isNaN(+els[0])) { // start with digit
const el = doc.getElementById(els);
return el ? [el] : [];
}
let list = root.querySelectorAll(els);
if (!list.length && els[0] !== '.' && els[0] !== '#') {
// see if mean to be a class
list = root.querySelectorAll('.' + els);
// else if mean to be an id
if (!list.length)
list = root.querySelectorAll('#' + els);
// else see if gs-id attribute
if (!list.length) {
const el = root.querySelector(`[gs-id="${els}"]`);
return el ? [el] : [];
}
}
return Array.from(list);
}
return [els];
}
/**
* Convert a potential selector into a single HTML element.
* Similar to getElements() but returns only the first match.
*
* @param els selector string or HTMLElement
* @param root optional root element to search within (defaults to document)
* @returns the first HTML element matching the selector, or null if not found
*
* @example
* const element = Utils.getElement('#myWidget');
* const first = Utils.getElement('.grid-item');
*/
static getElement(els, root = document) {
if (typeof els === 'string') {
const doc = ('getElementById' in root) ? root : undefined;
if (!els.length)
return null;
if (doc && els[0] === '#') {
return doc.getElementById(els.substring(1));
}
if (els[0] === '#' || els[0] === '.' || els[0] === '[') {
return root.querySelector(els);
}
// if we start with a digit, assume it's an id (error calling querySelector('#1')) as class are not valid CSS
if (doc && !isNaN(+els[0])) { // start with digit
return doc.getElementById(els);
}
// finally try string, then id, then class
let el = root.querySelector(els);
if (doc && !el) {
el = doc.getElementById(els);
}
if (!el) {
el = root.querySelector('.' + els);
}
return el;
}
return els;
}
/**
* Check if a widget should be lazy loaded based on node or grid settings.
*
* @param n the grid node to check
* @returns true if the item should be lazy loaded
*
* @example
* if (Utils.lazyLoad(node)) {
* // Set up intersection observer for lazy loading
* }
*/
static lazyLoad(n) {
return n.lazyLoad || n.grid?.opts?.lazyLoad && n.lazyLoad !== false;
}
/**
* Create a div element with the specified CSS classes.
*
* @param classes array of CSS class names to add
* @param parent optional parent element to append the div to
* @returns the created div element
*
* @example
* const div = Utils.createDiv(['grid-item', 'draggable']);
* const nested = Utils.createDiv(['content'], parentDiv);
*/
static createDiv(classes, parent) {
const el = document.createElement('div');
classes.forEach(c => { if (c)
el.classList.add(c); });
parent?.appendChild(el);
return el;
}
/**
* Check if a widget should resize to fit its content.
*
* @param n the grid node to check (can be undefined)
* @param strict if true, only returns true for explicit sizeToContent:true (not numbers)
* @returns true if the widget should resize to content
*
* @example
* if (Utils.shouldSizeToContent(node)) {
* // Trigger content-based resizing
* }
*/
static shouldSizeToContent(n, strict = false) {
return n?.grid && (strict ?
(n.sizeToContent === true || (n.grid.opts.sizeToContent === true && n.sizeToContent === undefined)) :
(!!n.sizeToContent || (n.grid.opts.sizeToContent && n.sizeToContent !== false)));
}
/**
* Check if two grid positions overlap/intersect.
*
* @param a first position with x, y, w, h properties
* @param b second position with x, y, w, h properties
* @returns true if the positions overlap
*
* @example
* const overlaps = Utils.isIntercepted(
* {x: 0, y: 0, w: 2, h: 1},
* {x: 1, y: 0, w: 2, h: 1}
* ); // true - they overlap
*/
static isIntercepted(a, b) {
return !(a.y >= b.y + b.h || a.y + a.h <= b.y || a.x + a.w <= b.x || a.x >= b.x + b.w);
}
/**
* Check if two grid positions are touching (edges or corners).
*
* @param a first position
* @param b second position
* @returns true if the positions are touching
*
* @example
* const touching = Utils.isTouching(
* {x: 0, y: 0, w: 2, h: 1},
* {x: 2, y: 0, w: 1, h: 1}
* ); // true - they share an edge
*/
static isTouching(a, b) {
return Utils.isIntercepted(a, { x: b.x - 0.5, y: b.y - 0.5, w: b.w + 1, h: b.h + 1 });
}
/**
* Calculate the overlapping area between two grid positions.
*
* @param a first position
* @param b second position
* @returns the area of overlap (0 if no overlap)
*
* @example
* const overlap = Utils.areaIntercept(
* {x: 0, y: 0, w: 3, h: 2},
* {x: 1, y: 0, w: 3, h: 2}
* ); // returns 4 (2x2 overlap)
*/
static areaIntercept(a, b) {
const x0 = (a.x > b.x) ? a.x : b.x;
const x1 = (a.x + a.w < b.x + b.w) ? a.x + a.w : b.x + b.w;
if (x1 <= x0)
return 0; // no overlap
const y0 = (a.y > b.y) ? a.y : b.y;
const y1 = (a.y + a.h < b.y + b.h) ? a.y + a.h : b.y + b.h;
if (y1 <= y0)
return 0; // no overlap
return (x1 - x0) * (y1 - y0);
}
/**
* Calculate the total area of a grid position.
*
* @param a position with width and height
* @returns the total area (width * height)
*
* @example
* const area = Utils.area({x: 0, y: 0, w: 3, h: 2}); // returns 6
*/
static area(a) {
return a.w * a.h;
}
/**
* Sort an array of grid nodes by position (y first, then x).
*
* @param nodes array of nodes to sort
* @param dir sort direction: 1 for ascending (top-left first), -1 for descending
* @returns the sorted array (modifies original)
*
* @example
* const sorted = Utils.sort(nodes); // Sort top-left to bottom-right
* const reverse = Utils.sort(nodes, -1); // Sort bottom-right to top-left
*/
static sort(nodes, dir = 1) {
const und = 10000;
return nodes.sort((a, b) => {
const diffY = dir * ((a.y ?? und) - (b.y ?? und));
if (diffY === 0)
return dir * ((a.x ?? und) - (b.x ?? und));
return diffY;
});
}
/**
* Find a grid node by its ID.
*
* @param nodes array of nodes to search
* @param id the ID to search for
* @returns the node with matching ID, or undefined if not found
*
* @example
* const node = Utils.find(nodes, 'widget-1');
* if (node) console.log('Found node at:', node.x, node.y);
*/
static find(nodes, id) {
return id ? nodes.find(n => n.id === id) : undefined;
}
/**
* Convert various value types to boolean.
* Handles strings like 'false', 'no', '0' as false.
*
* @param v value to convert
* @returns boolean representation
*
* @example
* Utils.toBool('true'); // true
* Utils.toBool('false'); // false
* Utils.toBool('no'); // false
* Utils.toBool('1'); // true
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static toBool(v) {
if (typeof v === 'boolean') {
return v;
}
if (typeof v === 'string') {
v = v.toLowerCase();
return !(v === '' || v === 'no' || v === 'false' || v === '0');
}
return Boolean(v);
}
/**
* Convert a string value to a number, handling null and empty strings.
*
* @param value string or null value to convert
* @returns number value, or undefined for null/empty strings
*
* @example
* Utils.toNumber('42'); // 42
* Utils.toNumber(''); // undefined
* Utils.toNumber(null); // undefined
*/
static toNumber(value) {
return (value === null || value.length === 0) ? undefined : Number(value);
}
/**
* Parse a height value with units into numeric value and unit string.
* Supports px, em, rem, vh, vw, %, cm, mm units.
*
* @param val height value as number or string with units
* @returns object with h (height) and unit properties
*
* @example
* Utils.parseHeight('100px'); // {h: 100, unit: 'px'}
* Utils.parseHeight('2rem'); // {h: 2, unit: 'rem'}
* Utils.parseHeight(50); // {h: 50, unit: 'px'}
*/
static parseHeight(val) {
let h;
let unit = 'px';
if (typeof val === 'string') {
if (val === 'auto' || val === '')
h = 0;
else {
const match = val.match(/^(-[0-9]+\.[0-9]+|[0-9]*\.[0-9]+|-[0-9]+|[0-9]+)(px|em|rem|vh|vw|%|cm|mm)?$/);
if (!match) {
throw new Error(`Invalid height val = ${val}`);
}
unit = match[2] || 'px';
h = parseFloat(match[1]);
}
}
else {
h = val;
}
return { h, unit };
}
/**
* Copy unset fields from source objects to target object (shallow merge with defaults).
* Similar to Object.assign but only sets undefined/null fields.
*
* @param target the object to copy defaults into
* @param sources one or more source objects to copy defaults from
* @returns the modified target object
*
* @example
* const config = { width: 100 };
* Utils.defaults(config, { width: 200, height: 50 });
* // config is now { width: 100, height: 50 }
*/
// eslint-disable-next-line
static defaults(target, ...sources) {
sources.forEach(source => {
for (const key in source) {
if (!source.hasOwnProperty(key))
return;
if (target[key] === null || target[key] === undefined) {
target[key] = source[key];
}
else if (typeof source[key] === 'object' && typeof target[key] === 'object') {
// property is an object, recursively add it's field over... #1373
Utils.defaults(target[key], source[key]);
}
}
});
return target;
}
/**
* Compare two objects for equality (shallow comparison).
* Checks if objects have the same fields and values at one level deep.
*
* @param a first object to compare
* @param b second object to compare
* @returns true if objects have the same values
*
* @example
* Utils.same({x: 1, y: 2}, {x: 1, y: 2}); // true
* Utils.same({x: 1}, {x: 1, y: 2}); // false
*/
static same(a, b) {
if (typeof a !== 'object')
return a == b;
if (typeof a !== typeof b)
return false;
// else we have object, check just 1 level deep for being same things...
if (Object.keys(a).length !== Object.keys(b).length)
return false;
for (const key in a) {
if (a[key] !== b[key])
return false;
}
return true;
}
/**
* Copy position and size properties from one widget to another.
* Copies x, y, w, h and optionally min/max constraints.
*
* @param a target widget to copy to
* @param b source widget to copy from
* @param doMinMax if true, also copy min/max width/height constraints
* @returns the target widget (a)
*
* @example
* Utils.copyPos(widget1, widget2); // Copy position/size
* Utils.copyPos(widget1, widget2, true); // Also copy constraints
*/
static copyPos(a, b, doMinMax = false) {
if (b.x !== undefined)
a.x = b.x;
if (b.y !== undefined)
a.y = b.y;
if (b.w !== undefined)
a.w = b.w;
if (b.h !== undefined)
a.h = b.h;
if (doMinMax) {
if (b.minW)
a.minW = b.minW;
if (b.minH)
a.minH = b.minH;
if (b.maxW)
a.maxW = b.maxW;
if (b.maxH)
a.maxH = b.maxH;
}
return a;
}
/** true if a and b has same size & position */
static samePos(a, b) {
return a && b && a.x === b.x && a.y === b.y && (a.w || 1) === (b.w || 1) && (a.h || 1) === (b.h || 1);
}
/** given a node, makes sure it's min/max are valid */
static sanitizeMinMax(node) {
// remove 0, undefine, null
if (!node.minW) {
delete node.minW;
}
if (!node.minH) {
delete node.minH;
}
if (!node.maxW) {
delete node.maxW;
}
if (!node.maxH) {
delete node.maxH;
}
}
/** removes field from the first object if same as the second objects (like diffing) and internal '_' for saving */
static removeInternalAndSame(a, b) {
if (typeof a !== 'object' || typeof b !== 'object')
return;
// skip arrays as we don't know how to hydrate them (unlike object spread operator)
if (Array.isArray(a) || Array.isArray(b))
return;
for (let key in a) {
const aVal = a[key];
const bVal = b[key];
if (key[0] === '_' || aVal === bVal) {
delete a[key];
}
else if (aVal && typeof aVal === 'object' && bVal !== undefined) {
Utils.removeInternalAndSame(aVal, bVal);
if (!Object.keys(aVal).length) {
delete a[key];
}
}
}
}
/** removes internal fields '_' and default values for saving */
static removeInternalForSave(n, removeEl = true) {
for (let key in n) {
if (key[0] === '_' || n[key] === null || n[key] === undefined)
delete n[key];
}
delete n.grid;
if (removeEl)
delete n.el;
// delete default values (will be re-created on read)
if (!n.autoPosition)
delete n.autoPosition;
if (!n.noResize)
delete n.noResize;
if (!n.noMove)
delete n.noMove;
if (!n.locked)
delete n.locked;
if (n.w === 1 || n.w === n.minW)
delete n.w;
if (n.h === 1 || n.h === n.minH)
delete n.h;
}
/** return the closest parent (or itself) matching the given class */
// static closestUpByClass(el: HTMLElement, name: string): HTMLElement {
// while (el) {
// if (el.classList.contains(name)) return el;
// el = el.parentElement
// }
// return null;
// }
/** delay calling the given function for given delay, preventing new calls from happening while waiting */
static throttle(func, delay) {
let isWaiting = false;
return (...args) => {
if (!isWaiting) {
isWaiting = true;
setTimeout(() => { func(...args); isWaiting = false; }, delay);
}
};
}
static removePositioningStyles(el) {
const style = el.style;
if (style.position) {
style.removeProperty('position');
}
if (style.left) {
style.removeProperty('left');
}
if (style.top) {
style.removeProperty('top');
}
if (style.width) {
style.removeProperty('width');
}
if (style.height) {
style.removeProperty('height');
}
}
/** @internal returns the passed element if scrollable, else the closest parent that will, up to the entire document scrolling element */
static getScrollElement(el) {
if (!el)
return document.scrollingElement || document.documentElement; // IE support
const style = getComputedStyle(el);
const overflowRegex = /(auto|scroll)/;
if (overflowRegex.test(style.overflow + style.overflowY)) {
return el;
}
else {
return Utils.getScrollElement(el.parentElement);
}
}
/** @internal */
static updateScrollPosition(el, position, distance) {
const scrollEl = Utils.getScrollElement(el);
if (!scrollEl)
return;
const elRect = el.getBoundingClientRect();
const scrollRect = scrollEl.getBoundingClientRect();
const innerHeightOrClientHeight = (window.innerHeight || document.documentElement.clientHeight);
const offsetDiffDown = elRect.bottom - Math.min(scrollRect.bottom, innerHeightOrClientHeight);
const offsetDiffUp = elRect.top - Math.max(scrollRect.top, 0);
const prevScroll = scrollEl.scrollTop;
if (offsetDiffUp < 0 && distance < 0) {
// scroll up
if (el.offsetHeight > scrollRect.height) {
scrollEl.scrollTop += distance;
}
else {
scrollEl.scrollTop += Math.abs(offsetDiffUp) > Math.abs(distance) ? distance : offsetDiffUp;
}
}
else if (offsetDiffDown > 0 && distance > 0) {
// scroll down
if (el.offsetHeight > scrollRect.height) {
scrollEl.scrollTop += distance;
}
else {
scrollEl.scrollTop += offsetDiffDown > distance ? distance : offsetDiffDown;
}
}
position.top += scrollEl.scrollTop - prevScroll;
}
/**
* @internal Function used to scroll the page.
*
* @param event `MouseEvent` that triggers the resize
* @param el `HTMLElement` that's being resized
* @param distance Distance from the V edges to start scrolling
*/
static updateScrollResize(event, el, distance) {
const scrollEl = Utils.getScrollElement(el);
const height = scrollEl.clientHeight;
// #1727 event.clientY is relative to viewport, so must compare this against position of scrollEl getBoundingClientRect().top
// #1745 Special situation if scrollEl is document 'html': here browser spec states that
// clientHeight is height of viewport, but getBoundingClientRect() is rectangle of html element;
// this discrepancy arises because in reality scrollbar is attached to viewport, not html element itself.
const offsetTop = (scrollEl === Utils.getScrollElement()) ? 0 : scrollEl.getBoundingClientRect().top;
const pointerPosY = event.clientY - offsetTop;
const top = pointerPosY < distance;
const bottom = pointerPosY > height - distance;
if (top) {
// This also can be done with a timeout to keep scrolling while the mouse is
// in the scrolling zone. (will have smoother behavior)
scrollEl.scrollBy({ behavior: 'smooth', top: pointerPosY - distance });
}
else if (bottom) {
scrollEl.scrollBy({ behavior: 'smooth', top: distance - (height - pointerPosY) });
}
}
/** single level clone, returning a new object with same top fields. This will share sub objects and arrays */
static clone(obj) {
if (obj === null || obj === undefined || typeof (obj) !== 'object') {
return obj;
}
// return Object.assign({}, obj);
if (obj instanceof Array) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return [...obj];
}
return { ...obj };
}
/**
* Recursive clone version that returns a full copy, checking for nested objects and arrays ONLY.
* Note: this will use as-is any key starting with double __ (and not copy inside) some lib have circular dependencies.
*/
static cloneDeep(obj) {
// list of fields we will skip during cloneDeep (nested objects, other internal)
const skipFields = ['parentGrid', 'el', 'grid', 'subGrid', 'engine'];
// return JSON.parse(JSON.stringify(obj)); // doesn't work with date format ?
const ret = Utils.clone(obj);
for (const key in ret) {
// NOTE: we don't support function/circular dependencies so skip those properties for now...
if (ret.hasOwnProperty(key) && typeof (ret[key]) === 'object' && key.substring(0, 2) !== '__' && !skipFields.find(k => k === key)) {
ret[key] = Utils.cloneDeep(obj[key]);
}
}
return ret;
}
/** deep clone the given HTML node, removing teh unique id field */
static cloneNode(el) {
const node = el.cloneNode(true);
node.removeAttribute('id');
return node;
}
static appendTo(el, parent) {
let parentNode;
if (typeof parent === 'string') {
parentNode = Utils.getElement(parent);
}
else {
parentNode = parent;
}
if (parentNode) {
parentNode.appendChild(el);
}
}
// public static setPositionRelative(el: HTMLElement): void {
// if (!(/^(?:r|a|f)/).test(getComputedStyle(el).position)) {
// el.style.position = "relative";
// }
// }
static addElStyles(el, styles) {
if (styles instanceof Object) {
for (const s in styles) {
if (styles.hasOwnProperty(s)) {
if (Array.isArray(styles[s])) {
// support fallback value
styles[s].forEach(val => {
el.style[s] = val;
});
}
else {
el.style[s] = styles[s];
}
}
}
}
}
static initEvent(e, info) {
const evt = { type: info.type };
const obj = {
button: 0,
which: 0,
buttons: 1,
bubbles: true,
cancelable: true,
target: info.target ? info.target : e.target
};
['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].forEach(p => evt[p] = e[p]); // keys
['pageX', 'pageY', 'clientX', 'clientY', 'screenX', 'screenY'].forEach(p => evt[p] = e[p]); // point info
return { ...evt, ...obj };
}
/** copies the MouseEvent (or convert Touch) properties and sends it as another event to the given target */
static simulateMouseEvent(e, simulatedType, target) {
const me = e;
const simulatedEvent = new MouseEvent(simulatedType, {
bubbles: true,
composed: true,
cancelable: true,
view: window,
detail: 1,
screenX: e.screenX,
screenY: e.screenY,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: me.ctrlKey ?? false,
altKey: me.altKey ?? false,
shiftKey: me.shiftKey ?? false,
metaKey: me.metaKey ?? false,
button: 0,
relatedTarget: e.target
});
(target || e.target).dispatchEvent(simulatedEvent);
}
/**
* defines an element that is used to get the offset and scale from grid transforms
* returns the scale and offsets from said element
*/
static getValuesFromTransformedElement(parent) {
const transformReference = document.createElement('div');
Utils.addElStyles(transformReference, {
opacity: '0',
position: 'fixed',
top: 0 + 'px',
left: 0 + 'px',
width: '1px',
height: '1px',
zIndex: '-999999',
});
parent.appendChild(transformReference);
const transformValues = transformReference.getBoundingClientRect();
parent.removeChild(transformReference);
transformReference.remove();
return {
xScale: 1 / transformValues.width,
yScale: 1 / transformValues.height,
xOffset: transformValues.left,
yOffset: transformValues.top,
};
}
/** swap the given object 2 field values */
static swap(o, a, b) {
if (!o)
return;
const tmp = o[a];
o[a] = o[b];
o[b] = tmp;
}
/** returns true if event is inside the given element rectangle */
// Note: Safari Mac has null event.relatedTarget which causes #1684 so check if DragEvent is inside the coordinates instead
// Utils.el.contains(event.relatedTarget as HTMLElement)
// public static inside(e: MouseEvent, el: HTMLElement): boolean {
// // srcElement, toElement, target: all set to placeholder when leaving simple grid, so we can't use that (Chrome)
// const target: HTMLElement = e.relatedTarget || (e as any).fromElement;
// if (!target) {
// const { bottom, left, right, top } = el.getBoundingClientRect();
// return (e.x < right && e.x > left && e.y < bottom && e.y > top);
// }
// return el.contains(target);
// }
/** true if the item can be rotated (checking for prop, not space available) */
static canBeRotated(n) {
return !(!n || n.w === n.h || n.locked || n.noResize || n.grid?.opts.disableResize || (n.minW && n.minW === n.maxW) || (n.minH && n.minH === n.maxH));
}
}
//# sourceMappingURL=utils.js.map