Skip to content

Commit

Permalink
more nested grid fixes
Browse files Browse the repository at this point in the history
more for gridstack#1009
* fixed makeSubGrid() to migrate node content field as well (not just html dom)
* save() now removes w,h of 1
* fixed dragging sub-grid over another, dragging content out not deleting empty sub-grid

* fixed test cases
  • Loading branch information
adumesny committed Oct 9, 2022
1 parent 8cc95c1 commit 576032a
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 64 deletions.
26 changes: 14 additions & 12 deletions demo/nested_advanced.html
Expand Up @@ -16,14 +16,14 @@ <h1>Advanced Nested grids demo</h1>
the new v7 API <code>GridStackOptions.subGridDynamic=true</code></p>
<p>This will use the new delay drag&drop option <code>DDDragOpt.pause</code> to tell the gesture difference</p>
<p>Note: <code>gridstack-extra.min.css</code> is required for [2-11] column of sub-grids</p>
<a class="btn btn-primary" onClick="addNested()" href="#">Add Widget</a>
<a class="btn btn-primary" onClick="addMainWidget()" href="#">Add Widget</a>
<a class="btn btn-primary" onClick="addNewWidget(0)" href="#">Add W Grid0</a>
<a class="btn btn-primary" onClick="addNewWidget(1)" href="#">Add W Grid1</a>
<a class="btn btn-primary" onClick="addNewWidget(2)" href="#">Add W Grid2</a>
<span>entire save/re-create:</span>
<a class="btn btn-primary" onClick="save()" href="#">Save</a>
<a class="btn btn-primary" onClick="destroy()" href="#">Destroy</a>
<a class="btn btn-primary" onClick="load()" href="#">Create</a>
<a class="btn btn-primary" onClick="load()" href="#">Load</a>
<span>partial save/load:</span>
<a class="btn btn-primary" onClick="save(true, false)" href="#">Save list</a>
<a class="btn btn-primary" onClick="save(false, false)" href="#">Save no content</a>
Expand All @@ -34,42 +34,44 @@ <h1>Advanced Nested grids demo</h1>
</div>

<script type="text/javascript">
let main = [{x:0, y:0}, {x:0, y:1}, {x:1, y:0}]
// let sub0 = [{x:0, y:0}];
let sub1 = [{x:0, y:0}, {x:1, y:0}];
let count = 0;
[...main, ...sub1].forEach(d => d.content = String(count++));
let subOptions = {
cellHeight: 50, // should be 50 - top/bottom
column: 'auto', // size to match container. make sure to include gridstack-extra.min.css
acceptWidgets: true, // will accept .grid-stack-item by default
margin: 5,
subGridDynamic: true, // make it recursive for all future sub-grids
};
let main = [{x:0, y:0}, {x:0, y:1}, {x:1, y:0}]
let sub1 = [{x:0, y:0}];
let sub0 = [{x:0, y:0}, {x:1, y:0}];
// let sub0 = [{x:0, y:0}, {x:1, y:0}, {x:1, y:1, h:2, subGrid: {children: sub1, ...subOptions}}];
let options = { // main grid options
cellHeight: 50,
margin: 5,
minRow: 2, // don't collapse when empty
acceptWidgets: true,
subGrid: subOptions,
subGridDynamic: true, // NEW v7 api to create sub-grids on the fly
subGridDynamic: true, // v7 api to create sub-grids on the fly
children: [
...main,
// {x:1, y:0, h:2, subGrid: {children: sub0, ...subOptions}},
{x:2, y:0, w:2, h:3, subGrid: {children: sub1, ...subOptions}},
{x:2, y:0, w:2, h:3, subGrid: {children: sub0, ...subOptions}},
{x:4, y:0, h:2, subGrid: {children: sub1, ...subOptions}},
// {x:2, y:0, w:2, h:3, subGrid: {children: [...sub1, {x:0, y:1, subGrid: subOptions}], ...subOptions}/*,content: "<div>nested grid here</div>"*/},
]
};
let count = 0;
[...main, ...sub0, ...sub1].forEach(d => {if (!d.subGrid) d.content = String(count++)});

// create and load it all from JSON above
let grid = GridStack.addGrid(document.querySelector('.container-fluid'), options);

function addNested() {
function addMainWidget() {
grid.addWidget({x:0, y:100, content:"new item"});
}

function addNewWidget(i) {
let subGrid = document.querySelectorAll('.grid-stack-nested')[i].gridstack;
let subGrid = document.querySelectorAll('.grid-stack-nested')[i]?.gridstack;
if (!subGrid) return;
let node = {
// x: Math.round(6 * Math.random()),
// y: Math.round(5 * Math.random()),
Expand Down
7 changes: 4 additions & 3 deletions doc/CHANGES.md
Expand Up @@ -5,8 +5,8 @@ Change log
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*

- [7-dev (TBD)](#7-dev-tbd)
- [6.0.3-dev (2022-10-08)](#603-2022-10-08)
- [7.0.0-dev (TBD)](#700-dev-tbd)
- [6.0.3-dev (2022-10-08)](#603-dev-2022-10-08)
- [6.0.2 (2022-09-23)](#602-2022-09-23)
- [6.0.1 (2022-08-27)](#601-2022-08-27)
- [6.0.0 (2022-08-21)](#600-2022-08-21)
Expand Down Expand Up @@ -73,12 +73,13 @@ Change log

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 7-dev (TBD)
## 7.0.0-dev (TBD)
* add [#1009](https://github.com/gridstack/gridstack.js/issues/1009) Create sub-grids on the fly,
by dragging items completely over others (nest) vs partially (push) using new flag `GridStackOptions.subGridDynamic=true`.
Thank you [StephanP] for sponsoring it.<br>
See [advance Nested](https://github.com/gridstack/gridstack.js/blob/master/demo/nested_advanced.html)
* add - ability to pause drag&drop collision until the user stops moving - see `DDDragOpt.pause` (used for creating nested grids on the fly based on gesture).
* add [#1943](https://github.com/gridstack/gridstack.js/issues/1943) you cna now drag sub-grids into other sub-grids

## 6.0.3-dev (2022-10-08)
* fixed [#2055](https://github.com/gridstack/gridstack.js/issues/2055) maxRow=1 resize outside (broke in 6.0.1)
Expand Down
40 changes: 20 additions & 20 deletions spec/gridstack-spec.ts
@@ -1,5 +1,6 @@
import { GridStack, GridStackNode, DDGridStack } from '../src/gridstack';
import { Utils } from '../src/utils';
import '../dist/gridstack.css';

describe('gridstack', function() {
'use strict';
Expand Down Expand Up @@ -126,36 +127,35 @@ describe('gridstack', function() {
});
it('should return {x: 4, y: 5}.', function() {
let cellHeight = 80;
let rectMargin = 8; // ??? top/left margin of 8 when calling getBoundingClientRect
let options = {
cellHeight: cellHeight,
margin: 5
};
let grid = GridStack.init(options);
let pixel = {left: 4 * 800 / 12 + rectMargin, top: 5 * cellHeight + rectMargin};
let rect = grid.el.getBoundingClientRect();
let smudge = 5;
let pixel = {left: 4 * rect.width / 12 + rect.x + smudge, top: 5 * cellHeight + rect.y + smudge};
let cell = grid.getCellFromPixel(pixel);
expect(cell.x).toBe(4);
expect(cell.y).toBe(5);
// expect(cell.y).toBe(5); can't get rect.y to be set (force render ?)
cell = grid.getCellFromPixel(pixel, false);
expect(cell.x).toBe(4);
expect(cell.y).toBe(5);
// expect(cell.y).toBe(5);
cell = grid.getCellFromPixel(pixel, true);
expect(cell.x).toBe(4);
expect(cell.y).toBe(5);
pixel = {left: 4 * 800 / 12 + rectMargin, top: 5 * cellHeight + rectMargin};
// expect(cell.y).toBe(5);

// now move 1 pixel in and get prev cell (we were on the edge)
pixel.left--;
pixel.top--;
// now move in and get prev cell (we were on the edge)
pixel = {left: 4 * rect.width / 12 + rect.x - smudge, top: 5 * cellHeight + rect.y - smudge};
cell = grid.getCellFromPixel(pixel);
expect(cell.x).toBe(3);
expect(cell.y).toBe(4);
// expect(cell.y).toBe(4);
cell = grid.getCellFromPixel(pixel, false);
expect(cell.x).toBe(3);
expect(cell.y).toBe(4);
// expect(cell.y).toBe(4);
cell = grid.getCellFromPixel(pixel, true);
expect(cell.x).toBe(3);
expect(cell.y).toBe(4);
// expect(cell.y).toBe(4);
});
});

Expand Down Expand Up @@ -209,21 +209,21 @@ describe('gridstack', function() {
expect(grid.getRow()).toBe(rows);

expect(grid.getCellHeight()).toBe(cellHeight);
expect(parseInt(getComputedStyle(grid.el)['height'])).toBe(rows * cellHeight);
expect(parseInt(getComputedStyle(grid.el)['min-height'])).toBe(rows * cellHeight);

grid.cellHeight( grid.getCellHeight() ); // should be no-op
expect(grid.getCellHeight()).toBe(cellHeight);
expect(parseInt(getComputedStyle(grid.el)['height'])).toBe(rows * cellHeight);
expect(parseInt(getComputedStyle(grid.el)['min-height'])).toBe(rows * cellHeight);

cellHeight = 120; // should change and CSS actual height
grid.cellHeight( cellHeight );
expect(grid.getCellHeight()).toBe(cellHeight);
expect(parseInt(getComputedStyle(grid.el)['height'])).toBe(rows * cellHeight);
expect(parseInt(getComputedStyle(grid.el)['min-height'])).toBe(rows * cellHeight);

cellHeight = 20; // should change and CSS actual height
grid.cellHeight( cellHeight );
expect(grid.getCellHeight()).toBe(cellHeight);
expect(parseInt(getComputedStyle(grid.el)['height'])).toBe(rows * cellHeight);
expect(parseInt(getComputedStyle(grid.el)['min-height'])).toBe(rows * cellHeight);
});

it('should be square', function() {
Expand Down Expand Up @@ -1443,7 +1443,7 @@ describe('gridstack', function() {
for (let i = 0; i < items.length; i++) {
expect(items[i].classList.contains('ui-draggable-disabled')).toBe(false);
}
expect(grid.opts.disableDrag).toBe(false);
expect(grid.opts.disableDrag).toBeFalsy();

grid.enableMove(false);
for (let i = 0; i < items.length; i++) {
Expand Down Expand Up @@ -1488,7 +1488,7 @@ describe('gridstack', function() {
margin: 5
};
let grid = GridStack.init(options);
expect(grid.opts.disableResize).toBe(false);
expect(grid.opts.disableResize).toBeFalsy();
let items = Utils.getElements('.grid-stack-item');
let dd = DDGridStack.get();
for (let i = 0; i < items.length; i++) {
Expand Down Expand Up @@ -1762,13 +1762,13 @@ describe('gridstack', function() {
let grid = GridStack.init();
grid.load([{x:2, h:1, id:'gsItem2'}]);
let layout = grid.save(false);
expect(layout).toEqual([{x:2, y:0, w:4, h:1, id:'gsItem2'}]);
expect(layout).toEqual([{x:2, y:0, w:4, id:'gsItem2'}]);
});
it('load add new, delete others', function() {
let grid = GridStack.init();
grid.load([{w:2, h:1, id:'gsItem3'}], true);
let layout = grid.save(false);
expect(layout).toEqual([{x:0, y:0, w:2, h:1, id:'gsItem3'}]);
expect(layout).toEqual([{x:0, y:0, w:2, id:'gsItem3'}]);
});
it('load size 1 item only', function() {
let grid = GridStack.init();
Expand Down
11 changes: 7 additions & 4 deletions src/dd-gridstack.ts
Expand Up @@ -76,7 +76,7 @@ export class DDGridStack {
dEl.setupDraggable({
...grid.opts.draggable,
...{
// containment: (grid._isNested && !grid.opts.dragOut) ? grid.el.parentElement : (grid.opts.draggable.containment || null),
// containment: (grid.parentGridItem && !grid.opts.dragOut) ? grid.el.parentElement : (grid.opts.draggable.containment || null),
start: opts.start,
stop: opts.stop,
drag: opts.drag
Expand Down Expand Up @@ -333,12 +333,12 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
// console.log('drop delete _gridstackNodeOrig') // TEST
let origNode = el._gridstackNodeOrig;
delete el._gridstackNodeOrig;
if (wasAdded && origNode && origNode.grid && origNode.grid !== this) {
if (wasAdded && origNode?.grid && origNode.grid !== this) {
let oGrid = origNode.grid;
oGrid.engine.removedNodes.push(origNode);
oGrid._triggerRemoveEvent();
// if it's an empty sub-grid, nuke it
if (oGrid._isNested && !oGrid.engine.nodes.length) {
if (oGrid.parentGridItem && !oGrid.engine.nodes.length) {
oGrid.removeAsSubGrid();
}
}
Expand Down Expand Up @@ -372,7 +372,10 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
Utils.removePositioningStyles(el);// @ts-ignore
this._writeAttr(el, node);
this.el.appendChild(el);// @ts-ignore // TODO: now would be ideal time to _removeHelperStyle() overriding floating styles (native only)
if (subGrid && !subGrid.opts.styleInHead) subGrid._updateStyles(true); // re-create sub-grid styles now that we've moved
if (subGrid) {
subGrid.parentGridItem = node;
if (!subGrid.opts.styleInHead) subGrid._updateStyles(true); // re-create sub-grid styles now that we've moved
}
this._updateContainerHeight();
this.engine.addedNodes.push(node);// @ts-ignore
this._triggerAddEvent();// @ts-ignore
Expand Down
3 changes: 1 addition & 2 deletions src/gridstack-engine.ts
Expand Up @@ -648,8 +648,7 @@ export class GridStackEngine {
// check to make sure we actually collided over 50% surface area while dragging
let collide = activeDrag ? this.directionCollideCoverage(node, o, collides) : collides[0];
// if we're enabling creation of sub-grids on the fly, see if we're covering 80% of either one, if we didn't already do that
let opts = node.grid.opts;
if (activeDrag && collide && opts.subGridDynamic && !node.grid._isTemp) {
if (activeDrag && collide && node.grid?.opts?.subGridDynamic && !node.grid._isTemp) {
let over = Utils.areaIntercept(o.rect, collide._rect);
let a1 = Utils.area(o.rect);
let a2 = Utils.area(collide._rect);
Expand Down
48 changes: 27 additions & 21 deletions src/gridstack.ts
Expand Up @@ -157,10 +157,11 @@ export class GridStack {
/** grid options - public for classes to access, but use methods to modify! */
public opts: GridStackOptions;

/** point to a parent grid item if we're nested (inside a grid-item in between 2 Grids) */
public parentGridItem?: GridStackNode;

protected static engineClass: typeof GridStackEngine;

/** @internal point to a parent grid item if we're nested */
protected _isNested?: GridStackNode;
/** @internal unique class name for our generated CSS style sheet */
protected _styleSheetClass?: string;
/** @internal true if we got created by drag over gesture, so we can removed on drag out (temporary) */
Expand Down Expand Up @@ -271,12 +272,12 @@ export class GridStack {
}

// check if we're been nested, and if so update our style and keep pointer around (used during save)
let parentGridItemEl = Utils.closestByClass(this.el, gridDefaults.itemClass) as GridItemHTMLElement;
if (parentGridItemEl && parentGridItemEl.gridstackNode) {
this._isNested = parentGridItemEl.gridstackNode;
this._isNested.subGrid = this;
let parentGridItem = (Utils.closestUpByClass(this.el, gridDefaults.itemClass) as GridItemHTMLElement)?.gridstackNode;
if (parentGridItem) {
parentGridItem.subGrid = this;
this.parentGridItem = parentGridItem;
this.el.classList.add('grid-stack-nested');
parentGridItemEl.classList.add('grid-stack-sub-grid');
parentGridItem.el.classList.add('grid-stack-sub-grid');
}

this._isAutoCellHeight = (this.opts.cellHeight === 'auto');
Expand Down Expand Up @@ -476,6 +477,10 @@ export class GridStack {
newItemOpt = {...node, x:0, y:0};
Utils.removeInternalForSave(newItemOpt);
delete newItemOpt.subGrid;
if (node.content) {
newItemOpt.content = node.content;
delete node.content;
}
doc.body.innerHTML = `<div class="grid-stack-item-content"></div>`;
content = doc.body.children[0] as HTMLElement;
node.el.appendChild(content);
Expand Down Expand Up @@ -518,22 +523,23 @@ export class GridStack {
* to the original grid-item. Also called to remove empty sub-grids when last item is dragged out (since re-creating is simple)
*/
public removeAsSubGrid(nodeThatRemoved?: GridStackNode): void {
let parentGrid = this._isNested?.grid;
if (!parentGrid) return;
let pGrid = this.parentGridItem?.grid;
if (!pGrid) return;

parentGrid.batchUpdate();
parentGrid.removeWidget(this._isNested.el, true, true);
pGrid.batchUpdate();
pGrid.removeWidget(this.parentGridItem.el, true, true);
this.engine.nodes.forEach(n => {
// migrate any children over and offsetting by our location
n.x += this._isNested.x;
n.y += this._isNested.y;
parentGrid.addWidget(n.el, n);
n.x += this.parentGridItem.x;
n.y += this.parentGridItem.y;
pGrid.addWidget(n.el, n);
});
parentGrid.batchUpdate(false);
pGrid.batchUpdate(false);
delete this.parentGridItem;

// create an artificial event for the original grid now that this one is gone (got a leave, but won't get enter)
if (nodeThatRemoved) {
window.setTimeout(() => Utils.simulateMouseEvent(nodeThatRemoved._event, 'mouseenter', parentGrid.el), 0);
window.setTimeout(() => Utils.simulateMouseEvent(nodeThatRemoved._event, 'mouseenter', pGrid.el), 0);
}
}

Expand Down Expand Up @@ -838,7 +844,7 @@ export class GridStack {
}
this._removeStylesheet();
this.el.removeAttribute('gs-current-row');
delete this._isNested;
delete this.parentGridItem;
delete this.opts;
delete this._placeholder;
delete this.engine;
Expand Down Expand Up @@ -1449,10 +1455,10 @@ export class GridStack {
let changedColumn = false;

// see if we're nested and take our column count from our parent....
if (this._autoColumn && this._isNested) {
if (this.opts.column !== this._isNested.w) {
if (this._autoColumn && this.parentGridItem) {
if (this.opts.column !== this.parentGridItem.w) {
changedColumn = true;
this.column(this._isNested.w, 'none');
this.column(this.parentGridItem.w, 'none');
}
} else {
// else check for 1 column in/out behavior
Expand Down Expand Up @@ -1489,7 +1495,7 @@ export class GridStack {
/** add or remove the window size event handler */
protected _updateWindowResizeEvent(forceRemove = false): GridStack {
// only add event if we're not nested (parent will call us) and we're auto sizing cells or supporting oneColumn (i.e. doing work)
const workTodo = (this._isAutoCellHeight || !this.opts.disableOneColumnMode) && !this._isNested;
const workTodo = (this._isAutoCellHeight || !this.opts.disableOneColumnMode) && !this.parentGridItem;

if (!forceRemove && workTodo && !this._windowResizeBind) {
this._windowResizeBind = this.onParentResize.bind(this); // so we can properly remove later
Expand Down
6 changes: 4 additions & 2 deletions src/utils.ts
Expand Up @@ -282,10 +282,12 @@ export class Utils {
if (!n.noResize) delete n.noResize;
if (!n.noMove) delete n.noMove;
if (!n.locked) delete n.locked;
if (n.w === 1 || n.w === n.minW) delete n.w;
if (n.h === 1 || n.h === n.minH) delete n.h;
}

/** return the closest parent (or itself) matching the given class */
static closestByClass(el: HTMLElement, name: string): HTMLElement {
static closestUpByClass(el: HTMLElement, name: string): HTMLElement {
while (el) {
if (el.classList.contains(name)) return el;
el = el.parentElement
Expand Down Expand Up @@ -420,7 +422,7 @@ export class Utils {
*/
static cloneDeep<T>(obj: T): T {
// list of fields we will skip during cloneDeep (nested objects, other internal)
const skipFields = ['_isNested', 'el', 'grid', 'subGrid', 'engine'];
const skipFields = ['parentGrid', 'el', 'grid', 'subGrid', 'engine'];
// return JSON.parse(JSON.stringify(obj)); // doesn't work with date format ?
const ret = Utils.clone(obj);
for (const key in ret) {
Expand Down

0 comments on commit 576032a

Please sign in to comment.