diff --git a/demo/column.html b/demo/column.html index 25156d287..189a0400a 100644 --- a/demo/column.html +++ b/demo/column.html @@ -58,20 +58,20 @@

setColumn() grid demo

]; var count = 0; grid.batchUpdate(); - for (count=0; count<3; count++) { + for (count=0; count<3;) { var n = items[count]; - grid.addWidget($('
' + count + (n.text ? n.text : '') + '
'), n); + grid.addWidget($('
' + count++ + (n.text ? n.text : '') + '
'), n); }; grid.commit(); $('#add-widget').click(function() { - var n = items[count++] || { + var n = items[count] || { x: Math.round(12 * Math.random()), y: Math.round(5 * Math.random()), width: Math.round(1 + 3 * Math.random()), height: Math.round(1 + 3 * Math.random()) }; - grid.addWidget($('
' + count + (n.text ? n.text : '') + '
'), n); + grid.addWidget($('
' + count++ + (n.text ? n.text : '') + '
'), n); }); $('#1column').click(function() { grid.setColumn(1); }); diff --git a/demo/float.html b/demo/float.html index 56a495014..59026f4d6 100644 --- a/demo/float.html +++ b/demo/float.html @@ -32,23 +32,24 @@

Float grid demo

new function () { this.items = [ - {x: 0, y: 6, width: 2, height: 2}, - {x: 3, y: 1, width: 1, height: 2}, - {x: 4, y: 2, width: 1, height: 1}, + {x: 2, y: 5, width: 1, height: 1}, {x: 2, y: 3, width: 3, height: 1}, - {x: 2, y: 5, width: 1, height: 1} + {x: 4, y: 2, width: 1, height: 1}, + {x: 3, y: 1, width: 1, height: 2}, + {x: 0, y: 6, width: 2, height: 2} ]; + var count = 0; this.grid = $('.grid-stack').data('gridstack'); this.addNewWidget = function() { - var node = this.items.pop() || { + var node = this.items[count] || { x: Math.round(12 * Math.random()), y: Math.round(5 * Math.random()), width: Math.round(1 + 3 * Math.random()), height: Math.round(1 + 3 * Math.random()) }; - this.grid.addWidget($('
'), node); + this.grid.addWidget($('
' + count++ + '
'), node); return false; }.bind(this); diff --git a/spec/gridstack-spec.js b/spec/gridstack-spec.js index ab516dda3..0b650c3b1 100644 --- a/spec/gridstack-spec.js +++ b/spec/gridstack-spec.js @@ -328,84 +328,116 @@ describe('gridstack', function() { }; $('.grid-stack').gridstack(options); var grid = $('.grid-stack').data('gridstack'); - var node1 = $('#item1').data('_gridstack_node'); - var node2 = $('#item2').data('_gridstack_node'); + var el1 = $('#item1') + var el2 = $('#item2') + // items start at 4x2 and 4x4 - expect(node1.x).toBe(0); - expect(node1.y).toBe(0); - expect(node1.width).toBe(4); - expect(node1.height).toBe(2); - - expect(node2.x).toBe(4); - expect(node2.y).toBe(0); - expect(node2.width).toBe(4); - expect(node2.height).toBe(4); + expect(parseInt(el1.attr('data-gs-x'))).toBe(0); + expect(parseInt(el1.attr('data-gs-y'))).toBe(0); + expect(parseInt(el1.attr('data-gs-width'))).toBe(4); + expect(parseInt(el1.attr('data-gs-height'))).toBe(2); + + expect(parseInt(el2.attr('data-gs-x'))).toBe(4); + expect(parseInt(el2.attr('data-gs-y'))).toBe(0); + expect(parseInt(el2.attr('data-gs-width'))).toBe(4); + expect(parseInt(el2.attr('data-gs-height'))).toBe(4); // 1 column will have item1, item2 grid.setColumn(1); - node1 = $('#item1').data('_gridstack_node'); - node2 = $('#item2').data('_gridstack_node'); expect(grid.opts.column).toBe(1); - expect(node1.x).toBe(0); - expect(node1.y).toBe(0); - expect(node1.width).toBe(1); - expect(node1.height).toBe(2); + expect(parseInt(el1.attr('data-gs-x'))).toBe(0); + expect(parseInt(el1.attr('data-gs-y'))).toBe(0); + expect(parseInt(el1.attr('data-gs-width'))).toBe(1); + expect(parseInt(el1.attr('data-gs-height'))).toBe(2); - expect(node2.x).toBe(0); - expect(node2.y).toBe(2); - expect(node2.width).toBe(1); - expect(node2.height).toBe(4); + expect(parseInt(el2.attr('data-gs-x'))).toBe(0); + expect(parseInt(el2.attr('data-gs-y'))).toBe(2); + expect(parseInt(el2.attr('data-gs-width'))).toBe(1); + expect(parseInt(el2.attr('data-gs-height'))).toBe(4); // add default 1x1 item to the end (1 column) var el3 = grid.addWidget(widgetHTML); expect(el3).not.toBe(null); - var node3 = $(el3).data('_gridstack_node'); - expect(node3.x).toBe(0); - expect(node3.y).toBe(6); - expect(node3.width).toBe(1); - expect(node3.height).toBe(1); + expect(parseInt(el3.attr('data-gs-x'))).toBe(0); + expect(parseInt(el3.attr('data-gs-y'))).toBe(6); + expect(parseInt(el3.attr('data-gs-width'))).toBe(1); + expect(parseInt(el3.attr('data-gs-height'))).toBe(1); - // 2 column will have item1, item2, item3 in 1 column still - grid.setColumn(2); - node1 = $('#item1').data('_gridstack_node'); - node2 = $('#item2').data('_gridstack_node'); - node3 = $('#item3').data('_gridstack_node'); - expect(grid.opts.column).toBe(2); - expect(node1.x).toBe(0); - expect(node1.y).toBe(0); - expect(node1.width).toBe(1); - expect(node1.height).toBe(2); + // back to 12 column and initial layout (other than new item3) + grid.setColumn(12); + expect(grid.opts.column).toBe(12); + expect(parseInt(el1.attr('data-gs-x'))).toBe(0); + expect(parseInt(el1.attr('data-gs-y'))).toBe(0); + expect(parseInt(el1.attr('data-gs-width'))).toBe(4); + expect(parseInt(el1.attr('data-gs-height'))).toBe(2); + + expect(parseInt(el2.attr('data-gs-x'))).toBe(4); + expect(parseInt(el2.attr('data-gs-y'))).toBe(0); + expect(parseInt(el2.attr('data-gs-width'))).toBe(4); + expect(parseInt(el2.attr('data-gs-height'))).toBe(4); + + expect(parseInt(el3.attr('data-gs-x'))).toBe(0); + expect(parseInt(el3.attr('data-gs-y'))).toBe(6); + expect(parseInt(el3.attr('data-gs-width'))).toBe(1); // ??? could take entire width if it did above + expect(parseInt(el3.attr('data-gs-height'))).toBe(1); + + // back to 1 column, move item2 to beginning to [3][1][2] vertically + grid.setColumn(1); + expect(grid.opts.column).toBe(1); + grid.move(el3, 0, 0); + + expect(parseInt(el3.attr('data-gs-x'))).toBe(0); + expect(parseInt(el3.attr('data-gs-y'))).toBe(0); + expect(parseInt(el3.attr('data-gs-width'))).toBe(1); + expect(parseInt(el3.attr('data-gs-height'))).toBe(1); - expect(node2.x).toBe(1); - expect(node2.y).toBe(0); - expect(node2.width).toBe(1); - expect(node2.height).toBe(4); + expect(parseInt(el1.attr('data-gs-y'))).toBe(1); + expect(parseInt(el1.attr('data-gs-width'))).toBe(1); + expect(parseInt(el1.attr('data-gs-height'))).toBe(2); - expect(node3.x).toBe(0); - expect(node3.y).toBe(6); - expect(node3.width).toBe(1); // ??? could stay at 1 or take entire width still ? - expect(node3.height).toBe(1); + expect(parseInt(el2.attr('data-gs-x'))).toBe(0); + expect(parseInt(el2.attr('data-gs-y'))).toBe(3); + expect(parseInt(el2.attr('data-gs-width'))).toBe(1); + expect(parseInt(el2.attr('data-gs-height'))).toBe(4); - // back to 12 column and initial layout (other than new item3) + // back to 12 column, el3 to be beginning still, but [1][2] to be in 1 columns still but wide 4x2 and 4x still grid.setColumn(12); expect(grid.opts.column).toBe(12); - node1 = $('#item1').data('_gridstack_node'); - node2 = $('#item2').data('_gridstack_node'); - node3 = $('#item3').data('_gridstack_node'); - expect(node1.x).toBe(0); - expect(node1.y).toBe(0); - expect(node1.width).toBe(4); - expect(node1.height).toBe(2); - - expect(node2.x).toBe(4); - expect(node2.y).toBe(0); - expect(node2.width).toBe(4); - expect(node2.height).toBe(4); - - expect(node3.x).toBe(0); - expect(node3.y).toBe(6); - expect(node3.width).toBe(6); // ??? could 6 or taken entire width if it did above - expect(node3.height).toBe(1); + + expect(parseInt(el3.attr('data-gs-x'))).toBe(0); + expect(parseInt(el3.attr('data-gs-y'))).toBe(0); + expect(parseInt(el3.attr('data-gs-width'))).toBe(1); + expect(parseInt(el3.attr('data-gs-height'))).toBe(1); + + expect(parseInt(el1.attr('data-gs-x'))).toBe(0); + expect(parseInt(el1.attr('data-gs-y'))).toBe(1); + expect(parseInt(el1.attr('data-gs-width'))).toBe(4); + expect(parseInt(el1.attr('data-gs-height'))).toBe(2); + + expect(parseInt(el2.attr('data-gs-x'))).toBe(0); + expect(parseInt(el2.attr('data-gs-y'))).toBe(3); + expect(parseInt(el2.attr('data-gs-width'))).toBe(4); + expect(parseInt(el2.attr('data-gs-height'))).toBe(4); + + // 2 column will have item1, item2, item3 in 1 column still but half the width + grid.setColumn(1); // test convert from small, should use 12 layout still + grid.setColumn(2); + expect(grid.opts.column).toBe(2); + + expect(parseInt(el3.attr('data-gs-x'))).toBe(0); + expect(parseInt(el3.attr('data-gs-y'))).toBe(0); + expect(parseInt(el3.attr('data-gs-width'))).toBe(1); // 1 as we scaled from 12 columns + expect(parseInt(el3.attr('data-gs-height'))).toBe(1); + + expect(parseInt(el1.attr('data-gs-x'))).toBe(0); + expect(parseInt(el1.attr('data-gs-y'))).toBe(1); + expect(parseInt(el1.attr('data-gs-width'))).toBe(1); + expect(parseInt(el1.attr('data-gs-height'))).toBe(2); + + expect(parseInt(el2.attr('data-gs-x'))).toBe(0); + expect(parseInt(el2.attr('data-gs-y'))).toBe(3); + expect(parseInt(el2.attr('data-gs-width'))).toBe(1); + expect(parseInt(el2.attr('data-gs-height'))).toBe(4); }); }); diff --git a/src/gridstack.js b/src/gridstack.js index a88b67fc3..0db53beba 100644 --- a/src/gridstack.js +++ b/src/gridstack.js @@ -479,7 +479,6 @@ if (node.minHeight !== undefined) { node.height = Math.max(node.height, node.minHeight); } node._id = node._id || ++idSeq; - // node._dirty = true; will be addEvent instead, unless it changes below... if (node.autoPosition) { this._sortNodes(); @@ -491,7 +490,6 @@ continue; } if (!this.nodes.find(Utils._isAddNodeIntercepted, {x: x, y: y, node: node})) { - node._dirty = (node.x !== x || node.y !== y); node.x = x; node.y = y; delete node.autoPosition; // found our slot @@ -525,6 +523,7 @@ }; GridStackEngine.prototype.removeAll = function(detachNode) { + delete this._layouts; if (this.nodes.length === 0) { return; } detachNode = (detachNode === undefined ? true : detachNode); this.nodes.forEach(function(n) { n._id = null; }); // hint that node is being removed @@ -1030,6 +1029,7 @@ // TODO: compare original X,Y,W,H (or entire node?) instead as _dirty can be a temporary state var elements = this.grid.getDirtyNodes(); if (elements && elements.length) { + this.grid._layoutsNodesChange(elements); this.container.trigger('change', [elements]); this.grid.cleanNodes(); // clear dirty flags now that we called } @@ -1038,6 +1038,7 @@ GridStack.prototype._triggerAddEvent = function() { if (this.grid._batchMode) { return; } if (this.grid._addedNodes && this.grid._addedNodes.length > 0) { + this.grid._layoutsNodesChange(this.grid._addedNodes); this.container.trigger('added', [this.grid._addedNodes]); this.grid._addedNodes = []; } @@ -1504,7 +1505,6 @@ GridStack.prototype.removeAll = function(detachNode) { if (detachNode !== false) { - delete this.grid._layouts; // remove our data structure before list gets emptied and DOM elements stay behind this.grid.nodes.forEach(function(node) { node.el.removeData('_gridstack_node') }); } @@ -1815,41 +1815,51 @@ } }; + /** called whenever a node is added or moved - updates the cached layouts */ + GridStackEngine.prototype._layoutsNodesChange = function(nodes) { + if (!this._layouts || this._ignoreLayoutsNodeChange) return; + // remove smaller layouts - we will re-generate those on the fly... larger ones need to update + this._layouts.forEach(function(layout, i) { + if (!layout || i === this.column) return; + if (i < this.column) { + this._layouts[i] = undefined; + } + else { + // TODO: save the original x,y,w (h isn't cached) and see what actually changed to propagate correctly ? + nodes.forEach(function(node) { + var n = layout.find(function(l) { return l._id === node._id }); + if (!n) return; + var ratio = i / this.column; + n.y = node.y; + n.x = Math.round(node.x * ratio); + // width ??? + }, this); + } + }, this); + } + /** - * Modify number of columns in the grid. Will attempt to update existing widgets - * to conform to new number of columns. Requires `gridstack-extra.css` or `gridstack-extra.min.css` for [1-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 doNotPropagate if true existing widgets will not be updated (optional) + * Called to scale the widget width & position up/down based on the column change. + * Note we store previous layouts (especially original ones) to make it possible to go + * from say 12 -> 1 -> 12 and get back to where we were. */ - GridStack.prototype.setColumn = function(column, doNotPropagate) { - if (this.opts.column === column) { return; } - var oldColumn = this.opts.column; - - this.container.removeClass('grid-stack-' + oldColumn); - this.container.addClass('grid-stack-' + column); - this.opts.column = this.grid.column = column; - - // - // now update the nodes positions, using the original ones with new ratio - // - - if (doNotPropagate === true || this.grid.nodes.length === 0) { return; } - var nodes = Utils.sort(this.grid.nodes, -1, oldColumn); // current column reverse sorting so we can insert last to front (limit collision) + GridStackEngine.prototype._updateNodeWidths = function(oldColumn, column) { + if (this.nodes.length === 0 || oldColumn === column) { return; } + var nodes = Utils.sort(this.nodes, -1, oldColumn); // current column reverse sorting so we can insert last to front (limit collision) // cache the current layout in case they want to go back (like 12 -> 1 -> 12) as it requires original data var copy = [nodes.length]; - nodes.forEach(function(n, i) {copy[i] = {x: n.x, y: n.y, width: n.width, _id: n._id}}); // only thing we use change is x,y,w and need id to find it back - this.grid._layouts = this.grid._layouts || []; // use array to find larger quick - this.grid._layouts[oldColumn] = copy; - - // see if we have cached previous layout. if NOT and we are going up in size (up-sampling) start with the largest layout we have (down-sampling) instead - var lastIndex = this.grid._layouts.length - 1; - var cacheNodes = this.grid._layouts[column] || []; - if (cacheNodes.length === 0 && column > oldColumn && lastIndex > column) { - cacheNodes = this.grid._layouts[lastIndex] || []; + nodes.forEach(function(n, i) {copy[i] = {x: n.x, y: n.y, width: n.width, _id: n._id}}); // only thing we change is x,y,w and id to find it back + this._layouts = this._layouts || []; // use array to find larger quick + this._layouts[oldColumn] = copy; + + // see if we have cached previous layout. if NOT and we are going up in size start with the largest layout as down-scaling is more accurate + var lastIndex = this._layouts.length - 1; + var cacheNodes = this._layouts[column] || []; + if (cacheNodes.length === 0 && column > oldColumn && column < lastIndex) { + cacheNodes = this._layouts[lastIndex] || []; if (cacheNodes.length) { - // pretend we came from that larger column by assigning those values at starting point) + // pretend we came from that larger column by assigning those values as starting point oldColumn = lastIndex; cacheNodes.forEach(function(cacheNode) { var j = nodes.findIndex(function(n) {return n && n._id === cacheNode._id}); @@ -1882,19 +1892,46 @@ nodes.forEach(function(node) { if (!node) return; node.x = Math.round(node.x * ratio); - node.width = Math.round(node.width * ratio) || 1; + node.width = (oldColumn === 1 ? 1 : (Math.round(node.width * ratio) || 1)); newNodes.push(node); }); newNodes = Utils.sort(newNodes, -1, column); // finally relayout them in reverse order (to get correct placement) + this._ignoreLayoutsNodeChange = true; this.batchUpdate(); - this.grid.nodes = []; // pretend we have no nodes to start with (we use same structures) to simplify layout + this.nodes = []; // pretend we have no nodes to start with (we use same structures) to simplify layout newNodes.forEach(function(node) { - this.grid.addNode(node, false); // 'false' for add event trigger + this.addNode(node, false); // 'false' for add event trigger node._dirty = true; // force attr update }, this); this.commit(); + delete this._ignoreLayoutsNodeChange; + } + + /** + * Modify number of columns in the grid. Will attempt to update existing widgets + * to conform to new number of columns. Requires `gridstack-extra.css` or `gridstack-extra.min.css` for [1-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 doNotPropagate if true existing widgets will not be updated (optional) + */ + GridStack.prototype.setColumn = function(column, doNotPropagate) { + if (this.opts.column === column) { return; } + var oldColumn = this.opts.column; + + this.container.removeClass('grid-stack-' + oldColumn); + this.container.addClass('grid-stack-' + column); + this.opts.column = this.grid.column = column; + + // update the items now + if (doNotPropagate === true) { return; } + this.grid._updateNodeWidths(oldColumn, column); + + // and trigger our event last... + this.grid._ignoreLayoutsNodeChange = true; + this._triggerChangeEvent(); + delete this.grid._ignoreLayoutsNodeChange; }; GridStack.prototype.float = function(val) {