-
Notifications
You must be signed in to change notification settings - Fork 867
Description
Relative issue : #4364
Bug Description
Tabulator incorrectly calculates header heights when the table container is inside an element with CSS transform: scale()
applied. This results in misaligned headers and improper table rendering.
Environment
- Tabulator Version: 6.3
- Browser: Chrome/Edge/Firefox (reproduced across browsers)
- OS: Windows 11
- Application Type: Electron app with pan/zoom workspace
Reproduction Steps
- Create a container with CSS transform scale:
.workspace {
transform: scale(0.5); /* Any scale different from 1 */
transform-origin: top left;
}
- Initialize a Tabulator table inside this scaled container:
var table = new Tabulator("#example-table", {
height: "300px",
layout: "fitDataFill",
data: [...],
columns: [...]
});
- Observe that header heights are calculated incorrectly
- The issue is more visible with complex headers or when resizing
Expected Behavior
Headers should render correctly regardless of parent container's CSS transform scale.
Actual Behavior
- Header heights appear compressed or stretched
- Column alignments are misaligned with header text
- Pagination controls are also affected with incorrect sizing and positioning
- Table rendering is visually broken
- Issue persists even after calling
table.redraw()
ortable.redraw(true)
Code Example
HTML Structure
<div class="workspace" style="transform: scale(0.75)">
<div class="widget">
<div id="my-table"></div>
</div>
</div>
JavaScript
const table = new Tabulator("#my-table", {
height: "250px",
layout: "fitDataFill",
data: [
{name: "John", age: 25, city: "New York"},
{name: "Jane", age: 30, city: "London"}
],
columns: [
{title: "Name", field: "name"},
{title: "Age", field: "age"},
{title: "City", field: "city"}
]
});
CSS
.workspace {
transform: scale(0.75);
transform-origin: top left;
width: 100vw;
height: 100vh;
}
.widget {
position: absolute;
width: 400px;
height: 300px;
}
Root Cause Analysis
The issue appears to be related to how Tabulator calculates dimensions using getBoundingClientRect()
or similar DOM measurement methods. When a parent element has transform: scale()
, these measurements return scaled values, but Tabulator's internal calculations don't account for this scaling factor.
Workarounds Attempted
- Manual redraw after initialization:
table.redraw(true)
- No effect - Delayed initialization:
setTimeout(() => table.redraw(), 100)
- No effect - ResizeObserver: Monitoring container size changes - Partial improvement but still buggy
- CSS zoom instead of transform: Not viable for our use case
- Recalculating dimensions manually: Complex and unreliable
Impact
This bug affects any application that uses CSS transforms for:
- Zoom/pan interfaces
- Responsive scaling
- Canvas-based applications
- Dashboard widgets with scaling
Suggested Solution
Tabulator should detect when it's inside a scaled container and adjust its dimension calculations accordingly. This could be done by:
- Walking up the DOM tree to detect
transform: scale()
on parent elements - Calculating the cumulative scale factor
- Adjusting internal dimension calculations by this factor
Proposed Patch (Experimental)
Note: An experimental patch has been developed and tested locally. This patch requires proper validation and integration by the Tabulator development team.
Patch Overview
The patch adds three utility functions to detect and correct CSS transform scale issues:
// Utility function to correct dimensions when CSS transform scale is applied
function getCorrectedDimensions(element, dimension) {
if (!element) return 0;
const rect = element.getBoundingClientRect();
const scaleFactors = getTransformScaleFactors(element);
switch(dimension) {
case 'height':
return Math.ceil(rect.height / scaleFactors.y);
case 'width':
return Math.ceil(rect.width / scaleFactors.x);
default:
return rect;
}
}
// Get cumulative CSS transform scale factors from element and all parents
function getTransformScaleFactors(element) {
let scaleX = 1, scaleY = 1;
let current = element;
while (current && current !== document.body) {
const style = window.getComputedStyle(current);
const transform = style.transform;
if (transform && transform !== 'none') {
const matrix = transform.match(/matrix\(([^)]+)\)/);
const matrix3d = transform.match(/matrix3d\(([^)]+)\)/);
if (matrix) {
const values = matrix[1].split(',').map(parseFloat);
scaleX *= values[0] || 1;
scaleY *= values[3] || 1;
} else if (matrix3d) {
const values = matrix3d[1].split(',').map(parseFloat);
scaleX *= values[0] || 1;
scaleY *= values[5] || 1;
}
}
current = current.parentElement;
}
return { x: scaleX, y: scaleY };
}
// Get corrected bounding client rect for range selections and positioning
function getCorrectedRect(element) {
if (!element) return { top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0 };
const rect = element.getBoundingClientRect();
const scaleFactors = getTransformScaleFactors(element);
return {
top: rect.top,
bottom: rect.top + rect.height / scaleFactors.y,
left: rect.left,
right: rect.left + rect.width / scaleFactors.x,
width: rect.width / scaleFactors.x,
height: rect.height / scaleFactors.y
};
}
Key Modifications Required
-
Column height calculations (around line 1509):
// Replace: return Math.ceil(this.element.getBoundingClientRect().height); // With: return getCorrectedDimensions(this.element, 'height');
-
Vertical fill mode calculations (around line 5785):
// Replace getBoundingClientRect() calls with getCorrectedDimensions() let columnHeight = getCorrectedDimensions(this.table.columnManager.getElement(), 'height'); let footerHeight = (this.table.footerManager && this.table.footerManager.active && !this.table.footerManager.external) ? getCorrectedDimensions(this.table.footerManager.getElement(), 'height') : 0; let otherHeight = Math.floor(columnHeight + footerHeight);
-
Column width fitting (around line 7428):
// Replace: var totalWidth = this.table.rowManager.element.getBoundingClientRect().width; // With: var totalWidth = getCorrectedDimensions(this.table.rowManager.element, 'width');
-
Range selection positioning (around lines 27155-27158):
// Replace getBoundingClientRect() calls with getCorrectedRect() rowRect = getCorrectedRect(row.getElement()); columnRect = getCorrectedRect(column.getElement()); rowManagerRect = getCorrectedRect(this.table.rowManager.getElement()); columnManagerRect = getCorrectedRect(this.table.columnManager.getElement());
Testing Results
- ✅ Headers maintain correct proportions at all scale levels (0.25x to 1.5x)
- ✅ Pagination controls render correctly regardless of transform scale
- ✅ Column width calculations work properly in scaled containers
- ✅ Range selections function correctly
- ✅ No regression observed in non-scaled environments
Limitations
This patch is experimental and:
- Requires thorough testing across different browsers and environments
- Needs performance impact assessment
- Should be reviewed by the Tabulator development team
- May need optimization for production use
- Only handles
matrix()
andmatrix3d()
transforms (most common cases)
Live Demo
A complete HTML demonstration file is available that shows the bug in action:
- Side-by-side comparison: Normal table vs scaled table
- Interactive scale slider: Test different zoom levels
- Pagination toggle: See how pagination is also affected
- Real-time testing: Redraw buttons and data addition to test fixes
The demo file can be opened directly in any browser to reproduce the issue immediately.
Additional Information
- The issue occurs with both programmatic table creation and dynamic data updates
- It's reproducible with minimal code (see example above)
- The bug affects the visual rendering but doesn't break functionality
- Similar issues might exist with other CSS transforms (
rotate
,skew
, etc.) - Pagination controls are equally affected with incorrect sizing and positioning
Related Issues
This might be related to similar issues in other libraries that calculate DOM dimensions inside transformed containers.
Note: This issue significantly impacts user experience in applications with zoom functionality. An experimental patch is available for testing but requires proper validation and integration by the development team.