diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts index 8fca68eab..615e5bdb4 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts @@ -228,6 +228,8 @@ export class Example5 { // checkboxSelector: { // hideInFilterHeaderRow: false, // hideInColumnTitleRow: true, + // onRowToggleStart: (e, args) => console.log('onBeforeRowToggle', args), + // onSelectAllToggleStart: () => this.sgb.treeDataService.toggleTreeDataCollapse(false, false), // }, enableTreeData: true, // you must enable this flag for the filtering & sorting to work as expected treeDataOptions: { diff --git a/packages/common/src/extensions/__tests__/slickCheckboxSelectColumn.spec.ts b/packages/common/src/extensions/__tests__/slickCheckboxSelectColumn.spec.ts index bf5c89dae..2616e068a 100644 --- a/packages/common/src/extensions/__tests__/slickCheckboxSelectColumn.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCheckboxSelectColumn.spec.ts @@ -1,3 +1,4 @@ +import 'jest-extended'; import { SlickCheckboxSelectColumn } from '../slickCheckboxSelectColumn'; import { Column, OnSelectedRowsChangedEventArgs, SlickGrid, SlickNamespace, } from '../../interfaces/index'; import { SlickRowSelectionModel } from '../../extensions/slickRowSelectionModel'; @@ -173,10 +174,12 @@ describe('SlickCheckboxSelectColumn Plugin', () => { .mockReturnValueOnce({ firstName: 'Jane', lastName: 'Doe', age: 28 }) .mockReturnValueOnce({ __group: true, __groupTotals: { age: { sum: 58 } } }); const setSelectedRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); + const onToggleEndMock = jest.fn(); + const onToggleStartMock = jest.fn(); plugin.selectedRowsLookup = { 1: false, 2: true }; plugin.init(gridStub); - plugin.setOptions({ hideInColumnTitleRow: false, hideInFilterHeaderRow: true, hideSelectAllCheckbox: false, }); + plugin.setOptions({ hideInColumnTitleRow: false, hideInFilterHeaderRow: true, hideSelectAllCheckbox: false, onSelectAllToggleStart: onToggleStartMock, onSelectAllToggleEnd: onToggleEndMock }); const checkboxElm = document.createElement('input'); checkboxElm.type = 'checkbox'; @@ -190,6 +193,9 @@ describe('SlickCheckboxSelectColumn Plugin', () => { expect(stopPropagationSpy).toHaveBeenCalled(); expect(stopImmediatePropagationSpy).toHaveBeenCalled(); expect(setSelectedRowSpy).toHaveBeenCalledWith([0, 1, 2], 'click.selectAll'); + expect(onToggleStartMock).toHaveBeenCalledWith(expect.anything(), { caller: 'click.selectAll', previousSelectedRows: undefined, }); + expect(onToggleEndMock).toHaveBeenCalledWith(expect.anything(), { caller: 'click.selectAll', previousSelectedRows: undefined, rows: [0, 2] }); + }); it('should create the plugin and call "setOptions" and expect options changed and hide both Select All toggle when setting "hideSelectAllCheckbox: false" and "hideInColumnTitleRow: true"', () => { @@ -336,7 +342,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { inputCheckboxElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true })); expect(inputCheckboxElm).toBeTruthy(); - expect(setSelectedRowSpy).toHaveBeenCalledWith([], 'click.selectAll'); + expect(setSelectedRowSpy).toHaveBeenCalledWith([], 'click.unselectAll'); }); it('should call the "create" method and expect plugin to be created with checkbox column to be created at position 0 when using default', () => { @@ -405,9 +411,48 @@ describe('SlickCheckboxSelectColumn Plugin', () => { }); it('should trigger "onClick" event and expect toggleRowSelection to be called', () => { - const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelection'); + const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelectionWithEvent'); + + plugin.init(gridStub); + const checkboxElm = document.createElement('input'); + checkboxElm.type = 'checkbox'; + const clickEvent = addJQueryEventPropagation(new Event('click'), '', '', checkboxElm); + const stopPropagationSpy = jest.spyOn(clickEvent, 'stopPropagation'); + const stopImmediatePropagationSpy = jest.spyOn(clickEvent, 'stopImmediatePropagation'); + gridStub.onClick.notify({ cell: 0, row: 2, grid: gridStub }, clickEvent); + + expect(plugin).toBeTruthy(); + expect(toggleRowSpy).toHaveBeenCalledWith(expect.anything(), 2); + expect(stopPropagationSpy).toHaveBeenCalled(); + expect(stopImmediatePropagationSpy).toHaveBeenCalled(); + }); + + it('should trigger "onClick" event and expect toggleRowSelection and "onRowToggleStart" be called when defined', () => { + const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelectionWithEvent'); + const onToggleStartMock = jest.fn(); + + plugin.init(gridStub); + plugin.setOptions({ onRowToggleStart: onToggleStartMock }); + const checkboxElm = document.createElement('input'); + checkboxElm.type = 'checkbox'; + const clickEvent = addJQueryEventPropagation(new Event('click'), '', '', checkboxElm); + const stopPropagationSpy = jest.spyOn(clickEvent, 'stopPropagation'); + const stopImmediatePropagationSpy = jest.spyOn(clickEvent, 'stopImmediatePropagation'); + gridStub.onClick.notify({ cell: 0, row: 2, grid: gridStub }, clickEvent); + + expect(plugin).toBeTruthy(); + expect(onToggleStartMock).toHaveBeenCalledWith(expect.anything(), { previousSelectedRows: [1, 2], row: 2, }); + expect(toggleRowSpy).toHaveBeenCalledWith(expect.anything(), 2); + expect(stopPropagationSpy).toHaveBeenCalled(); + expect(stopImmediatePropagationSpy).toHaveBeenCalled(); + }); + + it('should trigger "onClick" event and expect toggleRowSelection and "onRowToggleEnd" be called when defined', () => { + const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelectionWithEvent'); + const onToggleEndMock = jest.fn(); plugin.init(gridStub); + plugin.setOptions({ onRowToggleEnd: onToggleEndMock }); const checkboxElm = document.createElement('input'); checkboxElm.type = 'checkbox'; const clickEvent = addJQueryEventPropagation(new Event('click'), '', '', checkboxElm); @@ -416,13 +461,14 @@ describe('SlickCheckboxSelectColumn Plugin', () => { gridStub.onClick.notify({ cell: 0, row: 2, grid: gridStub }, clickEvent); expect(plugin).toBeTruthy(); - expect(toggleRowSpy).toHaveBeenCalledWith(2); + expect(onToggleEndMock).toHaveBeenCalledWith(expect.anything(), { previousSelectedRows: [1, 2], row: 2, }); + expect(toggleRowSpy).toHaveBeenCalledWith(expect.anything(), 2); expect(stopPropagationSpy).toHaveBeenCalled(); expect(stopImmediatePropagationSpy).toHaveBeenCalled(); }); it('should trigger "onClick" event and NOT expect toggleRowSelection to be called when editor "isActive" returns True and "commitCurrentEdit" returns False', () => { - const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelection'); + const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelectionWithEvent'); jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(true); jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(false); @@ -441,7 +487,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { }); it('should trigger "onKeyDown" event and expect toggleRowSelection to be called when editor "isActive" returns False', () => { - const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelection'); + const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelectionWithEvent'); jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(false); plugin.init(gridStub); @@ -453,13 +499,13 @@ describe('SlickCheckboxSelectColumn Plugin', () => { gridStub.onKeyDown.notify({ cell: 0, row: 2, grid: gridStub }, keyboardEvent); expect(plugin).toBeTruthy(); - expect(toggleRowSpy).toHaveBeenCalledWith(2); + expect(toggleRowSpy).toHaveBeenCalledWith(expect.anything(), 2); expect(preventDefaultSpy).toHaveBeenCalled(); expect(stopImmediatePropagationSpy).toHaveBeenCalled(); }); it('should trigger "onKeyDown" event and expect toggleRowSelection to be called when editor "commitCurrentEdit" returns True', () => { - const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelection'); + const toggleRowSpy = jest.spyOn(plugin, 'toggleRowSelectionWithEvent'); jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); plugin.init(gridStub); @@ -471,7 +517,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { gridStub.onKeyDown.notify({ cell: 0, row: 2, grid: gridStub }, keyboardEvent); expect(plugin).toBeTruthy(); - expect(toggleRowSpy).toHaveBeenCalledWith(2); + expect(toggleRowSpy).toHaveBeenCalledWith(expect.anything(), 2); expect(preventDefaultSpy).toHaveBeenCalled(); expect(stopImmediatePropagationSpy).toHaveBeenCalled(); }); diff --git a/packages/common/src/extensions/slickCheckboxSelectColumn.ts b/packages/common/src/extensions/slickCheckboxSelectColumn.ts index 544bf1db6..2a115bd9f 100644 --- a/packages/common/src/extensions/slickCheckboxSelectColumn.ts +++ b/packages/common/src/extensions/slickCheckboxSelectColumn.ts @@ -1,5 +1,5 @@ import { KeyCode } from '../enums/keyCode.enum'; -import { CheckboxSelectorOption, Column, GridOption, SelectableOverrideCallback, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; +import { CheckboxSelectorOption, Column, DOMMouseEvent, GridOption, SelectableOverrideCallback, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; import { SlickRowSelectionModel } from './slickRowSelectionModel'; import { createDomElement, emptyElement } from '../services/domUtilities'; import { BindingEventService } from '../services/bindingEvent.service'; @@ -200,15 +200,40 @@ export class SlickCheckboxSelectColumn { } } + /** + * Toggle a row selection by providing a row number + * @param {Number} row - grid row number to toggle + */ toggleRowSelection(row: number) { + this.toggleRowSelectionWithEvent(null, row); + } + + /** + * Toggle a row selection and also provide the event that triggered it + * @param {Object} event - event that triggered the row selection change + * @param {Number} row - grid row number to toggle + * @returns + */ + toggleRowSelectionWithEvent(event: Event | null, row: number) { const dataContext = this._grid.getDataItem(row); if (!this.checkSelectableOverride(row, dataContext, this._grid)) { return; } + // user can optionally execute a callback defined in its grid options prior to toggling the row + const previousSelectedRows = this._grid.getSelectedRows(); + if (this._addonOptions.onRowToggleStart) { + this._addonOptions.onRowToggleStart(event, { row, previousSelectedRows }); + } + const newSelectedRows = this._selectedRowsLookup[row] ? this._grid.getSelectedRows().filter((n) => n !== row) : this._grid.getSelectedRows().concat(row); this._grid.setSelectedRows(newSelectedRows, 'click.toggle'); this._grid.setActiveCell(row, this.getCheckboxColumnCellIndex()); + + // user can optionally execute a callback defined in its grid options after the row toggle is completed + if (this._addonOptions.onRowToggleEnd) { + this._addonOptions.onRowToggleEnd(event, { row, previousSelectedRows }); + } } @@ -241,7 +266,7 @@ export class SlickCheckboxSelectColumn { args.node.appendChild(spanElm); this._headerRowNode = args.node; - this._bindEventService.bind(spanElm, 'click', ((evnt: Event) => this.handleHeaderClick(evnt, args)) as EventListener); + this._bindEventService.bind(spanElm, 'click', ((e: DOMMouseEvent) => this.handleHeaderClick(e, args)) as EventListener); } }); } @@ -278,7 +303,7 @@ export class SlickCheckboxSelectColumn { return this._checkboxColumnCellIndex; } - protected handleClick(e: any, args: any) { + protected handleClick(e: DOMMouseEvent, args: { row: number; cell: number; grid: SlickGrid; }) { // clicking on a row select checkbox if (this._grid.getColumns()[args.cell].id === this._addonOptions.columnId && e.target.type === 'checkbox') { // if editing, try to commit @@ -288,13 +313,13 @@ export class SlickCheckboxSelectColumn { return; } - this.toggleRowSelection(args.row); + this.toggleRowSelectionWithEvent(e, args.row); e.stopPropagation(); e.stopImmediatePropagation(); } } - protected handleHeaderClick(e: any, args: any) { + protected handleHeaderClick(e: DOMMouseEvent, args: { column: Column; node: HTMLDivElement; grid: SlickGrid; }) { if (args.column.id === this._addonOptions.columnId && e.target.type === 'checkbox') { // if editing, try to commit if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) { @@ -303,7 +328,20 @@ export class SlickCheckboxSelectColumn { return; } - if (e.target.checked) { + // who called the selection? + const isExecutingSelectAll = e.target.checked; + const caller = isExecutingSelectAll ? 'click.selectAll' : 'click.unselectAll'; + + // trigger event before the real selection so that we have an event before & the next one after the change + const previousSelectedRows = this._grid.getSelectedRows(); + + // user can optionally execute a callback defined in its grid options prior to the Select All toggling + if (this._addonOptions.onSelectAllToggleStart) { + this._addonOptions.onSelectAllToggleStart(e, { previousSelectedRows, caller }); + } + + let newSelectedRows: number[] = []; // when unselecting all, the array will become empty + if (isExecutingSelectAll) { const rows = []; for (let i = 0; i < this._grid.getDataLength(); i++) { // Get the row and check it's a selectable row before pushing it onto the stack @@ -312,10 +350,17 @@ export class SlickCheckboxSelectColumn { rows.push(i); } } - this._grid.setSelectedRows(rows, 'click.selectAll'); - } else { - this._grid.setSelectedRows([], 'click.selectAll'); + newSelectedRows = rows; } + + // we finally need to call the actual row selection from SlickGrid method + this._grid.setSelectedRows(newSelectedRows, caller); + + // user can optionally execute a callback defined in its grid options after the Select All toggling is completed + if (this._addonOptions.onSelectAllToggleEnd) { + this._addonOptions.onSelectAllToggleEnd(e, { rows: newSelectedRows, previousSelectedRows, caller }); + } + e.stopPropagation(); e.stopImmediatePropagation(); } @@ -326,7 +371,7 @@ export class SlickCheckboxSelectColumn { if (this._grid.getColumns()[args.cell].id === this._addonOptions.columnId) { // if editing, try to commit if (!this._grid.getEditorLock().isActive() || this._grid.getEditorLock().commitCurrentEdit()) { - this.toggleRowSelection(args.row); + this.toggleRowSelectionWithEvent(e, args.row); } e.preventDefault(); e.stopImmediatePropagation(); diff --git a/packages/common/src/interfaces/checkboxSelectorOption.interface.ts b/packages/common/src/interfaces/checkboxSelectorOption.interface.ts index 6a5c40038..e727df404 100644 --- a/packages/common/src/interfaces/checkboxSelectorOption.interface.ts +++ b/packages/common/src/interfaces/checkboxSelectorOption.interface.ts @@ -34,4 +34,19 @@ export interface CheckboxSelectorOption { /** Override the logic for showing (or not) the expand icon (use case example: only every 2nd row is expandable) */ selectableOverride?: UsabilityOverrideFn; + + /** Optional callback method to be executed when the row checkbox gets clicked but prior to the actual toggling itself. */ + onRowToggleStart?: (e: Event | null, args: { row: number; previousSelectedRows: number[]; }) => void; + + /** Optional callback method to be executed after the row checkbox toggle is completed. */ + onRowToggleEnd?: (e: Event | null, args: { row: number; previousSelectedRows: number[]; }) => void; + + /** + * Optional callback method to be executed when the "Select All" gets clicked but prior to the actual toggling itself. + * For example we could expand all Groups or Tree prior to the selection so that we also have the chance to even include Group/Tree children in the selection. + */ + onSelectAllToggleStart?: (e: Event | null, args: { previousSelectedRows: number[]; caller: 'click.selectAll' | 'click.unselectAll'; }) => void; + + /** Optional callback method to be executed when the "Select All" toggled action is completed. */ + onSelectAllToggleEnd?: (e: Event | null, args: { rows: number[]; previousSelectedRows: number[]; caller: 'click.selectAll' | 'click.unselectAll'; }) => void; } diff --git a/packages/salesforce-vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip b/packages/salesforce-vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip index 511b3eb88..e740aa378 100644 Binary files a/packages/salesforce-vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip and b/packages/salesforce-vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip differ