From 00a54e0db06407066a7e860a64a82f1ee43c9a88 Mon Sep 17 00:00:00 2001 From: Alain Dumesny Date: Sat, 18 Nov 2023 18:51:01 -0800 Subject: [PATCH] new `GridStackOptions.responsive` * we now support much richer responsive behavior with `GridStackOptions.responsive` including any breakpoint width:column pairs, or automatic column sizing * `disableOneColumnMode`, `oneColumnSize`, `oneColumnModeDomSort` have been removed (see v10 migration doc) --- README.md | 10 + .../projects/demo/src/app/app.component.ts | 2 - demo/canvasJS.html | 1 - demo/column.html | 12 +- demo/custom-engine.html | 1 - demo/drag-and-drop-dataTransfer.html | 1 - demo/index.html | 3 +- demo/mobile.html | 1 - demo/nested.html | 1 - demo/old_two-jq.html | 1 - demo/react-hooks-controlled-multiple.html | 1 - demo/responsive.html | 66 ++--- demo/responsive_break.html | 66 +++++ demo/two.html | 1 - demo/web1.html | 1 - demo/web2.html | 1 - doc/CHANGES.md | 5 + doc/README.md | 21 +- spec/e2e/html/1570_drag_bottom_max_row.html | 1 - spec/e2e/html/1571_drop_onto_full.html | 1 - spec/e2e/html/1858_full_grid_overlap.html | 1 - spec/e2e/html/2394_save_sub_item_moved.html | 2 - spec/e2e/html/2469_min-height.html | 1 - spec/e2e/html/2492_load_twice.html | 2 +- spec/gridstack-spec.ts | 246 +++++++++--------- src/gridstack.ts | 154 +++++++---- src/types.ts | 49 ++-- 27 files changed, 377 insertions(+), 275 deletions(-) create mode 100644 demo/responsive_break.html diff --git a/README.md b/README.md index f2fb43ad3..2d4a6b23f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Join us on Slack: [https://gridstackjs.slack.com](https://gridstackjs.slack.com) - [Migrating to v7](#migrating-to-v7) - [Migrating to v8](#migrating-to-v8) - [Migrating to v9](#migrating-to-v9) + - [Migrating to v10](#migrating-to-v10) - [jQuery Application](#jquery-application) - [Changes](#changes) - [The Team](#the-team) @@ -450,6 +451,15 @@ New addition - see release notes about `sizeToContent` feature. Possible break: * `GridStack.onParentResize()` is now called `onResize()` as grid now directly track size change, no longer involving parent per say to tell us anything. Note sure why it was public. +## Migrating to v10 + +we now support much richer responsive behavior with `GridStackOptions.responsive` including any breakpoint width:column pairs, or automatic column sizing. + +breaking change: +* `disableOneColumnMode`, `oneColumnSize` have been removed (thought we temporary convert if you have them). use `{ responsive: breakpoints: [{w:768, c:1}] }` for the same behavior. +* 1 column mode switch is no longer by default (`responsive` is not defined) as too many new users had issues. Instead set it explicitly (see above). +* `oneColumnModeDomSort` has been removed. Planning to support per column layouts at some future times. TBD + # jQuery Application This is **old and no longer apply to v6+**. You'll need to use v5.1.1 and before diff --git a/angular/projects/demo/src/app/app.component.ts b/angular/projects/demo/src/app/app.component.ts index f12c6d059..2febd1643 100644 --- a/angular/projects/demo/src/app/app.component.ts +++ b/angular/projects/demo/src/app/app.component.ts @@ -62,7 +62,6 @@ export class AppComponent implements OnInit { cellHeight: 50, margin: 5, minRow: 2, // don't collapse when empty - disableOneColumnMode: true, acceptWidgets: true, children: this.subChildren }; @@ -71,7 +70,6 @@ export class AppComponent implements OnInit { cellHeight: 50, margin: 5, minRow: 1, // don't collapse when empty - disableOneColumnMode: true, removable: '.trash', acceptWidgets: true, float: true, diff --git a/demo/canvasJS.html b/demo/canvasJS.html index d3b379126..000907e26 100644 --- a/demo/canvasJS.html +++ b/demo/canvasJS.html @@ -23,7 +23,6 @@

CanvasJS grid demo

let grid = GridStack.init({ cellHeight: 'initial', // start square but will set to % of window width later animate: false, // show immediate (animate: true is nice for user dragging though) - disableOneColumnMode: true, // will manually do 1 column float: true }); diff --git a/demo/column.html b/demo/column.html index a40327ef8..2c79bb791 100644 --- a/demo/column.html +++ b/demo/column.html @@ -33,8 +33,7 @@

column() grid demo (fix cellHeight)

random Add Widget column: - 1 - 1 DOM + 1 2 3 4 @@ -107,14 +106,6 @@

column() grid demo (fix cellHeight)

grid.column(n, layout); text.innerHTML = n; } - function setOneColumn(dom) { - if (grid.opts.column === 1 && grid.opts.oneColumnModeDomSort !== dom) { - column(12); // go ack to 12 before going to new column1 version - } - grid.opts.oneColumnModeDomSort = dom; - grid.column(1, layout); - text.innerHTML = dom ? '1 DOM' : '1'; - } // dummy test method that moves items to the right each new layout... grid engine will validate those values (can't be neg or out of bounds) anyway... function columnLayout(column, oldColumn, nodes, oldNodes) { oldNodes.forEach(n => { @@ -126,7 +117,6 @@

column() grid demo (fix cellHeight)

function setLayout(name) { layout = name === 'custom' ? this.columnLayout : name; } - // setOneColumn(true); // test dom order diff --git a/demo/custom-engine.html b/demo/custom-engine.html index ac3d54d4f..4f5cce27e 100644 --- a/demo/custom-engine.html +++ b/demo/custom-engine.html @@ -50,7 +50,6 @@

Custom Engine

let grid = GridStack.init({ float: true, - disableOneColumnMode: true, cellHeight: 70 }).load(items); addEvents(grid); diff --git a/demo/drag-and-drop-dataTransfer.html b/demo/drag-and-drop-dataTransfer.html index f2ee892b1..00ebb9247 100644 --- a/demo/drag-and-drop-dataTransfer.html +++ b/demo/drag-and-drop-dataTransfer.html @@ -34,7 +34,6 @@

Event DataTransfer Demo

diff --git a/demo/nested.html b/demo/nested.html index 854a1f877..7bdeaa4d6 100644 --- a/demo/nested.html +++ b/demo/nested.html @@ -47,7 +47,6 @@

Nested grids demo

cellHeight: 50, margin: 5, minRow: 2, // don't collapse when empty - disableOneColumnMode: true, acceptWidgets: true, id: 'main', children: [ diff --git a/demo/old_two-jq.html b/demo/old_two-jq.html index 04b8c03d3..176f726e0 100644 --- a/demo/old_two-jq.html +++ b/demo/old_two-jq.html @@ -56,7 +56,6 @@

Two grids demo (old v5.1.1 Jquery version)

column: 6, minRow: 1, // don't collapse when empty cellHeight: 70, - disableOneColumnMode: true, float: false, // dragIn: '.sidebar .grid-stack-item', // add draggable to class // dragInOptions: { revert: 'invalid', scroll: false, appendTo: 'body', helper: 'clone' }, // clone diff --git a/demo/react-hooks-controlled-multiple.html b/demo/react-hooks-controlled-multiple.html index f8acb2fa2..460246c0d 100644 --- a/demo/react-hooks-controlled-multiple.html +++ b/demo/react-hooks-controlled-multiple.html @@ -52,7 +52,6 @@

Controlled stack

{ float: false, acceptWidgets: true, - disableOneColumnMode: true, // side-by-side and fever columns to fit smaller screens column: 6, minRow: 1, }, diff --git a/demo/responsive.html b/demo/responsive.html index 0926ba067..d5fbee67d 100644 --- a/demo/responsive.html +++ b/demo/responsive.html @@ -1,24 +1,23 @@ - Responsive grid demo + Responsive column -
-

Responsive grid demo

+

Responsive: by column size

+

Using new v10 GridStackOptions.responsive: { columnWidth: x }

+
Number of Columns:
- @@ -35,50 +34,33 @@

Responsive grid demo

diff --git a/demo/responsive_break.html b/demo/responsive_break.html new file mode 100644 index 000000000..9a8ccc709 --- /dev/null +++ b/demo/responsive_break.html @@ -0,0 +1,66 @@ + + + + Responsive breakpoint + + + + + + +
+

Responsive: using breakpoint

+

Using new v10 GridStackOptions.responsive: { breakpoints: [] }

+
+ Number of Columns: +
+
+ + + Clear + Add Widget +
+
+
+
+
+ + + + diff --git a/demo/two.html b/demo/two.html index dee191a13..79789672c 100644 --- a/demo/two.html +++ b/demo/two.html @@ -57,7 +57,6 @@

Two grids demo

column: 6, minRow: 1, // don't collapse when empty cellHeight: 70, - disableOneColumnMode: true, float: true, removable: '.trash', // true or drag-out delete class // itemClass: 'with-lines', // test a custom additional class #2110 diff --git a/demo/web1.html b/demo/web1.html index 58bcef716..ebe51a185 100644 --- a/demo/web1.html +++ b/demo/web1.html @@ -25,7 +25,6 @@

Web demo 1

diff --git a/spec/gridstack-spec.ts b/spec/gridstack-spec.ts index beb432d88..534ebe6db 100644 --- a/spec/gridstack-spec.ts +++ b/spec/gridstack-spec.ts @@ -431,130 +431,130 @@ describe('gridstack', function() { }); }); - describe('oneColumnModeDomSort', function() { - beforeEach(function() { - document.body.insertAdjacentHTML('afterbegin', gridstackEmptyHTML); - }); - afterEach(function() { - document.body.removeChild(document.getElementById('gs-cont')); - }); - it('should support default going to 1 column', function() { - let options = { - column: 12, - float: true - }; - let grid = GridStack.init(options); - grid.batchUpdate(); - grid.batchUpdate(); - let el1 = grid.addWidget({w:1, h:1}); - let el2 = grid.addWidget({x:2, y:0, w:2, h:1}); - let el3 = grid.addWidget({x:1, y:0, w:1, h:2}); - grid.batchUpdate(false); - grid.batchUpdate(false); + // describe('oneColumnModeDomSort', function() { + // beforeEach(function() { + // document.body.insertAdjacentHTML('afterbegin', gridstackEmptyHTML); + // }); + // afterEach(function() { + // document.body.removeChild(document.getElementById('gs-cont')); + // }); + // it('should support default going to 1 column', function() { + // let options = { + // column: 12, + // float: true + // }; + // let grid = GridStack.init(options); + // grid.batchUpdate(); + // grid.batchUpdate(); + // let el1 = grid.addWidget({w:1, h:1}); + // let el2 = grid.addWidget({x:2, y:0, w:2, h:1}); + // let el3 = grid.addWidget({x:1, y:0, w:1, h:2}); + // grid.batchUpdate(false); + // grid.batchUpdate(false); - // items are item1[1x1], item3[1x1], item2[2x1] - expect(parseInt(el1.getAttribute('gs-x'))).toBe(0); - expect(parseInt(el1.getAttribute('gs-y'))).toBe(0); - expect(el1.getAttribute('gs-w')).toBe(null); - expect(el1.getAttribute('gs-h')).toBe(null); - - expect(parseInt(el3.getAttribute('gs-x'))).toBe(1); - expect(parseInt(el3.getAttribute('gs-y'))).toBe(0); - expect(el3.getAttribute('gs-w')).toBe(null); - expect(parseInt(el3.getAttribute('gs-h'))).toBe(2); - - expect(parseInt(el2.getAttribute('gs-x'))).toBe(2); - expect(parseInt(el2.getAttribute('gs-y'))).toBe(0); - expect(parseInt(el2.getAttribute('gs-w'))).toBe(2); - expect(el2.getAttribute('gs-h')).toBe(null); - - // items are item1[1x1], item3[1x2], item2[1x1] in 1 column - grid.column(1); - expect(parseInt(el1.getAttribute('gs-x'))).toBe(0); - expect(parseInt(el1.getAttribute('gs-y'))).toBe(0); - expect(el1.getAttribute('gs-w')).toBe(null); - expect(el1.getAttribute('gs-h')).toBe(null); - - expect(parseInt(el3.getAttribute('gs-x'))).toBe(0); - expect(parseInt(el3.getAttribute('gs-y'))).toBe(1); - expect(el3.getAttribute('gs-w')).toBe(null); - expect(parseInt(el3.getAttribute('gs-h'))).toBe(2); - - expect(parseInt(el2.getAttribute('gs-x'))).toBe(0); - expect(parseInt(el2.getAttribute('gs-y'))).toBe(3); - expect(el2.getAttribute('gs-w')).toBe(null); - expect(el2.getAttribute('gs-h')).toBe(null); - }); - it('should support oneColumnModeDomSort ON going to 1 column', function() { - let options = { - column: 12, - oneColumnModeDomSort: true, - float: true - }; - let grid = GridStack.init(options); - let el1 = grid.addWidget({w:1, h:1}); - let el2 = grid.addWidget({x:2, y:0, w:2, h:1}); - let el3 = grid.addWidget({x:1, y:0, w:1, h:2}); - - // items are item1[1x1], item3[1x1], item2[2x1] - expect(parseInt(el1.getAttribute('gs-x'))).toBe(0); - expect(parseInt(el1.getAttribute('gs-y'))).toBe(0); - expect(el1.getAttribute('gs-w')).toBe(null); - expect(el1.getAttribute('gs-h')).toBe(null); - - expect(parseInt(el3.getAttribute('gs-x'))).toBe(1); - expect(parseInt(el3.getAttribute('gs-y'))).toBe(0); - expect(el3.getAttribute('gs-w')).toBe(null); - expect(parseInt(el3.getAttribute('gs-h'))).toBe(2); - - expect(parseInt(el2.getAttribute('gs-x'))).toBe(2); - expect(parseInt(el2.getAttribute('gs-y'))).toBe(0); - expect(parseInt(el2.getAttribute('gs-w'))).toBe(2); - expect(el2.getAttribute('gs-h')).toBe(null); - - // items are item1[1x1], item2[1x1], item3[1x2] in 1 column dom ordered - grid.column(1); - expect(parseInt(el1.getAttribute('gs-x'))).toBe(0); - expect(parseInt(el1.getAttribute('gs-y'))).toBe(0); - expect(el1.getAttribute('gs-w')).toBe(null); - expect(el1.getAttribute('gs-h')).toBe(null); - - expect(parseInt(el2.getAttribute('gs-x'))).toBe(0); - expect(parseInt(el2.getAttribute('gs-y'))).toBe(1); - expect(el2.getAttribute('gs-w')).toBe(null); - expect(el2.getAttribute('gs-h')).toBe(null); - - expect(parseInt(el3.getAttribute('gs-x'))).toBe(0); - expect(parseInt(el3.getAttribute('gs-y'))).toBe(2); - expect(el3.getAttribute('gs-w')).toBe(null); - expect(parseInt(el3.getAttribute('gs-h'))).toBe(2); - }); - }); - - describe('disableOneColumnMode', function() { - beforeEach(function() { - document.body.insertAdjacentHTML('afterbegin', gridstackSmallHTML); // smaller default to 1 column - }); - afterEach(function() { - document.body.removeChild(document.getElementById('gs-cont')); - }); - it('should go to 1 column', function() { - let grid = GridStack.init(); - expect(grid.getColumn()).toBe(1); - }); - it('should go to 1 column with large minW', function() { - let grid = GridStack.init({oneColumnSize: 1000}); - expect(grid.getColumn()).toBe(1); - }); - it('should stay at 12 with minW', function() { - let grid = GridStack.init({oneColumnSize: 300}); - expect(grid.getColumn()).toBe(12); - }); - it('should stay at 12 column', function() { - let grid = GridStack.init({disableOneColumnMode: true}); - expect(grid.getColumn()).toBe(12); - }); - }); + // // items are item1[1x1], item3[1x1], item2[2x1] + // expect(parseInt(el1.getAttribute('gs-x'))).toBe(0); + // expect(parseInt(el1.getAttribute('gs-y'))).toBe(0); + // expect(el1.getAttribute('gs-w')).toBe(null); + // expect(el1.getAttribute('gs-h')).toBe(null); + + // expect(parseInt(el3.getAttribute('gs-x'))).toBe(1); + // expect(parseInt(el3.getAttribute('gs-y'))).toBe(0); + // expect(el3.getAttribute('gs-w')).toBe(null); + // expect(parseInt(el3.getAttribute('gs-h'))).toBe(2); + + // expect(parseInt(el2.getAttribute('gs-x'))).toBe(2); + // expect(parseInt(el2.getAttribute('gs-y'))).toBe(0); + // expect(parseInt(el2.getAttribute('gs-w'))).toBe(2); + // expect(el2.getAttribute('gs-h')).toBe(null); + + // // items are item1[1x1], item3[1x2], item2[1x1] in 1 column + // grid.column(1); + // expect(parseInt(el1.getAttribute('gs-x'))).toBe(0); + // expect(parseInt(el1.getAttribute('gs-y'))).toBe(0); + // expect(el1.getAttribute('gs-w')).toBe(null); + // expect(el1.getAttribute('gs-h')).toBe(null); + + // expect(parseInt(el3.getAttribute('gs-x'))).toBe(0); + // expect(parseInt(el3.getAttribute('gs-y'))).toBe(1); + // expect(el3.getAttribute('gs-w')).toBe(null); + // expect(parseInt(el3.getAttribute('gs-h'))).toBe(2); + + // expect(parseInt(el2.getAttribute('gs-x'))).toBe(0); + // expect(parseInt(el2.getAttribute('gs-y'))).toBe(3); + // expect(el2.getAttribute('gs-w')).toBe(null); + // expect(el2.getAttribute('gs-h')).toBe(null); + // }); + // it('should support oneColumnModeDomSort ON going to 1 column', function() { + // let options = { + // column: 12, + // oneColumnModeDomSort: true, + // float: true + // }; + // let grid = GridStack.init(options); + // let el1 = grid.addWidget({w:1, h:1}); + // let el2 = grid.addWidget({x:2, y:0, w:2, h:1}); + // let el3 = grid.addWidget({x:1, y:0, w:1, h:2}); + + // // items are item1[1x1], item3[1x1], item2[2x1] + // expect(parseInt(el1.getAttribute('gs-x'))).toBe(0); + // expect(parseInt(el1.getAttribute('gs-y'))).toBe(0); + // expect(el1.getAttribute('gs-w')).toBe(null); + // expect(el1.getAttribute('gs-h')).toBe(null); + + // expect(parseInt(el3.getAttribute('gs-x'))).toBe(1); + // expect(parseInt(el3.getAttribute('gs-y'))).toBe(0); + // expect(el3.getAttribute('gs-w')).toBe(null); + // expect(parseInt(el3.getAttribute('gs-h'))).toBe(2); + + // expect(parseInt(el2.getAttribute('gs-x'))).toBe(2); + // expect(parseInt(el2.getAttribute('gs-y'))).toBe(0); + // expect(parseInt(el2.getAttribute('gs-w'))).toBe(2); + // expect(el2.getAttribute('gs-h')).toBe(null); + + // // items are item1[1x1], item2[1x1], item3[1x2] in 1 column dom ordered + // grid.column(1); + // expect(parseInt(el1.getAttribute('gs-x'))).toBe(0); + // expect(parseInt(el1.getAttribute('gs-y'))).toBe(0); + // expect(el1.getAttribute('gs-w')).toBe(null); + // expect(el1.getAttribute('gs-h')).toBe(null); + + // expect(parseInt(el2.getAttribute('gs-x'))).toBe(0); + // expect(parseInt(el2.getAttribute('gs-y'))).toBe(1); + // expect(el2.getAttribute('gs-w')).toBe(null); + // expect(el2.getAttribute('gs-h')).toBe(null); + + // expect(parseInt(el3.getAttribute('gs-x'))).toBe(0); + // expect(parseInt(el3.getAttribute('gs-y'))).toBe(2); + // expect(el3.getAttribute('gs-w')).toBe(null); + // expect(parseInt(el3.getAttribute('gs-h'))).toBe(2); + // }); + // }); + + // describe('disableOneColumnMode', function() { + // beforeEach(function() { + // document.body.insertAdjacentHTML('afterbegin', gridstackSmallHTML); // smaller default to 1 column + // }); + // afterEach(function() { + // document.body.removeChild(document.getElementById('gs-cont')); + // }); + // it('should go to 1 column', function() { + // let grid = GridStack.init(); + // expect(grid.getColumn()).toBe(1); + // }); + // it('should go to 1 column with large minW', function() { + // let grid = GridStack.init({oneColumnSize: 1000}); + // expect(grid.getColumn()).toBe(1); + // }); + // it('should stay at 12 with minW', function() { + // let grid = GridStack.init({oneColumnSize: 300}); + // expect(grid.getColumn()).toBe(12); + // }); + // it('should stay at 12 column', function() { + // let grid = GridStack.init({disableOneColumnMode: true}); + // expect(grid.getColumn()).toBe(12); + // }); + // }); describe('grid.minRow', function() { beforeEach(function() { diff --git a/src/gridstack.ts b/src/gridstack.ts index 79aacd836..ccc37b736 100644 --- a/src/gridstack.ts +++ b/src/gridstack.ts @@ -20,8 +20,7 @@ import { gridDefaults, ColumnOptions, GridItemHTMLElement, GridStackElement, Gri import { DDGridStack } from './dd-gridstack'; import { isTouch } from './dd-touch'; import { DDManager } from './dd-manager'; -import { DDElementHost } from './dd-element'; -/** global instance */ +import { DDElementHost } from './dd-element';/** global instance */ const dd = new DDGridStack; // export all dependent file as well to make it easier for users to just import the main file @@ -58,6 +57,17 @@ interface InternalGridStackOptions extends GridStackOptions { _alwaysShowResizeHandle?: true | false | 'mobile'; // so we can restore for save } +// temporary legacy (<10.x) support +interface OldOneColumnOpts extends GridStackOptions { + /** disables the onColumnMode when the grid width is less (default?: false) */ + disableOneColumnMode?: boolean; + /** minimal width before grid will be shown in one column mode (default?: 768) */ + oneColumnSize?: number; + /** set to true if you want oneColumnMode to use the DOM order and ignore x,y from normal multi column + layouts during sorting. This enables you to have custom 1 column layout that differ from the rest. (default?: false) */ + oneColumnModeDomSort?: boolean; +} + /** * Main gridstack class - you will need to call `GridStack.init()` first to initialize your grid. * Note: your grid elements MUST have the following classes for the CSS layout to work: @@ -231,8 +241,6 @@ export class GridStack { } /** @internal */ protected _placeholder: HTMLElement; - /** @internal */ - protected _prevColumn: number; /** @internal prevent cached layouts from being updated when loading into small column layouts */ protected _ignoreLayoutsNodeChange: boolean; /** @internal */ @@ -281,6 +289,37 @@ export class GridStack { if (opts.alwaysShowResizeHandle !== undefined) { (opts as InternalGridStackOptions)._alwaysShowResizeHandle = opts.alwaysShowResizeHandle; } + let bk = opts.responsive?.breakpoints; + // LEGACY: oneColumnMode stuff changed in v10.x - check if user explicitly set something to convert over + const oldOpts: OldOneColumnOpts = opts; + if (oldOpts.oneColumnModeDomSort) { + delete oldOpts.oneColumnModeDomSort; + console.log('Error: Gridstack oneColumnModeDomSort no longer supported. Check GridStackOptions.responsive instead.') + } + if (oldOpts.oneColumnSize || oldOpts.disableOneColumnMode === false) { + const oneSize = oldOpts.oneColumnSize || 768; + delete oldOpts.oneColumnSize; + delete oldOpts.disableOneColumnMode; + opts.responsive = opts.responsive || {}; + bk = opts.responsive.breakpoints = opts.responsive.breakpoints || []; + let oneColumn = bk.find(b => b.c === 1); + if (!oneColumn) { + oneColumn = {c: 1, w: oneSize}; + bk.push(oneColumn, {c: 12, w: oneSize+1}); + } else oneColumn.w = oneSize; + } + //...end LEGACY + // cleanup responsive opts (must have columnWidth | breakpoints) then sort breakpoints by size (so we can match during resize) + const resp = opts.responsive; + if (resp) { + if (!resp.columnWidth && !resp.breakpoints?.length) { + delete opts.responsive; + bk = undefined; + } else { + resp.columnMax = resp.columnMax || 12; + } + } + if (bk?.length > 1) bk.sort((a,b) => (b.w || 0) - (a.w || 0)); // elements DOM attributes override any passed options (like CSS style) - merge the two together let defaults: GridStackOptions = {...Utils.cloneDeep(gridDefaults), @@ -305,10 +344,7 @@ export class GridStack { this._initMargin(); // part of settings defaults... // Now check if we're loading into 1 column mode FIRST so we don't do un-necessary work (like cellHeight = width / 12 then go 1 column) - if (this.opts.column !== 1 && !this.opts.disableOneColumnMode && this._widthOrContainer() <= this.opts.oneColumnSize) { - this._prevColumn = this.getColumn(); - this.opts.column = 1; - } + this.checkDynamicColumn(); if (this.opts.rtl === 'auto') { this.opts.rtl = (el.style.direction === 'rtl'); @@ -498,7 +534,7 @@ export class GridStack { if (ops.column === 'auto') { autoColumn = true; ops.column = Math.max(node.w || 1, nodeToAdd?.w || 1); - ops.disableOneColumnMode = true; // driven by parent + delete ops.responsive; // 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 @@ -630,7 +666,6 @@ export class GridStack { } if (this._autoColumn) { o.column = 'auto'; - delete o.disableOneColumnMode; } const origShow = o._alwaysShowResizeHandle; delete o._alwaysShowResizeHandle; @@ -659,17 +694,18 @@ export class GridStack { */ public load(items: GridStackWidget[], addRemove: boolean | AddRemoveFcn = GridStack.addRemoveCB || true): GridStack { items = Utils.cloneDeep(items); // so we can mod - // if passed list has coordinates, use them (insert from end to beginning for conflict resolution) else force widget same order + const column = this.getColumn(); + + // if passed list has coordinates, use them (insert from end to beginning for conflict resolution) else keep widget order const haveCoord = items.some(w => w.x !== undefined || w.y !== undefined); - if (haveCoord) items = Utils.sort(items, -1, this._prevColumn || this.getColumn()); + if (haveCoord) items = Utils.sort(items, -1, column); this._insertNotAppend = haveCoord; // if we create in reverse order... - // if we're loading a layout into for example 1 column (_prevColumn is set only when going to 1) and items don't fit, make sure to save + // if we're loading a layout into for example 1 column and items don't fit, make sure to save // the original wanted layout so we can scale back up correctly #1471 - const column = this.opts.column as number; - if (this._prevColumn && this._prevColumn !== column && items.some(n => ((n.x || 0) + n.w) > column)) { + if (items.some(n => ((n.x || 0) + (n.w || 1)) > column)) { this._ignoreLayoutsNodeChange = true; // skip layout update - this.engine.cacheLayout(items, this._prevColumn, true); + this.engine.cacheLayout(items, 12, true); // TODO: 12 is arbitrary. use max value in layout ? } // if given a different callback, temporally set it as global option so creating will use it @@ -831,11 +867,35 @@ export class GridStack { public cellWidth(): number { return this._widthOrContainer() / this.getColumn(); } - /** return our expected width (or parent) for 1 column check */ - protected _widthOrContainer(): number { + /** return our expected width (or parent) , and optionally of window for dynamic column check */ + protected _widthOrContainer(forBreakpoint = false): number { // use `offsetWidth` or `clientWidth` (no scrollbar) ? // https://stackoverflow.com/questions/21064101/understanding-offsetwidth-clientwidth-scrollwidth-and-height-respectively - return (this.el.clientWidth || this.el.parentElement.clientWidth || window.innerWidth); + return forBreakpoint && this.opts.responsive?.breakpointForWindow ? window.innerWidth : (this.el.clientWidth || this.el.parentElement.clientWidth || window.innerWidth); + } + /** checks for dynamic column count for our current size, returning true if changed */ + protected checkDynamicColumn(): boolean { + const resp = this.opts.responsive; + if (!resp || (!resp.columnWidth && !resp.breakpoints?.length)) return false; + const column = this.getColumn(); + let newColumn = column; + const w = this._widthOrContainer(true); + if (resp.columnWidth) { + newColumn = Math.min(Math.round(w / resp.columnWidth) || 1, resp.columnMax); + } else { + // find the closest breakpoint (already sorted big to small) that matches + newColumn = resp.columnMax; + let i = 0; + while (i < resp.breakpoints.length && w <= resp.breakpoints[i].w) { + newColumn = resp.breakpoints[i++].c || column; + } + } + if (newColumn !== column) { + const bk = resp.breakpoints?.find(b => b.c === newColumn); + this.column(newColumn, bk?.layout || resp.layout); + return true; + } + return false; } /** @@ -862,30 +922,19 @@ export class GridStack { */ public column(column: number, layout: ColumnOptions = 'moveScale'): GridStack { if (!column || column < 1 || this.opts.column === column) return this; - let oldColumn = this.getColumn(); - // if we go into 1 column mode due to size change (disableOneColumnMode is off and we hit min width) - // then remember the original columns so we can restore. - if (column === 1 && !this.opts.disableOneColumnMode) { - this._prevColumn = oldColumn; - } else { - delete this._prevColumn; - } + let oldColumn = this.getColumn(); + this.opts.column = column; + if (!this.engine) return this; // called in constructor, noting else to do + this.engine.column = column; this.el.classList.remove('gs-' + oldColumn); this.el.classList.add('gs-' + column); - this.opts.column = this.engine.column = column; - - // update the items now - see if the dom order nodes should be passed instead (else default to current list) - let domNodes: GridStackNode[]; - if (column === 1 && this.opts.oneColumnModeDomSort) { - domNodes = []; - this.getGridItems().forEach(el => { // get dom elements in order - if (el.gridstackNode) { domNodes.push(el.gridstackNode); } - }); - if (!domNodes.length) { domNodes = undefined; } - } - this.engine.columnChanged(oldColumn, column, domNodes, layout); + + // update the items now, checking if we have a custom children layout + /*const newChildren = this.opts.responsive?.breakpoints?.find(r => r.c === column)?.children; + if (newChildren) this.load(newChildren); + else*/ this.engine.columnChanged(oldColumn, column, undefined, layout); if (this._isAutoCellHeight) this.cellHeight(); this.doContentResize(); @@ -901,9 +950,7 @@ export class GridStack { /** * get the number of columns in the grid (default 12) */ - public getColumn(): number { - return this.opts.column as number; - } + public getColumn(): number { return this.opts.column as number; } /** returns an array of grid HTML elements (no placeholder) - used to iterate through our children in DOM order */ public getGridItems(): GridItemHTMLElement[] { @@ -1030,9 +1077,9 @@ export class GridStack { this.makeSubGrid(el, node.subGridOpts, undefined, false); // node.subGrid will be used as option in method, no need to pass } - // if we're adding an item into 1 column (_prevColumn is set only when going to 1) make sure + // if we're adding an item into 1 column make sure // we don't override the larger 12 column layout that was already saved. #1985 - if (this._prevColumn && this.opts.column === 1) { + if (this.opts.column === 1) { this._ignoreLayoutsNodeChange = true; } this._triggerAddEvent(); @@ -1066,8 +1113,8 @@ export class GridStack { return this; } + // native CustomEvent handlers - cash the generic handlers so we can easily remove if (name === 'change' || name === 'added' || name === 'removed' || name === 'enable' || name === 'disable') { - // native CustomEvent handlers - cash the generic handlers so we can easily remove let noData = (name === 'enable' || name === 'disable'); if (noData) { this._gsEventHandler[name] = (event: Event) => (callback as GridStackEventHandler)(event); @@ -1081,7 +1128,7 @@ export class GridStack { // do same for start event to make it easier... this._gsEventHandler[name] = callback; } else { - console.log('GridStack.on(' + name + ') event not supported, but you can still use $(".grid-stack").on(...) while jquery-ui is still used internally.'); + console.log('GridStack.on(' + name + ') event not supported'); } return this; } @@ -1670,15 +1717,8 @@ export class GridStack { columnChanged = true; } } else { - // else check for 1 column in/out behavior - let oneColumn = !this.opts.disableOneColumnMode && this.el.clientWidth <= this.opts.oneColumnSize || - (this.opts.column === 1 && !this._prevColumn); - if ((this.opts.column === 1) !== oneColumn) { - // if (this.opts.animate) this.setAnimation(false); // 1 <-> 12 is too radical, turn off animation and we need it for sizeToContent - this.column(oneColumn ? 1 : this._prevColumn); - // if (this.opts.animate) setTimeout(() => this.setAnimation(true)); - columnChanged = true; - } + // else check for dynamic column + columnChanged = this.checkDynamicColumn(); } // make the cells content square again @@ -1719,9 +1759,9 @@ export class 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) + // only add event if we're not nested (parent will call us) and we're auto sizing cells or supporting dynamic column (i.e. doing work) // or supporting new sizeToContent option. - const trackSize = !this.parentGridItem && (this._isAutoCellHeight || this.opts.sizeToContent || !this.opts.disableOneColumnMode + const trackSize = !this.parentGridItem && (this._isAutoCellHeight || this.opts.sizeToContent || this.opts.responsive || this.engine.nodes.find(n => n.sizeToContent)); if (!forceRemove && trackSize && !this.resizeObserver) { diff --git a/src/types.ts b/src/types.ts index 913af0791..29c09759d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,7 +22,6 @@ export const gridDefaults: GridStackOptions = { marginUnit: 'px', maxRow: 0, minRow: 0, - oneColumnSize: 768, placeholderClass: 'grid-stack-placeholder', placeholderText: '', removableOptions: { accept: 'grid-stack-item', decline: 'grid-stack-non-removable'}, @@ -31,11 +30,9 @@ export const gridDefaults: GridStackOptions = { // **** same as not being set **** // disableDrag: false, - // disableOneColumnMode: false, // disableResize: false, // float: false, // handleClass: null, - // oneColumnModeDomSort: false, // removable: false, // staticGrid: false, // styleInHead: false, @@ -88,6 +85,31 @@ export type SaveFcn = (node: GridStackNode, w: GridStackWidget) => void; export type ResizeToContentFcn = (el: GridItemHTMLElement, useAttr?: boolean) => void; +/** describes the responsive nature of the grid */ +export interface Responsive { + /** wanted width to maintain (+-50%) to dynamically pick a column count */ + columnWidth?: number; + /** maximum number of columns allowed (default: 12). Note: make sure to have correct CSS to support this.*/ + columnMax?: number; + /** global re-layout mode when changing columns */ + layout?: ColumnOptions; + /** specify if breakpoints are for window size or grid size (default:false = grid) */ + breakpointForWindow?: boolean; + /** explicit width:column breakpoints instead of automatic 'columnWidth'. Note: make sure to have correct CSS to support this.*/ + breakpoints?: ResponsiveBreakpoint[]; +} + +export interface ResponsiveBreakpoint { + /** width */ + w?: number; + /** column */ + c: number; + /** re-layout mode if different from global one */ + layout?: ColumnOptions; + /** TODO: children layout, which spells out exact locations and could omit/add some children */ + // children?: GridStackWidget[]; +} + /** * Defines the options for a Grid */ @@ -147,9 +169,6 @@ export interface GridStackOptions { /** disallows dragging of widgets (default?: false) */ disableDrag?: boolean; - /** disables the onColumnMode when the grid width is less than oneColumnSize (default?: false) */ - disableOneColumnMode?: boolean; - /** disallows resizing of widgets (default?: false). */ disableResize?: boolean; @@ -204,15 +223,6 @@ export interface GridStackOptions { * GridStack will add it to the