diff --git a/demo/column.html b/demo/column.html index 0fe00de9a..25156d287 100644 --- a/demo/column.html +++ b/demo/column.html @@ -48,30 +48,30 @@

setColumn() grid demo

} var items = [ - {x: 0, y: 4, width: 12, height: 1}, - {x: 5, y: 3, width: 2, height: 1}, - {x: 1, y: 3, width: 4, height: 1}, - {x: 0, y: 0, width: 1, height: 1}, - {}, // autoPosition testing - {x: 5, y: 0, width: 1, height: 1}, + {x: 0, y: 0, width: 2, height: 2}, {x: 2, y: 0, width: 2, height: 1}, - {x: 0, y: 0, width: 2, height: 2} + {x: 5, y: 0, width: 1, height: 1}, + {text: ' auto'}, // autoPosition testing + {x: 1, y: 3, width: 4, height: 1}, + {x: 5, y: 3, width: 2, height: 1}, + {x: 0, y: 4, width: 12, height: 1} ]; var count = 0; grid.batchUpdate(); for (count=0; count<3; count++) { - grid.addWidget($('
' + count + '
'), items.pop()); + var n = items[count]; + grid.addWidget($('
' + count + (n.text ? n.text : '') + '
'), n); }; grid.commit(); $('#add-widget').click(function() { - var node = items.pop() || { + 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++ + '
'), node); + grid.addWidget($('
' + count + (n.text ? n.text : '') + '
'), n); }); $('#1column').click(function() { grid.setColumn(1); }); diff --git a/demo/two.html b/demo/two.html index c41c0bb12..907028062 100644 --- a/demo/two.html +++ b/demo/two.html @@ -74,11 +74,13 @@

Two grids demo

float: false + Compact
float: true + Compact
@@ -136,13 +138,21 @@

Two grids demo

appendTo: 'body', }); - $('#float1').click(function() { toggleFloat('#float1', '#grid1'); }); - $('#float2').click(function() { toggleFloat('#float2', '#grid2'); }); + $('#float1').click(function() { toggleFloat('#float1', '#grid1') }); + $('#float2').click(function() { toggleFloat('#float2', '#grid2') }); function toggleFloat(button, grid) { var grid = $(grid).data('gridstack'); grid.float(! grid.float()); $(button).html('float: ' + grid.float()); } + + $('#compact1').click(function() { compact('#grid1') }); + $('#compact2').click(function() { compact('#grid2') }); + function compact(grid) { + var grid = $(grid).data('gridstack'); + grid.compact(); + } + }); diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 61bb574c7..70dcb36fa 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -28,6 +28,7 @@ Change log ## v0.5.5-dev (upcoming changes) - add `float(val)` to set/get the grid float mode [#1088](https://github.com/gridstack/gridstack.js/pull/1088) +- add `compact()` relayout grid items to reclaim any empty space [#1101](https://github.com/gridstack/gridstack.js/pull/1101) - Allow percentage as a valid unit for height [#1093](https://github.com/gridstack/gridstack.js/pull/1093) - fixed callbacks to get either `added, removed, change` or combination if adding a node require also to change its (x,y) for example. Also you can now call `batchUpdate()` before calling a bunch of `addWidget()` and get a single event callback (more efficient). diff --git a/doc/README.md b/doc/README.md index 921803184..9baf38f11 100644 --- a/doc/README.md +++ b/doc/README.md @@ -23,6 +23,7 @@ gridstack.js API - [addWidget(el, [options])](#addwidgetel-options) - [addWidget(el, [x, y, width, height, autoPosition, minWidth, maxWidth, minHeight, maxHeight, id])](#addwidgetel-x-y-width-height-autoposition-minwidth-maxwidth-minheight-maxheight-id) - [batchUpdate()](#batchupdate) + - [compact()](#compact) - [cellHeight()](#cellheight) - [cellHeight(val, noUpdate)](#cellheightval-noupdate) - [cellWidth()](#cellwidth) @@ -252,6 +253,10 @@ grid.addWidget(el, 0, 0, 3, 2, true); starts batch updates. You will see no changes until `commit()` method is called. +### compact() + +relayout grid items to reclaim any empty space. + ### cellHeight() Gets current cell height. diff --git a/spec/gridstack-spec.js b/spec/gridstack-spec.js index be2e1d20f..ab516dda3 100644 --- a/spec/gridstack-spec.js +++ b/spec/gridstack-spec.js @@ -276,7 +276,7 @@ describe('gridstack', function() { }); }); - describe('grid.column', function() { + describe('grid.setColumn', function() { beforeEach(function() { document.body.insertAdjacentHTML('afterbegin', gridstackHTML); }); @@ -341,7 +341,7 @@ describe('gridstack', function() { expect(node2.width).toBe(4); expect(node2.height).toBe(4); - // one column will have item1, item2 + // 1 column will have item1, item2 grid.setColumn(1); node1 = $('#item1').data('_gridstack_node'); node2 = $('#item2').data('_gridstack_node'); @@ -365,6 +365,27 @@ describe('gridstack', function() { expect(node3.width).toBe(1); expect(node3.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); + + expect(node2.x).toBe(1); + expect(node2.y).toBe(0); + expect(node2.width).toBe(1); + expect(node2.height).toBe(4); + + 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); + // back to 12 column and initial layout (other than new item3) grid.setColumn(12); expect(grid.opts.column).toBe(12); @@ -383,7 +404,7 @@ describe('gridstack', function() { expect(node3.x).toBe(0); expect(node3.y).toBe(6); - expect(node3.width).toBe(12); // take entire row still + expect(node3.width).toBe(6); // ??? could 6 or taken entire width if it did above expect(node3.height).toBe(1); }); }); @@ -1251,4 +1272,38 @@ describe('gridstack', function() { } }); }); + + describe('grid.compact', function() { + beforeEach(function() { + document.body.insertAdjacentHTML('afterbegin', gridstackHTML); + }); + afterEach(function() { + document.body.removeChild(document.getElementById('gs-cont')); + }); + it('should move all 3 items to top-left with no space', function() { + $('.grid-stack').gridstack({float: true}); + var grid = $('.grid-stack').data('gridstack'); + + var el3 = grid.addWidget(widgetHTML, {x: 3, y: 5}); + expect(parseInt(el3.attr('data-gs-x'))).toBe(3); + expect(parseInt(el3.attr('data-gs-y'))).toBe(5); + + grid.compact(); + expect(parseInt(el3.attr('data-gs-x'))).toBe(8); + expect(parseInt(el3.attr('data-gs-y'))).toBe(0); + }); + it('not move locked item', function() { + $('.grid-stack').gridstack({float: true}); + var grid = $('.grid-stack').data('gridstack'); + + var el3 = grid.addWidget(widgetHTML, {x: 3, y: 5, locked: true, noMove: true}); + expect(parseInt(el3.attr('data-gs-x'))).toBe(3); + expect(parseInt(el3.attr('data-gs-y'))).toBe(5); + + grid.compact(); + expect(parseInt(el3.attr('data-gs-x'))).toBe(3); + expect(parseInt(el3.attr('data-gs-y'))).toBe(5); + }); + + }); }); diff --git a/src/gridstack.d.ts b/src/gridstack.d.ts index 526ba3df3..2394b40d2 100644 --- a/src/gridstack.d.ts +++ b/src/gridstack.d.ts @@ -96,6 +96,11 @@ interface GridStack { */ commit(): void; + /** + * relayout grid items to reclaim any empty space + */ + compact(): void; + /** * Destroys a grid instance. * @param detachGrid if false nodes and grid will not be removed from the DOM (Optional. Default true). diff --git a/src/gridstack.js b/src/gridstack.js index f771032c3..a88b67fc3 100644 --- a/src/gridstack.js +++ b/src/gridstack.js @@ -469,6 +469,8 @@ }; GridStackEngine.prototype.addNode = function(node, triggerAddEvent) { + var prev = {x: node.x, y: node.y, width: node.width, height: node.height}; + node = this._prepareNode(node); if (node.maxWidth !== undefined) { node.width = Math.min(node.width, node.maxWidth); } @@ -476,7 +478,7 @@ if (node.minWidth !== undefined) { node.width = Math.max(node.width, node.minWidth); } if (node.minHeight !== undefined) { node.height = Math.max(node.height, node.minHeight); } - node._id = ++idSeq; + node._id = node._id || ++idSeq; // node._dirty = true; will be addEvent instead, unless it changes below... if (node.autoPosition) { @@ -489,10 +491,10 @@ 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 - node._dirty = (node.x !== x || node.y !== y); break; } } @@ -502,6 +504,10 @@ if (triggerAddEvent) { this._addedNodes.push(node); } + // use single equal as they come as string/undefined but end as number.... + if (!node._dirty && (prev.x != node.x || prev.y != node.y || prev.width != node.width || prev.height != node.height)) { + node._dirty = true; + } this._fixCollisions(node); this._packNodes(); @@ -1697,6 +1703,24 @@ }); }; + /** + * relayout grid items to reclaim any empty space + */ + GridStack.prototype.compact = function() { + if (this.grid.nodes.length === 0) { return; } + this.batchUpdate(); + this.grid._sortNodes(); + var nodes = this.grid.nodes; + this.grid.nodes = []; // pretend we have no nodes to conflict layout to start with... + nodes.forEach(function(n) { + if (!n.noMove && !n.locked) { + n.autoPosition = true; + } + this.grid.addNode(n, false); // 'false' for add event trigger + }, this); + this.commit(); + }; + GridStack.prototype.verticalMargin = function(val, noUpdate) { if (val === undefined) { return this.opts.verticalMargin; @@ -1809,44 +1833,67 @@ // // 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) // 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] = Utils.clone(n)}); // clone to preserve _id that gets reset during removal, and changing x,y,w,h live objects - this.grid._layouts = this.grid._layouts || {}; + 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 prev values and if so re-use those nodes that are still current... - var newNodes = []; + // 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] || []; + if (cacheNodes.length) { + // pretend we came from that larger column by assigning those values at starting point) + oldColumn = lastIndex; + cacheNodes.forEach(function(cacheNode) { + var j = nodes.findIndex(function(n) {return n && n._id === cacheNode._id}); + if (j !== -1) { + // still current, use cache info positions + nodes[j].x = cacheNode.x; + nodes[j].y = cacheNode.y; + nodes[j].width = cacheNode.width; + } + }); + cacheNodes = []; // we still don't have new column cached data... will generate from larger one. + } + } + + // if we found cache re-use those nodes that are still current + var newNodes = []; cacheNodes.forEach(function(cacheNode) { var j = nodes.findIndex(function(n) {return n && n._id === cacheNode._id}); if (j !== -1) { - newNodes.push(cacheNode); // still current, use cache info - nodes[j] = null; + // still current, use cache info positions + nodes[j].x = cacheNode.x; + nodes[j].y = cacheNode.y; + nodes[j].width = cacheNode.width; + newNodes.push(nodes[j]); + nodes[j] = null; // erase it so we know what's left } }); // ...and add any extra non-cached ones var ratio = column / oldColumn; nodes.forEach(function(node) { if (!node) return; - newNodes.push($.extend({}, node, {x: Math.round(node.x * ratio), width: Math.round(node.width * ratio) || 1})); + node.x = Math.round(node.x * ratio); + node.width = Math.round(node.width * ratio) || 1; + newNodes.push(node); }); newNodes = Utils.sort(newNodes, -1, column); - // now temporary remove the existing gs info and add them from last to make sure we insert them where needed - // (batch mode will set float=true so we can position anywhere and do gravity relayout after) + // finally relayout them in reverse order (to get correct placement) this.batchUpdate(); - this.grid.removeAll(false); // 'false' = leave DOm elements behind + this.grid.nodes = []; // pretend we have no nodes to start with (we use same structures) to simplify layout newNodes.forEach(function(node) { - var newNode = this.addWidget(node.el, node).data('_gridstack_node'); - newNode._id = node._id; // keep same ID so we can re-use caches - newNode._dirty = true; + this.grid.addNode(node, false); // 'false' for add event trigger + node._dirty = true; // force attr update }, this); - this.grid._removedNodes = []; // prevent add/remove from being called (kept DOM) only change event - this.grid._addedNodes = []; this.commit(); };