From c2e0dbbd89f74db1916f97bc520210c400ce64e0 Mon Sep 17 00:00:00 2001 From: Alain Dumesny Date: Sat, 24 Sep 2022 15:56:52 -0700 Subject: [PATCH] using drag and drop to create nested Grids - part1 * partial fix for #1009 * Create sub-grids on the fly, by dragging items completely over others (nest) vs partially (push) using new flag `GridStackOptions.subGrid.createDynamic=true`. * ability to pause drag&drop collision until the user stops moving - see `DDDragOpt.pause` TODO: need to make it work on already nested grids, remove nested on drag out, more testing... --- demo/demo.css | 13 ++++ demo/float.html | 22 +++--- demo/index.html | 1 + demo/nested.html | 21 +----- demo/nested_advanced.html | 38 ++++------ demo/nested_constraint.html | 106 ++++++++++++++++++++++++++++ doc/CHANGES.md | 8 +++ src/dd-draggable.ts | 25 +++++-- src/dd-gridstack.ts | 40 +++++------ src/dd-manager.ts | 3 + src/gridstack-engine.ts | 55 +++++++++++---- src/gridstack.ts | 136 ++++++++++++++++++++++++++---------- src/types.ts | 42 ++++++++--- src/utils.ts | 52 ++++++++++++++ 14 files changed, 422 insertions(+), 140 deletions(-) create mode 100644 demo/nested_constraint.html diff --git a/demo/demo.css b/demo/demo.css index c6a9bce65..bbb5c1884 100644 --- a/demo/demo.css +++ b/demo/demo.css @@ -60,3 +60,16 @@ h1 { .sidebar .grid-stack-item .grid-stack-item-content { background: none; } + +/* make nested grid have slightly darker bg take almost all space (need some to tell them apart) so items inside can have similar to external size+margin */ +.grid-stack > .grid-stack-item.grid-stack-sub-grid > .grid-stack-item-content { + background: rgba(0,0,0,0.1); + inset: 0 2px; +} +.grid-stack.grid-stack-nested { + background: none; + /* background-color: red; */ + /* take entire space */ + position: absolute; + inset: 0; /* TODO change top: if you have content in nested grid */ +} diff --git a/demo/float.html b/demo/float.html index 1aad1918a..7e0cf1617 100644 --- a/demo/float.html +++ b/demo/float.html @@ -23,20 +23,21 @@

Float grid demo

diff --git a/demo/index.html b/demo/index.html index d6cda156d..19663ea0a 100644 --- a/demo/index.html +++ b/demo/index.html @@ -16,6 +16,7 @@

Demos

  • Knockout.js
  • Mobile touch (JQ)
  • Nested grids
  • +
  • Nested Constraint grids
  • Nested Advanced grids
  • ReactJS (Hooks)
  • ReactJS
  • diff --git a/demo/nested.html b/demo/nested.html index 5e2971e8f..2d05e8952 100644 --- a/demo/nested.html +++ b/demo/nested.html @@ -4,29 +4,10 @@ - Nested grids demo (ES6) + Nested grids demo -
    diff --git a/demo/nested_advanced.html b/demo/nested_advanced.html index eb343471c..e2f6741d4 100644 --- a/demo/nested_advanced.html +++ b/demo/nested_advanced.html @@ -4,26 +4,19 @@ - Nested grids demo (ES6) + Advance Nested grids demo -

    Advanced Nested grids demo

    -

    This example shows sub-grids only accepting pink items, while parent accept all.

    +

    Create sub-grids on the fly, by dragging items completely over others (nest) vs partially (push) using + the new v7 API GridStackOptions.subGrid.createDynamic=true

    +

    This will use the new delay drag&drop option DDDragOpt.pause to tell the gesture difference

    Add Widget - Add Widget Grid1 - Add Widget Grid2 + Add Widget Grid1 entire save/re-create: Save Destroy @@ -38,17 +31,16 @@

    Advanced Nested grids demo

    + + + +
    +

    Constraint Nested grids demo

    +

    This example shows sub-grids only accepting pink items, while parent accept all.

    + Add Widget + Add Widget Grid1 + Add Widget Grid2 + entire save/re-create: + Save + Destroy + Create + partial save/load: + Save list + Save no content + Clear + Load +

    + +
    + + + + diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 04c2177fa..334af8d96 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -5,6 +5,7 @@ Change log **Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)* +- [7-dev (TBD)](#7-dev-tbd) - [6.0.2 (2022-09-23)](#602-2022-09-23) - [6.0.1 (2022-08-27)](#601-2022-08-27) - [6.0.0 (2022-08-21)](#600-2022-08-21) @@ -71,6 +72,13 @@ Change log +## 7-dev (TBD) +* add [#1009](https://github.com/gridstack/gridstack.js/issues/1009) Create sub-grids on the fly, +by dragging items completely over others (nest) vs partially (push) using new flag `GridStackOptions.subGrid.createDynamic=true`. +Thank you [StephanP] for sponsoring it.
    +See [advance Nested](https://github.com/gridstack/gridstack.js/blob/master/demo/nested_advanced.html) +* add - ability to pause drag&drop collision until the user stops moving - see `DDDragOpt.pause` (used for creating nested grids on the fly based on gesture). + ## 6.0.2 (2022-09-23) * fixed [#2034](https://github.com/gridstack/gridstack.js/issues/2034) `removeWidget()` breaking resize handle feedback * fixed [#2043](https://github.com/gridstack/gridstack.js/issues/2043) when swapping shapes in maxRow grid, make sure we still check for 50% coverage diff --git a/src/dd-draggable.ts b/src/dd-draggable.ts index a3d7bfc9f..8c40b18f2 100644 --- a/src/dd-draggable.ts +++ b/src/dd-draggable.ts @@ -57,6 +57,8 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt protected helperContainment: HTMLElement; /** @internal properties we change during dragging, and restore back */ protected static originStyleProp = ['transition', 'pointerEvents', 'position', 'left', 'top']; + /** @internal pause before we call the actual drag hit collision code */ + protected dragTimeout: number; constructor(el: HTMLElement, option: DDDraggableOpt = {}) { super(); @@ -106,6 +108,8 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt } public destroy(): void { + if (this.dragTimeout) window.clearTimeout(this.dragTimeout); + delete this.dragTimeout; if (this.dragging) this._mouseUp(this.mouseDownEvent); this.disable(true); delete this.el; @@ -148,6 +152,16 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt return true; } + /** @internal method to call actual drag event */ + protected _callDrag(e: DragEvent) { + if (!this.dragging) return; + const ev = Utils.initEvent(e, { target: this.el, type: 'drag' }); + if (this.option.drag) { + this.option.drag(ev, this.ui()); + } + this.triggerEvent('drag', ev); + } + /** @internal called when the main page (after successful mousedown) receives a move event to drag the item around the screen */ protected _mouseMove(e: DragEvent): boolean { // console.log(`${count++} move ${e.x},${e.y}`) @@ -155,11 +169,14 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt if (this.dragging) { this._dragFollow(e); - const ev = Utils.initEvent(e, { target: this.el, type: 'drag' }); - if (this.option.drag) { - this.option.drag(ev, this.ui()); + // delay actual grid handling drag until we pause for a while if set + if (DDManager.pauseDrag) { + const pause = Number.isInteger(DDManager.pauseDrag) ? DDManager.pauseDrag as number : 100; + if (this.dragTimeout) window.clearTimeout(this.dragTimeout); + this.dragTimeout = window.setTimeout(() => this._callDrag(e), pause); + } else { + this._callDrag(e); } - this.triggerEvent('drag', ev); } else if (Math.abs(e.x - s.x) + Math.abs(e.y - s.y) > 3) { /** * don't start unless we've moved at least 3 pixels diff --git a/src/dd-gridstack.ts b/src/dd-gridstack.ts index 32e429653..ce0a811f5 100644 --- a/src/dd-gridstack.ts +++ b/src/dd-gridstack.ts @@ -4,7 +4,7 @@ */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { GridItemHTMLElement, GridStackNode, GridStackElement, DDUIData, DDDragInOpt, GridStackPosition } from './types'; +import { GridItemHTMLElement, GridStackNode, GridStackElement, DDUIData, DDDragInOpt, GridStackPosition, dragInDefaultOptions } from './types'; import { GridStack } from './gridstack'; import { Utils } from './utils'; import { DDManager } from './dd-manager'; @@ -417,28 +417,20 @@ GridStack.prototype._setupRemoveDrop = function(this: GridStack): GridStack { /** * 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 + * Called during GridStack.init() as options, but can also be called directly (last param are used) in case the toolbar * is dynamically create and needs to change later. **/ -GridStack.setupDragIn = function(this: GridStack, _dragIn?: string, _dragInOptions?: DDDragInOpt) { - let dragIn: string; - let dragInOptions: DDDragInOpt; - const dragInDefaultOptions: DDDragInOpt = { - handle: '.grid-stack-item-content', - appendTo: 'body', - // revert: 'invalid', - // scroll: false, - }; +GridStack.setupDragIn = function(this: GridStack, dragIn?: string, dragInOptions?: DDDragInOpt) { + if (dragInOptions?.pause !== undefined) { + DDManager.pauseDrag = dragInOptions.pause; + } - // cache in the passed in values (form grid init?) so they don't have to resend them each time - if (_dragIn) { - dragIn = _dragIn; - dragInOptions = {...dragInDefaultOptions, ...(_dragInOptions || {})}; + if (typeof dragIn === 'string') { + dragInOptions = {...dragInDefaultOptions, ...(dragInOptions || {})}; + Utils.getElements(dragIn).forEach(el => { + if (!dd.isDraggable(el)) dd.dragIn(el, dragInOptions); + }); } - if (typeof dragIn !== 'string') return; - Utils.getElements(dragIn).forEach(el => { - if (!dd.isDraggable(el)) dd.dragIn(el, dragInOptions); - }); } /** @internal prepares the element for drag&drop **/ @@ -483,6 +475,7 @@ GridStack.prototype._prepareDragDropByNode = function(this: GridStack, node: Gri let onEndMoving = (event: Event) => { this.placeholder.remove(); delete node._moving; + delete node._event; delete node._lastTried; // if the item has moved to another grid, we're done here @@ -613,7 +606,8 @@ GridStack.prototype._leave = function(this: GridStack, el: GridItemHTMLElement, } /** @internal called when item is being dragged/resized */ -GridStack.prototype._dragOrResize = function(this: GridStack, el: GridItemHTMLElement, event: Event, ui: DDUIData, node: GridStackNode, cellWidth: number, cellHeight: number) { +GridStack.prototype._dragOrResize = function(this: GridStack, el: GridItemHTMLElement, event: MouseEvent, ui: DDUIData, + node: GridStackNode, cellWidth: number, cellHeight: number) { let p = {...node._orig}; // could be undefined (_isExternal) which is ok (drag only set x,y and w,h will default to node value) let resizing: boolean; let mLeft = this.opts.marginLeft as number, @@ -659,7 +653,7 @@ GridStack.prototype._dragOrResize = function(this: GridStack, el: GridItemHTMLEl } else if (event.type === 'resize') { if (p.x < 0) return; // Scrolling page if needed - Utils.updateScrollResize(event as MouseEvent, el, cellHeight); + Utils.updateScrollResize(event, el, cellHeight); // get new size p.w = Math.round((ui.size.width - mLeft) / cellWidth); @@ -676,6 +670,7 @@ GridStack.prototype._dragOrResize = function(this: GridStack, el: GridItemHTMLEl resizing = true; } + node._event = event; node._lastTried = p; // set as last tried (will nuke if we go there) let rect: GridStackPosition = { // screen pix of the dragged box x: ui.position.left + mLeft, @@ -781,11 +776,12 @@ GridStack.prototype.enableResize = function(this: GridStack, doEnable: boolean): } /** removes any drag&drop present (called during destroy) */ -GridStack.prototype._removeDD = function(this: GridStack, el: GridItemHTMLElement): GridStack { +GridStack.prototype._removeDD = function(this: GridStack, el: DDElementHost): GridStack { dd.draggable(el, 'destroy').resizable(el, 'destroy'); if (el.gridstackNode) { delete el.gridstackNode._initDD; // reset our DD init flag } + delete el.ddElement; return this; } diff --git a/src/dd-manager.ts b/src/dd-manager.ts index b9267542f..3e0a72362 100644 --- a/src/dd-manager.ts +++ b/src/dd-manager.ts @@ -11,6 +11,9 @@ import { DDResizable } from './dd-resizable'; * globals that are shared across Drag & Drop instances */ export class DDManager { + /** if set (true | in msec), dragging placement (collision) will only happen after a pause by the user*/ + public static pauseDrag: boolean | number; + /** true if a mouse down event was handled */ public static mouseHandled: boolean; diff --git a/src/gridstack-engine.ts b/src/gridstack-engine.ts index e14fe6dd5..6165026b9 100644 --- a/src/gridstack-engine.ts +++ b/src/gridstack-engine.ts @@ -132,8 +132,8 @@ export class GridStackEngine { return this.nodes.filter(n => n !== skip && n !== skip2 && Utils.isIntercepted(n, area)); } - /** does a pixel coverage collision, returning the node that has the most coverage that is >50% mid line */ - public collideCoverage(node: GridStackNode, o: GridStackMoveOpts, collides: GridStackNode[]): GridStackNode { + /** does a pixel coverage collision based on where we started, returning the node that has the most coverage that is >50% mid line */ + protected directionCollideCoverage(node: GridStackNode, o: GridStackMoveOpts, collides: GridStackNode[]): GridStackNode { if (!o.rect || !node._rect) return; let r0 = node._rect; // where started let r = {...o.rect}; // where we are @@ -179,6 +179,23 @@ export class GridStackEngine { return collide; } + /** does a pixel coverage returning the node that has the most coverage by area */ + /* + protected collideCoverage(r: GridStackPosition, collides: GridStackNode[]): {collide: GridStackNode, over: number} { + let collide: GridStackNode; + let overMax = 0; + collides.forEach(n => { + if (n.locked || !n._rect) return; + let over = Utils.areaIntercept(r, n._rect); + if (over > overMax) { + overMax = over; + collide = n; + } + }); + return {collide, over: overMax}; + } + */ + /** called to cache the nodes pixel rectangles used for collision detection during drag */ public cacheRects(w: number, h: number, top: number, right: number, bottom: number, left: number): GridStackEngine { @@ -604,7 +621,10 @@ export class GridStackEngine { /** return true if the passed in node was actually moved (checks for no-op and locked) */ public moveNode(node: GridStackNode, o: GridStackMoveOpts): boolean { if (!node || /*node.locked ||*/ !o) return false; - if (o.pack === undefined) o.pack = true; + let wasUndefinedPack: boolean; + if (o.pack === undefined) { + wasUndefinedPack = o.pack = true; + } // constrain the passed in values and check if we're still changing our node if (typeof o.x !== 'number') { o.x = node.x; } @@ -624,12 +644,27 @@ export class GridStackEngine { let collides = this.collideAll(node, nn, o.skip); let needToMove = true; if (collides.length) { - // now check to make sure we actually collided over 50% surface area while dragging - let collide = node._moving && !o.nested ? this.collideCoverage(node, o, collides) : collides[0]; + let activeDrag = node._moving && !o.nested; + // check to make sure we actually collided over 50% surface area while dragging + let collide = activeDrag ? this.directionCollideCoverage(node, o, collides) : collides[0]; + // if we're enabling creation of sub-grids on the fly, see if we're covering 80% of either one, if we didn't already do that + let subOpt = node.grid.opts.subGrid; + if (activeDrag && collide && subOpt?.createDynamic && !subOpt.isTemp) { + let over = Utils.areaIntercept(o.rect, collide._rect); + let a1 = Utils.area(o.rect); + let a2 = Utils.area(collide._rect); + let perc = over / (a1 < a2 ? a1 : a2); + if (perc > .8) { + collide.grid.makeSubGrid(collide.el, undefined, node); + collide = undefined; + } + } + if (collide) { needToMove = !this._fixCollisions(node, nn, collide, o); // check if already moved... } else { needToMove = false; // we didn't cover >50% for a move, skip... + if (wasUndefinedPack) delete o.pack; } } @@ -680,15 +715,7 @@ export class GridStackEngine { let w: GridStackNode = {...n}; // use layout info instead if set if (wl) { w.x = wl.x; w.y = wl.y; w.w = wl.w; } - // delete internals - for (let key in w) { if (key[0] === '_' || w[key] === null || w[key] === undefined ) delete w[key]; } - delete w.grid; - if (!saveElement) delete w.el; - // delete default values (will be re-created on read) - if (!w.autoPosition) delete w.autoPosition; - if (!w.noResize) delete w.noResize; - if (!w.noMove) delete w.noMove; - if (!w.locked) delete w.locked; + Utils.removeInternalForSave(w, !saveElement); list.push(w); }); return list; diff --git a/src/gridstack.ts b/src/gridstack.ts index c5f7251f0..03a8a9068 100644 --- a/src/gridstack.ts +++ b/src/gridstack.ts @@ -7,8 +7,8 @@ */ import { GridStackEngine } from './gridstack-engine'; import { Utils, HeightData, obsolete } from './utils'; -import { GridDefaults, ColumnOptions, GridItemHTMLElement, GridStackElement, GridStackEventHandlerCallback, - GridStackNode, GridStackOptions, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition } from './types'; +import { gridDefaults, ColumnOptions, GridItemHTMLElement, GridStackElement, GridStackEventHandlerCallback, + GridStackNode, GridStackOptions, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition, GridStackSubOptions } from './types'; // export all dependent file as well to make it easier for users to just import the main file export * from './types'; @@ -174,7 +174,7 @@ export class GridStack { placeholderChild.innerHTML = this.opts.placeholderText; } this._placeholder = document.createElement('div'); - this._placeholder.classList.add(this.opts.placeholderClass, GridDefaults.itemClass, this.opts.itemClass); + this._placeholder.classList.add(this.opts.placeholderClass, gridDefaults.itemClass, this.opts.itemClass); this.placeholder.appendChild(placeholderChild); } return this._placeholder; @@ -235,16 +235,16 @@ export class GridStack { } // elements DOM attributes override any passed options (like CSS style) - merge the two together - let defaults: GridStackOptions = {...Utils.cloneDeep(GridDefaults), - column: Utils.toNumber(el.getAttribute('gs-column')) || GridDefaults.column, - minRow: rowAttr ? rowAttr : Utils.toNumber(el.getAttribute('gs-min-row')) || GridDefaults.minRow, - maxRow: rowAttr ? rowAttr : Utils.toNumber(el.getAttribute('gs-max-row')) || GridDefaults.maxRow, - staticGrid: Utils.toBool(el.getAttribute('gs-static')) || GridDefaults.staticGrid, + let defaults: GridStackOptions = {...Utils.cloneDeep(gridDefaults), + column: Utils.toNumber(el.getAttribute('gs-column')) || gridDefaults.column, + minRow: rowAttr ? rowAttr : Utils.toNumber(el.getAttribute('gs-min-row')) || gridDefaults.minRow, + maxRow: rowAttr ? rowAttr : Utils.toNumber(el.getAttribute('gs-max-row')) || gridDefaults.maxRow, + staticGrid: Utils.toBool(el.getAttribute('gs-static')) || gridDefaults.staticGrid, draggable: { - handle: (opts.handleClass ? '.' + opts.handleClass : (opts.handle ? opts.handle : '')) || GridDefaults.draggable.handle, + handle: (opts.handleClass ? '.' + opts.handleClass : (opts.handle ? opts.handle : '')) || gridDefaults.draggable.handle, }, removableOptions: { - accept: opts.itemClass ? '.' + opts.itemClass : GridDefaults.removableOptions.accept, + accept: opts.itemClass ? '.' + opts.itemClass : gridDefaults.removableOptions.accept, }, }; if (el.getAttribute('gs-animate')) { // default to true, but if set to false use that instead @@ -269,12 +269,12 @@ export class GridStack { } // check if we're been nested, and if so update our style and keep pointer around (used during save) - let parentGridItemEl = Utils.closestByClass(this.el, GridDefaults.itemClass) as GridItemHTMLElement; + let parentGridItemEl = Utils.closestByClass(this.el, gridDefaults.itemClass) as GridItemHTMLElement; if (parentGridItemEl && parentGridItemEl.gridstackNode) { this._isNested = parentGridItemEl.gridstackNode; this._isNested.subGrid = this; - parentGridItemEl.classList.add('grid-stack-nested'); this.el.classList.add('grid-stack-nested'); + parentGridItemEl.classList.add('grid-stack-sub-grid'); } this._isAutoCellHeight = (this.opts.cellHeight === 'auto'); @@ -283,7 +283,7 @@ export class GridStack { this.cellHeight(undefined, false); } else { // append unit if any are set - if (typeof this.opts.cellHeight == 'number' && this.opts.cellHeightUnit && this.opts.cellHeightUnit !== GridDefaults.cellHeightUnit) { + if (typeof this.opts.cellHeight == 'number' && this.opts.cellHeightUnit && this.opts.cellHeightUnit !== gridDefaults.cellHeightUnit) { this.opts.cellHeight = this.opts.cellHeight + this.opts.cellHeightUnit; delete this.opts.cellHeightUnit; } @@ -350,6 +350,10 @@ export class GridStack { delete this.opts.dragIn; delete this.opts.dragInOptions; + // dynamic grids require pausing during drag to detect over to nest vs push + if (this.opts.subGrid?.createDynamic && !DDManager.pauseDrag) DDManager.pauseDrag = true; + if (this.opts.draggable?.pause !== undefined) DDManager.pauseDrag = this.opts.draggable.pause; + this._setupRemoveDrop(); this._setupAcceptWidget(); this._updateWindowResizeEvent(); @@ -371,7 +375,6 @@ export class GridStack { * @param options widget position/size options (optional, and ignore if first param is already option) - see GridStackWidget */ public addWidget(els?: GridStackWidget | GridStackElement, options?: GridStackWidget): GridItemHTMLElement { - // support legacy call for now ? if (arguments.length > 2) { console.warn('gridstack.ts: `addWidget(el, x, y, width...)` is deprecated. Use `addWidget({x, y, w, content, ...})`. It will be removed soon'); @@ -420,19 +423,9 @@ export class GridStack { this._prepareElement(el, true, options); this._updateContainerHeight(); - // check if nested grid definition is present - if (node.subGrid && !(node.subGrid as GridStack).el) { // see if there is a sub-grid to create too - // if column special case it set, remember that flag and set default - let autoColumn: boolean; - let ops = node.subGrid as GridStackOptions; - if (ops.column === 'auto') { - ops.column = node.w; - ops.disableOneColumnMode = true; // driven by parent - autoColumn = true; - } - let content = node.el.querySelector('.grid-stack-item-content') as HTMLElement; - node.subGrid = GridStack.addGrid(content, node.subGrid as GridStackOptions); - if (autoColumn) { node.subGrid._autoColumn = true; } + // see if there is a sub-grid to create too + if (node.subGrid) { + this.makeSubGrid(node.el, undefined, undefined, false); } this._triggerAddEvent(); @@ -441,6 +434,78 @@ export class GridStack { return el; } + /** + * Convert an existing gridItem element into a sub-grid with the given (optional) options, else inherit them + * from the parent subGrid options. + * @param el gridItem element to convert + * @param ops (optional) sub-grid options, else default to node, then parent settings, else defaults + * @param nodeToAdd (optional) node to add to the newly created sub grid (used when dragging over existing regular item) + */ + public makeSubGrid(el: GridItemHTMLElement, ops?: GridStackSubOptions, nodeToAdd?: GridStackNode, saveContent = true): GridStack { + let node = el.gridstackNode; + if (!node) { + node = this.makeWidget(el).gridstackNode; + } + if ((node.subGrid as GridStack)?.el) return node.subGrid as GridStack; // already done + + ops = Utils.cloneDeep(ops || node.subGrid as GridStackOptions || this.opts.subGrid || this.opts); + node.subGrid = ops; + + // if column special case it set, remember that flag and set default + let autoColumn: boolean; + if (ops.column === 'auto') { + autoColumn = true; + ops.column = Math.max(node.w || 1, nodeToAdd?.w || 1); + ops.disableOneColumnMode = true; // driven by parent + } + + // if we're converting an existing full item, move over the content to be the first sub item in the new grid + let content = node.el.querySelector('.grid-stack-item-content') as HTMLElement; + let newItem: HTMLElement; + let newItemOpt: GridStackNode; + if (saveContent) { + this._removeDD(el); // remove any (content) D7D + let doc = document.implementation.createHTMLDocument(''); // IE needs a param + doc.body.innerHTML = `
    `; + newItem = doc.body.children[0] as HTMLElement; + newItem.appendChild(content); + newItemOpt = {...node, x:0, y:0}; + Utils.removeInternalForSave(newItemOpt); + delete newItemOpt.subGrid; + doc.body.innerHTML = `
    `; + content = doc.body.children[0] as HTMLElement; + node.el.appendChild(content); + this._prepareDragDropByNode(node); // ... and restore original D&D + } + + // if we're adding an additional item, make the container large enough to have them both + if (nodeToAdd) { + let w = autoColumn ? ops.column : node.w; + let h = node.h + nodeToAdd.h; + this.update(node.el, {w, h}); + ops.isTemp = true; // prevent re-nesting as we add over + } + + let grid = node.subGrid = GridStack.addGrid(content, ops); + if (autoColumn) node.subGrid._autoColumn = true; + + // add the original content back as a child of hte newly created grid + if (saveContent) { + grid.addWidget(newItem, newItemOpt); + } + + // now add any additional node + if (nodeToAdd) { + if (nodeToAdd._moving) { + // create an artificial event even for the just created grid to receive this item + window.setTimeout(() => Utils.simulateMouseEvent(nodeToAdd._event, 'mouseenter', grid.el), 0); + } else { + grid.addWidget(node.el, node); + } + } + return grid; + } + /** /** * saves the current layout returning a list of widgets for serialization which might include any nested grids. @@ -493,7 +558,7 @@ export class GridStack { } else { delete o.alwaysShowResizeHandle; } - Utils.removeInternalAndSame(o, GridDefaults); + Utils.removeInternalAndSame(o, gridDefaults); o.children = list; return o; } @@ -1140,8 +1205,8 @@ export class GridStack { } /** @internal */ - protected _triggerEvent(name: string, data?: GridStackNode[]): GridStack { - let event = data ? new CustomEvent(name, {bubbles: false, detail: data}) : new Event(name); + protected _triggerEvent(type: string, data?: GridStackNode[]): GridStack { + let event = data ? new CustomEvent(type, {bubbles: false, detail: data}) : new Event(type); this.el.dispatchEvent(event); return this; } @@ -1236,13 +1301,13 @@ export class GridStack { // } this.el.setAttribute('gs-current-row', String(row)); if (row === 0) { - this.el.style.removeProperty('height'); + this.el.style.removeProperty('min-height'); return this; } let cellHeight = this.opts.cellHeight as number; let unit = this.opts.cellHeightUnit; if (!cellHeight) return this; - this.el.style.height = row * cellHeight + unit; + this.el.style.minHeight = row * cellHeight + unit; return this; } @@ -1493,8 +1558,8 @@ export class GridStack { /** * 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 - * is dynamically create and needs to change later. + * Called during GridStack.init() as options, but can also be called directly (last param are used) in case the toolbar + * is dynamically create and needs to be set later. * @param dragIn string selector (ex: '.sidebar .grid-stack-item') * @param dragInOptions options - see DDDragInOpt. (default: {handle: '.grid-stack-item-content', appendTo: 'body'} **/ @@ -1564,6 +1629,7 @@ export class GridStack { * TODO: while we can generate a gridstack-static.js at smaller size - saves about 31k (41k -> 72k) * I don't know how to generate the DD only code at the remaining 31k to delay load as code depends on Gridstack.ts */ -import { DDGridStack } from './dd-gridstack'; +import { DDGridStack } from './dd-gridstack'; // not called, but compiled in import { isTouch } from './dd-touch'; +import { DDManager } from './dd-manager'; export * from './dd-gridstack'; diff --git a/src/types.ts b/src/types.ts index 05fa0d3e1..f9277bd2a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,7 +7,7 @@ import { GridStack } from './gridstack'; import { GridStackEngine } from './gridstack-engine'; // default values for grid options - used during init and when saving out -export const GridDefaults: GridStackOptions = { +export const gridDefaults: GridStackOptions = { alwaysShowResizeHandle: 'mobile', animate: true, auto: true, @@ -41,6 +41,14 @@ export const GridDefaults: GridStackOptions = { // styleInHead: false, }; +/** default dragIn options */ +export const dragInDefaultOptions: DDDragInOpt = { + handle: '.grid-stack-item-content', + appendTo: 'body', + // revert: 'invalid', + // scroll: false, +}; + /** different layout options when changing # of columns, * including a custom function that takes new/old column count, and array of new/old positions * Note: new list may be partially already filled if we have a cache of the layout at that size and new items were added later. @@ -130,13 +138,10 @@ export interface GridStackOptions { /** allows to override UI draggable options. (default?: { handle?: '.grid-stack-item-content', appendTo?: 'body' }) */ draggable?: DDDragOpt; - /** allows to drag external items using this selector - see dragInOptions. (default: undefined) */ + /** @internal Use `GridStack.setupDragIn()` instead (global, not per grid). old way to allow external items to be draggable. (default: undefined) */ dragIn?: string; - /** allows to drag external items using these options. See `GridStack.setupDragIn()` instead (not per grid really). - * (default?: { handle: '.grid-stack-item-content', appendTo: 'body' }) - * helper can be 'clone' or your own function (set what the drag/dropped item will be instead) - */ + /** @internal Use `GridStack.setupDragIn()` instead (global, not per grid). old way to allow external items to be draggable. (default: undefined) */ dragInOptions?: DDDragInOpt; /** let user drag nested grid items out of a parent or not (default true - not supported yet) */ @@ -232,6 +237,17 @@ export interface GridStackOptions { /** if `true` will add style element to `` otherwise will add it to element's parent node (default `false`). */ styleInHead?: boolean; + + /** list of differences in options for automatically created sub-grids under us */ + subGrid?: GridStackSubOptions; +} + +/** additional prop that only apply to sub-grids */ +export interface GridStackSubOptions extends GridStackOptions { + /** enable/disable the creation of sub-grids on the fly (drop over other items) */ + createDynamic?: boolean; + /** true if we got created by drag over gesture, so we can removed on drag out (temporary) */ + isTemp?: boolean; } /** options used during GridStackEngine.moveNode() */ @@ -295,7 +311,7 @@ export interface GridStackWidget extends GridStackPosition { /** html to append inside as content */ content?: string; /** optional nested grid options and list of children, which then turns into actual instance at runtime */ - subGrid?: GridStackOptions | GridStack; + subGrid?: GridStackSubOptions | GridStack; } /** Drag&Drop resize options */ @@ -321,16 +337,18 @@ export interface DDDragOpt { handle?: string; /** default to 'body' */ appendTo?: string; + /** if set (true | msec), dragging placement (collision) will only happen after a pause by the user. Note: this is Global */ + pause?: boolean | number; /** default to `true` */ // scroll?: boolean; /** parent constraining where item can be dragged out from (default: null = no constrain) */ // containment?: string; } export interface DDDragInOpt extends DDDragOpt { - /** used when dragging item from the outside, and canceling (ex: 'invalid' or your own method)*/ - // revert?: string | ((event: Event) => HTMLElement); - /** helper function when dropping (ex: 'clone' or your own method) */ - helper?: string | ((event: Event) => HTMLElement); + /** helper function when dropping (ex: 'clone' or your own method) */ + helper?: string | ((event: Event) => HTMLElement); + /** used when dragging item from the outside, and canceling (ex: 'invalid' or your own method)*/ + // revert?: string | ((event: Event) => HTMLElement); } export interface Size { @@ -375,6 +393,8 @@ export interface GridStackNode extends GridStackWidget { _isAboutToRemove?: boolean; /** @internal true if item came from outside of the grid -> actual item need to be moved over */ _isExternal?: boolean; + /** @internal Mouse event that's causing moving|resizing */ + _event?: MouseEvent; /** @internal moving vs resizing */ _moving?: boolean; /** @internal true if we jumped down past item below (one time jump so we don't have to totally pass it) */ diff --git a/src/utils.ts b/src/utils.ts index 16aa07958..0874ea255 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -100,6 +100,23 @@ export class Utils { static isTouching(a: GridStackPosition, b: GridStackPosition): boolean { return Utils.isIntercepted(a, {x: b.x-0.5, y: b.y-0.5, w: b.w+1, h: b.h+1}) } + + /** returns the area a and b overlap */ + static areaIntercept(a: GridStackPosition, b: GridStackPosition): number { + let x0 = (a.x > b.x) ? a.x : b.x; + let x1 = (a.x+a.w < b.x+b.w) ? a.x+a.w : b.x+b.w; + if (x1 <= x0) return 0; // no overlap + let y0 = (a.y > b.y) ? a.y : b.y; + let y1 = (a.y+a.h < b.y+b.h) ? a.y+a.h : b.y+b.h; + if (y1 <= y0) return 0; // no overlap + return (x1-x0) * (y1-y0); + } + + /** returns the area */ + static area(a: GridStackPosition): number { + return a.w * a.h; + } + /** * Sorts array of nodes * @param nodes array to sort @@ -255,6 +272,18 @@ export class Utils { } } + /** removes internal fields '_' and default values for saving */ + static removeInternalForSave(n: GridStackNode, removeEl = true) { + for (let key in n) { if (key[0] === '_' || n[key] === null || n[key] === undefined ) delete n[key]; } + delete n.grid; + if (removeEl) delete n.el; + // delete default values (will be re-created on read) + if (!n.autoPosition) delete n.autoPosition; + if (!n.noResize) delete n.noResize; + if (!n.noMove) delete n.noMove; + if (!n.locked) delete n.locked; + } + /** return the closest parent (or itself) matching the given class */ static closestByClass(el: HTMLElement, name: string): HTMLElement { while (el) { @@ -464,6 +493,29 @@ export class Utils { return {...evt, ...obj} as unknown as T; } + /** copies the MouseEvent properties and sends it as another event to the given target */ + public static simulateMouseEvent(e: MouseEvent, simulatedType: string, target?: EventTarget) { + const simulatedEvent = document.createEvent('MouseEvents'); + simulatedEvent.initMouseEvent( + simulatedType, // type + true, // bubbles + true, // cancelable + window, // view + 1, // detail + e.screenX, // screenX + e.screenY, // screenY + e.clientX, // clientX + e.clientY, // clientY + e.ctrlKey, // ctrlKey + e.altKey, // altKey + e.shiftKey, // shiftKey + e.metaKey, // metaKey + 0, // button + e.target // relatedTarget + ); + (target || e.target).dispatchEvent(simulatedEvent); + } + /** 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)