UNPKG

gridstack

Version:

TypeScript/JS lib for dashboard layout and creation, no external dependencies, with many wrappers (React, Angular, Vue, Ember, knockout...)

1,091 lines 67.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GridStack = void 0; /*! * GridStack 5.0 * https://gridstackjs.com/ * * Copyright (c) 2021 Alain Dumesny * see root license https://github.com/gridstack/gridstack.js/tree/master/LICENSE */ const gridstack_engine_1 = require("./gridstack-engine"); const utils_1 = require("./utils"); const gridstack_ddi_1 = require("./gridstack-ddi"); // export all dependent file as well to make it easier for users to just import the main file __exportStar(require("./types"), exports); __exportStar(require("./utils"), exports); __exportStar(require("./gridstack-engine"), exports); __exportStar(require("./gridstack-ddi"), exports); // default values for grid options - used during init and when saving out const GridDefaults = { column: 12, minRow: 0, maxRow: 0, itemClass: 'grid-stack-item', placeholderClass: 'grid-stack-placeholder', placeholderText: '', handle: '.grid-stack-item-content', handleClass: null, styleInHead: false, cellHeight: 'auto', cellHeightThrottle: 100, margin: 10, auto: true, minWidth: 768, float: false, staticGrid: false, animate: true, alwaysShowResizeHandle: false, resizable: { autoHide: true, handles: 'se' }, draggable: { handle: '.grid-stack-item-content', scroll: false, appendTo: 'body' }, disableDrag: false, disableResize: false, rtl: 'auto', removable: false, removableOptions: { accept: '.grid-stack-item' }, marginUnit: 'px', cellHeightUnit: 'px', disableOneColumnMode: false, oneColumnModeDomSort: false }; /** * 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 * <div class="grid-stack"> * <div class="grid-stack-item"> * <div class="grid-stack-item-content">Item 1</div> * </div> * </div> */ class GridStack { /** * Construct a grid item from the given element and options * @param el * @param opts */ constructor(el, opts = {}) { /** @internal */ this._gsEventHandler = {}; /** @internal extra row added when dragging at the bottom of the grid */ this._extraDragRow = 0; this.el = el; // exposed HTML element to the user opts = opts || {}; // handles null/undefined/0 // if row property exists, replace minRow and maxRow instead if (opts.row) { opts.minRow = opts.maxRow = opts.row; delete opts.row; } let rowAttr = utils_1.Utils.toNumber(el.getAttribute('gs-row')); // flag only valid in sub-grids (handled by parent, not here) if (opts.column === 'auto') { delete opts.column; } // elements attributes override any passed options (like CSS style) - merge the two together let defaults = Object.assign(Object.assign({}, utils_1.Utils.cloneDeep(GridDefaults)), { column: utils_1.Utils.toNumber(el.getAttribute('gs-column')) || 12, minRow: rowAttr ? rowAttr : utils_1.Utils.toNumber(el.getAttribute('gs-min-row')) || 0, maxRow: rowAttr ? rowAttr : utils_1.Utils.toNumber(el.getAttribute('gs-max-row')) || 0, staticGrid: utils_1.Utils.toBool(el.getAttribute('gs-static')) || false, _styleSheetClass: 'grid-stack-instance-' + (Math.random() * 10000).toFixed(0), alwaysShowResizeHandle: opts.alwaysShowResizeHandle || false, resizable: { autoHide: !(opts.alwaysShowResizeHandle || false), handles: 'se' }, draggable: { handle: (opts.handleClass ? '.' + opts.handleClass : (opts.handle ? opts.handle : '')) || '.grid-stack-item-content', scroll: false, appendTo: 'body' }, removableOptions: { accept: '.' + (opts.itemClass || 'grid-stack-item') } }); if (el.getAttribute('gs-animate')) { // default to true, but if set to false use that instead defaults.animate = utils_1.Utils.toBool(el.getAttribute('gs-animate')); } this.opts = utils_1.Utils.defaults(opts, defaults); opts = null; // make sure we use this.opts instead this.initMargin(); // part of settings defaults... // Now check if we're loading into 1 column mode FIRST so we don't do un-necessary work (like cellHeight = width / 12 then go 1 column) if (this.opts.column !== 1 && !this.opts.disableOneColumnMode && this._widthOrContainer() <= this.opts.minWidth) { this._prevColumn = this.getColumn(); this.opts.column = 1; } if (this.opts.rtl === 'auto') { this.opts.rtl = (el.style.direction === 'rtl'); } if (this.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) let parentGridItemEl = utils_1.Utils.closestByClass(this.el, GridDefaults.itemClass); if (parentGridItemEl && parentGridItemEl.gridstackNode) { this.opts._isNested = parentGridItemEl.gridstackNode; this.opts._isNested.subGrid = this; parentGridItemEl.classList.add('grid-stack-nested'); this.el.classList.add('grid-stack-nested'); } this._isAutoCellHeight = (this.opts.cellHeight === 'auto'); if (this._isAutoCellHeight || this.opts.cellHeight === 'initial') { // make the cell content square initially (will use resize/column event to keep it square) this.cellHeight(undefined, false); } else { // append unit if any are set if (typeof this.opts.cellHeight == 'number' && this.opts.cellHeightUnit && this.opts.cellHeightUnit !== GridDefaults.cellHeightUnit) { this.opts.cellHeight = this.opts.cellHeight + this.opts.cellHeightUnit; delete this.opts.cellHeightUnit; } this.cellHeight(this.opts.cellHeight, false); } this.el.classList.add(this.opts._styleSheetClass); this._setStaticClass(); this.engine = new gridstack_engine_1.GridStackEngine({ column: this.getColumn(), float: this.opts.float, maxRow: this.opts.maxRow, onChange: (cbNodes) => { let maxH = 0; this.engine.nodes.forEach(n => { maxH = Math.max(maxH, n.y + n.h); }); cbNodes.forEach(n => { let el = n.el; if (!el) return; if (n._removeDOM) { if (el) el.remove(); delete n._removeDOM; } else { this._writePosAttr(el, n); } }); this._updateStyles(false, maxH); // false = don't recreate, just append if need be } }); if (this.opts.auto) { this.batchUpdate(); // prevent in between re-layout #1535 TODO: this only set float=true, need to prevent collision check... let elements = []; this.getGridItems().forEach(el => { let x = parseInt(el.getAttribute('gs-x')); let y = parseInt(el.getAttribute('gs-y')); elements.push({ el, // if x,y are missing (autoPosition) add them to end of list - but keep their respective DOM order i: (Number.isNaN(x) ? 1000 : x) + (Number.isNaN(y) ? 1000 : y) * this.getColumn() }); }); elements.sort((a, b) => a.i - b.i).forEach(e => this._prepareElement(e.el)); this.commit(); } this.setAnimation(this.opts.animate); this._updateStyles(); if (this.opts.column != 12) { this.el.classList.add('grid-stack-' + this.opts.column); } // legacy support to appear 'per grid` options when really global. if (this.opts.dragIn) GridStack.setupDragIn(this.opts.dragIn, this.opts.dragInOptions); delete this.opts.dragIn; delete this.opts.dragInOptions; this._setupRemoveDrop(); this._setupAcceptWidget(); this._updateWindowResizeEvent(); } /** * 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 * let grid = GridStack.init(); * * Note: the HTMLElement (of type GridHTMLElement) will store a `gridstack: GridStack` value that can be retrieve later * let grid = document.querySelector('.grid-stack').gridstack; */ static init(options = {}, elOrString = '.grid-stack') { let 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_1.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 * let grids = GridStack.initAll(); * grids.forEach(...) */ static initAll(options = {}, selector = '.grid-stack') { let grids = []; GridStack.getGridElements(selector).forEach(el => { if (!el.gridstack) { el.gridstack = new GridStack(el, utils_1.Utils.cloneDeep(options)); delete options.dragIn; delete options.dragInOptions; // only need to be done once (really a static global thing, not per grid) } 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; // create the grid element, but check if the passed 'parent' already has grid styling and should be used instead let el = parent; if (!parent.classList.contains('grid-stack')) { let doc = document.implementation.createHTMLDocument(''); // IE needs a param doc.body.innerHTML = `<div class="grid-stack ${opt.class || ''}"></div>`; el = doc.body.children[0]; parent.appendChild(el); } // create grid class and load any children let grid = GridStack.init(opt, el); if (grid.opts.children) { let children = grid.opts.children; delete grid.opts.children; grid.load(children); } return grid; } /** @internal create placeholder DIV as needed */ get placeholder() { if (!this._placeholder) { let placeholderChild = document.createElement('div'); // child so padding match item-content placeholderChild.className = 'placeholder-content'; if (this.opts.placeholderText) { placeholderChild.innerHTML = this.opts.placeholderText; } this._placeholder = document.createElement('div'); this._placeholder.classList.add(this.opts.placeholderClass, GridDefaults.itemClass, this.opts.itemClass); this.placeholder.appendChild(placeholderChild); } return this._placeholder; } /** * 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()`. * * @example * let grid = GridStack.init(); * grid.addWidget({w: 3, content: 'hello'}); * grid.addWidget('<div class="grid-stack-item"><div class="grid-stack-item-content">hello</div></div>', {w: 3}); * * @param el GridStackWidget (which can have content string as well), html element, or string definition to add * @param options widget position/size options (optional, and ignore if first param is already option) - see GridStackWidget */ addWidget(els, options) { // support legacy call for now ? if (arguments.length > 2) { console.warn('gridstack.ts: `addWidget(el, x, y, width...)` is deprecated. Use `addWidget({x, y, w, content, ...})`. It will be removed soon'); // eslint-disable-next-line prefer-rest-params let a = arguments, i = 1, opt = { x: a[i++], y: a[i++], w: a[i++], h: a[i++], autoPosition: a[i++], minW: a[i++], maxW: a[i++], minH: a[i++], maxH: a[i++], id: a[i++] }; return this.addWidget(els, opt); } function isGridStackWidget(w) { return w.x !== undefined || w.y !== undefined || w.w !== undefined || w.h !== undefined || w.content !== undefined ? true : false; } let el; if (typeof els === 'string') { let doc = document.implementation.createHTMLDocument(''); // IE needs a param doc.body.innerHTML = els; el = doc.body.children[0]; } else if (arguments.length === 0 || arguments.length === 1 && isGridStackWidget(els)) { let content = els ? els.content || '' : ''; options = els; let doc = document.implementation.createHTMLDocument(''); // IE needs a param doc.body.innerHTML = `<div class="grid-stack-item ${this.opts.itemClass || ''}"><div class="grid-stack-item-content">${content}</div></div>`; el = doc.body.children[0]; } else { el = els; } // 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) let domAttr = this._readAttr(el); options = utils_1.Utils.cloneDeep(options) || {}; // make a copy before we modify in case caller re-uses it utils_1.Utils.defaults(options, domAttr); let node = this.engine.prepareNode(options); this._writeAttr(el, options); if (this._insertNotAppend) { this.el.prepend(el); } else { this.el.appendChild(el); } // similar to makeWidget() that doesn't read attr again and worse re-create a new node and loose any _id this._prepareElement(el, true, options); this._updateContainerHeight(); // check if nested grid definition is present if (node.subGrid && !node.subGrid.el) { // see if there is a sub-grid to create too // if column special case it set, remember that flag and set default let autoColumn; let ops = node.subGrid; if (ops.column === 'auto') { ops.column = node.w; ops.disableOneColumnMode = true; // driven by parent autoColumn = true; } let content = node.el.querySelector('.grid-stack-item-content'); node.subGrid = GridStack.addGrid(content, node.subGrid); if (autoColumn) { node.subGrid._autoColumn = true; } } this._triggerAddEvent(); this._triggerChangeEvent(); return el; } /** /** * 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. * @returns list of widgets or full grid option, including .children list of widgets */ save(saveContent = true, saveGridOpt = false) { // return copied nodes we can modify at will... let list = this.engine.save(saveContent); // check for HTML content and nested grids list.forEach(n => { if (saveContent && n.el && !n.subGrid) { // sub-grid are saved differently, not plain content let sub = n.el.querySelector('.grid-stack-item-content'); n.content = sub ? sub.innerHTML : undefined; if (!n.content) delete n.content; } else { if (!saveContent) { delete n.content; } // check for nested grid if (n.subGrid) { n.subGrid = n.subGrid.save(saveContent, true); } } delete n.el; }); // check if save entire grid options (needed for recursive) + children... if (saveGridOpt) { let o = utils_1.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'; delete o.disableOneColumnMode; } utils_1.Utils.removeInternalAndSame(o, GridDefaults); o.children = list; return o; } return list; } /** * load the widgets from a list. This will call update() on each (matching by id) or add/remove widgets that are not there. * * @param layout list of widgets definition to update/create * @param addAndRemove 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. * * @example * see http://gridstackjs.com/demo/serialization.html **/ load(layout, addAndRemove = true) { let items = GridStack.Utils.sort([...layout], -1, this._prevColumn || this.getColumn()); // make copy before we mod/sort this._insertNotAppend = true; // since create in reverse order... // if we're loading a layout into 1 column (_prevColumn is set only when going to 1) and items don't fit, make sure to save // the original wanted layout so we can scale back up correctly #1471 if (this._prevColumn && this._prevColumn !== this.opts.column && items.some(n => (n.x + n.w) > this.opts.column)) { this._ignoreLayoutsNodeChange = true; // skip layout update this.engine.cacheLayout(items, this._prevColumn, true); } let removed = []; this.batchUpdate(); // see if any items are missing from new layout and need to be removed first if (addAndRemove) { let copyNodes = [...this.engine.nodes]; // don't loop through array you modify copyNodes.forEach(n => { let item = items.find(w => n.id === w.id); if (!item) { if (typeof (addAndRemove) === 'function') { addAndRemove(this, n, false); } else { removed.push(n); // batch keep track this.removeWidget(n.el, true, false); } } }); } // now add/update the widgets items.forEach(w => { let item = (w.id || w.id === 0) ? this.engine.nodes.find(n => n.id === w.id) : undefined; if (item) { this.update(item.el, w); if (w.subGrid && w.subGrid.children) { // update any sub grid as well let sub = item.el.querySelector('.grid-stack'); if (sub && sub.gridstack) { sub.gridstack.load(w.subGrid.children); // TODO: support updating grid options ? this._insertNotAppend = true; // got reset by above call } } } else if (addAndRemove) { if (typeof (addAndRemove) === 'function') { w = addAndRemove(this, w, true).gridstackNode; } else { w = this.addWidget(w).gridstackNode; } } }); this.engine.removedNodes = removed; this.commit(); // after commit, clear that flag delete this._ignoreLayoutsNodeChange; delete this._insertNotAppend; return this; } /** * Initializes batch updates. You will see no changes until `commit()` method is called. */ batchUpdate() { this.engine.batchUpdate(); return this; } /** * Gets current cell height. */ getCellHeight(forcePixel = false) { if (this.opts.cellHeight && this.opts.cellHeight !== 'auto' && (!forcePixel || !this.opts.cellHeightUnit || this.opts.cellHeightUnit === 'px')) { return this.opts.cellHeight; } // else get first cell height let el = this.el.querySelector('.' + this.opts.itemClass); if (el) { let height = utils_1.Utils.toNumber(el.getAttribute('gs-h')); return Math.round(el.offsetHeight / height); } // else do entire grid and # of rows (but doesn't work if min-height is the actual constrain) let 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. * This method rebuilds an internal CSS style sheet. * Note: You can expect performance issues if call this method too often. * * @param val the cell height. If not passed (undefined), cells content will be made square (match width minus margin), * if pass 0 the CSS will be generated by the application instead. * @param update (Optional) if false, styles will not be updated * * @example * grid.cellHeight(100); // same as 100px * grid.cellHeight('70px'); * grid.cellHeight(grid.cellWidth() * 1.2); */ cellHeight(val, update = true) { // if not called internally, check if we're changing mode if (update && val !== undefined) { if (this._isAutoCellHeight !== (val === 'auto')) { this._isAutoCellHeight = (val === 'auto'); this._updateWindowResizeEvent(); } } if (val === 'initial' || val === 'auto') { val = undefined; } // make item content be square if (val === undefined) { let marginDiff = -this.opts.marginRight - this.opts.marginLeft + this.opts.marginTop + this.opts.marginBottom; val = this.cellWidth() + marginDiff; } let data = utils_1.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; if (update) { this._updateStyles(true, this.getRow()); // true = force re-create, for that # of rows } return this; } /** Gets current cell width. */ cellWidth() { return this._widthOrContainer() / this.getColumn(); } /** return our expected width (or parent) for 1 column check */ _widthOrContainer() { // use `offsetWidth` or `clientWidth` (no scrollbar) ? // https://stackoverflow.com/questions/21064101/understanding-offsetwidth-clientwidth-scrollwidth-and-height-respectively return (this.el.clientWidth || this.el.parentElement.clientWidth || window.innerWidth); } /** * Finishes batch updates. Updates DOM nodes. You must call it after batchUpdate. */ commit() { this.engine.commit(); this._triggerRemoveEvent(); this._triggerAddEvent(); this._triggerChangeEvent(); return this; } /** re-layout grid items to reclaim any empty space */ compact() { this.engine.compact(); 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], * 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 (position, size, etc...). * Note: items will never be outside of the current column boundaries. default (moveScale). Ignored for 1 column */ column(column, layout = 'moveScale') { if (column < 1 || this.opts.column === column) return this; let oldColumn = this.getColumn(); // if we go into 1 column mode (which happens if we're sized less than minW unless disableOneColumnMode is on) // then remember the original columns so we can restore. if (column === 1) { this._prevColumn = oldColumn; } else { delete this._prevColumn; } this.el.classList.remove('grid-stack-' + oldColumn); this.el.classList.add('grid-stack-' + column); this.opts.column = this.engine.column = column; // update the items now - see if the dom order nodes should be passed instead (else default to current list) let domNodes; if (column === 1 && this.opts.oneColumnModeDomSort) { domNodes = []; this.getGridItems().forEach(el => { if (el.gridstackNode) { domNodes.push(el.gridstackNode); } }); if (!domNodes.length) { domNodes = undefined; } } this.engine.updateNodeWidths(oldColumn, column, domNodes, layout); if (this._isAutoCellHeight) this.cellHeight(); // 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) */ getColumn() { return this.opts.column; } /** returns an array of grid HTML elements (no placeholder) - used to iterate through our children in DOM order */ getGridItems() { return Array.from(this.el.children) .filter((el) => el.matches('.' + this.opts.itemClass) && !el.matches('.' + this.opts.placeholderClass)); } /** * 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._updateWindowResizeEvent(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.classList.remove(this.opts._styleSheetClass); } else { this.el.parentNode.removeChild(this.el); } this._removeStylesheet(); this.el.removeAttribute('gs-current-row'); delete this.opts._isNested; delete this.opts; 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`) See [example](http://gridstackjs.com/demo/float.html) */ float(val) { this.engine.float = val; this._triggerChangeEvent(); return this; } /** * get the current float mode */ 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) { let 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}`) } let relativeLeft = position.left - containerPos.left; let relativeTop = position.top - containerPos.top; let columnWidth = (box.width / this.getColumn()); let 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 */ getRow() { return Math.max(this.engine.getRow(), this.opts.minRow); } /** * Checks if specified area is empty. * @param x the position x. * @param y the position y. * @param w the width of to check * @param h the height of to check */ isAreaEmpty(x, y, w, h) { return this.engine.isAreaEmpty(x, y, w, h); } /** * If you add elements to your grid by hand, 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. * * @example * let grid = GridStack.init(); * grid.el.appendChild('<div id="gsi-1" gs-w="3"></div>'); * grid.makeWidget('#gsi-1'); */ makeWidget(els) { let el = GridStack.getElement(els); this._prepareElement(el, true); this._updateContainerHeight(); this._triggerAddEvent(); this._triggerChangeEvent(); return el; } /** * Event handler that extracts our CustomEvent data out automatically for receiving custom * notifications (see doc for supported events) * @param name of the event (see possible values) or list of names space separated * @param callback function called with event and optional second/third param * (see README documentation for each signature). * * @example * grid.on('added', function(e, items) { log('added ', items)} ); * or * grid.on('added removed change', function(e, items) { log(e.type, items)} ); * * Note: in some cases it is the same as calling native handler and parsing the event. * grid.el.addEventListener('added', function(event) { log('added ', event.detail)} ); * */ on(name, callback) { // check for array of names being passed instead if (name.indexOf(' ') !== -1) { let names = name.split(' '); names.forEach(name => this.on(name, callback)); return this; } if (name === 'change' || name === 'added' || name === 'removed' || name === 'enable' || name === 'disable') { // native CustomEvent handlers - cash the generic handlers so we can easily remove let noData = (name === 'enable' || name === 'disable'); if (noData) { this._gsEventHandler[name] = (event) => callback(event); } else { this._gsEventHandler[name] = (event) => 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') { // 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.log('GridStack.on(' + name + ') event not supported, but you can still use $(".grid-stack").on(...) while jquery-ui is still used internally.'); } return this; } /** * unsubscribe from the 'on' event below * @param name of the event (see possible values) */ off(name) { // check for array of names being passed instead if (name.indexOf(' ') !== -1) { let 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; } /** * 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) { GridStack.getElements(els).forEach(el => { if (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; // remove our DOM data (circular link) and drag&drop permanently delete el.gridstackNode; gridstack_ddi_1.GridStackDDI.get().remove(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`). */ removeAll(removeDOM = true) { // always remove our DOM data (circular link) before list gets emptied and drag&drop permanently this.engine.nodes.forEach(n => { delete n.el.gridstackNode; gridstack_ddi_1.GridStackDDI.get().remove(n.el); }); this.engine.removeAll(removeDOM); this._triggerRemoveEvent(); return this; } /** * Toggle the grid animation state. Toggles the `grid-stack-animate` class. * @param doAnimate if true the grid will animate. */ setAnimation(doAnimate) { if (doAnimate) { this.el.classList.add('grid-stack-animate'); } else { this.el.classList.remove('grid-stack-animate'); } return this; } /** * 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. */ setStatic(val, updateClass = true) { if (this.opts.staticGrid === val) return this; this.opts.staticGrid = val; this._setupRemoveDrop(); this._setupAcceptWidget(); this.engine.nodes.forEach(n => this._prepareDragDropByNode(n)); // either delete or init Drag&drop if (updateClass) { this._setStaticClass(); } return this; } /** * Updates widget position/size and other info. Note: if you need to call this on all nodes, use load() instead which will update what changed. * @param els widget or selector of objects to modify (note: setting the same x,y for multiple items will be indeterministic and likely unwanted) * @param opt new widget options (x,y,w,h, etc..). Only those set will be updated. */ update(els, opt) { // support legacy call for now ? if (arguments.length > 2) { console.warn('gridstack.ts: `update(el, x, y, w, h)` is deprecated. Use `update(el, {x, w, content, ...})`. It will be removed soon'); // eslint-disable-next-line prefer-rest-params let a = arguments, i = 1; opt = { x: a[i++], y: a[i++], w: a[i++], h: a[i++] }; return this.update(els, opt); } GridStack.getElements(els).forEach(el => { if (!el || !el.gridstackNode) return; let n = el.gridstackNode; let w = utils_1.Utils.cloneDeep(opt); // make a copy we can modify in case they re-use it or multiple items delete w.autoPosition; // move/resize widget if anything changed let 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) { let sub = el.querySelector('.grid-stack-item-content'); if (sub && sub.innerHTML !== w.content) { sub.innerHTML = w.content; } 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')); } } // finally move the widget if (m) { this.engine.cleanNodes() .beginUpdate(n) .moveNode(n, m); this._updateContainerHeight(); this._triggerChangeEvent(); this.engine.endUpdate(); } if (changed) { // move will only update x,y,w,h so update the rest too this._writeAttr(el, n); } if (ddChanged) { this._prepareDragDropByNode(n); } }); return this; } /** * Updates the margins which will set all 4 sides at once - see `GridStackOptions.margin` for format options (CSS string format of 1,2,4 values or single number). * @param value margin value */ margin(value) { let isMultiValue = (typeof value === 'string' && value.split(' ').length > 1); // check if we can skip re-creating our CSS file... won't check if multi values (too much hassle) if (!isMultiValue) { let data = utils_1.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(); this._updateStyles(true); // true = force re-create return this; } /** returns current margin number value (undefined if 4 sides don't match) */ 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 let a = arguments, 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; let 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 && this.engine.addedNodes.length > 0) { 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; }); this._triggerEvent('added', this.engine.addedNodes); this.engine.addedNodes = []; } return this; } /** @internal */ _triggerRemoveEvent() { if (this.engine.batchMode) return this; if (this.engine.removedNodes && this.engine.removedNodes.length > 0) { this._triggerEvent('removed', this.engine.removedNodes); this.engine.removedNodes = []; } return this; } /** @internal */ _triggerEvent(name, data) { let event = data ? new CustomEvent(name, { bubbles: false, detail: data }) : new Event(name); this.el.dispatchEvent(event); return this; } /** @internal called to delete the current dynamic style sheet used for our layout */ _removeStylesheet() { if (this._styles) { utils_1.Utils.removeStylesheet(this._styles._id); delete this._styles; } return this; } /** @internal updated/create the CSS styles for row based layout and initial margin setting */ _updateStyles(forceUpdate = false, maxH) { // call to delete existing one if we change cellHeight / margin if (forceUpdate) { this._removeStylesheet(); } this._updateContainerHeight(); // if user is telling us they will handle the CSS themselves by setting heights to 0. Do we need this opts really ?? if (this.opts.cellHeight === 0) { return this; } let cellHeight = this.opts.cellHeight; let cellHeightUnit = this.opts.cellHeightUnit; let prefix = `.${this.opts._styleSheetClass} > .${this.opts.itemClass}`; // create one as needed if (!this._styles) { let id = 'gridstack-style-' + (Math.random() * 100000).toFixed(); // insert style to parent (instead of 'head' by default) to support WebComponent let styleLocation = this.opts.styleInHead ? undefined : this.el.parentNode; this._styles = utils_1.Utils.createStylesheet(id, styleLocation); if (!this._styles) return this; this._styles._id = id; this._styles._max = 0; // these are done once only utils_1.Utils.addCSSRule(this._styles, prefix, `min-height: ${cellHeight}${cellHeightUnit}`); // content margins let top = this.opts.marginTop + this.opts.marginUnit; let bottom = this.opts.marginBottom + this.opts.marginUnit; le