/*!
* GridStack 12.3.2
* https://gridstackjs.com/
*
* Copyright (c) 2021-2025 Alain Dumesny
* see root license https://github.com/gridstack/gridstack.js/tree/master/LICENSE
*/
import { GridStackEngine } from './gridstack-engine';
import { Utils, obsolete } from './utils';
import { gridDefaults } from './types';
/*
* and include D&D by default
* TODO: while we could generate a gridstack-static.js at smaller size - saves about 31k (41k -> 72k)
* I don't know how to generate the DD only code at the remaining 31k to delay load as code depends on Gridstack.ts
* also it caused loading issues in prod - see https://github.com/gridstack/gridstack.js/issues/2039
*/
import { DDGridStack } from './dd-gridstack';
import { isTouch } from './dd-touch';
import { DDManager } from './dd-manager';
const dd = new DDGridStack;
// export all dependent file as well to make it easier for users to just import the main file
export * from './types';
export * from './utils';
export * from './gridstack-engine';
export * from './dd-gridstack';
export * from './dd-manager';
export * from './dd-element';
export * from './dd-draggable';
export * from './dd-droppable';
export * from './dd-resizable';
export * from './dd-resizable-handle';
export * from './dd-base-impl';
/**
* Main gridstack class - you will need to call `GridStack.init()` first to initialize your grid.
* Note: your grid elements MUST have the following classes for the CSS layout to work:
* @example
*
*/
class GridStack {
/**
* initializing the HTML element, or selector string, into a grid will return the grid. Calling it again will
* simply return the existing instance (ignore any passed options). There is also an initAll() version that support
* multiple grids initialization at once. Or you can use addGrid() to create the entire grid from JSON.
* @param options grid options (optional)
* @param elOrString element or CSS selector (first one used) to convert to a grid (default to '.grid-stack' class selector)
*
* @example
* const grid = GridStack.init();
*
* Note: the HTMLElement (of type GridHTMLElement) will store a `gridstack: GridStack` value that can be retrieve later
* const grid = document.querySelector('.grid-stack').gridstack;
*/
static init(options = {}, elOrString = '.grid-stack') {
if (typeof document === 'undefined')
return null; // temp workaround SSR
const el = GridStack.getGridElement(elOrString);
if (!el) {
if (typeof elOrString === 'string') {
console.error('GridStack.initAll() no grid was found with selector "' + elOrString + '" - element missing or wrong selector ?' +
'\nNote: ".grid-stack" is required for proper CSS styling and drag/drop, and is the default selector.');
}
else {
console.error('GridStack.init() no grid element was passed.');
}
return null;
}
if (!el.gridstack) {
el.gridstack = new GridStack(el, Utils.cloneDeep(options));
}
return el.gridstack;
}
/**
* Will initialize a list of elements (given a selector) and return an array of grids.
* @param options grid options (optional)
* @param selector elements selector to convert to grids (default to '.grid-stack' class selector)
*
* @example
* const grids = GridStack.initAll();
* grids.forEach(...)
*/
static initAll(options = {}, selector = '.grid-stack') {
const grids = [];
if (typeof document === 'undefined')
return grids; // temp workaround SSR
GridStack.getGridElements(selector).forEach(el => {
if (!el.gridstack) {
el.gridstack = new GridStack(el, Utils.cloneDeep(options));
}
grids.push(el.gridstack);
});
if (grids.length === 0) {
console.error('GridStack.initAll() no grid was found with selector "' + selector + '" - element missing or wrong selector ?' +
'\nNote: ".grid-stack" is required for proper CSS styling and drag/drop, and is the default selector.');
}
return grids;
}
/**
* call to create a grid with the given options, including loading any children from JSON structure. This will call GridStack.init(), then
* grid.load() on any passed children (recursively). Great alternative to calling init() if you want entire grid to come from
* JSON serialized data, including options.
* @param parent HTML element parent to the grid
* @param opt grids options used to initialize the grid, and list of children
*/
static addGrid(parent, opt = {}) {
if (!parent)
return null;
let el = parent;
if (el.gridstack) {
// already a grid - set option and load data
const grid = el.gridstack;
if (opt)
grid.opts = { ...grid.opts, ...opt };
if (opt.children !== undefined)
grid.load(opt.children);
return grid;
}
// create the grid element, but check if the passed 'parent' already has grid styling and should be used instead
const parentIsGrid = parent.classList.contains('grid-stack');
if (!parentIsGrid || GridStack.addRemoveCB) {
if (GridStack.addRemoveCB) {
el = GridStack.addRemoveCB(parent, opt, true, true);
}
else {
el = Utils.createDiv(['grid-stack', opt.class], parent);
}
}
// create grid class and load any children
const grid = GridStack.init(opt, el);
return grid;
}
/** call this method to register your engine instead of the default one.
* See instead `GridStackOptions.engineClass` if you only need to
* replace just one instance.
*/
static registerEngine(engineClass) {
GridStack.engineClass = engineClass;
}
/**
* @internal create placeholder DIV as needed
* @returns the placeholder element for indicating drop zones during drag operations
*/
get placeholder() {
if (!this._placeholder) {
this._placeholder = Utils.createDiv([this.opts.placeholderClass, gridDefaults.itemClass, this.opts.itemClass]);
const placeholderChild = Utils.createDiv(['placeholder-content'], this._placeholder);
if (this.opts.placeholderText) {
placeholderChild.textContent = this.opts.placeholderText;
}
}
return this._placeholder;
}
/**
* Construct a grid item from the given element and options
* @param el the HTML element tied to this grid after it's been initialized
* @param opts grid options - public for classes to access, but use methods to modify!
*/
constructor(el, opts = {}) {
this.el = el;
this.opts = opts;
/** time to wait for animation (if enabled) to be done so content sizing can happen */
this.animationDelay = 300 + 10;
/** @internal */
this._gsEventHandler = {};
/** @internal extra row added when dragging at the bottom of the grid */
this._extraDragRow = 0;
/** @internal meant to store the scale of the active grid */
this.dragTransform = { xScale: 1, yScale: 1, xOffset: 0, yOffset: 0 };
el.gridstack = this;
this.opts = opts = opts || {}; // handles null/undefined/0
if (!el.classList.contains('grid-stack')) {
this.el.classList.add('grid-stack');
}
// if row property exists, replace minRow and maxRow instead
if (opts.row) {
opts.minRow = opts.maxRow = opts.row;
delete opts.row;
}
const rowAttr = Utils.toNumber(el.getAttribute('gs-row'));
// flag only valid in sub-grids (handled by parent, not here)
if (opts.column === 'auto') {
delete opts.column;
}
// save original setting so we can restore on save
if (opts.alwaysShowResizeHandle !== undefined) {
opts._alwaysShowResizeHandle = opts.alwaysShowResizeHandle;
}
// cleanup responsive opts (must have columnWidth | breakpoints) then sort breakpoints by size (so we can match during resize)
const resp = opts.columnOpts;
if (resp) {
const bk = resp.breakpoints;
if (!resp.columnWidth && !bk?.length) {
delete opts.columnOpts;
}
else {
resp.columnMax = resp.columnMax || 12;
if (bk?.length > 1)
bk.sort((a, b) => (b.w || 0) - (a.w || 0));
}
}
// elements DOM attributes override any passed options (like CSS style) - merge the two together
const defaults = {
...Utils.cloneDeep(gridDefaults),
column: Utils.toNumber(el.getAttribute('gs-column')) || gridDefaults.column,
minRow: rowAttr ? rowAttr : Utils.toNumber(el.getAttribute('gs-min-row')) || gridDefaults.minRow,
maxRow: rowAttr ? rowAttr : Utils.toNumber(el.getAttribute('gs-max-row')) || gridDefaults.maxRow,
staticGrid: Utils.toBool(el.getAttribute('gs-static')) || gridDefaults.staticGrid,
sizeToContent: Utils.toBool(el.getAttribute('gs-size-to-content')) || undefined,
draggable: {
handle: (opts.handleClass ? '.' + opts.handleClass : (opts.handle ? opts.handle : '')) || gridDefaults.draggable.handle,
},
removableOptions: {
accept: opts.itemClass || gridDefaults.removableOptions.accept,
decline: gridDefaults.removableOptions.decline
},
};
if (el.getAttribute('gs-animate')) { // default to true, but if set to false use that instead
defaults.animate = Utils.toBool(el.getAttribute('gs-animate'));
}
opts = Utils.defaults(opts, defaults);
this._initMargin(); // part of settings defaults...
// Now check if we're loading into !12 column mode FIRST so we don't do un-necessary work (like cellHeight = width / 12 then go 1 column)
this.checkDynamicColumn();
this._updateColumnVar(opts);
if (opts.rtl === 'auto') {
opts.rtl = (el.style.direction === 'rtl');
}
if (opts.rtl) {
this.el.classList.add('grid-stack-rtl');
}
// check if we're been nested, and if so update our style and keep pointer around (used during save)
const parentGridItem = this.el.closest('.' + gridDefaults.itemClass);
const parentNode = parentGridItem?.gridstackNode;
if (parentNode) {
parentNode.subGrid = this;
this.parentGridNode = parentNode;
this.el.classList.add('grid-stack-nested');
parentNode.el.classList.add('grid-stack-sub-grid');
}
this._isAutoCellHeight = (opts.cellHeight === 'auto');
if (this._isAutoCellHeight || opts.cellHeight === 'initial') {
// make the cell content square initially (will use resize/column event to keep it square)
this.cellHeight(undefined);
}
else {
// append unit if any are set
if (typeof opts.cellHeight == 'number' && opts.cellHeightUnit && opts.cellHeightUnit !== gridDefaults.cellHeightUnit) {
opts.cellHeight = opts.cellHeight + opts.cellHeightUnit;
delete opts.cellHeightUnit;
}
const val = opts.cellHeight;
delete opts.cellHeight; // force initial cellHeight() call to set the value
this.cellHeight(val);
}
// see if we need to adjust auto-hide
if (opts.alwaysShowResizeHandle === 'mobile') {
opts.alwaysShowResizeHandle = isTouch;
}
this._setStaticClass();
const engineClass = opts.engineClass || GridStack.engineClass || GridStackEngine;
this.engine = new engineClass({
column: this.getColumn(),
float: opts.float,
maxRow: opts.maxRow,
onChange: (cbNodes) => {
cbNodes.forEach(n => {
const el = n.el;
if (!el)
return;
if (n._removeDOM) {
if (el)
el.remove();
delete n._removeDOM;
}
else {
this._writePosAttr(el, n);
}
});
this._updateContainerHeight();
}
});
if (opts.auto) {
this.batchUpdate(); // prevent in between re-layout #1535 TODO: this only set float=true, need to prevent collision check...
this.engine._loading = true; // loading collision check
this.getGridItems().forEach(el => this._prepareElement(el));
delete this.engine._loading;
this.batchUpdate(false);
}
// load any passed in children as well, which overrides any DOM layout done above
if (opts.children) {
const children = opts.children;
delete opts.children;
if (children.length)
this.load(children); // don't load empty
}
this.setAnimation();
// dynamic grids require pausing during drag to detect over to nest vs push
if (opts.subGridDynamic && !DDManager.pauseDrag)
DDManager.pauseDrag = true;
if (opts.draggable?.pause !== undefined)
DDManager.pauseDrag = opts.draggable.pause;
this._setupRemoveDrop();
this._setupAcceptWidget();
this._updateResizeEvent();
}
_updateColumnVar(opts = this.opts) {
this.el.classList.add('gs-' + opts.column);
if (typeof opts.column === 'number')
this.el.style.setProperty('--gs-column-width', `${100 / opts.column}%`);
}
/**
* add a new widget and returns it.
*
* Widget will be always placed even if result height is more than actual grid height.
* You need to use `willItFit()` before calling addWidget for additional check.
* See also `makeWidget(el)` for DOM element.
*
* @example
* const grid = GridStack.init();
* grid.addWidget({w: 3, content: 'hello'});
*
* @param w GridStackWidget definition. used MakeWidget(el) if you have dom element instead.
*/
addWidget(w) {
if (!w)
return;
if (typeof w === 'string') {
console.error('V11: GridStack.addWidget() does not support string anymore. see #2736');
return;
}
if (w.ELEMENT_NODE) {
console.error('V11: GridStack.addWidget() does not support HTMLElement anymore. use makeWidget()');
return this.makeWidget(w);
}
let el;
let node = w;
node.grid = this;
if (node.el) {
el = node.el; // re-use element stored in the node
}
else if (GridStack.addRemoveCB) {
el = GridStack.addRemoveCB(this.el, w, true, false);
}
else {
el = this.createWidgetDivs(node);
}
if (!el)
return;
// if the caller ended up initializing the widget in addRemoveCB, or we stared with one already, skip the rest
node = el.gridstackNode;
if (node && el.parentElement === this.el && this.engine.nodes.find(n => n._id === node._id))
return el;
// Tempting to initialize the passed in opt with default and valid values, but this break knockout demos
// as the actual value are filled in when _prepareElement() calls el.getAttribute('gs-xyz') before adding the node.
// So make sure we load any DOM attributes that are not specified in passed in options (which override)
const domAttr = this._readAttr(el);
Utils.defaults(w, domAttr);
this.engine.prepareNode(w);
// this._writeAttr(el, w); why write possibly incorrect values back when makeWidget() will ?
this.el.appendChild(el);
this.makeWidget(el, w);
return el;
}
/**
* Create the default grid item divs and content (possibly lazy loaded) by using GridStack.renderCB().
*
* @param n GridStackNode definition containing widget configuration
* @returns the created HTML element with proper grid item structure
*
* @example
* const element = grid.createWidgetDivs({ w: 2, h: 1, content: 'Hello World' });
*/
createWidgetDivs(n) {
const el = Utils.createDiv(['grid-stack-item', this.opts.itemClass]);
const cont = Utils.createDiv(['grid-stack-item-content'], el);
if (Utils.lazyLoad(n)) {
if (!n.visibleObservable) {
n.visibleObservable = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
n.visibleObservable?.disconnect();
delete n.visibleObservable;
GridStack.renderCB(cont, n);
n.grid?.prepareDragDrop(n.el);
}
});
window.setTimeout(() => n.visibleObservable?.observe(el)); // wait until callee sets position attributes
}
}
else
GridStack.renderCB(cont, n);
return el;
}
/**
* Convert an existing gridItem element into a sub-grid with the given (optional) options, else inherit them
* from the parent's subGrid options.
* @param el gridItem element to convert
* @param ops (optional) sub-grid options, else default to node, then parent settings, else defaults
* @param nodeToAdd (optional) node to add to the newly created sub grid (used when dragging over existing regular item)
* @param saveContent if true (default) the html inside .grid-stack-content will be saved to child widget
* @returns newly created grid
*/
makeSubGrid(el, ops, nodeToAdd, saveContent = true) {
let node = el.gridstackNode;
if (!node) {
node = this.makeWidget(el).gridstackNode;
}
if (node.subGrid?.el)
return node.subGrid; // already done
// find the template subGrid stored on a parent as fallback...
let subGridTemplate; // eslint-disable-next-line @typescript-eslint/no-this-alias
let grid = this;
while (grid && !subGridTemplate) {
subGridTemplate = grid.opts?.subGridOpts;
grid = grid.parentGridNode?.grid;
}
//... and set the create options
ops = Utils.cloneDeep({
// by default sub-grid inherit from us | parent, other than id, children, etc...
...this.opts, id: undefined, children: undefined, column: 'auto', columnOpts: undefined, layout: 'list', subGridOpts: undefined,
...(subGridTemplate || {}),
...(ops || node.subGridOpts || {})
});
node.subGridOpts = ops;
// if column special case it set, remember that flag and set default
let autoColumn;
if (ops.column === 'auto') {
autoColumn = true;
ops.column = Math.max(node.w || 1, nodeToAdd?.w || 1);
delete ops.columnOpts; // driven by parent
}
// if we're converting an existing full item, move over the content to be the first sub item in the new grid
let content = node.el.querySelector('.grid-stack-item-content');
let newItem;
let newItemOpt;
if (saveContent) {
this._removeDD(node.el); // remove D&D since it's set on content div
newItemOpt = { ...node, x: 0, y: 0 };
Utils.removeInternalForSave(newItemOpt);
delete newItemOpt.subGridOpts;
if (node.content) {
newItemOpt.content = node.content;
delete node.content;
}
if (GridStack.addRemoveCB) {
newItem = GridStack.addRemoveCB(this.el, newItemOpt, true, false);
}
else {
newItem = Utils.createDiv(['grid-stack-item']);
newItem.appendChild(content);
content = Utils.createDiv(['grid-stack-item-content'], node.el);
}
this.prepareDragDrop(node.el); // ... and restore original D&D
}
// if we're adding an additional item, make the container large enough to have them both
if (nodeToAdd) {
const w = autoColumn ? ops.column : node.w;
const h = node.h + nodeToAdd.h;
const style = node.el.style;
style.transition = 'none'; // show up instantly so we don't see scrollbar with nodeToAdd
this.update(node.el, { w, h });
setTimeout(() => style.transition = null); // recover animation
}
const subGrid = node.subGrid = GridStack.addGrid(content, ops);
if (nodeToAdd?._moving)
subGrid._isTemp = true; // prevent re-nesting as we add over
if (autoColumn)
subGrid._autoColumn = true;
// add the original content back as a child of the newly created grid
if (saveContent) {
subGrid.makeWidget(newItem, newItemOpt);
}
// now add any additional node
if (nodeToAdd) {
if (nodeToAdd._moving) {
// create an artificial event even for the just created grid to receive this item
window.setTimeout(() => Utils.simulateMouseEvent(nodeToAdd._event, 'mouseenter', subGrid.el), 0);
}
else {
subGrid.makeWidget(node.el, node);
}
}
// if sizedToContent, we need to re-calc the size of ourself
this.resizeToContentCheck(false, node);
return subGrid;
}
/**
* called when an item was converted into a nested grid to accommodate a dragged over item, but then item leaves - return back
* to the original grid-item. Also called to remove empty sub-grids when last item is dragged out (since re-creating is simple)
*/
removeAsSubGrid(nodeThatRemoved) {
const pGrid = this.parentGridNode?.grid;
if (!pGrid)
return;
pGrid.batchUpdate();
pGrid.removeWidget(this.parentGridNode.el, true, true);
this.engine.nodes.forEach(n => {
// migrate any children over and offsetting by our location
n.x += this.parentGridNode.x;
n.y += this.parentGridNode.y;
pGrid.makeWidget(n.el, n);
});
pGrid.batchUpdate(false);
if (this.parentGridNode)
delete this.parentGridNode.subGrid;
delete this.parentGridNode;
// create an artificial event for the original grid now that this one is gone (got a leave, but won't get enter)
if (nodeThatRemoved) {
window.setTimeout(() => Utils.simulateMouseEvent(nodeThatRemoved._event, 'mouseenter', pGrid.el), 0);
}
}
/**
* saves the current layout returning a list of widgets for serialization which might include any nested grids.
* @param saveContent if true (default) the latest html inside .grid-stack-content will be saved to GridStackWidget.content field, else it will
* be removed.
* @param saveGridOpt if true (default false), save the grid options itself, so you can call the new GridStack.addGrid()
* to recreate everything from scratch. GridStackOptions.children would then contain the widget list instead.
* @param saveCB callback for each node -> widget, so application can insert additional data to be saved into the widget data structure.
* @param column if provided, the grid will be saved for the given column size (IFF we have matching internal saved layout, or current layout).
* Otherwise it will use the largest possible layout (say 12 even if rendering at 1 column) so we can restore to all layouts.
* NOTE: if you want to save to currently display layout, pass this.getColumn() as column.
* NOTE2: nested grids will ALWAYS save to the container size to be in sync with parent.
* @returns list of widgets or full grid option, including .children list of widgets
*/
save(saveContent = true, saveGridOpt = false, saveCB = GridStack.saveCB, column) {
// return copied GridStackWidget (with optionally .el) we can modify at will...
const list = this.engine.save(saveContent, saveCB, column);
// check for HTML content and nested grids
list.forEach(n => {
if (saveContent && n.el && !n.subGrid && !saveCB) { // sub-grid are saved differently, not plain content
const itemContent = n.el.querySelector('.grid-stack-item-content');
n.content = itemContent?.innerHTML;
if (!n.content)
delete n.content;
}
else {
if (!saveContent && !saveCB) {
delete n.content;
}
// check for nested grid - make sure it saves to the given container size to be consistent
if (n.subGrid?.el) {
const column = n.w || n.subGrid.getColumn();
const listOrOpt = n.subGrid.save(saveContent, saveGridOpt, saveCB, column);
n.subGridOpts = (saveGridOpt ? listOrOpt : { children: listOrOpt });
delete n.subGrid;
}
}
delete n.el;
});
// check if save entire grid options (needed for recursive) + children...
if (saveGridOpt) {
const o = Utils.cloneDeep(this.opts);
// delete default values that will be recreated on launch
if (o.marginBottom === o.marginTop && o.marginRight === o.marginLeft && o.marginTop === o.marginRight) {
o.margin = o.marginTop;
delete o.marginTop;
delete o.marginRight;
delete o.marginBottom;
delete o.marginLeft;
}
if (o.rtl === (this.el.style.direction === 'rtl')) {
o.rtl = 'auto';
}
if (this._isAutoCellHeight) {
o.cellHeight = 'auto';
}
if (this._autoColumn) {
o.column = 'auto';
}
const origShow = o._alwaysShowResizeHandle;
delete o._alwaysShowResizeHandle;
if (origShow !== undefined) {
o.alwaysShowResizeHandle = origShow;
}
else {
delete o.alwaysShowResizeHandle;
}
Utils.removeInternalAndSame(o, gridDefaults);
o.children = list;
return o;
}
return list;
}
/**
* Load widgets from a list. This will call update() on each (matching by id) or add/remove widgets that are not there.
* Used to restore a grid layout for a saved layout list (see `save()`).
*
* @param items list of widgets definition to update/create
* @param addRemove boolean (default true) or callback method can be passed to control if and how missing widgets can be added/removed, giving
* the user control of insertion.
* @returns the grid instance for chaining
*
* @example
* // Basic usage with saved layout
* const savedLayout = grid.save(); // Save current layout
* // ... later restore it
* grid.load(savedLayout);
*
* // Load with custom add/remove callback
* grid.load(layout, (items, grid, add) => {
* if (add) {
* // Custom logic for adding new widgets
* items.forEach(item => {
* const el = document.createElement('div');
* el.innerHTML = item.content || '';
* grid.addWidget(el, item);
* });
* } else {
* // Custom logic for removing widgets
* items.forEach(item => grid.removeWidget(item.el));
* }
* });
*
* // Load without adding/removing missing widgets
* grid.load(layout, false);
*
* @see {@link http://gridstackjs.com/demo/serialization.html} for complete example
*/
load(items, addRemove = GridStack.addRemoveCB || true) {
items = Utils.cloneDeep(items); // so we can mod
const column = this.getColumn();
// make sure size 1x1 (default) is present as it may need to override current sizes
items.forEach(n => { n.w = n.w || n.minW || 1; n.h = n.h || n.minH || 1; });
// sort items. those without coord will be appended last
items = Utils.sort(items);
this.engine.skipCacheUpdate = this._ignoreLayoutsNodeChange = true; // skip layout update
// if we're loading a layout into for example 1 column and items don't fit, make sure to save
// the original wanted layout so we can scale back up correctly #1471
let maxColumn = 0;
items.forEach(n => { maxColumn = Math.max(maxColumn, (n.x || 0) + n.w); });
if (maxColumn > this.engine.defaultColumn)
this.engine.defaultColumn = maxColumn;
if (maxColumn > column) {
// if we're loading (from empty) into a smaller column, check for special responsive layout
if (this.engine.nodes.length === 0 && this.responseLayout) {
this.engine.nodes = items;
this.engine.columnChanged(maxColumn, column, this.responseLayout);
items = this.engine.nodes;
this.engine.nodes = [];
delete this.responseLayout;
}
else
this.engine.cacheLayout(items, maxColumn, true);
}
// if given a different callback, temporally set it as global option so creating will use it
const prevCB = GridStack.addRemoveCB;
if (typeof (addRemove) === 'function')
GridStack.addRemoveCB = addRemove;
const removed = [];
this.batchUpdate();
// if we are loading from empty temporarily remove animation
const blank = !this.engine.nodes.length;
const noAnim = blank && this.opts.animate;
if (noAnim)
this.setAnimation(false);
// see if any items are missing from new layout and need to be removed first
if (!blank && addRemove) {
const copyNodes = [...this.engine.nodes]; // don't loop through array you modify
copyNodes.forEach(n => {
if (!n.id)
return;
const item = Utils.find(items, n.id);
if (!item) {
if (GridStack.addRemoveCB)
GridStack.addRemoveCB(this.el, n, false, false);
removed.push(n); // batch keep track
this.removeWidget(n.el, true, false);
}
});
}
// now add/update the widgets - starting with removing items in the new layout we will reposition
// to reduce collision and add no-coord ones at next available spot
this.engine._loading = true; // help with collision
const updateNodes = [];
this.engine.nodes = this.engine.nodes.filter(n => {
if (Utils.find(items, n.id)) {
updateNodes.push(n);
return false;
} // remove if found from list
return true;
});
items.forEach(w => {
const item = Utils.find(updateNodes, w.id);
if (item) {
// if item sizes to content, re-use the exiting height so it's a better guess at the final size (same if width doesn't change)
if (Utils.shouldSizeToContent(item))
w.h = item.h;
// check if missing coord, in which case find next empty slot with new (or old if missing) sizes
this.engine.nodeBoundFix(w);
if (w.autoPosition || w.x === undefined || w.y === undefined) {
w.w = w.w || item.w;
w.h = w.h || item.h;
this.engine.findEmptyPosition(w);
}
// add back to current list BUT force a collision check if it 'appears' we didn't change to make sure we don't overlap others now
this.engine.nodes.push(item);
if (Utils.samePos(item, w) && this.engine.nodes.length > 1) {
this.moveNode(item, { ...w, forceCollide: true });
Utils.copyPos(w, item); // use possily updated values before update() is called next (no-op since already moved)
}
this.update(item.el, w);
if (w.subGridOpts?.children) { // update any sub grid as well
const sub = item.el.querySelector('.grid-stack');
if (sub && sub.gridstack) {
sub.gridstack.load(w.subGridOpts.children); // TODO: support updating grid options ?
}
}
}
else if (addRemove) {
this.addWidget(w);
}
});
delete this.engine._loading; // done loading
this.engine.removedNodes = removed;
this.batchUpdate(false);
// after commit, clear that flag
delete this._ignoreLayoutsNodeChange;
delete this.engine.skipCacheUpdate;
prevCB ? GridStack.addRemoveCB = prevCB : delete GridStack.addRemoveCB;
if (noAnim)
this.setAnimation(true, true); // delay adding animation back
return this;
}
/**
* use before calling a bunch of `addWidget()` to prevent un-necessary relayouts in between (more efficient)
* and get a single event callback. You will see no changes until `batchUpdate(false)` is called.
*/
batchUpdate(flag = true) {
this.engine.batchUpdate(flag);
if (!flag) {
this._updateContainerHeight();
this._triggerRemoveEvent();
this._triggerAddEvent();
this._triggerChangeEvent();
}
return this;
}
/**
* Gets the current cell height in pixels. This takes into account the unit type and converts to pixels if necessary.
*
* @param forcePixel if true, forces conversion to pixels even when cellHeight is specified in other units
* @returns the cell height in pixels
*
* @example
* const height = grid.getCellHeight();
* console.log('Cell height:', height, 'px');
*
* // Force pixel conversion
* const pixelHeight = grid.getCellHeight(true);
*/
getCellHeight(forcePixel = false) {
if (this.opts.cellHeight && this.opts.cellHeight !== 'auto' &&
(!forcePixel || !this.opts.cellHeightUnit || this.opts.cellHeightUnit === 'px')) {
return this.opts.cellHeight;
}
// do rem/em/cm/mm to px conversion
if (this.opts.cellHeightUnit === 'rem') {
return this.opts.cellHeight * parseFloat(getComputedStyle(document.documentElement).fontSize);
}
if (this.opts.cellHeightUnit === 'em') {
return this.opts.cellHeight * parseFloat(getComputedStyle(this.el).fontSize);
}
if (this.opts.cellHeightUnit === 'cm') {
// 1cm = 96px/2.54. See https://www.w3.org/TR/css-values-3/#absolute-lengths
return this.opts.cellHeight * (96 / 2.54);
}
if (this.opts.cellHeightUnit === 'mm') {
return this.opts.cellHeight * (96 / 2.54) / 10;
}
// else get first cell height
const el = this.el.querySelector('.' + this.opts.itemClass);
if (el) {
const h = Utils.toNumber(el.getAttribute('gs-h')) || 1; // since we don't write 1 anymore
return Math.round(el.offsetHeight / h);
}
// else do entire grid and # of rows (but doesn't work if min-height is the actual constrain)
const rows = parseInt(this.el.getAttribute('gs-current-row'));
return rows ? Math.round(this.el.getBoundingClientRect().height / rows) : this.opts.cellHeight;
}
/**
* Update current cell height - see `GridStackOptions.cellHeight` for format by updating eh Browser CSS variable.
*
* @param val the cell height. Options:
* - `undefined`: cells content will be made square (match width minus margin)
* - `0`: the CSS will be generated by the application instead
* - number: height in pixels
* - string: height with units (e.g., '70px', '5rem', '2em')
* @returns the grid instance for chaining
*
* @example
* grid.cellHeight(100); // 100px height
* grid.cellHeight('70px'); // explicit pixel height
* grid.cellHeight('5rem'); // relative to root font size
* grid.cellHeight(grid.cellWidth() * 1.2); // aspect ratio
* grid.cellHeight('auto'); // auto-size based on content
*/
cellHeight(val) {
// if not called internally, check if we're changing mode
if (val !== undefined) {
if (this._isAutoCellHeight !== (val === 'auto')) {
this._isAutoCellHeight = (val === 'auto');
this._updateResizeEvent();
}
}
if (val === 'initial' || val === 'auto') {
val = undefined;
}
// make item content be square
if (val === undefined) {
const marginDiff = -this.opts.marginRight - this.opts.marginLeft
+ this.opts.marginTop + this.opts.marginBottom;
val = this.cellWidth() + marginDiff;
}
const data = Utils.parseHeight(val);
if (this.opts.cellHeightUnit === data.unit && this.opts.cellHeight === data.h) {
return this;
}
this.opts.cellHeightUnit = data.unit;
this.opts.cellHeight = data.h;
// finally update var and container
this.el.style.setProperty('--gs-cell-height', `${this.opts.cellHeight}${this.opts.cellHeightUnit}`);
this._updateContainerHeight();
this.resizeToContentCheck();
return this;
}
/** Gets current cell width. */
/**
* Gets the current cell width in pixels. This is calculated based on the grid container width divided by the number of columns.
*
* @returns the cell width in pixels
*
* @example
* const width = grid.cellWidth();
* console.log('Cell width:', width, 'px');
*
* // Use cell width to calculate widget dimensions
* const widgetWidth = width * 3; // For a 3-column wide widget
*/
cellWidth() {
return this._widthOrContainer() / this.getColumn();
}
/** return our expected width (or parent) , and optionally of window for dynamic column check */
_widthOrContainer(forBreakpoint = false) {
// use `offsetWidth` or `clientWidth` (no scrollbar) ?
// https://stackoverflow.com/questions/21064101/understanding-offsetwidth-clientwidth-scrollwidth-and-height-respectively
return forBreakpoint && this.opts.columnOpts?.breakpointForWindow ? window.innerWidth : (this.el.clientWidth || this.el.parentElement.clientWidth || window.innerWidth);
}
/** checks for dynamic column count for our current size, returning true if changed */
checkDynamicColumn() {
const resp = this.opts.columnOpts;
if (!resp || (!resp.columnWidth && !resp.breakpoints?.length))
return false;
const column = this.getColumn();
let newColumn = column;
const w = this._widthOrContainer(true);
if (resp.columnWidth) {
newColumn = Math.min(Math.round(w / resp.columnWidth) || 1, resp.columnMax);
}
else {
// find the closest breakpoint (already sorted big to small) that matches
newColumn = resp.columnMax;
let i = 0;
while (i < resp.breakpoints.length && w <= resp.breakpoints[i].w) {
newColumn = resp.breakpoints[i++].c || column;
}
}
if (newColumn !== column) {
const bk = resp.breakpoints?.find(b => b.c === newColumn);
this.column(newColumn, bk?.layout || resp.layout);
return true;
}
return false;
}
/**
* Re-layout grid items to reclaim any empty space. This is useful after removing widgets
* or when you want to optimize the layout.
*
* @param layout layout type. Options:
* - 'compact' (default): might re-order items to fill any empty space
* - 'list': keep the widget left->right order the same, even if that means leaving an empty slot if things don't fit
* @param doSort re-sort items first based on x,y position. Set to false to do your own sorting ahead (default: true)
* @returns the grid instance for chaining
*
* @example
* // Compact layout after removing widgets
* grid.removeWidget('.widget-to-remove');
* grid.compact();
*
* // Use list layout (preserve order)
* grid.compact('list');
*
* // Compact without sorting first
* grid.compact('compact', false);
*/
compact(layout = 'compact', doSort = true) {
this.engine.compact(layout, doSort);
this._triggerChangeEvent();
return this;
}
/**
* Set the number of columns in the grid. Will update existing widgets to conform to new number of columns,
* as well as cache the original layout so you can revert back to previous positions without loss.
*
* Requires `gridstack-extra.css` or `gridstack-extra.min.css` for [2-11] columns,
* else you will need to generate correct CSS.
* See: https://github.com/gridstack/gridstack.js#change-grid-columns
*
* @param column Integer > 0 (default 12)
* @param layout specify the type of re-layout that will happen. Options:
* - 'moveScale' (default): scale widget positions and sizes
* - 'move': keep widget sizes, only move positions
* - 'scale': keep widget positions, only scale sizes
* - 'none': don't change widget positions or sizes
* Note: items will never be outside of the current column boundaries.
* Ignored for `column=1` as we always want to vertically stack.
* @returns the grid instance for chaining
*
* @example
* // Change to 6 columns with default scaling
* grid.column(6);
*
* // Change to 4 columns, only move positions
* grid.column(4, 'move');
*
* // Single column layout (vertical stack)
* grid.column(1);
*/
column(column, layout = 'moveScale') {
if (!column || column < 1 || this.opts.column === column)
return this;
const oldColumn = this.getColumn();
this.opts.column = column;
if (!this.engine) {
// called in constructor, noting else to do but remember that breakpoint layout
this.responseLayout = layout;
return this;
}
this.engine.column = column;
this.el.classList.remove('gs-' + oldColumn);
this._updateColumnVar();
// update the items now
this.engine.columnChanged(oldColumn, column, layout);
if (this._isAutoCellHeight)
this.cellHeight();
this.resizeToContentCheck(true); // wait for width resizing
// and trigger our event last...
this._ignoreLayoutsNodeChange = true; // skip layout update
this._triggerChangeEvent();
delete this._ignoreLayoutsNodeChange;
return this;
}
/**
* Get the number of columns in the grid (default 12).
*
* @returns the current number of columns in the grid
*
* @example
* const columnCount = grid.getColumn(); // returns 12 by default
*/
getColumn() { return this.opts.column; }
/**
* Returns an array of grid HTML elements (no placeholder) - used to iterate through our children in DOM order.
* This method excludes placeholder elements and returns only actual grid items.
*
* @returns array of GridItemHTMLElement instances representing all grid items
*
* @example
* const items = grid.getGridItems();
* items.forEach(item => {
* console.log('Item ID:', item.gridstackNode.id);
* });
*/
getGridItems() {
return Array.from(this.el.children)
.filter((el) => el.matches('.' + this.opts.itemClass) && !el.matches('.' + this.opts.placeholderClass));
}
/**
* Returns true if change callbacks should be ignored due to column change, sizeToContent, loading, etc.
* This is useful for callers who want to implement dirty flag functionality.
*
* @returns true if change callbacks are currently being ignored
*
* @example
* if (!grid.isIgnoreChangeCB()) {
* // Process the change event
* console.log('Grid layout changed');
* }
*/
isIgnoreChangeCB() { return this._ignoreLayoutsNodeChange; }
/**
* Destroys a grid instance. DO NOT CALL any methods or access any vars after this as it will free up members.
* @param removeDOM if `false` grid and items HTML elements will not be removed from the DOM (Optional. Default `true`).
*/
destroy(removeDOM = true) {
if (!this.el)
return; // prevent multiple calls
this.offAll();
this._updateResizeEvent(true);
this.setStatic(true, false); // permanently removes DD but don't set CSS class (we're going away)
this.setAnimation(false);
if (!removeDOM) {
this.removeAll(removeDOM);
this.el.removeAttribute('gs-current-row');
}
else {
this.el.parentNode.removeChild(this.el);
}
if (this.parentGridNode)
delete this.parentGridNode.subGrid;
delete this.parentGridNode;
delete this.opts;
delete this._placeholder?.gridstackNode;
delete this._placeholder;
delete this.engine;
delete this.el.gridstack; // remove circular dependency that would prevent a freeing
delete this.el;
return this;
}
/**
* Enable/disable floating widgets (default: `false`). When enabled, widgets can float up to fill empty spaces.
* See [example](http://gridstackjs.com/demo/float.html)
*
* @param val true to enable floating, false to disable
* @returns the grid instance for chaining
*
* @example
* grid.float(true); // Enable floating
* grid.float(false); // Disable floating (default)
*/
float(val) {
if (this.opts.float !== val) {
this.opts.float = this.engine.float = val;
this._triggerChangeEvent();
}
return this;
}
/**
* Get the current float mode setting.
*
* @returns true if floating is enabled, false otherwise
*
* @example
* const isFloating = grid.getFloat();
* console.log('Floating enabled:', isFloating);
*/
getFloat() {
return this.engine.float;
}
/**
* Get the position of the cell under a pixel on screen.
* @param position the position of the pixel to resolve in
* absolute coordinates, as an object with top and left properties
* @param useDocRelative if true, value will be based on document position vs parent position (Optional. Default false).
* Useful when grid is within `position: relative` element
*
* Returns an object with properties `x` and `y` i.e. the column and row in the grid.
*/
getCellFromPixel(position, useDocRelative = false) {
const box = this.el.getBoundingClientRect();
// console.log(`getBoundingClientRect left: ${box.left} top: ${box.top} w: ${box.w} h: ${box.h}`)
let containerPos;
if (useDocRelative) {
containerPos = { top: box.top + document.documentElement.scrollTop, left: box.left };
// console.log(`getCellFromPixel scrollTop: ${document.documentElement.scrollTop}`)
}
else {
containerPos = { top: this.el.offsetTop, left: this.el.offsetLeft };
// console.log(`getCellFromPixel offsetTop: ${containerPos.left} offsetLeft: ${containerPos.top}`)
}
const relativeLeft = position.left - containerPos.left;
const relativeTop = position.top - containerPos.top;
const columnWidth = (box.width / this.getColumn());
const rowHeight = (box.height / parseInt(this.el.getAttribute('gs-current-row')));
return { x: Math.floor(relativeLeft / columnWidth), y: Math.floor(relativeTop / rowHeight) };
}
/**
* Returns the current number of rows, which will be at least `minRow` if set.
* The row count is based on the highest positioned widget in the grid.
*
* @returns the current number of rows in the grid
*
* @example
* const rowCount = grid.getRow();
* console.log('Grid has', rowCount, 'rows');
*/
getRow() {
return Math.max(this.engine.getRow(), this.opts.minRow || 0);
}
/**
* Checks if the specified rectangular area is empty (no widgets occupy any part of it).
*
* @param x the x coordinate (column) of the area to check
* @param y the y coordinate (row) of the area to check
* @param w the width in columns of the area to check
* @param h the height in rows of the area to check
* @returns true if the area is completely empty, false if any widget overlaps
*
* @example
* // Check if a 2x2 area at position (1,1) is empty
* if (grid.isAreaEmpty(1, 1, 2, 2)) {
* console.log('Area is available for placement');
* }
*/
isAreaEmpty(x, y, w, h) {
return this.engine.isAreaEmpty(x, y, w, h);
}
/**
* If you add elements to your grid by hand (or have some framework creating DOM), you have to tell gridstack afterwards to make them widgets.
* If you want gridstack to add the elements for you, use `addWidget()` instead.
* Makes the given element a widget and returns it.
*
* @param els widget or single selector to convert.
* @param options widget definition to use instead of reading attributes or using default sizing values
* @returns the converted GridItemHTMLElement
*
* @example
* const grid = GridStack.init();
*
* // Create HTML content manually, possibly looking like:
* //
* grid.el.innerHTML = '';
*
* // Convert existing elements to widgets
* grid.makeWidget('#item-1'); // Uses gs-* attributes from DOM
* grid.makeWidget('#item-2', {w: 2, h: 1, content: 'Hello World'});
*
* // Or pass DOM element directly
* const element = document.getElementById('item-3');
* grid.makeWidget(element, {x: 0, y: 1, w: 4, h: 2});
*/
makeWidget(els, options) {
const el = GridStack.getElement(els);
if (!el || el.gridstackNode)
return el;
if (!el.parentElement)
this.el.appendChild(el);
this._prepareElement(el, true, options);
const node = el.gridstackNode;
this._updateContainerHeight();
// see if there is a sub-grid to create
if (node.subGridOpts) {
this.makeSubGrid(el, node.subGridOpts, undefined, false); // node.subGrid will be used as option in method, no need to pass
}
// if we're adding an item into 1 column make sure
// we don't override the larger 12 column layout that was already saved. #1985
let resetIgnoreLayoutsNodeChange;
if (this.opts.column === 1 && !this._ignoreLayoutsNodeChange) {
resetIgnoreLayoutsNodeChange = this._ignoreLayoutsNodeChange = true;
}
this._triggerAddEvent();
this._triggerChangeEvent();
if (resetIgnoreLayoutsNodeChange)
delete this._ignoreLayoutsNodeChange;
return el;
}
on(name, callback) {
// check for array of names being passed instead
if (name.indexOf(' ') !== -1) {
const names = name.split(' ');
names.forEach(name => this.on(name, callback));
return this;
}
// native CustomEvent handlers - cash the generic handlers so we can easily remove
if (name === 'change' || name === 'added' || name === 'removed' || name === 'enable' || name === 'disable') {
const noData = (name === 'enable' || name === 'disable');
if (noData) {
this._gsEventHandler[name] = (event) => callback(event);
}
else {
this._gsEventHandler[name] = (event) => { if (event.detail)
callback(event, event.detail); };
}
this.el.addEventListener(name, this._gsEventHandler[name]);
}
else if (name === 'drag' || name === 'dragstart' || name === 'dragstop' || name === 'resizestart' || name === 'resize'
|| name === 'resizestop' || name === 'dropped' || name === 'resizecontent') {
// drag&drop stop events NEED to be call them AFTER we update node attributes so handle them ourself.
// do same for start event to make it easier...
this._gsEventHandler[name] = callback;
}
else {
console.error('GridStack.on(' + name + ') event not supported');
}
return this;
}
/**
* unsubscribe from the 'on' event GridStackEvent
* @param name of the event (see possible values) or list of names space separated
*/
off(name) {
// check for array of names being passed instead
if (name.indexOf(' ') !== -1) {
const names = name.split(' ');
names.forEach(name => this.off(name));
return this;
}
if (name === 'change' || name === 'added' || name === 'removed' || name === 'enable' || name === 'disable') {
// remove native CustomEvent handlers
if (this._gsEventHandler[name]) {
this.el.removeEventListener(name, this._gsEventHandler[name]);
}
}
delete this._gsEventHandler[name];
return this;
}
/**
* Remove all event handlers from the grid. This is useful for cleanup when destroying a grid.
*
* @returns the grid instance for chaining
*
* @example
* grid.offAll(); // Remove all event listeners
*/
offAll() {
Object.keys(this._gsEventHandler).forEach((key) => this.off(key));
return this;
}
/**
* Removes widget from the grid.
* @param el widget or selector to modify
* @param removeDOM if `false` DOM element won't be removed from the tree (Default? true).
* @param triggerEvent if `false` (quiet mode) element will not be added to removed list and no 'removed' callbacks will be called (Default? true).
*/
removeWidget(els, removeDOM = true, triggerEvent = true) {
if (!els) {
console.error('Error: GridStack.removeWidget(undefined) called');
return this;
}
GridStack.getElements(els).forEach(el => {
if (el.parentElement && el.parentElement !== this.el)
return; // not our child!
let node = el.gridstackNode;
// For Meteor support: https://github.com/gridstack/gridstack.js/pull/272
if (!node) {
node = this.engine.nodes.find(n => el === n.el);
}
if (!node)
return;
if (removeDOM && GridStack.addRemoveCB) {
GridStack.addRemoveCB(this.el, node, false, false);
}
// remove our DOM data (circular link) and drag&drop permanently
delete el.gridstackNode;
this._removeDD(el);
this.engine.removeNode(node, removeDOM, triggerEvent);
if (removeDOM && el.parentElement) {
el.remove(); // in batch mode engine.removeNode doesn't call back to remove DOM
}
});
if (triggerEvent) {
this._triggerRemoveEvent();
this._triggerChangeEvent();
}
return this;
}
/**
* Removes all widgets from the grid.
* @param removeDOM if `false` DOM elements won't be removed from the tree (Default? `true`).
* @param triggerEvent if `false` (quiet mode) element will not be added to removed list and no 'removed' callbacks will be called (Default? true).
*/
removeAll(removeDOM = true, triggerEvent = true) {
// always remove our DOM data (circular link) before list gets emptied and drag&drop permanently
this.engine.nodes.forEach(n => {
if (removeDOM && GridStack.addRemoveCB) {
GridStack.addRemoveCB(this.el, n, false, false);
}
delete n.el.gridstackNode;
if (!this.opts.staticGrid)
this._removeDD(n.el);
});
this.engine.removeAll(removeDOM, triggerEvent);
if (triggerEvent)
this._triggerRemoveEvent();
return this;
}
/**
* Toggle the grid animation state. Toggles the `grid-stack-animate` class.
* @param doAnimate if true the grid will animate.
* @param delay if true setting will be set on next event loop.
*/
setAnimation(doAnimate = this.opts.animate, delay) {
if (delay) {
// delay, but check to make sure grid (opt) is still around
setTimeout(() => { if (this.opts)
this.setAnimation(doAnimate); });
}
else if (doAnimate) {
this.el.classList.add('grid-stack-animate');
}
else {
this.el.classList.remove('grid-stack-animate');
}
this.opts.animate = doAnimate;
return this;
}
/** @internal */
hasAnimationCSS() { return this.el.classList.contains('grid-stack-animate'); }
/**
* Toggle the grid static state, which permanently removes/add Drag&Drop support, unlike disable()/enable() that just turns it off/on.
* Also toggle the grid-stack-static class.
* @param val if true the grid become static.
* @param updateClass true (default) if css class gets updated
* @param recurse true (default) if sub-grids also get updated
*/
setStatic(val, updateClass = true, recurse = true) {
if (!!this.opts.staticGrid === val)
return this;
val ? this.opts.staticGrid = true : delete this.opts.staticGrid;
this._setupRemoveDrop();
this._setupAcceptWidget();
this.engine.nodes.forEach(n => {
this.prepareDragDrop(n.el); // either delete or init Drag&drop
if (n.subGrid && recurse)
n.subGrid.setStatic(val, updateClass, recurse);
});
if (updateClass) {
this._setStaticClass();
}
return this;
}
/**
* Updates the passed in options on the grid (similar to update(widget) for for the grid options).
* @param options PARTIAL grid options to update - only items specified will be updated.
* NOTE: not all options updating are currently supported (lot of code, unlikely to change)
*/
updateOptions(o) {
const opts = this.opts;
if (o === opts)
return this; // nothing to do
if (o.acceptWidgets !== undefined) {
opts.acceptWidgets = o.acceptWidgets;
this._setupAcceptWidget();
}
if (o.animate !== undefined)
this.setAnimation(o.animate);
if (o.cellHeight)
this.cellHeight(o.cellHeight);
if (o.class !== undefined && o.class !== opts.class) {
if (opts.class)
this.el.classList.remove(opts.class);
if (o.class)
this.el.classList.add(o.class);
}
// responsive column take over actual count (keep what we have now)
if (o.columnOpts) {
this.opts.columnOpts = o.columnOpts;
this.checkDynamicColumn();
}
else if (o.columnOpts === null && this.opts.columnOpts) {
delete this.opts.columnOpts;
this._updateResizeEvent();
}
else if (typeof (o.column) === 'number')
this.column(o.column);
if (o.margin !== undefined)
this.margin(o.margin);
if (o.staticGrid !== undefined)
this.setStatic(o.staticGrid);
if (o.disableDrag !== undefined && !o.staticGrid)
this.enableMove(!o.disableDrag);
if (o.disableResize !== undefined && !o.staticGrid)
this.enableResize(!o.disableResize);
if (o.float !== undefined)
this.float(o.float);
if (o.row !== undefined) {
opts.minRow = opts.maxRow = opts.row = o.row;
this._updateContainerHeight();
}
else {
if (o.minRow !== undefined) {
opts.minRow = o.minRow;
this._updateContainerHeight();
}
if (o.maxRow !== undefined)
opts.maxRow = o.maxRow;
}
if (o.children?.length)
this.load(o.children);
// TBD if we have a real need for these (more complex code)
// alwaysShowResizeHandle, draggable, handle, handleClass, itemClass, layout, placeholderClass, placeholderText, resizable, removable, row,...
return this;
}
/**
* Updates widget position/size and other info. This is used to change widget properties after creation.
* Can update position, size, content, and other widget properties.
*
* Note: If you need to call this on all nodes, use load() instead which will update what changed.
* Setting the same x,y for multiple items will be indeterministic and likely unwanted.
*
* @param els widget element(s) or selector to modify
* @param opt new widget options (x,y,w,h, etc.). Only those set will be updated.
* @returns the grid instance for chaining
*
* @example
* // Update widget size and position
* grid.update('.my-widget', { x: 2, y: 1, w: 3, h: 2 });
*
* // Update widget content
* grid.update(widget, { content: 'New content
' });
*
* // Update multiple properties
* grid.update('#my-widget', {
* w: 4,
* h: 3,
* noResize: true,
* locked: true
* });
*/
update(els, opt) {
GridStack.getElements(els).forEach(el => {
const n = el?.gridstackNode;
if (!n)
return;
const w = { ...Utils.copyPos({}, n), ...Utils.cloneDeep(opt) }; // make a copy we can modify in case they re-use it or multiple items
this.engine.nodeBoundFix(w);
delete w.autoPosition;
// move/resize widget if anything changed
const keys = ['x', 'y', 'w', 'h'];
let m;
if (keys.some(k => w[k] !== undefined && w[k] !== n[k])) {
m = {};
keys.forEach(k => {
m[k] = (w[k] !== undefined) ? w[k] : n[k];
delete w[k];
});
}
// for a move as well IFF there is any min/max fields set
if (!m && (w.minW || w.minH || w.maxW || w.maxH)) {
m = {}; // will use node position but validate values
}
// check for content changing
if (w.content !== undefined) {
const itemContent = el.querySelector('.grid-stack-item-content');
if (itemContent && itemContent.textContent !== w.content) {
n.content = w.content;
GridStack.renderCB(itemContent, w);
// restore any sub-grid back
if (n.subGrid?.el) {
itemContent.appendChild(n.subGrid.el);
n.subGrid._updateContainerHeight();
}
}
delete w.content;
}
// any remaining fields are assigned, but check for dragging changes, resize constrain
let changed = false;
let ddChanged = false;
for (const key in w) {
if (key[0] !== '_' && n[key] !== w[key]) {
n[key] = w[key];
changed = true;
ddChanged = ddChanged || (!this.opts.staticGrid && (key === 'noResize' || key === 'noMove' || key === 'locked'));
}
}
Utils.sanitizeMinMax(n);
// finally move the widget and update attr
if (m) {
const widthChanged = (m.w !== undefined && m.w !== n.w);
this.moveNode(n, m);
if (widthChanged && n.subGrid) {
// if we're animating the client size hasn't changed yet, so force a change (not exact size)
n.subGrid.onResize(this.hasAnimationCSS() ? n.w : undefined);
}
else {
this.resizeToContentCheck(widthChanged, n);
}
delete n._orig; // clear out original position now that we moved #2669
}
if (m || changed) {
this._writeAttr(el, n);
}
if (ddChanged) {
this.prepareDragDrop(n.el);
}
if (GridStack.updateCB)
GridStack.updateCB(n); // call user callback so they know widget got updated
});
return this;
}
moveNode(n, m) {
const wasUpdating = n._updating;
if (!wasUpdating)
this.engine.cleanNodes().beginUpdate(n);
this.engine.moveNode(n, m);
this._updateContainerHeight();
if (!wasUpdating) {
this._triggerChangeEvent();
this.engine.endUpdate();
}
}
/**
* Updates widget height to match the content height to avoid vertical scrollbars or dead space.
* This automatically adjusts the widget height based on its content size.
*
* Note: This assumes only 1 child under resizeToContentParent='.grid-stack-item-content'
* (sized to gridItem minus padding) that represents the entire content size.
*
* @param el the grid item element to resize
*
* @example
* // Resize a widget to fit its content
* const widget = document.querySelector('.grid-stack-item');
* grid.resizeToContent(widget);
*
* // This is commonly used with dynamic content:
* widget.querySelector('.content').innerHTML = 'New longer content...';
* grid.resizeToContent(widget);
*/
resizeToContent(el) {
if (!el)
return;
el.classList.remove('size-to-content-max');
if (!el.clientHeight)
return; // 0 when hidden, skip
const n = el.gridstackNode;
if (!n)
return;
const grid = n.grid;
if (!grid || el.parentElement !== grid.el)
return; // skip if we are not inside a grid
const cell = grid.getCellHeight(true);
if (!cell)
return;
let height = n.h ? n.h * cell : el.clientHeight; // getBoundingClientRect().height seem to flicker back and forth
let item;
if (n.resizeToContentParent)
item = el.querySelector(n.resizeToContentParent);
if (!item)
item = el.querySelector(GridStack.resizeToContentParent);
if (!item)
return;
const padding = el.clientHeight - item.clientHeight; // full - available height to our child (minus border, padding...)
const itemH = n.h ? n.h * cell - padding : item.clientHeight; // calculated to what cellHeight is or will become (rather than actual to prevent waiting for animation to finish)
let wantedH;
if (n.subGrid) {
// sub-grid - use their actual row count * their cell height, BUT append any content outside of the grid (eg: above text)
wantedH = n.subGrid.getRow() * n.subGrid.getCellHeight(true);
const subRec = n.subGrid.el.getBoundingClientRect();
const parentRec = el.getBoundingClientRect();
wantedH += subRec.top - parentRec.top;
}
else if (n.subGridOpts?.children?.length) {
// not sub-grid just yet (case above) wait until we do
return;
}
else {
// NOTE: clientHeight & getBoundingClientRect() is undefined for text and other leaf nodes. use container!
const child = item.firstElementChild;
if (!child) {
console.error(`Error: GridStack.resizeToContent() widget id:${n.id} '${GridStack.resizeToContentParent}'.firstElementChild is null, make sure to have a div like container. Skipping sizing.`);
return;
}
wantedH = child.getBoundingClientRect().height || itemH;
}
if (itemH === wantedH)
return;
height += wantedH - itemH;
let h = Math.ceil(height / cell);
// check for min/max and special sizing
const softMax = Number.isInteger(n.sizeToContent) ? n.sizeToContent : 0;
if (softMax && h > softMax) {
h = softMax;
el.classList.add('size-to-content-max'); // get v-scroll back
}
if (n.minH && h < n.minH)
h = n.minH;
else if (n.maxH && h > n.maxH)
h = n.maxH;
if (h !== n.h) {
grid._ignoreLayoutsNodeChange = true;
grid.moveNode(n, { h });
delete grid._ignoreLayoutsNodeChange;
}
}
/** call the user resize (so they can do extra work) else our build in version */
resizeToContentCBCheck(el) {
if (GridStack.resizeToContentCB)
GridStack.resizeToContentCB(el);
else
this.resizeToContent(el);
}
/**
* Rotate widgets by swapping their width and height. This is typically called when the user presses 'r' during dragging.
* The rotation swaps the w/h dimensions and adjusts min/max constraints accordingly.
*
* @param els widget element(s) or selector to rotate
* @param relative optional pixel coordinate relative to upper/left corner to rotate around (keeps that cell under cursor)
* @returns the grid instance for chaining
*
* @example
* // Rotate a specific widget
* grid.rotate('.my-widget');
*
* // Rotate with relative positioning during drag
* grid.rotate(widget, { left: 50, top: 30 });
*/
rotate(els, relative) {
GridStack.getElements(els).forEach(el => {
const n = el.gridstackNode;
if (!Utils.canBeRotated(n))
return;
const rot = { w: n.h, h: n.w, minH: n.minW, minW: n.minH, maxH: n.maxW, maxW: n.maxH };
// if given an offset, adjust x/y by column/row bounds when user presses 'r' during dragging
if (relative) {
const pivotX = relative.left > 0 ? Math.floor(relative.left / this.cellWidth()) : 0;
const pivotY = relative.top > 0 ? Math.floor(relative.top / this.opts.cellHeight) : 0;
rot.x = n.x + pivotX - (n.h - (pivotY + 1));
rot.y = (n.y + pivotY) - pivotX;
}
Object.keys(rot).forEach(k => { if (rot[k] === undefined)
delete rot[k]; });
const _orig = n._orig;
this.update(el, rot);
n._orig = _orig; // restore as move() will delete it
});
return this;
}
/**
* Updates the margins which will set all 4 sides at once - see `GridStackOptions.margin` for format options.
* Supports CSS string format of 1, 2, or 4 values or a single number.
*
* @param value margin value - can be:
* - Single number: `10` (applies to all sides)
* - Two values: `'10px 20px'` (top/bottom, left/right)
* - Four values: `'10px 20px 5px 15px'` (top, right, bottom, left)
* @returns the grid instance for chaining
*
* @example
* grid.margin(10); // 10px all sides
* grid.margin('10px 20px'); // 10px top/bottom, 20px left/right
* grid.margin('5px 10px 15px 20px'); // Different for each side
*/
margin(value) {
const isMultiValue = (typeof value === 'string' && value.split(' ').length > 1);
// check if we can skip... won't check if multi values (too much hassle)
if (!isMultiValue) {
const data = Utils.parseHeight(value);
if (this.opts.marginUnit === data.unit && this.opts.margin === data.h)
return;
}
// re-use existing margin handling
this.opts.margin = value;
this.opts.marginTop = this.opts.marginBottom = this.opts.marginLeft = this.opts.marginRight = undefined;
this._initMargin();
return this;
}
/**
* Returns the current margin value as a number (undefined if the 4 sides don't match).
* This only returns a number if all sides have the same margin value.
*
* @returns the margin value in pixels, or undefined if sides have different values
*
* @example
* const margin = grid.getMargin();
* if (margin !== undefined) {
* console.log('Uniform margin:', margin, 'px');
* } else {
* console.log('Margins are different on different sides');
* }
*/
getMargin() { return this.opts.margin; }
/**
* Returns true if the height of the grid will be less than the vertical
* constraint. Always returns true if grid doesn't have height constraint.
* @param node contains x,y,w,h,auto-position options
*
* @example
* if (grid.willItFit(newWidget)) {
* grid.addWidget(newWidget);
* } else {
* alert('Not enough free space to place the widget');
* }
*/
willItFit(node) {
// support legacy call for now
if (arguments.length > 1) {
console.warn('gridstack.ts: `willItFit(x,y,w,h,autoPosition)` is deprecated. Use `willItFit({x, y,...})`. It will be removed soon');
// eslint-disable-next-line prefer-rest-params
const a = arguments;
let i = 0, w = { x: a[i++], y: a[i++], w: a[i++], h: a[i++], autoPosition: a[i++] };
return this.willItFit(w);
}
return this.engine.willItFit(node);
}
/** @internal */
_triggerChangeEvent() {
if (this.engine.batchMode)
return this;
const elements = this.engine.getDirtyNodes(true); // verify they really changed
if (elements && elements.length) {
if (!this._ignoreLayoutsNodeChange) {
this.engine.layoutsNodesChange(elements);
}
this._triggerEvent('change', elements);
}
this.engine.saveInitial(); // we called, now reset initial values & dirty flags
return this;
}
/** @internal */
_triggerAddEvent() {
if (this.engine.batchMode)
return this;
if (this.engine.addedNodes?.length) {
if (!this._ignoreLayoutsNodeChange) {
this.engine.layoutsNodesChange(this.engine.addedNodes);
}
// prevent added nodes from also triggering 'change' event (which is called next)
this.engine.addedNodes.forEach(n => { delete n._dirty; });
const addedNodes = [...this.engine.addedNodes];
this.engine.addedNodes = [];
this._triggerEvent('added', addedNodes);
}
return this;
}
/** @internal */
_triggerRemoveEvent() {
if (this.engine.batchMode)
return this;
if (this.engine.removedNodes?.length) {
const removedNodes = [...this.engine.removedNodes];
this.engine.removedNodes = [];
this._triggerEvent('removed', removedNodes);
}
return this;
}
/** @internal */
_triggerEvent(type, data) {
const event = data ? new CustomEvent(type, { bubbles: false, detail: data }) : new Event(type);
// check if we're nested, and if so call the outermost grid to trigger the event
// eslint-disable-next-line @typescript-eslint/no-this-alias
let grid = this;
while (grid.parentGridNode)
grid = grid.parentGridNode.grid;
grid.el.dispatchEvent(event);
return this;
}
/** @internal */
_updateContainerHeight() {
if (!this.engine || this.engine.batchMode)
return this;
const parent = this.parentGridNode;
let row = this.getRow() + this._extraDragRow; // this checks for minRow already
const cellHeight = this.opts.cellHeight;
const unit = this.opts.cellHeightUnit;
if (!cellHeight)
return this;
// check for css min height (non nested grid). TODO: support mismatch, say: min % while unit is px.
// If `minRow` was applied, don't override it with this check, and avoid performance issues
// (reflows) using `getComputedStyle`
if (!parent && !this.opts.minRow) {
const cssMinHeight = Utils.parseHeight(getComputedStyle(this.el)['minHeight']);
if (cssMinHeight.h > 0 && cssMinHeight.unit === unit) {
const minRow = Math.floor(cssMinHeight.h / cellHeight);
if (row < minRow) {
row = minRow;
}
}
}
this.el.setAttribute('gs-current-row', String(row));
this.el.style.removeProperty('min-height');
this.el.style.removeProperty('height');
if (row) {
// nested grids have 'insert:0' to fill the space of parent by default, but we may be taller so use min-height for possible scrollbars
this.el.style[parent ? 'minHeight' : 'height'] = row * cellHeight + unit;
}
// if we're a nested grid inside an sizeToContent item, tell it to resize itself too
if (parent && Utils.shouldSizeToContent(parent)) {
parent.grid.resizeToContentCBCheck(parent.el);
}
return this;
}
/** @internal */
_prepareElement(el, triggerAddEvent = false, node) {
node = node || this._readAttr(el);
el.gridstackNode = node;
node.el = el;
node.grid = this;
node = this.engine.addNode(node, triggerAddEvent);
// write the dom sizes and class
this._writeAttr(el, node);
el.classList.add(gridDefaults.itemClass, this.opts.itemClass);
const sizeToContent = Utils.shouldSizeToContent(node);
sizeToContent ? el.classList.add('size-to-content') : el.classList.remove('size-to-content');
if (sizeToContent)
this.resizeToContentCheck(false, node);
if (!Utils.lazyLoad(node))
this.prepareDragDrop(node.el);
return this;
}
/** @internal write position CSS vars and x,y,w,h attributes (not used for CSS but by users) back to element */
_writePosAttr(el, n) {
// Avoid overwriting the inline style of the element during drag/resize, but always update the placeholder
if ((!n._moving && !n._resizing) || this._placeholder === el) {
// width/height:1 x/y:0 is set by default in the main CSS, so no need to set inlined vars
el.style.top = n.y ? (n.y === 1 ? `var(--gs-cell-height)` : `calc(${n.y} * var(--gs-cell-height))`) : null;
el.style.left = n.x ? (n.x === 1 ? `var(--gs-column-width)` : `calc(${n.x} * var(--gs-column-width))`) : null;
el.style.width = n.w > 1 ? `calc(${n.w} * var(--gs-column-width))` : null;
el.style.height = n.h > 1 ? `calc(${n.h} * var(--gs-cell-height))` : null;
}
// NOTE: those are technically not needed anymore (v12+) as we have CSS vars for everything, but some users depends on them to render item size using CSS
n.x > 0 ? el.setAttribute('gs-x', String(n.x)) : el.removeAttribute('gs-x');
n.y > 0 ? el.setAttribute('gs-y', String(n.y)) : el.removeAttribute('gs-y');
n.w > 1 ? el.setAttribute('gs-w', String(n.w)) : el.removeAttribute('gs-w');
n.h > 1 ? el.setAttribute('gs-h', String(n.h)) : el.removeAttribute('gs-h');
return this;
}
/** @internal call to write any default attributes back to element */
_writeAttr(el, node) {
if (!node)
return this;
this._writePosAttr(el, node);
const attrs /*: GridStackWidget but strings */ = {
// autoPosition: 'gs-auto-position', // no need to write out as already in node and doesn't affect CSS
noResize: 'gs-no-resize',
noMove: 'gs-no-move',
locked: 'gs-locked',
id: 'gs-id',
sizeToContent: 'gs-size-to-content',
};
for (const key in attrs) {
if (node[key]) { // 0 is valid for x,y only but done above already and not in list anyway
el.setAttribute(attrs[key], String(node[key]));
}
else {
el.removeAttribute(attrs[key]);
}
}
return this;
}
/** @internal call to read any default attributes from element */
_readAttr(el, clearDefaultAttr = true) {
const n = {};
n.x = Utils.toNumber(el.getAttribute('gs-x'));
n.y = Utils.toNumber(el.getAttribute('gs-y'));
n.w = Utils.toNumber(el.getAttribute('gs-w'));
n.h = Utils.toNumber(el.getAttribute('gs-h'));
n.autoPosition = Utils.toBool(el.getAttribute('gs-auto-position'));
n.noResize = Utils.toBool(el.getAttribute('gs-no-resize'));
n.noMove = Utils.toBool(el.getAttribute('gs-no-move'));
n.locked = Utils.toBool(el.getAttribute('gs-locked'));
const attr = el.getAttribute('gs-size-to-content');
if (attr) {
if (attr === 'true' || attr === 'false')
n.sizeToContent = Utils.toBool(attr);
else
n.sizeToContent = parseInt(attr, 10);
}
n.id = el.getAttribute('gs-id');
// read but never written out
n.maxW = Utils.toNumber(el.getAttribute('gs-max-w'));
n.minW = Utils.toNumber(el.getAttribute('gs-min-w'));
n.maxH = Utils.toNumber(el.getAttribute('gs-max-h'));
n.minH = Utils.toNumber(el.getAttribute('gs-min-h'));
// v8.x optimization to reduce un-needed attr that don't render or are default CSS
if (clearDefaultAttr) {
if (n.w === 1)
el.removeAttribute('gs-w');
if (n.h === 1)
el.removeAttribute('gs-h');
if (n.maxW)
el.removeAttribute('gs-max-w');
if (n.minW)
el.removeAttribute('gs-min-w');
if (n.maxH)
el.removeAttribute('gs-max-h');
if (n.minH)
el.removeAttribute('gs-min-h');
}
// remove any key not found (null or false which is default, unless sizeToContent=false override)
for (const key in n) {
if (!n.hasOwnProperty(key))
return;
if (!n[key] && n[key] !== 0 && key !== 'sizeToContent') { // 0 can be valid value (x,y only really)
delete n[key];
}
}
return n;
}
/** @internal */
_setStaticClass() {
const classes = ['grid-stack-static'];
if (this.opts.staticGrid) {
this.el.classList.add(...classes);
this.el.setAttribute('gs-static', 'true');
}
else {
this.el.classList.remove(...classes);
this.el.removeAttribute('gs-static');
}
return this;
}
/**
* called when we are being resized - check if the one Column Mode needs to be turned on/off
* and remember the prev columns we used, or get our count from parent, as well as check for cellHeight==='auto' (square)
* or `sizeToContent` gridItem options.
*/
onResize(clientWidth = this.el?.clientWidth) {
if (!clientWidth)
return; // return if we're gone or no size yet (will get called again)
if (this.prevWidth === clientWidth)
return; // no-op
this.prevWidth = clientWidth;
// console.log('onResize ', clientWidth);
this.batchUpdate();
// see if we're nested and take our column count from our parent....
let columnChanged = false;
if (this._autoColumn && this.parentGridNode) {
if (this.opts.column !== this.parentGridNode.w) {
this.column(this.parentGridNode.w, this.opts.layout || 'list');
columnChanged = true;
}
}
else {
// else check for dynamic column
columnChanged = this.checkDynamicColumn();
}
// make the cells content square again
if (this._isAutoCellHeight)
this.cellHeight();
// update any nested grids, or items size
this.engine.nodes.forEach(n => {
if (n.subGrid)
n.subGrid.onResize();
});
if (!this._skipInitialResize)
this.resizeToContentCheck(columnChanged); // wait for anim of column changed (DOM reflow before we can size correctly)
delete this._skipInitialResize;
this.batchUpdate(false);
return this;
}
/** resizes content for given node (or all) if shouldSizeToContent() is true */
resizeToContentCheck(delay = false, n = undefined) {
if (!this.engine)
return; // we've been deleted in between!
// update any gridItem height with sizeToContent, but wait for DOM $animation_speed to settle if we changed column count
// TODO: is there a way to know what the final (post animation) size of the content will be so we can animate the column width and height together rather than sequentially ?
if (delay && this.hasAnimationCSS())
return setTimeout(() => this.resizeToContentCheck(false, n), this.animationDelay);
if (n) {
if (Utils.shouldSizeToContent(n))
this.resizeToContentCBCheck(n.el);
}
else if (this.engine.nodes.some(n => Utils.shouldSizeToContent(n))) {
const nodes = [...this.engine.nodes]; // in case order changes while resizing one
this.batchUpdate();
nodes.forEach(n => {
if (Utils.shouldSizeToContent(n))
this.resizeToContentCBCheck(n.el);
});
this._ignoreLayoutsNodeChange = true; // loop through each node will set/reset around each move, so set it here again
this.batchUpdate(false);
this._ignoreLayoutsNodeChange = false;
}
// call this regardless of shouldSizeToContent because widget might need to stretch to take available space after a resize
if (this._gsEventHandler['resizecontent'])
this._gsEventHandler['resizecontent'](null, n ? [n] : this.engine.nodes);
}
/** add or remove the grid element size event handler */
_updateResizeEvent(forceRemove = false) {
// only add event if we're not nested (parent will call us) and we're auto sizing cells or supporting dynamic column (i.e. doing work)
// or supporting new sizeToContent option.
const trackSize = !this.parentGridNode && (this._isAutoCellHeight || this.opts.sizeToContent || this.opts.columnOpts
|| this.engine.nodes.find(n => n.sizeToContent));
if (!forceRemove && trackSize && !this.resizeObserver) {
this._sizeThrottle = Utils.throttle(() => this.onResize(), this.opts.cellHeightThrottle);
this.resizeObserver = new ResizeObserver(() => this._sizeThrottle());
this.resizeObserver.observe(this.el);
this._skipInitialResize = true; // makeWidget will originally have called on startup
}
else if ((forceRemove || !trackSize) && this.resizeObserver) {
this.resizeObserver.disconnect();
delete this.resizeObserver;
delete this._sizeThrottle;
}
return this;
}
/** @internal convert a potential selector into actual element */
static getElement(els = '.grid-stack-item') { return Utils.getElement(els); }
/** @internal */
static getElements(els = '.grid-stack-item') { return Utils.getElements(els); }
/** @internal */
static getGridElement(els) { return GridStack.getElement(els); }
/** @internal */
static getGridElements(els) { return Utils.getElements(els); }
/** @internal initialize margin top/bottom/left/right and units */
_initMargin() {
let data;
let margin = 0;
// support passing multiple values like CSS (ex: '5px 10px 0 20px')
let margins = [];
if (typeof this.opts.margin === 'string') {
margins = this.opts.margin.split(' ');
}
if (margins.length === 2) { // top/bot, left/right like CSS
this.opts.marginTop = this.opts.marginBottom = margins[0];
this.opts.marginLeft = this.opts.marginRight = margins[1];
}
else if (margins.length === 4) { // Clockwise like CSS
this.opts.marginTop = margins[0];
this.opts.marginRight = margins[1];
this.opts.marginBottom = margins[2];
this.opts.marginLeft = margins[3];
}
else {
data = Utils.parseHeight(this.opts.margin);
this.opts.marginUnit = data.unit;
margin = this.opts.margin = data.h;
}
// see if top/bottom/left/right need to be set as well
const keys = ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'];
keys.forEach(k => {
if (this.opts[k] === undefined) {
this.opts[k] = margin;
}
else {
data = Utils.parseHeight(this.opts[k]);
this.opts[k] = data.h;
delete this.opts.margin;
}
});
this.opts.marginUnit = data.unit; // in case side were spelled out, use those units instead...
if (this.opts.marginTop === this.opts.marginBottom && this.opts.marginLeft === this.opts.marginRight && this.opts.marginTop === this.opts.marginRight) {
this.opts.margin = this.opts.marginTop; // makes it easier to check for no-ops in setMargin()
}
// finally Update the CSS margin variables (inside the cell height) */
const style = this.el.style;
style.setProperty('--gs-item-margin-top', `${this.opts.marginTop}${this.opts.marginUnit}`);
style.setProperty('--gs-item-margin-bottom', `${this.opts.marginBottom}${this.opts.marginUnit}`);
style.setProperty('--gs-item-margin-right', `${this.opts.marginRight}${this.opts.marginUnit}`);
style.setProperty('--gs-item-margin-left', `${this.opts.marginLeft}${this.opts.marginUnit}`);
return this;
}
/* ===========================================================================================
* drag&drop methods that used to be stubbed out and implemented in dd-gridstack.ts
* but caused loading issues in prod - see https://github.com/gridstack/gridstack.js/issues/2039
* ===========================================================================================
*/
/**
* Get the global drag & drop implementation instance.
* This provides access to the underlying drag & drop functionality.
*
* @returns the DDGridStack instance used for drag & drop operations
*
* @example
* const dd = GridStack.getDD();
* // Access drag & drop functionality
*/
static getDD() {
return dd;
}
/**
* call to setup dragging in from the outside (say toolbar), by specifying the class selection and options.
* Called during GridStack.init() as options, but can also be called directly (last param are used) in case the toolbar
* is dynamically create and needs to be set later.
* @param dragIn string selector (ex: '.sidebar-item') or list of dom elements
* @param dragInOptions options - see DDDragOpt. (default: {handle: '.grid-stack-item-content', appendTo: 'body'}
* @param widgets GridStackWidget def to assign to each element which defines what to create on drop
* @param root optional root which defaults to document (for shadow dom pass the parent HTMLDocument)
*/
static setupDragIn(dragIn, dragInOptions, widgets, root = document) {
if (dragInOptions?.pause !== undefined) {
DDManager.pauseDrag = dragInOptions.pause;
}
dragInOptions = { appendTo: 'body', helper: 'clone', ...(dragInOptions || {}) }; // default to handle:undefined = drag by the whole item
const els = (typeof dragIn === 'string') ? Utils.getElements(dragIn, root) : dragIn;
els.forEach((el, i) => {
if (!dd.isDraggable(el))
dd.dragIn(el, dragInOptions);
if (widgets?.[i])
el.gridstackNode = widgets[i];
});
}
/**
* Enables/Disables dragging by the user for specific grid elements.
* For all items and future items, use enableMove() instead. No-op for static grids.
*
* Note: If you want to prevent an item from moving due to being pushed around by another
* during collision, use the 'locked' property instead.
*
* @param els widget element(s) or selector to modify
* @param val if true widget will be draggable, assuming the parent grid isn't noMove or static
* @returns the grid instance for chaining
*
* @example
* // Make specific widgets draggable
* grid.movable('.my-widget', true);
*
* // Disable dragging for specific widgets
* grid.movable('#fixed-widget', false);
*/
movable(els, val) {
if (this.opts.staticGrid)
return this; // can't move a static grid!
GridStack.getElements(els).forEach(el => {
const n = el.gridstackNode;
if (!n)
return;
val ? delete n.noMove : n.noMove = true;
this.prepareDragDrop(n.el); // init DD if need be, and adjust
});
return this;
}
/**
* Enables/Disables user resizing for specific grid elements.
* For all items and future items, use enableResize() instead. No-op for static grids.
*
* @param els widget element(s) or selector to modify
* @param val if true widget will be resizable, assuming the parent grid isn't noResize or static
* @returns the grid instance for chaining
*
* @example
* // Make specific widgets resizable
* grid.resizable('.my-widget', true);
*
* // Disable resizing for specific widgets
* grid.resizable('#fixed-size-widget', false);
*/
resizable(els, val) {
if (this.opts.staticGrid)
return this; // can't resize a static grid!
GridStack.getElements(els).forEach(el => {
const n = el.gridstackNode;
if (!n)
return;
val ? delete n.noResize : n.noResize = true;
this.prepareDragDrop(n.el); // init DD if need be, and adjust
});
return this;
}
/**
* Temporarily disables widgets moving/resizing.
* If you want a more permanent way (which freezes up resources) use `setStatic(true)` instead.
*
* Note: This is a no-op for static grids.
*
* This is a shortcut for:
* ```typescript
* grid.enableMove(false);
* grid.enableResize(false);
* ```
*
* @param recurse if true (default), sub-grids also get updated
* @returns the grid instance for chaining
*
* @example
* // Disable all interactions
* grid.disable();
*
* // Disable only this grid, not sub-grids
* grid.disable(false);
*/
disable(recurse = true) {
if (this.opts.staticGrid)
return;
this.enableMove(false, recurse);
this.enableResize(false, recurse);
this._triggerEvent('disable');
return this;
}
/**
* Re-enables widgets moving/resizing - see disable().
* Note: This is a no-op for static grids.
*
* This is a shortcut for:
* ```typescript
* grid.enableMove(true);
* grid.enableResize(true);
* ```
*
* @param recurse if true (default), sub-grids also get updated
* @returns the grid instance for chaining
*
* @example
* // Re-enable all interactions
* grid.enable();
*
* // Enable only this grid, not sub-grids
* grid.enable(false);
*/
enable(recurse = true) {
if (this.opts.staticGrid)
return;
this.enableMove(true, recurse);
this.enableResize(true, recurse);
this._triggerEvent('enable');
return this;
}
/**
* Enables/disables widget moving for all widgets. No-op for static grids.
* Note: locally defined items (with noMove property) still override this setting.
*
* @param doEnable if true widgets will be movable, if false moving is disabled
* @param recurse if true (default), sub-grids also get updated
* @returns the grid instance for chaining
*
* @example
* // Enable moving for all widgets
* grid.enableMove(true);
*
* // Disable moving for all widgets
* grid.enableMove(false);
*
* // Enable only this grid, not sub-grids
* grid.enableMove(true, false);
*/
enableMove(doEnable, recurse = true) {
if (this.opts.staticGrid)
return this; // can't move a static grid!
doEnable ? delete this.opts.disableDrag : this.opts.disableDrag = true; // FIRST before we update children as grid overrides #1658
this.engine.nodes.forEach(n => {
this.prepareDragDrop(n.el);
if (n.subGrid && recurse)
n.subGrid.enableMove(doEnable, recurse);
});
return this;
}
/**
* Enables/disables widget resizing for all widgets. No-op for static grids.
* Note: locally defined items (with noResize property) still override this setting.
*
* @param doEnable if true widgets will be resizable, if false resizing is disabled
* @param recurse if true (default), sub-grids also get updated
* @returns the grid instance for chaining
*
* @example
* // Enable resizing for all widgets
* grid.enableResize(true);
*
* // Disable resizing for all widgets
* grid.enableResize(false);
*
* // Enable only this grid, not sub-grids
* grid.enableResize(true, false);
*/
enableResize(doEnable, recurse = true) {
if (this.opts.staticGrid)
return this; // can't size a static grid!
doEnable ? delete this.opts.disableResize : this.opts.disableResize = true; // FIRST before we update children as grid overrides #1658
this.engine.nodes.forEach(n => {
this.prepareDragDrop(n.el);
if (n.subGrid && recurse)
n.subGrid.enableResize(doEnable, recurse);
});
return this;
}
/** @internal call when drag (and drop) needs to be cancelled (Esc key) */
cancelDrag() {
const n = this._placeholder?.gridstackNode;
if (!n)
return;
if (n._isExternal) {
// remove any newly inserted nodes (from outside)
n._isAboutToRemove = true;
this.engine.removeNode(n);
}
else if (n._isAboutToRemove) {
// restore any temp removed (dragged over trash)
GridStack._itemRemoving(n.el, false);
}
this.engine.restoreInitial();
}
/** @internal removes any drag&drop present (called during destroy) */
_removeDD(el) {
dd.draggable(el, 'destroy').resizable(el, 'destroy');
if (el.gridstackNode) {
delete el.gridstackNode._initDD; // reset our DD init flag
}
delete el.ddElement;
return this;
}
/** @internal called to add drag over to support widgets being added externally */
_setupAcceptWidget() {
// check if we need to disable things
if (this.opts.staticGrid || (!this.opts.acceptWidgets && !this.opts.removable)) {
dd.droppable(this.el, 'destroy');
return this;
}
// vars shared across all methods
let cellHeight, cellWidth;
const onDrag = (event, el, helper) => {
helper = helper || el;
const node = helper.gridstackNode;
if (!node)
return;
// if the element is being dragged from outside, scale it down to match the grid's scale
// and slightly adjust its position relative to the mouse
if (!node.grid?.el) {
// this scales the helper down
helper.style.transform = `scale(${1 / this.dragTransform.xScale},${1 / this.dragTransform.yScale})`;
// this makes it so that the helper is well positioned relative to the mouse after scaling
const helperRect = helper.getBoundingClientRect();
helper.style.left = helperRect.x + (this.dragTransform.xScale - 1) * (event.clientX - helperRect.x) / this.dragTransform.xScale + 'px';
helper.style.top = helperRect.y + (this.dragTransform.yScale - 1) * (event.clientY - helperRect.y) / this.dragTransform.yScale + 'px';
helper.style.transformOrigin = `0px 0px`;
}
let { top, left } = helper.getBoundingClientRect();
const rect = this.el.getBoundingClientRect();
left -= rect.left;
top -= rect.top;
const ui = {
position: {
top: top * this.dragTransform.xScale,
left: left * this.dragTransform.yScale
}
};
if (node._temporaryRemoved) {
node.x = Math.max(0, Math.round(left / cellWidth));
node.y = Math.max(0, Math.round(top / cellHeight));
delete node.autoPosition;
this.engine.nodeBoundFix(node);
// don't accept *initial* location if doesn't fit #1419 (locked drop region, or can't grow), but maybe try if it will go somewhere
if (!this.engine.willItFit(node)) {
node.autoPosition = true; // ignore x,y and try for any slot...
if (!this.engine.willItFit(node)) {
dd.off(el, 'drag'); // stop calling us
return; // full grid or can't grow
}
if (node._willFitPos) {
// use the auto position instead #1687
Utils.copyPos(node, node._willFitPos);
delete node._willFitPos;
}
}
// re-use the existing node dragging method
this._onStartMoving(helper, event, ui, node, cellWidth, cellHeight);
}
else {
// re-use the existing node dragging that does so much of the collision detection
this._dragOrResize(helper, event, ui, node, cellWidth, cellHeight);
}
};
dd.droppable(this.el, {
accept: (el) => {
const node = el.gridstackNode || this._readAttr(el, false);
// set accept drop to true on ourself (which we ignore) so we don't get "can't drop" icon in HTML5 mode while moving
if (node?.grid === this)
return true;
if (!this.opts.acceptWidgets)
return false;
// check for accept method or class matching
let canAccept = true;
if (typeof this.opts.acceptWidgets === 'function') {
canAccept = this.opts.acceptWidgets(el);
}
else {
const selector = (this.opts.acceptWidgets === true ? '.grid-stack-item' : this.opts.acceptWidgets);
canAccept = el.matches(selector);
}
// finally check to make sure we actually have space left #1571 #2633
if (canAccept && node && this.opts.maxRow) {
const n = { w: node.w, h: node.h, minW: node.minW, minH: node.minH }; // only width/height matters and autoPosition
canAccept = this.engine.willItFit(n);
}
return canAccept;
}
})
/**
* entering our grid area
*/
.on(this.el, 'dropover', (event, el, helper) => {
// console.log(`over ${this.el.gridstack.opts.id} ${count++}`); // TEST
let node = helper?.gridstackNode || el.gridstackNode;
// ignore drop enter on ourself (unless we temporarily removed) which happens on a simple drag of our item
if (node?.grid === this && !node._temporaryRemoved) {
// delete node._added; // reset this to track placeholder again in case we were over other grid #1484 (dropout doesn't always clear)
return false; // prevent parent from receiving msg (which may be a grid as well)
}
// If sidebar item, restore the sidebar node size to ensure consistent behavior when dragging between grids
if (node?._sidebarOrig) {
node.w = node._sidebarOrig.w;
node.h = node._sidebarOrig.h;
}
// fix #1578 when dragging fast, we may not get a leave on the previous grid so force one now
if (node?.grid && node.grid !== this && !node._temporaryRemoved) {
// console.log('dropover without leave'); // TEST
const otherGrid = node.grid;
otherGrid._leave(el, helper);
}
helper = helper || el;
// cache cell dimensions (which don't change), position can animate if we removed an item in otherGrid that affects us...
cellWidth = this.cellWidth();
cellHeight = this.getCellHeight(true);
// sidebar items: load any element attributes if we don't have a node on first enter from the sidebar
if (!node) {
const attr = helper.getAttribute('data-gs-widget') || helper.getAttribute('gridstacknode'); // TBD: temp support for old V11.0.0 attribute
if (attr) {
try {
node = JSON.parse(attr);
}
catch (error) {
console.error("Gridstack dropover: Bad JSON format: ", attr);
}
helper.removeAttribute('data-gs-widget');
helper.removeAttribute('gridstacknode');
}
if (!node)
node = this._readAttr(helper); // used to pass false for #2354, but now we clone top level node
// On first grid enter from sidebar, set the initial sidebar item size properties for the node
node._sidebarOrig = { w: node.w, h: node.h };
}
if (!node.grid) { // sidebar item
if (!node.el)
node = { ...node }; // clone first time we're coming from sidebar (since 'clone' doesn't copy vars)
node._isExternal = true;
helper.gridstackNode = node;
}
// calculate the grid size based on element outer size
const w = node.w || Math.round(helper.offsetWidth / cellWidth) || 1;
const h = node.h || Math.round(helper.offsetHeight / cellHeight) || 1;
// if the item came from another grid, make a copy and save the original info in case we go back there
if (node.grid && node.grid !== this) {
// copy the node original values (min/max/id/etc...) but override width/height/other flags which are this grid specific
// console.log('dropover cloning node'); // TEST
if (!el._gridstackNodeOrig)
el._gridstackNodeOrig = node; // shouldn't have multiple nested!
el.gridstackNode = node = { ...node, w, h, grid: this };
delete node.x;
delete node.y;
this.engine.cleanupNode(node)
.nodeBoundFix(node);
// restore some internal fields we need after clearing them all
node._initDD =
node._isExternal = // DOM needs to be re-parented on a drop
node._temporaryRemoved = true; // so it can be inserted onDrag below
}
else {
node.w = w;
node.h = h;
node._temporaryRemoved = true; // so we can insert it
}
// clear any marked for complete removal (Note: don't check _isAboutToRemove as that is cleared above - just do it)
GridStack._itemRemoving(node.el, false);
dd.on(el, 'drag', onDrag);
// make sure this is called at least once when going fast #1578
onDrag(event, el, helper);
return false; // prevent parent from receiving msg (which may be a grid as well)
})
/**
* Leaving our grid area...
*/
.on(this.el, 'dropout', (event, el, helper) => {
// console.log(`out ${this.el.gridstack.opts.id} ${count++}`); // TEST
const node = helper?.gridstackNode || el.gridstackNode;
if (!node)
return false;
// fix #1578 when dragging fast, we might get leave after other grid gets enter (which calls us to clean)
// so skip this one if we're not the active grid really..
if (!node.grid || node.grid === this) {
this._leave(el, helper);
// if we were created as temporary nested grid, go back to before state
if (this._isTemp) {
this.removeAsSubGrid(node);
}
}
return false; // prevent parent from receiving msg (which may be grid as well)
})
/**
* end - releasing the mouse
*/
.on(this.el, 'drop', (event, el, helper) => {
const node = helper?.gridstackNode || el.gridstackNode;
// ignore drop on ourself from ourself that didn't come from the outside - dragend will handle the simple move instead
if (node?.grid === this && !node._isExternal)
return false;
const wasAdded = !!this.placeholder.parentElement; // skip items not actually added to us because of constrains, but do cleanup #1419
const wasSidebar = el !== helper;
this.placeholder.remove();
delete this.placeholder.gridstackNode;
// disable animation when replacing a placeholder (already positioned) with actual content
if (wasAdded && this.opts.animate) {
this.setAnimation(false);
this.setAnimation(true, true); // delay adding back
}
// notify previous grid of removal
// console.log('drop delete _gridstackNodeOrig') // TEST
const origNode = el._gridstackNodeOrig;
delete el._gridstackNodeOrig;
if (wasAdded && origNode?.grid && origNode.grid !== this) {
const oGrid = origNode.grid;
oGrid.engine.removeNodeFromLayoutCache(origNode);
oGrid.engine.removedNodes.push(origNode);
oGrid._triggerRemoveEvent()._triggerChangeEvent();
// if it's an empty sub-grid that got auto-created, nuke it
if (oGrid.parentGridNode && !oGrid.engine.nodes.length && oGrid.opts.subGridDynamic) {
oGrid.removeAsSubGrid();
}
}
if (!node)
return false;
// use existing placeholder node as it's already in our list with drop location
if (wasAdded) {
this.engine.cleanupNode(node); // removes all internal _xyz values
node.grid = this;
}
delete node.grid?._isTemp;
dd.off(el, 'drag');
// if we made a copy insert that instead of the original (sidebar item)
if (helper !== el) {
helper.remove();
el = helper;
}
else {
el.remove(); // reduce flicker as we change depth here, and size further down
}
this._removeDD(el);
if (!wasAdded)
return false;
const subGrid = node.subGrid?.el?.gridstack; // set when actual sub-grid present
Utils.copyPos(node, this._readAttr(this.placeholder)); // placeholder values as moving VERY fast can throw things off #1578
Utils.removePositioningStyles(el);
// give the user a chance to alter the widget that will get inserted if new sidebar item
if (wasSidebar && (node.content || node.subGridOpts || GridStack.addRemoveCB)) {
delete node.el;
el = this.addWidget(node);
}
else {
this._prepareElement(el, true, node);
this.el.appendChild(el);
// resizeToContent is skipped in _prepareElement() until node is visible (clientHeight=0) so call it now
this.resizeToContentCheck(false, node);
if (subGrid) {
subGrid.parentGridNode = node;
}
this._updateContainerHeight();
}
this.engine.addedNodes.push(node);
this._triggerAddEvent();
this._triggerChangeEvent();
this.engine.endUpdate();
if (this._gsEventHandler['dropped']) {
this._gsEventHandler['dropped']({ ...event, type: 'dropped' }, origNode && origNode.grid ? origNode : undefined, node);
}
return false; // prevent parent from receiving msg (which may be grid as well)
});
return this;
}
/** @internal mark item for removal */
static _itemRemoving(el, remove) {
if (!el)
return;
const node = el ? el.gridstackNode : undefined;
if (!node?.grid || el.classList.contains(node.grid.opts.removableOptions.decline))
return;
remove ? node._isAboutToRemove = true : delete node._isAboutToRemove;
remove ? el.classList.add('grid-stack-item-removing') : el.classList.remove('grid-stack-item-removing');
}
/** @internal called to setup a trash drop zone if the user specifies it */
_setupRemoveDrop() {
if (typeof this.opts.removable !== 'string')
return this;
const trashEl = document.querySelector(this.opts.removable);
if (!trashEl)
return this;
// only register ONE static drop-over/dropout callback for the 'trash', and it will
// update the passed in item and parent grid because the '.trash' is a shared resource anyway,
// and Native DD only has 1 event CB (having a list and technically a per grid removableOptions complicates things greatly)
if (!this.opts.staticGrid && !dd.isDroppable(trashEl)) {
dd.droppable(trashEl, this.opts.removableOptions)
.on(trashEl, 'dropover', (event, el) => GridStack._itemRemoving(el, true))
.on(trashEl, 'dropout', (event, el) => GridStack._itemRemoving(el, false));
}
return this;
}
/**
* prepares the element for drag&drop - this is normally called by makeWidget() unless are are delay loading
* @param el GridItemHTMLElement of the widget
* @param [force=false]
* */
prepareDragDrop(el, force = false) {
const node = el?.gridstackNode;
if (!node)
return;
const noMove = node.noMove || this.opts.disableDrag;
const noResize = node.noResize || this.opts.disableResize;
// check for disabled grid first
const disable = this.opts.staticGrid || (noMove && noResize);
if (force || disable) {
if (node._initDD) {
this._removeDD(el); // nukes everything instead of just disable, will add some styles back next
delete node._initDD;
}
if (disable)
el.classList.add('ui-draggable-disabled', 'ui-resizable-disabled'); // add styles one might depend on #1435
if (!force)
return this;
}
if (!node._initDD) {
// variables used/cashed between the 3 start/move/end methods, in addition to node passed above
let cellWidth;
let cellHeight;
/** called when item starts moving/resizing */
const onStartMoving = (event, ui) => {
// trigger any 'dragstart' / 'resizestart' manually
this.triggerEvent(event, event.target);
cellWidth = this.cellWidth();
cellHeight = this.getCellHeight(true); // force pixels for calculations
this._onStartMoving(el, event, ui, node, cellWidth, cellHeight);
};
/** called when item is being dragged/resized */
const dragOrResize = (event, ui) => {
this._dragOrResize(el, event, ui, node, cellWidth, cellHeight);
};
/** called when the item stops moving/resizing */
const onEndMoving = (event) => {
this.placeholder.remove();
delete this.placeholder.gridstackNode;
delete node._moving;
delete node._resizing;
delete node._event;
delete node._lastTried;
const widthChanged = node.w !== node._orig.w;
// if the item has moved to another grid, we're done here
const target = event.target;
if (!target.gridstackNode || target.gridstackNode.grid !== this)
return;
node.el = target;
if (node._isAboutToRemove) {
const grid = el.gridstackNode.grid;
if (grid._gsEventHandler[event.type]) {
grid._gsEventHandler[event.type](event, target);
}
grid.engine.nodes.push(node); // temp add it back so we can proper remove it next
grid.removeWidget(el, true, true);
}
else {
Utils.removePositioningStyles(target);
if (node._temporaryRemoved) {
// use last position we were at (not _orig as we may have pushed others and moved) and add it back
this._writePosAttr(target, node);
this.engine.addNode(node);
}
else {
// move to new placeholder location
this._writePosAttr(target, node);
}
this.triggerEvent(event, target);
}
// @ts-ignore
this._extraDragRow = 0; // @ts-ignore
this._updateContainerHeight(); // @ts-ignore
this._triggerChangeEvent();
this.engine.endUpdate();
if (event.type === 'resizestop') {
if (Number.isInteger(node.sizeToContent))
node.sizeToContent = node.h; // new soft limit
this.resizeToContentCheck(widthChanged, node); // wait for width animation if changed
}
};
dd.draggable(el, {
start: onStartMoving,
stop: onEndMoving,
drag: dragOrResize
}).resizable(el, {
start: onStartMoving,
stop: onEndMoving,
resize: dragOrResize
});
node._initDD = true; // we've set DD support now
}
// finally fine tune move vs resize by disabling any part...
dd.draggable(el, noMove ? 'disable' : 'enable')
.resizable(el, noResize ? 'disable' : 'enable');
return this;
}
/** @internal handles actual drag/resize start */
_onStartMoving(el, event, ui, node, cellWidth, cellHeight) {
this.engine.cleanNodes()
.beginUpdate(node);
// @ts-ignore
this._writePosAttr(this.placeholder, node);
this.el.appendChild(this.placeholder);
this.placeholder.gridstackNode = node;
// console.log('_onStartMoving placeholder') // TEST
// if the element is inside a grid, it has already been scaled
// we can use that as a scale reference
if (node.grid?.el) {
this.dragTransform = Utils.getValuesFromTransformedElement(el);
}
// if the element is being dragged from outside (not from any grid)
// we use the grid as the transformation reference, since the helper is not subject to transformation
else if (this.placeholder && this.placeholder.closest('.grid-stack')) {
const gridEl = this.placeholder.closest('.grid-stack');
this.dragTransform = Utils.getValuesFromTransformedElement(gridEl);
}
// Fallback
else {
this.dragTransform = {
xScale: 1,
xOffset: 0,
yScale: 1,
yOffset: 0,
};
}
node.el = this.placeholder;
node._lastUiPosition = ui.position;
node._prevYPix = ui.position.top;
node._moving = (event.type === 'dragstart'); // 'dropover' are not initially moving so they can go exactly where they enter (will push stuff out of the way)
node._resizing = (event.type === 'resizestart');
delete node._lastTried;
if (event.type === 'dropover' && node._temporaryRemoved) {
// console.log('engine.addNode x=' + node.x); // TEST
this.engine.addNode(node); // will add, fix collisions, update attr and clear _temporaryRemoved
node._moving = true; // AFTER, mark as moving object (wanted fix location before)
}
// set the min/max resize info taking into account the column count and position (so we don't resize outside the grid)
this.engine.cacheRects(cellWidth, cellHeight, this.opts.marginTop, this.opts.marginRight, this.opts.marginBottom, this.opts.marginLeft);
if (event.type === 'resizestart') {
const colLeft = this.getColumn() - node.x;
const rowLeft = (this.opts.maxRow || Number.MAX_SAFE_INTEGER) - node.y;
dd.resizable(el, 'option', 'minWidth', cellWidth * Math.min(node.minW || 1, colLeft))
.resizable(el, 'option', 'minHeight', cellHeight * Math.min(node.minH || 1, rowLeft))
.resizable(el, 'option', 'maxWidth', cellWidth * Math.min(node.maxW || Number.MAX_SAFE_INTEGER, colLeft))
.resizable(el, 'option', 'maxWidthMoveLeft', cellWidth * Math.min(node.maxW || Number.MAX_SAFE_INTEGER, node.x + node.w))
.resizable(el, 'option', 'maxHeight', cellHeight * Math.min(node.maxH || Number.MAX_SAFE_INTEGER, rowLeft))
.resizable(el, 'option', 'maxHeightMoveUp', cellHeight * Math.min(node.maxH || Number.MAX_SAFE_INTEGER, node.y + node.h));
}
}
/** @internal handles actual drag/resize */
_dragOrResize(el, event, ui, node, cellWidth, cellHeight) {
const p = { ...node._orig }; // could be undefined (_isExternal) which is ok (drag only set x,y and w,h will default to node value)
let resizing;
let mLeft = this.opts.marginLeft, mRight = this.opts.marginRight, mTop = this.opts.marginTop, mBottom = this.opts.marginBottom;
// if margins (which are used to pass mid point by) are large relative to cell height/width, reduce them down #1855
const mHeight = Math.round(cellHeight * 0.1), mWidth = Math.round(cellWidth * 0.1);
mLeft = Math.min(mLeft, mWidth);
mRight = Math.min(mRight, mWidth);
mTop = Math.min(mTop, mHeight);
mBottom = Math.min(mBottom, mHeight);
if (event.type === 'drag') {
if (node._temporaryRemoved)
return; // handled by dropover
const distance = ui.position.top - node._prevYPix;
node._prevYPix = ui.position.top;
if (this.opts.draggable.scroll !== false) {
Utils.updateScrollPosition(el, ui.position, distance);
}
// get new position taking into account the margin in the direction we are moving! (need to pass mid point by margin)
const left = ui.position.left + (ui.position.left > node._lastUiPosition.left ? -mRight : mLeft);
const top = ui.position.top + (ui.position.top > node._lastUiPosition.top ? -mBottom : mTop);
p.x = Math.round(left / cellWidth);
p.y = Math.round(top / cellHeight);
// @ts-ignore// if we're at the bottom hitting something else, grow the grid so cursor doesn't leave when trying to place below others
const prev = this._extraDragRow;
if (this.engine.collide(node, p)) {
const row = this.getRow();
let extra = Math.max(0, (p.y + node.h) - row);
if (this.opts.maxRow && row + extra > this.opts.maxRow) {
extra = Math.max(0, this.opts.maxRow - row);
} // @ts-ignore
this._extraDragRow = extra; // @ts-ignore
}
else
this._extraDragRow = 0; // @ts-ignore
if (this._extraDragRow !== prev)
this._updateContainerHeight();
if (node.x === p.x && node.y === p.y)
return; // skip same
// DON'T skip one we tried as we might have failed because of coverage <50% before
// if (node._lastTried && node._lastTried.x === x && node._lastTried.y === y) return;
}
else if (event.type === 'resize') {
if (p.x < 0)
return;
// Scrolling page if needed
Utils.updateScrollResize(event, el, cellHeight);
// get new size
p.w = Math.round((ui.size.width - mLeft) / cellWidth);
p.h = Math.round((ui.size.height - mTop) / cellHeight);
if (node.w === p.w && node.h === p.h)
return;
if (node._lastTried && node._lastTried.w === p.w && node._lastTried.h === p.h)
return; // skip one we tried (but failed)
// if we size on left/top side this might move us, so get possible new position as well
const left = ui.position.left + mLeft;
const top = ui.position.top + mTop;
p.x = Math.round(left / cellWidth);
p.y = Math.round(top / cellHeight);
resizing = true;
}
node._event = event;
node._lastTried = p; // set as last tried (will nuke if we go there)
const rect = {
x: ui.position.left + mLeft,
y: ui.position.top + mTop,
w: (ui.size ? ui.size.width : node.w * cellWidth) - mLeft - mRight,
h: (ui.size ? ui.size.height : node.h * cellHeight) - mTop - mBottom
};
if (this.engine.moveNodeCheck(node, { ...p, cellWidth, cellHeight, rect, resizing })) {
node._lastUiPosition = ui.position;
this.engine.cacheRects(cellWidth, cellHeight, mTop, mRight, mBottom, mLeft);
delete node._skipDown;
if (resizing && node.subGrid)
node.subGrid.onResize();
this._extraDragRow = 0; // @ts-ignore
this._updateContainerHeight();
const target = event.target; // @ts-ignore
// Do not write sidebar item attributes back to the original sidebar el
if (!node._sidebarOrig) {
this._writePosAttr(target, node);
}
this.triggerEvent(event, target);
}
}
/** call given event callback on our main top-most grid (if we're nested) */
triggerEvent(event, target) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let grid = this;
while (grid.parentGridNode)
grid = grid.parentGridNode.grid;
if (grid._gsEventHandler[event.type]) {
grid._gsEventHandler[event.type](event, target);
}
}
/** @internal called when item leaving our area by either cursor dropout event
* or shape is outside our boundaries. remove it from us, and mark temporary if this was
* our item to start with else restore prev node values from prev grid it came from.
*/
_leave(el, helper) {
helper = helper || el;
const node = helper.gridstackNode;
if (!node)
return;
// remove the scale of the helper on leave
helper.style.transform = helper.style.transformOrigin = null;
dd.off(el, 'drag'); // no need to track while being outside
// this gets called when cursor leaves and shape is outside, so only do this once
if (node._temporaryRemoved)
return;
node._temporaryRemoved = true;
this.engine.removeNode(node); // remove placeholder as well, otherwise it's a sign node is not in our list, which is a bigger issue
node.el = node._isExternal && helper ? helper : el; // point back to real item being dragged
const sidebarOrig = node._sidebarOrig;
if (node._isExternal)
this.engine.cleanupNode(node);
// Restore sidebar item initial size info to stay consistent when dragging between multiple grids
node._sidebarOrig = sidebarOrig;
if (this.opts.removable === true) { // boolean vs a class string
// item leaving us and we are supposed to remove on leave (no need to drag onto trash) mark it so
GridStack._itemRemoving(el, true);
}
// finally if item originally came from another grid, but left us, restore things back to prev info
if (el._gridstackNodeOrig) {
// console.log('leave delete _gridstackNodeOrig') // TEST
el.gridstackNode = el._gridstackNodeOrig;
delete el._gridstackNodeOrig;
}
else if (node._isExternal) {
// item came from outside restore all nodes back to original
this.engine.restoreInitial();
}
}
// legacy method removed
commit() { obsolete(this, this.batchUpdate(false), 'commit', 'batchUpdate', '5.2'); return this; }
}
/**
* callback to create the content of widgets so the app can control how to store and restore it
* By default this lib will do 'el.textContent = w.content' forcing text only support for avoiding potential XSS issues.
*/
GridStack.renderCB = (el, w) => { if (el && w?.content)
el.textContent = w.content; };
/** parent class for sizing content. defaults to '.grid-stack-item-content' */
GridStack.resizeToContentParent = '.grid-stack-item-content';
/** scoping so users can call GridStack.Utils.sort() for example */
GridStack.Utils = Utils;
/** scoping so users can call new GridStack.Engine(12) for example */
GridStack.Engine = GridStackEngine;
/** @internal current version compiled in code */
GridStack.GDRev = '12.3.2';
export { GridStack };
//# sourceMappingURL=gridstack.js.map