Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

more nested grid fixes #2066

Merged
merged 1 commit into from
Oct 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 14 additions & 12 deletions demo/nested_advanced.html
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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