diff --git a/cypress/integration/column.js b/cypress/integration/column.js
index b80d724..94f7c97 100644
--- a/cypress/integration/column.js
+++ b/cypress/integration/column.js
@@ -67,4 +67,50 @@ describe('Column', function () {
cy.getCell(4, 1).should('have.css', 'width')
.and('match', /9\dpx/);
});
-});
\ No newline at end of file
+
+ it('keeps sticky columns pinned while scrolling horizontally', function () {
+ const expectPinned = (actual, expected) => {
+ expect(actual).to.be.closeTo(expected, 1);
+ };
+
+ cy.get('.dt-scrollable').then(($scrollable) => {
+ const scrollable = $scrollable[0];
+ const stickyCheckboxBodyCell = Cypress.$('.dt-cell--0-0')[0];
+ const stickyCheckboxHeaderCell = Cypress.$('.dt-cell--header-0')[0];
+ const stickySerialBodyCell = Cypress.$('.dt-cell--1-0')[0];
+ const stickySerialHeaderCell = Cypress.$('.dt-cell--header-1')[0];
+ const stickyCustomBodyCell = Cypress.$('.dt-cell--2-0')[0];
+ const stickyCustomHeaderCell = Cypress.$('.dt-cell--header-2')[0];
+ const regularBodyCell = Cypress.$('.dt-cell--4-0')[0];
+
+ const initialStickyCheckboxBodyLeft = stickyCheckboxBodyCell.getBoundingClientRect().left;
+ const initialStickyCheckboxHeaderLeft = stickyCheckboxHeaderCell.getBoundingClientRect().left;
+ const initialStickySerialBodyLeft = stickySerialBodyCell.getBoundingClientRect().left;
+ const initialStickySerialHeaderLeft = stickySerialHeaderCell.getBoundingClientRect().left;
+ const initialStickyCustomBodyLeft = stickyCustomBodyCell.getBoundingClientRect().left;
+ const initialStickyCustomHeaderLeft = stickyCustomHeaderCell.getBoundingClientRect().left;
+ const initialRegularBodyLeft = regularBodyCell.getBoundingClientRect().left;
+
+ scrollable.scrollLeft = 220;
+ scrollable.dispatchEvent(new Event('scroll'));
+
+ cy.wait(50).then(() => {
+ const nextStickyCheckboxBodyLeft = stickyCheckboxBodyCell.getBoundingClientRect().left;
+ const nextStickyCheckboxHeaderLeft = stickyCheckboxHeaderCell.getBoundingClientRect().left;
+ const nextStickySerialBodyLeft = stickySerialBodyCell.getBoundingClientRect().left;
+ const nextStickySerialHeaderLeft = stickySerialHeaderCell.getBoundingClientRect().left;
+ const nextStickyCustomBodyLeft = stickyCustomBodyCell.getBoundingClientRect().left;
+ const nextStickyCustomHeaderLeft = stickyCustomHeaderCell.getBoundingClientRect().left;
+ const nextRegularBodyLeft = regularBodyCell.getBoundingClientRect().left;
+
+ expectPinned(nextStickyCheckboxBodyLeft, initialStickyCheckboxBodyLeft);
+ expectPinned(nextStickyCheckboxHeaderLeft, initialStickyCheckboxHeaderLeft);
+ expectPinned(nextStickySerialBodyLeft, initialStickySerialBodyLeft);
+ expectPinned(nextStickySerialHeaderLeft, initialStickySerialHeaderLeft);
+ expectPinned(nextStickyCustomBodyLeft, initialStickyCustomBodyLeft);
+ expectPinned(nextStickyCustomHeaderLeft, initialStickyCustomHeaderLeft);
+ expect(nextRegularBodyLeft).to.be.lessThan(initialRegularBodyLeft);
+ });
+ });
+ });
+});
diff --git a/index.html b/index.html
index 58ed474..12fb222 100644
--- a/index.html
+++ b/index.html
@@ -151,9 +151,9 @@
Frappe DataTable
function buildData() {
columns = [
- { name: "Name", width: 150,},
+ { name: "Name", width: 150, sticky: true },
{ name: "Position", width: 200 },
- { name: "Office", sticky: true },
+ { name: "Office", sticky: true },
{ name: "Extn." },
{
name: "Start Date",
diff --git a/src/cellmanager.js b/src/cellmanager.js
index 8275bfd..84c675f 100644
--- a/src/cellmanager.js
+++ b/src/cellmanager.js
@@ -812,8 +812,15 @@ export default class CellManager {
});
const row = this.datamanager.getRow(rowIndex);
+ const column = cell.column || this.datamanager.getColumn(colIndex) || {};
const isBodyCell = !(isHeader || isFilter || isTotalRow);
+ const isSticky = Boolean(column.sticky);
+ const stickyColumns = this.datamanager.getColumns().filter(col => col.sticky);
+ const lastStickyColumn = stickyColumns[stickyColumns.length - 1];
+ const isLastStickyColumn = isSticky &&
+ lastStickyColumn &&
+ lastStickyColumn.colIndex === colIndex;
const className = [
'dt-cell',
@@ -823,7 +830,10 @@ export default class CellManager {
isHeader ? 'dt-cell--header' : '',
isHeader ? `dt-cell--header-${colIndex}` : '',
isFilter ? 'dt-cell--filter' : '',
- isBodyCell && (row && row.meta.isTreeNodeClose) ? 'dt-cell--tree-close' : ''
+ isBodyCell && (row && row.meta.isTreeNodeClose) ? 'dt-cell--tree-close' : '',
+ isSticky ? 'dt-cell--sticky' : '',
+ isSticky && !isBodyCell ? 'dt-cell--sticky-top' : '',
+ isLastStickyColumn ? 'dt-cell--sticky-last' : ''
].join(' ');
return `
diff --git a/src/datamanager.js b/src/datamanager.js
index 6e7ba0b..1071862 100644
--- a/src/datamanager.js
+++ b/src/datamanager.js
@@ -62,6 +62,7 @@ export default class DataManager {
sortable: false,
focusable: false,
dropdown: false,
+ sticky: true,
width: 32
};
this.columns.push(cell);
@@ -75,7 +76,8 @@ export default class DataManager {
editable: false,
resizable: false,
focusable: false,
- dropdown: false
+ dropdown: false,
+ sticky: true
};
if (this.options.data.length > 1000) {
cell.resizable = true;
diff --git a/src/style.css b/src/style.css
index c14fdb9..21082ff 100644
--- a/src/style.css
+++ b/src/style.css
@@ -17,6 +17,7 @@
--dt-toast-message-border: none;
--dt-header-cell-bg: var(--dt-cell-bg);
--dt-no-data-message-width: 90px;
+ --dt-scroll-left: 0px;
}
.datatable {
@@ -170,6 +171,29 @@
&:last-child {
border-right: 1px solid var(--dt-border-color);
}
+
+ &--sticky {
+ position: sticky;
+ left: 0;
+ z-index: 1;
+ }
+
+ &--sticky-top {
+ z-index: 4;
+ will-change: transform;
+ }
+
+ &--sticky-last::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: -1px;
+ width: 8px;
+ height: 100%;
+ pointer-events: none;
+ box-shadow: 4px 0 6px -4px rgba(15, 23, 42, 0.2);
+ }
+
}
.datatable[dir=rtl] .dt-cell__resize-handle {
diff --git a/src/style.js b/src/style.js
index c846ad3..f977f67 100644
--- a/src/style.js
+++ b/src/style.js
@@ -41,6 +41,7 @@ export default class Style {
bindScrollHeader() {
this._settingHeaderPosition = false;
+ this.updateStickyTopPositions(0);
$.on(this.bodyScrollable, 'scroll', (e) => {
if (this._settingHeaderPosition) return;
@@ -48,7 +49,8 @@ export default class Style {
this._settingHeaderPosition = true;
requestAnimationFrame(() => {
- const left = -e.target.scrollLeft;
+ const scrollLeft = e.target.scrollLeft;
+ const left = -scrollLeft;
$.style(this.header, {
transform: `translateX(${left}px)`
@@ -56,6 +58,7 @@ export default class Style {
$.style(this.footer, {
transform: `translateX(${left}px)`
});
+ this.updateStickyTopPositions(scrollLeft);
this._settingHeaderPosition = false;
if (this.instance.noData) {
$.style($('.no-data-message'), {
@@ -153,6 +156,8 @@ export default class Style {
this.setupColumnWidth();
this.distributeRemainingWidth();
this.setColumnStyle();
+ this.setStickyColumnStyle();
+ this.updateStickyTopPositions(this.bodyScrollable.scrollLeft || 0);
this.setBodyStyle();
}
@@ -310,6 +315,8 @@ export default class Style {
this.columnmanager.setColumnHeaderWidth(column.colIndex);
this.columnmanager.setColumnWidth(column.colIndex);
});
+ this.setStickyColumnStyle();
+ this.updateStickyTopPositions(this.bodyScrollable.scrollLeft || 0);
}
setBodyStyle() {
@@ -371,6 +378,56 @@ export default class Style {
return $(`.dt-cell--col-${colIndex}`, this.header);
}
+ setStickyColumnStyle() {
+ if (!this.datamanager || !this.datamanager.getColumns) return;
+
+ const stickySelectors = [];
+ let stickyOffset = 0;
+ let normalOffset = 0;
+
+ this.datamanager.getColumns().forEach((column) => {
+ const $headerCell = this.getColumnHeaderElement(column.colIndex);
+ const renderedWidth = $headerCell ? $headerCell.offsetWidth : column.width;
+
+ if (column.sticky) {
+ const selector = `.dt-cell--col-${column.colIndex}.dt-cell--sticky`;
+ const style = {
+ left: `${stickyOffset}px`
+ };
+
+ column.stickyLeft = stickyOffset;
+ column.stickyScrollTrigger = normalOffset - stickyOffset;
+ column.renderedWidth = renderedWidth;
+ this.setStyle(selector, style);
+ stickySelectors.push(selector);
+ stickyOffset += renderedWidth;
+ }
+ normalOffset += renderedWidth;
+ });
+
+ const staleSelectors = (this._stickySelectors || [])
+ .filter(selector => !stickySelectors.includes(selector));
+
+ staleSelectors.forEach(selector => this.removeStyle(selector));
+ this._stickySelectors = stickySelectors;
+ }
+
+ updateStickyTopPositions(scrollLeft) {
+ if (!this.datamanager || !this.datamanager.getColumns) return;
+
+ const stickyColumns = this.datamanager.getColumns().filter(column => column.sticky);
+
+ stickyColumns.forEach((column) => {
+ const trigger = Math.max(0, column.stickyScrollTrigger || 0);
+ const compensation = Math.max(0, scrollLeft - trigger);
+ const cells = $.each(`.dt-cell--col-${column.colIndex}.dt-cell--sticky-top`, this.wrapper) || [];
+
+ $.style(cells, {
+ transform: compensation ? `translateX(${compensation}px)` : ''
+ });
+ });
+ }
+
getRowIndexColumnWidth() {
const rowCount = this.datamanager.getRowCount();
const padding = 22;