diff --git a/examples/vite-demo-vanilla-bundle/src/app-routing.ts b/examples/vite-demo-vanilla-bundle/src/app-routing.ts
index 29c583c70..8cb41d816 100644
--- a/examples/vite-demo-vanilla-bundle/src/app-routing.ts
+++ b/examples/vite-demo-vanilla-bundle/src/app-routing.ts
@@ -19,6 +19,7 @@ import Example15 from './examples/example15';
import Example16 from './examples/example16';
import Example17 from './examples/example17';
import Example18 from './examples/example18';
+import Example19 from './examples/example19';
export class AppRouting {
constructor(private config: RouterConfig) {
@@ -43,6 +44,7 @@ export class AppRouting {
{ route: 'example16', name: 'example16', view: './examples/example16.html', viewModel: Example16, title: 'Example16', },
{ route: 'example17', name: 'example17', view: './examples/example17.html', viewModel: Example17, title: 'Example17', },
{ route: 'example18', name: 'example18', view: './examples/example18.html', viewModel: Example18, title: 'Example18', },
+ { route: 'example19', name: 'example19', view: './examples/example19.html', viewModel: Example19, title: 'Example19', },
{ 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 994850756..ec4ba32ad 100644
--- a/examples/vite-demo-vanilla-bundle/src/app.html
+++ b/examples/vite-demo-vanilla-bundle/src/app.html
@@ -89,6 +89,9 @@
Slickgrid-Universal
Example18 - Real-Time Trading Platform
+
+ Example19 - ExcelCopyBuffer with Cell Selection
+
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example19.html b/examples/vite-demo-vanilla-bundle/src/examples/example19.html
new file mode 100644
index 000000000..25559ac56
--- /dev/null
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example19.html
@@ -0,0 +1,27 @@
+
+ Example 19 - ExcelCopyBuffer with Cell Selection
+ (with Salesforce Theme)
+
+
+
+
+ Grid - using enableExcelCopyBuffer
which uses SlickCellSelectionModel
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example19.scss b/examples/vite-demo-vanilla-bundle/src/examples/example19.scss
new file mode 100644
index 000000000..4d6b5cc83
--- /dev/null
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example19.scss
@@ -0,0 +1,10 @@
+/** override slick-cell to make it look like Excel sheet */
+.grid19 {
+ --slick-border-color: #d4d4d4;
+ --slick-cell-odd-background-color: #fbfbfb;
+ --slick-cell-border-left: 1px solid var(--slick-border-color);
+ --slick-header-menu-display: none;
+ --slick-header-column-height: 20px;
+ --slick-grid-border-color: #d4d4d4;
+ --slick-row-selected-color: #d4ebfd;
+}
\ No newline at end of file
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example19.ts b/examples/vite-demo-vanilla-bundle/src/examples/example19.ts
new file mode 100644
index 000000000..6c4a4b18f
--- /dev/null
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example19.ts
@@ -0,0 +1,132 @@
+import { CellRange, Column, GridOption, SlickEventHandler, SlickNamespace, } from '@slickgrid-universal/common';
+import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
+import { ExampleGridOptions } from './example-grid-options';
+import '../salesforce-styles.scss';
+import './example19.scss';
+
+const NB_ITEMS = 100;
+declare const Slick: SlickNamespace;
+export default class Example34 {
+ protected _eventHandler: SlickEventHandler;
+ title = 'Example 19: ExcelCopyBuffer with Cell Selection';
+ subTitle = `Cell Selection using "Shift+{key}" where "key" can be any of:
+
+ - Arrow Up/Down/Left/Right
+ - Page Up/Down
+ - Home
+ - End
+
`;
+
+ columnDefinitions: Column[] = [];
+ dataset: any[] = [];
+ gridOptions!: GridOption;
+ gridContainerElm: HTMLDivElement;
+ isWithPagination = true;
+ sgb: SlickVanillaGridBundle;
+
+ attached() {
+ this._eventHandler = new Slick.EventHandler();
+
+ // 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(`.grid19`) as HTMLDivElement;
+ this.sgb = new Slicker.GridBundle(document.querySelector(`.grid19`) as HTMLDivElement, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset);
+ document.body.classList.add('salesforce-theme');
+
+ // bind any of the grid events
+ const cellSelectionModel = this.sgb.slickGrid!.getSelectionModel();
+ this._eventHandler.subscribe(cellSelectionModel!.onSelectedRangesChanged, (_e, args: CellRange[]) => {
+ const targetRange = document.querySelector('#selectionRange') as HTMLSpanElement;
+ targetRange.textContent = '';
+
+ for (const slickRange of args) {
+ targetRange.textContent += JSON.stringify(slickRange);
+ }
+ });
+ }
+
+ dispose() {
+ this._eventHandler.unsubscribeAll();
+ this.sgb?.dispose();
+ this.gridContainerElm.remove();
+ document.body.classList.remove('salesforce-theme');
+ }
+
+ /* Define grid Options and Columns */
+ defineGrid() {
+ this.columnDefinitions = [
+ {
+ id: 'selector',
+ name: '',
+ field: 'num',
+ width: 30
+ }
+ ];
+
+ for (let i = 0; i < NB_ITEMS; i++) {
+ this.columnDefinitions.push({
+ id: i,
+ name: i < 26
+ ? String.fromCharCode('A'.charCodeAt(0) + (i % 26))
+ : String.fromCharCode('A'.charCodeAt(0) + ((i / 26) | 0) -1) + String.fromCharCode('A'.charCodeAt(0) + (i % 26)),
+ field: i as any,
+ minWidth: 60,
+ width: 60,
+ });
+ }
+
+ this.gridOptions = {
+ autoResize: {
+ container: '.demo-container',
+ },
+ enableCellNavigation: true,
+ enablePagination: true,
+ pagination: {
+ pageSizes: [5, 10, 15, 20, 25, 50, 75, 100],
+ pageSize: 20
+ },
+ headerRowHeight: 35,
+ rowHeight: 30,
+
+ // when using the ExcelCopyBuffer, you can see what the selection range is
+ enableExcelCopyBuffer: true,
+ // excelCopyBufferOptions: {
+ // onCopyCells: (e, args: { ranges: SelectedRange[] }) => console.log('onCopyCells', args.ranges),
+ // onPasteCells: (e, args: { ranges: SelectedRange[] }) => console.log('onPasteCells', args.ranges),
+ // onCopyCancelled: (e, args: { ranges: SelectedRange[] }) => console.log('onCopyCancelled', args.ranges),
+ // }
+ };
+ }
+
+ getData(itemCount: number) {
+ // mock a dataset
+ const datasetTmp: any[] = [];
+ for (let i = 0; i < itemCount; i++) {
+ const d: any = (datasetTmp[i] = {});
+ d['id'] = i;
+ d['num'] = i;
+ }
+
+ return datasetTmp;
+ }
+
+ generatePhoneNumber(): string {
+ let phone = '';
+ for (let i = 0; i < 10; i++) {
+ phone += Math.round(Math.random() * 9) + '';
+ }
+ return phone;
+ }
+
+ // 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/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts b/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts
index 78379135d..1d692a59a 100644
--- a/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts
+++ b/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts
@@ -6,6 +6,8 @@ import { SlickCellSelectionModel } from '../slickCellSelectionModel';
declare const Slick: SlickNamespace;
const GRID_UID = 'slickgrid_12345';
+const NB_ITEMS = 200;
+const CALCULATED_PAGE_ROW_COUNT = 23; // pageRowCount with our mocked sizes is 23 => ((600 - 17) / 25)
jest.mock('flatpickr', () => { });
const addVanillaEventPropagation = function (event, commandKey = '', keyName = '') {
@@ -23,6 +25,7 @@ const addVanillaEventPropagation = function (event, commandKey = '', keyName = '
const mockGridOptions = {
frozenColumn: 1,
frozenRow: -1,
+ rowHeight: 25
} as GridOption;
const getEditorLockMock = {
@@ -30,6 +33,11 @@ const getEditorLockMock = {
isActive: jest.fn(),
};
+const dataViewStub = {
+ getLength: () => NB_ITEMS,
+ getPagingInfo: () => ({ pageSize: 0 }),
+};
+
const gridStub = {
canCellBeSelected: jest.fn(),
getActiveCell: jest.fn(),
@@ -38,9 +46,13 @@ const gridStub = {
getCellFromEvent: jest.fn(),
getCellFromPoint: jest.fn(),
getCellNodeBox: jest.fn(),
+ getData: () => dataViewStub,
+ getDataLength: jest.fn(),
getEditorLock: () => getEditorLockMock,
getOptions: () => mockGridOptions,
getUID: () => GRID_UID,
+ getScrollbarDimensions: () => ({ height: 17, width: 17}),
+ getViewportNode: jest.fn(),
focus: jest.fn(),
registerPlugin: jest.fn(),
setActiveCell: jest.fn(),
@@ -81,6 +93,8 @@ describe('CellSelectionModel Plugin', () => {
beforeEach(() => {
plugin = new SlickCellSelectionModel();
+ jest.spyOn(gridStub, 'getViewportNode').mockReturnValue(viewportElm);
+ Object.defineProperty(viewportElm, 'clientHeight', { writable: true, configurable: true, value: 600 });
});
afterEach(() => {
@@ -109,7 +123,6 @@ describe('CellSelectionModel Plugin', () => {
plugin.init(gridStub);
expect(plugin.cellRangeSelector).toBeTruthy();
- expect(plugin.canvas).toBeTruthy();
expect(plugin.addonOptions).toEqual({ selectActiveCell: true });
expect(registerSpy).toHaveBeenCalledWith(plugin.cellRangeSelector);
});
@@ -121,7 +134,6 @@ describe('CellSelectionModel Plugin', () => {
plugin.init(gridStub);
expect(plugin.cellRangeSelector).toBeTruthy();
- expect(plugin.canvas).toBeTruthy();
expect(plugin.addonOptions).toEqual({ selectActiveCell: false });
expect(registerSpy).toHaveBeenCalledWith(plugin.cellRangeSelector);
});
@@ -134,7 +146,6 @@ describe('CellSelectionModel Plugin', () => {
plugin.init(gridStub);
expect(plugin.cellRangeSelector).toBeTruthy();
- expect(plugin.canvas).toBeTruthy();
expect(plugin.addonOptions).toEqual({ selectActiveCell: true, cellRangeSelector: mockCellRangeSelector });
expect(registerSpy).toHaveBeenCalledWith(plugin.cellRangeSelector);
});
@@ -153,7 +164,6 @@ describe('CellSelectionModel Plugin', () => {
plugin.refreshSelections();
expect(plugin.cellRangeSelector).toBeTruthy();
- expect(plugin.canvas).toBeTruthy();
expect(registerSpy).toHaveBeenCalledWith(plugin.cellRangeSelector);
expect(setSelectedRangesSpy).toHaveBeenCalledWith([
{ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 },
@@ -229,6 +239,9 @@ describe('CellSelectionModel Plugin', () => {
});
it('should call "setSelectedRanges" with Slick Range with a Right direction when triggered by "onKeyDown" with key combo of Shift+ArrowRight', () => {
+ // let's test this one without a DataView (aka SlickGrid only)
+ jest.spyOn(gridStub, 'getData').mockReturnValueOnce([]);
+ jest.spyOn(gridStub, 'getDataLength').mockReturnValueOnce(NB_ITEMS);
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 });
plugin.init(gridStub);
@@ -246,7 +259,7 @@ describe('CellSelectionModel Plugin', () => {
}]);
});
- it('should call "setSelectedRanges" with Slick Range with a Right direction when triggered by "onKeyDown" with key combo of Shift+ArrowUp', () => {
+ it('should call "setSelectedRanges" with Slick Range with an Up direction when triggered by "onKeyDown" with key combo of Shift+ArrowUp', () => {
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 });
plugin.init(gridStub);
@@ -264,7 +277,7 @@ describe('CellSelectionModel Plugin', () => {
}]);
});
- it('should call "setSelectedRanges" with Slick Range with a Right direction when triggered by "onKeyDown" with key combo of Shift+ArrowDown', () => {
+ it('should call "setSelectedRanges" with Slick Range with a Down direction when triggered by "onKeyDown" with key combo of Shift+ArrowDown', () => {
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 });
plugin.init(gridStub);
@@ -311,6 +324,168 @@ describe('CellSelectionModel Plugin', () => {
expect(onSelectedRangeSpy).toHaveBeenCalledWith(expectedRangeCalled, expect.objectContaining({ detail: { caller: 'SlickCellSelectionModel.setSelectedRanges' } }));
});
+ it('should call "setSelectedRanges" with Slick Range from current position to a calculated size of a page down when using Shift+PageDown key combo when triggered by "onKeyDown"', () => {
+ const notifyingRowNumber = 3;
+ jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
+ jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
+ const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');
+
+ plugin.init(gridStub);
+ plugin.resetPageRowCount();
+ plugin.setSelectedRanges([
+ { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: () => false },
+ { fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 4, contains: () => false }
+ ] as unknown as SlickRange[]);
+ const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
+ const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'PageDown');
+ gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);
+
+ const expectedRangeCalled = [
+ { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: expect.toBeFunction(), } as unknown as SlickRange,
+ {
+ fromCell: 2, fromRow: 3, toCell: 2, toRow: (notifyingRowNumber + CALCULATED_PAGE_ROW_COUNT),
+ contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
+ },
+ ];
+ expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
+ expect(scrollCellSpy).toHaveBeenCalledWith((notifyingRowNumber + CALCULATED_PAGE_ROW_COUNT), 2, false);
+ });
+
+ it('should call "setSelectedRanges" with Slick Range from current position to the last row index when using Shift+PageDown key combo but there is less rows than an actual page left to display', () => {
+ const notifyingRowNumber = NB_ITEMS - 10; // will be less than a page size (row count)
+ const lastRowIndex = NB_ITEMS - 1;
+ jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
+ jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
+ const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');
+
+ plugin.init(gridStub);
+ plugin.setSelectedRanges([
+ { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: () => false },
+ { fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 4, contains: () => false }
+ ] as unknown as SlickRange[]);
+ const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
+ const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'PageDown');
+ gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);
+
+ const expectedRangeCalled = [
+ { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: expect.toBeFunction(), } as unknown as SlickRange,
+ {
+ fromCell: 2, fromRow: notifyingRowNumber, toCell: 2, toRow: lastRowIndex,
+ contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
+ },
+ ];
+ expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
+ expect(scrollCellSpy).toHaveBeenCalledWith(lastRowIndex, 2, false);
+ });
+
+ it('should call "setSelectedRanges" with Slick Range from current position to a calculated size of a page up when using Shift+PageUp key combo when triggered by "onKeyDown"', () => {
+ const notifyingRowNumber = 100;
+ const CALCULATED_PAGE_ROW_COUNT = 23;
+ jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
+ jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
+ const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');
+
+ plugin.init(gridStub);
+ plugin.setSelectedRanges([
+ { fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: () => false },
+ { fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 120, contains: () => false }
+ ] as unknown as SlickRange[]);
+ const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
+ const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'PageUp');
+ gridStub.onKeyDown.notify({ cell: 2, row: 101, grid: gridStub }, keyDownEvent, gridStub);
+
+ const expectedRangeCalled = [
+ { fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: expect.toBeFunction(), } as unknown as SlickRange,
+ {
+ fromCell: 2, fromRow: (notifyingRowNumber - CALCULATED_PAGE_ROW_COUNT), toCell: 2, toRow: 100,
+ contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
+ },
+ ];
+ expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
+ expect(scrollCellSpy).toHaveBeenCalledWith((notifyingRowNumber - CALCULATED_PAGE_ROW_COUNT), 2, false);
+ });
+
+ it('should call "setSelectedRanges" with Slick Range from current position to the first row index when using Shift+PageUp key combo but there is less rows than an actual page left to display', () => {
+ const notifyingRowNumber = 10; // will be less than a page size (row count)
+ const firstRowIndex = 0;
+ jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
+ jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
+ const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');
+
+ plugin.init(gridStub);
+ plugin.setSelectedRanges([
+ { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: () => false },
+ { fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 4, contains: () => false }
+ ] as unknown as SlickRange[]);
+ const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
+ const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'PageUp');
+ gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);
+
+ const expectedRangeCalled = [
+ { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: expect.toBeFunction(), } as unknown as SlickRange,
+ {
+ fromCell: 2, fromRow: firstRowIndex, toCell: 2, toRow: notifyingRowNumber,
+ contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
+ },
+ ];
+ expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
+ expect(scrollCellSpy).toHaveBeenCalledWith(firstRowIndex, 2, false);
+ });
+
+ it('should call "setSelectedRanges" with Slick Range from current position to row index 0 when using Shift+Home key combo when triggered by "onKeyDown"', () => {
+ const notifyingRowNumber = 100;
+ const expectedRowZeroIdx = 0;
+ jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
+ jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
+ const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');
+
+ plugin.init(gridStub);
+ plugin.setSelectedRanges([
+ { fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: () => false },
+ { fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 120, contains: () => false }
+ ] as unknown as SlickRange[]);
+ const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
+ const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'Home');
+ gridStub.onKeyDown.notify({ cell: 2, row: 101, grid: gridStub }, keyDownEvent, gridStub);
+
+ const expectedRangeCalled = [
+ { fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: expect.toBeFunction(), } as unknown as SlickRange,
+ {
+ fromCell: 2, fromRow: expectedRowZeroIdx, toCell: 2, toRow: 100,
+ contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
+ },
+ ];
+ expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
+ expect(scrollCellSpy).toHaveBeenCalledWith(expectedRowZeroIdx, 2, false);
+ });
+
+ it('should call "setSelectedRanges" with Slick Range from current position to last row index when using Shift+End key combo when triggered by "onKeyDown"', () => {
+ const notifyingRowNumber = 100;
+ const expectedLastRowIdx = NB_ITEMS - 1;
+ jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
+ jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
+ const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');
+
+ plugin.init(gridStub);
+ plugin.setSelectedRanges([
+ { fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: () => false },
+ { fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 120, contains: () => false }
+ ] as unknown as SlickRange[]);
+ const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
+ const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'End');
+ gridStub.onKeyDown.notify({ cell: 2, row: 101, grid: gridStub }, keyDownEvent, gridStub);
+
+ const expectedRangeCalled = [
+ { fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: expect.toBeFunction(), } as unknown as SlickRange,
+ {
+ fromCell: 2, fromRow: notifyingRowNumber, toCell: 2, toRow: expectedLastRowIdx,
+ contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
+ },
+ ];
+ expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
+ expect(scrollCellSpy).toHaveBeenCalledWith(expectedLastRowIdx, 2, false);
+ });
+
it('should call "rangesAreEqual" and expect True when both ranges are equal', () => {
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 });
diff --git a/packages/common/src/extensions/slickCellSelectionModel.ts b/packages/common/src/extensions/slickCellSelectionModel.ts
index 53ee481ad..96ef8e41d 100644
--- a/packages/common/src/extensions/slickCellSelectionModel.ts
+++ b/packages/common/src/extensions/slickCellSelectionModel.ts
@@ -1,5 +1,4 @@
-import { KeyCode } from '../enums/index';
-import type { CellRange, OnActiveCellChangedEventArgs, SlickEventHandler, SlickGrid, SlickNamespace, SlickRange, } from '../interfaces/index';
+import type { CellRange, OnActiveCellChangedEventArgs, SlickDataView, SlickEventHandler, SlickGrid, SlickNamespace, SlickRange } from '../interfaces/index';
import { SlickCellRangeSelector } from './index';
// using external SlickGrid JS libraries
@@ -12,9 +11,12 @@ export interface CellSelectionModelOption {
export class SlickCellSelectionModel {
protected _addonOptions?: CellSelectionModelOption;
- protected _canvas: HTMLElement | null = null;
+ protected _cachedPageRowCount = 0;
protected _eventHandler: SlickEventHandler;
+ protected _dataView?: SlickDataView;
protected _grid!: SlickGrid;
+ protected _prevSelectedRow?: number;
+ protected _prevKeyDown = '';
protected _ranges: CellRange[] = [];
protected _selector: SlickCellRangeSelector;
protected _defaults = {
@@ -37,10 +39,6 @@ export class SlickCellSelectionModel {
return this._addonOptions;
}
- get canvas() {
- return this._canvas;
- }
-
get cellRangeSelector() {
return this._selector;
}
@@ -52,6 +50,9 @@ export class SlickCellSelectionModel {
init(grid: SlickGrid) {
this._grid = grid;
+ if (this.hasDataView()) {
+ this._dataView = grid?.getData() ?? {} as SlickDataView;
+ }
this._addonOptions = { ...this._defaults, ...this._addonOptions } as CellSelectionModelOption;
this._eventHandler
.subscribe(this._grid.onActiveCellChanged, this.handleActiveCellChange.bind(this) as EventListener)
@@ -61,7 +62,6 @@ export class SlickCellSelectionModel {
// register the cell range selector plugin
grid.registerPlugin(this._selector);
- this._canvas = this._grid.getCanvasNode();
}
destroy() {
@@ -69,7 +69,6 @@ export class SlickCellSelectionModel {
}
dispose() {
- this._canvas = null;
if (this._selector) {
this._selector.onBeforeCellRangeSelected.unsubscribe(this.handleBeforeCellRangeSelected.bind(this) as EventListener);
this._selector.onCellRangeSelected.unsubscribe(this.handleCellRangeSelected.bind(this) as EventListener);
@@ -83,6 +82,23 @@ export class SlickCellSelectionModel {
return this._ranges;
}
+ /**
+ * Get the number of rows displayed in the viewport
+ * Note that the row count is an approximation because it is a calculated value using this formula (viewport / rowHeight = rowCount),
+ * the viewport must also be displayed for this calculation to work.
+ * @return {Number} rowCount
+ */
+ getViewportRowCount() {
+ const viewportElm = this._grid.getViewportNode();
+ const viewportHeight = viewportElm?.clientHeight ?? 0;
+ const scrollbarHeight = this._grid.getScrollbarDimensions()?.height ?? 0;
+ return Math.floor((viewportHeight - scrollbarHeight) / this._grid.getOptions().rowHeight!) || 1;
+ }
+
+ hasDataView() {
+ return !Array.isArray(this._grid.getData());
+ }
+
rangesAreEqual(range1: CellRange[], range2: CellRange[]) {
let areDifferent = (range1.length !== range2.length);
if (!areDifferent) {
@@ -115,6 +131,11 @@ export class SlickCellSelectionModel {
return result;
}
+ /** Provide a way to force a recalculation of page row count (for example on grid resize) */
+ resetPageRowCount() {
+ this._cachedPageRowCount = 0;
+ }
+
setSelectedRanges(ranges: CellRange[], caller = 'SlickCellSelectionModel.setSelectedRanges') {
// simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) {
@@ -137,6 +158,7 @@ export class SlickCellSelectionModel {
// ---------------------
protected handleActiveCellChange(_e: Event, args: OnActiveCellChangedEventArgs) {
+ this._prevSelectedRow = undefined;
if (this._addonOptions?.selectActiveCell && args.row !== null && args.cell !== null) {
this.setSelectedRanges([new Slick.Range(args.row, args.cell)]);
} else if (!this._addonOptions?.selectActiveCell) {
@@ -157,18 +179,24 @@ export class SlickCellSelectionModel {
this.setSelectedRanges([args.range as SlickRange]);
}
+ protected isKeyAllowed(key: string) {
+ return ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'PageDown', 'PageUp', 'Home', 'End'].some(k => k === key);
+ }
+
protected handleKeyDown(e: KeyboardEvent) {
let ranges: CellRange[];
let last: SlickRange;
const active = this._grid.getActiveCell();
const metaKey = e.ctrlKey || e.metaKey;
- if (active && e.shiftKey && !metaKey && !e.altKey &&
- (e.which === KeyCode.LEFT || e.key === 'ArrowLeft'
- || e.which === KeyCode.RIGHT || e.key === 'ArrowRight'
- || e.which === KeyCode.UP || e.key === 'ArrowUp'
- || e.which === KeyCode.DOWN || e.key === 'ArrowDown')) {
+ let dataLn = 0;
+ if (this._dataView) {
+ dataLn = this._dataView?.getPagingInfo().pageSize || this._dataView.getLength();
+ } else {
+ dataLn = this._grid.getDataLength();
+ }
+ if (active && e.shiftKey && !metaKey && !e.altKey && this.isKeyAllowed(e.key)) {
ranges = this.getSelectedRanges().slice();
if (!ranges.length) {
ranges.push(new Slick.Range(active.row, active.cell));
@@ -187,25 +215,65 @@ export class SlickCellSelectionModel {
// walking direction
const dirRow = active.row === last.fromRow ? 1 : -1;
const dirCell = active.cell === last.fromCell ? 1 : -1;
-
- if (e.which === KeyCode.LEFT || e.key === 'ArrowLeft') {
- dCell -= dirCell;
- } else if (e.which === KeyCode.RIGHT || e.key === 'ArrowRight') {
- dCell += dirCell;
- } else if (e.which === KeyCode.UP || e.key === 'ArrowUp') {
- dRow -= dirRow;
- } else if (e.which === KeyCode.DOWN || e.key === 'ArrowDown') {
- dRow += dirRow;
+ const isSingleKeyMove = e.key.startsWith('Arrow');
+ let toRow = 0;
+
+ if (isSingleKeyMove) {
+ // single cell move: (Arrow{Up/ArrowDown/ArrowLeft/ArrowRight})
+ if (e.key === 'ArrowLeft') {
+ dCell -= dirCell;
+ } else if (e.key === 'ArrowRight') {
+ dCell += dirCell;
+ } else if (e.key === 'ArrowUp') {
+ dRow -= dirRow;
+ } else if (e.key === 'ArrowDown') {
+ dRow += dirRow;
+ }
+ toRow = active.row + dirRow * dRow;
+ } else {
+ // multiple cell moves: (Home, End, Page{Up/Down}), we need to know how many rows are displayed on a page
+ if (this._cachedPageRowCount < 1) {
+ this._cachedPageRowCount = this.getViewportRowCount();
+ }
+ if (this._prevSelectedRow === undefined) {
+ this._prevSelectedRow = active.row;
+ }
+
+ if (e.key === 'Home') {
+ toRow = 0;
+ } else if (e.key === 'End') {
+ toRow = dataLn - 1;
+ } else if (e.key === 'PageUp') {
+ if (this._prevSelectedRow >= 0) {
+ toRow = this._prevSelectedRow - this._cachedPageRowCount;
+ }
+ if (toRow < 0) {
+ toRow = 0;
+ }
+ } else if (e.key === 'PageDown') {
+ if (this._prevSelectedRow <= dataLn - 1) {
+ toRow = this._prevSelectedRow + this._cachedPageRowCount;
+ }
+ if (toRow > dataLn - 1) {
+ toRow = dataLn - 1;
+ }
+ }
+ this._prevSelectedRow = toRow;
}
// define new selection range
- const newLast = new Slick.Range(active.row, active.cell, active.row + dirRow * dRow, active.cell + dirCell * dCell);
+ const newLast = new Slick.Range(active.row, active.cell, toRow, active.cell + dirCell * dCell);
if (this.removeInvalidRanges([newLast]).length) {
ranges.push(newLast);
const viewRow = dirRow > 0 ? newLast.toRow : newLast.fromRow;
const viewCell = dirCell > 0 ? newLast.toCell : newLast.fromCell;
- this._grid.scrollRowIntoView(viewRow);
- this._grid.scrollCellIntoView(viewRow, viewCell, false);
+ if (isSingleKeyMove) {
+ this._grid.scrollRowIntoView(viewRow);
+ this._grid.scrollCellIntoView(viewRow, viewCell, false);
+ } else {
+ this._grid.scrollRowIntoView(toRow);
+ this._grid.scrollCellIntoView(toRow, viewCell, false);
+ }
} else {
ranges.push(last);
}
@@ -213,6 +281,7 @@ export class SlickCellSelectionModel {
e.preventDefault();
e.stopPropagation();
+ this._prevKeyDown = e.key;
}
}
}
diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss
index 6560fc4ad..66aacffb9 100644
--- a/packages/common/src/styles/_variables.scss
+++ b/packages/common/src/styles/_variables.scss
@@ -320,6 +320,7 @@ $slick-column-picker-title-width: calc(100% - #{$slick
$slick-column-picker-z-index: 9000 !default;
/* Grid Menu - hamburger menu */
+$slick-grid-menu-button-display: inline-flex !default;
$slick-grid-menu-button-padding: 0 2px !default;
$slick-grid-menu-label-margin: 4px !default;
$slick-grid-menu-label-font-weight: normal !default;
diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss
index e8d0e9f3d..af6f525b5 100644
--- a/packages/common/src/styles/slick-plugins.scss
+++ b/packages/common/src/styles/slick-plugins.scss
@@ -198,16 +198,17 @@ li.hidden {
}
.slick-grid-menu-button {
- position: absolute;
- cursor: pointer;
- right: 0;
- padding: var(--slick-grid-menu-button-padding, $slick-grid-menu-button-padding);
- margin-top: var(--slick-grid-menu-icon-top-margin, $slick-grid-menu-icon-top-margin);
background-color: transparent;
border: 0;
+ cursor: pointer;
+ right: 0;
+ position: absolute;
width: 22px;
- font-size: var(--slick-grid-menu-icon-font-size, $slick-grid-menu-icon-font-size);
z-index: 2;
+ display: var(--slick-grid-menu-button-display, $slick-grid-menu-button-display);
+ font-size: var(--slick-grid-menu-icon-font-size, $slick-grid-menu-icon-font-size);
+ padding: var(--slick-grid-menu-button-padding, $slick-grid-menu-button-padding);
+ margin-top: var(--slick-grid-menu-icon-top-margin, $slick-grid-menu-icon-top-margin);
}
.slick-grid-menu-list {
diff --git a/test/cypress/e2e/example19.cy.ts b/test/cypress/e2e/example19.cy.ts
new file mode 100644
index 000000000..2cecc0f7d
--- /dev/null
+++ b/test/cypress/e2e/example19.cy.ts
@@ -0,0 +1,147 @@
+describe('Example 19 - ExcelCopyBuffer with Cell Selection', { retries: 0 }, () => {
+ const titles = [
+ '', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
+ 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
+ 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK'
+ ];
+ const GRID_ROW_HEIGHT = 30;
+
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseUrl')}/example19`);
+ cy.get('h3').should('contain', 'Example 19 - ExcelCopyBuffer with Cell Selection');
+ });
+
+ it('should have exact column titles on 1st grid', () => {
+ cy.get('.grid19')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => {
+ if (index < titles.length) {
+ expect($child.text()).to.eq(titles[index]);
+ }
+ });
+ });
+
+ describe('with Pagination of size 20', () => {
+ it('should click on cell B14 then Shift+End w/selection B14-19', () => {
+ cy.getCell(14, 2, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_B14')
+ .click();
+
+ cy.get('@cell_B14')
+ .type('{shift}{end}');
+
+ cy.get('#selectionRange')
+ .should('have.text', '{"fromRow":14,"fromCell":2,"toRow":19,"toCell":2}');
+ });
+
+ it('should click on cell C19 then Shift+End w/selection C0-19', () => {
+ cy.getCell(19, 2, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_C19')
+ .click();
+
+ cy.get('@cell_C19')
+ .type('{shift}{home}');
+
+ cy.get('#selectionRange')
+ .should('have.text', '{"fromRow":0,"fromCell":2,"toRow":19,"toCell":2}');
+ });
+
+ it('should click on cell E3 then Shift+PageDown multiple times with current page selection starting at E3 w/selection E3-19', () => {
+ cy.getCell(3, 5, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_E3')
+ .click();
+
+ cy.get('@cell_E3')
+ .type('{shift}{pagedown}{pagedown}{pagedown}');
+
+ cy.get('#selectionRange')
+ .should('have.text', '{"fromRow":3,"fromCell":5,"toRow":19,"toCell":5}');
+ });
+
+ it('should change to 2nd page then click on cell D41 then Shift+PageUp multiple times with current page selection w/selection D25-41', () => {
+ cy.get('.slick-pagination .icon-seek-next').click();
+
+ cy.getCell(15, 4, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_D41')
+ .click();
+
+ cy.get('@cell_D41')
+ .type('{shift}{pageup}{pageup}{pageup}');
+
+ cy.get('#selectionRange')
+ .should('have.text', '{"fromRow":0,"fromCell":4,"toRow":15,"toCell":4}');
+ });
+ });
+
+ describe('no Pagination - showing all', () => {
+ it('should hide Pagination', () => {
+ cy.get('[data-text="toggle-pagination-btn"]')
+ .click();
+ });
+
+ it('should click on cell B10 and ArrowUp 3 times and ArrowDown 1 time and expect cell selection B8-B10', () => {
+ cy.getCell(10, 2, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_B10')
+ .click();
+
+ cy.get('@cell_B10')
+ .type('{shift}{uparrow}{uparrow}{uparrow}{downarrow}');
+
+ cy.get('.slick-cell.l2.r2.selected')
+ .should('have.length', 3);
+
+ cy.get('#selectionRange')
+ .should('have.text', '{"fromRow":8,"fromCell":2,"toRow":10,"toCell":2}');
+ });
+
+ it('should click on cell D10 then PageDown 2 times w/selection D10-D50 (or D10-D52)', () => {
+ // 52 is because of a page row count found to be 21 for current browser resolution set in Cypress => 21*2+10 = 52
+ cy.getCell(10, 4, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_D10')
+ .click();
+
+ cy.get('@cell_D10')
+ .type('{shift}{pagedown}{pagedown}');
+
+ cy.get('#selectionRange')
+ .should('contains', /{"fromRow":10,"fromCell":4,"toRow":5[0-2],"toCell":4}/);
+ });
+
+ it('should click on cell D10 then PageDown 3 times then PageUp 1 time w/selection D10-D50 (or D10-D52)', () => {
+ cy.getCell(10, 4, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_D10')
+ .click();
+
+ cy.get('@cell_D10')
+ .type('{shift}{pagedown}{pagedown}{pagedown}{pageup}');
+
+ cy.get('#selectionRange')
+ .should('contains', /{"fromRow":10,"fromCell":4,"toRow":5[0-2],"toCell":4}/);
+ });
+
+ it('should click on cell E12 then End key w/selection E52-E99', () => {
+ cy.getCell(52, 5, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_E52')
+ .click();
+
+ cy.get('@cell_E52')
+ .type('{shift}{end}');
+
+ cy.get('#selectionRange')
+ .should('have.text', '{"fromRow":52,"fromCell":5,"toRow":99,"toCell":5}');
+ });
+
+ it('should click on cell C85 then End key w/selection C0-C85', () => {
+ cy.getCell(85, 3, '', { parentSelector: '.grid19', rowHeight: GRID_ROW_HEIGHT })
+ .as('cell_C85')
+ .click();
+
+ cy.get('@cell_C85')
+ .type('{shift}{home}');
+
+ cy.get('#selectionRange')
+ .should('have.text', '{"fromRow":0,"fromCell":3,"toRow":85,"toCell":3}');
+ });
+ });
+});