Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions demo/column.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,30 @@ <h1>setColumn() grid demo</h1>
}

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($('<div><div class="grid-stack-item-content">' + count + '</div></div>'), items.pop());
var n = items[count];
grid.addWidget($('<div><div class="grid-stack-item-content">' + count + (n.text ? n.text : '') + '</div></div>'), 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($('<div><div class="grid-stack-item-content">' + count++ + '</div></div>'), node);
grid.addWidget($('<div><div class="grid-stack-item-content">' + count + (n.text ? n.text : '') + '</div></div>'), n);
});

$('#1column').click(function() { grid.setColumn(1); });
Expand Down
14 changes: 12 additions & 2 deletions demo/two.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ <h1>Two grids demo</h1>
<div class="row">
<div class="col-md-6">
<a class="btn btn-primary" id="float1" href="#">float: false</a>
<a class="btn btn-primary" id="compact1" href="#">Compact</a>
<div class="grid-stack grid-stack-6" id="grid1">
</div>
</div>
<div class="col-md-6">
<a class="btn btn-primary" id="float2" href="#">float: true</a>
<a class="btn btn-primary" id="compact2" href="#">Compact</a>
<div class="grid-stack grid-stack-6" id="grid2">
</div>
</div>
Expand Down Expand Up @@ -136,13 +138,21 @@ <h1>Two grids demo</h1>
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();
}

});
</script>
</body>
Expand Down
1 change: 1 addition & 0 deletions doc/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
5 changes: 5 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
61 changes: 58 additions & 3 deletions spec/gridstack-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ describe('gridstack', function() {
});
});

describe('grid.column', function() {
describe('grid.setColumn', function() {
beforeEach(function() {
document.body.insertAdjacentHTML('afterbegin', gridstackHTML);
});
Expand Down Expand Up @@ -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');
Expand All @@ -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);
Expand All @@ -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);
});
});
Expand Down Expand Up @@ -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);
});

});
});
5 changes: 5 additions & 0 deletions src/gridstack.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
81 changes: 64 additions & 17 deletions src/gridstack.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,14 +469,16 @@
};

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); }
if (node.maxHeight !== undefined) { node.height = Math.min(node.height, node.maxHeight); }
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) {
Expand All @@ -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;
}
}
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
};

Expand Down