Skip to content

Commit 4fc6a7b

Browse files
committed
auto-size height to fit content
* fix #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')
1 parent d541d82 commit 4fc6a7b

File tree

8 files changed

+144
-49
lines changed

8 files changed

+144
-49
lines changed

demo/fitToContent.html

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<title>FitToContent demo</title>
8+
9+
<link rel="stylesheet" href="demo.css"/>
10+
<script src="../dist/gridstack-all.js"></script>
11+
<style type="text/css">
12+
.grid-stack-item-content {
13+
text-align: unset;
14+
}
15+
</style>
16+
</head>
17+
<body>
18+
<div class="container">
19+
<h1>Cell FitToContent options demo</h1>
20+
<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>
21+
<br>
22+
<div class="grid-stack"></div>
23+
</div>
24+
<script type="text/javascript">
25+
let opts = {
26+
margin: 5,
27+
cellHeight: 50,
28+
fitToContent: true, // default to make them all fit
29+
// cellHeightThrottle: 100, // ms before fitToContent happens
30+
}
31+
let grid = GridStack.init(opts);
32+
let text ='some very large content that will normally not fit in the window.'
33+
text = text + text;
34+
let items = [
35+
{x:0, y:0, w:2, content: `<div>A: ${text}</div>`},
36+
{x:2, y:0, w:1, h:2, content: '<div>B: shrink</div>'}, // make taller than needed upfront
37+
{x:3, y:0, w:2, fitToContent: false, content: `<div>C: WILL SCROLL. ${text}</div>`}, // prevent this from fitting testing
38+
{x:0, y:1, w:3, content: `<div>D: ${text} ${text}</div>`},
39+
];
40+
grid.load(items);
41+
</script>
42+
</body>
43+
</html>

demo/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ <h1>Demos</h1>
1212
<li><a href="anijs.html">AniJS</a></li>
1313
<li><a href="cell-height.html">Cell Height</a></li>
1414
<li><a href="column.html">Column</a></li>
15+
<li><a href="fitToContent.html">Fit To Content</a></li>
1516
<li><a href="float.html">Float grid</a></li>
1617
<li><a href="knockout.html">Knockout.js</a></li>
1718
<li><a href="mobile.html">Mobile touch</a></li>

doc/CHANGES.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Change log
55
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
66
**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*
77

8-
- [8.4.0-dev (2023-07-20)](#840-2023-07-20)
8+
- [8.4.0-dev (TBD)](#840-dev-tbd)
9+
- [8.4.0 (2023-07-20)](#840-2023-07-20)
910
- [8.3.0 (2023-06-13)](#830-2023-06-13)
1011
- [8.2.3 (2023-06-11)](#823-2023-06-11)
1112
- [8.2.1 (2023-05-26)](#821-2023-05-26)
@@ -92,7 +93,10 @@ Change log
9293

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

95-
## 8.4.0-dev (2023-07-20)
96+
## 8.4.0-dev (TBD)
97+
- 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.
98+
99+
## 8.4.0 (2023-07-20)
96100
* feat [#2378](https://github.com/gridstack/gridstack.js/pull/2378) attribute `DDRemoveOpt.decline` to deny the removal of a specific class.
97101
* fix: dragging onto trash now calls removeWidget() and therefore `GridStack.addRemoveCB` (for component cleanup)
98102
* feat: `load()` support re-order loading without explicit coordinates (`autoPosition` or missing `x,y`) uses passed order.

doc/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ gridstack.js API
9999
- `draggable` - allows to override draggable options - see `DDDragOpt`. (default: `{handle: '.grid-stack-item-content', appendTo: 'body', scroll: true}`)
100100
- `dragOut` to let user drag nested grid items out of a parent or not (default false) See [example](http://gridstackjs.com/demo/nested.html)
101101
- `engineClass` - the type of engine to create (so you can subclass) default to GridStackEngine
102+
- `fitToContent` - make gridItems size themselves to their content, calling `resizeToContent(el)` whenever the grid or item is resized.
102103
- `float` - enable floating widgets (default: `false`) See [example](http://gridstackjs.com/demo/float.html)
103104
- `handle` - draggable handle selector (default: `'.grid-stack-item-content'`)
104105
- `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
158159
- `noMove` - disable element moving
159160
- `id`- (number | string) good for quick identification (for example in change event)
160161
- `content` - (string) html content to be added when calling `grid.load()/addWidget()` as content inside the item
162+
- `fitToContent` - make gridItem size itself to the content, calling `GridStack.resizeToContent(el)` whenever the grid or item is resized.
161163
- `subGrid`?: GridStackOptions - optional nested grid options and list of children
162164
- `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.
163165

src/gridstack.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ $animation_speed: .3s !default;
5151
overflow-x: hidden;
5252
overflow-y: auto;
5353
}
54+
&.fit-to-content > .grid-stack-item-content {
55+
overflow-y: hidden;
56+
}
5457
}
5558

5659
.grid-stack-item {

src/gridstack.ts

Lines changed: 78 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { GridStackEngine } from './gridstack-engine';
99
import { Utils, HeightData, obsolete } from './utils';
1010
import { gridDefaults, ColumnOptions, GridItemHTMLElement, GridStackElement, GridStackEventHandlerCallback,
1111
GridStackNode, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition, GridStackOptions,
12-
dragInDefaultOptions, GridStackEventHandler, GridStackNodesHandler, AddRemoveFcn, SaveFcn, CompactOptions } from './types';
12+
dragInDefaultOptions, GridStackEventHandler, GridStackNodesHandler, AddRemoveFcn, SaveFcn, CompactOptions, GridStackMoveOpts } from './types';
1313

1414
/*
1515
* and include D&D by default
@@ -203,6 +203,7 @@ export class GridStack {
203203
public parentGridItem?: GridStackNode;
204204

205205
protected static engineClass: typeof GridStackEngine;
206+
protected resizeObserver: ResizeObserver;
206207

207208
/** @internal unique class name for our generated CSS style sheet */
208209
protected _styleSheetClass?: string;
@@ -235,10 +236,10 @@ export class GridStack {
235236
protected _styles: GridCSSStyleSheet;
236237
/** @internal flag to keep cells square during resize */
237238
protected _isAutoCellHeight: boolean;
238-
/** @internal track event binding to window resize so we can remove */
239-
protected _windowResizeBind: () => void;
240239
/** @internal limit auto cell resizing method */
241-
protected _cellHeightThrottle: () => void;
240+
protected _sizeThrottle: () => void;
241+
/** @internal limit auto cell resizing method */
242+
protected prevWidth: number;
242243
/** @internal true when loading items to insert first rather than append */
243244
protected _insertNotAppend: boolean;
244245
/** @internal extra row added when dragging at the bottom of the grid */
@@ -388,7 +389,7 @@ export class GridStack {
388389

389390
this._setupRemoveDrop();
390391
this._setupAcceptWidget();
391-
this._updateWindowResizeEvent();
392+
this._updateResizeEvent();
392393
}
393394

394395
/**
@@ -772,7 +773,7 @@ export class GridStack {
772773
if (update && val !== undefined) {
773774
if (this._isAutoCellHeight !== (val === 'auto')) {
774775
this._isAutoCellHeight = (val === 'auto');
775-
this._updateWindowResizeEvent();
776+
this._updateResizeEvent();
776777
}
777778
}
778779
if (val === 'initial' || val === 'auto') { val = undefined; }
@@ -857,6 +858,9 @@ export class GridStack {
857858
}
858859
this.engine.columnChanged(oldColumn, column, domNodes, layout);
859860
if (this._isAutoCellHeight) this.cellHeight();
861+
// this.engine.nodes.forEach(n => {
862+
// if (Utils.shouldFitToContent(n)) this.resizeToContent(n.el);
863+
// });
860864

861865
// and trigger our event last...
862866
this._ignoreLayoutsNodeChange = true; // skip layout update
@@ -886,7 +890,7 @@ export class GridStack {
886890
public destroy(removeDOM = true): GridStack {
887891
if (!this.el) return; // prevent multiple calls
888892
this.offAll();
889-
this._updateWindowResizeEvent(true);
893+
this._updateResizeEvent(true);
890894
this.setStatic(true, false); // permanently removes DD but don't set CSS class (we're going away)
891895
this.setAnimation(false);
892896
if (!removeDOM) {
@@ -1227,14 +1231,7 @@ export class GridStack {
12271231
Utils.sanitizeMinMax(n);
12281232

12291233
// finally move the widget
1230-
if (m) {
1231-
this.engine.cleanNodes()
1232-
.beginUpdate(n)
1233-
.moveNode(n, m);
1234-
this._updateContainerHeight();
1235-
this._triggerChangeEvent();
1236-
this.engine.endUpdate();
1237-
}
1234+
if (m) this.moveNode(n, m);
12381235
if (changed) { // move will only update x,y,w,h so update the rest too
12391236
this._writeAttr(el, n);
12401237
}
@@ -1245,6 +1242,37 @@ export class GridStack {
12451242
return this;
12461243
}
12471244

1245+
private moveNode(n: GridStackNode, m: GridStackMoveOpts) {
1246+
this.engine.cleanNodes()
1247+
.beginUpdate(n)
1248+
.moveNode(n, m);
1249+
this._updateContainerHeight();
1250+
this._triggerChangeEvent();
1251+
this.engine.endUpdate();
1252+
}
1253+
1254+
/** Updates widget height to match the content height to avoid v-scrollbar or dead space.
1255+
Note: this assumes only 1 child under '.grid-stack-item-content' (sized to gridItem minus padding) that is at the entire content size wanted */
1256+
public resizeToContent(els: GridStackElement) {
1257+
GridStack.getElements(els).forEach(el => {
1258+
let n = el?.gridstackNode;
1259+
if (!n) return;
1260+
let height = el.clientHeight;
1261+
if (!height) return; // 0 when hidden, skip
1262+
const item = el.querySelector('.grid-stack-item-content');
1263+
if (!item) return;
1264+
const itemH = item.clientHeight;
1265+
const wantedH = (item.firstChild as Element)?.clientHeight || itemH; // NOTE: clientHeight & getBoundingClientRect() is undefined for text and other leaf nodes. use <div> container!
1266+
if (itemH === wantedH) return;
1267+
height += wantedH - itemH;
1268+
const cell = this.getCellHeight();
1269+
if (!cell) return;
1270+
let h = Math.ceil(height / cell);
1271+
if (n.maxH && h > n.maxH) h = n.maxH;
1272+
if (h !== n.h) this.moveNode(n, {h});
1273+
});
1274+
}
1275+
12481276
/**
12491277
* 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).
12501278
* @param value margin value
@@ -1450,6 +1478,7 @@ export class GridStack {
14501478
if (!Utils.same(node, copy)) {
14511479
this._writeAttr(el, node);
14521480
}
1481+
if (Utils.shouldFitToContent(node)) el.classList.add('fit-to-content');
14531482
this._prepareDragDropByNode(node);
14541483
return this;
14551484
}
@@ -1541,62 +1570,64 @@ export class GridStack {
15411570
}
15421571

15431572
/**
1544-
* called when we are being resized by the window - check if the one Column Mode needs to be turned on/off
1545-
* and remember the prev columns we used, or get our count from parent, as well as check for auto cell height (square)
1573+
* called when we are being resized - check if the one Column Mode needs to be turned on/off
1574+
* and remember the prev columns we used, or get our count from parent, as well as check for cellHeight==='auto' (square)
1575+
* or `fitToContent` gridItem options.
15461576
*/
1547-
public onParentResize(): GridStack {
1548-
if (!this.el || !this.el.clientWidth) return; // return if we're gone or no size yet (will get called again)
1549-
let changedColumn = false;
1577+
public onResize(): GridStack {
1578+
if (!this.el?.clientWidth) return; // return if we're gone or no size yet (will get called again)
1579+
if (this.prevWidth === this.el.clientWidth) return; // no-op
1580+
this.prevWidth = this.el.clientWidth
1581+
// console.log('onResize ', this.el.clientWidth);
15501582

15511583
// see if we're nested and take our column count from our parent....
1584+
let columnChanged = false;
15521585
if (this._autoColumn && this.parentGridItem) {
15531586
if (this.opts.column !== this.parentGridItem.w) {
1554-
changedColumn = true;
15551587
this.column(this.parentGridItem.w, 'none');
1588+
columnChanged = true;
15561589
}
15571590
} else {
15581591
// else check for 1 column in/out behavior
15591592
let oneColumn = !this.opts.disableOneColumnMode && this.el.clientWidth <= this.opts.oneColumnSize;
15601593
if ((this.opts.column === 1) !== oneColumn) {
1561-
changedColumn = true;
1562-
if (this.opts.animate) { this.setAnimation(false); } // 1 <-> 12 is too radical, turn off animation
1594+
// if (this.opts.animate) this.setAnimation(false); // 1 <-> 12 is too radical, turn off animation and we need it for fitToContent
15631595
this.column(oneColumn ? 1 : this._prevColumn);
1564-
if (this.opts.animate) { this.setAnimation(true); }
1596+
// if (this.opts.animate) setTimeout(() => this.setAnimation(true));
1597+
columnChanged = true;
15651598
}
15661599
}
15671600

15681601
// make the cells content square again
1569-
if (this._isAutoCellHeight) {
1570-
if (!changedColumn && this.opts.cellHeightThrottle) {
1571-
if (!this._cellHeightThrottle) {
1572-
this._cellHeightThrottle = Utils.throttle(() => this.cellHeight(), this.opts.cellHeightThrottle);
1573-
}
1574-
this._cellHeightThrottle();
1575-
} else {
1576-
// immediate update if we've changed column count or have no threshold
1577-
this.cellHeight();
1578-
}
1579-
}
1602+
if (this._isAutoCellHeight) this.cellHeight();
15801603

1581-
// finally update any nested grids
1604+
// update any nested grids, or items size
15821605
this.engine.nodes.forEach(n => {
1583-
if (n.subGrid) n.subGrid.onParentResize()
1606+
if (n.subGrid) n.subGrid.onResize()
1607+
// update any gridItem height with fitToContent, but wait for DOM $animation_speed to settle if we changed column count
1608+
// 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 ?
1609+
if (Utils.shouldFitToContent(n)) {
1610+
columnChanged ? setTimeout(() => this.resizeToContent(n.el), 300 + 10) : this.resizeToContent(n.el);
1611+
}
15841612
});
15851613

15861614
return this;
15871615
}
15881616

1589-
/** add or remove the window size event handler */
1590-
protected _updateWindowResizeEvent(forceRemove = false): GridStack {
1617+
/** add or remove the grid element size event handler */
1618+
protected _updateResizeEvent(forceRemove = false): GridStack {
15911619
// 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)
1592-
const workTodo = (this._isAutoCellHeight || !this.opts.disableOneColumnMode) && !this.parentGridItem;
1620+
// or supporting new fitToContent option.
1621+
const trackSize = !this.parentGridItem && (this._isAutoCellHeight || this.opts.fitToContent || !this.opts.disableOneColumnMode || this.engine.nodes.find(n => n.fitToContent));
15931622

1594-
if (!forceRemove && workTodo && !this._windowResizeBind) {
1595-
this._windowResizeBind = this.onParentResize.bind(this); // so we can properly remove later
1596-
window.addEventListener('resize', this._windowResizeBind);
1597-
} else if ((forceRemove || !workTodo) && this._windowResizeBind) {
1598-
window.removeEventListener('resize', this._windowResizeBind);
1599-
delete this._windowResizeBind; // remove link to us so we can free
1623+
if (!forceRemove && trackSize && !this.resizeObserver) {
1624+
this._sizeThrottle = Utils.throttle(() => this.onResize(), this.opts.cellHeightThrottle);
1625+
this.resizeObserver = new ResizeObserver(entries => this._sizeThrottle());
1626+
this.resizeObserver.observe(this.el);
1627+
} else if ((forceRemove || !trackSize) && this.resizeObserver) {
1628+
this.resizeObserver.disconnect();
1629+
delete this.resizeObserver;
1630+
delete this._sizeThrottle;
16001631
}
16011632

16021633
return this;
@@ -2285,7 +2316,7 @@ export class GridStack {
22852316
node._lastUiPosition = ui.position;
22862317
this.engine.cacheRects(cellWidth, cellHeight, mTop, mRight, mBottom, mLeft);
22872318
delete node._skipDown;
2288-
if (resizing && node.subGrid) node.subGrid.onParentResize();
2319+
if (resizing && node.subGrid) node.subGrid.onResize();
22892320
this._extraDragRow = 0;// @ts-ignore
22902321
this._updateContainerHeight();
22912322

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ export interface GridStackOptions {
161161
/** the type of engine to create (so you can subclass) default to GridStackEngine */
162162
engineClass?: typeof GridStackEngine;
163163

164+
/** 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.
165+
Note: this is still row based, not pixels, so it will use ceil(getBoundingClientRect().height / getCellHeight()) */
166+
fitToContent?: boolean;
167+
164168
/** enable floating widgets (default?: false) See example (http://gridstack.github.io/gridstack.js/demo/float.html) */
165169
float?: boolean;
166170

@@ -316,6 +320,8 @@ export interface GridStackWidget extends GridStackPosition {
316320
id?: string;
317321
/** html to append inside as content */
318322
content?: string;
323+
/** local (grid) override - see GridStackOptions */
324+
fitToContent?: boolean;
319325
/** optional nested grid options and list of children, which then turns into actual instance at runtime to get options from */
320326
subGridOpts?: GridStackOptions;
321327
}

src/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ export class Utils {
102102
return els;
103103
}
104104

105+
/** true if we should resize to content */
106+
static shouldFitToContent(n: GridStackNode): boolean {
107+
return n.fitToContent || (n.grid?.opts.fitToContent && n.fitToContent !== false);
108+
}
109+
105110
/** returns true if a and b overlap */
106111
static isIntercepted(a: GridStackPosition, b: GridStackPosition): boolean {
107112
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);

0 commit comments

Comments
 (0)