Skip to content

Commit

Permalink
feat(footer): add row selection count to the footer component
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Apr 22, 2021
1 parent 7e4c417 commit 8ba146c
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 18 deletions.
1 change: 1 addition & 0 deletions examples/webpack-demo-vanilla-bundle/assets/i18n/en.json
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json
Expand Up @@ -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 à",
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/constants.ts
Expand Up @@ -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',
Expand Down
5 changes: 4 additions & 1 deletion packages/common/src/global-grid-options.ts
Expand Up @@ -73,6 +73,7 @@ export const GlobalGridOptions: GridOption = {
},
customFooterOptions: {
dateFormat: 'YYYY-MM-DD, hh:mm a',
hideRowSelectionCount: false,
hideTotalItemCount: false,
hideLastUpdateTimestamp: true,
footerHeight: 25,
Expand All @@ -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: {
Expand Down
22 changes: 7 additions & 15 deletions 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) */
Expand All @@ -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;

Expand Down
1 change: 1 addition & 0 deletions packages/common/src/interfaces/index.ts
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/interfaces/locale.interface.ts
Expand Up @@ -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;

Expand Down
29 changes: 29 additions & 0 deletions 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;
};
Expand Up @@ -6,6 +6,8 @@ function removeExtraSpaces(text: string) {
return `${text}`.replace(/\s{2,}/g, '');
}

declare const Slick: any;

const mockGridOptions = {
enableTranslate: false,
showCustomFooter: true,
Expand All @@ -14,6 +16,7 @@ const mockGridOptions = {
const gridStub = {
getOptions: () => mockGridOptions,
getUID: () => 'slickgrid_123456',
onSelectedRowsChanged: new Slick.Event(),
registerPlugin: jest.fn(),
} as unknown as SlickGrid;

Expand Down Expand Up @@ -203,5 +206,78 @@ describe('Slick-Footer Component', () => {
<span class="item-count">7</span><span> de </span><span class="total-count">99</span><span> éléments </span>
</div>`));
});

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(
`<div class="right-footer metrics ">
<span class="timestamp"><span><span class="last-update">some last update 2019-05-03, 12:00:01am</span><span class="separator"> | </span></span></span>
<span class="item-count">7</span><span> some of </span><span class="total-count">99</span><span> some items </span>
</div>`));
});

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(
`<div class="right-footer metrics ">
<span class="timestamp"><span><span class="last-update">some last update 2019-05-03, 12:00:01am</span><span class="separator"> | </span></span></span>
<span class="item-count">7</span><span> some of </span><span class="total-count">99</span><span> some items </span>
</div>`));

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(
`<div class="right-footer metrics ">
<span class="timestamp"><span><span class="last-update">some last update 2019-05-03, 12:00:01am</span><span class="separator"> | </span></span></span>
<span class="item-count">7</span><span> some of </span><span class="total-count">99</span><span> some items </span>
</div>`));
});
});
});
Expand Up @@ -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',
},
Expand Down Expand Up @@ -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',
Expand Down
42 changes: 41 additions & 1 deletion packages/vanilla-bundle/src/components/slick-footer.component.ts
Expand Up @@ -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() ?? '';
}
Expand All @@ -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();
}

/**
Expand All @@ -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';
}

Expand All @@ -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
// --------------------
Expand Down Expand Up @@ -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<GetSlickEventType<typeof onSelectedRowsChangedHandler>>).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) {
Expand Down
3 changes: 3 additions & 0 deletions packages/vanilla-bundle/src/salesforce-global-grid-options.ts
Expand Up @@ -49,6 +49,9 @@ export const SalesforceGlobalGridOptions = {
hideMetrics: false,
hideTotalItemCount: false,
hideLastUpdateTimestamp: true,
metricTexts: {
itemsSelected: 'records selected',
}
},
headerRowHeight: 35,
rowHeight: 33,
Expand Down
4 changes: 3 additions & 1 deletion test/translateServiceStub.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 8ba146c

Please sign in to comment.