diff --git a/README.md b/README.md index 631eb4555..105116b1d 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Join us on Slack: https://gridstackjs.troolee.com - [Migrating to v1](#migrating-to-v1) - [Migrating to v2](#migrating-to-v2) - [Migrating to v3](#migrating-to-v3) + - [Migrating to v4](#migrating-to-v4) - [jQuery Application](#jquery-application) - [Changes](#changes) - [The Team](#the-team) @@ -390,6 +391,30 @@ Breaking changes: 5. `GridStackWidget` used in most API `width|height|minWidth|minHeight|maxWidth|maxHeight` are now shorter `w|h|minW|minH|maxW|maxH` as well +## Migrating to v4 + +make sure to read v3 migration first! + +v4 is a complete re-write of the collision and drag in/out heuristics to fix some very long standing request & bugs. It also greatly improved usability. Read the release notes for more detail. + +**Unlikely** Breaking Changes (internal usage): + +1. `removeTimeout` was removed (feedback over trash will be immediate - actual removal still on mouse up) + +2. the following `GridStackEngine` methods changed (used internally, doesn't affect `GridStack` public API) + +```js +// moved to 3 methods with new option params to support new code and pixel coverage check +`collision()` -> `collide(), collideAll(), collideCoverage()` +`moveNodeCheck(node, x, y, w, h)` -> `moveNodeCheck(node, opt: GridStackMoveOpts)` +`isNodeChangedPosition(node, x, y, w, h)` -> `changedPosConstrain(node, opt: GridStackMoveOpts)` +`moveNode(node, x, y, w, h, noPack)` -> `moveNode(node, opt: GridStackMoveOpts)` +``` + +3. removed old obsolete (v0.6-v1 methods/attrs) `getGridHeight()`, `verticalMargin`, `data-gs-current-height`, +`locked()`, `maxWidth()`, `minWidth()`, `maxHeight()`, `minHeight()`, `move()`, `resize()` + + # jQuery Application We now have a native HTML5 drag'n'drop through the plugin system (default), but the jquery-ui version can be used instead. It will bundle `jquery` (3.5.1) + `jquery-ui` (1.12.1 minimal drag|drop|resize) + `jquery-ui-touch-punch` (1.0.8 for mobile support) in `gridstack-jq.js`. IFF you want to use gridstack-jq instead and your app needs to bring your own JQ version (only possible in 1.x), you should **instead** use v1.x and include `gridstack-poly.min.js` (optional IE support) + `gridstack.min.js` + `gridstack.jQueryUI.min.js` after you import your JQ libs. But note that there are issue with jQuery and ES6 import (see [1306](https://github.com/gridstack/gridstack.js/issues/1306)). diff --git a/demo/two-jq.html b/demo/two-jq.html index 986e708c0..69202ac49 100644 --- a/demo/two-jq.html +++ b/demo/two-jq.html @@ -92,7 +92,6 @@

Two grids demo (Jquery version)

dragIn: '.sidebar .grid-stack-item', // add draggable to class dragInOptions: { revert: 'invalid', scroll: false, appendTo: 'body', helper: 'clone' }, // clone removable: '.trash', // drag-out delete class - removeTimeout: 100, acceptWidgets: function(el) { return true; } // function example, else can be simple: true | false | '.someClass' value }; grids = GridStack.initAll(options); diff --git a/demo/two.html b/demo/two.html index 732556a3a..8671f9095 100644 --- a/demo/two.html +++ b/demo/two.html @@ -90,7 +90,6 @@

Two grids demo

// dragIn: '.sidebar .grid-stack-item', // add draggable to class // dragInOptions: { revert: 'invalid', scroll: false, appendTo: 'body', helper: 'clone' }, // clone removable: '.trash', // drag-out delete class - removeTimeout: 100, acceptWidgets: function(el) { return true; } // function example, else can be simple: true | false | '.someClass' value }; let grids = GridStack.initAll(options); diff --git a/demo/web2.html b/demo/web2.html index 9534133a3..d92f6e1c7 100644 --- a/demo/web2.html +++ b/demo/web2.html @@ -62,7 +62,6 @@

Advanced Demo

dragIn: '.newWidget', // class that can be dragged from outside dragInOptions: { revert: 'invalid', scroll: false, appendTo: 'body', helper: 'clone' }, removable: '#trash', // drag-out delete class - removeTimeout: 100, }); let items = [ diff --git a/doc/CHANGES.md b/doc/CHANGES.md index ebeca32fd..426b205bd 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -50,13 +50,19 @@ Change log ## 3.3.0-dev -- fix [#149](https://github.com/gridstack/gridstack.js/issues/149) [#1094](https://github.com/gridstack/gridstack.js/issues/1094) [#1605](https://github.com/gridstack/gridstack.js/issues/1605) re-write of the **collision code**! you can now swap items of the same size (vertical/horizontal) when grid is full, and is the default in `float:false` (top gravity) as it feels more natural. Could add Alt key for swap vs push behavior later. -- Dragging up and down now behave the same (used to require push WAY down past to swap/append). Also much more efficient collision code. -- handle mid point of dragged over items (>50%) rather than a new row/column and check for the most covered when multiple items collide. +- fix [#149](https://github.com/gridstack/gridstack.js/issues/149) [#1094](https://github.com/gridstack/gridstack.js/issues/1094) [#1605](https://github.com/gridstack/gridstack.js/issues/1605) re-write of the **collision code - fixing 6 years old most requested request** +1. you can now swap items of the same size (vertical/horizontal) when grid is full, and is the default in `float:false` (top gravity) as it feels more natural. Could add Alt key for swap vs push behavior later. +2. Dragging up and down now behave the same (used to require push WAY down past to swap/append). Also much more efficient collision code. +3. handle mid point of dragged over items (>50%) rather than just a new row/column and check for the most covered item when multiple collide. + +- fix [#393](https://github.com/gridstack/gridstack.js/issues/393) [#1612](https://github.com/gridstack/gridstack.js/issues/1612) [#1578](https://github.com/gridstack/gridstack.js/issues/1578) re-write of the **drag in/out code - fixing 5 years old bug** +1. we now remove item when cursor leaves (`acceptWidgets` case using `dropout` event) or shape is outside (re-using same method) and re-insert on cursor enter (since we only get `dropover` event). Should **not be possible to have 2 placeholders** which confuses the grids. +2. major re-write and cleanup of the drag in/out. Vars have been renamed and fully documented as I couldn't understand the legacy buggy code. +3. removed any over trash delay feedback as I don't see the point and could introduce race conditions. - fix [1617](https://github.com/gridstack/gridstack.js/issues/1617) FireFox DOM order issue. Thanks [@marcel-necker](https://github.com/marcel-necker) - fix changing column # `column(n)` now resizes `cellHeight:'auto'` to keep square -- add `drag | resize` events while dragging [1616](https://github.com/gridstack/gridstack.js/pull/1616). Thanks [@MrCorba](https://github.com/MrCorba) -- add `GridStack.setupDragIn()` so user can update external draggable after the grid has been created [1637](https://github.com/gridstack/gridstack.js/issues/1637) +- add [1616](https://github.com/gridstack/gridstack.js/pull/1616) `drag | resize` events while dragging. Thanks [@MrCorba](https://github.com/MrCorba) +- add [1637](https://github.com/gridstack/gridstack.js/issues/1637) `GridStack.setupDragIn()` so user can update external draggable after the grid has been created ## 3.3.0 (2021-2-2) diff --git a/spec/e2e/html/1570_drag_bottom_max_row.html b/spec/e2e/html/1570_drag_bottom_max_row.html index e4be4c1c8..7e1e63e1c 100644 --- a/spec/e2e/html/1570_drag_bottom_max_row.html +++ b/spec/e2e/html/1570_drag_bottom_max_row.html @@ -91,7 +91,6 @@ maxRow: 3, // change this to show issue acceptWidgets: true, removable: true, - removeTimeout: 0, float: true } diff --git a/spec/e2e/html/1571_drop_onto_full.html b/spec/e2e/html/1571_drop_onto_full.html index 537c6cf0a..f0f417a60 100644 --- a/spec/e2e/html/1571_drop_onto_full.html +++ b/spec/e2e/html/1571_drop_onto_full.html @@ -89,7 +89,6 @@

drop onto full

dragIn: '.sidebar .grid-stack-item', // class that can be dragged from outside dragInOptions: { revert: 'invalid', scroll: false, appendTo: 'body', helper: 'clone' }, // clone removable: '.trash', // drag-out delete class - removeTimeout: 100, acceptWidgets: function(el) { return true; } // function example, else can be simple: true | false | '.someClass' value }; let grids = GridStack.initAll(options); diff --git a/src/gridstack-dd.ts b/src/gridstack-dd.ts index 987f1af14..ae9d2f537 100644 --- a/src/gridstack-dd.ts +++ b/src/gridstack-dd.ts @@ -6,7 +6,6 @@ * gridstack.js may be freely distributed under the MIT license. */ /* eslint-disable @typescript-eslint/no-unused-vars */ - import { GridStackDDI } from './gridstack-ddi'; import { GridItemHTMLElement, GridStackNode, GridStackElement, DDUIData, DDDragInOpt, GridStackPosition } from './types'; import { GridStack, MousePosition } from './gridstack'; @@ -93,8 +92,9 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { let cellHeight: number, cellWidth: number; let onDrag = (event: DragEvent, el: GridItemHTMLElement, helper: GridItemHTMLElement) => { - let node = el.gridstackNode; + if (!node) return; + helper = helper || el; // let left = event.pageX - gridPos.left; // let top = event.pageY - gridPos.top; @@ -103,7 +103,7 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { let top = rec.top - gridPos.top; let ui: DDUIData = {position: {top, left}}; - if (!node._added) { + if (node._temporaryRemoved) { node.x = Math.max(0, Math.round(left / cellWidth)); node.y = Math.max(0, Math.round(top / cellHeight)); delete node.autoPosition; @@ -119,13 +119,12 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { } // re-use the existing node dragging method - delete node._updating; // make sure beginUpdate() is called cleanly on this this._onStartMoving(event, ui, node, cellWidth, cellHeight); } else { // re-use the existing node dragging that does so much of the collision detection this._dragOrResize(event, ui, node, cellWidth, cellHeight); } - }; + } GridStackDD.get() .droppable(this.el, { @@ -153,12 +152,18 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { * entering our grid area */ .on(this.el, 'dropover', (event: Event, el: GridItemHTMLElement, helper: GridItemHTMLElement) => { - - // ignore drop enter on ourself, and prevent parent from receiving event let node = el.gridstackNode; - if (node && node.grid === this) { - delete node._added; // reset this to track placeholder again in case we were over other grid #1484 (dropout doesn't always clear) - return false; + // ignore drop enter on ourself (unless we temporarily removed) which happens on a simple drag of our item + if (node && node.grid === this && !node._temporaryRemoved) { + // delete node._added; // reset this to track placeholder again in case we were over other grid #1484 (dropout doesn't always clear) + return false; // prevent parent from receiving msg (which may be a grid as well) + } + + // fix #1578 when dragging fast, we may not get a leave on the previous grid so force one now + if (node && node.grid && node.grid !== this && !node._temporaryRemoved) { + // TEST console.log('dropover without leave'); + let otherGrid = node.grid; + otherGrid._leave(el.gridstackNode, el, helper, true); // MATCH line 222 } // get grid screen coordinates and cell dimensions @@ -171,50 +176,51 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { if (!node) { node = this._readAttr(el); } - - // if the item came from another grid, let it know it was added here to removed duplicate shadow #393 - if (node.grid && node.grid !== this) { - node._added = true; + if (!node.grid) { + node._isExternal = true; + el.gridstackNode = node; } - - // if not calculate the grid size based on element outer size + + // calculate the grid size based on element outer size helper = helper || el; let w = node.w || Math.round(helper.offsetWidth / cellWidth) || 1; let h = node.h || Math.round(helper.offsetHeight / cellHeight) || 1; - // COPY the node original values (min/max/id/etc...) but override width/height/other flags which are this grid specific - let newNode = this.engine.prepareNode({...node, ...{w, h, _added: false, _temporary: true, _isOutOfGrid: true}}); - el.gridstackNode = newNode; - el._gridstackNodeOrig = node; + // if the item came from another grid, make a copy and save the original info in case we go back there + if (node.grid && node.grid !== this) { + // copy the node original values (min/max/id/etc...) but override width/height/other flags which are this grid specific + // TEST console.log('dropover cloning node'); + if (!el._gridstackNodeOrig) el._gridstackNodeOrig = node; // shouldn't have multiple nested! + el.gridstackNode = node = {...node, w, h, grid: this}; + this.engine.cleanupNode(node) + .nodeBoundFix(node); + // restore some internal fields we need after clearing them all + node._initDD = + node._isExternal = // DOM needs to be re-parented on a drop + node._temporaryRemoved = true; + } else { + node.w = w; node.h = h; + node._temporaryRemoved = true; // so we can insert it + } + + // we're entering this grid (even if we left another) + delete node._isCursorOutside; - onDrag(event as DragEvent, el, helper); // make sure this is called at least once when going fast #1578 GridStackDD.get().on(el, 'drag', onDrag); - return false; // prevent parent from receiving msg (which may be grid as well) + // make sure this is called at least once when going fast #1578 + onDrag(event as DragEvent, el, helper); + return false; // prevent parent from receiving msg (which may be a grid as well) }) /** * Leaving our grid area... */ - .on(this.el, 'dropout', (event, el: GridItemHTMLElement) => { + .on(this.el, 'dropout', (event, el: GridItemHTMLElement, helper: GridItemHTMLElement) => { let node = el.gridstackNode; - if (!node) return; - - // clear any added flag now that we are leaving #1484 - delete node._added; - - // jquery-ui bug. Must verify widget is being dropped out - // check node variable that gets set when widget is out of grid - if (!node._isOutOfGrid) { - return; - } - - GridStackDD.get().off(el, 'drag'); - node.el = null; - this.engine.removeNode(node); - if (this.placeholder.parentNode === this.el) { - this.placeholder.remove(); + // fix #1578 when dragging fast, we might get leave after other grid gets enter (which calls us to clean) + // so skip this one if we're not the active grid really.. + if (!node.grid || node.grid === this) { + this._leave(node, el, helper, true); // MATCH line 166 } - this._updateContainerHeight(); - el.gridstackNode = el._gridstackNodeOrig; return false; // prevent parent from receiving msg (which may be grid as well) }) /** @@ -222,19 +228,18 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { */ .on(this.el, 'drop', (event, el: GridItemHTMLElement, helper: GridItemHTMLElement) => { let node = el.gridstackNode; - let wasAdded = !!this.placeholder.parentElement; // skip items not actually added to us because of constrains, but do cleanup #1419 - // ignore drop on ourself from ourself - dragend will handle the simple move instead - if (node && node.grid === this) return false; + // ignore drop on ourself from ourself that didn't come from the outside - dragend will handle the simple move instead + if (node && node.grid === this && !node._isExternal) return false; + let wasAdded = !!this.placeholder.parentElement; // skip items not actually added to us because of constrains, but do cleanup #1419 this.placeholder.remove(); // notify previous grid of removal + // TEST console.log('drop delete _gridstackNodeOrig') let origNode = el._gridstackNodeOrig; delete el._gridstackNodeOrig; if (wasAdded && origNode && origNode.grid && origNode.grid !== this) { let oGrid = origNode.grid; - oGrid.placeholder.remove(); - origNode.el = el; // was using placeholder, have it point to node we've moved instead oGrid.engine.removedNodes.push(origNode); oGrid._triggerRemoveEvent(); } @@ -243,9 +248,7 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { // use existing placeholder node as it's already in our list with drop location if (wasAdded) { - const _id = node._id; - this.engine.cleanupNode(node); // removes all internal _xyz values (including the _id so add that back) - node._id = _id; + this.engine.cleanupNode(node); // removes all internal _xyz values node.grid = this; } GridStackDD.get().off(el, 'drag'); @@ -265,6 +268,7 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { el.gridstackNode = node; node.el = el; + Utils.copyPos(node, this._readAttr(this.placeholder)); // placeholder values as moving VERY fast can throw things off #1578 Utils.removePositioningStyles(el); this._writeAttr(el, node); this.el.appendChild(el); @@ -279,9 +283,13 @@ GridStack.prototype._setupAcceptWidget = function(): GridStack { } // wait till we return out of the drag callback to set the new drag&resize handler or they may get messed up - // IFF we are still there (some application will use as placeholder and insert their real widget instead) window.setTimeout(() => { - if (node.el && node.el.parentElement) this._prepareDragDropByNode(node); + // IFF we are still there (some application will use as placeholder and insert their real widget instead and better call makeWidget()) + if (node.el && node.el.parentElement) { + this._prepareDragDropByNode(node); + } else { + this.engine.removeNode(node); + } }); return false; // prevent parent from receiving msg (which may be grid as well) @@ -302,42 +310,20 @@ GridStack.prototype._setupRemoveDrop = function(): GridStack { .on(trashEl, 'dropover', function(event, el) { // don't use => notation to avoid using 'this' as grid by mistake... let node = el.gridstackNode; if (!node || !node.grid) return; - el.dataset.inTrashZone = 'true'; - node.grid._setupRemovingTimeout(el); + node._isAboutToRemove = true; + el.classList.add('grid-stack-item-removing'); }) .on(trashEl, 'dropout', function(event, el) { // same let node = el.gridstackNode; if (!node || !node.grid) return; - delete el.dataset.inTrashZone; - node.grid._clearRemovingTimeout(el); + delete node._isAboutToRemove; + el.classList.remove('grid-stack-item-removing'); }); } } return this; } -/** @internal */ -GridStack.prototype._setupRemovingTimeout = function(el: GridItemHTMLElement): GridStack { - let node = el.gridstackNode; - if (!node || node._removeTimeout || !this.opts.removable) return this; - node._removeTimeout = window.setTimeout(() => { - el.classList.add('grid-stack-item-removing'); - node._isAboutToRemove = true; - }, this.opts.removeTimeout); - return this; -} - -/** @internal */ -GridStack.prototype._clearRemovingTimeout = function(el: GridItemHTMLElement): GridStack { - let node = el.gridstackNode; - if (!node || !node._removeTimeout) return this; - clearTimeout(node._removeTimeout); - delete node._removeTimeout; - el.classList.remove('grid-stack-item-removing'); - delete node._isAboutToRemove; - return this; -} - /** * call to setup dragging in from the outside (say toolbar), by specifying the class selection and options. * Called during GridStack.init() as options, but can also be called directly (last param are cached) in case the toolbar @@ -398,7 +384,7 @@ GridStack.prototype._prepareDragDropByNode = function(node: GridStackNode): Grid let cellHeight: number; /** called when item starts moving/resizing */ - let onStartMoving = (event: Event, ui: DDUIData): void => { + let onStartMoving = (event: Event, ui: DDUIData) => { // trigger any 'dragstart' / 'resizestart' manually if (this._gsEventHandler[event.type]) { this._gsEventHandler[event.type](event, event.target); @@ -410,15 +396,13 @@ GridStack.prototype._prepareDragDropByNode = function(node: GridStackNode): Grid } /** called when item is being dragged/resized */ - let dragOrResize = (event: Event, ui: DDUIData): void => { + let dragOrResize = (event: Event, ui: DDUIData) => { this._dragOrResize(event, ui, node, cellWidth, cellHeight); } /** called when the item stops moving/resizing */ - let onEndMoving = (event: Event): void => { - if (this.placeholder.parentNode === this.el) { - this.placeholder.remove(); - } + let onEndMoving = (event: Event) => { + this.placeholder.remove(); delete node._moving; delete node._lastTried; @@ -433,24 +417,24 @@ GridStack.prototype._prepareDragDropByNode = function(node: GridStackNode): Grid if (gridToNotify._gsEventHandler[event.type]) { gridToNotify._gsEventHandler[event.type](event, target); } - gridToNotify.engine.removedNodes.push(node); GridStackDD.get().remove(el); - delete el.gridstackNode; // hint we're removing it next and break circular link + gridToNotify.engine.removedNodes.push(node); gridToNotify._triggerRemoveEvent(); - if (el.parentElement) { - el.remove(); // finally remove it - } + // break circular links and remove DOM + delete el.gridstackNode; + delete node.el; + el.remove(); } else { - this._clearRemovingTimeout(el); if (!node._temporaryRemoved) { + // move to new placeholder location Utils.removePositioningStyles(target); this._writePosAttr(target, node); } else { + // got removed - restore item back to before dragging position Utils.removePositioningStyles(target); - this._writePosAttr(target, {...node._beforeDrag, w: node.w, h: node.h}); - node.x = node._beforeDrag.x; - node.y = node._beforeDrag.y; - delete node._temporaryRemoved; + Utils.copyPos(node, node._beforeDrag); + delete node._beforeDrag; + this._writePosAttr(target, node); this.engine.addNode(node); } if (this._gsEventHandler[event.type]) { @@ -462,13 +446,6 @@ GridStack.prototype._prepareDragDropByNode = function(node: GridStackNode): Grid this._triggerChangeEvent(); this.engine.endUpdate(); - - /* doing it on live resize instead - // if we re-sized a nested grid item, let the children resize as well - if (event.type === 'resizestop') { - if (target.gridstackNode.subGrid) {(target.gridstackNode.subGrid as GridStack).onParentResize()} - } - */ } GridStackDD.get() @@ -495,23 +472,24 @@ GridStack.prototype._prepareDragDropByNode = function(node: GridStackNode): Grid } /** @internal called when item is starting a drag/resize */ -GridStack.prototype._onStartMoving = function(event: Event, ui: DDUIData, node: GridStackNode, cellWidth: number, cellHeight: number): void { +GridStack.prototype._onStartMoving = function(event: Event, ui: DDUIData, node: GridStackNode, cellWidth: number, cellHeight: number) { this.engine.cleanNodes() .beginUpdate(node); this._writePosAttr(this.placeholder, node) - this.el.append(this.placeholder); + this.el.appendChild(this.placeholder); + // TEST console.log('_onStartMoving placeholder') node.el = this.placeholder; node._lastUiPosition = ui.position; node._prevYPix = ui.position.top; node._moving = (event.type === 'dragstart'); delete node._lastTried; + delete node._isCursorOutside; - if (event.type === 'dropover' && !node._added) { - node._added = true; - this.engine.addNode(node); - this._writePosAttr(this.placeholder, node); + if (event.type === 'dropover' && node._temporaryRemoved) { + // TEST console.log('engine.addNode x=' + node.x); + this.engine.addNode(node); // will add, constrain, update attr and clear _temporaryRemoved node._moving = true; // lastly mark as moving object } @@ -527,8 +505,39 @@ GridStack.prototype._onStartMoving = function(event: Event, ui: DDUIData, node: } } +/** @internal called when item leaving our area by either cursor dropout event + * or shape is outside our boundaries. remove it from us, and mark temporary if this was + * our item to start with else restore prev node values from prev grid it came from. + **/ +GridStack.prototype._leave = function(node: GridStackNode, el: GridItemHTMLElement, helper?: GridItemHTMLElement, dropoutEvent = false) { + if (!node) return; + + if (dropoutEvent) { + node._isCursorOutside = true; + GridStackDD.get().off(el, 'drag'); // no need to track while being outside + } + + // this gets called when cursor leaves and shape is outside, so only do this once + if (node._temporaryRemoved) return; + node._temporaryRemoved = true; + + this.engine.removeNode(node); // remove placeholder as well + node.el = node._isExternal && helper ? helper : el; // point back to real item being dragged + + // finally if item originally came from another grid, but left us, restore things back to prev info + if (el._gridstackNodeOrig) { + // TEST console.log('leave delete _gridstackNodeOrig') + el.gridstackNode = el._gridstackNodeOrig; + delete el._gridstackNodeOrig; + } else if (node._isExternal) { + // item came from outside (like a toolbar) so nuke any node info + delete node.el; + delete el.gridstackNode; + } +} + /** @internal called when item is being dragged/resized */ -GridStack.prototype._dragOrResize = function(event: Event, ui: DDUIData, node: GridStackNode, cellWidth: number, cellHeight: number): void { +GridStack.prototype._dragOrResize = function(event: Event, ui: DDUIData, node: GridStackNode, cellWidth: number, cellHeight: number) { let el = node.el || event.target as GridItemHTMLElement; // calculate the place where we're landing by offsetting margin so actual edge crosses mid point let left = ui.position.left + (ui.position.left > node._lastUiPosition.left ? -this.opts.marginRight : this.opts.marginLeft); @@ -537,42 +546,22 @@ GridStack.prototype._dragOrResize = function(event: Event, ui: DDUIData, node: G let y = Math.round(top / cellHeight); let w = node.w; let h = node.h; - if (node._isOutOfGrid) { - // items coming from outside are handled by 'dragout' event instead, so make coordinates fit - let fix = this.engine.nodeBoundFix({x, y, w, h}); - x = fix.x; y = fix.y; w = fix.w; h = fix.h; - } let resizing: boolean; if (event.type === 'drag') { + if (node._isCursorOutside) return; // handled by dropover let distance = ui.position.top - node._prevYPix; node._prevYPix = ui.position.top; Utils.updateScrollPosition(el, ui.position, distance); - // if inTrash, outside of the bounds or added to another grid (#393) temporarily remove it from us - if (el.dataset.inTrashZone || (node._added && !node._isOutOfGrid) || this.engine.isOutside(x, y, node)) { - if (node._temporaryRemoved) return; - if (this.opts.removable === true) { - this._setupRemovingTimeout(el); - } - - x = node._beforeDrag.x; - y = node._beforeDrag.y; - - if (this.placeholder.parentNode === this.el) { - this.placeholder.remove(); - } - this.engine.removeNode(node); - this._updateContainerHeight(); - - node._temporaryRemoved = true; - delete node._added; // no need for this now + // if inTrash or outside of the bounds (but not external which is handled by 'dropout' event), temporarily remove it from us + if (node._isAboutToRemove || (!node._isExternal && this.engine.isOutside(x, y, node))) { + this._leave(node, event.target); } else { - if (node._removeTimeout) this._clearRemovingTimeout(el); - if (node._temporaryRemoved) { node.el = this.placeholder; this.engine.addNode(node); this.el.appendChild(this.placeholder); + // TEST console.log('drag placeholder'); delete node._temporaryRemoved; } } diff --git a/src/gridstack-engine.ts b/src/gridstack-engine.ts index 07b12161b..fe0bf9ea7 100644 --- a/src/gridstack-engine.ts +++ b/src/gridstack-engine.ts @@ -42,7 +42,7 @@ export class GridStackEngine { private _layouts?: Layout[][]; // maps column # to array of values nodes /** @internal */ private _ignoreLayoutsNodeChange: boolean; - /** @internal */ + /** @internal unique global internal _id counter NOT starting at 0 */ private static _idSeq = 1; public constructor(opts: GridStackEngineOptions = {}) { @@ -73,7 +73,8 @@ export class GridStackEngine { /** @internal fix collision on given 'node', going to given new location 'nn', with optional 'collide' node already found. * return true if we moved. */ private _fixCollisions(node: GridStackNode, nn = node, collide?: GridStackNode, opt: GridStackMoveOpts = {}): boolean { - // this._sortNodes(-1); collision doesn't care about sorting + this._sortNodes(-1); // TODO: collision should not care about sorting but it does (behaves differently second time trying to insert same spot) + collide = collide || this.collide(node, nn); if (!collide) return false; @@ -261,24 +262,26 @@ export class GridStackEngine { return this; } - /** @internal called to top gravity pack the items back */ + /** @internal called to top gravity pack the items back OR revert back to original Y positions when floating */ private _packNodes(): GridStackEngine { this._sortNodes(); if (this.float) { + // restore original Y pos this.nodes.forEach(n => { if (n._updating || n._packY === undefined || n.y === n._packY) return; let newY = n.y; - while (newY >= n._packY) { + while (newY > n._packY) { + --newY; let collide = this.collide(n, {x: n.x, y: newY, w: n.w, h: n.h}); if (!collide) { n._dirty = true; n.y = newY; } - --newY; } }); } else { + // top gravity pack this.nodes.forEach((n, i) => { if (n.locked) return; while (n.y > 0) { @@ -416,7 +419,10 @@ export class GridStackEngine { public addNode(node: GridStackNode, triggerAddEvent = false): GridStackNode { let dup: GridStackNode; if (dup = this.nodes.find(n => n._id === node._id)) return dup; // prevent inserting twice! return it instead. + node = this.prepareNode(node); + delete node._temporaryRemoved; + delete node._removeDOM; if (node.autoPosition) { this._sortNodes(); @@ -447,20 +453,21 @@ export class GridStackEngine { } public removeNode(node: GridStackNode, removeDOM = true, triggerEvent = false): GridStackEngine { + if (!this.nodes.find(n => n === node)) return; // not in our list if (triggerEvent) { // we wait until final drop to manually track removed items (rather than during drag) this.removedNodes.push(node); } - node._id = null; // hint that node is being removed + if (removeDOM) node._removeDOM = true; // let CB remove actual HTML (used to set _id to null, but then we loose layout info) // don't use 'faster' .splice(findIndex(),1) in case node isn't in our list, or in multiple times. this.nodes = this.nodes.filter(n => n !== node); - !this.float && this._packNodes(); - return this._notify(node, removeDOM); + return this._packNodes() + ._notify(node, removeDOM); } public removeAll(removeDOM = true): GridStackEngine { delete this._layouts; if (this.nodes.length === 0) return this; - removeDOM && this.nodes.forEach(n => n._id = null); // hint that node is being removed + removeDOM && this.nodes.forEach(n => n._removeDOM = true); // let CB remove actual HTML (used to set _id to null, but then we loose layout info) this.removedNodes = this.nodes; this.nodes = []; return this._notify(this.removedNodes, removeDOM); @@ -536,7 +543,7 @@ export class GridStackEngine { /** return true if the passed in node (x,y) is being dragged outside of the grid, and not added to bottom */ public isOutside(x: number, y: number, node: GridStackNode): boolean { - if (node._isOutOfGrid) return false; // dragging out is handled by 'dropout' event instead + if (node._isCursorOutside) return false; // dragging out is handled by 'dropout' event instead // simple outside boundaries if (x < 0 || x >= this.column || y < 0) return true; if (this.maxRow) return (y >= this.maxRow); @@ -813,16 +820,13 @@ export class GridStackEngine { } - /** called to remove all internal values */ + /** called to remove all internal values but the _id */ public cleanupNode(node: GridStackNode): GridStackEngine { for (let prop in node) { - if (prop[0] === '_') delete node[prop]; + if (prop[0] === '_' && prop !== '_id') delete node[prop]; } return this; } - - /** @internal legacy method renames */ - private getGridHeight = obsolete(this, GridStackEngine.prototype.getRow, 'getGridHeight', 'getRow', 'v1.0.0'); } /** @internal class to store per column layout bare minimal info (subset of GridStackWidget) */ diff --git a/src/gridstack.ts b/src/gridstack.ts index 61192fddc..782f671a9 100644 --- a/src/gridstack.ts +++ b/src/gridstack.ts @@ -77,7 +77,6 @@ const GridDefaults: GridStackOptions = { removableOptions: { accept: '.grid-stack-item' }, - removeTimeout: 2000, marginUnit: 'px', cellHeightUnit: 'px', disableOneColumnMode: false, @@ -165,7 +164,7 @@ export class GridStack { let doc = document.implementation.createHTMLDocument(); doc.body.innerHTML = `
`; let el = doc.body.children[0] as HTMLElement; - parent.append(el); + parent.appendChild(el); // create grid class and load any children let grid = GridStack.init(opt, el); @@ -234,10 +233,6 @@ export class GridStack { this.el = el; // exposed HTML element to the user opts = opts || {}; // handles null/undefined/0 - obsoleteOpts(opts, 'verticalMargin', 'margin', 'v2.0'); - - obsoleteAttr(this.el, 'data-gs-current-height', 'gs-current-row', 'v1.0.0'); - // if row property exists, replace minRow and maxRow instead if (opts.row) { opts.minRow = opts.maxRow = opts.row; @@ -317,8 +312,9 @@ export class GridStack { this.engine.nodes.forEach(n => { maxH = Math.max(maxH, n.y + n.h) }); cbNodes.forEach(n => { let el = n.el; - if (removeDOM && n._id === null) { - if (el && el.parentNode) { el.parentNode.removeChild(el) } + if (removeDOM && n._removeDOM) { // TODO: do we need to pass 'removeDOM' ? + if (el) el.remove(); + delete n._removeDOM; } else { this._writePosAttr(el, n); } @@ -1516,30 +1512,12 @@ export class GridStack { public _setupAcceptWidget(): GridStack { return this } /** @internal called to setup a trash drop zone if the user specifies it */ public _setupRemoveDrop(): GridStack { return this } - /** @internal */ - public _setupRemovingTimeout(el: GridItemHTMLElement): GridStack { return this } - /** @internal */ - public _clearRemovingTimeout(el: GridItemHTMLElement): GridStack { return this } /** @internal prepares the element for drag&drop **/ public _prepareDragDropByNode(node: GridStackNode): GridStack { return this } /** @internal handles actual drag/resize start **/ public _onStartMoving(event: Event, ui: DDUIData, node: GridStackNode, cellWidth: number, cellHeight: number): void { return } /** @internal handles actual drag/resize **/ public _dragOrResize(event: Event, ui: DDUIData, node: GridStackNode, cellWidth: number, cellHeight: number): void { return } - - // 2.x API that just calls the new and better update() - keep those around for backward compat only... - /** @internal */ - public locked(els: GridStackElement, locked: boolean): GridStack { return this.update(els, {locked}) } - /** @internal */ - public maxWidth(els: GridStackElement, maxW: number): GridStack { return this.update(els, {maxW}) } - /** @internal */ - public minWidth(els: GridStackElement, minW: number): GridStack { return this.update(els, {minW}) } - /** @internal */ - public maxHeight(els: GridStackElement, maxH: number): GridStack { return this.update(els, {maxH}) } - /** @internal */ - public minHeight(els: GridStackElement, minH: number): GridStack { return this.update(els, {minH}) } - /** @internal */ - public move(els: GridStackElement, x?: number, y?: number): GridStack { return this.update(els, {x, y}) } - /** @internal */ - public resize(els: GridStackElement, w?: number, h?: number): GridStack { return this.update(els, {w, h}) } + /** @internal called when a node leaves our area (mouse out or shape outside) **/ + public _leave(node: GridStackNode, el: GridItemHTMLElement, helper?: GridItemHTMLElement, dropoutEvent = false): void { return } } diff --git a/src/h5/dd-utils.ts b/src/h5/dd-utils.ts index db96b656b..2f79c7c43 100644 --- a/src/h5/dd-utils.ts +++ b/src/h5/dd-utils.ts @@ -37,7 +37,7 @@ export class DDUtils { parentNode = parent as HTMLElement; } if (parentNode) { - parentNode.append(el); + parentNode.appendChild(el); } } diff --git a/src/types.ts b/src/types.ts index b7823912f..d5ab11acc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -165,12 +165,9 @@ export interface GridStackOptions { */ removable?: boolean | string; - /** allows to override UI removable options. (default?: { accept: '.' + opts.itemClass }) */ + /** allows to override UI removable options. (default?: { accept: '.grid-stack-item' }) */ removableOptions?: DDRemoveOpt; - /** time in milliseconds before widget is being removed while dragging outside of the grid. (default?: 2000) */ - removeTimeout?: number; - /** fix grid number of rows. This is a shortcut of writing `minRow:N, maxRow:N`. (default `0` no constrain) */ row?: number; @@ -321,18 +318,18 @@ export interface GridStackNode extends GridStackWidget { el?: GridItemHTMLElement; /** pointer back to Grid instance */ grid?: GridStack; - /** @internal */ + /** @internal internal id used to match when cloning engines or saving column layouts */ _id?: number; /** @internal */ _dirty?: boolean; /** @internal */ _updating?: boolean; - /** @internal */ - _added?: boolean; - /** @internal */ - _temporary?: boolean; - /** @internal */ - _isOutOfGrid?: boolean; + /** @internal true if the cursor is outside of the grid, as we get dropout/dropover vs shape being outside */ + _isCursorOutside?: boolean; + /** @internal true when over trash/another grid so we don't bother removing drag CSS style that would animate back to old position */ + _isAboutToRemove?: boolean; + /** @internal true if item came from outside of the grid -> actual item need to be moved over */ + _isExternal?: boolean; /** @internal moving vs resizing */ _moving?: boolean; /** @internal true if we jump down past item below (one time jump so we don't have to totally pass it) */ @@ -347,16 +344,14 @@ export interface GridStackNode extends GridStackWidget { _lastUiPosition?: Position; /** @internal set on the item being dragged/resized remember the last positions we've tried (but failed) so we don't try again during drag/resize */ _lastTried?: GridStackPosition; - /** @internal */ + /** @internal original Y when another item is dragged around a float=true so we can restore back as item is dragged around */ _packY?: number; - /** @internal */ - _isAboutToRemove?: boolean; - /** @internal */ - _removeTimeout?: number; /** @internal last drag Y pixel position used to incrementally update V scroll bar */ _prevYPix?: number; - /** @internal */ + /** @internal true if we've remove the item from ourself (dragging out) but might revert it back (release on nothing -> goes back) */ _temporaryRemoved?: boolean; + /** @internal true if we should remove DOM element on _notify() rather than clearing _id (old way) */ + _removeDOM?: boolean; /** @internal */ _initDD?: boolean; }