From 809903a1bd4eb5d935cfd17666cbca0600c19fdb Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Sat, 15 Jun 2024 16:06:39 -0400 Subject: [PATCH] feat(footer): add Footer Totals Row and fix footer styling (#1576) - this new demo was mainly doable before but this PR also includes CSS styling fixes for the footer totals to show up correctly including fixes for the Dark Mode as well --- .../src/app-routing.ts | 2 + .../vite-demo-vanilla-bundle/src/app.html | 3 + .../src/examples/example20.ts | 12 +- .../src/examples/example24.html | 25 +++ .../src/examples/example24.ts | 143 ++++++++++++++++++ .../vite-demo-vanilla-bundle/src/styles.scss | 3 + packages/common/src/styles/_variables.scss | 11 ++ packages/common/src/styles/slick-grid.scss | 14 ++ test/cypress/e2e/example24.cy.ts | 58 +++++++ 9 files changed, 260 insertions(+), 11 deletions(-) create mode 100644 examples/vite-demo-vanilla-bundle/src/examples/example24.html create mode 100644 examples/vite-demo-vanilla-bundle/src/examples/example24.ts create mode 100644 test/cypress/e2e/example24.cy.ts diff --git a/examples/vite-demo-vanilla-bundle/src/app-routing.ts b/examples/vite-demo-vanilla-bundle/src/app-routing.ts index f09d80d6c..32159c197 100644 --- a/examples/vite-demo-vanilla-bundle/src/app-routing.ts +++ b/examples/vite-demo-vanilla-bundle/src/app-routing.ts @@ -24,6 +24,7 @@ import Example20 from './examples/example20'; import Example21 from './examples/example21'; import Example22 from './examples/example22'; import Example23 from './examples/example23'; +import Example24 from './examples/example24'; export class AppRouting { constructor(private config: RouterConfig) { @@ -53,6 +54,7 @@ export class AppRouting { { route: 'example21', name: 'example21', view: './examples/example21.html', viewModel: Example21, title: 'Example21', }, { route: 'example22', name: 'example22', view: './examples/example22.html', viewModel: Example22, title: 'Example22', }, { route: 'example23', name: 'example23', view: './examples/example23.html', viewModel: Example23, title: 'Example23', }, + { route: 'example24', name: 'example24', view: './examples/example24.html', viewModel: Example24, title: 'Example24', }, { route: '', redirect: 'example01' }, { route: '**', redirect: 'example01' } ]; diff --git a/examples/vite-demo-vanilla-bundle/src/app.html b/examples/vite-demo-vanilla-bundle/src/app.html index 21b88503e..ab3a34936 100644 --- a/examples/vite-demo-vanilla-bundle/src/app.html +++ b/examples/vite-demo-vanilla-bundle/src/app.html @@ -105,6 +105,9 @@

Slickgrid-Universal

Example23 - Excel Export Formulas + + Example24 - Footer Totals Row + diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example20.ts b/examples/vite-demo-vanilla-bundle/src/examples/example20.ts index 12138c014..a1bcdb33f 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example20.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example20.ts @@ -16,7 +16,6 @@ export default class Example20 { dataset: any[] = []; gridOptions!: GridOption; gridContainerElm: HTMLDivElement; - isWithPagination = true; sgb: SlickVanillaGridBundle; attached() { @@ -35,7 +34,7 @@ export default class Example20 { // let's wrap the grid resize in a delay & show the grid only after the resize setTimeout(() => { this.sgb = new Slicker.GridBundle(shadowObj.gridContainer as HTMLDivElement, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); - this.sgb.resizerService.resizeGrid(); + this.sgb.resizerService.resizeGrid(150); shadowObj.gridContainer.style.opacity = '1'; }, 50); } @@ -124,13 +123,4 @@ export default class Example20 { return datasetTmp; } - - // Toggle the Grid Pagination - // IMPORTANT, the Pagination MUST BE CREATED on initial page load before you can start toggling it - // Basically you cannot toggle a Pagination that doesn't exist (must created at the time as the grid) - togglePagination() { - this.isWithPagination = !this.isWithPagination; - this.sgb.paginationService!.togglePaginationVisibility(this.isWithPagination); - this.sgb.slickGrid!.setSelectedRows([]); - } } diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example24.html b/examples/vite-demo-vanilla-bundle/src/examples/example24.html new file mode 100644 index 000000000..9047ae77f --- /dev/null +++ b/examples/vite-demo-vanilla-bundle/src/examples/example24.html @@ -0,0 +1,25 @@ +

+ Example 24 - Footer Totals Row + (with Salesforce Theme) + + + + + +

+ +
+ Display a totals row at the end of the grid. +
+ +
+
\ No newline at end of file diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example24.ts b/examples/vite-demo-vanilla-bundle/src/examples/example24.ts new file mode 100644 index 000000000..f40dc1841 --- /dev/null +++ b/examples/vite-demo-vanilla-bundle/src/examples/example24.ts @@ -0,0 +1,143 @@ +import { type Column, Editors, FieldType, type GridOption, SlickEventHandler, } from '@slickgrid-universal/common'; +import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; + +import { ExampleGridOptions } from './example-grid-options'; +import { BindingEventService } from '@slickgrid-universal/binding'; + +const NB_ITEMS = 100; + +export default class Example20 { + private _bindingEventService: BindingEventService; + private _darkMode = false; + private _eventHandler: SlickEventHandler; + + columnDefinitions: Column[] = []; + dataset: any[] = []; + gridOptions!: GridOption; + gridContainerElm: HTMLDivElement; + sgb: SlickVanillaGridBundle; + + constructor() { + this._bindingEventService = new BindingEventService(); + } + + attached() { + this._eventHandler = new SlickEventHandler(); + + // define the grid options & columns and then create the grid itself + this.defineGrid(); + + // mock some data (different in each dataset) + this.dataset = this.getData(NB_ITEMS); + this.gridContainerElm = document.querySelector('.grid24') as HTMLDivElement; + + this.sgb = new Slicker.GridBundle(document.querySelector('.grid24') as HTMLDivElement, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); + this.updateAllTotals(); + + // bind any of the grid events + this._bindingEventService.bind(this.gridContainerElm, 'oncolumnsreordered', this.handleOnColumnsReordered.bind(this)); + this._bindingEventService.bind(this.gridContainerElm, 'oncellchange', this.handleOnCellChange.bind(this)); + + document.body.classList.add('salesforce-theme'); + } + + dispose() { + this._eventHandler.unsubscribeAll(); + this.sgb?.dispose(); + this.gridContainerElm.remove(); + document.querySelector('.demo-container')?.classList.remove('dark-mode'); + document.body.setAttribute('data-theme', 'light'); + } + + /* Define grid Options and Columns */ + defineGrid() { + const columnDefs: Column[] = []; + for (let i = 0; i < 10; i++) { + columnDefs.push({ + id: i, + name: String.fromCharCode('A'.charCodeAt(0) + i), + field: String(i), + type: FieldType.number, + width: 58, + editor: { model: Editors.integer } + }); + } + this.columnDefinitions = columnDefs; + + this.gridOptions = { + autoEdit: true, + autoCommitEdit: true, + editable: true, + darkMode: this._darkMode, + gridHeight: 450, + gridWidth: 800, + enableCellNavigation: true, + rowHeight: 33, + createFooterRow: true, + showFooterRow: true, + footerRowHeight: 35 + }; + } + + getData(itemCount: number) { + // mock a dataset + const datasetTmp: any[] = []; + for (let i = 0; i < itemCount; i++) { + const d = (datasetTmp[i] = {} as any); + d.id = i; + for (let j = 0; j < this.columnDefinitions.length; j++) { + d[j] = Math.round(Math.random() * 10); + } + } + + return datasetTmp; + } + + handleOnCellChange(e) { + const args = e?.detail?.args; + this.updateTotal(args.cell); + } + + handleOnColumnsReordered() { + this.updateAllTotals(); + } + + toggleDarkMode() { + this._darkMode = !this._darkMode; + this.toggleBodyBackground(); + this.sgb.gridOptions = { ...this.sgb.gridOptions, darkMode: this._darkMode }; + this.sgb.slickGrid?.setOptions({ darkMode: this._darkMode }); + this.updateAllTotals(); + } + + toggleBodyBackground() { + if (this._darkMode) { + document.body.setAttribute('data-theme', 'dark'); + document.querySelector('.demo-container')?.classList.add('dark-mode'); + } else { + document.body.setAttribute('data-theme', 'light'); + document.querySelector('.demo-container')?.classList.remove('dark-mode'); + } + } + + updateAllTotals() { + let columnIdx = this.sgb.slickGrid?.getColumns().length || 0; + while (columnIdx--) { + this.updateTotal(columnIdx); + } + } + + updateTotal(cell: number) { + const columnId = this.sgb.slickGrid?.getColumns()[cell].id as number; + + let total = 0; + let i = this.dataset.length; + while (i--) { + total += (parseInt(this.dataset[i][columnId], 10) || 0); + } + const columnElement = this.sgb.slickGrid?.getFooterRowColumn(columnId); + if (columnElement) { + columnElement.textContent = `Sum: ${total}`; + } + } +} diff --git a/examples/vite-demo-vanilla-bundle/src/styles.scss b/examples/vite-demo-vanilla-bundle/src/styles.scss index 012ce46ac..752a4790e 100644 --- a/examples/vite-demo-vanilla-bundle/src/styles.scss +++ b/examples/vite-demo-vanilla-bundle/src/styles.scss @@ -128,4 +128,7 @@ input.is-narrow { } .justify-center { justify-content: center; +} +.italic { + font-style: italic; } \ No newline at end of file diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index 2fbd984de..f1c887939 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -881,6 +881,16 @@ $slick-viewport-border-right: 0 none !default; $slick-viewport-border-bottom: 0 none !default; $slick-viewport-border-left: 0 none !default; +/* SlickGrid built-in Footer */ +$slick-grid-footer-display: flex !default; +$slick-grid-footer-align-items: center !default; +$slick-grid-footer-justify-content: normal !default; +$slick-grid-footer-top-border-top: 1px solid #d0d0d0 !default; +$slick-grid-footer-bg-color: #fafaf9 !default; +$slick-grid-footer-font-size: 13px !default; +$slick-grid-footer-font-style: normal !default; +$slick-grid-footer-font-weight: bold !default; + /* Custom Footer */ $slick-footer-bg-color: transparent !default; $slick-footer-font-size: $slick-font-size-base !default; @@ -1012,6 +1022,7 @@ $slick-dark-text-color: #d4d4d4; --slick-grid-menu-icon-btn-color: #ededed; --slick-row-mouse-hover-color: #2c3034; --slick-header-background-color: #1c1c1c; + --slick-grid-footer-bg-color: #1c1c1c; --slick-header-text-color: #e4e4e4; --slick-hover-header-color: var(--slick-base-dark-text-color); --slick-header-row-background-color: #2d2d2d; diff --git a/packages/common/src/styles/slick-grid.scss b/packages/common/src/styles/slick-grid.scss index 592f24dc6..5bb301c10 100644 --- a/packages/common/src/styles/slick-grid.scss +++ b/packages/common/src/styles/slick-grid.scss @@ -582,6 +582,20 @@ } } + .slick-footerrow { + border-top: var(--slick-grid-footer-top-border-top, $slick-grid-footer-top-border-top); + background-color: var(--slick-grid-footer-bg-color, $slick-grid-footer-bg-color); + .slick-footerrow-columns { + .slick-footerrow-column { + display: var(--slick-grid-footer-display, $slick-grid-footer-display); + align-items: var(--slick-grid-footer-align-items, $slick-grid-footer-align-items); + justify-content: var(--slick-grid-footer-justify-content, $slick-grid-footer-justify-content); + font-size: var(--slick-grid-footer-font-size, $slick-grid-footer-font-size); + font-style: var(--slick-grid-footer-font-style, $slick-grid-footer-font-style); + font-weight: var(--slick-grid-footer-font-weight, $slick-grid-footer-font-weight); + } + } + } .slick-header-columns { background: var(--slick-grid-header-background, $slick-grid-header-background); background-color: var(--slick-header-background-color, $slick-header-background-color); diff --git a/test/cypress/e2e/example24.cy.ts b/test/cypress/e2e/example24.cy.ts new file mode 100644 index 000000000..7c4274a2c --- /dev/null +++ b/test/cypress/e2e/example24.cy.ts @@ -0,0 +1,58 @@ +describe('Example 24 - Footer Totals Row', () => { + const fullTitles = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']; + const GRID_ROW_HEIGHT = 33; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example24`); + cy.get('h3').should('contain', 'Example 24 - Footer Totals Row'); + cy.get('h3 span.subtitle').should('contain', '(with Salesforce Theme)'); + }); + + it('should have exact Column Header Titles in the grid', () => { + cy.get('.grid24') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should have a total sum displayed in the footer for each column', () => { + for (let i = 0; i < 10; i++) { + cy.get(`.slick-footerrow-columns .slick-footerrow-column:nth(${i})`) + .should($span => { + const totalStr = $span.text(); + const totalVal = Number(totalStr.replace('Sum: ', '')); + + expect(totalStr).to.contain('Sum:'); + expect(totalVal).to.gte(400); + }); + + } + }); + + it('should be able to increase cell value by a number of 5 and expect column sum to be increased by 5 as well', () => { + let cellVal = 0; + let totalVal = 0; + const increasingVal = 50; + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`) + .should($span => { + cellVal = Number($span.text()); + expect(cellVal).to.gte(0); + }); + cy.get('.slick-footerrow-columns .slick-footerrow-column:nth(0)') + .should($span => { + totalVal = parseInt($span.text().replace('Sum: ', '')); + expect(totalVal).to.gte(400); + }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).click(); + cy.get('.editor-0').type(`${increasingVal}{enter}`); + cy.wait(1); + + cy.get('.slick-footerrow-columns .slick-footerrow-column:nth(0)') + .should($span => { + const newTotalVal = parseInt($span.text().replace('Sum: ', '')); + expect(newTotalVal).to.eq(totalVal - cellVal + increasingVal); + }); + }); +});