Skip to content

Commit 80f0e1f

Browse files
committed
using drag and drop to create nested Grids
fix #1009 remaining issues * dragging out last item will nuke empty nested grid * dragging over item to create nested grid, will revert back to normal item if you continue to drag out * fixed CSS issues dragging nested in/out couple times TODO: fix test cases, doc, more testing
1 parent 9f47613 commit 80f0e1f

File tree

10 files changed

+78
-48
lines changed

10 files changed

+78
-48
lines changed

demo/nested_advanced.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<div class="container-fluid">
1414
<h1>Advanced Nested grids demo</h1>
1515
<p>Create sub-grids (darker background) on the fly, by dragging items completely over others (nest) vs partially (push) using
16-
the new v7 API <code>GridStackOptions.subGrid.createDynamic=true</code></p>
16+
the new v7 API <code>GridStackOptions.subGridDynamic=true</code></p>
1717
<p>This will use the new delay drag&drop option <code>DDDragOpt.pause</code> to tell the gesture difference</p>
1818
<p>Note: <code>gridstack-extra.min.css</code> is required for [2-11] column of sub-grids</p>
1919
<a class="btn btn-primary" onClick="addNested()" href="#">Add Widget</a>
@@ -34,28 +34,28 @@ <h1>Advanced Nested grids demo</h1>
3434
</div>
3535

3636
<script type="text/javascript">
37-
let main = [{x:0, y:0}, {x:0, y:1}, {x:0, y:2}]
38-
let sub0 = [{x:0, y:0}];
37+
let main = [{x:0, y:0}, {x:0, y:1}, {x:1, y:0}]
38+
// let sub0 = [{x:0, y:0}];
3939
let sub1 = [{x:0, y:0}, {x:1, y:0}];
4040
let count = 0;
41-
[...main, ...sub0, ...sub1].forEach(d => d.content = String(count++));
41+
[...main, ...sub1].forEach(d => d.content = String(count++));
4242
let subOptions = {
4343
cellHeight: 50, // should be 50 - top/bottom
4444
column: 'auto', // size to match container. make sure to include gridstack-extra.min.css
4545
acceptWidgets: true, // will accept .grid-stack-item by default
46-
createDynamic: true, // NEW v7 api to create sub-grids on the fly
4746
margin: 5,
47+
subGridDynamic: true, // make it recursive for all future sub-grids
4848
};
4949
let options = { // main grid options
5050
cellHeight: 50,
5151
margin: 5,
5252
minRow: 2, // don't collapse when empty
5353
acceptWidgets: true,
54-
id: 'main',
5554
subGrid: subOptions,
55+
subGridDynamic: true, // NEW v7 api to create sub-grids on the fly
5656
children: [
5757
...main,
58-
{x:1, y:2, h:2, subGrid: {children: sub0, ...subOptions}},
58+
// {x:1, y:0, h:2, subGrid: {children: sub0, ...subOptions}},
5959
{x:2, y:0, w:2, h:3, subGrid: {children: sub1, ...subOptions}},
6060
// {x:2, y:0, w:2, h:3, subGrid: {children: [...sub1, {x:0, y:1, subGrid: subOptions}], ...subOptions}/*,content: "<div>nested grid here</div>"*/},
6161
]

doc/CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Change log
7474

7575
## 7-dev (TBD)
7676
* add [#1009](https://github.com/gridstack/gridstack.js/issues/1009) Create sub-grids on the fly,
77-
by dragging items completely over others (nest) vs partially (push) using new flag `GridStackOptions.subGrid.createDynamic=true`.
77+
by dragging items completely over others (nest) vs partially (push) using new flag `GridStackOptions.subGridDynamic=true`.
7878
Thank you [StephanP] for sponsoring it.<br>
7979
See [advance Nested](https://github.com/gridstack/gridstack.js/blob/master/demo/nested_advanced.html)
8080
* 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).

src/dd-droppable.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
99
import { Utils } from './utils';
1010
import { DDElementHost } from './dd-element';
1111
import { isTouch, pointerenter, pointerleave } from './dd-touch';
12-
import { GridHTMLElement } from './gridstack';
1312

1413
export interface DDDroppableOpt {
1514
accept?: string | ((el: HTMLElement) => boolean);

src/dd-gridstack.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
22
* dd-gridstack.ts 6.0.2-dev
3-
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
3+
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
44
*/
55

66
/* eslint-disable @typescript-eslint/no-unused-vars */
7-
import { GridItemHTMLElement, GridStackNode, GridStackElement, DDUIData, DDDragInOpt, GridStackPosition, dragInDefaultOptions } from './types';
7+
import { GridItemHTMLElement, GridStackNode, GridStackElement, DDUIData, DDDragInOpt, GridStackPosition, dragInDefaultOptions, GridStackOptions } from './types';
88
import { GridStack } from './gridstack';
99
import { Utils } from './utils';
1010
import { DDManager } from './dd-manager';
@@ -311,6 +311,10 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
311311
// so skip this one if we're not the active grid really..
312312
if (!node.grid || node.grid === this) {
313313
this._leave(el, helper);
314+
// if we were created as temporary nested grid, go back to before state
315+
if (this._isTemp) {
316+
this.removeAsSubGrid(node);
317+
}
314318
}
315319
return false; // prevent parent from receiving msg (which may be grid as well)
316320
})
@@ -333,6 +337,10 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
333337
let oGrid = origNode.grid;
334338
oGrid.engine.removedNodes.push(origNode);
335339
oGrid._triggerRemoveEvent();
340+
// if it's an empty sub-grid, nuke it
341+
if (oGrid._isNested && !oGrid.engine.nodes.length) {
342+
oGrid.removeAsSubGrid();
343+
}
336344
}
337345

338346
if (!node) return false;
@@ -358,13 +366,13 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
358366
if (!wasAdded) return false;
359367
el.gridstackNode = node;
360368
node.el = el;
369+
let subGrid = (node.subGrid as GridStack)?.el?.gridstack; // set when actual sub-grid present
361370
// @ts-ignore
362371
Utils.copyPos(node, this._readAttr(this.placeholder)); // placeholder values as moving VERY fast can throw things off #1578
363372
Utils.removePositioningStyles(el);// @ts-ignore
364373
this._writeAttr(el, node);
365374
this.el.appendChild(el);// @ts-ignore // TODO: now would be ideal time to _removeHelperStyle() overriding floating styles (native only)
366-
let subGrid: GridStack = node.subGrid;
367-
if (subGrid?.el && !subGrid.opts.styleInHead) subGrid._updateStyles(true); // re-create sub-grid styles now that we've moved
375+
if (subGrid && !subGrid.opts.styleInHead) subGrid._updateStyles(true); // re-create sub-grid styles now that we've moved
368376
this._updateContainerHeight();
369377
this.engine.addedNodes.push(node);// @ts-ignore
370378
this._triggerAddEvent();// @ts-ignore
@@ -383,6 +391,7 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
383391
} else {
384392
this.engine.removeNode(node);
385393
}
394+
delete node.grid._isTemp;
386395
});
387396

388397
return false; // prevent parent from receiving msg (which may be grid as well)

src/dd-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* dd-manager.ts 6.0.2-dev
3-
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
3+
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
44
*/
55

66
import { DDDraggable } from './dd-draggable';

src/gridstack-engine.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { Utils } from './utils';
7-
import { GridStackNode, ColumnOptions, GridStackPosition, GridStackMoveOpts } from './types';
7+
import { GridStackNode, ColumnOptions, GridStackPosition, GridStackMoveOpts, GridStackOptions } from './types';
88

99
/** callback to update the DOM attributes since this class is generic (no HTML or other info) for items that changed - see _notify() */
1010
type OnChangeCB = (nodes: GridStackNode[]) => void;
@@ -44,7 +44,7 @@ export class GridStackEngine {
4444
/** @internal true if we have some items locked */
4545
protected _hasLocked: boolean;
4646
/** @internal unique global internal _id counter NOT starting at 0 */
47-
protected static _idSeq = 1;
47+
public static _idSeq = 1;
4848

4949
public constructor(opts: GridStackEngineOptions = {}) {
5050
this.column = opts.column || 12;
@@ -648,8 +648,8 @@ export class GridStackEngine {
648648
// check to make sure we actually collided over 50% surface area while dragging
649649
let collide = activeDrag ? this.directionCollideCoverage(node, o, collides) : collides[0];
650650
// 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
651-
let subOpt = node.grid.opts.subGrid;
652-
if (activeDrag && collide && subOpt?.createDynamic && !subOpt.isTemp) {
651+
let opts = node.grid.opts;
652+
if (activeDrag && collide && opts.subGridDynamic && !node.grid._isTemp) {
653653
let over = Utils.areaIntercept(o.rect, collide._rect);
654654
let a1 = Utils.area(o.rect);
655655
let a2 = Utils.area(collide._rect);

src/gridstack.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* gridstack SASS styles 6.0.2-dev
3-
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
3+
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
44
*/
55

66
@use "sass:math";

src/gridstack.ts

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { GridStackEngine } from './gridstack-engine';
99
import { Utils, HeightData, obsolete } from './utils';
1010
import { gridDefaults, ColumnOptions, GridItemHTMLElement, GridStackElement, GridStackEventHandlerCallback,
11-
GridStackNode, GridStackOptions, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition, GridStackSubOptions } from './types';
11+
GridStackNode, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition, GridStackOptions } from './types';
1212

1313
// export all dependent file as well to make it easier for users to just import the main file
1414
export * from './types';
@@ -35,8 +35,7 @@ export interface CellPosition {
3535
}
3636

3737
interface GridCSSStyleSheet extends CSSStyleSheet {
38-
_id?: string; // random id we will use to style us
39-
_max?: number; // internal tracker of the max # of rows we created\
38+
_max?: number; // internal tracker of the max # of rows we created
4039
}
4140

4241
/**
@@ -164,6 +163,9 @@ export class GridStack {
164163
protected _isNested?: GridStackNode;
165164
/** @internal unique class name for our generated CSS style sheet */
166165
protected _styleSheetClass?: string;
166+
/** @internal true if we got created by drag over gesture, so we can removed on drag out (temporary) */
167+
public _isTemp?: boolean;
168+
167169

168170
/** @internal create placeholder DIV as needed */
169171
public get placeholder(): HTMLElement {
@@ -295,7 +297,7 @@ export class GridStack {
295297
this.opts.alwaysShowResizeHandle = isTouch;
296298
}
297299

298-
this._styleSheetClass = 'grid-stack-instance-' + (Math.random() * 10000).toFixed(0)
300+
this._styleSheetClass = 'grid-stack-instance-' + GridStackEngine._idSeq++;
299301
this.el.classList.add(this._styleSheetClass);
300302

301303
this._setStaticClass();
@@ -351,7 +353,7 @@ export class GridStack {
351353
delete this.opts.dragInOptions;
352354

353355
// dynamic grids require pausing during drag to detect over to nest vs push
354-
if (this.opts.subGrid?.createDynamic && !DDManager.pauseDrag) DDManager.pauseDrag = true;
356+
if (this.opts.subGridDynamic && !DDManager.pauseDrag) DDManager.pauseDrag = true;
355357
if (this.opts.draggable?.pause !== undefined) DDManager.pauseDrag = this.opts.draggable.pause;
356358

357359
this._setupRemoveDrop();
@@ -440,15 +442,17 @@ export class GridStack {
440442
* @param el gridItem element to convert
441443
* @param ops (optional) sub-grid options, else default to node, then parent settings, else defaults
442444
* @param nodeToAdd (optional) node to add to the newly created sub grid (used when dragging over existing regular item)
445+
* @returns newly created grid
443446
*/
444-
public makeSubGrid(el: GridItemHTMLElement, ops?: GridStackSubOptions, nodeToAdd?: GridStackNode, saveContent = true): GridStack {
447+
public makeSubGrid(el: GridItemHTMLElement, ops?: GridStackOptions, nodeToAdd?: GridStackNode, saveContent = true): GridStack {
445448
let node = el.gridstackNode;
446449
if (!node) {
447450
node = this.makeWidget(el).gridstackNode;
448451
}
449452
if ((node.subGrid as GridStack)?.el) return node.subGrid as GridStack; // already done
450453

451-
ops = Utils.cloneDeep(ops || node.subGrid as GridStackOptions || this.opts.subGrid || this.opts);
454+
ops = Utils.cloneDeep(ops || node.subGrid as GridStackOptions || {...this.opts.subGrid, children: undefined});
455+
ops.subGrid = Utils.cloneDeep(ops); // carry nesting settings to next one down
452456
node.subGrid = ops;
453457

454458
// if column special case it set, remember that flag and set default
@@ -486,27 +490,51 @@ export class GridStack {
486490
style.transition = 'none'; // show up instantly so we don't see scrollbar with nodeToAdd
487491
this.update(node.el, {w, h});
488492
setTimeout(() => style.transition = null); // recover animation
489-
ops.isTemp = true; // prevent re-nesting as we add over
490493
}
491494

492-
let grid = node.subGrid = GridStack.addGrid(content, ops);
493-
if (autoColumn) node.subGrid._autoColumn = true;
495+
let subGrid = node.subGrid = GridStack.addGrid(content, ops);
496+
if (nodeToAdd?._moving) subGrid._isTemp = true; // prevent re-nesting as we add over
497+
if (autoColumn) subGrid._autoColumn = true;
494498

495499
// add the original content back as a child of hte newly created grid
496500
if (saveContent) {
497-
grid.addWidget(newItem, newItemOpt);
501+
subGrid.addWidget(newItem, newItemOpt);
498502
}
499503

500504
// now add any additional node
501505
if (nodeToAdd) {
502506
if (nodeToAdd._moving) {
503507
// create an artificial event even for the just created grid to receive this item
504-
window.setTimeout(() => Utils.simulateMouseEvent(nodeToAdd._event, 'mouseenter', grid.el), 0);
508+
window.setTimeout(() => Utils.simulateMouseEvent(nodeToAdd._event, 'mouseenter', subGrid.el), 0);
505509
} else {
506-
grid.addWidget(node.el, node);
510+
subGrid.addWidget(node.el, node);
507511
}
508512
}
509-
return grid;
513+
return subGrid;
514+
}
515+
516+
/**
517+
* called when an item was converted into a nested grid to accommodate a dragged over item, but then item leaves - return back
518+
* to the original grid-item. Also called to remove empty sub-grids when last item is dragged out (since re-creating is simple)
519+
*/
520+
public removeAsSubGrid(nodeThatRemoved?: GridStackNode): void {
521+
let parentGrid = this._isNested?.grid;
522+
if (!parentGrid) return;
523+
524+
parentGrid.batchUpdate();
525+
parentGrid.removeWidget(this._isNested.el, true, true);
526+
this.engine.nodes.forEach(n => {
527+
// migrate any children over and offsetting by our location
528+
n.x += this._isNested.x;
529+
n.y += this._isNested.y;
530+
parentGrid.addWidget(n.el, n);
531+
});
532+
parentGrid.batchUpdate(false);
533+
534+
// create an artificial event for the original grid now that this one is gone (got a leave, but won't get enter)
535+
if (nodeThatRemoved) {
536+
window.setTimeout(() => Utils.simulateMouseEvent(nodeThatRemoved._event, 'mouseenter', parentGrid.el), 0);
537+
}
510538
}
511539

512540
/**
@@ -712,7 +740,7 @@ export class GridStack {
712740
this.opts.cellHeight = data.h;
713741

714742
if (update) {
715-
this._updateStyles(true, this.getRow()); // true = force re-create, for that # of rows
743+
this._updateStyles(true); // true = force re-create for current # of rows
716744
}
717745
return this;
718746
}
@@ -1218,7 +1246,7 @@ export class GridStack {
12181246
protected _removeStylesheet(): GridStack {
12191247

12201248
if (this._styles) {
1221-
Utils.removeStylesheet(this._styles._id);
1249+
Utils.removeStylesheet(this._styleSheetClass);
12221250
delete this._styles;
12231251
}
12241252
return this;
@@ -1231,6 +1259,7 @@ export class GridStack {
12311259
this._removeStylesheet();
12321260
}
12331261

1262+
if (!maxH) maxH = this.getRow();
12341263
this._updateContainerHeight();
12351264

12361265
// if user is telling us they will handle the CSS themselves by setting heights to 0. Do we need this opts really ??
@@ -1244,12 +1273,10 @@ export class GridStack {
12441273

12451274
// create one as needed
12461275
if (!this._styles) {
1247-
let id = 'gridstack-style-' + (Math.random() * 100000).toFixed();
12481276
// insert style to parent (instead of 'head' by default) to support WebComponent
12491277
let styleLocation = this.opts.styleInHead ? undefined : this.el.parentNode as HTMLElement;
1250-
this._styles = Utils.createStylesheet(id, styleLocation);
1278+
this._styles = Utils.createStylesheet(this._styleSheetClass, styleLocation);
12511279
if (!this._styles) return this;
1252-
this._styles._id = id;
12531280
this._styles._max = 0;
12541281

12551282
// these are done once only

src/types.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* types.ts 6.0.2-dev
3-
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
3+
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
44
*/
55

66
import { GridStack } from './gridstack';
@@ -239,15 +239,10 @@ export interface GridStackOptions {
239239
styleInHead?: boolean;
240240

241241
/** list of differences in options for automatically created sub-grids under us */
242-
subGrid?: GridStackSubOptions;
243-
}
242+
subGrid?: GridStackOptions;
244243

245-
/** additional prop that only apply to sub-grids */
246-
export interface GridStackSubOptions extends GridStackOptions {
247244
/** enable/disable the creation of sub-grids on the fly (drop over other items) */
248-
createDynamic?: boolean;
249-
/** true if we got created by drag over gesture, so we can removed on drag out (temporary) */
250-
isTemp?: boolean;
245+
subGridDynamic?: boolean;
251246
}
252247

253248
/** options used during GridStackEngine.moveNode() */
@@ -311,7 +306,7 @@ export interface GridStackWidget extends GridStackPosition {
311306
/** html to append inside as content */
312307
content?: string;
313308
/** optional nested grid options and list of children, which then turns into actual instance at runtime */
314-
subGrid?: GridStackSubOptions | GridStack;
309+
subGrid?: GridStackOptions | GridStack;
315310
}
316311

317312
/** Drag&Drop resize options */

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* utils.ts 6.0.2-dev
3-
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
3+
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
44
*/
55

66
import { GridStackElement, GridStackNode, GridStackOptions, numberOrString, GridStackPosition, GridStackWidget } from './types';

0 commit comments

Comments
 (0)