Skip to content

Commit 2d4d2c8

Browse files
committed
fix #1785 switching column overlap
* guard against 0 column * making changes in a layout, propagates the differences to larger layout (deleting smaller ones) causing potential negative (x,y) cached values. We always used nodeBoundFix() to prevent that when adding the nodes back, but due to saving _orig value after (instead of early) and combination of _dirty, we didn't always updating collision in the DOM * also changed updateNodeWidths() to always scale things down live (regardless of cached layout to make it smoother) but use the largest 12 column layout to scale things up if not already cached. * more efficient in updating _dirty only nodes attributes from last values. * added test file showing issue.
1 parent ace8dca commit 2d4d2c8

File tree

7 files changed

+151
-55
lines changed

7 files changed

+151
-55
lines changed

demo/column.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ <h1>column() grid demo (fix cellHeight)</h1>
7272
{x: 0, y: 4, w: 12}
7373
];
7474
let count = 0;
75-
items.forEach(n => n.content = '<button onClick="grid.removeWidget(this.parentNode.parentNode)">X</button><br>' + count++ + (n.text ? n.text : ''));
76-
grid.load(items);
75+
items.forEach(n => {
76+
n.content = '<button onClick="grid.removeWidget(this.parentNode.parentNode)">X</button><br>' + count++ + (n.text ? n.text : '');
77+
grid.addWidget(n); // DOM order will be 0,1,2,3,4,5,6 vs column1 = 0,1,4,3,2,5,6
78+
});
7779

7880
function addWidget() {
7981
let n = items[count] || {
@@ -91,6 +93,9 @@ <h1>column() grid demo (fix cellHeight)</h1>
9193
text.innerHTML = n;
9294
}
9395
function setOneColumn(dom) {
96+
if (grid.opts.column === 1 && grid.opts.oneColumnModeDomSort !== dom) {
97+
column(12); // go ack to 12 before going to new column1 version
98+
}
9499
grid.opts.oneColumnModeDomSort = dom;
95100
grid.column(1, layout);
96101
text.innerHTML = dom ? '1 DOM' : '1';
@@ -106,7 +111,7 @@ <h1>column() grid demo (fix cellHeight)</h1>
106111
function setLayout(name) {
107112
layout = name === 'custom' ? this.columnLayout : name;
108113
}
109-
114+
// setOneColumn(true); // test dom order
110115
</script>
111116
</body>
112117
</html>

doc/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Change log
55
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
66
**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*
77

8+
- [4.3.1-dev TBD](#431-dev-tbd)
89
- [4.3.1 (2021-10-18)](#431-2021-10-18)
910
- [4.3.0 (2021-10-15)](#430-2021-10-15)
1011
- [4.2.7 (2021-9-12)](#427-2021-9-12)
@@ -64,6 +65,7 @@ Change log
6465
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
6566

6667
## 4.3.1-dev TBD
68+
* fix [#1785](https://github.com/gridstack/gridstack.js/issue/1785) overlapping items when switching column() and making edits. Thank you [@radovanobal](https://github.com/radovanobal) for sponsoring it.
6769
* add [#1887](https://github.com/gridstack/gridstack.js/pull/1887) support for IE (new es5 folder) by [@SmileLifeIven](https://github.com/SmileLifeIven)
6870

6971
## 4.3.1 (2021-10-18)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<title>Changing Column a lot</title>
8+
9+
<link rel="stylesheet" href="../../../demo/demo.css"/>
10+
<link rel="stylesheet" href="../../../dist/gridstack-extra.min.css"/>
11+
<script src="../../../dist/gridstack-h5.js"></script>
12+
13+
</head>
14+
<body>
15+
<div class="container-fluid">
16+
<h1>Changing Column 5 x 12 causing overlap https://github.com/gridstack/gridstack.js/issues/1785</h1>
17+
<div>
18+
<a class="btn btn-primary" onClick="next()" href="#">Next</a>
19+
<a class="btn btn-primary" onclick="auto()" href="#">Auto</a>
20+
</div>
21+
22+
<div class="grid-stack"></div>
23+
</div>
24+
<script src="events.js"></script>
25+
<script type="text/javascript">
26+
let count = 0;
27+
let grid;
28+
let items = [
29+
// simplest case data would show the issue at 7 column (overlapping 0 & 2)
30+
// {x: 0, y: 0},
31+
// {x: 2, y: 0, w: 3},
32+
// {x: 0, y: 1, w: 2},
33+
// {x: 3, y: 1, h: 2},
34+
// {x: 6, y: 6, w: 3, h: 2}
35+
];
36+
items.forEach(item => item.content = String(count++));
37+
38+
function addNewWidget() {
39+
var node = items[count] || {
40+
x: Math.round(12 * Math.random()),
41+
y: Math.round(5 * Math.random()),
42+
w: Math.round(1 + 3 * Math.random()),
43+
h: Math.round(1 + 3 * Math.random())
44+
};
45+
node.content = String(count++);
46+
if (grid) {
47+
grid.addWidget(node);
48+
} else {
49+
items.push(node);
50+
}
51+
};
52+
53+
grid = GridStack.init({cellHeight: 50, margin: 5}).load(items);
54+
let column = 1;
55+
for (i=0; i<7; i++) addNewWidget(); // load 7 random items
56+
57+
function next() {
58+
setColumn(column++);
59+
if (column > 12) { column = 1; }
60+
}
61+
function auto() {
62+
for (let i = 1; i <= 12; i++) {
63+
setColumn(i);
64+
}
65+
}
66+
function setColumn(n) {
67+
console.log("gridding at", n);
68+
grid.column(n);
69+
grid.compact();
70+
}
71+
for (let i = 1; i <= 5; i++) {
72+
auto();
73+
}
74+
</script>
75+
</body>
76+
</html>

src/gridstack-engine.ts

Lines changed: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ export class GridStackEngine {
3434
private _float: boolean;
3535
/** @internal */
3636
private _prevFloat: boolean;
37-
/** @internal */
37+
/** @internal cached layouts of difference column count so we can restore ack (eg 12 -> 1 -> 12) */
3838
private _layouts?: Layout[][]; // maps column # to array of values nodes
39-
/** @internal */
40-
private _ignoreLayoutsNodeChange: boolean;
39+
/** @internal true while we are resizing widgets during column resize to skip certain parts */
40+
private _inColumnResize: boolean;
4141
/** @internal true if we have some items locked */
4242
private _hasLocked: boolean;
4343
/** @internal unique global internal _id counter NOT starting at 0 */
@@ -273,6 +273,7 @@ export class GridStackEngine {
273273

274274
/** @internal called to top gravity pack the items back OR revert back to original Y positions when floating */
275275
private _packNodes(): GridStackEngine {
276+
if (this.batchMode) { return; }
276277
this._sortNodes(); // first to last
277278

278279
if (this.float) {
@@ -346,6 +347,8 @@ export class GridStackEngine {
346347
/** part2 of preparing a node to fit inside our grid - checks for x,y from grid dimensions */
347348
public nodeBoundFix(node: GridStackNode, resizing?: boolean): GridStackNode {
348349

350+
let before = node._orig || Utils.copyPos({}, node);
351+
349352
if (node.maxW) { node.w = Math.min(node.w, node.maxW); }
350353
if (node.maxH) { node.h = Math.min(node.h, node.maxH); }
351354
if (node.minW && node.minW <= this.column) { node.w = Math.max(node.w, node.minW); }
@@ -354,7 +357,8 @@ export class GridStackEngine {
354357
if (node.w > this.column) {
355358
// if user loaded a larger than allowed widget for current # of columns,
356359
// remember it's full width so we can restore back (1 -> 12 column) #1655
357-
if (this.column < 12) {
360+
// IFF we're not in the middle of column resizing!
361+
if (this.column < 12 && !this._inColumnResize) {
358362
node.w = Math.min(12, node.w);
359363
this.cacheOneLayout(node, 12);
360364
}
@@ -391,6 +395,10 @@ export class GridStackEngine {
391395
}
392396
}
393397

398+
if (!Utils.samePos(node, before)) {
399+
node._dirty = true;
400+
}
401+
394402
return node;
395403
}
396404

@@ -446,10 +454,11 @@ export class GridStackEngine {
446454

447455
/** call to add the given node to our list, fixing collision and re-packing */
448456
public addNode(node: GridStackNode, triggerAddEvent = false): GridStackNode {
449-
let dup: GridStackNode;
450-
if (dup = this.nodes.find(n => n._id === node._id)) return dup; // prevent inserting twice! return it instead.
457+
let dup = this.nodes.find(n => n._id === node._id);
458+
if (dup) return dup; // prevent inserting twice! return it instead.
451459

452-
node = this.prepareNode(node);
460+
// skip prepareNode if we're in middle of column resize (not new) but do check for bounds!
461+
node = this._inColumnResize ? this.nodeBoundFix(node) : this.prepareNode(node);
453462
delete node._temporaryRemoved;
454463
delete node._removeDOM;
455464

@@ -473,11 +482,10 @@ export class GridStackEngine {
473482
}
474483

475484
this.nodes.push(node);
476-
triggerAddEvent && this.addedNodes.push(node);
485+
if (triggerAddEvent) { this.addedNodes.push(node); }
477486

478487
this._fixCollisions(node);
479-
this._packNodes()
480-
._notify();
488+
if (!this.batchMode) { this._packNodes()._notify(); }
481489
return node;
482490
}
483491

@@ -696,7 +704,7 @@ export class GridStackEngine {
696704

697705
/** @internal called whenever a node is added or moved - updates the cached layouts */
698706
public layoutsNodesChange(nodes: GridStackNode[]): GridStackEngine {
699-
if (!this._layouts || this._ignoreLayoutsNodeChange) return this;
707+
if (!this._layouts || this._inColumnResize) return this;
700708
// remove smaller layouts - we will re-generate those on the fly... larger ones need to update
701709
this._layouts.forEach((layout, column) => {
702710
if (!layout || column === this.column) return this;
@@ -705,12 +713,12 @@ export class GridStackEngine {
705713
}
706714
else {
707715
// we save the original x,y,w (h isn't cached) to see what actually changed to propagate better.
708-
// Note: we don't need to check against out of bound scaling/moving as that will be done when using those cache values.
716+
// NOTE: we don't need to check against out of bound scaling/moving as that will be done when using those cache values. #1785
717+
let ratio = column / this.column;
709718
nodes.forEach(node => {
710719
if (!node._orig) return; // didn't change (newly added ?)
711720
let n = layout.find(l => l._id === node._id);
712721
if (!n) return; // no cache for new nodes. Will use those values.
713-
let ratio = column / this.column;
714722
// Y changed, push down same amount
715723
// TODO: detect doing item 'swaps' will help instead of move (especially in 1 column mode)
716724
if (node.y !== node._orig.y) {
@@ -736,55 +744,60 @@ export class GridStackEngine {
736744
* Note we store previous layouts (especially original ones) to make it possible to go
737745
* from say 12 -> 1 -> 12 and get back to where we were.
738746
*
739-
* @param oldColumn previous number of columns
747+
* @param prevColumn previous number of columns
740748
* @param column new column number
741749
* @param nodes different sorted list (ex: DOM order) instead of current list
742750
* @param layout specify the type of re-layout that will happen (position, size, etc...).
743751
* Note: items will never be outside of the current column boundaries. default (moveScale). Ignored for 1 column
744752
*/
745-
public updateNodeWidths(oldColumn: number, column: number, nodes: GridStackNode[], layout: ColumnOptions = 'moveScale'): GridStackEngine {
746-
if (!this.nodes.length || oldColumn === column) return this;
753+
public updateNodeWidths(prevColumn: number, column: number, nodes: GridStackNode[], layout: ColumnOptions = 'moveScale'): GridStackEngine {
754+
if (!this.nodes.length || !column || prevColumn === column) return this;
747755

748756
// cache the current layout in case they want to go back (like 12 -> 1 -> 12) as it requires original data
749-
this.cacheLayout(this.nodes, oldColumn);
757+
this.cacheLayout(this.nodes, prevColumn);
758+
this.batchUpdate(); // do this EARLY as it will call saveInitial() so we can detect where we started for _dirty and collision
759+
let newNodes: GridStackNode[] = [];
750760

751761
// if we're going to 1 column and using DOM order rather than default sorting, then generate that layout
752-
if (column === 1 && nodes && nodes.length) {
762+
let domOrder = false;
763+
if (column === 1 && nodes?.length) {
764+
domOrder = true;
753765
let top = 0;
754766
nodes.forEach(n => {
755767
n.x = 0;
756768
n.w = 1;
757769
n.y = Math.max(n.y, top);
758770
top = n.y + n.h;
759771
});
772+
newNodes = nodes;
773+
nodes = [];
760774
} else {
761-
nodes = Utils.sort(this.nodes, -1, oldColumn); // current column reverse sorting so we can insert last to front (limit collision)
762-
}
763-
764-
// see if we have cached previous layout.
765-
let cacheNodes = this._layouts[column] || [];
766-
// if not AND we are going up in size start with the largest layout as down-scaling is more accurate
767-
let lastIndex = this._layouts.length - 1;
768-
if (cacheNodes.length === 0 && column > oldColumn && column < lastIndex) {
769-
cacheNodes = this._layouts[lastIndex] || [];
770-
if (cacheNodes.length) {
771-
// pretend we came from that larger column by assigning those values as starting point
772-
oldColumn = lastIndex;
773-
cacheNodes.forEach(cacheNode => {
774-
let j = nodes.findIndex(n => n._id === cacheNode._id);
775-
if (j !== -1) {
775+
nodes = Utils.sort(this.nodes, -1, prevColumn); // current column reverse sorting so we can insert last to front (limit collision)
776+
}
777+
778+
// see if we have cached previous layout IFF we are going up in size (restore) otherwise always
779+
// generate next size down from where we are (looks more natural as you gradually size down).
780+
let cacheNodes: Layout[] = [];
781+
if (column > prevColumn) {
782+
cacheNodes = this._layouts[column] || [];
783+
// ...if not, start with the largest layout (if not already there) as down-scaling is more accurate
784+
// by pretending we came from that larger column by assigning those values as starting point
785+
let lastIndex = this._layouts.length - 1;
786+
if (!cacheNodes.length && prevColumn !== lastIndex && this._layouts[lastIndex]?.length) {
787+
prevColumn = lastIndex;
788+
this._layouts[lastIndex].forEach(cacheNode => {
789+
let n = nodes.find(n => n._id === cacheNode._id);
790+
if (n) {
776791
// still current, use cache info positions
777-
nodes[j].x = cacheNode.x;
778-
nodes[j].y = cacheNode.y;
779-
nodes[j].w = cacheNode.w;
792+
n.x = cacheNode.x;
793+
n.y = cacheNode.y;
794+
n.w = cacheNode.w;
780795
}
781796
});
782-
cacheNodes = []; // we still don't have new column cached data... will generate from larger one.
783797
}
784798
}
785799

786800
// if we found cache re-use those nodes that are still current
787-
let newNodes: GridStackNode[] = [];
788801
cacheNodes.forEach(cacheNode => {
789802
let j = nodes.findIndex(n => n._id === cacheNode._id);
790803
if (j !== -1) {
@@ -799,14 +812,15 @@ export class GridStackEngine {
799812
// ...and add any extra non-cached ones
800813
if (nodes.length) {
801814
if (typeof layout === 'function') {
802-
layout(column, oldColumn, newNodes, nodes);
803-
} else {
804-
let ratio = column / oldColumn;
815+
layout(column, prevColumn, newNodes, nodes);
816+
} else if (!domOrder) {
817+
let ratio = column / prevColumn;
805818
let move = (layout === 'move' || layout === 'moveScale');
806819
let scale = (layout === 'scale' || layout === 'moveScale');
807820
nodes.forEach(node => {
821+
// NOTE: x + w could be outside of the grid, but addNode() below will handle that
808822
node.x = (column === 1 ? 0 : (move ? Math.round(node.x * ratio) : Math.min(node.x, column - 1)));
809-
node.w = ((column === 1 || oldColumn === 1) ? 1 :
823+
node.w = ((column === 1 || prevColumn === 1) ? 1 :
810824
scale ? (Math.round(node.w * ratio) || 1) : (Math.min(node.w, column)));
811825
newNodes.push(node);
812826
});
@@ -816,15 +830,14 @@ export class GridStackEngine {
816830

817831
// finally re-layout them in reverse order (to get correct placement)
818832
newNodes = Utils.sort(newNodes, -1, column);
819-
this._ignoreLayoutsNodeChange = true;
820-
this.batchUpdate();
821-
this.nodes = []; // pretend we have no nodes to start with (we use same structures) to simplify layout
833+
this._inColumnResize = true; // prevent cache update
834+
this.nodes = []; // pretend we have no nodes to start with (add() will use same structures) to simplify layout
822835
newNodes.forEach(node => {
823836
this.addNode(node, false); // 'false' for add event trigger
824-
node._dirty = true; // force attr update
825-
}, this);
837+
delete node._orig; // make sure the commit doesn't try to restore things back to original
838+
});
826839
this.commit();
827-
delete this._ignoreLayoutsNodeChange;
840+
delete this._inColumnResize;
828841
return this;
829842
}
830843

src/gridstack.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ export class GridStack {
668668
* Note: items will never be outside of the current column boundaries. default (moveScale). Ignored for 1 column
669669
*/
670670
public column(column: number, layout: ColumnOptions = 'moveScale'): GridStack {
671-
if (this.opts.column === column) return this;
671+
if (column < 1 || this.opts.column === column) return this;
672672
let oldColumn = this.opts.column;
673673

674674
// if we go into 1 column mode (which happens if we're sized less than minW unless disableOneColumnMode is on)

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export interface GridStackMoveOpts extends GridStackPosition {
201201
pack?: boolean;
202202
/** true if we are calling this recursively to prevent simple swap or coverage collision - default false*/
203203
nested?: boolean;
204-
/* vars to calculate other cells coordinates */
204+
/** vars to calculate other cells coordinates */
205205
cellWidth?: number;
206206
cellHeight?: number;
207207
marginTop?: number;

src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export class Utils {
219219
return true;
220220
}
221221

222-
/* copies over b size & position (GridStackPosition), and possibly min/max as well */
222+
/** copies over b size & position (GridStackPosition), and possibly min/max as well */
223223
static copyPos(a: GridStackWidget, b: GridStackWidget, minMax = false): GridStackWidget {
224224
a.x = b.x;
225225
a.y = b.y;
@@ -233,7 +233,7 @@ export class Utils {
233233
return a;
234234
}
235235

236-
/* true if a and b has same size & position */
236+
/** true if a and b has same size & position */
237237
static samePos(a: GridStackPosition, b: GridStackPosition): boolean {
238238
return a && b && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
239239
}

0 commit comments

Comments
 (0)