From 98836f688470689256d70243f185e2140119c2f0 Mon Sep 17 00:00:00 2001 From: Alain Dumesny Date: Mon, 27 Dec 2021 13:08:32 -0800 Subject: [PATCH] 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. --- demo/nested.html | 46 ++++++++----- doc/CHANGES.md | 2 + .../1558-vertical-grids-scroll-too-much.html | 44 +++++++++++++ src/gridstack-dd.ts | 19 +++--- src/gridstack.scss | 7 +- src/h5/dd-droppable.ts | 65 ++++++++++++------- src/h5/dd-utils.ts | 13 ++++ src/types.ts | 3 + src/utils.ts | 6 +- 9 files changed, 149 insertions(+), 56 deletions(-) create mode 100644 spec/e2e/html/1558-vertical-grids-scroll-too-much.html diff --git a/demo/nested.html b/demo/nested.html index 4ce44881f..e3e8a64a3 100644 --- a/demo/nested.html +++ b/demo/nested.html @@ -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%; + }

Nested grids demo

-

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).

Add Widget Add Widget Grid1 Add Widget Grid2 @@ -46,22 +51,27 @@

Nested grids demo

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) { @@ -78,9 +88,9 @@

Nested grids demo

}; 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) { @@ -92,9 +102,9 @@

Nested grids demo

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