From ecd9c57bd6c1315e2358722785a87582ec939f85 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Sat, 18 Dec 2021 17:17:42 -0500 Subject: [PATCH 1/3] feat(selection): auto-scroll the viewport when dragging with selection - implements latest SlickGrid [PR #656](https://github.com/6pac/SlickGrid/pull/656) to auto-scroll when making cell or row selection --- .../src/app-routing.ts | 1 + .../webpack-demo-vanilla-bundle/src/app.html | 5 +- .../src/examples/example17.html | 50 +++ .../src/examples/example17.scss | 34 ++ .../src/examples/example17.ts | 156 +++++++++ .../__tests__/slickCellRangeSelector.spec.ts | 320 +++++++++++++++++- .../__tests__/slickCellSelectionModel.spec.ts | 8 +- .../__tests__/slickRowSelectionModel.spec.ts | 68 ++++ .../src/extensions/slickCellRangeSelector.ts | 251 ++++++++++++-- .../src/extensions/slickCellSelectionModel.ts | 14 +- .../src/extensions/slickRowSelectionModel.ts | 77 ++++- .../src/interfaces/cellRange.interface.ts | 15 + packages/common/src/interfaces/index.ts | 1 + .../mouseOffsetViewport.interface.ts | 25 ++ .../rowSelectionModelOption.interface.ts | 5 + .../src/interfaces/slickGrid.interface.ts | 10 +- 16 files changed, 979 insertions(+), 61 deletions(-) create mode 100644 examples/webpack-demo-vanilla-bundle/src/examples/example17.html create mode 100644 examples/webpack-demo-vanilla-bundle/src/examples/example17.scss create mode 100644 examples/webpack-demo-vanilla-bundle/src/examples/example17.ts create mode 100644 packages/common/src/interfaces/mouseOffsetViewport.interface.ts diff --git a/examples/webpack-demo-vanilla-bundle/src/app-routing.ts b/examples/webpack-demo-vanilla-bundle/src/app-routing.ts index d24166405..acbcbd8cf 100644 --- a/examples/webpack-demo-vanilla-bundle/src/app-routing.ts +++ b/examples/webpack-demo-vanilla-bundle/src/app-routing.ts @@ -20,6 +20,7 @@ export class AppRouting { { route: 'example14', name: 'example14', title: 'Example14', moduleId: './examples/example14' }, { route: 'example15', name: 'example15', title: 'Example15', moduleId: './examples/example15' }, { route: 'example16', name: 'example16', title: 'Example16', moduleId: './examples/example16' }, + { route: 'example17', name: 'example17', title: 'Example17', moduleId: './examples/example17' }, { route: 'icons', name: 'icons', title: 'icons', moduleId: './examples/icons' }, { route: '', redirect: 'example01' }, { route: '**', redirect: 'example01' } diff --git a/examples/webpack-demo-vanilla-bundle/src/app.html b/examples/webpack-demo-vanilla-bundle/src/app.html index 4273001bb..15aa14378 100644 --- a/examples/webpack-demo-vanilla-bundle/src/app.html +++ b/examples/webpack-demo-vanilla-bundle/src/app.html @@ -12,7 +12,7 @@

Slickgrid-Universal

Example16 - Regular & Custom Tooltips + + Example17 - Auto-Scroll for Selector + diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example17.html b/examples/webpack-demo-vanilla-bundle/src/examples/example17.html new file mode 100644 index 000000000..b8bef5fe7 --- /dev/null +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example17.html @@ -0,0 +1,50 @@ +

Example 17 - Auto-Scroll with Range Selector + (with Salesforce Theme) + +

+ +
+
+ + + + + + + + +
+
+ + + + + + + + +
+ +
Grid 1 - Using "SlickCellRangeSelector"
+
+
+ +
+
+
+ +
Grid 2 - Using "SlickRowSelectionModel"
+
+
+
\ No newline at end of file diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example17.scss b/examples/webpack-demo-vanilla-bundle/src/examples/example17.scss new file mode 100644 index 000000000..27c82ee73 --- /dev/null +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example17.scss @@ -0,0 +1,34 @@ +$control-height: 2.4em; +@import 'bulma/bulma'; + +.scroll-configs input { + width: 50px; +} +.cell-effort-driven { + text-align: center; +} +.slick-group-title[level='0'] { + font-weight: bold; +} +.slick-group-title[level='1'] { + text-decoration: underline; +} +.slick-group-title[level='2'] { + font-style: italic; +} +.slick-row:not(.slick-group) >.cell-unselectable { + background: #efefef; +} +.slick-row .slick-cell.frozen:last-child, +.slick-header-column.frozen:last-child, +.slick-headerrow-column.frozen:last-child, +.slick-footerrow-column.frozen:last-child { + border-right: 1px solid red; +} + +.slick-row.frozen:last-child .slick-cell { + border-bottom: 1px solid red; +} +.option-item { + padding: 6px; +} \ No newline at end of file diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example17.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example17.ts new file mode 100644 index 000000000..ade978397 --- /dev/null +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example17.ts @@ -0,0 +1,156 @@ +import { Column, Formatters, GridOption, SlickCellRangeSelector, SlickCellSelectionModel, SlickRowSelectionModel } from '@slickgrid-universal/common'; +import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; +import { ExampleGridOptions } from './example-grid-options'; + +// use any of the Styling Theme +import '../material-styles.scss'; +// import '../salesforce-styles.scss'; +import './example17.scss'; + +const NB_ITEMS = 995; + +export class Example17 { + gridOptions1: GridOption; + gridOptions2: GridOption; + columnDefinitions1: Column[]; + columnDefinitions2: Column[]; + dataset1: any[]; + dataset2: any[]; + sgb1: SlickVanillaGridBundle; + sgb2: SlickVanillaGridBundle; + isAutoScroll = true; + minInterval = 30; + maxInterval = 600; + delayCursor = 5; + + attached() { + this.defineGrids(); + + // mock some data (different in each dataset) + this.dataset1 = this.mockData(NB_ITEMS); + this.dataset2 = this.mockData(NB_ITEMS); + + this.sgb1 = new Slicker.GridBundle(document.querySelector(`.grid1`), this.columnDefinitions1, { ...ExampleGridOptions, ...this.gridOptions1 }, this.dataset1); + this.sgb2 = new Slicker.GridBundle(document.querySelector(`.grid2`), this.columnDefinitions2, { ...ExampleGridOptions, ...this.gridOptions2 }, this.dataset2); + + this.setOptions(); + } + + dispose() { + this.sgb1?.dispose(); + this.sgb2?.dispose(); + } + + /* Define grid Options and Columns */ + defineGrids() { + this.columnDefinitions1 = [ + { id: 'title', name: 'Title', field: 'title', sortable: true, minWidth: 100, filterable: true }, + { id: 'duration', name: 'Duration', field: 'duration', sortable: true, minWidth: 100, filterable: true }, + { id: '%', name: '% Complete', field: 'percentComplete', sortable: true, minWidth: 100, filterable: true, formatter: Formatters.percentCompleteBar }, + { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, minWidth: 120, exportWithFormatter: true, filterable: true }, + { id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, minWidth: 120, exportWithFormatter: true, filterable: true }, + { id: 'cost', name: 'Cost', field: 'cost', formatter: Formatters.dollar, minWidth: 75, exportWithFormatter: true, filterable: true }, + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', formatter: Formatters.checkmarkMaterial, sortable: true, minWidth: 75, filterable: true } + ]; + + for (let i = 0; i < 30; i++) { + this.columnDefinitions1.push({ id: `mock${i}`, name: `Mock${i}`, field: `mock${i}`, minWidth: 75 }); + } + + this.gridOptions1 = { + enableAutoResize: false, + enableCellNavigation: true, + gridHeight: 225, + gridWidth: 800, + rowHeight: 33, + // enableExcelCopyBuffer: true, + }; + + // copy the same Grid Options and Column Definitions to 2nd grid + this.columnDefinitions2 = this.columnDefinitions1; + this.gridOptions2 = { + ...this.gridOptions1, + ...{ + enableCheckboxSelector: true, + // enableExcelCopyBuffer: false, + gridHeight: 255, + } + }; + } + + mockData(count: number) { + // mock a dataset + const mockDataset = []; + for (let i = 0; i < count; i++) { + const randomYear = 2000 + Math.floor(Math.random() * 10); + const randomMonth = Math.floor(Math.random() * 11); + const randomDay = Math.floor((Math.random() * 29)); + const randomPercent = Math.round(Math.random() * 100); + + mockDataset[i] = { + id: i, + title: 'Task ' + i, + duration: Math.round(Math.random() * 100) + '', + percentComplete: randomPercent, + start: new Date(randomYear, randomMonth + 1, randomDay), + finish: new Date(randomYear + 1, randomMonth + 1, randomDay), + cost: Math.round(Math.random() * 10000) / 100, + effortDriven: (i % 5 === 0) + }; + for (let j = 0; j < 30; j++) { + mockDataset[i]['mock' + j] = j; + } + } + + return mockDataset; + } + + toggleFrozen() { + const option = this.sgb1.slickGrid.getOptions(); + const frozenRow = option.frozenRow; + const frozenColumn = option.frozenColumn; + const newOption = { + frozenColumn: frozenColumn === -1 ? 1 : -1, + frozenRow: frozenRow === -1 ? 3 : -1 + }; + this.sgb1.slickGrid.setOptions(newOption); + this.sgb2.slickGrid.setOptions(newOption); + } + + setDefaultOptions() { + this.isAutoScroll = true; + this.minInterval = 30; + this.maxInterval = 600; + this.delayCursor = 5; + this.setOptions(); + } + + setOptions() { + this.sgb1.slickGrid.setSelectionModel(new SlickCellSelectionModel({ + selectActiveCell: true, + cellRangeSelector: new SlickCellRangeSelector({ + selectionCss: { + border: '2px dashed #01b83b' + } as CSSStyleDeclaration, + autoScroll: this.isAutoScroll, + minIntervalToShowNextCell: +this.minInterval, + maxIntervalToShowNextCell: +this.maxInterval, + accelerateInterval: +this.delayCursor + }) + })); + + this.sgb2.slickGrid.setSelectionModel(new SlickRowSelectionModel({ + cellRangeSelector: new SlickCellRangeSelector({ + selectionCss: { + border: 'none' + } as CSSStyleDeclaration, + autoScroll: this.isAutoScroll, + minIntervalToShowNextCell: +this.minInterval, + maxIntervalToShowNextCell: +this.maxInterval, + accelerateInterval: +this.delayCursor + }) + })); + this.sgb1.slickGrid.invalidate(); + this.sgb2.slickGrid.invalidate(); + } +} diff --git a/packages/common/src/extensions/__tests__/slickCellRangeSelector.spec.ts b/packages/common/src/extensions/__tests__/slickCellRangeSelector.spec.ts index 526e4ed42..e2464315d 100644 --- a/packages/common/src/extensions/__tests__/slickCellRangeSelector.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCellRangeSelector.spec.ts @@ -16,19 +16,24 @@ const addJQueryEventPropagation = function (event) { const mockGridOptions = { frozenColumn: 1, frozenRow: -1, + rowHeight: 30, } as GridOption; const gridStub = { canCellBeSelected: jest.fn(), + getAbsoluteColumnMinWidth: jest.fn(), getActiveCell: jest.fn(), getActiveCanvasNode: jest.fn(), + getActiveViewportNode: jest.fn(), getCanvasNode: jest.fn(), getCellFromEvent: jest.fn(), getCellFromPoint: jest.fn(), getCellNodeBox: jest.fn(), + getDisplayedScrollbarDimensions: jest.fn(), getOptions: () => mockGridOptions, getUID: () => GRID_UID, focus: jest.fn(), + scrollCellIntoView: jest.fn(), onDragInit: new Slick.Event(), onDragStart: new Slick.Event(), onDrag: new Slick.Event(), @@ -63,6 +68,7 @@ describe('CellRangeSelector Plugin', () => { beforeEach(() => { plugin = new SlickCellRangeSelector(); + jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); }); afterEach(() => { @@ -71,18 +77,29 @@ describe('CellRangeSelector Plugin', () => { mockGridOptions.frozenColumn = -1; mockGridOptions.frozenRow = -1; mockGridOptions.frozenBottom = false; + mockGridOptions.rowHeight = 30; }); it('should create the plugin', () => { expect(plugin).toBeTruthy(); expect(plugin.eventHandler).toBeTruthy(); expect(plugin.addonOptions).toEqual({ + autoScroll: true, + minIntervalToShowNextCell: 30, + maxIntervalToShowNextCell: 600, + accelerateInterval: 5, selectionCss: { border: '2px dashed blue', } }); }); + it('should dispose the plugin when calling destroy', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + it('should create the plugin and initialize it', () => { plugin.init(gridStub); @@ -91,8 +108,14 @@ describe('CellRangeSelector Plugin', () => { it('should handle drag but return without executing anything when item cannot be dragged and cell cannot be selected', () => { const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; divCanvas.className = 'grid-canvas-bottom grid-canvas-left'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(false); jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); @@ -126,12 +149,19 @@ describe('CellRangeSelector Plugin', () => { it('should handle drag in bottom left canvas', () => { mockGridOptions.frozenRow = 2; const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; divCanvas.className = 'grid-canvas-bottom grid-canvas-left'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); const focusSpy = jest.spyOn(gridStub, 'focus'); + const scrollSpy = jest.spyOn(gridStub, 'scrollCellIntoView'); jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); plugin.init(gridStub); @@ -147,22 +177,32 @@ describe('CellRangeSelector Plugin', () => { gridStub.onDragStart.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventStart, gridStub); const dragEvent = addJQueryEventPropagation(new Event('drag')); + dragEvent.pageX = 0; + dragEvent.pageY = 0; gridStub.onDrag.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEvent, gridStub); expect(focusSpy).toHaveBeenCalled(); expect(decoratorShowSpy).toHaveBeenCalled(); expect(plugin.getCurrentRange()).toEqual({ start: { cell: 4, row: 5 }, end: {} }); + expect(scrollSpy).not.toHaveBeenCalled(); }); it('should handle drag in bottom right canvas with decorator showing dragging range', () => { mockGridOptions.frozenColumn = 3; const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); const focusSpy = jest.spyOn(gridStub, 'focus'); + const scrollSpy = jest.spyOn(gridStub, 'scrollCellIntoView'); const onBeforeCellSpy = jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); plugin.init(gridStub); @@ -178,12 +218,14 @@ describe('CellRangeSelector Plugin', () => { gridStub.onDragStart.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventStart, gridStub); const dragEvent = addJQueryEventPropagation(new Event('drag')); + dragEvent.pageX = -2; + dragEvent.pageY = -156; gridStub.onDrag.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEvent, gridStub); expect(focusSpy).toHaveBeenCalled(); expect(onBeforeCellSpy).toHaveBeenCalled(); expect(decoratorShowSpy).toHaveBeenCalledWith({ - fromCell: 2, fromRow: 3, toCell: 4, toRow: 5, + fromCell: 4, fromRow: 5, toCell: 4, toRow: 5, contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), }); expect(plugin.getCurrentRange()).toEqual({ start: { cell: 4, row: 5 }, end: {} }); @@ -192,8 +234,14 @@ describe('CellRangeSelector Plugin', () => { it('should handle drag end in bottom right canvas with "onCellRangeSelected" published', () => { mockGridOptions.frozenColumn = 3; const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); @@ -236,17 +284,76 @@ describe('CellRangeSelector Plugin', () => { expect(plugin.getCurrentRange()).toEqual({ start: { cell: 4, row: 5 }, end: {} }); }); - it('should handle drag and return when "canCellBeSelected" returs', () => { + it('should handle drag and return when "canCellBeSelected" returns False', () => { mockGridOptions.frozenColumn = 3; const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValueOnce(true).mockReturnValueOnce(false); jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); + jest.spyOn(gridStub, 'getCellNodeBox').mockReturnValue({ right: 2, bottom: 3, left: 4, top: 5, height: 20, width: 33, visible: true }); + const focusSpy = jest.spyOn(gridStub, 'focus'); + const onBeforeCellRangeSpy = jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + const onCellRangeSpy = jest.spyOn(plugin.onCellRangeSelected, 'notify').mockReturnValue(true); + const scrollSpy = jest.spyOn(gridStub, 'scrollCellIntoView'); + const onCellRangeSelectingSpy = jest.spyOn(plugin.onCellRangeSelecting, 'notify'); + + plugin.init(gridStub); + const decoratorHideSpy = jest.spyOn(plugin.getCellDecorator(), 'hide'); + const decoratorShowSpy = jest.spyOn(plugin.getCellDecorator(), 'show'); + + const scrollEvent = addJQueryEventPropagation(new Event('scroll')); + gridStub.onScroll.notify({ scrollTop: 10, scrollLeft: 15, grid: gridStub }, scrollEvent, gridStub); + + const dragEventInit = addJQueryEventPropagation(new Event('dragInit')); + gridStub.onDragInit.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventInit, gridStub); + + const dragEventStart = addJQueryEventPropagation(new Event('dragStart')); + gridStub.onDragStart.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventStart, gridStub); + + const dragEvent = addJQueryEventPropagation(new Event('drag')); + gridStub.onDrag.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEvent, gridStub); + + expect(focusSpy).toHaveBeenCalled(); + expect(onBeforeCellRangeSpy).toHaveBeenCalled(); + expect(onCellRangeSpy).not.toHaveBeenCalled(); + expect(decoratorHideSpy).not.toHaveBeenCalled(); + expect(decoratorShowSpy).toHaveBeenCalledWith({ + fromCell: 4, fromRow: 5, toCell: 4, toRow: 5, + contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + }); + expect(plugin.getCurrentRange()).toEqual({ start: { cell: 4, row: 5 }, end: {} }); + expect(scrollSpy).toHaveBeenCalledWith(5, 4); + expect(onCellRangeSelectingSpy).not.toHaveBeenCalled(); + }); + + it('should handle drag and cell range selection to be changed when "canCellBeSelected" returns True', () => { + mockGridOptions.frozenColumn = 3; + const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; + divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValueOnce(true); + jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); + jest.spyOn(gridStub, 'getCellNodeBox').mockReturnValue({ right: 2, bottom: 3, left: 4, top: 5, height: 20, width: 33, visible: true }); const focusSpy = jest.spyOn(gridStub, 'focus'); const onBeforeCellRangeSpy = jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); const onCellRangeSpy = jest.spyOn(plugin.onCellRangeSelected, 'notify').mockReturnValue(true); + const scrollSpy = jest.spyOn(gridStub, 'scrollCellIntoView'); + const onCellRangeSelectingSpy = jest.spyOn(plugin.onCellRangeSelecting, 'notify'); plugin.init(gridStub); const decoratorHideSpy = jest.spyOn(plugin.getCellDecorator(), 'hide'); @@ -273,14 +380,30 @@ describe('CellRangeSelector Plugin', () => { contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), }); expect(plugin.getCurrentRange()).toEqual({ start: { cell: 4, row: 5 }, end: {} }); + expect(scrollSpy).toHaveBeenCalledWith(5, 4); + expect(onCellRangeSelectingSpy).toHaveBeenCalledWith({ + range: { + fromCell: 2, fromRow: 3, toCell: 4, toRow: 5, + contains: expect.toBeFunction(), + isSingleCell: expect.toBeFunction(), + isSingleRow: expect.toBeFunction(), + toString: expect.toBeFunction(), + }, + }); }); it('should handle drag and expect the decorator to NOT call the "show" method and return (frozen row) with canvas bottom right', () => { mockGridOptions.frozenColumn = 3; mockGridOptions.frozenRow = 1; const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 0 }); @@ -302,24 +425,30 @@ describe('CellRangeSelector Plugin', () => { const dragEvent = addJQueryEventPropagation(new Event('drag')); gridStub.onDrag.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEvent, gridStub); - expect(focusSpy).toHaveBeenCalled(); + // expect(focusSpy).toHaveBeenCalled(); expect(onBeforeCellRangeSpy).toHaveBeenCalled(); expect(decoratorShowSpy).not.toHaveBeenCalledWith({ fromCell: 4, fromRow: 5, toCell: 4, toRow: 5, // from handleDrag contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), }); - expect(decoratorShowSpy).toHaveBeenCalledWith({ - fromCell: 4, fromRow: 0, toCell: 4, toRow: 0, // from handleDragStart - contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), - }); + // expect(decoratorShowSpy).toHaveBeenCalledWith({ + // fromCell: 4, fromRow: 0, toCell: 4, toRow: 0, // from handleDragStart + // contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + // }); }); it('should handle drag and expect the decorator to NOT call the "show" method and return (frozen column) with canvas top right', () => { mockGridOptions.frozenColumn = 5; mockGridOptions.frozenRow = 1; const divCanvas = document.createElement('div'); - divCanvas.className = 'grid-canvas-top grid-canvas-right'; + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; + divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 0 }); @@ -352,4 +481,179 @@ describe('CellRangeSelector Plugin', () => { contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), }); }); + + it('should call onDrag and handle drag outside the viewport when drag is detected as outside the viewport', (done) => { + mockGridOptions.frozenRow = 2; + const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; + divCanvas.className = 'grid-canvas-bottom grid-canvas-left'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); + jest.spyOn(plugin, 'getMouseOffsetViewport').mockReturnValue({ + e: new MouseEvent('dragstart'), + dd: { startX: 5, startY: 15, range: { start: { row: 2, cell: 22 }, end: { row: 5, cell: 22 } } }, + viewport: { left: 23, top: 24, right: 25, bottom: 26, offset: { left: 27, top: 28, right: 29, bottom: 30 } }, + offset: { x: 0, y: 0 }, + isOutsideViewport: true + }); + const focusSpy = jest.spyOn(gridStub, 'focus'); + jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + const getCellFromPointSpy = jest.spyOn(gridStub, 'getCellFromPoint'); + const onCellRangeSelectingSpy = jest.spyOn(plugin.onCellRangeSelecting, 'notify'); + + plugin.init(gridStub); + plugin.addonOptions.minIntervalToShowNextCell = 5; + plugin.addonOptions.maxIntervalToShowNextCell = 6; + const decoratorShowSpy = jest.spyOn(plugin.getCellDecorator(), 'show'); + + const scrollEvent = addJQueryEventPropagation(new Event('scroll')); + gridStub.onScroll.notify({ scrollTop: 10, scrollLeft: 15, grid: gridStub }, scrollEvent, gridStub); + + const dragEventInit = addJQueryEventPropagation(new Event('dragInit')); + gridStub.onDragInit.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventInit, gridStub); + + const dragEventStart = addJQueryEventPropagation(new Event('dragStart')); + gridStub.onDragStart.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventStart, gridStub); + + const dragEvent = addJQueryEventPropagation(new Event('drag')); + gridStub.onDrag.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEvent, gridStub); + + expect(focusSpy).toHaveBeenCalled(); + expect(decoratorShowSpy).toHaveBeenCalled(); + expect(plugin.getCurrentRange()).toEqual({ start: { cell: 4, row: 5 }, end: {} }); + expect(getCellFromPointSpy).toHaveBeenCalledWith(3, 14); + setTimeout(() => { + expect(onCellRangeSelectingSpy).not.toHaveBeenCalled(); + done(); + }, 7); + }); + + it('should call onDrag and handle drag outside the viewport and expect drag to be moved to a new position', (done) => { + mockGridOptions.frozenRow = 2; + const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; + divCanvas.className = 'grid-canvas-bottom grid-canvas-left'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); + jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); + jest.spyOn(plugin, 'getMouseOffsetViewport').mockReturnValue({ + e: new MouseEvent('dragstart'), + dd: { startX: 5, startY: 15, range: { start: { row: 2, cell: 22 }, end: { row: 5, cell: 22 } } }, + viewport: { left: 23, top: 24, right: 25, bottom: 26, offset: { left: 27, top: 28, right: 29, bottom: 30 } }, + offset: { x: 1, y: 1 }, + isOutsideViewport: true + }); + const focusSpy = jest.spyOn(gridStub, 'focus'); + jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + const getCellFromPointSpy = jest.spyOn(gridStub, 'getCellFromPoint'); + const onCellRangeSelectingSpy = jest.spyOn(plugin.onCellRangeSelecting, 'notify'); + + plugin.init(gridStub); + plugin.addonOptions.minIntervalToShowNextCell = 5; + plugin.addonOptions.maxIntervalToShowNextCell = 6; + const decoratorShowSpy = jest.spyOn(plugin.getCellDecorator(), 'show'); + + const scrollEvent = addJQueryEventPropagation(new Event('scroll')); + gridStub.onScroll.notify({ scrollTop: 10, scrollLeft: 15, grid: gridStub }, scrollEvent, gridStub); + + const dragEventInit = addJQueryEventPropagation(new Event('dragInit')); + gridStub.onDragInit.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventInit, gridStub); + + const dragEventStart = addJQueryEventPropagation(new Event('dragStart')); + gridStub.onDragStart.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventStart, gridStub); + + const dragEvent = addJQueryEventPropagation(new Event('drag')); + gridStub.onDrag.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEvent, gridStub); + + expect(focusSpy).toHaveBeenCalled(); + expect(decoratorShowSpy).toHaveBeenCalled(); + expect(plugin.getCurrentRange()).toEqual({ start: { cell: 4, row: 5 }, end: {} }); + expect(getCellFromPointSpy).toHaveBeenCalledWith(3, 14); + + setTimeout(() => { + expect(onCellRangeSelectingSpy).toHaveBeenCalledWith({ + range: { + fromCell: 4, fromRow: 2, toCell: 22, toRow: 5, + contains: expect.toBeFunction(), + isSingleCell: expect.toBeFunction(), + isSingleRow: expect.toBeFunction(), + toString: expect.toBeFunction(), + }, + }); + done(); + }, 7); + }); + + it('should call onDrag and handle drag outside the viewport with negative offset and expect drag to be moved to a new position', (done) => { + mockGridOptions.frozenRow = 2; + const divCanvas = document.createElement('div'); + const divViewport = document.createElement('div'); + divViewport.className = 'slick-viewport'; + divCanvas.className = 'grid-canvas-bottom grid-canvas-left'; + divViewport.appendChild(divCanvas); + jest.spyOn(gridStub, 'getActiveViewportNode').mockReturnValue(divViewport); + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getDisplayedScrollbarDimensions').mockReturnValue({ height: 200, width: 155 }); + jest.spyOn(gridStub, 'getAbsoluteColumnMinWidth').mockReturnValue(47); + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValueOnce(true); + jest.spyOn(plugin, 'getMouseOffsetViewport').mockReturnValue({ + e: new MouseEvent('dragstart'), + dd: { startX: 5, startY: 15, range: { start: { row: 2, cell: 22 }, end: { row: 5, cell: 22 } } }, + viewport: { left: 23, top: 24, right: 25, bottom: 26, offset: { left: 27, top: 28, right: 29, bottom: 30 } }, + offset: { x: -2, y: -4 }, + isOutsideViewport: true + }); + const focusSpy = jest.spyOn(gridStub, 'focus'); + jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + const getCellFromPointSpy = jest.spyOn(gridStub, 'getCellFromPoint'); + const onCellRangeSelectingSpy = jest.spyOn(plugin.onCellRangeSelecting, 'notify'); + + plugin.init(gridStub); + plugin.addonOptions.minIntervalToShowNextCell = 5; + plugin.addonOptions.maxIntervalToShowNextCell = 6; + const decoratorShowSpy = jest.spyOn(plugin.getCellDecorator(), 'show'); + + const scrollEvent = addJQueryEventPropagation(new Event('scroll')); + gridStub.onScroll.notify({ scrollTop: 10, scrollLeft: 15, grid: gridStub }, scrollEvent, gridStub); + + const dragEventInit = addJQueryEventPropagation(new Event('dragInit')); + gridStub.onDragInit.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventInit, gridStub); + + const dragEventStart = addJQueryEventPropagation(new Event('dragStart')); + gridStub.onDragStart.notify({ offsetX: 6, offsetY: 7, row: 1, startX: 3, startY: 4 } as any, dragEventStart, gridStub); + + const dragEvent = addJQueryEventPropagation(new Event('drag')); + gridStub.onDrag.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEvent, gridStub); + + expect(focusSpy).toHaveBeenCalled(); + expect(decoratorShowSpy).toHaveBeenCalled(); + expect(plugin.getCurrentRange()).toEqual({ start: { cell: 4, row: 5 }, end: {} }); + expect(getCellFromPointSpy).toHaveBeenCalledWith(3, 14); + + setTimeout(() => { + expect(onCellRangeSelectingSpy).toHaveBeenCalledWith({ + range: { + fromCell: 4, fromRow: 2, toCell: 22, toRow: 5, + contains: expect.toBeFunction(), + isSingleCell: expect.toBeFunction(), + isSingleRow: expect.toBeFunction(), + toString: expect.toBeFunction(), + }, + }); + done(); + }, 7); + }); }); \ No newline at end of file diff --git a/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts b/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts index 0819e769c..b24db95dd 100644 --- a/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts @@ -97,6 +97,12 @@ describe('CellSelectionModel Plugin', () => { expect(plugin.cellRangeSelector).toBeTruthy(); }); + it('should dispose the plugin when calling destroy', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + it('should create the plugin and initialize it', () => { const registerSpy = jest.spyOn(gridStub, 'registerPlugin'); @@ -139,7 +145,7 @@ describe('CellSelectionModel Plugin', () => { const stopPropSpy = jest.spyOn(mouseEvent, 'stopPropagation'); plugin.init(gridStub); - const output = plugin.cellRangeSelector.onBeforeCellRangeSelected.notify({ cell: 2, row: 3, grid: gridStub }, mouseEvent, gridStub); + const output = plugin.cellRangeSelector.onBeforeCellRangeSelected.notify({ cell: 2, row: 3 }, mouseEvent, gridStub); expect(output).toBeFalsy(); expect(stopPropSpy).toHaveBeenCalled(); diff --git a/packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts b/packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts index 3b9356fb8..bf6320f0f 100644 --- a/packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts +++ b/packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts @@ -1,6 +1,7 @@ import 'jest-extended'; import { Column, GridOption, SlickGrid, SlickNamespace, SlickRange, } from '../../interfaces/index'; +import { SlickCellRangeSelector } from '../slickCellRangeSelector'; import { SlickRowSelectionModel } from '../slickRowSelectionModel'; declare const Slick: SlickNamespace; @@ -106,6 +107,12 @@ describe('SlickRowSelectionModel Plugin', () => { expect(plugin.eventHandler).toBeTruthy(); }); + it('should dispose the plugin when calling destroy', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + it('should create the plugin and initialize it', () => { plugin.init(gridStub); @@ -441,4 +448,65 @@ describe('SlickRowSelectionModel Plugin', () => { contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), }]); }); + + describe('with Selector', () => { + beforeEach(() => { + plugin.addonOptions.cellRangeSelector = new SlickCellRangeSelector({ + selectionCss: { + border: 'none' + } as CSSStyleDeclaration, + autoScroll: true, + minIntervalToShowNextCell: 30, + maxIntervalToShowNextCell: 500, + accelerateInterval: 5 + }) + }); + + it('should call "setSelectedRanges" when "onCellRangeSelected" event is triggered', () => { + const setSelectedRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + + plugin.init(gridStub); + const scrollEvent = addJQueryEventPropagation(new Event('scroll')); + plugin.getCellRangeSelector().onCellRangeSelected.notify({ range: { fromCell: 2, fromRow: 3, toCell: 4, toRow: 5 } }, scrollEvent, gridStub); + + expect(setSelectedRangeSpy).toHaveBeenCalledWith([{ + fromCell: 0, fromRow: 3, toCell: 2, toRow: 5, + contains: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), toString: expect.toBeFunction() + }]); + }); + + it('should call "setSelectedRanges" when "onCellRangeSelected" event is triggered', () => { + const setSelectedRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + mockGridOptions.multiSelect = false; + + plugin.init(gridStub); + const scrollEvent = addJQueryEventPropagation(new Event('scroll')); + plugin.getCellRangeSelector().onCellRangeSelected.notify({ range: { fromCell: 2, fromRow: 3, toCell: 4, toRow: 5 } }, scrollEvent, gridStub); + + expect(setSelectedRangeSpy).not.toHaveBeenCalled(); + }); + + it('should call "setActiveCell" when "onBeforeCellRangeSelected" event is triggered', () => { + const setActiveCellSpy = jest.spyOn(gridStub, 'setActiveCell'); + mockGridOptions.multiSelect = false; + + plugin.init(gridStub); + const scrollEvent = addJQueryEventPropagation(new Event('scroll')); + plugin.getCellRangeSelector().onBeforeCellRangeSelected.notify({ row: 2, cell: 4 }, scrollEvent, gridStub); + + expect(setActiveCellSpy).toHaveBeenCalledWith(2, 4); + }); + + it('should NOT call "setActiveCell" when EditorLock isActive is returning True', () => { + const setActiveCellSpy = jest.spyOn(gridStub, 'setActiveCell'); + jest.spyOn(getEditorLockMock, 'isActive').mockReturnValue(true) + mockGridOptions.multiSelect = false; + + plugin.init(gridStub); + const scrollEvent = addJQueryEventPropagation(new Event('scroll')); + plugin.getCellRangeSelector().onBeforeCellRangeSelected.notify({ row: 2, cell: 4 }, scrollEvent, gridStub); + + expect(setActiveCellSpy).not.toHaveBeenCalled(); + }); + }); }); \ No newline at end of file diff --git a/packages/common/src/extensions/slickCellRangeSelector.ts b/packages/common/src/extensions/slickCellRangeSelector.ts index a0e9dd184..29c800fbf 100644 --- a/packages/common/src/extensions/slickCellRangeSelector.ts +++ b/packages/common/src/extensions/slickCellRangeSelector.ts @@ -1,11 +1,23 @@ +import { + CellRange, + CellRangeSelectorOption, + DOMMouseEvent, + DragPosition, + DragRange, + GridOption, + MouseOffsetViewport, + OnScrollEventArgs, + SlickEventData, + SlickEventHandler, + SlickGrid, + SlickNamespace +} from '../interfaces/index'; import { emptyElement, getHtmlElementOffset, } from '../services/domUtilities'; -import { CellRange, CellRangeSelectorOption, DOMMouseEvent, DragPosition, DragRange, GridOption, OnScrollEventArgs, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; -import { SlickCellRangeDecorator } from './index'; import { deepMerge } from '../services/utilities'; +import { SlickCellRangeDecorator } from './index'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; - export class SlickCellRangeSelector { protected _activeCanvas?: HTMLElement; protected _addonOptions!: CellRangeSelectorOption; @@ -24,16 +36,31 @@ export class SlickCellRangeSelector { protected _isRightCanvas = false; protected _isBottomCanvas = false; + // autoScroll related constiables + protected _activeViewport!: HTMLElement; + protected _autoScrollTimerId?: NodeJS.Timeout; + protected _draggingMouseOffset!: MouseOffsetViewport; + protected _moveDistanceForOneCell!: { x: number; y: number; }; + protected _xDelayForNextCell = 0; + protected _yDelayForNextCell = 0; + protected _viewportHeight = 0; + protected _viewportWidth = 0; + // Scrollings protected _scrollLeft = 0; protected _scrollTop = 0; protected _defaults = { + autoScroll: true, + minIntervalToShowNextCell: 30, + maxIntervalToShowNextCell: 600, // better to a multiple of minIntervalToShowNextCell + accelerateInterval: 5, // increase 5ms when cursor 1px outside the viewport. selectionCss: { border: '2px dashed blue' } } as CellRangeSelectorOption; pluginName = 'CellRangeSelector'; - onBeforeCellRangeSelected = new Slick.Event(); + onBeforeCellRangeSelected = new Slick.Event<{ row: number; cell: number; }>(); + onCellRangeSelecting = new Slick.Event<{ range: CellRange; }>(); onCellRangeSelected = new Slick.Event<{ range: CellRange; }>(); constructor(options?: Partial) { @@ -72,14 +99,17 @@ export class SlickCellRangeSelector { .subscribe(this._grid.onScroll, this.handleScroll.bind(this) as EventListener); } + destroy() { + this.dispose(); + } + /** Dispose the plugin. */ dispose() { this._eventHandler?.unsubscribeAll(); emptyElement(this._activeCanvas); emptyElement(this._canvas); - if (this._decorator?.dispose) { - this._decorator.dispose(); - } + this._decorator?.dispose(); + this.stopIntervalTimer(); } getCellDecorator() { @@ -90,58 +120,206 @@ export class SlickCellRangeSelector { return this._currentlySelectedRange; } + getMouseOffsetViewport(e: SlickEventData, dd: DragPosition): MouseOffsetViewport { + const viewportLeft = this._activeViewport.scrollLeft; + const viewportTop = this._activeViewport.scrollTop; + const viewportRight = viewportLeft + this._viewportWidth; + const viewportBottom = viewportTop + this._viewportHeight; + + const viewportOffset = getHtmlElementOffset(this._activeViewport); + const viewportOffsetLeft = viewportOffset?.left ?? 0; + const viewportOffsetTop = viewportOffset?.top ?? 0; + const viewportOffsetRight = viewportOffsetLeft + this._viewportWidth; + const viewportOffsetBottom = viewportOffsetTop + this._viewportHeight; + + const result = { + e, + dd, + viewport: { + left: viewportLeft, top: viewportTop, right: viewportRight, bottom: viewportBottom, + offset: { left: viewportOffsetLeft, top: viewportOffsetTop, right: viewportOffsetRight, bottom: viewportOffsetBottom } + }, + // Consider the viewport as the origin, the `offset` is based on the coordinate system: + // the cursor is on the viewport's left/bottom when it is less than 0, and on the right/top when greater than 0. + offset: { x: 0, y: 0 }, + isOutsideViewport: false + }; + + // ... horizontal + if (e.pageX < viewportOffsetLeft) { + result.offset.x = e.pageX - viewportOffsetLeft; + } else if (e.pageX > viewportOffsetRight) { + result.offset.x = e.pageX - viewportOffsetRight; + } + // ... vertical + if (e.pageY < viewportOffsetTop) { + result.offset.y = viewportOffsetTop - e.pageY; + } else if (e.pageY > viewportOffsetBottom) { + result.offset.y = viewportOffsetBottom - e.pageY; + } + result.isOutsideViewport = !!result.offset.x || !!result.offset.y; + return result; + } + + stopIntervalTimer() { + if (this._autoScrollTimerId) { + clearInterval(this._autoScrollTimerId); + this._autoScrollTimerId = undefined; + } + } + // // protected functions // --------------------- - protected handleDrag(e: any, dd: DragPosition) { + protected handleDrag(e: SlickEventData, dd: DragPosition) { if (!this._dragging) { return; } e.stopImmediatePropagation(); + if (this.addonOptions.autoScroll) { + this._draggingMouseOffset = this.getMouseOffsetViewport(e, dd); + if (this._draggingMouseOffset.isOutsideViewport) { + return this.handleDragOutsideViewport(); + } + } + this.stopIntervalTimer(); + this.handleDragTo(e, dd); + } + + protected handleDragOutsideViewport() { + this._xDelayForNextCell = this.addonOptions.maxIntervalToShowNextCell - Math.abs(this._draggingMouseOffset.offset.x) * this.addonOptions.accelerateInterval; + this._yDelayForNextCell = this.addonOptions.maxIntervalToShowNextCell - Math.abs(this._draggingMouseOffset.offset.y) * this.addonOptions.accelerateInterval; + + // only one timer is created to handle the case that cursor outside the viewport + if (!this._autoScrollTimerId) { + let xTotalDelay = 0; + let yTotalDelay = 0; + + this._autoScrollTimerId = setInterval(() => { + let xNeedUpdate = false; + let yNeedUpdate = false; + // ... horizontal + if (this._draggingMouseOffset.offset.x) { + xTotalDelay += this.addonOptions.minIntervalToShowNextCell; + xNeedUpdate = xTotalDelay >= this._xDelayForNextCell; + } else { + xTotalDelay = 0; + } + // ... vertical + if (this._draggingMouseOffset.offset.y) { + yTotalDelay += this.addonOptions.minIntervalToShowNextCell; + yNeedUpdate = yTotalDelay >= this._yDelayForNextCell; + } else { + yTotalDelay = 0; + } + if (xNeedUpdate || yNeedUpdate) { + if (xNeedUpdate) { + xTotalDelay = 0; + } + if (yNeedUpdate) { + yTotalDelay = 0; + } + this.handleDragToNewPosition(xNeedUpdate, yNeedUpdate); + } + }, this.addonOptions.minIntervalToShowNextCell); + } + } + + protected handleDragToNewPosition(xNeedUpdate: boolean, yNeedUpdate: boolean) { + let pageX = this._draggingMouseOffset.e.pageX; + let pageY = this._draggingMouseOffset.e.pageY; + const mouseOffsetX = this._draggingMouseOffset.offset.x; + const mouseOffsetY = this._draggingMouseOffset.offset.y; + const viewportOffset = this._draggingMouseOffset.viewport.offset; + // ... horizontal + if (xNeedUpdate && mouseOffsetX) { + if (mouseOffsetX > 0) { + pageX = viewportOffset.right + this._moveDistanceForOneCell.x; + } else { + pageX = viewportOffset.left - this._moveDistanceForOneCell.x; + } + } + // ... vertical + if (yNeedUpdate && mouseOffsetY) { + if (mouseOffsetY > 0) { + pageY = viewportOffset.top - this._moveDistanceForOneCell.y; + } else { + pageY = viewportOffset.bottom + this._moveDistanceForOneCell.y; + } + } + this.handleDragTo({ pageX, pageY }, this._draggingMouseOffset.dd); + } + + protected handleDragTo(e: { pageX: number; pageY: number; }, dd: DragPosition) { const end = this._grid.getCellFromPoint( e.pageX - (getHtmlElementOffset(this._activeCanvas)?.left ?? 0) + this._columnOffset, e.pageY - (getHtmlElementOffset(this._activeCanvas)?.top ?? 0) + this._rowOffset ); - // ... frozen column(s), - if (this._gridOptions.frozenColumn! >= 0 && ((!this._isRightCanvas && (end.cell > this._gridOptions.frozenColumn!)) || (this._isRightCanvas && (end.cell <= this._gridOptions.frozenColumn!)))) { - return; - } + if (end !== undefined) { + // ... frozen column(s), + if (this._gridOptions.frozenColumn! >= 0 && ((!this._isRightCanvas && (end.cell > this._gridOptions.frozenColumn!)) || (this._isRightCanvas && (end.cell <= this._gridOptions.frozenColumn!)))) { + return; + } - // ... or frozen row(s) - if (this._gridOptions.frozenRow! >= 0 && ((!this._isBottomCanvas && (end.row >= this._gridOptions.frozenRow!)) || (this._isBottomCanvas && (end.row < this._gridOptions.frozenRow!)))) { - return; - } + // ... or frozen row(s) + if (this._gridOptions.frozenRow! >= 0 && ((!this._isBottomCanvas && (end.row >= this._gridOptions.frozenRow!)) || (this._isBottomCanvas && (end.row < this._gridOptions.frozenRow!)))) { + return; + } - // ... or regular grid (without any frozen options) - if (!this._grid.canCellBeSelected(end.row, end.cell)) { - return; - } + // scrolling the viewport to display the target `end` cell if it is not fully displayed + if (this.addonOptions.autoScroll && this._draggingMouseOffset) { + const endCellBox = this._grid.getCellNodeBox(end.row, end.cell); + if (endCellBox) { + const viewport = this._draggingMouseOffset.viewport; + if (endCellBox.left < viewport.left || endCellBox.right > viewport.right + || endCellBox.top < viewport.top || endCellBox.bottom > viewport.bottom) { + this._grid.scrollCellIntoView(end.row, end.cell); + } + } + } - dd.range.end = end; - this._decorator.show(new Slick.Range(dd.range.start.row, dd.range.start.cell, end.row, end.cell)); + // ... or regular grid (without any frozen options) + if (!this._grid.canCellBeSelected(end.row, end.cell)) { + return; + } + + dd.range.end = end; + const range = new Slick.Range(dd.range.start.row, dd.range.start.cell, end.row, end.cell); + this._decorator.show(range); + this.onCellRangeSelecting.notify({ range }); + } } protected handleDragEnd(e: any, dd: DragPosition) { - if (!this._dragging) { - return; + if (this._dragging) { + this._dragging = false; + e.stopImmediatePropagation(); + + this.stopIntervalTimer(); + this._decorator.hide(); + this.onCellRangeSelected.notify({ + range: new Slick.Range(dd.range.start.row, dd.range.start.cell, dd.range.end.row, dd.range.end.cell) + }); } - - this._dragging = false; - e.stopImmediatePropagation(); - - this._decorator.hide(); - this.onCellRangeSelected.notify({ - range: new Slick.Range(dd.range.start.row, dd.range.start.cell, dd.range.end.row, dd.range.end.cell) - }); } protected handleDragInit(e: any) { // Set the active canvas node because the decorator needs to append its // box to the correct canvas this._activeCanvas = this._grid.getActiveCanvasNode(e); + this._activeViewport = this._grid.getActiveViewportNode(e); + + const scrollbarDimensions = this._grid.getDisplayedScrollbarDimensions(); + this._viewportWidth = this._activeViewport.offsetWidth - scrollbarDimensions.width; + this._viewportHeight = this._activeViewport.offsetHeight - scrollbarDimensions.height; + + this._moveDistanceForOneCell = { + x: this._grid.getAbsoluteColumnMinWidth() / 2, + y: this._gridOptions.rowHeight! / 2 + }; this._rowOffset = 0; this._columnOffset = 0; @@ -164,12 +342,11 @@ export class SlickCellRangeSelector { protected handleDragStart(e: DOMMouseEvent, dd: DragPosition) { const cellObj = this._grid.getCellFromEvent(e); - if (this.onBeforeCellRangeSelected.notify(cellObj) !== false) { - if (cellObj && this._grid.canCellBeSelected(cellObj.row, cellObj.cell)) { - this._dragging = true; - e.stopImmediatePropagation(); - } + if (cellObj && this.onBeforeCellRangeSelected.notify(cellObj) !== false && this._grid.canCellBeSelected(cellObj.row, cellObj.cell)) { + this._dragging = true; + e.stopImmediatePropagation(); } + if (!this._dragging) { return; } diff --git a/packages/common/src/extensions/slickCellSelectionModel.ts b/packages/common/src/extensions/slickCellSelectionModel.ts index fae4ecc5a..cb426dec0 100644 --- a/packages/common/src/extensions/slickCellSelectionModel.ts +++ b/packages/common/src/extensions/slickCellSelectionModel.ts @@ -6,7 +6,7 @@ import { SlickCellRangeSelector } from './index'; declare const Slick: SlickNamespace; export interface CellSelectionModelOption { - selectActiveCell: boolean; + selectActiveCell?: boolean; cellRangeSelector: SlickCellRangeSelector; } @@ -20,13 +20,13 @@ export class SlickCellSelectionModel { protected _defaults = { selectActiveCell: true, }; - onSelectedRangesChanged = new Slick.Event(); + onSelectedRangesChanged = new Slick.Event(); pluginName = 'CellSelectionModel'; constructor(options?: { selectActiveCell: boolean; cellRangeSelector: SlickCellRangeSelector; }) { this._eventHandler = new Slick.EventHandler(); if (options === undefined || options.cellRangeSelector === undefined) { - this._selector = new SlickCellRangeSelector({ selectionCss: { border: '2px solid black' } as unknown as CSSStyleDeclaration }); + this._selector = new SlickCellRangeSelector({ selectionCss: { border: '2px solid black' } as CSSStyleDeclaration }); } else { this._selector = options.cellRangeSelector; } @@ -64,8 +64,16 @@ export class SlickCellSelectionModel { this._canvas = this._grid.getCanvasNode(); } + destroy() { + this.dispose(); + } + 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); + } this._eventHandler.unsubscribeAll(); this._grid?.unregisterPlugin(this._selector); this._selector?.dispose(); diff --git a/packages/common/src/extensions/slickRowSelectionModel.ts b/packages/common/src/extensions/slickRowSelectionModel.ts index e4a5a2354..5ca5351c7 100644 --- a/packages/common/src/extensions/slickRowSelectionModel.ts +++ b/packages/common/src/extensions/slickRowSelectionModel.ts @@ -1,5 +1,15 @@ import { KeyCode } from '../enums/index'; -import { CellRange, OnActiveCellChangedEventArgs, RowSelectionModelOption, SlickEventHandler, SlickGrid, SlickNamespace, } from '../interfaces/index'; +import { + CellRange, + GridOption, + OnActiveCellChangedEventArgs, + RowSelectionModelOption, + SlickEventData, + SlickEventHandler, + SlickGrid, + SlickNamespace, +} from '../interfaces/index'; +import { SlickCellRangeSelector } from '../extensions/slickCellRangeSelector'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; @@ -9,7 +19,9 @@ export class SlickRowSelectionModel { protected _eventHandler: SlickEventHandler; protected _grid!: SlickGrid; protected _ranges: CellRange[] = []; + protected _selector?: SlickCellRangeSelector; protected _defaults = { + cellRangeSelector: undefined, selectActiveRow: true } as RowSelectionModelOption; pluginName = 'RowSelectionModel'; @@ -30,19 +42,51 @@ export class SlickRowSelectionModel { return this._eventHandler; } + get gridOptions(): GridOption { + return this._grid.getOptions(); + } init(grid: SlickGrid) { this._grid = grid; this._addonOptions = { ...this._defaults, ...this._addonOptions }; + this._selector = this.addonOptions.cellRangeSelector; this._eventHandler - .subscribe(this._grid.onActiveCellChanged, this.handleActiveCellChange.bind(this) as EventListener) - .subscribe(this._grid.onClick, this.handleClick.bind(this) as EventListener) - .subscribe(this._grid.onKeyDown, this.handleKeyDown.bind(this) as EventListener); + .subscribe(this._grid.onActiveCellChanged, this.handleActiveCellChange.bind(this)) + .subscribe(this._grid.onClick, this.handleClick.bind(this)) + .subscribe(this._grid.onKeyDown, this.handleKeyDown.bind(this)); + + if (this._selector) { + this._grid.registerPlugin(this._selector); + this._eventHandler + .subscribe(this._selector.onCellRangeSelecting, this.handleCellRangeSelected.bind(this) as EventListener) + .subscribe(this._selector.onCellRangeSelected, this.handleCellRangeSelected.bind(this) as EventListener) + .subscribe(this._selector.onBeforeCellRangeSelected, this.handleBeforeCellRangeSelected.bind(this) as EventListener); + } + } + + destroy() { + this.dispose(); } dispose() { this._eventHandler.unsubscribeAll(); + this.disposeSelector(); + } + + disposeSelector() { + if (this._selector) { + this._selector.onCellRangeSelecting.unsubscribe(this.handleCellRangeSelected.bind(this) as EventListener); + this._selector.onCellRangeSelected.unsubscribe(this.handleCellRangeSelected.bind(this) as EventListener); + this._selector.onBeforeCellRangeSelected.unsubscribe(this.handleBeforeCellRangeSelected.bind(this) as EventListener); + this._grid.unregisterPlugin(this._selector); + this._selector?.destroy(); + this._selector?.dispose(); + } + } + + getCellRangeSelector() { + return this._selector; } getSelectedRanges() { @@ -84,19 +128,34 @@ export class SlickRowSelectionModel { return rows; } - protected handleActiveCellChange(_e: any, args: OnActiveCellChangedEventArgs) { + protected handleBeforeCellRangeSelected(e: SlickEventData, cell: { row: number; cell: number; }): boolean | void { + if (this._grid.getEditorLock().isActive()) { + e.stopPropagation(); + return false; + } + this._grid.setActiveCell(cell.row, cell.cell); + } + + protected handleCellRangeSelected(_e: SlickEventData, args: { range: CellRange; }): boolean | void { + if (!this.gridOptions.multiSelect || !this.addonOptions.selectActiveRow) { + return false; + } + this.setSelectedRanges([new Slick.Range(args.range.fromRow, 0, args.range.toRow, this._grid.getColumns().length - 1)]); + } + + protected handleActiveCellChange(_e: SlickEventData, args: OnActiveCellChangedEventArgs) { if (this._addonOptions.selectActiveRow && args.row !== null) { this.setSelectedRanges([new Slick.Range(args.row, 0, args.row, this._grid.getColumns().length - 1)]); } } - protected handleClick(e: any): boolean | void { + protected handleClick(e: SlickEventData): boolean | void { const cell = this._grid.getCellFromEvent(e); if (!cell || !this._grid.canCellBeActive(cell.row, cell.cell)) { return false; } - if (!this._grid.getOptions().multiSelect || ( + if (!this.gridOptions.multiSelect || ( !e.ctrlKey && !e.shiftKey && !e.metaKey)) { return false; } @@ -131,10 +190,10 @@ export class SlickRowSelectionModel { return true; } - protected handleKeyDown(e: any) { + protected handleKeyDown(e: SlickEventData) { const activeRow = this._grid.getActiveCell(); - if (this._grid.getOptions().multiSelect && activeRow && + if (this.gridOptions.multiSelect && activeRow && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && (e.which === KeyCode.UP || e.key === 'ArrowUp' || e.which === KeyCode.DOWN || e.key === 'ArrowDown') ) { diff --git a/packages/common/src/interfaces/cellRange.interface.ts b/packages/common/src/interfaces/cellRange.interface.ts index fbf0e8cc3..70bc60419 100644 --- a/packages/common/src/interfaces/cellRange.interface.ts +++ b/packages/common/src/interfaces/cellRange.interface.ts @@ -21,7 +21,22 @@ export interface CellRangeDecoratorOption { } export interface CellRangeSelectorOption { + /** Defaults to True, should we enable auto-scroll? */ + autoScroll?: boolean; + + /** minimum internal to show the next cell? better to a multiple of minIntervalToShowNextCell */ + minIntervalToShowNextCell: number; + + /** maximum internal to show the next cell? better to a multiple of minIntervalToShowNextCell */ + maxIntervalToShowNextCell: number; + + /** how fast do we want to accelerate the interval of auto-scroll? increase 5ms when cursor 1px outside the viewport. */ + accelerateInterval: number; + + /** cell decorator service */ cellDecorator: SlickCellRangeDecorator; + + /** styling (for example blue background on cell) */ selectionCss: CSSStyleDeclaration; } diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index c33512c79..a88c29d19 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -114,6 +114,7 @@ export * from './menuOptionItem.interface'; export * from './menuOptionItemCallbackArgs.interface'; export * from './metrics.interface'; export * from './metricTexts.interface'; +export * from './mouseOffsetViewport.interface'; export * from './multiColumnSort.interface'; export * from './multipleSelectOption.interface'; export * from './onEventArgs.interface'; diff --git a/packages/common/src/interfaces/mouseOffsetViewport.interface.ts b/packages/common/src/interfaces/mouseOffsetViewport.interface.ts new file mode 100644 index 000000000..a019c4a8b --- /dev/null +++ b/packages/common/src/interfaces/mouseOffsetViewport.interface.ts @@ -0,0 +1,25 @@ +import { DragPosition } from './drag.interface'; + +export interface MouseOffsetViewport { + e: any, + dd: DragPosition, + viewport: { + left: number; + top: number; + right: number; + bottom: number; + offset: { + left: number; + top: number; + right: number; + bottom: number; + } + }, + // Consider the viewport as the origin, the `offset` is based on the coordinate system: + // the cursor is on the viewport's left/bottom when it is less than 0, and on the right/top when greater than 0. + offset: { + x: number; + y: number; + }, + isOutsideViewport?: boolean; +} \ No newline at end of file diff --git a/packages/common/src/interfaces/rowSelectionModelOption.interface.ts b/packages/common/src/interfaces/rowSelectionModelOption.interface.ts index dd9e5aa9c..824cb8b6c 100644 --- a/packages/common/src/interfaces/rowSelectionModelOption.interface.ts +++ b/packages/common/src/interfaces/rowSelectionModelOption.interface.ts @@ -1,4 +1,9 @@ +import { SlickCellRangeSelector } from '../extensions/slickCellRangeSelector'; + export type RowSelectionModelOption = { + /** cell range selector */ + cellRangeSelector?: SlickCellRangeSelector; + /** defaults to True, do we want to select the active row? */ selectActiveRow?: boolean; }; diff --git a/packages/common/src/interfaces/slickGrid.interface.ts b/packages/common/src/interfaces/slickGrid.interface.ts index 866eb6f1c..6df5c7f2c 100644 --- a/packages/common/src/interfaces/slickGrid.interface.ts +++ b/packages/common/src/interfaces/slickGrid.interface.ts @@ -88,6 +88,9 @@ export interface SlickGrid { /** Set focus */ focus(): void; + /** Get the absolute column minimum width */ + getAbsoluteColumnMinWidth(): number; + /** Get the canvas DOM element */ getActiveCanvasNode(element?: HTMLElement | JQuery): HTMLElement; @@ -108,7 +111,10 @@ export interface SlickGrid { getActiveCellPosition(): ElementPosition; /** Get the active Viewport DOM node element */ - getActiveViewportNode(): HTMLDivElement; + getActiveViewportNode(elm?: HTMLElement): HTMLDivElement; + + /** Get the displayed scrollbar dimensions */ + getDisplayedScrollbarDimensions(): { height: number; width: number; } /** Get the canvas DOM element */ getCanvases(): HTMLDivElement; @@ -351,7 +357,7 @@ export interface SlickGrid { resizeCanvas(): void; /** Scroll to a specific cell and make it into the view */ - scrollCellIntoView(row: number, cell: number, doPaging: boolean): void; + scrollCellIntoView(row: number, cell: number, doPaging?: boolean): void; /** Scroll to a specific column and show it into the viewport */ scrollColumnIntoView(cell: number): void; From eacad7cda3916a68ff7bed62cb131ca472e07d7a Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 20 Dec 2021 02:57:56 -0500 Subject: [PATCH 2/3] tests: add Cypress E2E tests for auto-scroll --- .../src/examples/example17.html | 20 +- .../src/examples/example17.scss | 7 +- .../src/examples/example17.ts | 111 ++++--- test/cypress/integration/example17.spec.js | 302 ++++++++++++++++++ test/cypress/support/commands.js | 13 + test/cypress/support/common.js | 48 +++ test/cypress/support/drag.js | 66 ++++ 7 files changed, 522 insertions(+), 45 deletions(-) create mode 100644 test/cypress/integration/example17.spec.js create mode 100644 test/cypress/support/common.js create mode 100644 test/cypress/support/drag.js diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example17.html b/examples/webpack-demo-vanilla-bundle/src/examples/example17.html index b8bef5fe7..d33a7b222 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example17.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example17.html @@ -2,13 +2,16 @@

Example 17 - Auto-Scroll with Range Selector (with Salesforce Theme)

+
+ Dragging from 2nd column to make a selection, and the viewport will auto scroll. +
-
+
@@ -24,7 +27,7 @@

Example 17 - Auto-Scroll with Range Selector - +

-
Grid 1 - Using "SlickCellRangeSelector"
-
+
Grid 1 - Using SlickCellRangeSelector
+

-
Grid 2 - Using "SlickRowSelectionModel"
-
+
Grid 2 - Using SlickCellRangeSelector and SlickRowSelectionModel
+
\ No newline at end of file diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example17.scss b/examples/webpack-demo-vanilla-bundle/src/examples/example17.scss index 27c82ee73..86735a02b 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example17.scss +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example17.scss @@ -1,6 +1,11 @@ $control-height: 2.4em; @import 'bulma/bulma'; +:root { + --slick-header-row-count: 1; + --slick-header-column-height: 17px; +} + .scroll-configs input { width: 50px; } @@ -17,7 +22,7 @@ $control-height: 2.4em; font-style: italic; } .slick-row:not(.slick-group) >.cell-unselectable { - background: #efefef; + background: #ececec !important; } .slick-row .slick-cell.frozen:last-child, .slick-header-column.frozen:last-child, diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example17.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example17.ts index ade978397..2db5e4bde 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example17.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example17.ts @@ -1,4 +1,4 @@ -import { Column, Formatters, GridOption, SlickCellRangeSelector, SlickCellSelectionModel, SlickRowSelectionModel } from '@slickgrid-universal/common'; +import { Aggregators, Column, Formatters, GridOption, Grouping, GroupTotalFormatters, SlickCellRangeSelector, SlickCellSelectionModel, SlickRowSelectionModel } from '@slickgrid-universal/common'; import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; import { ExampleGridOptions } from './example-grid-options'; @@ -7,7 +7,7 @@ import '../material-styles.scss'; // import '../salesforce-styles.scss'; import './example17.scss'; -const NB_ITEMS = 995; +const NB_ITEMS = 300; export class Example17 { gridOptions1: GridOption; @@ -30,8 +30,8 @@ export class Example17 { this.dataset1 = this.mockData(NB_ITEMS); this.dataset2 = this.mockData(NB_ITEMS); - this.sgb1 = new Slicker.GridBundle(document.querySelector(`.grid1`), this.columnDefinitions1, { ...ExampleGridOptions, ...this.gridOptions1 }, this.dataset1); - this.sgb2 = new Slicker.GridBundle(document.querySelector(`.grid2`), this.columnDefinitions2, { ...ExampleGridOptions, ...this.gridOptions2 }, this.dataset2); + this.sgb1 = new Slicker.GridBundle(document.querySelector(`.grid17-1`), this.columnDefinitions1, { ...ExampleGridOptions, ...this.gridOptions1 }, this.dataset1); + this.sgb2 = new Slicker.GridBundle(document.querySelector(`.grid17-2`), this.columnDefinitions2, { ...ExampleGridOptions, ...this.gridOptions2 }, this.dataset2); this.setOptions(); } @@ -44,36 +44,46 @@ export class Example17 { /* Define grid Options and Columns */ defineGrids() { this.columnDefinitions1 = [ - { id: 'title', name: 'Title', field: 'title', sortable: true, minWidth: 100, filterable: true }, - { id: 'duration', name: 'Duration', field: 'duration', sortable: true, minWidth: 100, filterable: true }, - { id: '%', name: '% Complete', field: 'percentComplete', sortable: true, minWidth: 100, filterable: true, formatter: Formatters.percentCompleteBar }, - { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, minWidth: 120, exportWithFormatter: true, filterable: true }, - { id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, minWidth: 120, exportWithFormatter: true, filterable: true }, - { id: 'cost', name: 'Cost', field: 'cost', formatter: Formatters.dollar, minWidth: 75, exportWithFormatter: true, filterable: true }, - { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', formatter: Formatters.checkmarkMaterial, sortable: true, minWidth: 75, filterable: true } + { id: 'sel', name: '#', field: 'id', cssClass: 'cell-unselectable', resizable: false, selectable: false, focusable: false, width: 40 }, + { id: 'title', name: 'Title', field: 'title', cssClass: 'cell-title', sortable: true, width: 90, filterable: true }, + { id: 'duration', name: 'Duration', field: 'duration', width: 90, sortable: true, filterable: true, groupTotalsFormatter: GroupTotalFormatters.sumTotals }, + { id: '%', name: '% Complete', field: 'percentComplete', width: 90, sortable: true, filterable: true, formatter: Formatters.percentCompleteBar }, + { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, width: 90, exportWithFormatter: true, filterable: true }, + { id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, width: 90, exportWithFormatter: true, filterable: true }, + { id: 'cost', name: 'Cost', field: 'cost', formatter: Formatters.dollar, width: 90, exportWithFormatter: true, filterable: true }, + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', cssClass: 'cell-effort-driven', width: 90, formatter: Formatters.checkmarkMaterial, sortable: true, filterable: true } ]; for (let i = 0; i < 30; i++) { - this.columnDefinitions1.push({ id: `mock${i}`, name: `Mock${i}`, field: `mock${i}`, minWidth: 75 }); + this.columnDefinitions1.push({ id: `mock${i}`, name: `Mock${i}`, field: `mock${i}`, width: 90 }); } this.gridOptions1 = { enableAutoResize: false, + enableAutoSizeColumns: false, + autoFitColumnsOnFirstLoad: false, + autosizeColumnsByCellContentOnFirstLoad: true, + enableAutoResizeColumnsByCellContent: true, enableCellNavigation: true, - gridHeight: 225, + enableColumnReorder: false, + editable: true, + asyncEditorLoading: false, + autoEdit: false, + enableGrouping: true, + gridHeight: 350, gridWidth: 800, - rowHeight: 33, + rowHeight: 35, + frozenColumn: -1, + frozenRow: -1, // enableExcelCopyBuffer: true, }; // copy the same Grid Options and Column Definitions to 2nd grid - this.columnDefinitions2 = this.columnDefinitions1; + this.columnDefinitions2 = this.columnDefinitions1.slice(); this.gridOptions2 = { ...this.gridOptions1, ...{ - enableCheckboxSelector: true, - // enableExcelCopyBuffer: false, - gridHeight: 255, + // enableCheckboxSelector: true, } }; } @@ -82,18 +92,14 @@ export class Example17 { // mock a dataset const mockDataset = []; for (let i = 0; i < count; i++) { - const randomYear = 2000 + Math.floor(Math.random() * 10); - const randomMonth = Math.floor(Math.random() * 11); - const randomDay = Math.floor((Math.random() * 29)); - const randomPercent = Math.round(Math.random() * 100); - + const someDates = ['2009-01-01', '2009-02-02', '2009-03-03']; mockDataset[i] = { id: i, title: 'Task ' + i, - duration: Math.round(Math.random() * 100) + '', - percentComplete: randomPercent, - start: new Date(randomYear, randomMonth + 1, randomDay), - finish: new Date(randomYear + 1, randomMonth + 1, randomDay), + duration: i % 20, + percentComplete: Math.round(Math.random() * 100), + start: someDates[Math.floor((Math.random() * 2))], + finish: someDates[Math.floor((Math.random() * 2))], cost: Math.round(Math.random() * 10000) / 100, effortDriven: (i % 5 === 0) }; @@ -105,16 +111,30 @@ export class Example17 { return mockDataset; } - toggleFrozen() { - const option = this.sgb1.slickGrid.getOptions(); - const frozenRow = option.frozenRow; - const frozenColumn = option.frozenColumn; - const newOption = { - frozenColumn: frozenColumn === -1 ? 1 : -1, - frozenRow: frozenRow === -1 ? 3 : -1 - }; - this.sgb1.slickGrid.setOptions(newOption); - this.sgb2.slickGrid.setOptions(newOption); + groupByDuration1() { + this.sgb1.dataView.setGrouping({ + getter: 'duration', + formatter: (g) => `Duration: ${g.value} (${g.count} items)`, + aggregators: [ + new Aggregators.Avg('percentComplete'), + new Aggregators.Sum('cost') + ], + aggregateCollapsed: false, + lazyTotalsCalculation: true + } as Grouping); + } + + groupByDuration2() { + this.sgb2.dataView.setGrouping({ + getter: 'duration', + formatter: (g) => `Duration: ${g.value} (${g.count} items)`, + aggregators: [ + new Aggregators.Avg('percentComplete'), + new Aggregators.Sum('cost') + ], + aggregateCollapsed: false, + lazyTotalsCalculation: true + } as Grouping); } setDefaultOptions() { @@ -153,4 +173,21 @@ export class Example17 { this.sgb1.slickGrid.invalidate(); this.sgb2.slickGrid.invalidate(); } + + toggleGroup() { + (this.sgb1.dataView.getGrouping() && this.sgb1.dataView.getGrouping().length > 0) ? this.sgb1.dataView.setGrouping([]) : this.groupByDuration1(); + (this.sgb2.dataView.getGrouping() && this.sgb2.dataView.getGrouping().length > 0) ? this.sgb2.dataView.setGrouping([]) : this.groupByDuration2(); + } + + toggleFrozen() { + const option = this.sgb1.slickGrid.getOptions(); + const frozenRow = option.frozenRow; + const frozenColumn = option.frozenColumn; + const newOption = { + frozenColumn: frozenColumn === -1 ? 1 : -1, + frozenRow: frozenRow === -1 ? 3 : -1 + }; + this.sgb1.slickGrid.setOptions(newOption); + this.sgb2.slickGrid.setOptions(newOption); + } } diff --git a/test/cypress/integration/example17.spec.js b/test/cypress/integration/example17.spec.js new file mode 100644 index 000000000..ec4cc2e49 --- /dev/null +++ b/test/cypress/integration/example17.spec.js @@ -0,0 +1,302 @@ +/// +import { getScrollDistanceWhenDragOutsideGrid } from '../support/drag'; + +describe('Example 17 - Auto-Scroll with Range Selector', { retries: 0 }, () => { + // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows + const CELL_WIDTH = 80; + const CELL_HEIGHT = 35; + const SCROLLBAR_DIMENSION = 17; + const fullTitles = ['#', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Cost', 'Effort Driven']; + for (let i = 0; i < 30; i++) { + fullTitles.push(`Mock${i}`); + } + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseExampleUrl')}/example17`); + cy.get('h3').should('contain', 'Example 17 - Auto-Scroll with Range Selector'); + cy.get('h3 span.subtitle').should('contain', '(with Salesforce Theme)'); + }); + + it('should have exact column titles on both grids', () => { + cy.get('.grid17-1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + + cy.get('.grid17-1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should select border shown in cell selection model, and hidden in row selection model when dragging', { scrollBehavior: false }, () => { + cy.getCell(0, 1, '', { parentSelector: '.grid17-1', rowHeight: CELL_HEIGHT }) + .as('cell1') + .dragStart(); + cy.get('.grid17-1 .slick-range-decorator').should('be.exist').and('have.css', 'border-color').and('not.equal', 'none'); + cy.get('@cell1') + .drag(0, 5) + .dragEnd('.grid17-1'); + cy.get('.grid17-1 .slick-range-decorator').should('not.be.exist'); + cy.get('.grid17-1 .slick-cell.selected').should('have.length', 6); + + cy.getCell(0, 1, '', { parentSelector: '.grid17-2', rowHeight: CELL_HEIGHT }) + .as('cell2') + .dragStart(); + cy.get('.grid17-2 .slick-range-decorator').should('be.exist').and('have.css', 'border-style').and('equal', 'none'); + cy.get('@cell2') + .drag(5, 1) + .dragEnd('.grid17-2'); + cy.get('.grid17-2 .slick-range-decorator').should('not.be.exist'); + cy.get('.grid17-2 .slick-row:nth-child(-n+6)') + .children(':not(.cell-unselectable)') + .each(($child) => expect($child.attr('class')).to.include('true')); + }) + + function testScroll() { + return getScrollDistanceWhenDragOutsideGrid('.grid17-1', 'topLeft', 'right', 0, 1).then(cellScrollDistance => { + return getScrollDistanceWhenDragOutsideGrid('.grid17-2', 'topLeft', 'bottom', 0, 1).then(rowScrollDistance => { + return cy.wrap({ + cell: { + scrollBefore: cellScrollDistance.scrollLeftBefore, + scrollAfter: cellScrollDistance.scrollLeftAfter + }, + row: { + scrollBefore: rowScrollDistance.scrollTopBefore, + scrollAfter: rowScrollDistance.scrollTopAfter + } + }); + }); + }); + } + + it('should auto scroll take effect to display the selecting element when dragging', { scrollBehavior: false }, () => { + testScroll().then(scrollDistance => { + expect(scrollDistance.cell.scrollBefore).to.be.lessThan(scrollDistance.cell.scrollAfter); + expect(scrollDistance.row.scrollBefore).to.be.lessThan(scrollDistance.row.scrollAfter); + }); + + cy.get('[data-test="is-autoscroll-chk"]').uncheck(); + cy.get('[data-test="set-options-btn"]').click(); + + testScroll().then(scrollDistance => { + expect(scrollDistance.cell.scrollBefore).to.be.equal(scrollDistance.cell.scrollAfter); + expect(scrollDistance.row.scrollBefore).to.be.equal(scrollDistance.row.scrollAfter); + }); + + cy.get('[data-test="default-options-btn"]').click(); + cy.get('[data-test="is-autoscroll-chk"]').should('have.value', 'on') + }); + + function getIntervalUntilRow12Displayed(selector, px, rowNumber = 12) { + const viewportSelector = (`${selector} .slick-viewport:first`); + cy.getCell(0, 1, '', { parentSelector: selector, rowHeight: CELL_HEIGHT }) + .dragStart(); + + return cy.get(viewportSelector).invoke('scrollTop').then(scrollBefore => { + cy.dragOutside('bottom', 0, px, { parentSelector: selector }); + + const start = performance.now(); + cy.get(`${selector} .slick-row:not(.slick-group) >.cell-unselectable`) + .contains(`${rowNumber}`, { timeout: 10000 }) // actually #8 will be selected + .should('not.be.hidden'); + + return cy.get(viewportSelector).invoke('scrollTop').then(scrollAfter => { + cy.dragEnd(selector); + var interval = performance.now() - start; + expect(scrollBefore).to.be.lte(scrollAfter); + cy.get(viewportSelector).scrollTo(0, 0, { ensureScrollable: false }); + return cy.wrap(interval); + }); + }); + } + + function testInterval(px, rowNumber) { + return getIntervalUntilRow12Displayed('.grid17-1', px, rowNumber).then(intervalCell => { + return getIntervalUntilRow12Displayed('.grid17-2', px, rowNumber).then(intervalRow => { + return cy.wrap({ + cell: intervalCell, + row: intervalRow + }); + }); + }); + } + + it('should MIN interval take effect when auto scroll: 30ms -> 90ms', { scrollBehavior: false }, () => { + // cy.get('[data-test="min-interval-input"]').type('{selectall}90'); // 30ms -> 90ms + // By default the MIN interval to show next cell is 30ms. + testInterval(CELL_HEIGHT * 10).then(defaultInterval => { + + // Setting the interval to 90ms (3 times of the default). + cy.get('[data-test="min-interval-input"]').type('{selectall}90'); // 30ms -> 90ms + cy.get('[data-test="set-options-btn"]').click(); + + // Ideally if we scrolling to same row by MIN interval, the used time should be 3 times slower than default. + // Considering the threshold, 1.2 times slower than default is expected + testInterval(CELL_HEIGHT * 10).then(newInterval => { + + // max scrolling speed is slower than before + expect(newInterval.cell).to.be.greaterThan(0.9 * defaultInterval.cell); + expect(newInterval.row).to.be.greaterThan(0.9 * defaultInterval.row); + + cy.get('[data-test="default-options-btn"]').click(); + cy.get('[data-test="min-interval-input"]').should('have.value', '30'); + }); + }); + }); + + it('should MAX interval take effect when auto scroll: 600ms -> 200ms', { scrollBehavior: false }, () => { + // By default the MAX interval to show next cell is 600ms. + testInterval(0, 9).then(defaultInterval => { + + // Setting the interval to 200ms (1/3 of the default). + cy.get('[data-test="max-interval-input"]').type('{selectall}200'); // 600ms -> 200ms + cy.get('[data-test="set-options-btn"]').click(); + + // Ideally if we scrolling to same row by MAX interval, the used time should be 3 times faster than default. + // Considering the threshold, 1.5 times faster than default is expected + testInterval(0, 9).then(newInterval => { + + // min scrolling speed is quicker than before + expect(0.9 * newInterval.cell).to.be.lessThan(defaultInterval.cell); + expect(0.9 * newInterval.row).to.be.lessThan(defaultInterval.row); + + cy.get('[data-test="default-options-btn"]').click(); + cy.get('[data-test="max-interval-input"]').should('have.value', '600'); + }); + }); + }); + + it('should Delay per Px take effect when auto scroll: 5ms/px -> 50ms/px', { scrollBehavior: false }, () => { + // By default the Delay per Px is 5ms/px. + testInterval(SCROLLBAR_DIMENSION).then(defaultInterval => { + + // Setting to 50ms/px (10 times of the default). + cy.get('[data-test="delay-cursor-input"]').type('{selectall}50'); // 5ms/px -> 50ms/px + cy.get('[data-test="set-options-btn"]').click(); + + // Ideally if we scrolling to same row, and set cursor to 17px, the new interval will be set to MIN interval (Math.max(30, 600 - 50 * 17) = 30ms), + // and the used time should be around 17 times faster than default. + // Considering the threshold, 5 times faster than default is expected + testInterval(SCROLLBAR_DIMENSION).then(newInterval => { + + // scrolling speed is quicker than before + expect(3.2 * newInterval.cell).to.be.lessThan(defaultInterval.cell); + expect(3.2 * newInterval.row).to.be.lessThan(defaultInterval.row); + + cy.get('[data-test="default-options-btn"]').click(); + cy.get('[data-test="delay-cursor-input"]').should('have.value', '5'); + }); + }); + }); + + it('should have a frozen grid with 4 containers with 2 columns on the left and 3 rows on the top after click Set/Clear Frozen button', () => { + cy.get(`.grid17-1 [style="top:0px"]`).should('have.length', 1); + cy.get(`.grid17-2 [style="top:0px"]`).should('have.length', 1); + + cy.get('[data-test="set-clear-frozen-btn"]').click(); + + cy.get(`.grid17-1 [style="top:0px"]`).should('have.length', 2 * 2); + cy.get(`.grid17-2 [style="top:0px"]`).should('have.length', 2 * 2); + cy.get(`.grid17-1 .grid-canvas-left > [style="top:0px"]`).children().should('have.length', 2 * 2); + cy.get(`.grid17-2 .grid-canvas-left > [style="top:0px"]`).children().should('have.length', 2 * 2); + cy.get('.grid17-1 .grid-canvas-top').children().should('have.length', 3 * 2 + 1); // +1 for "Empty Data" div + cy.get('.grid17-2 .grid-canvas-top').children().should('have.length', 3 * 2 + 1); + }); + + function resetScrollInFrozen() { + cy.get('.grid17-1 .slick-viewport:last').scrollTo(0, 0); + cy.get('.grid17-2 .slick-viewport:last').scrollTo(0, 0); + } + + it('should auto scroll to display the selecting element when dragging in frozen grid', { scrollBehavior: false }, () => { + // top left - to bottomRight + getScrollDistanceWhenDragOutsideGrid('.grid17-1', 'topLeft', 'bottomRight', 0, 1).then(result => { + expect(result.scrollTopBefore).to.be.equal(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.equal(result.scrollLeftAfter); + }); + getScrollDistanceWhenDragOutsideGrid('.grid17-2', 'topLeft', 'bottomRight', 0, 1).then(result => { + expect(result.scrollTopBefore).to.be.equal(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.equal(result.scrollLeftAfter); + }); + + // top right - to bottomRight + getScrollDistanceWhenDragOutsideGrid('.grid17-1', 'topRight', 'bottomRight', 0, 0).then(result => { + expect(result.scrollTopBefore).to.be.equal(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.lte(result.scrollLeftAfter); + }); + getScrollDistanceWhenDragOutsideGrid('.grid17-2', 'topRight', 'bottomRight', 0, 0).then(result => { + expect(result.scrollTopBefore).to.be.equal(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.lte(result.scrollLeftAfter); + }); + resetScrollInFrozen(); + + // bottom left - to bottomRight + getScrollDistanceWhenDragOutsideGrid('.grid17-1', 'bottomLeft', 'bottomRight', 0, 1).then(result => { + expect(result.scrollTopBefore).to.be.lessThan(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.equal(result.scrollLeftAfter); + }); + getScrollDistanceWhenDragOutsideGrid('.grid17-2', 'bottomLeft', 'bottomRight', 0, 1).then(result => { + expect(result.scrollTopBefore).to.be.lessThan(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.equal(result.scrollLeftAfter); + }); + resetScrollInFrozen(); + + // bottom right - to bottomRight + getScrollDistanceWhenDragOutsideGrid('.grid17-1', 'bottomRight', 'bottomRight', 0, 0).then(result => { + expect(result.scrollTopBefore).to.be.lessThan(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.lessThan(result.scrollLeftAfter); + }); + getScrollDistanceWhenDragOutsideGrid('.grid17-2', 'bottomRight', 'bottomRight', 0, 0).then(result => { + expect(result.scrollTopBefore).to.be.lessThan(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.lessThan(result.scrollLeftAfter); + }); + resetScrollInFrozen(); + cy.get('.grid17-1 .slick-viewport-bottom.slick-viewport-right').scrollTo(CELL_WIDTH * 3, CELL_HEIGHT * 3); + cy.get('.grid17-2 .slick-viewport-bottom.slick-viewport-right').scrollTo(CELL_WIDTH * 3, CELL_HEIGHT * 3); + + // bottom right - to topLeft + getScrollDistanceWhenDragOutsideGrid('.grid17-1', 'bottomRight', 'topLeft', 6, 4, 140).then(result => { + expect(result.scrollTopBefore).to.be.greaterThan(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.greaterThan(result.scrollLeftAfter); + }); + getScrollDistanceWhenDragOutsideGrid('.grid17-2', 'bottomRight', 'topLeft', 6, 4, 140).then(result => { + expect(result.scrollTopBefore).to.be.greaterThan(result.scrollTopAfter); + expect(result.scrollLeftBefore).to.be.greaterThan(result.scrollLeftAfter); + }); + resetScrollInFrozen(); + }); + + it('should have a frozen & grouping by Duration grid after click Set/Clear grouping by Duration button', { scrollBehavior: false }, () => { + cy.get('[data-test="set-clear-grouping-btn"]').trigger('click'); + cy.get(`.grid17-1 [style="top:0px"]`).should('have.length', 2 * 2); + cy.get(`.grid17-2 [style="top:0px"]`).should('have.length', 2 * 2); + cy.get('.grid17-1 .grid-canvas-top.grid-canvas-left').contains('Duration'); + cy.get('.grid17-2 .grid-canvas-top.grid-canvas-left').contains('Duration'); + }); + + function testDragInGrouping(selector) { + cy.getCell(7, 0, 'bottomRight', { parentSelector: selector, rowHeight: CELL_HEIGHT }) + .dragStart(); + cy.get(`${selector} .slick-viewport:last`).as('viewport').invoke('scrollTop').then(scrollBefore => { + cy.dragOutside('bottom', 400, CELL_HEIGHT * 12, { parentSelector: selector }); + cy.get('@viewport').invoke('scrollTop').then(scrollAfter => { + expect(scrollBefore).to.be.lessThan(scrollAfter); + cy.dragEnd(selector); + cy.get(`${selector} [style='top:${CELL_HEIGHT * 14}px'].slick-group`).should('exist'); + }); + }); + } + + it('should auto scroll to display the selecting element even unselectable cell exist in grouping grid', { scrollBehavior: false }, () => { + testDragInGrouping('.grid17-1'); + testDragInGrouping('.grid17-2'); + }); + + it('should reset to default grid when click Set/Clear Frozen button and Set/Clear grouping button', () => { + cy.get('[data-test="set-clear-frozen-btn"]').trigger('click'); + cy.get('[data-test="set-clear-grouping-btn"]').trigger('click'); + cy.get(`.grid17-1 [style="top:0px"]`).should('have.length', 1); + cy.get(`.grid17-2 [style="top:0px"]`).should('have.length', 1); + }); +}); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index ca4d256f3..af7299a75 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -23,3 +23,16 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +import { convertPosition } from './common'; + +// convert position like 'topLeft' to the object { x: 'left|right', y: 'top|bottom' } +Cypress.Commands.add("convertPosition", (viewport = 'topLeft') => cy.wrap(convertPosition(viewport))); + +Cypress.Commands.add("getCell", (row, col, viewport = 'topLeft', { parentSelector = '', rowHeight = 35 } = {}) => { + const position = convertPosition(viewport); + const canvasSelectorX = position.x ? `.grid-canvas-${position.x}` : ''; + const canvasSelectorY = position.y ? `.grid-canvas-${position.y}` : ''; + + return cy.get(`${parentSelector} ${canvasSelectorX}${canvasSelectorY} [style="top:${row * rowHeight}px"] > .slick-cell:nth(${col})`); +}); \ No newline at end of file diff --git a/test/cypress/support/common.js b/test/cypress/support/common.js new file mode 100644 index 000000000..03b4fb0bb --- /dev/null +++ b/test/cypress/support/common.js @@ -0,0 +1,48 @@ + +/** + * @typedef {('left'|'right')} xAllowed + * @typedef {('top'|'bottom')} yAllowed + * + * Define allowed input position + * @typedef {(xAllowed|yAllowed|'topLeft'|'topRight'|'bottomLeft'|'bottomRight')} AllowedInputPosition + * + * Define position object + * @typedef {Object} Position + * @property {xAllowed} x - horizontal + * @property {yAllowed} y - vertical + */ + +/** + * Enum for position values + * @constant + * @enum {(xAllowed|yAllowed)} + */ +const POSITION = Object.freeze({ + TOP: 'top', + BOTTOM: 'bottom', + LEFT: 'left', + RIGHT: 'right' +}) + +/** + * Convert position string to object + * + * @param {AllowedInputPosition} position + * @returns {Position} + */ +export function convertPosition(position) { + let x = ''; + let y = ''; + const _position = position.toLowerCase(); + if (_position.includes(POSITION.LEFT)) { + x = POSITION.LEFT; + } else if (_position.includes(POSITION.RIGHT)) { + x = POSITION.RIGHT; + } + if (_position.includes(POSITION.TOP)) { + y = POSITION.TOP; + } else if (_position.includes(POSITION.BOTTOM)) { + y = POSITION.BOTTOM; + } + return { x, y }; +} \ No newline at end of file diff --git a/test/cypress/support/drag.js b/test/cypress/support/drag.js new file mode 100644 index 000000000..5d88655dd --- /dev/null +++ b/test/cypress/support/drag.js @@ -0,0 +1,66 @@ +import { convertPosition } from './common'; + +Cypress.Commands.add("dragStart", { prevSubject: true }, (subject, { cellWidth = 90, cellHeight = 35 } = {}) => { + return cy.wrap(subject).click({ force: true }) + .trigger('mousedown', { which: 1 }, { force: true }) + .trigger('mousemove', cellWidth / 3, cellHeight / 3); +}) + +Cypress.Commands.add("drag", { prevSubject: true }, (subject, addRow, addCell, { cellWidth = 90, cellHeight = 35 } = {}) => { + return cy.wrap(subject).trigger('mousemove', cellWidth * (addCell + 0.5), cellHeight * (addRow + 0.5), { force: true }); +}) + +Cypress.Commands.add("dragOutside", (viewport = 'topLeft', ms = 0, px = 0, { parentSelector = 'div[class^="slickgrid_"]', scrollbarDimension = 17 } = {}) => { + const $parent = cy.$$(parentSelector); + const gridWidth = $parent.width(); + const gridHeight = $parent.height(); + var x = gridWidth / 2; + var y = gridHeight / 2; + const position = convertPosition(viewport); + if (position.x === "left") { + x = -px; + } else if (position.x === "right") { + x = gridWidth - scrollbarDimension + 3 + px; + } + if (position.y === "top") { + y = -px; + } else if (position.y === "bottom") { + y = gridHeight - scrollbarDimension + 3 + px; + } + + cy.get(parentSelector).trigger('mousemove', x, y, { force: true }); + if (ms) { + cy.wait(ms); + } + return; +}) + +Cypress.Commands.add("dragEnd", { prevSubject: 'optional' }, (_subject, gridSelector = 'div[class^="slickgrid_"]') => { + cy.get(gridSelector).trigger('mouseup', { force: true }); + return; +}) + +export function getScrollDistanceWhenDragOutsideGrid(selector, viewport, dragDirection, fromRow, fromCol, px = 140) { + return cy.convertPosition(viewport).then(_viewportPosition => { + const viewportSelector = `${selector} .slick-viewport-${_viewportPosition.x}.slick-viewport-${_viewportPosition.y}` + cy.getCell(fromRow, fromCol, viewport, { parentSelector: selector }) + .dragStart(); + return cy.get(viewportSelector).then($viewport => { + const scrollTopBefore = $viewport.scrollTop(); + const scrollLeftBefore = $viewport.scrollLeft(); + cy.dragOutside(dragDirection, 300, px, { parentSelector: selector }); + return cy.get(viewportSelector).then($viewportAfter => { + cy.dragEnd(selector); + const scrollTopAfter = $viewportAfter.scrollTop(); + const scrollLeftAfter = $viewportAfter.scrollLeft(); + cy.get(viewportSelector).scrollTo(0, 0, { ensureScrollable: false }); + return cy.wrap({ + scrollTopBefore, + scrollLeftBefore, + scrollTopAfter, + scrollLeftAfter + }); + }); + }); + }); +} \ No newline at end of file From 1405ac850ae839a1f7c51313fc4c7fead89458f3 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 20 Dec 2021 03:24:31 -0500 Subject: [PATCH 3/3] tests: refactor code to have full unit test coverage --- packages/common/src/extensions/slickCellRangeSelector.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/common/src/extensions/slickCellRangeSelector.ts b/packages/common/src/extensions/slickCellRangeSelector.ts index 29c800fbf..09397dce8 100644 --- a/packages/common/src/extensions/slickCellRangeSelector.ts +++ b/packages/common/src/extensions/slickCellRangeSelector.ts @@ -274,8 +274,7 @@ export class SlickCellRangeSelector { const endCellBox = this._grid.getCellNodeBox(end.row, end.cell); if (endCellBox) { const viewport = this._draggingMouseOffset.viewport; - if (endCellBox.left < viewport.left || endCellBox.right > viewport.right - || endCellBox.top < viewport.top || endCellBox.bottom > viewport.bottom) { + if (endCellBox.left < viewport.left || endCellBox.right > viewport.right || endCellBox.top < viewport.top || endCellBox.bottom > viewport.bottom) { this._grid.scrollCellIntoView(end.row, end.cell); } }