From 1c5bc14a691f84e1117dfe8a70d13b452defd330 Mon Sep 17 00:00:00 2001 From: Alain Dumesny Date: Tue, 17 Dec 2019 16:50:47 -0800 Subject: [PATCH 1/2] new setColumn() heuristics * complete re-write of the `setColumn()` code that moves/sizes widgets. we now do a reasonable job positioning widgets (especially 1 column), and we remember positions for each column layout to going from 12 -> 1 -> 12 will bring it exactly back for example! * new column.html test case (now that it works) to be published. * fixed bunch of bugs I found with overlapping widgets (going to 2 or 1 column) * fixed bug where default (empty) option when adding a widget would render it at (0,0) overlapping any existing widgets. added `_readAttr()/_writeAttr()` to help there. * unit test greatly updated with exact location expected of widgets (not that I agree with location) better fix than #809 fixing responsive.html will come next... --- demo/column.html | 88 ++++++++++++++++++++ demo/float.html | 13 ++- demo/index.html | 7 +- demo/responsive.html | 6 +- doc/CHANGES.md | 5 +- doc/README.md | 7 +- spec/e2e/html/column.html | 53 ------------ spec/gridstack-spec.js | 91 ++++++++++++++++++--- src/gridstack.d.ts | 5 +- src/gridstack.js | 164 +++++++++++++++++++++++++------------- 10 files changed, 301 insertions(+), 138 deletions(-) create mode 100644 demo/column.html delete mode 100644 spec/e2e/html/column.html diff --git a/demo/column.html b/demo/column.html new file mode 100644 index 000000000..f0f543d07 --- /dev/null +++ b/demo/column.html @@ -0,0 +1,88 @@ + + + + + + + Column grid demo + + + + + + + + + + +
+

setColumn() grid demo

+ Add Widget + 1 Column + 2 Column + 3 Column + 4 Column + 6 Column + 8 Column + 10 Column + 12 Column +

+
+
+ + + + diff --git a/demo/float.html b/demo/float.html index af90c5843..56a495014 100644 --- a/demo/float.html +++ b/demo/float.html @@ -28,10 +28,7 @@

Float grid demo

- - - - - -
-

setColumn() grid demo

- 1 Column - 2 Column - 6 Column - 12 Column -

-
-
- - - - - diff --git a/spec/gridstack-spec.js b/spec/gridstack-spec.js index 8ab5793e0..72190ae5e 100644 --- a/spec/gridstack-spec.js +++ b/spec/gridstack-spec.js @@ -295,7 +295,7 @@ describe('gridstack', function() { grid.setGridWidth(12); // old API expect(grid.opts.column).toBe(12); }); - it('should change column number, no relayout', function() { + it('should SMALL change column number, no relayout', function() { var options = { column: 12 }; @@ -314,21 +314,77 @@ describe('gridstack', function() { for (var j = 0; j < items.length; j++) { expect(parseInt($(items[j]).attr('data-gs-y'), 10)).toBe(0); } + + grid.setColumn(12); + expect(grid.opts.column).toBe(12); + for (var j = 0; j < items.length; j++) { + expect(parseInt($(items[j]).attr('data-gs-y'), 10)).toBe(0); + } }); it('should change column number and relayout items', function() { var options = { - column: 12 + column: 12, + float: true }; $('.grid-stack').gridstack(options); var grid = $('.grid-stack').data('gridstack'); - var items = $('.grid-stack-item'); + var node1 = $('#item1').data('_gridstack_node'); + var node2 = $('#item2').data('_gridstack_node'); + // 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); + // one column will have item1, item2 grid.setColumn(1); + node1 = $('#item1').data('_gridstack_node'); + node2 = $('#item2').data('_gridstack_node'); expect(grid.opts.column).toBe(1); - for (var j = 0; j < items.length; j++) { - expect(parseInt($(items[j]).attr('data-gs-x'), 10)).toBe(0); - // TODO: check Y position but I don't currently agree with order. [Alain] - } + expect(node1.x).toBe(0); + expect(node1.y).toBe(0); + expect(node1.width).toBe(1); + expect(node1.height).toBe(2); + + expect(node2.x).toBe(0); + expect(node2.y).toBe(2); + expect(node2.width).toBe(1); + expect(node2.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); + + // back to 12 column and initial layout (other than new item3) + 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(12); // take entire row still + expect(node3.height).toBe(1); }); }); @@ -1089,14 +1145,14 @@ describe('gridstack', function() { expect($widget.attr('data-gs-max-height')).toBe(undefined); expect($widget.attr('data-gs-id')).toBe('optionWidget'); }); - it('should not autoPosition (correct X, missing Y)', function() { + it('should autoPosition (correct X, missing Y)', function() { $('.grid-stack').gridstack(); var grid = $('.grid-stack').data('gridstack'); var widget = grid.addWidget(widgetHTML, {x: 8, height: 2, id: 'optionWidget'}); var $widget = $(widget); expect(parseInt($widget.attr('data-gs-x'), 10)).toBe(8); - expect($widget.attr('data-gs-y')).toBe(undefined); - expect($widget.attr('data-gs-width')).toBe(undefined); + expect(parseInt($widget.attr('data-gs-y'), 10)).toBe(0); + expect(parseInt($widget.attr('data-gs-width'), 10)).toBe(1); expect(parseInt($widget.attr('data-gs-height'), 10)).toBe(2); expect($widget.attr('data-gs-auto-position')).toBe(undefined); expect($widget.attr('data-gs-min-width')).toBe(undefined); @@ -1105,6 +1161,21 @@ describe('gridstack', function() { expect($widget.attr('data-gs-max-height')).toBe(undefined); expect($widget.attr('data-gs-id')).toBe('optionWidget'); }); + it('should autoPosition (empty options)', function() { + $('.grid-stack').gridstack(); + var grid = $('.grid-stack').data('gridstack'); + var widget = grid.addWidget(widgetHTML, {}); + var $widget = $(widget); + expect(parseInt($widget.attr('data-gs-x'), 10)).toBe(8); + expect(parseInt($widget.attr('data-gs-y'), 10)).toBe(0); + expect(parseInt($widget.attr('data-gs-width'), 10)).toBe(1); + expect(parseInt($widget.attr('data-gs-height'), 10)).toBe(1); + expect($widget.attr('data-gs-auto-position')).toBe(undefined); + expect($widget.attr('data-gs-min-width')).toBe(undefined); + expect($widget.attr('data-gs-max-width')).toBe(undefined); + expect($widget.attr('data-gs-min-height')).toBe(undefined); + expect($widget.attr('data-gs-max-height')).toBe(undefined); + }); }); diff --git a/src/gridstack.d.ts b/src/gridstack.d.ts index 9dd82b9ca..526ba3df3 100644 --- a/src/gridstack.d.ts +++ b/src/gridstack.d.ts @@ -275,8 +275,9 @@ interface GridStack { setAnimation(doAnimate: boolean): void; /** - * (Experimental) 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], + * Modify 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 [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) diff --git a/src/gridstack.js b/src/gridstack.js index 399cb7d23..477f7b6be 100644 --- a/src/gridstack.js +++ b/src/gridstack.js @@ -484,7 +484,7 @@ if (node.minHeight !== undefined) { node.height = Math.max(node.height, node.minHeight); } node._id = ++idSeq; - // node._dirty = true; will be addEvent instead + // node._dirty = true; will be addEvent instead, unless it changes below... if (node.autoPosition) { this._sortNodes(); @@ -496,9 +496,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; } } @@ -518,7 +519,7 @@ GridStackEngine.prototype.removeNode = function(node, detachNode) { detachNode = (detachNode === undefined ? true : detachNode); this._removedNodes.push(node); - node._id = null; + node._id = null; // hint that node is being removed this.nodes = Utils.without(this.nodes, node); this._packNodes(); this._notify(node, detachNode); @@ -1393,7 +1394,7 @@ this.dd.resizable(el, 'disable'); } - el.attr('data-gs-locked', node.locked ? 'yes' : null); + this._writeAttr(el, node); }; GridStack.prototype._prepareElement = function(el, triggerAddEvent) { @@ -1402,29 +1403,55 @@ el = $(el); el.addClass(this.opts.itemClass); - var node = self.grid.addNode({ - x: el.attr('data-gs-x'), - y: el.attr('data-gs-y'), - width: el.attr('data-gs-width'), - height: el.attr('data-gs-height'), - maxWidth: el.attr('data-gs-max-width'), - minWidth: el.attr('data-gs-min-width'), - maxHeight: el.attr('data-gs-max-height'), - minHeight: el.attr('data-gs-min-height'), - autoPosition: Utils.toBool(el.attr('data-gs-auto-position')), - noResize: Utils.toBool(el.attr('data-gs-no-resize')), - noMove: Utils.toBool(el.attr('data-gs-no-move')), - locked: Utils.toBool(el.attr('data-gs-locked')), - resizeHandles: el.attr('data-gs-resize-handles'), - el: el, - id: el.attr('data-gs-id'), - _grid: self - }, triggerAddEvent); + var node = this._readAttr(el, {el: el, _grid: self}); + node = self.grid.addNode(node, triggerAddEvent); el.data('_gridstack_node', node); this._prepareElementsByNode(el, node); }; + /** call to write any default attributes back to element */ + GridStack.prototype._writeAttr = function(el, node) { + el = $(el); + node = node || {}; + // Note: passing null removes the attr in jquery + if (node.x !== undefined) { el.attr('data-gs-x', node.x); } + if (node.y !== undefined) { el.attr('data-gs-y', node.y); } + if (node.width !== undefined) { el.attr('data-gs-width', node.width); } + if (node.height !== undefined) { el.attr('data-gs-height', node.height); } + if (node.autoPosition !== undefined) { el.attr('data-gs-auto-position', node.autoPosition ? true : null); } + if (node.minWidth !== undefined) { el.attr('data-gs-min-width', node.minWidth); } + if (node.maxWidth !== undefined) { el.attr('data-gs-max-width', node.maxWidth); } + if (node.minHeight !== undefined) { el.attr('data-gs-min-height', node.minHeight); } + if (node.maxHeight !== undefined) { el.attr('data-gs-max-height', node.maxHeight); } + if (node.noResize !== undefined) { el.attr('data-gs-no-resize', node.noResize ? true : null); } + if (node.noMove !== undefined) { el.attr('data-gs-no-move', node.noMove ? true : null); } + if (node.locked !== undefined) { el.attr('data-gs-locked', node.locked ? true : null); } + if (node.resizeHandles !== undefined) { el.attr('data-gs-resize-handles', node.resizeHandles); } + if (node.id !== undefined) { el.attr('data-gs-id', node.id); } + }; + + /** call to write any default attributes back to element */ + GridStack.prototype._readAttr = function(el, node) { + el = $(el); + node = node || {}; + node.x = el.attr('data-gs-x'); + node.y = el.attr('data-gs-y'); + node.width = el.attr('data-gs-width'); + node.height = el.attr('data-gs-height'); + node.autoPosition = Utils.toBool(el.attr('data-gs-auto-position')); + node.maxWidth = el.attr('data-gs-max-width'); + node.minWidth = el.attr('data-gs-min-width'); + node.maxHeight = el.attr('data-gs-max-height'); + node.minHeight = el.attr('data-gs-min-height'); + node.noResize = Utils.toBool(el.attr('data-gs-no-resize')); + node.noMove = Utils.toBool(el.attr('data-gs-no-move')); + node.locked = Utils.toBool(el.attr('data-gs-locked')); + node.resizeHandles = el.attr('data-gs-resize-handles'); + node.id = el.attr('data-gs-id'); + return node; + } + GridStack.prototype.setAnimation = function(enable) { if (enable) { this.container.addClass('grid-stack-animate'); @@ -1448,17 +1475,7 @@ } el = $(el); - // Note: passing null removes the attr in jquery - if (node.x !== undefined) { el.attr('data-gs-x', node.x); } - if (node.y !== undefined) { el.attr('data-gs-y', node.y); } - if (node.width !== undefined) { el.attr('data-gs-width', node.width); } - if (node.height !== undefined) { el.attr('data-gs-height', node.height); } - if (node.autoPosition !== undefined) { el.attr('data-gs-auto-position', node.autoPosition ? true : null); } - if (node.minWidth !== undefined) { el.attr('data-gs-min-width', node.minWidth); } - if (node.maxWidth !== undefined) { el.attr('data-gs-max-width', node.maxWidth); } - if (node.minHeight !== undefined) { el.attr('data-gs-min-height', node.minHeight); } - if (node.maxHeight !== undefined) { el.attr('data-gs-max-height', node.maxHeight); } - if (node.id !== undefined) { el.attr('data-gs-id', node.id); } + this._writeAttr(el, node); this.container.append(el); this._prepareElement(el, true); this._updateContainerHeight(); @@ -1499,10 +1516,11 @@ }; GridStack.prototype.removeAll = function(detachNode) { - // remove our data structure before list gets emptied (in case detachNode is false) - this.grid.nodes.forEach(function(node) { - node.el.removeData('_gridstack_node'); - }); + if (detachNode !== false) { + delete this.grid._layouts; + // remove our data structure before list gets emptied and DOM elements stay behind + this.grid.nodes.forEach(node => { node.el.removeData('_gridstack_node'); }); + } this.grid.removeAll(detachNode); this._triggerRemoveEvent(); }; @@ -1792,27 +1810,65 @@ } }; - GridStack.prototype._updateNodeWidths = function(oldWidth, newWidth) { - this.grid._sortNodes(); - this.batchUpdate(); - var node = {}; - for (var i = 0; i < this.grid.nodes.length; i++) { - node = this.grid.nodes[i]; - this.update(node.el, Math.round(node.x * newWidth / oldWidth), undefined, - Math.round(node.width * newWidth / oldWidth), undefined); - } - this.commit(); - }; - + /** + * 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; } - this.container.removeClass('grid-stack-' + this.opts.column); - if (doNotPropagate !== true) { - this._updateNodeWidths(this.opts.column, column); - } - this.opts.column = this.grid.column = column; + 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) + + // 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((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 || {}; + 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 = []; + var cacheNodes = this.grid._layouts[column] || []; + cacheNodes.forEach(cacheNode => { + var j = nodes.findIndex(n => n && n._id === cacheNode._id); + if (j !== -1) { + newNodes.push(cacheNode); // still current, use cache info + nodes[j] = null; + } + }); + // ...and add any extra non-cached ones + var ratio = column / oldColumn; + nodes.forEach(node => { + if (!node) return; + newNodes.push($.extend({}, node, {x: Math.round(node.x * ratio), width: Math.round(node.width * ratio) || 1})); + }); + 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) + this.batchUpdate(); + this.grid.removeAll(false); // 'false' = leave DOm elements behind + newNodes.forEach(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._removedNodes = []; // prevent add/remove from being called (kept DOM) only change event + this.grid._addedNodes = []; + this.commit(); }; + // legacy call from <= 0.5.2 - use new method instead. GridStack.prototype.setGridWidth = function(column, doNotPropagate) { console.warn('gridstack.js: setGridWidth() is deprecated as of v0.5.3 and has been replaced ' + From 1f9560d5bdab57a5f8055e6d87dbb46e6d7b88ce Mon Sep 17 00:00:00 2001 From: Alain Dumesny Date: Wed, 18 Dec 2019 18:36:41 -0800 Subject: [PATCH 2/2] grunt uglify not supporting ES6 --- src/gridstack.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/gridstack.js b/src/gridstack.js index 477f7b6be..959094199 100644 --- a/src/gridstack.js +++ b/src/gridstack.js @@ -1450,7 +1450,7 @@ node.resizeHandles = el.attr('data-gs-resize-handles'); node.id = el.attr('data-gs-id'); return node; - } + }; GridStack.prototype.setAnimation = function(enable) { if (enable) { @@ -1519,7 +1519,7 @@ if (detachNode !== false) { delete this.grid._layouts; // remove our data structure before list gets emptied and DOM elements stay behind - this.grid.nodes.forEach(node => { node.el.removeData('_gridstack_node'); }); + this.grid.nodes.forEach(function(node) { node.el.removeData('_gridstack_node') }); } this.grid.removeAll(detachNode); this._triggerRemoveEvent(); @@ -1833,15 +1833,15 @@ // 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((n, i) => copy[i] = Utils.clone(n)); // clone to preserve _id that gets reset during removal, and changing x,y,w,h live objects + 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 || {}; 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 = []; var cacheNodes = this.grid._layouts[column] || []; - cacheNodes.forEach(cacheNode => { - var j = nodes.findIndex(n => n && n._id === cacheNode._id); + 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; @@ -1849,7 +1849,7 @@ }); // ...and add any extra non-cached ones var ratio = column / oldColumn; - nodes.forEach(node => { + nodes.forEach(function(node) { if (!node) return; newNodes.push($.extend({}, node, {x: Math.round(node.x * ratio), width: Math.round(node.width * ratio) || 1})); }); @@ -1859,11 +1859,11 @@ // (batch mode will set float=true so we can position anywhere and do gravity relayout after) this.batchUpdate(); this.grid.removeAll(false); // 'false' = leave DOm elements behind - newNodes.forEach(node => { + 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); this.grid._removedNodes = []; // prevent add/remove from being called (kept DOM) only change event this.grid._addedNodes = []; this.commit();