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)
-
Note: HTML5 release doesn't yet support 'dragOut:false' constrain so use JQ version if you need that.
+
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)
+
Note: HTML5 release doesn't yet support 'dragOut:false' constrain so use JQ version if you need that (nested 2 case).
}
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);
}
}
diff --git a/doc/CHANGES.md b/doc/CHANGES.md
index 2c1b9f37a..5114e24e1 100644
--- a/doc/CHANGES.md
+++ b/doc/CHANGES.md
@@ -67,7 +67,9 @@ Change log
## 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
diff --git a/spec/e2e/html/1558-vertical-grids-scroll-too-much.html b/spec/e2e/html/1558-vertical-grids-scroll-too-much.html
new file mode 100644
index 000000000..4cd6fe278
--- /dev/null
+++ b/spec/e2e/html/1558-vertical-grids-scroll-too-much.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ disable move after
+
+
+
+
+
+
+
+
#1558 items moves too much
+
+
+
item1
+
+
+
item2
+
+
+
+
+
+
item1
+
+
+
item2
+
+
+
+
+
+
+
diff --git a/src/gridstack-dd.ts b/src/gridstack-dd.ts
index 46dbeccd0..1812ad54c 100644
--- a/src/gridstack-dd.ts
+++ b/src/gridstack-dd.ts
@@ -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)
*/
@@ -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 so look at alternatives ?
* https://www.typescriptlang.org/docs/handbook/declaration-merging.html
* https://www.typescriptlang.org/docs/handbook/mixins.html
********************************************************************************/
@@ -82,7 +83,6 @@ 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) => {
@@ -90,9 +90,10 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack {
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) {
@@ -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) {
@@ -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) {
@@ -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)
diff --git a/src/gridstack.scss b/src/gridstack.scss
index 1d211111d..a4d1121b8 100644
--- a/src/gridstack.scss
+++ b/src/gridstack.scss
@@ -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;
+ // }
}
diff --git a/src/h5/dd-droppable.ts b/src/h5/dd-droppable.ts
index 2d044b663..bfe4c4cf8 100644
--- a/src/h5/dd-droppable.ts
+++ b/src/h5/dd-droppable.ts
@@ -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);
@@ -15,6 +16,8 @@ export interface DDDroppableOpt {
out?: (event: DragEvent, ui) => void;
}
+// TEST let count = 0;
+
export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt {
public accept: (el: HTMLElement) => boolean;
@@ -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();
@@ -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();
}
@@ -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(event, { target: this.el, type: 'dropover' });
@@ -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 */
@@ -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(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 */
@@ -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 */
diff --git a/src/h5/dd-utils.ts b/src/h5/dd-utils.ts
index b7b19aea0..3bcaf1b9a 100644
--- a/src/h5/dd-utils.ts
+++ b/src/h5/dd-utils.ts
@@ -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);
+ }
}
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
index cfdad13e7..67cd68bea 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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;
diff --git a/src/utils.ts b/src/utils.ts
index 9dd9e9cfa..1dff42055 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -254,11 +254,11 @@ export class Utils {
}
}
- /** return the closest parent matching the given class */
+ /** return the closest parent (or itself) matching the given class */
static closestByClass(el: HTMLElement, name: string): HTMLElement {
-
- while(el = el.parentElement) {
+ while (el) {
if (el.classList.contains(name)) return el;
+ el = el.parentElement
}
return null;
}