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

using drag and drop to create nested Grids #2053

Merged
merged 1 commit into from Sep 26, 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
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