Skip to content

Commit

Permalink
nested grid in/out drag from parent support
Browse files Browse the repository at this point in the history
* fix gridstack#992
* we now support dragging into and out of nested grids from parents
* nested.html was updated to showcase this, settings all grids to accept the same widgets. using CSS to differentiate nested items vs not for styling/demo purpose only.
* tested nested, float nad two.html - all seem to continue working (this was a lot of work to fine tune)

* also fix gridstack#1558 as we no longer cache the grid position (as we may move when items are placed elsewhere) but get it on every move to reflect latest data

Thank you [@arclogos132](https://github.com/arclogos132) for sponsoring it.
  • Loading branch information
adumesny committed Dec 29, 2021
1 parent 003e8b9 commit 98836f6
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 56 deletions.
46 changes: 28 additions & 18 deletions demo/nested.html
Expand Up @@ -15,13 +15,18 @@
.grid-stack .grid-stack .grid-stack-item-content {
background: lightpink;
}
/* make nested grid take entire item content */
.grid-stack-item-content .grid-stack {
min-height: 100%;
min-width: 100%;
}
</style>
</head>
<body>
<div class="container-fluid">
<h1>Nested grids demo</h1>
<p>This example uses new v3.1 API to load the entire nested grid from JSON, and shows dragging between nested grid items (pink) vs dragging higher grid items (green)</p>
<p>Note: HTML5 release doesn't yet support 'dragOut:false' constrain so use JQ version if you need that.</p>
<p>This example uses new v3.1 API to load the entire nested grid from JSON, and shows dragging between nested grid items (pink) vs dragging higher items (green)</p>
<p>Note: HTML5 release doesn't yet support 'dragOut:false' constrain so use JQ version if you need that (nested 2 case).</p>
<a class="btn btn-primary" onClick="addNested()" href="#">Add Widget</a>
<a class="btn btn-primary" onClick="addNewWidget('.nested1')" href="#">Add Widget Grid1</a>
<a class="btn btn-primary" onClick="addNewWidget('.nested2')" href="#">Add Widget Grid2</a>
Expand All @@ -46,22 +51,27 @@ <h1>Nested grids demo</h1>
let subOptions = {
cellHeight: 30,
column: 4, // make sure to include gridstack-extra.min.css
itemClass: 'sub', // style sub items differently and use to prevent dragging in/out
acceptWidgets: '.grid-stack-item.sub', // only pink sub items can be inserted, otherwise grid-items causes all sort of issues
minWidth: 300, // min to go 1 column mode
margin: 1
acceptWidgets: true, // will accept .grid-stack-item by default
minWidth: 300, // min to go 1 column mode (much smaller than default)
margin: 2
};
let options = { // main grid options
cellHeight: 70,
minRow: 2, // don't collapse when empty
acceptWidgets: true,
id: 'main',
children: [
{y:0, content: 'regular item'},
{x:1, w:4, h:4, subGrid: {children: sub1, dragOut: true, id: 'sub1', ...subOptions}},
{x:5, w:4, h:4, subGrid: {children: sub2, id: 'sub2', ...subOptions}},
]
};
let json = {cellHeight: 70, minRow: 2, children: [
{y:0, content: 'regular item'},
{x:1, w:4, h:4, content: 'nested 1 - can drag items out', subGrid: {children: sub1, dragOut: true, class: 'nested1', ...subOptions}},
{x:5, w:4, h:4, content: 'nested 2 - constrained to parent (default)', subGrid: {children: sub2, class: 'nested2', ...subOptions}},
]};

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

addNested = function() {
grid.addWidget({x:0, y:0, w:3, h:3, content:"nested add", subGrid: {children: sub1, dragOut: true, class: 'nested1', ...subOptions}});
grid.addWidget({x:0, y:0, content:"new item"});
}

addNewWidget = function(selector) {
Expand All @@ -78,9 +88,9 @@ <h1>Nested grids demo</h1>
};

save = function(content = true, full = true) {
json = grid.save(content, full);
console.log(json);
// console.log(JSON.stringify(json));
options = grid.save(content, full);
console.log(options);
// console.log(JSON.stringify(options));
}
destroy = function(full = true) {
if (full) {
Expand All @@ -92,9 +102,9 @@ <h1>Nested grids demo</h1>
}
load = function(full = true) {
if (full) {
grid = GridStack.addGrid(document.querySelector('.container-fluid'), json);
grid = GridStack.addGrid(document.querySelector('.container-fluid'), options);
} else {
grid.load(json);
grid.load(options);
}
}

Expand Down
2 changes: 2 additions & 0 deletions doc/CHANGES.md
Expand Up @@ -67,7 +67,9 @@ Change log
<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 4.4.1-dev (TBD)
* add [#992](https://github.com/gridstack/gridstack.js/issues/992) support dragging into and out of nested grids from parents! Thank you [@arclogos132](https://github.com/arclogos132) for sponsoring it.
* fix [#1902](https://github.com/gridstack/gridstack.js/pull/1902) nested.html: dragging between sub-grids show items clipped
* fix [#1558](https://github.com/gridstack/gridstack.js/issues/1558) dragging between vertical grids causes too much growth, not follow mouse.

## 4.4.1 (2021-12-24)
* fix [#1901](https://github.com/gridstack/gridstack.js/pull/1901) error introduced for #1785 when re-loading with fewer objects
Expand Down
44 changes: 44 additions & 0 deletions spec/e2e/html/1558-vertical-grids-scroll-too-much.html
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>disable move after</title>

<link rel="stylesheet" href="../../../demo/demo.css"/>
<script src="../../../dist/gridstack-h5.js"></script>

</head>
<body>
<div class="container-fluid">
<h1>#1558 items moves too much</h1>
<div class="grid-stack">
<div class="grid-stack-item" gs-x="0" gs-y="0" gs-w="2" gs-h="1">
<div class="grid-stack-item-content">item1 </div>
</div>
<div class="grid-stack-item" gs-x="3" gs-y="1" gs-w="2" gs-h="1">
<div class="grid-stack-item-content">item2</div>
</div>
</div>
<br>
<div class="grid-stack">
<div class="grid-stack-item" gs-x="0" gs-y="0" gs-w="2" gs-h="1">
<div class="grid-stack-item-content">item1 </div>
</div>
<div class="grid-stack-item" gs-x="0" gs-y="1" gs-w="2" gs-h="1">
<div class="grid-stack-item-content">item2</div>
</div>
</div>
</div>
<script src="../../../demo/events.js"></script>
<script type="text/javascript">
var options = {
float: true,
acceptWidgets: true,
cellHeight: 80
};
GridStack.initAll(options);
</script>
</body>
</html>
19 changes: 10 additions & 9 deletions src/gridstack-dd.ts
Expand Up @@ -24,6 +24,8 @@ export type DDValue = number | string;
/** drag&drop events callbacks */
export type DDCallback = (event: Event, arg2: GridItemHTMLElement, helper?: GridItemHTMLElement) => void;

// TEST let count = 0;

/**
* Base class implementing common Grid drag'n'drop functionality, with domain specific subclass (h5 vs jq subclasses)
*/
Expand Down Expand Up @@ -67,7 +69,6 @@ export abstract class GridStackDD extends GridStackDDI {
/********************************************************************************
* GridStack code that is doing drag&drop extracted here so main class is smaller
* for static grid that don't do any of this work anyway. Saves about 10k.
* TODO: no code hint in code below as this is <any> so look at alternatives ?
* https://www.typescriptlang.org/docs/handbook/declaration-merging.html
* https://www.typescriptlang.org/docs/handbook/mixins.html
********************************************************************************/
Expand All @@ -82,17 +83,17 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
}

// vars shared across all methods
let gridPos: MousePosition;
let cellHeight: number, cellWidth: number;

let onDrag = (event: DragEvent, el: GridItemHTMLElement, helper: GridItemHTMLElement) => {
let node = el.gridstackNode;
if (!node) return;

helper = helper || el;
let rec = helper.getBoundingClientRect();
let left = rec.left - gridPos.left;
let top = rec.top - gridPos.top;
let parent = this.el.getBoundingClientRect();
let {top, left} = helper.getBoundingClientRect();
left -= parent.left;
top -= parent.top;
let ui: DDUIData = {position: {top, left}};

if (node._temporaryRemoved) {
Expand Down Expand Up @@ -150,6 +151,7 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
* entering our grid area
*/
.on(this.el, 'dropover', (event: Event, el: GridItemHTMLElement, helper: GridItemHTMLElement) => {
// TEST console.log(`over ${this.el.gridstack.opts.id} ${count++}`);
let node = el.gridstackNode;
// ignore drop enter on ourself (unless we temporarily removed) which happens on a simple drag of our item
if (node?.grid === this && !node._temporaryRemoved) {
Expand All @@ -164,14 +166,12 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
otherGrid._leave(el, helper);
}

// get grid screen coordinates and cell dimensions
let box = this.el.getBoundingClientRect();
gridPos = {top: box.top, left: box.left};
// cache cell dimensions (which don't change), position can animate if we removed an item in otherGrid that affects us...
cellWidth = this.cellWidth();
cellHeight = this.getCellHeight(true);

// load any element attributes if we don't have a node
if (!node) {// @ts-ignore
if (!node) {// @ts-ignore private read only on ourself
node = this._readAttr(el);
}
if (!node.grid) {
Expand Down Expand Up @@ -213,6 +213,7 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
* Leaving our grid area...
*/
.on(this.el, 'dropout', (event, el: GridItemHTMLElement, helper: GridItemHTMLElement) => {
// TEST console.log(`out ${this.el.gridstack.opts.id} ${count++}`);
let node = el.gridstackNode;
if (!node) return false;
// fix #1578 when dragging fast, we might get leave after other grid gets enter (which calls us to clean)
Expand Down
7 changes: 4 additions & 3 deletions src/gridstack.scss
Expand Up @@ -126,7 +126,8 @@ $animation_speed: .3s !default;
}

// without this, the html5 drag will flicker between no-drop and drop when dragging over second grid
&.ui-droppable.ui-droppable-over > *:not(.ui-droppable) {
pointer-events: none;
}
// Update: removed that as it causes nested grids to no receive dragenter events when parent drags and sets this for #992. not seeing cursor flicker (chrome).
// &.ui-droppable.ui-droppable-over > *:not(.ui-droppable) {
// pointer-events: none;
// }
}
65 changes: 42 additions & 23 deletions src/h5/dd-droppable.ts
Expand Up @@ -7,6 +7,7 @@ import { DDDraggable } from './dd-draggable';
import { DDManager } from './dd-manager';
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
import { DDUtils } from './dd-utils';
import { GridHTMLElement, GridStack } from '../gridstack';

export interface DDDroppableOpt {
accept?: string | ((el: HTMLElement) => boolean);
Expand All @@ -15,6 +16,8 @@ export interface DDDroppableOpt {
out?: (event: DragEvent, ui) => void;
}

// TEST let count = 0;

export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt<DDDroppableOpt> {

public accept: (el: HTMLElement) => boolean;
Expand All @@ -23,6 +26,7 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt

/** @internal */
private moving: boolean;
private static lastActive: DDDroppable;

constructor(el: HTMLElement, opts: DDDroppableOpt = {}) {
super();
Expand Down Expand Up @@ -62,13 +66,10 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt
}

public destroy(): void {
if (this.moving) {
this._removeLeaveCallbacks();
}
this._removeLeaveCallbacks();
this.disable(true);
this.el.classList.remove('ui-droppable');
this.el.classList.remove('ui-droppable-disabled');
delete this.moving;
super.destroy();
}

Expand All @@ -80,10 +81,13 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt

/** @internal called when the cursor enters our area - prepare for a possible drop and track leaving */
private _dragEnter(event: DragEvent): void {
// TEST console.log(`${count++} Enter ${(this.el as GridHTMLElement).gridstack.opts.id}`);
if (!this._canDrop()) return;
event.preventDefault();
event.stopPropagation();

if (this.moving) return; // ignore multiple 'dragenter' as we go over existing items
// ignore multiple 'dragenter' as we go over existing items
if (this.moving) return;
this.moving = true;

const ev = DDUtils.initEvent<DragEvent>(event, { target: this.el, type: 'dropover' });
Expand All @@ -94,7 +98,14 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt
this.el.addEventListener('dragover', this._dragOver);
this.el.addEventListener('drop', this._drop);
this.el.addEventListener('dragleave', this._dragLeave);
this.el.classList.add('ui-droppable-over');
// Update: removed that as it causes nested grids to no receive dragenter events when parent drags and sets this for #992. not seeing cursor flicker (chrome).
// this.el.classList.add('ui-droppable-over');

// make sure when we enter this, that the last one gets a leave to correctly cleanup as we don't always do
if (DDDroppable.lastActive && DDDroppable.lastActive !== this) {
DDDroppable.lastActive._dragLeave(event, true);
}
DDDroppable.lastActive = this;
}

/** @internal called when an moving to drop item is being dragged over - do nothing but eat the event */
Expand All @@ -104,25 +115,34 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt
}

/** @internal called when the item is leaving our area, stop tracking if we had moving item */
private _dragLeave(event: DragEvent): void {
private _dragLeave(event: DragEvent, forceLeave?: boolean): void {
// TEST console.log(`${count++} Leave ${(this.el as GridHTMLElement).gridstack.opts.id}`);
event.preventDefault();
event.stopPropagation();

// ignore leave events on our children (get when starting to drag our items)
// Note: Safari Mac has null relatedTarget which causes #1684 so check if DragEvent is inside the grid instead
if (!event.relatedTarget) {
const { bottom, left, right, top } = this.el.getBoundingClientRect();
if (event.x < right && event.x > left && event.y < bottom && event.y > top) return;
} else if (this.el.contains(event.relatedTarget as HTMLElement)) return;
// ignore leave events on our children (we get them when starting to drag our items)
// but exclude nested grids since we would still be leaving ourself
if (!forceLeave) {
let onChild = DDUtils.inside(event, this.el);
if (onChild) {
let nestedEl = (this.el as GridHTMLElement).gridstack.engine.nodes.filter(n => n.subGrid).map(n => (n.subGrid as GridStack).el);
onChild = !nestedEl.some(el => DDUtils.inside(event, el));
}
if (onChild) return;
}

this._removeLeaveCallbacks();
if (this.moving) {
event.preventDefault();
const ev = DDUtils.initEvent<DragEvent>(event, { target: this.el, type: 'dropout' });
if (this.option.out) {
this.option.out(ev, this._ui(DDManager.dragElement))
}
this.triggerEvent('dropout', ev);
}
delete this.moving;
this._removeLeaveCallbacks();

if (DDDroppable.lastActive === this) {
delete DDDroppable.lastActive;
}
}

/** @internal item is being dropped on us - call the client drop event */
Expand All @@ -135,18 +155,17 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt
}
this.triggerEvent('drop', ev);
this._removeLeaveCallbacks();
delete this.moving;
}

/** @internal called to remove callbacks when leaving or dropping */
private _removeLeaveCallbacks() {
if (!this.moving) { return; }
delete this.moving;
this.el.removeEventListener('dragover', this._dragOver);
this.el.removeEventListener('drop', this._drop);
this.el.removeEventListener('dragleave', this._dragLeave);
this.el.classList.remove('ui-droppable-over');
if (this.moving) {
this.el.removeEventListener('dragover', this._dragOver);
this.el.removeEventListener('drop', this._drop);
}
// Note: this.moving is reset by callee of this routine to control the flow
// Update: removed that as it causes nested grids to no receive dragenter events when parent drags and sets this for #992. not seeing cursor flicker (chrome).
// this.el.classList.remove('ui-droppable-over');
}

/** @internal */
Expand Down
13 changes: 13 additions & 0 deletions src/h5/dd-utils.ts
Expand Up @@ -78,4 +78,17 @@ export class DDUtils {
['pageX','pageY','clientX','clientY','screenX','screenY'].forEach(p => evt[p] = e[p]); // point info
return {...evt, ...obj} as unknown as T;
}

/** returns true if event is inside the given element rectangle */
// Note: Safari Mac has null event.relatedTarget which causes #1684 so check if DragEvent is inside the coordinates instead
// this.el.contains(event.relatedTarget as HTMLElement)
public static inside(e: MouseEvent, el: HTMLElement): boolean {
// srcElement, toElement, target: all set to placeholder when leaving simple grid, so we can't use that (Chrome)
let target: HTMLElement = e.relatedTarget || (e as any).fromElement;
if (!target) {
const { bottom, left, right, top } = el.getBoundingClientRect();
return (e.x < right && e.x > left && e.y < bottom && e.y > top);
}
return el.contains(target);
}
}
3 changes: 3 additions & 0 deletions src/types.ts
Expand Up @@ -111,6 +111,9 @@ export interface GridStackOptions {
/** draggable handle class (e.g. 'grid-stack-item-content'). If set 'handle' is ignored (default?: null) */
handleClass?: string;

/** id used to debug grid instance, not currently stored in DOM attributes */
id?: numberOrString;

/** additional widget class (default?: 'grid-stack-item') */
itemClass?: string;

Expand Down

0 comments on commit 98836f6

Please sign in to comment.