Skip to content

Commit

Permalink
using drag and drop to create nested Grids
Browse files Browse the repository at this point in the history
fix gridstack#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
  • Loading branch information
adumesny committed Sep 26, 2022
1 parent 9f47613 commit 80f0e1f
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 48 deletions.
14 changes: 7 additions & 7 deletions demo/nested_advanced.html
Expand Up @@ -13,7 +13,7 @@
<div class="container-fluid">
<h1>Advanced Nested grids demo</h1>
<p>Create sub-grids (darker background) on the fly, by dragging items completely over others (nest) vs partially (push) using
the new v7 API <code>GridStackOptions.subGrid.createDynamic=true</code></p>
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>
Expand All @@ -34,28 +34,28 @@ <h1>Advanced Nested grids demo</h1>
</div>

<script type="text/javascript">
let main = [{x:0, y:0}, {x:0, y:1}, {x:0, y:2}]
let sub0 = [{x:0, y:0}];
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, ...sub0, ...sub1].forEach(d => d.content = String(count++));
[...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
createDynamic: true, // NEW v7 api to create sub-grids on the fly
margin: 5,
subGridDynamic: true, // make it recursive for all future sub-grids
};
let options = { // main grid options
cellHeight: 50,
margin: 5,
minRow: 2, // don't collapse when empty
acceptWidgets: true,
id: 'main',
subGrid: subOptions,
subGridDynamic: true, // NEW v7 api to create sub-grids on the fly
children: [
...main,
{x:1, y:2, h:2, subGrid: {children: sub0, ...subOptions}},
// {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: [...sub1, {x:0, y:1, subGrid: subOptions}], ...subOptions}/*,content: "<div>nested grid here</div>"*/},
]
Expand Down
2 changes: 1 addition & 1 deletion doc/CHANGES.md
Expand Up @@ -74,7 +74,7 @@ Change log

## 7-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.subGrid.createDynamic=true`.
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).
Expand Down
1 change: 0 additions & 1 deletion src/dd-droppable.ts
Expand Up @@ -9,7 +9,6 @@ import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
import { Utils } from './utils';
import { DDElementHost } from './dd-element';
import { isTouch, pointerenter, pointerleave } from './dd-touch';
import { GridHTMLElement } from './gridstack';

export interface DDDroppableOpt {
accept?: string | ((el: HTMLElement) => boolean);
Expand Down
17 changes: 13 additions & 4 deletions src/dd-gridstack.ts
@@ -1,10 +1,10 @@
/**
* dd-gridstack.ts 6.0.2-dev
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
*/

/* eslint-disable @typescript-eslint/no-unused-vars */
import { GridItemHTMLElement, GridStackNode, GridStackElement, DDUIData, DDDragInOpt, GridStackPosition, dragInDefaultOptions } from './types';
import { GridItemHTMLElement, GridStackNode, GridStackElement, DDUIData, DDDragInOpt, GridStackPosition, dragInDefaultOptions, GridStackOptions } from './types';
import { GridStack } from './gridstack';
import { Utils } from './utils';
import { DDManager } from './dd-manager';
Expand Down Expand Up @@ -311,6 +311,10 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
// so skip this one if we're not the active grid really..
if (!node.grid || node.grid === this) {
this._leave(el, helper);
// if we were created as temporary nested grid, go back to before state
if (this._isTemp) {
this.removeAsSubGrid(node);
}
}
return false; // prevent parent from receiving msg (which may be grid as well)
})
Expand All @@ -333,6 +337,10 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
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) {
oGrid.removeAsSubGrid();
}
}

if (!node) return false;
Expand All @@ -358,13 +366,13 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
if (!wasAdded) return false;
el.gridstackNode = node;
node.el = el;
let subGrid = (node.subGrid as GridStack)?.el?.gridstack; // set when actual sub-grid present
// @ts-ignore
Utils.copyPos(node, this._readAttr(this.placeholder)); // placeholder values as moving VERY fast can throw things off #1578
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)
let subGrid: GridStack = node.subGrid;
if (subGrid?.el && !subGrid.opts.styleInHead) subGrid._updateStyles(true); // re-create sub-grid styles now that we've moved
if (subGrid && !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 All @@ -383,6 +391,7 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
} else {
this.engine.removeNode(node);
}
delete node.grid._isTemp;
});

return false; // prevent parent from receiving msg (which may be grid as well)
Expand Down
2 changes: 1 addition & 1 deletion src/dd-manager.ts
@@ -1,6 +1,6 @@
/**
* dd-manager.ts 6.0.2-dev
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
*/

import { DDDraggable } from './dd-draggable';
Expand Down
8 changes: 4 additions & 4 deletions src/gridstack-engine.ts
Expand Up @@ -4,7 +4,7 @@
*/

import { Utils } from './utils';
import { GridStackNode, ColumnOptions, GridStackPosition, GridStackMoveOpts } from './types';
import { GridStackNode, ColumnOptions, GridStackPosition, GridStackMoveOpts, GridStackOptions } from './types';

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

public constructor(opts: GridStackEngineOptions = {}) {
this.column = opts.column || 12;
Expand Down Expand Up @@ -648,8 +648,8 @@ 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 subOpt = node.grid.opts.subGrid;
if (activeDrag && collide && subOpt?.createDynamic && !subOpt.isTemp) {
let opts = node.grid.opts;
if (activeDrag && collide && 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
2 changes: 1 addition & 1 deletion src/gridstack.scss
@@ -1,6 +1,6 @@
/**
* gridstack SASS styles 6.0.2-dev
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
*/

@use "sass:math";
Expand Down
65 changes: 46 additions & 19 deletions src/gridstack.ts
Expand Up @@ -8,7 +8,7 @@
import { GridStackEngine } from './gridstack-engine';
import { Utils, HeightData, obsolete } from './utils';
import { gridDefaults, ColumnOptions, GridItemHTMLElement, GridStackElement, GridStackEventHandlerCallback,
GridStackNode, GridStackOptions, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition, GridStackSubOptions } from './types';
GridStackNode, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition, GridStackOptions } from './types';

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

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

/**
Expand Down Expand Up @@ -164,6 +163,9 @@ export class GridStack {
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) */
public _isTemp?: boolean;


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

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

this._setStaticClass();
Expand Down Expand Up @@ -351,7 +353,7 @@ export class GridStack {
delete this.opts.dragInOptions;

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

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

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

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

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

// add the original content back as a child of hte newly created grid
if (saveContent) {
grid.addWidget(newItem, newItemOpt);
subGrid.addWidget(newItem, newItemOpt);
}

// now add any additional node
if (nodeToAdd) {
if (nodeToAdd._moving) {
// create an artificial event even for the just created grid to receive this item
window.setTimeout(() => Utils.simulateMouseEvent(nodeToAdd._event, 'mouseenter', grid.el), 0);
window.setTimeout(() => Utils.simulateMouseEvent(nodeToAdd._event, 'mouseenter', subGrid.el), 0);
} else {
grid.addWidget(node.el, node);
subGrid.addWidget(node.el, node);
}
}
return grid;
return subGrid;
}

/**
* called when an item was converted into a nested grid to accommodate a dragged over item, but then item leaves - return back
* 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;

parentGrid.batchUpdate();
parentGrid.removeWidget(this._isNested.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);
});
parentGrid.batchUpdate(false);

// 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);
}
}

/**
Expand Down Expand Up @@ -712,7 +740,7 @@ export class GridStack {
this.opts.cellHeight = data.h;

if (update) {
this._updateStyles(true, this.getRow()); // true = force re-create, for that # of rows
this._updateStyles(true); // true = force re-create for current # of rows
}
return this;
}
Expand Down Expand Up @@ -1218,7 +1246,7 @@ export class GridStack {
protected _removeStylesheet(): GridStack {

if (this._styles) {
Utils.removeStylesheet(this._styles._id);
Utils.removeStylesheet(this._styleSheetClass);
delete this._styles;
}
return this;
Expand All @@ -1231,6 +1259,7 @@ export class GridStack {
this._removeStylesheet();
}

if (!maxH) maxH = this.getRow();
this._updateContainerHeight();

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

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

// these are done once only
Expand Down
13 changes: 4 additions & 9 deletions src/types.ts
@@ -1,6 +1,6 @@
/**
* types.ts 6.0.2-dev
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
*/

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

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

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

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

/** Drag&Drop resize options */
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
@@ -1,6 +1,6 @@
/**
* utils.ts 6.0.2-dev
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
* Copyright (c) 2021-2022 Alain Dumesny - see GridStack root license
*/

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

0 comments on commit 80f0e1f

Please sign in to comment.