Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auto-size height to fit content #2404

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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