Skip to content

Commit 9a2eb39

Browse files
committed
#1286 new save()/restore() API
* added new api to save and relod layouts and test cases showing usage * GridstackWidget -> GridStackWidget typo fix
1 parent 96ce9c0 commit 9a2eb39

File tree

9 files changed

+156
-27
lines changed

9 files changed

+156
-27
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ make sure to read v1.0.0 migration first!
335335
v2.x is a Typescript rewrite of 1.x, removing all jquery events, using classes and overall code cleanup. Your code might need to change from 1.x
336336
337337
1. In general methods that used no args (getter) vs setter are not used in Typescript.
338-
Also legacy methods that used to take tons of parameters will now take a single object (typically `GridstackOptions` or `GridstackWidget`).
338+
Also legacy methods that used to take tons of parameters will now take a single object (typically `GridstackOptions` or `GridStackWidget`).
339339
340340
```
341341
removed `addWidget(el, x, y, width, ...)` --> use the widget options version instead `addWidget(el, {with, ...})`

demo/serialization.html

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
<h1>Serialization demo</h1>
1515
<a onClick="saveGrid()" class="btn btn-primary" href="#">Save</a>
1616
<a onClick="loadGrid()" class="btn btn-primary" href="#">Load</a>
17+
<a onClick="saveGridManual()" class="btn btn-primary" href="#">Save Manual</a>
18+
<a onClick="loadGridManual()" class="btn btn-primary" href="#">Load Manual</a>
1719
<a onClick="clearGrid()" class="btn btn-primary" href="#">Clear</a>
1820
<br/><br/>
19-
<div class="grid-stack"></div>
21+
<div class="grid-stack"></div>
2022
<hr/>
2123
<textarea id="saved-data" cols="100" rows="20" readonly="readonly"></textarea>
2224
</div>
@@ -31,31 +33,56 @@ <h1>Serialization demo</h1>
3133
});
3234

3335
let serializedData = [
34-
{x: 0, y: 0, width: 2, height: 2},
35-
{x: 3, y: 1, width: 1, height: 2},
36-
{x: 4, y: 1, width: 1, height: 1},
37-
{x: 2, y: 3, width: 3, height: 1},
38-
{x: 1, y: 3, width: 1, height: 1}
36+
{x: 0, y: 0, width: 2, height: 2, id: '0'},
37+
{x: 3, y: 1, width: 1, height: 2, id: '1'},
38+
{x: 4, y: 1, width: 1, height: 1, id: '2'},
39+
{x: 2, y: 3, width: 3, height: 1, id: '3'},
40+
{x: 1, y: 3, width: 1, height: 1, id: '4'}
3941
];
4042

43+
// NEW 2.x method
4144
loadGrid = function() {
42-
grid.removeAll();
45+
grid.restore(serializedData, true);
46+
}
47+
48+
// NEW 2.x method
49+
saveGrid = function() {
50+
serializedData = grid.save();
51+
document.querySelector('#saved-data').value = JSON.stringify(serializedData, null, ' ');
52+
}
53+
54+
// old (pre 2.x) way to manually load a grid
55+
loadGridManual = function() {
4356
let items = GridStack.Utils.sort(serializedData);
4457
grid.batchUpdate();
45-
items.forEach(function (node) {
46-
grid.addWidget('<div><div class="grid-stack-item-content"></div></div>', node);
47-
});
58+
59+
if (grid.engine.nodes.length === 0) {
60+
// load from empty
61+
items.forEach(function (item) {
62+
grid.addWidget('<div><div class="grid-stack-item-content">' + item.id + '</div></div>', item);
63+
});
64+
} else {
65+
// else update existing nodes (instead of calling grid.removeAll())
66+
grid.engine.nodes.forEach(function (node) {
67+
let item = items.find(function(e) { return e.id === node.id});
68+
grid.update(node.el, item.x, item.y, item.width, item.height);
69+
});
70+
}
71+
4872
grid.commit();
4973
};
5074

51-
saveGrid = function() {
75+
// old (pre 2.x) way to manually save a grid
76+
saveGridManual = function() {
5277
serializedData = [];
5378
grid.engine.nodes.forEach(function(node) {
5479
serializedData.push({
5580
x: node.x,
5681
y: node.y,
5782
width: node.width,
58-
height: node.height
83+
height: node.height,
84+
id: node.id,
85+
custom: 'save anything here'
5986
});
6087
});
6188
document.querySelector('#saved-data').value = JSON.stringify(serializedData, null, ' ');

doc/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Change log
3939
- re-write to native Typescript, removing all JQuery from main code and API (drag&drop plugin still using for now)
4040
- add `getGridItems()` to return list of HTML grid items
4141
- add `{dragIn | dragInOptions}` grid attributes to handle external drag&drop items
42+
- add `save()` and `restore()` to serialize grids from JSON, saving all attributes (not just w,h,x,y) [1286](https://github.com/gridstack/gridstack.js/issues/1286)
4243

4344
## 1.1.2 (2020-05-17)
4445

doc/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ gridstack.js API
3636
- [float(val?)](#floatval)
3737
- [getCellHeight()](#getcellheight)
3838
- [getCellFromPixel(position[, useOffset])](#getcellfrompixelposition-useoffset)
39+
- [getGridItems(): GridItemHTMLElement[]](#getgriditems-griditemhtmlelement)
3940
- [isAreaEmpty(x, y, width, height)](#isareaemptyx-y-width-height)
4041
- [locked(el, val)](#lockedel-val)
4142
- [makeWidget(el)](#makewidgetel)
@@ -49,6 +50,8 @@ gridstack.js API
4950
- [removeAll([removeDOM])](#removeallremovedom)
5051
- [resize(el, width, height)](#resizeel-width-height)
5152
- [resizable(el, val)](#resizableel-val)
53+
- [restore(layout: GridStackWidget[], addAndRemove?: boolean)](#restorelayout-gridstackwidget-addandremove-boolean)
54+
- [save(): GridStackWidget[]](#save-gridstackwidget)
5255
- [setAnimation(doAnimate)](#setanimationdoanimate)
5356
- [setStatic(staticValue)](#setstaticstaticvalue)
5457
- [update(el, x, y, width, height)](#updateel-x-y-width-height)
@@ -83,6 +86,8 @@ gridstack.js API
8386
- `disableDrag` - disallows dragging of widgets (default: `false`).
8487
- `disableOneColumnMode` - disables the onColumnMode when the grid width is less than minWidth (default: 'false')
8588
- `disableResize` - disallows resizing of widgets (default: `false`).
89+
- `dragIn` - specify the class of items that can be dragged into the grid (ex: dragIn: '.newWidget'
90+
- `dragInOptions` - options for items that can be dragged into the grid (ex: dragInOptions: { revert: 'invalid', scroll: false, appendTo: 'body', helper: 'clone' }
8691
- `draggable` - allows to override jQuery UI draggable options. (default: `{handle: '.grid-stack-item-content', scroll: false, appendTo: 'body', containment: null}`)
8792
- `dragOut` to let user drag nested grid items out of a parent or not (default false) See [example](http://gridstackjs.com/demo/nested.html)
8893
- `float` - enable floating widgets (default: `false`) See [example](http://gridstackjs.com/demo/float.html)
@@ -361,6 +366,10 @@ Parameters :
361366

362367
Returns an object with properties `x` and `y` i.e. the column and row in the grid.
363368

369+
### getGridItems(): GridItemHTMLElement[]
370+
371+
Return list of GridItem HTML dom elements (excluding temporary placeholder)
372+
364373
### isAreaEmpty(x, y, width, height)
365374

366375
Checks if specified area is empty.
@@ -464,6 +473,17 @@ Enables/Disables resizing.
464473
- `el` - widget to modify
465474
- `val` - if `true` widget will be resizable.
466475

476+
### restore(layout: GridStackWidget[], addAndRemove?: boolean)
477+
478+
- used to restore a grid layout for a saved layout list (see `save()`).
479+
- Optional `addAndRemove` can be passed if new widgets should be added or removed if the are not present (`id` is used to look items up)
480+
- see [example](http://gridstackjs.com/demo/serialization.html)
481+
482+
### save(): GridStackWidget[]
483+
484+
- returns the layout of the grid that can be serialized (list of item non default attributes, not just w,y,x,y but also min/max and id). See `restore()`
485+
- see [example](http://gridstackjs.com/demo/serialization.html)
486+
467487
### setAnimation(doAnimate)
468488

469489
Toggle the grid animation state. Toggles the `grid-stack-animate` class.

spec/gridstack-spec.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ describe('gridstack', function() {
77
// grid has 4x2 and 4x4 top-left aligned - used on most test cases
88
let gridHTML =
99
'<div class="grid-stack">' +
10-
' <div class="grid-stack-item" data-gs-x="0" data-gs-y="0" data-gs-width="4" data-gs-height="2" id="item1">' +
10+
' <div class="grid-stack-item" data-gs-x="0" data-gs-y="0" data-gs-width="4" data-gs-height="2" data-gs-id="item1" id="item1">' +
1111
' <div class="grid-stack-item-content">item 1</div>' +
1212
' </div>' +
13-
' <div class="grid-stack-item" data-gs-x="4" data-gs-y="0" data-gs-width="4" data-gs-height="4" id="item2">' +
13+
' <div class="grid-stack-item" data-gs-x="4" data-gs-y="0" data-gs-width="4" data-gs-height="4" data-gs-id="item2" id="item2">' +
1414
' <div class="grid-stack-item-content">item 2</div>' +
1515
' </div>' +
1616
'</div>';
@@ -1469,6 +1469,38 @@ describe('gridstack', function() {
14691469

14701470
});
14711471

1472+
describe('save & restore', function() {
1473+
beforeEach(function() {
1474+
document.body.insertAdjacentHTML('afterbegin', gridstackHTML);
1475+
});
1476+
afterEach(function() {
1477+
document.body.removeChild(document.getElementById('gs-cont'));
1478+
});
1479+
it('save layout', function() {
1480+
let grid = GridStack.init();
1481+
let layout = grid.save();
1482+
expect(layout).toEqual([{x:0, y:0, width:4, height:2, id:'item1'}, {x:4, y:0, width:4, height:4, id:'item2'}]);
1483+
});
1484+
it('restore size 1 item', function() {
1485+
let grid = GridStack.init();
1486+
grid.restore([{height:3, id:'item1'}]);
1487+
let layout = grid.save();
1488+
expect(layout).toEqual([{x:0, y:0, width:4, height:3, id:'item1'}, {x:4, y:0, width:4, height:4, id:'item2'}]);
1489+
});
1490+
it('restore move 1 item, delete others', function() {
1491+
let grid = GridStack.init();
1492+
grid.restore([{x:2, height:1, id:'item2'}], true);
1493+
let layout = grid.save();
1494+
expect(layout).toEqual([{x:2, y:0, width:4, height:1, id:'item2'}]);
1495+
});
1496+
it('restore add new, delete others', function() {
1497+
let grid = GridStack.init();
1498+
grid.restore([{width:2, height:1, id:'item3'}], true);
1499+
let layout = grid.save();
1500+
expect(layout).toEqual([{x:0, y:0, width:2, height:1, id:'item3'}]);
1501+
});
1502+
});
1503+
14721504
// ..and finally track log warnings at the end, instead of displaying them....
14731505
describe('obsolete warnings', function() {
14741506
console.warn = jasmine.createSpy('log'); // track warnings instead of displaying them

src/gridstack-engine.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { Utils, obsolete } from './utils';
10-
import { GridStackNode } from './types';
10+
import { GridStackNode, GridStackWidget } from './types';
1111

1212
export type onChangeCB = (nodes: GridStackNode[], removeDOM?: boolean) => void;
1313

@@ -466,6 +466,26 @@ export class GridStackEngine {
466466
return this;
467467
}
468468

469+
/** saves the current layout returning a list of widgets for serialization */
470+
public save(): GridStackWidget[] {
471+
let widgets: GridStackWidget[] = [];
472+
Utils.sort(this.nodes);
473+
this.nodes.forEach(n => {
474+
let w: GridStackNode = {};
475+
for (let key in n) { if (key[0] !== '_' && n[key] !== null && n[key] !== undefined ) w[key] = n[key]; }
476+
// delete other internals
477+
delete w.el;
478+
delete w.grid;
479+
// delete default values (will be re-created on read)
480+
if (!w.autoPosition) delete w.autoPosition;
481+
if (!w.noResize) delete w.noResize;
482+
if (!w.noMove) delete w.noMove;
483+
if (!w.locked) delete w.locked;
484+
widgets.push(w);
485+
});
486+
return widgets;
487+
}
488+
469489
/** @internal called whenever a node is added or moved - updates the cached layouts */
470490
public layoutsNodesChange(nodes: GridStackNode[]): GridStackEngine {
471491
if (!this._layouts || this._ignoreLayoutsNodeChange) return this;
@@ -614,7 +634,7 @@ export class GridStackEngine {
614634
private getGridHeight = obsolete(this, GridStackEngine.prototype.getRow, 'getGridHeight', 'getRow', 'v1.0.0');
615635
}
616636

617-
/** @internal class to store per column layout bare minimal info (subset of GridstackWidget) */
637+
/** @internal class to store per column layout bare minimal info (subset of GridStackWidget) */
618638
interface Layout {
619639
x: number;
620640
y: number;

src/gridstack.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { GridStackEngine } from './gridstack-engine';
1010
import { obsoleteOpts, obsoleteOptsDel, obsoleteAttr, obsolete, Utils } from './utils';
11-
import { GridItemHTMLElement, GridstackWidget, GridStackNode, GridstackOptions, numberOrString } from './types';
11+
import { GridItemHTMLElement, GridStackWidget, GridStackNode, GridstackOptions, numberOrString } from './types';
1212
import { GridStackDragDropPlugin } from './gridstack-dragdrop-plugin';
1313

1414
export type GridStackElement = string | HTMLElement | GridItemHTMLElement;
@@ -310,14 +310,14 @@ export class GridStack {
310310
* @param el html element or string definition to add
311311
* @param options widget position/size options (optional) - see GridStackWidget
312312
*/
313-
public addWidget(el: GridStackElement, options?: GridstackWidget): GridItemHTMLElement {
313+
public addWidget(el: GridStackElement, options?: GridStackWidget): GridItemHTMLElement {
314314

315315
// support legacy call for now ?
316316
if (arguments.length > 2) {
317317
console.warn('gridstack.ts: `addWidget(el, x, y, width...)` is deprecated. Use `addWidget(el, {x, y, width,...})`. It will be removed soon');
318318
// eslint-disable-next-line prefer-rest-params
319319
let a = arguments, i = 1,
320-
opt: GridstackWidget = { x:a[i++], y:a[i++], width:a[i++], height:a[i++], autoPosition:a[i++],
320+
opt: GridStackWidget = { x:a[i++], y:a[i++], width:a[i++], height:a[i++], autoPosition:a[i++],
321321
minWidth:a[i++], maxWidth:a[i++], minHeight:a[i++], maxHeight:a[i++], id:a[i++] };
322322
return this.addWidget(el, opt);
323323
}
@@ -342,6 +342,35 @@ export class GridStack {
342342
return this.makeWidget(el);
343343
}
344344

345+
/** saves the current layout returning a list of widgets for serialization */
346+
public save(): GridStackWidget[] { return this.engine.save(); }
347+
348+
/** restore the widgets from a list. This will call update() on each (matching by id),
349+
* or optionally add/remove widgets that are not there (either a boolean or a callback method) */
350+
public restore(layout: GridStackWidget[], addAndRemove?: boolean) {
351+
let items = GridStack.Utils.sort(layout);
352+
this.batchUpdate();
353+
// see if any items are missing from new layout and need to be removed first
354+
if (addAndRemove) {
355+
this.engine.nodes.forEach(n => {
356+
let item = items.find(w => n.id === w.id);
357+
if (!item) {
358+
this.removeWidget(n.el);
359+
}
360+
});
361+
}
362+
// now add/update the widgets
363+
items.forEach(w => {
364+
let item = this.engine.nodes.find(n => n.id === w.id);
365+
if (item) {
366+
this.update(item.el, w.x, w.y, w.width, w.height); // TODO: full update
367+
} else if (addAndRemove) {
368+
this.addWidget('<div><div class="grid-stack-item-content"></div></div>', w);
369+
}
370+
});
371+
this.commit();
372+
}
373+
345374
/**
346375
* Initializes batch updates. You will see no changes until `commit()` method is called.
347376
*/
@@ -465,7 +494,7 @@ export class GridStack {
465494
return this.opts.column;
466495
}
467496

468-
/** returns an array of grid HTML elements (no placeholder) - used internally to iterate through our children */
497+
/** returns an array of grid HTML elements (no placeholder) - used to iterate through our children */
469498
public getGridItems(): GridItemHTMLElement[] {
470499
return Array.from(this.el.children)
471500
.filter((el: HTMLElement) => el.matches('.' + this.opts.itemClass) && !el.matches('.' + this.opts.placeholderClass)) as GridItemHTMLElement[];
@@ -1380,7 +1409,7 @@ export class GridStack {
13801409
}
13811410

13821411
/** @internal call to write any default attributes back to element */
1383-
private _writeAttr(el: HTMLElement, node: GridstackWidget): GridStack {
1412+
private _writeAttr(el: HTMLElement, node: GridStackWidget): GridStack {
13841413
if (!node) return this;
13851414
this._writeAttrs(el, node.x, node.y, node.width, node.height);
13861415

@@ -1418,7 +1447,7 @@ export class GridStack {
14181447
}
14191448

14201449
/** @internal call to read any default attributes from element */
1421-
private _readAttr(el: HTMLElement, node: GridStackNode = {}): GridstackWidget {
1450+
private _readAttr(el: HTMLElement, node: GridStackNode = {}): GridStackWidget {
14221451
node.x = Utils.toNumber(el.getAttribute('data-gs-x'));
14231452
node.y = Utils.toNumber(el.getAttribute('data-gs-y'));
14241453
node.width = Utils.toNumber(el.getAttribute('data-gs-width'));

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export interface GridstackOptions {
166166
/**
167167
* Gridstack Widget creation options
168168
*/
169-
export interface GridstackWidget {
169+
export interface GridStackWidget {
170170
/** widget position x (default?: 0) */
171171
x?: number;
172172
/** widget position y (default?: 0) */
@@ -235,7 +235,7 @@ export interface DDDragInOpt extends DDDragOpt {
235235
/**
236236
* internal descriptions describing the items in the grid
237237
*/
238-
export interface GridStackNode extends GridstackWidget {
238+
export interface GridStackNode extends GridStackWidget {
239239
/** pointer back to HTML element */
240240
el?: GridItemHTMLElement;
241241
/** pointer back to Grid instance */

src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* gridstack.js may be freely distributed under the MIT license.
77
*/
88

9-
import { GridstackWidget, GridStackNode, GridstackOptions, numberOrString } from './types';
9+
import { GridStackWidget, GridStackNode, GridstackOptions, numberOrString } from './types';
1010

1111
/** checks for obsolete method names */
1212
export function obsolete(self, f, oldName: string, newName: string, rev: string) {
@@ -51,7 +51,7 @@ export function obsoleteAttr(el: HTMLElement, oldName: string, newName: string,
5151
export class Utils {
5252

5353
/** returns true if a and b overlap */
54-
static isIntercepted(a: GridstackWidget, b: GridstackWidget): boolean {
54+
static isIntercepted(a: GridStackWidget, b: GridStackWidget): boolean {
5555
return !(a.x + a.width <= b.x || b.x + b.width <= a.x || a.y + a.height <= b.y || b.y + b.height <= a.y);
5656
}
5757

0 commit comments

Comments
 (0)