Skip to content

Commit d9c2241

Browse files
committed
load() in 1 column mode layout fix
* fif for 1471 * when `load()` is called into a grid that is 1 column mode due to small area, we now cache the original 12 column layout to restore correctly * added sample code. Had to change more code than expected. * Also optimized loading to not re-do some items (re-writing attr multiple times) * Also fixed react sample
1 parent e4ac6b0 commit d9c2241

File tree

6 files changed

+165
-39
lines changed

6 files changed

+165
-39
lines changed

demo/react.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>Gridstack.js React integration example</title>
77
<link rel="stylesheet" href="demo.css" />
8-
<script src="../dist/gridstack.all.js"></script>
8+
<script src="../dist/gridstack-h5.js"></script>
99

1010
<!-- Scripts to use react inside html -->
1111
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>

doc/CHANGES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ Change log
4545

4646
- we now have a React example, in addition to Vue - Angular is next!. thanks [@eloparco](https://github.com/eloparco)
4747
- fix placeholder not having custom `GridStackOptions.itemClass`. thanks [@pablosichert](https://github.com/pablosichert)
48-
- fix [1484](https://github.com/gridstack/gridstack.js/issues/1484) draging between 2 grids and back (regression in 2.0.1)
48+
- fix [1484](https://github.com/gridstack/gridstack.js/issues/1484) dragging between 2 grids and back (regression in 2.0.1)
49+
- fix [1471](https://github.com/gridstack/gridstack.js/issues/1471) `load()` into 1 column mode doesn't resize back to 12 correctly
4950
- del `ddPlugin` grid option as we only have one drag&drop plugin at runtime, which is defined by the include you use (HTML5 vs jquery vs none)
5051

5152
## 2.2.0 (2020-11-7)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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>load() 1 column</title>
8+
9+
<link rel="stylesheet" href="../../../demo/demo.css"/>
10+
<script src="../../../dist/gridstack-h5.js"></script>
11+
12+
</head>
13+
<body>
14+
<div class="container-fluid">
15+
<h1>load() 1 column</h1>
16+
<p>resize to below 768px, reload, then expand up to check 12 column</p>
17+
<div>
18+
<a class="btn btn-primary" onClick="addNewWidget()" href="#">Add Widget</a>
19+
<a class="btn btn-primary" onclick="toggleFloat()" id="float" href="#">float: true</a>
20+
</div>
21+
<br><br>
22+
<div class="grid-stack"></div>
23+
</div>
24+
<script src="../../../demo/events.js"></script>
25+
<script type="text/javascript">
26+
let grid = GridStack.init({float: true});
27+
addEvents(grid);
28+
29+
let items = [
30+
{x: 1, y: 0, width: 2, height: 1, content: '0'},
31+
{x: 3, y: 1, width: 1, height: 2, content: '1'},
32+
{x: 4, y: 1, width: 1, height: 1, content: '2'},
33+
{x: 2, y: 3, width: 3, height: 1, content: '3'},
34+
{x: 0, y: 6, width: 2, height: 2, content: '4'}
35+
];
36+
grid.load(items);
37+
38+
addNewWidget = function() {
39+
let n = items[count] || {
40+
x: Math.round(12 * Math.random()),
41+
y: Math.round(5 * Math.random()),
42+
width: Math.round(1 + 3 * Math.random()),
43+
height: Math.round(1 + 3 * Math.random())
44+
};
45+
n.content = String(count++);
46+
grid.addWidget(n);
47+
};
48+
49+
toggleFloat = function() {
50+
grid.float(! grid.getFloat());
51+
document.querySelector('#float').innerHTML = 'float: ' + grid.getFloat();
52+
};
53+
</script>
54+
</body>
55+
</html>

src/gridstack-engine.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -196,17 +196,21 @@ export class GridStackEngine {
196196

197197
// assign defaults for missing required fields
198198
let defaults = {width: 1, height: 1, x: 0, y: 0};
199-
node = Utils.defaults(node, defaults);
199+
Utils.defaults(node, defaults);
200200

201-
node.autoPosition = node.autoPosition || false;
202-
node.noResize = node.noResize || false;
203-
node.noMove = node.noMove || false;
201+
if (!node.autoPosition) { delete node.autoPosition; }
202+
if (!node.noResize) { delete node.noResize; }
203+
if (!node.noMove) { delete node.noMove; }
204204

205205
// check for NaN (in case messed up strings were passed. can't do parseInt() || defaults.x above as 0 is valid #)
206-
if (Number.isNaN(node.x)) { node.x = defaults.x; node.autoPosition = true; }
207-
if (Number.isNaN(node.y)) { node.y = defaults.y; node.autoPosition = true; }
208-
if (Number.isNaN(node.width)) { node.width = defaults.width; }
209-
if (Number.isNaN(node.height)) { node.height = defaults.height; }
206+
if (typeof node.x == 'string') { node.x = Number(node.x); }
207+
if (typeof node.y == 'string') { node.y = Number(node.y); }
208+
if (typeof node.width == 'string') { node.width = Number(node.width); }
209+
if (typeof node.height == 'string') { node.height = Number(node.height); }
210+
if (isNaN(node.x)) { node.x = defaults.x; node.autoPosition = true; }
211+
if (isNaN(node.y)) { node.y = defaults.y; node.autoPosition = true; }
212+
if (isNaN(node.width)) { node.width = defaults.width; }
213+
if (isNaN(node.height)) { node.height = defaults.height; }
210214

211215
if (node.maxWidth) { node.width = Math.min(node.width, node.maxWidth); }
212216
if (node.maxHeight) { node.height = Math.min(node.height, node.maxHeight); }
@@ -541,10 +545,7 @@ export class GridStackEngine {
541545
if (!this.nodes.length || oldColumn === column) { return this }
542546

543547
// cache the current layout in case they want to go back (like 12 -> 1 -> 12) as it requires original data
544-
let copy: Layout[] = [];
545-
this.nodes.forEach((n, i) => { copy[i] = {x: n.x, y: n.y, width: n.width, _id: n._id} }); // only thing we change is x,y,w and id to find it back
546-
this._layouts = this._layouts || []; // use array to find larger quick
547-
this._layouts[oldColumn] = copy;
548+
this.cacheLayout(this.nodes, oldColumn);
548549

549550
// if we're going to 1 column and using DOM order rather than default sorting, then generate that layout
550551
if (column === 1 && nodes && nodes.length) {
@@ -638,6 +639,24 @@ export class GridStackEngine {
638639
return this;
639640
}
640641

642+
/**
643+
* call to cache the given layout internally to the given location so we can restore back when column changes size
644+
* @param nodes list of nodes
645+
* @param column corresponding column index to save it under
646+
* @param clear if true, will force other caches to be removed (default false)
647+
*/
648+
public cacheLayout(nodes: GridStackNode[], column: number, clear = false): GridStackEngine {
649+
let copy: Layout[] = [];
650+
nodes.forEach((n, i) => {
651+
n._id = n._id || GridStackEngine._idSeq++; // make sure we have an id in case this is new layout, else re-use id already set
652+
copy[i] = {x: n.x, y: n.y, width: n.width, _id: n._id} // only thing we change is x,y,w and id to find it back
653+
});
654+
this._layouts = clear ? [] : this._layouts || []; // use array to find larger quick
655+
this._layouts[column] = copy;
656+
return this;
657+
}
658+
659+
641660
/** called to remove all internal values */
642661
public cleanupNode(node: GridStackNode): GridStackEngine {
643662
for (let prop in node) {

src/gridstack.ts

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ export class GridStack {
128128
/** @internal */
129129
private _prevColumn: number;
130130
/** @internal */
131+
private _ignoreLayoutsNodeChange: boolean;
132+
/** @internal */
131133
public _gsEventHandler = {};
132134
/** @internal */
133135
private _styles: GridCSSStyleSheet;
@@ -287,12 +289,10 @@ export class GridStack {
287289

288290
this._updateContainerHeight();
289291

290-
this.onParentResize();
291-
292292
this._setupDragIn();
293293
this._setupRemoveDrop();
294294
this._setupAcceptWidget();
295-
this._updateWindowResizeEvent();
295+
this._updateWindowResizeEvent(); // finally this may size us down to 1 column
296296
}
297297

298298
/**
@@ -307,7 +307,7 @@ export class GridStack {
307307
* grid.addWidget({width: 3, content: 'hello'});
308308
* grid.addWidget('<div class="grid-stack-item"><div class="grid-stack-item-content">hello</div></div>', {width: 3});
309309
*
310-
* @param el html element, or string definition, or GridStackWidget (which can have content string as well) to add
310+
* @param el GridStackWidget (which can have content string as well), html element, or string definition to add
311311
* @param options widget position/size options (optional, and ignore if first param is already option) - see GridStackWidget
312312
*/
313313
public addWidget(els?: GridStackWidget | GridStackElement, options?: GridStackWidget): GridItemHTMLElement {
@@ -343,17 +343,22 @@ export class GridStack {
343343

344344
// Tempting to initialize the passed in opt with default and valid values, but this break knockout demos
345345
// as the actual value are filled in when _prepareElement() calls el.getAttribute('data-gs-xyz) before adding the node.
346-
if (options) {
347-
options = {...options}; // make a copy before we modify in case caller re-uses it
348-
// make sure we load any DOM attributes that are not specified in passed in options (which override)
349-
let domAttr = this._readAttr(el);
350-
Utils.defaults(options, domAttr);
351-
this.engine.prepareNode(options);
352-
this._writeAttr(el, options);
353-
}
346+
// So make sure we load any DOM attributes that are not specified in passed in options (which override)
347+
let domAttr = this._readAttr(el);
348+
options = {...(options || {})}; // make a copy before we modify in case caller re-uses it
349+
Utils.defaults(options, domAttr);
350+
this.engine.prepareNode(options);
351+
this._writeAttr(el, options);
354352

355353
this.el.appendChild(el);
356-
return this.makeWidget(el);
354+
355+
// similar to makeWidget() that doesn't read attr again and worse re-create a new node and loose any _id
356+
this._prepareElement(el, true, options);
357+
this._updateContainerHeight();
358+
this._triggerAddEvent();
359+
this._triggerChangeEvent();
360+
361+
return el;
357362
}
358363

359364
/** saves the current layout returning a list of widgets for serialization */
@@ -385,6 +390,14 @@ export class GridStack {
385390
**/
386391
public load(layout: GridStackWidget[], addAndRemove: boolean | ((w: GridStackWidget, add: boolean) => void) = true): GridStack {
387392
let items = GridStack.Utils.sort(layout);
393+
394+
// if we're loading a layout into 1 column (_prevColumn is set only when going to 1) and items don't fit, make sure to save
395+
// the original wanted layout so we can scale back up correctly #1471
396+
if (this._prevColumn && this._prevColumn !== this.opts.column && items.some(n => (n.x + n.width) > this.opts.column)) {
397+
this._ignoreLayoutsNodeChange = true; // skip layout update
398+
this.engine.cacheLayout(items, this._prevColumn, true);
399+
}
400+
388401
let removed: GridStackNode[] = [];
389402
this.batchUpdate();
390403
// see if any items are missing from new layout and need to be removed first
@@ -404,7 +417,7 @@ export class GridStack {
404417
}
405418
// now add/update the widgets
406419
items.forEach(w => {
407-
let item = this.engine.nodes.find(n => n.id === w.id);
420+
let item = (w.id || w.id === 0) ? this.engine.nodes.find(n => n.id === w.id) : undefined;
408421
if (item) {
409422
this.update(item.el, w.x, w.y, w.width, w.height); // TODO: full update
410423
} else if (addAndRemove) {
@@ -417,6 +430,10 @@ export class GridStack {
417430
});
418431
this.engine.removedNodes = removed;
419432
this.commit();
433+
434+
// after commit, clear that flag
435+
delete this._ignoreLayoutsNodeChange;
436+
420437
return this;
421438
}
422439

@@ -531,7 +548,10 @@ export class GridStack {
531548
this.engine.updateNodeWidths(oldColumn, column, domNodes, layout);
532549

533550
// and trigger our event last...
534-
this._triggerChangeEvent(true); // skip layout update
551+
this._ignoreLayoutsNodeChange = true; // skip layout update
552+
this._triggerChangeEvent();
553+
delete this._ignoreLayoutsNodeChange;
554+
535555
return this;
536556
}
537557

@@ -1007,11 +1027,11 @@ export class GridStack {
10071027
}
10081028

10091029
/** @internal */
1010-
private _triggerChangeEvent(skipLayoutChange?: boolean): GridStack {
1030+
private _triggerChangeEvent(): GridStack {
10111031
if (this.engine.batchMode) { return this; }
10121032
let elements = this.engine.getDirtyNodes(true); // verify they really changed
10131033
if (elements && elements.length) {
1014-
if (!skipLayoutChange) {
1034+
if (!this._ignoreLayoutsNodeChange) {
10151035
this.engine.layoutsNodesChange(elements);
10161036
}
10171037
this._triggerEvent('change', elements);
@@ -1024,7 +1044,9 @@ export class GridStack {
10241044
private _triggerAddEvent(): GridStack {
10251045
if (this.engine.batchMode) { return this }
10261046
if (this.engine.addedNodes && this.engine.addedNodes.length > 0) {
1027-
this.engine.layoutsNodesChange(this.engine.addedNodes);
1047+
if (!this._ignoreLayoutsNodeChange) {
1048+
this.engine.layoutsNodesChange(this.engine.addedNodes);
1049+
}
10281050
// prevent added nodes from also triggering 'change' event (which is called next)
10291051
this.engine.addedNodes.forEach(n => { delete n._dirty; });
10301052
this._triggerEvent('added', this.engine.addedNodes);
@@ -1157,12 +1179,20 @@ export class GridStack {
11571179

11581180

11591181
/** @internal */
1160-
private _prepareElement(el: GridItemHTMLElement, triggerAddEvent = false): GridStack {
1161-
el.classList.add(this.opts.itemClass);
1162-
let node = this._readAttr(el, { el: el, grid: this });
1163-
node = this.engine.addNode(node, triggerAddEvent);
1182+
private _prepareElement(el: GridItemHTMLElement, triggerAddEvent = false, node?: GridStackNode): GridStack {
1183+
if (!node) {
1184+
el.classList.add(this.opts.itemClass);
1185+
node = this._readAttr(el);
1186+
}
11641187
el.gridstackNode = node;
1165-
this._writeAttr(el, node);
1188+
node.el = el;
1189+
node.grid = this;
1190+
let copy = {...node};
1191+
node = this.engine.addNode(node, triggerAddEvent);
1192+
// write node attr back in case there was collision or we have to fix bad values during addNode()
1193+
if (!Utils.same(node, copy)) {
1194+
this._writeAttr(el, node);
1195+
}
11661196
this._prepareDragDropByNode(node);
11671197
return this;
11681198
}
@@ -1231,6 +1261,14 @@ export class GridStack {
12311261
node.resizeHandles = el.getAttribute('data-gs-resize-handles');
12321262
node.id = el.getAttribute('data-gs-id');
12331263

1264+
// remove any key not found (null or false which is default)
1265+
for (const key in node) {
1266+
if (!node.hasOwnProperty(key)) { return; }
1267+
if (node[key] === null || node[key] === false) {
1268+
delete node[key];
1269+
}
1270+
}
1271+
12341272
return node;
12351273
}
12361274

@@ -1305,6 +1343,7 @@ export class GridStack {
13051343
if (workTodo && !forceRemove && !this.opts._isNested && !this._windowResizeBind) {
13061344
this._windowResizeBind = this.onParentResize.bind(this); // so we can properly remove later
13071345
window.addEventListener('resize', this._windowResizeBind);
1346+
this.onParentResize(); // initially call it once...
13081347
} else if ((forceRemove || !workTodo) && this._windowResizeBind) {
13091348
window.removeEventListener('resize', this._windowResizeBind);
13101349
delete this._windowResizeBind; // remove link to us so we can free

src/utils.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ export class Utils {
172172
return Boolean(v);
173173
}
174174

175-
static toNumber(value: null | string): number | null {
176-
return (value === null || value.length === 0) ? null : Number(value);
175+
static toNumber(value: null | string): number {
176+
return (value === null || value.length === 0) ? undefined : Number(value);
177177
}
178178

179179
static parseHeight(val: numberOrString): HeightData {
@@ -211,6 +211,18 @@ export class Utils {
211211
return target;
212212
}
213213

214+
/** given 2 objects return true if they have the same values. Checks for Object {} having same fields and values (just 1 level down) */
215+
static same(a: unknown, b: unknown): boolean {
216+
if (typeof a !== 'object') { return a == b; }
217+
if (typeof a !== typeof b) { return false; }
218+
// else we have object, check just 1 level deep for being same things...
219+
if (Object.keys(a).length !== Object.keys(b).length) { return false; }
220+
for (const key in a) {
221+
if (a[key] !== b[key]) { return false; }
222+
}
223+
return true;
224+
}
225+
214226
/** makes a shallow copy of the passed json struct */
215227
// eslint-disable-next-line
216228
static clone(target: {}): {} {

0 commit comments

Comments
 (0)