diff --git a/README.md b/README.md index 4bd7d7a91..ae744eb16 100644 --- a/README.md +++ b/README.md @@ -63,16 +63,16 @@ Using gridstack.js with jQuery UI ```html - + ``` * Using CDN (debug): ```html - - - + + + ``` * or local: diff --git a/demo/advance.html b/demo/advance.html index aecde0146..f80232148 100644 --- a/demo/advance.html +++ b/demo/advance.html @@ -53,7 +53,7 @@

Advanced Demo

-
+
1
@@ -105,7 +105,19 @@

Advanced Demo

diff --git a/spec/gridstack-spec.js b/spec/gridstack-spec.js index 9458c4f44..841e73eaa 100644 --- a/spec/gridstack-spec.js +++ b/spec/gridstack-spec.js @@ -979,12 +979,45 @@ describe('gridstack', function() { afterEach(function() { document.body.removeChild(document.getElementById('gs-cont')); }); - it('should keep all widget options the same (autoPosition off)', function() { + it('should autoPosition (missing X,Y)', function() { $('.grid-stack').gridstack(); var grid = $('.grid-stack').data('gridstack'); - var widget = grid.addWidget(widgetHTML, {x: 8, height: 2, id: 'optionWidget'}); + var widget = grid.addWidget(widgetHTML, {height: 2, id: 'optionWidget'}); + 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(2); + 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); + expect($widget.attr('data-gs-id')).toBe('optionWidget'); + }); + it('should autoPosition (missing X)', function() { + $('.grid-stack').gridstack(); + var grid = $('.grid-stack').data('gridstack'); + var widget = grid.addWidget(widgetHTML, {y: 9, height: 2, id: 'optionWidget'}); + 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(2); + 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); + expect($widget.attr('data-gs-id')).toBe('optionWidget'); + }); + it('should autoPosition (missing Y)', function() { + $('.grid-stack').gridstack(); + var grid = $('.grid-stack').data('gridstack'); + var widget = grid.addWidget(widgetHTML, {x: 9, height: 2, id: 'optionWidget'}); 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(2); expect($widget.attr('data-gs-auto-position')).toBe(undefined); @@ -994,6 +1027,23 @@ 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() { + $('.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-height'), 10)).toBe(2); + 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); + expect($widget.attr('data-gs-id')).toBe('optionWidget'); + }); + }); describe('addWidget() with bad string value widget options', function() { diff --git a/src/gridstack.d.ts b/src/gridstack.d.ts index 6d0e6276e..9dd82b9ca 100644 --- a/src/gridstack.d.ts +++ b/src/gridstack.d.ts @@ -191,7 +191,7 @@ interface GridStack { * @example * $('.grid-stack').gridstack(); * $('.grid-stack').append('
') + * data-gs-auto-position="true">
') * var grid = $('.grid-stack').data('gridstack'); * grid.makeWidget('gsi-1'); */ diff --git a/src/gridstack.js b/src/gridstack.js index ce2ec416d..4f86fc62a 100644 --- a/src/gridstack.js +++ b/src/gridstack.js @@ -309,7 +309,7 @@ if (this._batchMode) return; this._batchMode = true; this._prevFloat = this.float; - this.float = true; + this.float = true; // let things go anywhere for now... commit() will restore and possibly reposition }; GridStackEngine.prototype.commit = function() { @@ -379,9 +379,7 @@ }, this); } else { this.nodes.forEach(function(n, i) { - if (n.locked) { - return; - } + if (n.locked) { return; } while (n.y > 0) { var newY = n.y - 1; var canBeMoved = i === 0; @@ -393,10 +391,11 @@ canBeMoved = collisionNode === undefined; } - if (!canBeMoved) { - break; - } - n._dirty = n.y !== newY; + if (!canBeMoved) { break; } + // Note: must be dirty (from last position) for GridStack::OnChange CB to update positions + // and move items back. The user 'change' CB should detect changes from the original + // starting position instead. + n._dirty = (n.y !== newY); n.y = newY; } }, this); @@ -459,21 +458,17 @@ }; GridStackEngine.prototype._notify = function() { + if (this._batchMode) { return; } var args = Array.prototype.slice.call(arguments, 0); args[0] = args[0] === undefined ? [] : [args[0]]; args[1] = args[1] === undefined ? true : args[1]; - if (this._batchMode) { - return; - } - var deletedNodes = args[0].concat(this.getDirtyNodes()); - this.onchange(deletedNodes, args[1]); + var dirtyNodes = args[0].concat(this.getDirtyNodes()); + this.onchange(dirtyNodes, args[1]); }; GridStackEngine.prototype.cleanNodes = function() { - if (this._batchMode) { - return; - } - this.nodes.forEach(function(n) { n._dirty = false; }); + if (this._batchMode) { return; } + this.nodes.forEach(function(n) { delete n._dirty; }); }; GridStackEngine.prototype.getDirtyNodes = function() { @@ -489,7 +484,7 @@ if (node.minHeight !== undefined) { node.height = Math.max(node.height, node.minHeight); } node._id = ++idSeq; - node._dirty = true; + // node._dirty = true; will be addEvent instead if (node.autoPosition) { this._sortNodes(); @@ -501,6 +496,7 @@ 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; break; @@ -510,7 +506,7 @@ this.nodes.push(node); if (triggerAddEvent) { - this._addedNodes.push(Utils.clone(node)); + this._addedNodes.push(node); } this._fixCollisions(node); @@ -645,19 +641,16 @@ }; GridStackEngine.prototype.beginUpdate = function(node) { - this.nodes.forEach(function(n) { - n._origY = n.y; - }); + if (node._updating) return; node._updating = true; + this.nodes.forEach(function(n) { n._origY = n.y; }); }; GridStackEngine.prototype.endUpdate = function() { - this.nodes.forEach(function(n) { - n._origY = n.y; - }); var n = this.nodes.find(function(n) { return n._updating; }); if (n) { n._updating = false; + this.nodes.forEach(function(n) { delete n._origY; }); } }; @@ -884,7 +877,7 @@ .on(trashZone, 'dropover', function(event, ui) { var el = $(ui.draggable); var node = el.data('_gridstack_node'); - if (node._grid !== self) { + if (!node || node._grid !== self) { return; } el.data('inTrashZone', true); @@ -893,7 +886,7 @@ .on(trashZone, 'dropout', function(event, ui) { var el = $(ui.draggable); var node = el.data('_gridstack_node'); - if (node._grid !== self) { + if (!node || node._grid !== self) { return; } el.data('inTrashZone', false); @@ -1035,31 +1028,28 @@ } }; - GridStack.prototype._triggerChangeEvent = function(forceTrigger) { + GridStack.prototype._triggerChangeEvent = function(/*forceTrigger*/) { + if (this.grid._batchMode) { return; } + // TODO: compare original X,Y,W,H (or entire node?) instead as _dirty can be a temporary state var elements = this.grid.getDirtyNodes(); - var hasChanges = false; - - var eventParams = []; if (elements && elements.length) { - eventParams.push(elements); - hasChanges = true; - } - - if (hasChanges || forceTrigger === true) { - this.container.trigger('change', eventParams); + this.container.trigger('change', [elements]); + this.grid.cleanNodes(); // clear dirty flags now that we called } }; GridStack.prototype._triggerAddEvent = function() { + if (this.grid._batchMode) { return; } if (this.grid._addedNodes && this.grid._addedNodes.length > 0) { - this.container.trigger('added', [this.grid._addedNodes.map(Utils.clone)]); + this.container.trigger('added', [this.grid._addedNodes]); this.grid._addedNodes = []; } }; GridStack.prototype._triggerRemoveEvent = function() { + if (this.grid._batchMode) { return; } if (this.grid._removedNodes && this.grid._removedNodes.length > 0) { - this.container.trigger('removed', [this.grid._removedNodes.map(Utils.clone)]); + this.container.trigger('removed', [this.grid._removedNodes]); this.grid._removedNodes = []; } }; @@ -1145,9 +1135,7 @@ }; GridStack.prototype._updateContainerHeight = function() { - if (this.grid._batchMode) { - return; - } + if (this.grid._batchMode) { return; } var height = this.grid.getGridHeight(); // check for css min height. Each row is cellHeight + verticalMargin, until last one which has no margin below var cssMinHeight = parseInt(this.container.css('min-height')); @@ -1323,13 +1311,13 @@ return; } - var forceNotify = false; + // var forceNotify = false; what is the point of calling 'change' event with no data, when the 'removed' event is already called ? self.placeholder.detach(); node.el = o; self.placeholder.hide(); if (node._isAboutToRemove) { - forceNotify = true; + // forceNotify = true; var gridToNotify = el.data('_gridstack_node')._grid; gridToNotify._triggerRemoveEvent(); el.removeData('_gridstack_node'); @@ -1357,7 +1345,7 @@ } } self._updateContainerHeight(); - self._triggerChangeEvent(forceNotify); + self._triggerChangeEvent(/*forceNotify*/); self.grid.endUpdate(); @@ -1436,30 +1424,37 @@ } }; - GridStack.prototype.addWidget = function(el, x, y, width, height, autoPosition, minWidth, maxWidth, minHeight, maxHeight, id) { + GridStack.prototype.addWidget = function(el, node, y, width, height, autoPosition, minWidth, maxWidth, minHeight, maxHeight, id) { - // instead of passing all the params, the user might pass an object with all fields instead, if so extract them and call us back - if (x !== null && typeof x === 'object') { - return this.addWidget(el, x.x, x.y, x.width, x.height, x.autoPosition, x.minWidth, x.maxWidth, x.minHeight, x.maxHeight, x.id); + // new way of calling with an object - make sure all items have been properly initialized + if (node === undefined || typeof node === 'object') { + // Tempting to initialize the passed in node with default and valid values, but this break knockout demos + // as the actual value are filled in when _prepareElement() calls el.attr('data-gs-xyz) before adding the node. + // node = this.grid._prepareNode(node); + node = node || {}; + } else { + // old legacy way of calling with items spelled out - call us back with single object instead (so we can properly initialized values) + return this.addWidget(el, {x: node, y: y, width: width, height: height, autoPosition: autoPosition, + minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight, id: id}); } el = $(el); // Note: passing null removes the attr in jquery - if (x !== undefined) { el.attr('data-gs-x', x); } - if (y !== undefined) { el.attr('data-gs-y', y); } - if (width !== undefined) { el.attr('data-gs-width', width); } - if (height !== undefined) { el.attr('data-gs-height', height); } - if (autoPosition !== undefined) { el.attr('data-gs-auto-position', autoPosition ? 'yes' : null); } - if (minWidth !== undefined) { el.attr('data-gs-min-width', minWidth); } - if (maxWidth !== undefined) { el.attr('data-gs-max-width', maxWidth); } - if (minHeight !== undefined) { el.attr('data-gs-min-height', minHeight); } - if (maxHeight !== undefined) { el.attr('data-gs-max-height', maxHeight); } - if (id !== undefined) { el.attr('data-gs-id', id); } + 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.container.append(el); this._prepareElement(el, true); - this._triggerAddEvent(); this._updateContainerHeight(); - this._triggerChangeEvent(true); + this._triggerAddEvent(); + // this._triggerChangeEvent(true); already have AddEvent return el; }; @@ -1467,9 +1462,9 @@ GridStack.prototype.makeWidget = function(el) { el = $(el); this._prepareElement(el, true); - this._triggerAddEvent(); this._updateContainerHeight(); - this._triggerChangeEvent(true); + this._triggerAddEvent(); + // this._triggerChangeEvent(true); already have AddEvent return el; }; @@ -1495,8 +1490,8 @@ if (detachNode) { el.remove(); } - this._triggerChangeEvent(true); this._triggerRemoveEvent(); + // this._triggerChangeEvent(true); already have removeEvent }; GridStack.prototype.removeAll = function(detachNode) { @@ -1767,6 +1762,9 @@ GridStack.prototype.commit = function() { this.grid.commit(); this._updateContainerHeight(); + this._triggerRemoveEvent(); + this._triggerAddEvent(); + this._triggerChangeEvent(); }; GridStack.prototype.isAreaEmpty = function(x, y, width, height) { @@ -1792,14 +1790,14 @@ GridStack.prototype._updateNodeWidths = function(oldWidth, newWidth) { this.grid._sortNodes(); - this.grid.batchUpdate(); + 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.grid.commit(); + this.commit(); }; GridStack.prototype.setColumn = function(column, doNotPropagate) {