From 8ba146cd4cbdccdb61f3441918065fad4561ff84 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Thu, 22 Apr 2021 13:37:15 -0400 Subject: [PATCH] feat(footer): add row selection count to the footer component --- .../assets/i18n/en.json | 1 + .../assets/i18n/fr.json | 1 + packages/common/src/constants.ts | 1 + packages/common/src/global-grid-options.ts | 5 +- .../customFooterOption.interface.ts | 22 ++---- packages/common/src/interfaces/index.ts | 1 + .../common/src/interfaces/locale.interface.ts | 3 + .../src/interfaces/metricTexts.interface.ts | 29 +++++++ .../components/__tests__/slick-footer.spec.ts | 76 +++++++++++++++++++ .../__tests__/slick-vanilla-grid.spec.ts | 4 + .../src/components/slick-footer.component.ts | 42 +++++++++- .../src/salesforce-global-grid-options.ts | 3 + test/translateServiceStub.ts | 4 +- 13 files changed, 174 insertions(+), 18 deletions(-) create mode 100644 packages/common/src/interfaces/metricTexts.interface.ts diff --git a/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json b/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json index 07b3384c3..26fcab885 100644 --- a/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json +++ b/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json @@ -34,6 +34,7 @@ "IN_COLLECTION_SEPERATED_BY_COMMA": "Search items in a collection, must be separated by a comma (a,b)", "ITEMS": "items", "ITEMS_PER_PAGE": "items per page", + "ITEMS_SELECTED": "items selected", "LAST_UPDATE": "Last Update", "LESS_THAN": "Less than", "LESS_THAN_OR_EQUAL_TO": "Less than or equal to", diff --git a/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json b/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json index ceacd51e8..08e37894c 100644 --- a/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json +++ b/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json @@ -34,6 +34,7 @@ "INVALID_FLOAT": "Le nombre doit être valide et avoir un maximum de {{maxDecimal}} décimales.", "ITEMS": "éléments", "ITEMS_PER_PAGE": "éléments par page", + "ITEMS_SELECTED": "éléments sélectionnés", "LAST_UPDATE": "Dernière mise à jour", "LESS_THAN": "Plus petit que", "LESS_THAN_OR_EQUAL_TO": "Plus petit ou égal à", diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 52f743081..3a5ac053a 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -40,6 +40,7 @@ export class Constants { TEXT_HIDE_COLUMN: 'Hide Column', TEXT_ITEMS: 'items', TEXT_ITEMS_PER_PAGE: 'items per page', + TEXT_ITEMS_SELECTED: 'items selected', TEXT_OF: 'of', TEXT_OK: 'OK', TEXT_LAST_UPDATE: 'Last Update', diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index 1b2ef1c91..166952561 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -73,6 +73,7 @@ export const GlobalGridOptions: GridOption = { }, customFooterOptions: { dateFormat: 'YYYY-MM-DD, hh:mm a', + hideRowSelectionCount: false, hideTotalItemCount: false, hideLastUpdateTimestamp: true, footerHeight: 25, @@ -81,9 +82,11 @@ export const GlobalGridOptions: GridOption = { metricSeparator: '|', metricTexts: { items: 'items', - of: 'of', itemsKey: 'ITEMS', + of: 'of', ofKey: 'OF', + itemsSelected: 'items selected', + itemsSelectedKey: 'ITEMS_SELECTED' } }, dataView: { diff --git a/packages/common/src/interfaces/customFooterOption.interface.ts b/packages/common/src/interfaces/customFooterOption.interface.ts index c9c6ffa0b..daf1e5d65 100644 --- a/packages/common/src/interfaces/customFooterOption.interface.ts +++ b/packages/common/src/interfaces/customFooterOption.interface.ts @@ -1,18 +1,4 @@ -export type MetricTexts = { - /** Defaults to empty string, optionally pass a text (Last Update) to display before the metrics endTime timestamp. */ - lastUpdate?: string; - /** Defaults to "items", word to display at the end of the metrics to represent the items (e.g. you could change it for "users" or anything else). */ - items?: string; - /** Defaults to "of", text word separator to display between the filtered items count and the total unfiltered items count (e.g.: "10 of 100 items"). */ - of?: string; - // -- Translation Keys --// - /** Defaults to "ITEMS", translation key used for the word displayed at the end of the metrics to represent the items (e.g. you could change it for "users" or anything else). */ - itemsKey?: string; - /** Defaults to empty string, optionally pass a translation key (internally we use "LAST_UPDATE") to display before the metrics endTime timestamp. */ - lastUpdateKey?: string; - /** Defaults to "OF", translation key used for the to display between the filtered items count and the total unfiltered items count. */ - ofKey?: string; -}; +import { MetricTexts } from './metricTexts.interface'; export interface CustomFooterOption { /** Optionally pass some text to be displayed on the left side (in the "left-footer" css class) */ @@ -27,6 +13,12 @@ export interface CustomFooterOption { /** Defaults to 25, height of the Custom Footer in pixels, it could be a number (25) or a string ("25px") but it has to be in pixels. It will be used by the auto-resizer calculations. */ footerHeight?: number | string; + /** + * Defaults to false, which will hide the selected rows count on the bottom left of the footer. + * NOTE: if users defined a `leftFooterText`, then the selected rows count will NOT show up. + */ + hideRowSelectionCount?: boolean; + /** Defaults to false, do we want to hide the last update timestamp (endTime)? */ hideLastUpdateTimestamp?: boolean; diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index f3dec471c..79b2349be 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -105,6 +105,7 @@ export * from './menuItem.interface'; export * from './menuOptionItem.interface'; export * from './menuOptionItemCallbackArgs.interface'; export * from './metrics.interface'; +export * from './metricTexts.interface'; export * from './multiColumnSort.interface'; export * from './multipleSelectOption.interface'; export * from './onEventArgs.interface'; diff --git a/packages/common/src/interfaces/locale.interface.ts b/packages/common/src/interfaces/locale.interface.ts index f27341e9a..6f811039a 100644 --- a/packages/common/src/interfaces/locale.interface.ts +++ b/packages/common/src/interfaces/locale.interface.ts @@ -113,6 +113,9 @@ export interface Locale { /** Text "items per page" displayed in the Pagination (when enabled) */ TEXT_ITEMS_PER_PAGE?: string; + /** Text "Records Selected" displayed in the Custom Footer */ + TEXT_ITEMS_SELECTED?: string; + /** Text "of" displayed in the Pagination (when enabled) */ TEXT_OF?: string; diff --git a/packages/common/src/interfaces/metricTexts.interface.ts b/packages/common/src/interfaces/metricTexts.interface.ts new file mode 100644 index 000000000..cebad5370 --- /dev/null +++ b/packages/common/src/interfaces/metricTexts.interface.ts @@ -0,0 +1,29 @@ +export type MetricTexts = { + /** Defaults to empty string, optionally pass a text (Last Update) to display before the metrics endTime timestamp. */ + lastUpdate?: string; + + /** Defaults to "items", word to display at the end of the metrics to represent the items (e.g. you could change it for "users" or anything else). */ + items?: string; + + /** Defaults to "of", text word separator to display between the filtered items count and the total unfiltered items count (e.g.: "10 of 100 items"). */ + of?: string; + + /** Defaults to "records selected", text word that is associated to the row selection count. */ + itemsSelected?: string; + + // -- + // Translation Keys + // ------------------ + + /** Defaults to "ITEMS", translation key used for the word displayed at the end of the metrics to represent the items (e.g. you could change it for "users" or anything else). */ + itemsKey?: string; + + /** Defaults to empty string, optionally pass a translation key (internally we use "LAST_UPDATE") to display before the metrics endTime timestamp. */ + lastUpdateKey?: string; + + /** Defaults to "OF", translation key used for the to display between the filtered items count and the total unfiltered items count. */ + ofKey?: string; + + /** Defaults to "ITEMS_SELECTED", text word that is associated to the row selection count. */ + itemsSelectedKey?: string; +}; \ No newline at end of file diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-footer.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-footer.spec.ts index 8a9bc9b3b..5120d17be 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-footer.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-footer.spec.ts @@ -6,6 +6,8 @@ function removeExtraSpaces(text: string) { return `${text}`.replace(/\s{2,}/g, ''); } +declare const Slick: any; + const mockGridOptions = { enableTranslate: false, showCustomFooter: true, @@ -14,6 +16,7 @@ const mockGridOptions = { const gridStub = { getOptions: () => mockGridOptions, getUID: () => 'slickgrid_123456', + onSelectedRowsChanged: new Slick.Event(), registerPlugin: jest.fn(), } as unknown as SlickGrid; @@ -203,5 +206,78 @@ describe('Slick-Footer Component', () => { 7 de 99 éléments `)); }); + + it('should display custom text on the left side footer section when calling the leftFooterText SETTER', () => { + mockGridOptions.enableCheckboxSelector = true; + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); + component.renderFooter(div); + component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; + component.leftFooterText = 'custom left footer text'; + + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; + + expect(component.eventHandler).toEqual(expect.toBeObject()); + expect(footerContainerElm).toBeTruthy(); + expect(leftFooterElm).toBeTruthy(); + expect(rightFooterElm).toBeTruthy(); + expect(leftFooterElm.innerHTML).toBe('custom left footer text'); + expect(rightFooterElm.innerHTML).toBe(removeExtraSpaces( + ``)); + }); + + it('should display 1 items selected on the left side footer section after triggering "onSelectedRowsChanged" event', () => { + mockGridOptions.enableCheckboxSelector = true; + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); + component.renderFooter(div); + component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; + gridStub.onSelectedRowsChanged.notify({ rows: [1], grid: gridStub, previousSelectedRows: [] }); + + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; + + expect(component.eventHandler).toEqual(expect.toBeObject()); + expect(footerContainerElm).toBeTruthy(); + expect(leftFooterElm).toBeTruthy(); + expect(rightFooterElm).toBeTruthy(); + expect(leftFooterElm.innerHTML).toBe('1 items selected'); + expect(rightFooterElm.innerHTML).toBe(removeExtraSpaces( + ``)); + + gridStub.onSelectedRowsChanged.notify({ rows: [1, 2, 3, 4, 5], grid: gridStub, previousSelectedRows: [] }); + expect(leftFooterElm.innerHTML).toBe('5 items selected'); + }); + + it('should not not display row selection count after triggering "onSelectedRowsChanged" event when "hideRowSelectionCount" is set to True', () => { + mockGridOptions.enableCheckboxSelector = true; + mockGridOptions.customFooterOptions.hideRowSelectionCount = true; + component = new SlickFooterComponent(gridStub, mockGridOptions.customFooterOptions as CustomFooterOption, translateService); + component.renderFooter(div); + component.metrics = { startTime: mockTimestamp, endTime: mockTimestamp, itemCount: 7, totalItemCount: 99 }; + gridStub.onSelectedRowsChanged.notify({ rows: [1], grid: gridStub, previousSelectedRows: [] }); + + const footerContainerElm = document.querySelector('div.slick-custom-footer.slickgrid_123456') as HTMLDivElement; + const leftFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.left-footer') as HTMLSpanElement; + const rightFooterElm = document.querySelector('div.slick-custom-footer.slickgrid_123456 > div.metrics') as HTMLSpanElement; + + expect(component.eventHandler).toEqual(expect.toBeObject()); + expect(footerContainerElm).toBeTruthy(); + expect(leftFooterElm).toBeTruthy(); + expect(rightFooterElm).toBeTruthy(); + expect(leftFooterElm.innerHTML).toBe(''); + expect(rightFooterElm.innerHTML).toBe(removeExtraSpaces( + ``)); + }); }); }); diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts index ea410db14..7c10c711d 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts @@ -1796,6 +1796,8 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () metricTexts: { items: 'éléments', itemsKey: 'ITEMS', + itemsSelected: 'éléments sélectionnés', + itemsSelectedKey: 'ITEMS_SELECTED', of: 'de', ofKey: 'OF', }, @@ -1833,6 +1835,8 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () metricTexts: { items: 'some items', itemsKey: 'ITEMS', + itemsSelected: 'items selected', + itemsSelectedKey: 'ITEMS_SELECTED', lastUpdate: 'some last update', of: 'some of', ofKey: 'OF', diff --git a/packages/vanilla-bundle/src/components/slick-footer.component.ts b/packages/vanilla-bundle/src/components/slick-footer.component.ts index 9adc1e5c4..54f1a064e 100644 --- a/packages/vanilla-bundle/src/components/slick-footer.component.ts +++ b/packages/vanilla-bundle/src/components/slick-footer.component.ts @@ -4,20 +4,29 @@ const moment = (moment_ as any)['default'] || moment_; // patch to fix rollup "m import { Constants, CustomFooterOption, + GetSlickEventType, GridOption, Locale, Metrics, MetricTexts, + sanitizeTextByAvailableSanitizer, + SlickEventHandler, SlickGrid, + SlickNamespace, TranslaterService, - sanitizeTextByAvailableSanitizer, } from '@slickgrid-universal/common'; import { BindingHelper } from '../services/binding.helper'; +declare const Slick: SlickNamespace; export class SlickFooterComponent { private _bindingHelper: BindingHelper; + private _eventHandler!: SlickEventHandler; private _footerElement!: HTMLDivElement; + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + get gridUid(): string { return this.grid?.getUID() ?? ''; } @@ -36,14 +45,21 @@ export class SlickFooterComponent { this.renderMetrics(metrics); } + set leftFooterText(text: string) { + this.renderLeftFooterText(text); + } + constructor(private grid: SlickGrid, private customFooterOptions: CustomFooterOption, private translaterService?: TranslaterService) { this._bindingHelper = new BindingHelper(); this._bindingHelper.querySelectorPrefix = `.${this.gridUid} `; + this._eventHandler = new Slick.EventHandler(); + this.registerOnSelectedRowsChangedWhenEnabled(customFooterOptions); } dispose() { this._bindingHelper.dispose(); this._footerElement?.remove(); + this._eventHandler.unsubscribeAll(); } /** @@ -57,6 +73,7 @@ export class SlickFooterComponent { this.customFooterOptions.metricTexts = this.customFooterOptions.metricTexts || {}; this.customFooterOptions.metricTexts.lastUpdate = this.customFooterOptions.metricTexts.lastUpdate || this.locales?.TEXT_LAST_UPDATE || 'TEXT_LAST_UPDATE'; this.customFooterOptions.metricTexts.items = this.customFooterOptions.metricTexts.items || this.locales?.TEXT_ITEMS || 'TEXT_ITEMS'; + this.customFooterOptions.metricTexts.itemsSelected = this.customFooterOptions.metricTexts.itemsSelected || this.locales?.TEXT_ITEMS_SELECTED || 'TEXT_ITEMS_SELECTED'; this.customFooterOptions.metricTexts.of = this.customFooterOptions.metricTexts.of || this.locales?.TEXT_OF || 'TEXT_OF'; } @@ -74,6 +91,11 @@ export class SlickFooterComponent { this._bindingHelper.setElementAttributeValue('span.total-count', 'textContent', metrics.totalItemCount); } + /** Render the left side footer text */ + renderLeftFooterText(text: string) { + this._bindingHelper.setElementAttributeValue('div.left-footer', 'textContent', text); + } + // -- // private functions // -------------------- @@ -172,6 +194,24 @@ export class SlickFooterComponent { return lastUpdateContainerElm; } + /** + * When user has row selections enabled and does not have any custom text shown on the left side footer, + * we will show the row selection count on the bottom left side of the footer (by subscribing to the SlickGrid `onSelectedRowsChanged` event). + * @param customFooterOptions + */ + private registerOnSelectedRowsChangedWhenEnabled(customFooterOptions: CustomFooterOption) { + const isRowSelectionEnabled = this.gridOptions.enableCheckboxSelector || this.gridOptions.enableRowSelection; + if (isRowSelectionEnabled && customFooterOptions && (!customFooterOptions.hideRowSelectionCount && !customFooterOptions.leftFooterText)) { + const selectedCountText = customFooterOptions.metricTexts?.itemsSelected ?? this.locales?.TEXT_ITEMS_SELECTED ?? 'TEXT_ITEMS_SELECTED'; + customFooterOptions.leftFooterText = `0 ${selectedCountText}`; + + const onSelectedRowsChangedHandler = this.grid.onSelectedRowsChanged; + (this._eventHandler as SlickEventHandler>).subscribe(onSelectedRowsChangedHandler, (_e, args) => { + this.leftFooterText = `${args.rows.length || 0} ${selectedCountText}`; + }); + } + } + /** Translate all Custom Footer Texts (footer with metrics) */ private translateCustomFooterTexts() { if (this.translaterService?.translate) { diff --git a/packages/vanilla-bundle/src/salesforce-global-grid-options.ts b/packages/vanilla-bundle/src/salesforce-global-grid-options.ts index b7d2067ca..c49c3995c 100644 --- a/packages/vanilla-bundle/src/salesforce-global-grid-options.ts +++ b/packages/vanilla-bundle/src/salesforce-global-grid-options.ts @@ -49,6 +49,9 @@ export const SalesforceGlobalGridOptions = { hideMetrics: false, hideTotalItemCount: false, hideLastUpdateTimestamp: true, + metricTexts: { + itemsSelected: 'records selected', + } }, headerRowHeight: 35, rowHeight: 33, diff --git a/test/translateServiceStub.ts b/test/translateServiceStub.ts index bea98afab..4c749852a 100644 --- a/test/translateServiceStub.ts +++ b/test/translateServiceStub.ts @@ -4,7 +4,8 @@ export class TranslateServiceStub implements TranslaterService { eventName = 'onLanguageChange' as TranslateServiceEventName; private _locale = 'en'; - addPubSubMessaging(pubSubService: PubSubService) { } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + addPubSubMessaging(_pubSubService: PubSubService) { } getCurrentLanguage(): string { return this._locale; @@ -56,6 +57,7 @@ export class TranslateServiceStub implements TranslaterService { case 'MALE': output = this._locale === 'en' ? 'Male' : 'Mâle'; break; case 'ITEMS': output = this._locale === 'en' ? 'items' : 'éléments'; break; case 'ITEMS_PER_PAGE': output = this._locale === 'en' ? 'items per page' : 'éléments par page'; break; + case 'ITEMS_SELECTED': output = this._locale === 'en' ? 'items selected' : 'éléments sélectionnés'; break; case 'NOT_CONTAINS': output = this._locale === 'en' ? 'Not contains' : 'Ne contient pas'; break; case 'NOT_EQUAL_TO': output = this._locale === 'en' ? 'Not equal to' : 'Non égal à'; break; case 'OF': output = this._locale === 'en' ? 'of' : 'de'; break;