Skip to content

Commit 2e18461

Browse files
authored
Merge pull request #1894 from adumesny/master
fix #1785 switching column overlap
2 parents 9d01c00 + 2d4d2c8 commit 2e18461

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)