From 0249a33c2f227f2a277b568cc71ebcc39061d561 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 16 Apr 2026 01:14:49 +0530 Subject: [PATCH 1/5] feat: sticky columns from UI --- cypress/integration/column.js | 23 +++++++++++++++++++++ index.html | 6 +++--- src/columnmanager.js | 39 ++++++++++++++++++++++++++++++++++- src/datatable.js | 4 ++++ src/defaults.js | 16 ++++++++++++++ src/translations/de.json | 2 ++ src/translations/en.json | 2 ++ src/translations/fr.json | 2 ++ src/translations/it.json | 2 ++ 9 files changed, 92 insertions(+), 4 deletions(-) diff --git a/cypress/integration/column.js b/cypress/integration/column.js index 94f7c972..f33956ae 100644 --- a/cypress/integration/column.js +++ b/cypress/integration/column.js @@ -68,6 +68,29 @@ describe('Column', function () { .and('match', /9\dpx/); }); + it('pins a column from the dropdown menu', function () { + cy.clickDropdown(3); + cy.clickDropdownItem(3, 'Stick to left'); + + cy.window().then(win => win.datatable.getColumn(3)) + .its('sticky') + .should('eq', true); + + cy.get('.dt-scrollable').then(($scrollable) => { + const scrollable = $scrollable[0]; + const stickyBodyCell = Cypress.$('.dt-cell--3-0')[0]; + const initialStickyBodyLeft = stickyBodyCell.getBoundingClientRect().left; + + scrollable.scrollLeft = 220; + scrollable.dispatchEvent(new Event('scroll')); + + cy.wait(50).then(() => { + const nextStickyBodyLeft = stickyBodyCell.getBoundingClientRect().left; + expect(nextStickyBodyLeft).to.be.closeTo(initialStickyBodyLeft, 1); + }); + }); + }); + it('keeps sticky columns pinned while scrolling horizontally', function () { const expectPinned = (actual, expected) => { expect(actual).to.be.closeTo(expected, 1); diff --git a/index.html b/index.html index bd545dda..a7868c6b 100644 --- a/index.html +++ b/index.html @@ -151,9 +151,9 @@

Frappe DataTable

function buildData() { columns = [ - { name: "Name", width: 150, sticky: true }, - { name: "Position", width: 200 }, - { name: "Office", sticky: true }, + { name: "Name", width: 150}, + { name: "Position", width: 200}, + { name: "Office", sticky: true}, { name: "Extn." }, { name: "Start Date", diff --git a/src/columnmanager.js b/src/columnmanager.js index 93344da2..d27d9acf 100644 --- a/src/columnmanager.js +++ b/src/columnmanager.js @@ -93,7 +93,7 @@ export default class ColumnManager { }); $.on(this.$dropdownList, 'click', '.dt-dropdown__list-item', (e, $item) => { - if (!this._dropdownActiveColIndex) return; + if (this._dropdownActiveColIndex == null) return; const dropdownItems = this.options.headerDropdown; const { index } = $.data($item); const colIndex = this._dropdownActiveColIndex; @@ -108,6 +108,11 @@ export default class ColumnManager { _this.hideDropdown(); } + this.stickDropdownIndex = this.options.headerDropdown + .findIndex(item => item.stickyAction === 'stick'); + this.unstickDropdownIndex = this.options.headerDropdown + .findIndex(item => item.stickyAction === 'unstick'); + this.hideDropdown(); } @@ -124,6 +129,7 @@ export default class ColumnManager { const $cell = $.closest('.dt-cell', e.target); const { colIndex } = $.data($cell); this._dropdownActiveColIndex = colIndex; + this.updateStickyDropdownItems(this.getColumn(colIndex)); } hideDropdown() { @@ -304,6 +310,20 @@ export default class ColumnManager { }); } + setColumnSticky(colIndex, sticky) { + const column = this.getColumn(colIndex); + if (!column || column.sticky === sticky) { + return; + } + + this.instance.freeze(); + this.datamanager.updateColumn(colIndex, { sticky }); + + this.refreshHeader(); + this.rowmanager.refreshRows() + .then(() => this.instance.unfreeze()); + } + switchColumn(oldIndex, newIndex) { this.instance.freeze(); this.datamanager.switchColumn(oldIndex, newIndex) @@ -493,4 +513,21 @@ export default class ColumnManager { toggleDropdownItem(index) { $('.dt-dropdown__list', this.instance.dropdownContainer).children[index].classList.toggle('dt-hidden'); } + + updateStickyDropdownItems(column) { + if (!column) return; + if (this.stickDropdownIndex === -1 || this.unstickDropdownIndex === -1) return; + + const stickItem = this.$dropdownList.children[this.stickDropdownIndex]; + const unstickItem = this.$dropdownList.children[this.unstickDropdownIndex]; + if (!(stickItem && unstickItem)) return; + + if (column.sticky) { + stickItem.classList.add('dt-hidden'); + unstickItem.classList.remove('dt-hidden'); + } else { + stickItem.classList.remove('dt-hidden'); + unstickItem.classList.add('dt-hidden'); + } + } } diff --git a/src/datatable.js b/src/datatable.js index c34ebc34..6d1f62a5 100644 --- a/src/datatable.js +++ b/src/datatable.js @@ -222,6 +222,10 @@ class DataTable { this.columnmanager.removeColumn(colIndex); } + setColumnSticky(colIndex, sticky) { + this.columnmanager.setColumnSticky(colIndex, sticky); + } + scrollToLastColumn() { this.datatableWrapper.scrollLeft = 9999; } diff --git a/src/defaults.js b/src/defaults.js index 359f8e8a..b815f8ed 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -30,6 +30,22 @@ export default function getDefaultOptions(instance) { action: function (column) { this.removeColumn(column.colIndex); } + }, + { + label: instance.translate('Stick to left'), + stickyAction: 'stick', + display: 'hidden', + action: function (column) { + this.setColumnSticky(column.colIndex, true); + } + }, + { + label: instance.translate('Unstick from left'), + stickyAction: 'unstick', + display: 'hidden', + action: function (column) { + this.setColumnSticky(column.colIndex, false); + } } ], events: { diff --git a/src/translations/de.json b/src/translations/de.json index 0e667df5..584e3af1 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -3,6 +3,8 @@ "Sort Descending": "Absteigend sortieren", "Reset sorting": "Sortierung zurücksetzen", "Remove column": "Spalte entfernen", + "Stick to left": "Links anheften", + "Unstick from left": "Linke Anheftung lösen", "No Data": "Keine Daten", "{count} cells copied": { "1": "{count} Zelle kopiert", diff --git a/src/translations/en.json b/src/translations/en.json index 80298682..ce674a1f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3,6 +3,8 @@ "Sort Descending": "Sort Descending", "Reset sorting": "Reset sorting", "Remove column": "Remove column", + "Stick to left": "Stick to left", + "Unstick from left": "Unstick from left", "No Data": "No Data", "{count} cells copied": { "1": "{count} cell copied", diff --git a/src/translations/fr.json b/src/translations/fr.json index 194ec108..3540ef9d 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -3,6 +3,8 @@ "Sort Descending": "Trier par ordre décroissant", "Reset sorting": "Réinitialiser le tri", "Remove column": "Supprimer colonne", + "Stick to left": "Épingler à gauche", + "Unstick from left": "Désépingler de la gauche", "No Data": "Pas de données", "{count} cells copied": { "1": "{count} cellule copiée", diff --git a/src/translations/it.json b/src/translations/it.json index a7308c15..2454edf2 100644 --- a/src/translations/it.json +++ b/src/translations/it.json @@ -3,6 +3,8 @@ "Sort Descending": "Ordinamento decrescente", "Reset sorting": "Azzeramento ordinamento", "Remove column": "Rimuovi colonna", + "Stick to left": "Blocca a sinistra", + "Unstick from left": "Sblocca dalla sinistra", "No Data": "Nessun dato", "{count} cells copied": { "1": "Copiato {count} cella", From 84ea493aa2ccafe90160319406744da77a9793e5 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 16 Apr 2026 10:00:44 +0530 Subject: [PATCH 2/5] test: change before to beforeEach --- cypress/integration/column.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/column.js b/cypress/integration/column.js index f33956ae..ccbc363f 100644 --- a/cypress/integration/column.js +++ b/cypress/integration/column.js @@ -1,5 +1,5 @@ describe('Column', function () { - before(function () { + beforeEach(function () { cy.visit('/'); }); From 27a9c7197e90c8844498d5061abd24eb8907b1f7 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 16 Apr 2026 10:06:56 +0530 Subject: [PATCH 3/5] test: change sticky column in test --- cypress/integration/column.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cypress/integration/column.js b/cypress/integration/column.js index ccbc363f..e8b3eda9 100644 --- a/cypress/integration/column.js +++ b/cypress/integration/column.js @@ -69,16 +69,16 @@ describe('Column', function () { }); it('pins a column from the dropdown menu', function () { - cy.clickDropdown(3); - cy.clickDropdownItem(3, 'Stick to left'); + cy.clickDropdown(2); + cy.clickDropdownItem(2, 'Stick to left'); - cy.window().then(win => win.datatable.getColumn(3)) + cy.window().then(win => win.datatable.getColumn(2)) .its('sticky') .should('eq', true); cy.get('.dt-scrollable').then(($scrollable) => { const scrollable = $scrollable[0]; - const stickyBodyCell = Cypress.$('.dt-cell--3-0')[0]; + const stickyBodyCell = Cypress.$('.dt-cell--2-0')[0]; const initialStickyBodyLeft = stickyBodyCell.getBoundingClientRect().left; scrollable.scrollLeft = 220; @@ -102,9 +102,9 @@ describe('Column', function () { 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 stickyCustomBodyCell = Cypress.$('.dt-cell--4-0')[0]; + const stickyCustomHeaderCell = Cypress.$('.dt-cell--header-4')[0]; + const regularBodyCell = Cypress.$('.dt-cell--2-0')[0]; const initialStickyCheckboxBodyLeft = stickyCheckboxBodyCell.getBoundingClientRect().left; const initialStickyCheckboxHeaderLeft = stickyCheckboxHeaderCell.getBoundingClientRect().left; From 6511144e6b22f24726e98f6d8dc72e39e077f618 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 16 Apr 2026 10:10:09 +0530 Subject: [PATCH 4/5] test: match state with window state --- cypress/integration/column.js | 76 ++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/cypress/integration/column.js b/cypress/integration/column.js index e8b3eda9..1376d6aa 100644 --- a/cypress/integration/column.js +++ b/cypress/integration/column.js @@ -96,43 +96,45 @@ describe('Column', function () { 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--4-0')[0]; - const stickyCustomHeaderCell = Cypress.$('.dt-cell--header-4')[0]; - const regularBodyCell = Cypress.$('.dt-cell--2-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); + cy.window().then((win) => { + const stickyColumns = win.datatable.getColumns() + .filter(column => column.sticky) + .map(column => column.colIndex); + const nonStickyColumn = win.datatable.getColumns() + .find(column => !column.sticky && column.focusable !== false); + + cy.get('.dt-scrollable').then(($scrollable) => { + const scrollable = $scrollable[0]; + const stickyBodyCells = stickyColumns + .map(colIndex => Cypress.$(`.dt-cell--${colIndex}-0`)[0]); + const stickyHeaderCells = stickyColumns + .map(colIndex => Cypress.$(`.dt-cell--header-${colIndex}`)[0]); + const regularBodyCell = Cypress.$(`.dt-cell--${nonStickyColumn.colIndex}-0`)[0]; + + const initialStickyBodyLefts = stickyBodyCells + .map(cell => cell.getBoundingClientRect().left); + const initialStickyHeaderLefts = stickyHeaderCells + .map(cell => cell.getBoundingClientRect().left); + const initialRegularBodyLeft = regularBodyCell.getBoundingClientRect().left; + + scrollable.scrollLeft = 220; + scrollable.dispatchEvent(new Event('scroll')); + + cy.wait(50).then(() => { + const nextStickyBodyLefts = stickyBodyCells + .map(cell => cell.getBoundingClientRect().left); + const nextStickyHeaderLefts = stickyHeaderCells + .map(cell => cell.getBoundingClientRect().left); + const nextRegularBodyLeft = regularBodyCell.getBoundingClientRect().left; + + nextStickyBodyLefts.forEach((left, index) => { + expectPinned(left, initialStickyBodyLefts[index]); + }); + nextStickyHeaderLefts.forEach((left, index) => { + expectPinned(left, initialStickyHeaderLefts[index]); + }); + expect(nextRegularBodyLeft).to.be.lessThan(initialRegularBodyLeft); + }); }); }); }); From 646946a10a68ee0379cae6e4402137007d3de754 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 16 Apr 2026 10:14:15 +0530 Subject: [PATCH 5/5] test: remove bad assumptions from test --- cypress/integration/column.js | 75 +++++++++++++++-------------------- 1 file changed, 32 insertions(+), 43 deletions(-) diff --git a/cypress/integration/column.js b/cypress/integration/column.js index 1376d6aa..bab33c49 100644 --- a/cypress/integration/column.js +++ b/cypress/integration/column.js @@ -92,49 +92,38 @@ describe('Column', function () { }); it('keeps sticky columns pinned while scrolling horizontally', function () { - const expectPinned = (actual, expected) => { - expect(actual).to.be.closeTo(expected, 1); - }; - - cy.window().then((win) => { - const stickyColumns = win.datatable.getColumns() - .filter(column => column.sticky) - .map(column => column.colIndex); - const nonStickyColumn = win.datatable.getColumns() - .find(column => !column.sticky && column.focusable !== false); - - cy.get('.dt-scrollable').then(($scrollable) => { - const scrollable = $scrollable[0]; - const stickyBodyCells = stickyColumns - .map(colIndex => Cypress.$(`.dt-cell--${colIndex}-0`)[0]); - const stickyHeaderCells = stickyColumns - .map(colIndex => Cypress.$(`.dt-cell--header-${colIndex}`)[0]); - const regularBodyCell = Cypress.$(`.dt-cell--${nonStickyColumn.colIndex}-0`)[0]; - - const initialStickyBodyLefts = stickyBodyCells - .map(cell => cell.getBoundingClientRect().left); - const initialStickyHeaderLefts = stickyHeaderCells - .map(cell => cell.getBoundingClientRect().left); - const initialRegularBodyLeft = regularBodyCell.getBoundingClientRect().left; - - scrollable.scrollLeft = 220; - scrollable.dispatchEvent(new Event('scroll')); - - cy.wait(50).then(() => { - const nextStickyBodyLefts = stickyBodyCells - .map(cell => cell.getBoundingClientRect().left); - const nextStickyHeaderLefts = stickyHeaderCells - .map(cell => cell.getBoundingClientRect().left); - const nextRegularBodyLeft = regularBodyCell.getBoundingClientRect().left; - - nextStickyBodyLefts.forEach((left, index) => { - expectPinned(left, initialStickyBodyLefts[index]); - }); - nextStickyHeaderLefts.forEach((left, index) => { - expectPinned(left, initialStickyHeaderLefts[index]); - }); - expect(nextRegularBodyLeft).to.be.lessThan(initialRegularBodyLeft); - }); + cy.get('.dt-scrollable').then(($scrollable) => { + const scrollable = $scrollable[0]; + const checkboxBodyCell = Cypress.$('.dt-cell--0-0')[0]; + const checkboxHeaderCell = Cypress.$('.dt-cell--header-0')[0]; + const serialBodyCell = Cypress.$('.dt-cell--1-0')[0]; + const serialHeaderCell = Cypress.$('.dt-cell--header-1')[0]; + const officeBodyCell = Cypress.$('.dt-cell--4-0')[0]; + const officeHeaderCell = Cypress.$('.dt-cell--header-4')[0]; + const nameBodyCell = Cypress.$('.dt-cell--2-0')[0]; + + const initialCheckboxLeft = checkboxBodyCell.getBoundingClientRect().left; + const initialSerialLeft = serialBodyCell.getBoundingClientRect().left; + const initialNameLeft = nameBodyCell.getBoundingClientRect().left; + + scrollable.scrollLeft = 220; + scrollable.dispatchEvent(new Event('scroll')); + + cy.wait(50).then(() => { + const nextCheckboxBodyLeft = checkboxBodyCell.getBoundingClientRect().left; + const nextCheckboxHeaderLeft = checkboxHeaderCell.getBoundingClientRect().left; + const nextSerialBodyLeft = serialBodyCell.getBoundingClientRect().left; + const nextSerialHeaderLeft = serialHeaderCell.getBoundingClientRect().left; + const nextOfficeBodyLeft = officeBodyCell.getBoundingClientRect().left; + const nextOfficeHeaderLeft = officeHeaderCell.getBoundingClientRect().left; + const nextNameLeft = nameBodyCell.getBoundingClientRect().left; + + expect(nextCheckboxBodyLeft).to.be.closeTo(initialCheckboxLeft, 1); + expect(nextSerialBodyLeft).to.be.closeTo(initialSerialLeft, 1); + expect(nextCheckboxHeaderLeft).to.be.closeTo(nextCheckboxBodyLeft, 1); + expect(nextSerialHeaderLeft).to.be.closeTo(nextSerialBodyLeft, 1); + expect(nextOfficeHeaderLeft).to.be.closeTo(nextOfficeBodyLeft, 1); + expect(nextNameLeft).to.be.lessThan(initialNameLeft); }); }); });