Skip to content

Commit 98836f6

Browse files
committed
nested grid in/out drag from parent support
* fix #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 #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.
1 parent 003e8b9 commit 98836f6

File tree

9 files changed

+149
-56
lines changed

9 files changed

+149
-56
lines changed

demo/nested.html

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@
1515
.grid-stack .grid-stack .grid-stack-item-content {
1616
background: lightpink;
1717
}
18+
/* make nested grid take entire item content */
19+
.grid-stack-item-content .grid-stack {
20+
min-height: 100%;
21+
min-width: 100%;
22+
}
1823
</style>
1924
</head>
2025
<body>
2126
<div class="container-fluid">
2227
<h1>Nested grids demo</h1>
23-
<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>
24-
<p>Note: HTML5 release doesn't yet support 'dragOut:false' constrain so use JQ version if you need that.</p>
28+
<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>
29+
<p>Note: HTML5 release doesn't yet support 'dragOut:false' constrain so use JQ version if you need that (nested 2 case).</p>
2530
<a class="btn btn-primary" onClick="addNested()" href="#">Add Widget</a>
2631
<a class="btn btn-primary" onClick="addNewWidget('.nested1')" href="#">Add Widget Grid1</a>
2732
<a class="btn btn-primary" onClick="addNewWidget('.nested2')" href="#">Add Widget Grid2</a>
@@ -46,22 +51,27 @@ <h1>Nested grids demo</h1>
4651
let subOptions = {
4752
cellHeight: 30,
4853
column: 4, // make sure to include gridstack-extra.min.css
49-
itemClass: 'sub', // style sub items differently and use to prevent dragging in/out
50-
acceptWidgets: '.grid-stack-item.sub', // only pink sub items can be inserted, otherwise grid-items causes all sort of issues
51-
minWidth: 300, // min to go 1 column mode
52-
margin: 1
54+
acceptWidgets: true, // will accept .grid-stack-item by default
55+
minWidth: 300, // min to go 1 column mode (much smaller than default)
56+
margin: 2
57+
};
58+
let options = { // main grid options
59+
cellHeight: 70,
60+
minRow: 2, // don't collapse when empty
61+
acceptWidgets: true,
62+
id: 'main',
63+
children: [
64+
{y:0, content: 'regular item'},
65+
{x:1, w:4, h:4, subGrid: {children: sub1, dragOut: true, id: 'sub1', ...subOptions}},
66+
{x:5, w:4, h:4, subGrid: {children: sub2, id: 'sub2', ...subOptions}},
67+
]
5368
};
54-
let json = {cellHeight: 70, minRow: 2, children: [
55-
{y:0, content: 'regular item'},
56-
{x:1, w:4, h:4, content: 'nested 1 - can drag items out', subGrid: {children: sub1, dragOut: true, class: 'nested1', ...subOptions}},
57-
{x:5, w:4, h:4, content: 'nested 2 - constrained to parent (default)', subGrid: {children: sub2, class: 'nested2', ...subOptions}},
58-
]};
5969

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

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

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

8090
save = function(content = true, full = true) {
81-
json = grid.save(content, full);
82-
console.log(json);
83-
// console.log(JSON.stringify(json));
91+
options = grid.save(content, full);
92+
console.log(options);
93+
// console.log(JSON.stringify(options));
8494
}
8595
destroy = function(full = true) {
8696
if (full) {
@@ -92,9 +102,9 @@ <h1>Nested grids demo</h1>
92102
}
93103
load = function(full = true) {
94104
if (full) {
95-
grid = GridStack.addGrid(document.querySelector('.container-fluid'), json);
105+
grid = GridStack.addGrid(document.querySelector('.container-fluid'), options);
96106
} else {
97-
grid.load(json);
107+
grid.load(options);
98108
}
99109
}
100110

doc/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ Change log
6767
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
6868

6969
## 4.4.1-dev (TBD)
70+
* 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.
7071
* fix [#1902](https://github.com/gridstack/gridstack.js/pull/1902) nested.html: dragging between sub-grids show items clipped
72+
* fix [#1558](https://github.com/gridstack/gridstack.js/issues/1558) dragging between vertical grids causes too much growth, not follow mouse.
7173

7274
## 4.4.1 (2021-12-24)
7375
* fix [#1901](https://github.com/gridstack/gridstack.js/pull/1901) error introduced for #1785 when re-loading with fewer objects
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<title>disable move after</title>
8+
9+
<link rel="stylesheet" href="../../../demo/demo.css"/>
10+
<script src="../../../dist/gridstack-h5.js"></script>
11+
12+
</head>
13+
<body>
14+
<div class="container-fluid">
15+
<h1>#1558 items moves too much</h1>
16+
<div class="grid-stack">
17+
<div class="grid-stack-item" gs-x="0" gs-y="0" gs-w="2" gs-h="1">
18+
<div class="grid-stack-item-content">item1 </div>
19+
</div>
20+
<div class="grid-stack-item" gs-x="3" gs-y="1" gs-w="2" gs-h="1">
21+
<div class="grid-stack-item-content">item2</div>
22+
</div>
23+
</div>
24+
<br>
25+
<div class="grid-stack">
26+
<div class="grid-stack-item" gs-x="0" gs-y="0" gs-w="2" gs-h="1">
27+
<div class="grid-stack-item-content">item1 </div>
28+
</div>
29+
<div class="grid-stack-item" gs-x="0" gs-y="1" gs-w="2" gs-h="1">
30+
<div class="grid-stack-item-content">item2</div>
31+
</div>
32+
</div>
33+
</div>
34+
<script src="../../../demo/events.js"></script>
35+
<script type="text/javascript">
36+
var options = {
37+
float: true,
38+
acceptWidgets: true,
39+
cellHeight: 80
40+
};
41+
GridStack.initAll(options);
42+
</script>
43+
</body>
44+
</html>

src/gridstack-dd.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export type DDValue = number | string;
2424
/** drag&drop events callbacks */
2525
export type DDCallback = (event: Event, arg2: GridItemHTMLElement, helper?: GridItemHTMLElement) => void;
2626

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

8485
// vars shared across all methods
85-
let gridPos: MousePosition;
8686
let cellHeight: number, cellWidth: number;
8787

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

9292
helper = helper || el;
93-
let rec = helper.getBoundingClientRect();
94-
let left = rec.left - gridPos.left;
95-
let top = rec.top - gridPos.top;
93+
let parent = this.el.getBoundingClientRect();
94+
let {top, left} = helper.getBoundingClientRect();
95+
left -= parent.left;
96+
top -= parent.top;
9697
let ui: DDUIData = {position: {top, left}};
9798

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

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

173173
// load any element attributes if we don't have a node
174-
if (!node) {// @ts-ignore
174+
if (!node) {// @ts-ignore private read only on ourself
175175
node = this._readAttr(el);
176176
}
177177
if (!node.grid) {
@@ -213,6 +213,7 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
213213
* Leaving our grid area...
214214
*/
215215
.on(this.el, 'dropout', (event, el: GridItemHTMLElement, helper: GridItemHTMLElement) => {
216+
// TEST console.log(`out ${this.el.gridstack.opts.id} ${count++}`);
216217
let node = el.gridstackNode;
217218
if (!node) return false;
218219
// fix #1578 when dragging fast, we might get leave after other grid gets enter (which calls us to clean)

src/gridstack.scss

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ $animation_speed: .3s !default;
126126
}
127127

128128
// without this, the html5 drag will flicker between no-drop and drop when dragging over second grid
129-
&.ui-droppable.ui-droppable-over > *:not(.ui-droppable) {
130-
pointer-events: none;
131-
}
129+
// 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).
130+
// &.ui-droppable.ui-droppable-over > *:not(.ui-droppable) {
131+
// pointer-events: none;
132+
// }
132133
}

src/h5/dd-droppable.ts

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DDDraggable } from './dd-draggable';
77
import { DDManager } from './dd-manager';
88
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
99
import { DDUtils } from './dd-utils';
10+
import { GridHTMLElement, GridStack } from '../gridstack';
1011

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

19+
// TEST let count = 0;
20+
1821
export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt<DDDroppableOpt> {
1922

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

2427
/** @internal */
2528
private moving: boolean;
29+
private static lastActive: DDDroppable;
2630

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

6468
public destroy(): void {
65-
if (this.moving) {
66-
this._removeLeaveCallbacks();
67-
}
69+
this._removeLeaveCallbacks();
6870
this.disable(true);
6971
this.el.classList.remove('ui-droppable');
7072
this.el.classList.remove('ui-droppable-disabled');
71-
delete this.moving;
7273
super.destroy();
7374
}
7475

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

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

86-
if (this.moving) return; // ignore multiple 'dragenter' as we go over existing items
89+
// ignore multiple 'dragenter' as we go over existing items
90+
if (this.moving) return;
8791
this.moving = true;
8892

8993
const ev = DDUtils.initEvent<DragEvent>(event, { target: this.el, type: 'dropover' });
@@ -94,7 +98,14 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt
9498
this.el.addEventListener('dragover', this._dragOver);
9599
this.el.addEventListener('drop', this._drop);
96100
this.el.addEventListener('dragleave', this._dragLeave);
97-
this.el.classList.add('ui-droppable-over');
101+
// 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).
102+
// this.el.classList.add('ui-droppable-over');
103+
104+
// make sure when we enter this, that the last one gets a leave to correctly cleanup as we don't always do
105+
if (DDDroppable.lastActive && DDDroppable.lastActive !== this) {
106+
DDDroppable.lastActive._dragLeave(event, true);
107+
}
108+
DDDroppable.lastActive = this;
98109
}
99110

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

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

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

116-
this._removeLeaveCallbacks();
117134
if (this.moving) {
118-
event.preventDefault();
119135
const ev = DDUtils.initEvent<DragEvent>(event, { target: this.el, type: 'dropout' });
120136
if (this.option.out) {
121137
this.option.out(ev, this._ui(DDManager.dragElement))
122138
}
123139
this.triggerEvent('dropout', ev);
124140
}
125-
delete this.moving;
141+
this._removeLeaveCallbacks();
142+
143+
if (DDDroppable.lastActive === this) {
144+
delete DDDroppable.lastActive;
145+
}
126146
}
127147

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

141160
/** @internal called to remove callbacks when leaving or dropping */
142161
private _removeLeaveCallbacks() {
162+
if (!this.moving) { return; }
163+
delete this.moving;
164+
this.el.removeEventListener('dragover', this._dragOver);
165+
this.el.removeEventListener('drop', this._drop);
143166
this.el.removeEventListener('dragleave', this._dragLeave);
144-
this.el.classList.remove('ui-droppable-over');
145-
if (this.moving) {
146-
this.el.removeEventListener('dragover', this._dragOver);
147-
this.el.removeEventListener('drop', this._drop);
148-
}
149-
// Note: this.moving is reset by callee of this routine to control the flow
167+
// 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).
168+
// this.el.classList.remove('ui-droppable-over');
150169
}
151170

152171
/** @internal */

src/h5/dd-utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,17 @@ export class DDUtils {
7878
['pageX','pageY','clientX','clientY','screenX','screenY'].forEach(p => evt[p] = e[p]); // point info
7979
return {...evt, ...obj} as unknown as T;
8080
}
81+
82+
/** returns true if event is inside the given element rectangle */
83+
// Note: Safari Mac has null event.relatedTarget which causes #1684 so check if DragEvent is inside the coordinates instead
84+
// this.el.contains(event.relatedTarget as HTMLElement)
85+
public static inside(e: MouseEvent, el: HTMLElement): boolean {
86+
// srcElement, toElement, target: all set to placeholder when leaving simple grid, so we can't use that (Chrome)
87+
let target: HTMLElement = e.relatedTarget || (e as any).fromElement;
88+
if (!target) {
89+
const { bottom, left, right, top } = el.getBoundingClientRect();
90+
return (e.x < right && e.x > left && e.y < bottom && e.y > top);
91+
}
92+
return el.contains(target);
93+
}
8194
}

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export interface GridStackOptions {
111111
/** draggable handle class (e.g. 'grid-stack-item-content'). If set 'handle' is ignored (default?: null) */
112112
handleClass?: string;
113113

114+
/** id used to debug grid instance, not currently stored in DOM attributes */
115+
id?: numberOrString;
116+
114117
/** additional widget class (default?: 'grid-stack-item') */
115118
itemClass?: string;
116119

0 commit comments

Comments
 (0)