diff --git a/demo/fitToContent.html b/demo/fitToContent.html new file mode 100644 index 000000000..869ad1348 --- /dev/null +++ b/demo/fitToContent.html @@ -0,0 +1,43 @@ + + + + + + + FitToContent demo + + + + + + +
+

Cell FitToContent options demo

+

new 9.x feature that size the items to fit their content height as to not have scroll bars (unless `fitToContent:false` in C: case)

+
+
+
+ + + diff --git a/demo/index.html b/demo/index.html index ceaef41dc..730190b20 100644 --- a/demo/index.html +++ b/demo/index.html @@ -12,6 +12,7 @@

Demos

  • AniJS
  • Cell Height
  • Column
  • +
  • Fit To Content
  • Float grid
  • Knockout.js
  • Mobile touch
  • diff --git a/doc/CHANGES.md b/doc/CHANGES.md index d7d5470db..d8949e691 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -5,7 +5,8 @@ Change log **Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)* -- [8.4.0-dev (2023-07-20)](#840-2023-07-20) +- [8.4.0-dev (TBD)](#840-dev-tbd) +- [8.4.0 (2023-07-20)](#840-2023-07-20) - [8.3.0 (2023-06-13)](#830-2023-06-13) - [8.2.3 (2023-06-11)](#823-2023-06-11) - [8.2.1 (2023-05-26)](#821-2023-05-26) @@ -92,7 +93,10 @@ Change log -## 8.4.0-dev (2023-07-20) +## 8.4.0-dev (TBD) +- feat [#404](https://github.com/gridstack/gridstack.js/issues/404) added `GridStackOptions.fitToContent` and `GridStackWidget.fitToContent` to make gridItems size themselves to their content (no scroll bar), calling `GridStack.resizeToContent(el)` whenever the grid or item is resized. + +## 8.4.0 (2023-07-20) * feat [#2378](https://github.com/gridstack/gridstack.js/pull/2378) attribute `DDRemoveOpt.decline` to deny the removal of a specific class. * fix: dragging onto trash now calls removeWidget() and therefore `GridStack.addRemoveCB` (for component cleanup) * feat: `load()` support re-order loading without explicit coordinates (`autoPosition` or missing `x,y`) uses passed order. diff --git a/doc/README.md b/doc/README.md index 8bca6ddaa..07778d763 100644 --- a/doc/README.md +++ b/doc/README.md @@ -99,6 +99,7 @@ gridstack.js API - `draggable` - allows to override draggable options - see `DDDragOpt`. (default: `{handle: '.grid-stack-item-content', appendTo: 'body', scroll: true}`) - `dragOut` to let user drag nested grid items out of a parent or not (default false) See [example](http://gridstackjs.com/demo/nested.html) - `engineClass` - the type of engine to create (so you can subclass) default to GridStackEngine +- `fitToContent` - make gridItems size themselves to their content, calling `resizeToContent(el)` whenever the grid or item is resized. - `float` - enable floating widgets (default: `false`) See [example](http://gridstackjs.com/demo/float.html) - `handle` - draggable handle selector (default: `'.grid-stack-item-content'`) - `handleClass` - draggable handle class (e.g. `'grid-stack-item-content'`). If set `handle` is ignored (default: `null`) @@ -158,6 +159,7 @@ You need to add `noResize` and `noMove` attributes to completely lock the widget - `noMove` - disable element moving - `id`- (number | string) good for quick identification (for example in change event) - `content` - (string) html content to be added when calling `grid.load()/addWidget()` as content inside the item +- `fitToContent` - make gridItem size itself to the content, calling `GridStack.resizeToContent(el)` whenever the grid or item is resized. - `subGrid`?: GridStackOptions - optional nested grid options and list of children - `subGridDynamic`?: boolean - enable/disable the creation of sub-grids on the fly by dragging items completely over others (nest) vs partially (push). Forces `DDDragOpt.pause=true` to accomplish that. diff --git a/src/gridstack.scss b/src/gridstack.scss index c6658f0b1..cc4934eed 100644 --- a/src/gridstack.scss +++ b/src/gridstack.scss @@ -51,6 +51,9 @@ $animation_speed: .3s !default; overflow-x: hidden; overflow-y: auto; } + &.fit-to-content > .grid-stack-item-content { + overflow-y: hidden; + } } .grid-stack-item { diff --git a/src/gridstack.ts b/src/gridstack.ts index 0dea9b45f..a6af8a2c8 100644 --- a/src/gridstack.ts +++ b/src/gridstack.ts @@ -9,7 +9,7 @@ import { GridStackEngine } from './gridstack-engine'; import { Utils, HeightData, obsolete } from './utils'; import { gridDefaults, ColumnOptions, GridItemHTMLElement, GridStackElement, GridStackEventHandlerCallback, GridStackNode, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition, GridStackOptions, - dragInDefaultOptions, GridStackEventHandler, GridStackNodesHandler, AddRemoveFcn, SaveFcn, CompactOptions } from './types'; + dragInDefaultOptions, GridStackEventHandler, GridStackNodesHandler, AddRemoveFcn, SaveFcn, CompactOptions, GridStackMoveOpts } from './types'; /* * and include D&D by default @@ -203,6 +203,7 @@ export class GridStack { public parentGridItem?: GridStackNode; protected static engineClass: typeof GridStackEngine; + protected resizeObserver: ResizeObserver; /** @internal unique class name for our generated CSS style sheet */ protected _styleSheetClass?: string; @@ -235,10 +236,10 @@ export class GridStack { protected _styles: GridCSSStyleSheet; /** @internal flag to keep cells square during resize */ protected _isAutoCellHeight: boolean; - /** @internal track event binding to window resize so we can remove */ - protected _windowResizeBind: () => void; /** @internal limit auto cell resizing method */ - protected _cellHeightThrottle: () => void; + protected _sizeThrottle: () => void; + /** @internal limit auto cell resizing method */ + protected prevWidth: number; /** @internal true when loading items to insert first rather than append */ protected _insertNotAppend: boolean; /** @internal extra row added when dragging at the bottom of the grid */ @@ -388,7 +389,7 @@ export class GridStack { this._setupRemoveDrop(); this._setupAcceptWidget(); - this._updateWindowResizeEvent(); + this._updateResizeEvent(); } /** @@ -772,7 +773,7 @@ export class GridStack { if (update && val !== undefined) { if (this._isAutoCellHeight !== (val === 'auto')) { this._isAutoCellHeight = (val === 'auto'); - this._updateWindowResizeEvent(); + this._updateResizeEvent(); } } if (val === 'initial' || val === 'auto') { val = undefined; } @@ -857,6 +858,9 @@ export class GridStack { } this.engine.columnChanged(oldColumn, column, domNodes, layout); if (this._isAutoCellHeight) this.cellHeight(); + // this.engine.nodes.forEach(n => { + // if (Utils.shouldFitToContent(n)) this.resizeToContent(n.el); + // }); // and trigger our event last... this._ignoreLayoutsNodeChange = true; // skip layout update @@ -886,7 +890,7 @@ export class GridStack { public destroy(removeDOM = true): GridStack { if (!this.el) return; // prevent multiple calls this.offAll(); - this._updateWindowResizeEvent(true); + this._updateResizeEvent(true); this.setStatic(true, false); // permanently removes DD but don't set CSS class (we're going away) this.setAnimation(false); if (!removeDOM) { @@ -1227,14 +1231,7 @@ export class GridStack { Utils.sanitizeMinMax(n); // finally move the widget - if (m) { - this.engine.cleanNodes() - .beginUpdate(n) - .moveNode(n, m); - this._updateContainerHeight(); - this._triggerChangeEvent(); - this.engine.endUpdate(); - } + if (m) this.moveNode(n, m); if (changed) { // move will only update x,y,w,h so update the rest too this._writeAttr(el, n); } @@ -1245,6 +1242,37 @@ export class GridStack { return this; } + private moveNode(n: GridStackNode, m: GridStackMoveOpts) { + this.engine.cleanNodes() + .beginUpdate(n) + .moveNode(n, m); + this._updateContainerHeight(); + this._triggerChangeEvent(); + this.engine.endUpdate(); + } + + /** Updates widget height to match the content height to avoid v-scrollbar or dead space. + Note: this assumes only 1 child under '.grid-stack-item-content' (sized to gridItem minus padding) that is at the entire content size wanted */ + public resizeToContent(els: GridStackElement) { + GridStack.getElements(els).forEach(el => { + let n = el?.gridstackNode; + if (!n) return; + let height = el.clientHeight; + if (!height) return; // 0 when hidden, skip + const item = el.querySelector('.grid-stack-item-content'); + if (!item) return; + const itemH = item.clientHeight; + const wantedH = (item.firstChild as Element)?.clientHeight || itemH; // NOTE: clientHeight & getBoundingClientRect() is undefined for text and other leaf nodes. use
    container! + if (itemH === wantedH) return; + height += wantedH - itemH; + const cell = this.getCellHeight(); + if (!cell) return; + let h = Math.ceil(height / cell); + if (n.maxH && h > n.maxH) h = n.maxH; + if (h !== n.h) this.moveNode(n, {h}); + }); + } + /** * Updates the margins which will set all 4 sides at once - see `GridStackOptions.margin` for format options (CSS string format of 1,2,4 values or single number). * @param value margin value @@ -1450,6 +1478,7 @@ export class GridStack { if (!Utils.same(node, copy)) { this._writeAttr(el, node); } + if (Utils.shouldFitToContent(node)) el.classList.add('fit-to-content'); this._prepareDragDropByNode(node); return this; } @@ -1541,62 +1570,64 @@ export class GridStack { } /** - * called when we are being resized by the window - check if the one Column Mode needs to be turned on/off - * and remember the prev columns we used, or get our count from parent, as well as check for auto cell height (square) + * called when we are being resized - check if the one Column Mode needs to be turned on/off + * and remember the prev columns we used, or get our count from parent, as well as check for cellHeight==='auto' (square) + * or `fitToContent` gridItem options. */ - public onParentResize(): GridStack { - if (!this.el || !this.el.clientWidth) return; // return if we're gone or no size yet (will get called again) - let changedColumn = false; + public onResize(): GridStack { + if (!this.el?.clientWidth) return; // return if we're gone or no size yet (will get called again) + if (this.prevWidth === this.el.clientWidth) return; // no-op + this.prevWidth = this.el.clientWidth + // console.log('onResize ', this.el.clientWidth); // see if we're nested and take our column count from our parent.... + let columnChanged = false; if (this._autoColumn && this.parentGridItem) { if (this.opts.column !== this.parentGridItem.w) { - changedColumn = true; this.column(this.parentGridItem.w, 'none'); + columnChanged = true; } } else { // else check for 1 column in/out behavior let oneColumn = !this.opts.disableOneColumnMode && this.el.clientWidth <= this.opts.oneColumnSize; if ((this.opts.column === 1) !== oneColumn) { - changedColumn = true; - if (this.opts.animate) { this.setAnimation(false); } // 1 <-> 12 is too radical, turn off animation + // if (this.opts.animate) this.setAnimation(false); // 1 <-> 12 is too radical, turn off animation and we need it for fitToContent this.column(oneColumn ? 1 : this._prevColumn); - if (this.opts.animate) { this.setAnimation(true); } + // if (this.opts.animate) setTimeout(() => this.setAnimation(true)); + columnChanged = true; } } // make the cells content square again - if (this._isAutoCellHeight) { - if (!changedColumn && this.opts.cellHeightThrottle) { - if (!this._cellHeightThrottle) { - this._cellHeightThrottle = Utils.throttle(() => this.cellHeight(), this.opts.cellHeightThrottle); - } - this._cellHeightThrottle(); - } else { - // immediate update if we've changed column count or have no threshold - this.cellHeight(); - } - } + if (this._isAutoCellHeight) this.cellHeight(); - // finally update any nested grids + // update any nested grids, or items size this.engine.nodes.forEach(n => { - if (n.subGrid) n.subGrid.onParentResize() + if (n.subGrid) n.subGrid.onResize() + // update any gridItem height with fitToContent, but wait for DOM $animation_speed to settle if we changed column count + // TODO: is there a way to know what the final (post animation) size of the content will be so we can animate the column width and height together rather than sequentially ? + if (Utils.shouldFitToContent(n)) { + columnChanged ? setTimeout(() => this.resizeToContent(n.el), 300 + 10) : this.resizeToContent(n.el); + } }); return this; } - /** add or remove the window size event handler */ - protected _updateWindowResizeEvent(forceRemove = false): GridStack { + /** add or remove the grid element size event handler */ + protected _updateResizeEvent(forceRemove = false): GridStack { // only add event if we're not nested (parent will call us) and we're auto sizing cells or supporting oneColumn (i.e. doing work) - const workTodo = (this._isAutoCellHeight || !this.opts.disableOneColumnMode) && !this.parentGridItem; + // or supporting new fitToContent option. + const trackSize = !this.parentGridItem && (this._isAutoCellHeight || this.opts.fitToContent || !this.opts.disableOneColumnMode || this.engine.nodes.find(n => n.fitToContent)); - if (!forceRemove && workTodo && !this._windowResizeBind) { - this._windowResizeBind = this.onParentResize.bind(this); // so we can properly remove later - window.addEventListener('resize', this._windowResizeBind); - } else if ((forceRemove || !workTodo) && this._windowResizeBind) { - window.removeEventListener('resize', this._windowResizeBind); - delete this._windowResizeBind; // remove link to us so we can free + if (!forceRemove && trackSize && !this.resizeObserver) { + this._sizeThrottle = Utils.throttle(() => this.onResize(), this.opts.cellHeightThrottle); + this.resizeObserver = new ResizeObserver(entries => this._sizeThrottle()); + this.resizeObserver.observe(this.el); + } else if ((forceRemove || !trackSize) && this.resizeObserver) { + this.resizeObserver.disconnect(); + delete this.resizeObserver; + delete this._sizeThrottle; } return this; @@ -2285,7 +2316,7 @@ export class GridStack { node._lastUiPosition = ui.position; this.engine.cacheRects(cellWidth, cellHeight, mTop, mRight, mBottom, mLeft); delete node._skipDown; - if (resizing && node.subGrid) node.subGrid.onParentResize(); + if (resizing && node.subGrid) node.subGrid.onResize(); this._extraDragRow = 0;// @ts-ignore this._updateContainerHeight(); diff --git a/src/types.ts b/src/types.ts index b858b6113..e9da5f406 100644 --- a/src/types.ts +++ b/src/types.ts @@ -161,6 +161,10 @@ export interface GridStackOptions { /** the type of engine to create (so you can subclass) default to GridStackEngine */ engineClass?: typeof GridStackEngine; + /** set to true if all grid items (by default, but item can also override) height should be based on content size instead of WidgetItem.h to avoid v-scrollbars. + Note: this is still row based, not pixels, so it will use ceil(getBoundingClientRect().height / getCellHeight()) */ + fitToContent?: boolean; + /** enable floating widgets (default?: false) See example (http://gridstack.github.io/gridstack.js/demo/float.html) */ float?: boolean; @@ -316,6 +320,8 @@ export interface GridStackWidget extends GridStackPosition { id?: string; /** html to append inside as content */ content?: string; + /** local (grid) override - see GridStackOptions */ + fitToContent?: boolean; /** optional nested grid options and list of children, which then turns into actual instance at runtime to get options from */ subGridOpts?: GridStackOptions; } diff --git a/src/utils.ts b/src/utils.ts index c55ec8a3f..8cee09f2c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -102,6 +102,11 @@ export class Utils { return els; } + /** true if we should resize to content */ + static shouldFitToContent(n: GridStackNode): boolean { + return n.fitToContent || (n.grid?.opts.fitToContent && n.fitToContent !== false); + } + /** returns true if a and b overlap */ static isIntercepted(a: GridStackPosition, b: GridStackPosition): boolean { return !(a.y >= b.y + b.h || a.y + a.h <= b.y || a.x + a.w <= b.x || a.x >= b.x + b.w);