Skip to content

Commit

Permalink
auto-size height to fit content
Browse files Browse the repository at this point in the history
* fix gridstack#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
* added demo showing behavior
* fixed sizing event to use much more accurate ResizeObserver on grid rather than generic window.addEventListener('resize')
  • Loading branch information
adumesny committed Aug 7, 2023
1 parent d541d82 commit 4fc6a7b
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 49 deletions.
43 changes: 43 additions & 0 deletions demo/fitToContent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!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>FitToContent demo</title>

<link rel="stylesheet" href="demo.css"/>
<script src="../dist/gridstack-all.js"></script>
<style type="text/css">
.grid-stack-item-content {
text-align: unset;
}
</style>
</head>
<body>
<div class="container">
<h1>Cell FitToContent options demo</h1>
<p>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) </p>
<br>
<div class="grid-stack"></div>
</div>
<script type="text/javascript">
let opts = {
margin: 5,
cellHeight: 50,
fitToContent: true, // default to make them all fit
// cellHeightThrottle: 100, // ms before fitToContent happens
}
let grid = GridStack.init(opts);
let text ='some very large content that will normally not fit in the window.'
text = text + text;
let items = [
{x:0, y:0, w:2, content: `<div>A: ${text}</div>`},
{x:2, y:0, w:1, h:2, content: '<div>B: shrink</div>'}, // make taller than needed upfront
{x:3, y:0, w:2, fitToContent: false, content: `<div>C: WILL SCROLL. ${text}</div>`}, // prevent this from fitting testing
{x:0, y:1, w:3, content: `<div>D: ${text} ${text}</div>`},
];
grid.load(items);
</script>
</body>
</html>
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ <h1>Demos</h1>
<li><a href="anijs.html">AniJS</a></li>
<li><a href="cell-height.html">Cell Height</a></li>
<li><a href="column.html">Column</a></li>
<li><a href="fitToContent.html">Fit To Content</a></li>
<li><a href="float.html">Float grid</a></li>
<li><a href="knockout.html">Knockout.js</a></li>
<li><a href="mobile.html">Mobile touch</a></li>
Expand Down
8 changes: 6 additions & 2 deletions doc/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Change log
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**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)
Expand Down Expand Up @@ -92,7 +93,10 @@ Change log

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 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.
Expand Down
2 changes: 2 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions src/gridstack.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
125 changes: 78 additions & 47 deletions src/gridstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -388,7 +389,7 @@ export class GridStack {

this._setupRemoveDrop();
this._setupAcceptWidget();
this._updateWindowResizeEvent();
this._updateResizeEvent();
}

/**
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 <div> 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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 4fc6a7b

Please sign in to comment.