diff --git a/src/js/core/ColumnManager.js b/src/js/core/ColumnManager.js index 335529072..338ecb760 100644 --- a/src/js/core/ColumnManager.js +++ b/src/js/core/ColumnManager.js @@ -29,6 +29,8 @@ export default class ColumnManager extends CoreFeature { this.redrawBlockUpdate = null; //store latest redraw update only status this.renderer = null; + this.tableMinWidthStyled = false; + this.columnWidthOverflowAdjustQueued = false; } ////////////// Setup Functions ///////////////// @@ -377,6 +379,8 @@ export default class ColumnManager extends CoreFeature { }); this.table.rowManager.adjustTableSize(); + this.adjustForColumnWidthOverflow(); + this.queueColumnWidthOverflowAdjustment(); } } @@ -520,6 +524,34 @@ export default class ColumnManager extends CoreFeature { return width; } + columnWidthOverflow(){ + return this.getWidth() > this.table.rowManager.element.clientWidth; + } + + adjustForColumnWidthOverflow(){ + if(this.columnWidthOverflow()){ + // https://github.com/tabulator-tables/tabulator/issues/4840 + this.table.rowManager.tableElement.style.minWidth = this.getWidth() + "px"; + this.tableMinWidthStyled = true; + }else if(this.tableMinWidthStyled){ + this.table.rowManager.tableElement.style.minWidth = ""; + this.tableMinWidthStyled = false; + } + } + + queueColumnWidthOverflowAdjustment(){ + if(this.columnWidthOverflowAdjustQueued || typeof requestAnimationFrame !== "function"){ + return; + } + + this.columnWidthOverflowAdjustQueued = true; + + requestAnimationFrame(() => { + this.columnWidthOverflowAdjustQueued = false; + this.adjustForColumnWidthOverflow(); + }); + } + moveColumn(from, to, after){ to.element.parentNode.insertBefore(from.element, to.element); @@ -672,7 +704,7 @@ export default class ColumnManager extends CoreFeature { if(column.visible){ - width = column.definition.width || 0; + width = column.widthUser ? column.getWidth() : column.definition.width || 0; minWidth = parseInt(column.minWidth); @@ -747,7 +779,9 @@ export default class ColumnManager extends CoreFeature { rerenderColumns(update, silent){ if(!this.redrawBlock){ + this.adjustForColumnWidthOverflow(); this.renderer.rerenderColumns(update, silent); + this.queueColumnWidthOverflowAdjustment(); }else{ if(update === false || (update === true && this.redrawBlockUpdate === null)){ this.redrawBlockUpdate = update; diff --git a/src/js/core/column/Column.js b/src/js/core/column/Column.js index 9d0b3bde3..a8295d779 100644 --- a/src/js/core/column/Column.js +++ b/src/js/core/column/Column.js @@ -49,6 +49,7 @@ export default class Column extends CoreFeature{ this.minWidth = null; //column minimum width this.minWidthStyled = ""; //column minimum pre-styled to improve render efficiency this.widthFixed = false; //user has specified a width for this column + this.widthUser = false; //column width has been manually resized by the user this.visible = true; //default visible state @@ -662,8 +663,13 @@ export default class Column extends CoreFeature{ } } - setWidth(width){ + setWidth(width, user){ this.widthFixed = true; + + if(user){ + this.widthUser = true; + } + this.setWidthActual(width); } @@ -864,8 +870,13 @@ export default class Column extends CoreFeature{ return !column || column.visible ? column : this._prevVisibleColumn(index - 1); } - reinitializeWidth(force){ + reinitializeWidth(force, user){ + if(this.widthUser && !force){ + return; + } + this.widthFixed = false; + this.widthUser = false; //set width if present if(typeof this.definition.width !== "undefined" && !force){ @@ -875,13 +886,13 @@ export default class Column extends CoreFeature{ this.dispatch("column-width-fit-before", this); - this.fitToData(force); + this.fitToData(force, user); this.dispatch("column-width-fit-after", this); } //set column width to maximum cell width for non group columns - fitToData(force){ + fitToData(force, user){ if(this.isGroup){ return; } @@ -909,7 +920,7 @@ export default class Column extends CoreFeature{ var setTo = maxWidth + 1; if(force){ - this.setWidth(setTo); + this.setWidth(setTo, user); }else{ if (this.maxInitialWidth && !force) { setTo = Math.min(setTo, this.maxInitialWidth); @@ -975,4 +986,4 @@ export default class Column extends CoreFeature{ getParentComponent(){ return this.parent instanceof Column ? this.parent.getComponent() : false; } -} \ No newline at end of file +} diff --git a/src/js/core/rendering/renderers/BasicHorizontal.js b/src/js/core/rendering/renderers/BasicHorizontal.js index 747903fdc..1c33966ae 100644 --- a/src/js/core/rendering/renderers/BasicHorizontal.js +++ b/src/js/core/rendering/renderers/BasicHorizontal.js @@ -21,7 +21,9 @@ export default class BasicHorizontal extends Renderer{ reinitializeColumnWidths(columns){ columns.forEach(function(column){ - column.reinitializeWidth(); + if(!column.widthUser){ + column.reinitializeWidth(); + } }); } -} \ No newline at end of file +} diff --git a/src/js/core/rendering/renderers/VirtualDomHorizontal.js b/src/js/core/rendering/renderers/VirtualDomHorizontal.js index d9f23e4ac..042c8ca15 100644 --- a/src/js/core/rendering/renderers/VirtualDomHorizontal.js +++ b/src/js/core/rendering/renderers/VirtualDomHorizontal.js @@ -193,7 +193,7 @@ export default class VirtualDomHorizontal extends Renderer{ for(let i = this.leftCol; i <= this.rightCol; i++){ let col = this.columns[i]; - if(col){ + if(col && !col.widthUser){ col.reinitializeWidth(); } } @@ -225,7 +225,7 @@ export default class VirtualDomHorizontal extends Renderer{ if(this.isFitData){ this.table.columnManager.columnsByIndex.forEach((column) => { - if(!column.definition.width && column.visible){ + if(!column.definition.width && !column.widthUser && column.visible){ change = true; } }); @@ -248,7 +248,9 @@ export default class VirtualDomHorizontal extends Renderer{ let cell = row.cells[colEnd]; rowEl.appendChild(cell.getElement()); - cell.column.reinitializeWidth(); + if(!cell.column.widthUser){ + cell.column.reinitializeWidth(); + } } rowEl.parentNode.removeChild(rowEl); @@ -521,7 +523,7 @@ export default class VirtualDomHorizontal extends Renderer{ fitDataColActualWidthCheck(column){ var newWidth, widthDiff; - if(column.modules.vdomHoz.fitDataCheck){ + if(column.modules.vdomHoz.fitDataCheck && !column.widthUser){ column.reinitializeWidth(); newWidth = column.getWidth(); diff --git a/src/js/modules/Layout/defaults/modes/fitColumns.js b/src/js/modules/Layout/defaults/modes/fitColumns.js index ea5c6d9e8..20f5b15d7 100644 --- a/src/js/modules/Layout/defaults/modes/fitColumns.js +++ b/src/js/modules/Layout/defaults/modes/fitColumns.js @@ -103,7 +103,7 @@ export default function(columns, forced){ if(column.visible){ - width = column.definition.width; + width = column.widthUser ? column.getWidth() : column.definition.width; minWidth = parseInt(column.minWidth); if(width){ @@ -112,7 +112,7 @@ export default function(columns, forced){ fixedWidth += colWidth > minWidth ? colWidth : minWidth; - if(column.definition.widthShrink){ + if(column.definition.widthShrink && !column.widthUser){ fixedShrinkColumns.push({ column:column, width:colWidth > minWidth ? colWidth : minWidth diff --git a/src/js/modules/Layout/defaults/modes/fitDataGeneral.js b/src/js/modules/Layout/defaults/modes/fitDataGeneral.js index 95bdbf530..dcd4cedf7 100644 --- a/src/js/modules/Layout/defaults/modes/fitDataGeneral.js +++ b/src/js/modules/Layout/defaults/modes/fitDataGeneral.js @@ -1,10 +1,12 @@ //resize columns to fit data they contain and stretch row to fill table, also used for fitDataTable export default function(columns, forced){ columns.forEach(function(column){ - column.reinitializeWidth(); + if(!column.widthUser){ + column.reinitializeWidth(); + } }); if(this.table.options.responsiveLayout && this.table.modExists("responsiveLayout", true)){ this.table.modules.responsiveLayout.update(); } -} \ No newline at end of file +} diff --git a/src/js/modules/Layout/defaults/modes/fitDataStretch.js b/src/js/modules/Layout/defaults/modes/fitDataStretch.js index 6d8a8edd9..7d081ba66 100644 --- a/src/js/modules/Layout/defaults/modes/fitDataStretch.js +++ b/src/js/modules/Layout/defaults/modes/fitDataStretch.js @@ -22,6 +22,10 @@ export default function(columns, forced){ if(lastCol){ gap = tableWidth - colsWidth + lastCol.getWidth(); + if(lastCol.widthUser){ + return; + } + if(this.table.options.responsiveLayout && this.table.modExists("responsiveLayout", true)){ lastCol.setWidth(0); this.table.modules.responsiveLayout.update(); @@ -37,4 +41,4 @@ export default function(columns, forced){ this.table.modules.responsiveLayout.update(); } } -} \ No newline at end of file +} diff --git a/src/js/modules/ResizeColumns/ResizeColumns.js b/src/js/modules/ResizeColumns/ResizeColumns.js index fdeb5e58d..977df0532 100644 --- a/src/js/modules/ResizeColumns/ResizeColumns.js +++ b/src/js/modules/ResizeColumns/ResizeColumns.js @@ -162,7 +162,8 @@ export default class ResizeColumns extends Module{ var oldWidth = nearestColumn.getWidth(); e.stopPropagation(); - nearestColumn.reinitializeWidth(true); + // https://github.com/tabulator-tables/tabulator/issues/4840 + nearestColumn.reinitializeWidth(true, true); if(oldWidth !== nearestColumn.getWidth()){ self.dispatch("column-resized", nearestColumn); @@ -242,7 +243,8 @@ export default class ResizeColumns extends Module{ blockedBefore = column.width == column.minWidth || column.width == column.maxWidth; - column.setWidth(this.startWidth + startDiff); + // https://github.com/tabulator-tables/tabulator/issues/4840 + column.setWidth(this.startWidth + startDiff, true); blockedAfter = column.width == column.minWidth || column.width == column.maxWidth; @@ -260,7 +262,8 @@ export default class ResizeColumns extends Module{ } if(this.nextColumn){ - this.nextColumn.setWidth(this.nextColumn.getWidth() - moveDiff); + // https://github.com/tabulator-tables/tabulator/issues/4840 + this.nextColumn.setWidth(this.nextColumn.getWidth() - moveDiff, true); } } @@ -362,4 +365,4 @@ export default class ResizeColumns extends Module{ handle.addEventListener("touchmove", mouseMove, {passive: true}); handle.addEventListener("touchend", mouseUp); } -} \ No newline at end of file +} diff --git a/test/unit/core/ColumnManager.spec.js b/test/unit/core/ColumnManager.spec.js index c887e2130..9a5764090 100644 --- a/test/unit/core/ColumnManager.spec.js +++ b/test/unit/core/ColumnManager.spec.js @@ -1,4 +1,5 @@ import TabulatorFull from "../../../src/js/core/TabulatorFull"; +import ColumnManager from "../../../src/js/core/ColumnManager"; describe("ColumnManager - calculateSorterFromValue with BigInt values", () => { // Fix https://github.com/tabulator-tables/tabulator/pull/4894 @@ -41,3 +42,114 @@ describe("ColumnManager - calculateSorterFromValue with BigInt values", () => { expect(column.definition.sorter).toBe("number"); }); }); + +describe("ColumnManager - column width overflow", () => { + it("sets the table body minWidth when columns overflow horizontally", () => { + // https://github.com/tabulator-tables/tabulator/issues/4840 + const tableElement = document.createElement("div"); + const rowElement = document.createElement("div"); + Object.defineProperty(rowElement, "clientWidth", { value: 300 }); + + const columnManager = new ColumnManager({ + rowManager: { + element: rowElement, + tableElement: tableElement, + } + }); + + columnManager.columnsByIndex = [ + { visible: true, getWidth: () => 220 }, + { visible: true, getWidth: () => 160 }, + ]; + + columnManager.adjustForColumnWidthOverflow(); + + expect(tableElement.style.minWidth).toBe("380px"); + }); + + it("does not set the table body minWidth when columns fit", () => { + // https://github.com/tabulator-tables/tabulator/issues/4840 + const tableElement = document.createElement("div"); + const rowElement = document.createElement("div"); + Object.defineProperty(rowElement, "clientWidth", { value: 300 }); + + const columnManager = new ColumnManager({ + rowManager: { + element: rowElement, + tableElement: tableElement, + } + }); + + columnManager.columnsByIndex = [ + { visible: true, getWidth: () => 120 }, + { visible: true, getWidth: () => 120 }, + ]; + + columnManager.adjustForColumnWidthOverflow(); + + expect(tableElement.style.minWidth).toBe(""); + }); + + it("clears only the table body minWidth it set when overflow is resolved", () => { + // https://github.com/tabulator-tables/tabulator/issues/4840 + const tableElement = document.createElement("div"); + const rowElement = document.createElement("div"); + Object.defineProperty(rowElement, "clientWidth", { value: 300 }); + + const columnManager = new ColumnManager({ + rowManager: { + element: rowElement, + tableElement: tableElement, + } + }); + + columnManager.columnsByIndex = [ + { visible: true, getWidth: () => 220 }, + { visible: true, getWidth: () => 160 }, + ]; + + columnManager.adjustForColumnWidthOverflow(); + expect(tableElement.style.minWidth).toBe("380px"); + + columnManager.columnsByIndex = [ + { visible: true, getWidth: () => 120 }, + { visible: true, getWidth: () => 120 }, + ]; + + columnManager.adjustForColumnWidthOverflow(); + expect(tableElement.style.minWidth).toBe(""); + }); + + it("queues only one post-layout overflow adjustment", () => { + // https://github.com/tabulator-tables/tabulator/issues/4840 + const originalRequestAnimationFrame = global.requestAnimationFrame; + const callbacks = []; + global.requestAnimationFrame = jest.fn((callback) => { + callbacks.push(callback); + }); + + const columnManager = new ColumnManager({ + rowManager: { + element: document.createElement("div"), + tableElement: document.createElement("div"), + } + }); + + columnManager.adjustForColumnWidthOverflow = jest.fn(); + + try { + columnManager.queueColumnWidthOverflowAdjustment(); + columnManager.queueColumnWidthOverflowAdjustment(); + + expect(global.requestAnimationFrame).toHaveBeenCalledTimes(1); + expect(callbacks).toHaveLength(1); + + callbacks[0](); + + expect(columnManager.adjustForColumnWidthOverflow).toHaveBeenCalledTimes(1); + expect(columnManager.columnWidthOverflowAdjustQueued).toBe(false); + } finally { + global.requestAnimationFrame = originalRequestAnimationFrame; + } + }); +}); diff --git a/test/unit/modules/Layout.spec.js b/test/unit/modules/Layout.spec.js index aed0e98ed..ecc3acc54 100644 --- a/test/unit/modules/Layout.spec.js +++ b/test/unit/modules/Layout.spec.js @@ -227,4 +227,114 @@ describe("Layout module", () => { // Verify row height normalization was called (due to textarea formatter) expect(mockTable.rowManager.normalizeHeight).toHaveBeenCalledWith(true); }); + + it("should keep manually resized columns fixed during fitColumns layout", () => { + // https://github.com/tabulator-tables/tabulator/issues/4840 + const userColumn = { + visible: true, + widthUser: true, + definition: {}, + minWidth: 10, + getWidth: jest.fn(() => 180), + setWidth: jest.fn() + }; + const flexColumn = { + visible: true, + widthUser: false, + definition: {}, + minWidth: 10, + getWidth: jest.fn(() => 100), + setWidth: jest.fn() + }; + + defaultModes.fitColumns.call(layout, [userColumn, flexColumn]); + + expect(userColumn.setWidth).not.toHaveBeenCalled(); + expect(flexColumn.setWidth).toHaveBeenCalledWith(600); + }); + + it("should skip manually resized columns during fitDataFill layout", () => { + // https://github.com/tabulator-tables/tabulator/issues/4840 + const userColumn = { + widthUser: true, + reinitializeWidth: jest.fn() + }; + const autoColumn = { + widthUser: false, + reinitializeWidth: jest.fn() + }; + + defaultModes.fitDataFill.call(layout, [userColumn, autoColumn]); + + expect(userColumn.reinitializeWidth).not.toHaveBeenCalled(); + expect(autoColumn.reinitializeWidth).toHaveBeenCalled(); + }); + + it("should not stretch a manually resized final column during fitDataStretch layout", () => { + // https://github.com/tabulator-tables/tabulator/issues/4840 + const firstColumn = { + visible: true, + widthFixed: true, + widthUser: false, + modules: {}, + getWidth: jest.fn(() => 100), + reinitializeWidth: jest.fn(), + setWidth: jest.fn() + }; + const lastColumn = { + visible: true, + widthFixed: true, + widthUser: true, + modules: {}, + getWidth: jest.fn(() => 150), + reinitializeWidth: jest.fn(), + setWidth: jest.fn() + }; + + defaultModes.fitDataStretch.call(layout, [firstColumn, lastColumn]); + + expect(lastColumn.setWidth).not.toHaveBeenCalled(); + expect(lastColumn.reinitializeWidth).not.toHaveBeenCalled(); + }); + + it("should not reset a manually resized final column before responsive fitDataStretch updates", () => { + // https://github.com/tabulator-tables/tabulator/issues/4840 + mockTable.options.responsiveLayout = true; + mockTable.modExists = jest.fn(() => true); + mockTable.modules.responsiveLayout = { + update: jest.fn() + }; + + const firstColumn = { + visible: true, + widthFixed: true, + widthUser: false, + modules: { + responsive: { + visible: true + } + }, + getWidth: jest.fn(() => 100), + reinitializeWidth: jest.fn(), + setWidth: jest.fn() + }; + const lastColumn = { + visible: true, + widthFixed: true, + widthUser: true, + modules: { + responsive: { + visible: true + } + }, + getWidth: jest.fn(() => 150), + reinitializeWidth: jest.fn(), + setWidth: jest.fn() + }; + + defaultModes.fitDataStretch.call(layout, [firstColumn, lastColumn]); + + expect(lastColumn.setWidth).not.toHaveBeenCalled(); + expect(mockTable.modules.responsiveLayout.update).not.toHaveBeenCalled(); + }); }); diff --git a/test/unit/modules/ResizeColumns.spec.js b/test/unit/modules/ResizeColumns.spec.js index 06c3edc7d..2f76becdb 100644 --- a/test/unit/modules/ResizeColumns.spec.js +++ b/test/unit/modules/ResizeColumns.spec.js @@ -76,7 +76,8 @@ describe("ResizeColumns module", () => { resizeColumnsMod.resize({ clientX: 150 }, mockColumn); // Verify column width was set - expect(mockColumn.setWidth).toHaveBeenCalledWith(150); // 100 + (150 - 100) + // https://github.com/tabulator-tables/tabulator/issues/4840 + expect(mockColumn.setWidth).toHaveBeenCalledWith(150, true); // 100 + (150 - 100) // Clean up dispatchSpy.mockRestore(); @@ -145,4 +146,4 @@ describe("ResizeColumns module", () => { // Verify column width was set correctly expect(mockColumn.setWidth).toHaveBeenCalledWith(120); }); -}); \ No newline at end of file +});