Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 28 additions & 18 deletions demo/nested.html
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading