From 3b4ddcaff6e2e8db5804b995ff2282f306cc1a7a Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Wed, 6 Oct 2021 23:05:09 -0400 Subject: [PATCH] feat(plugins): add all Cell Range/Selection plugins into Universal - also add full unit test suite for all plugins --- .../src/examples/example13.ts | 8 +- packages/common/src/enums/keyCode.enum.ts | 2 + .../common/src/enums/slickPluginList.enum.ts | 2 + .../__tests__/extensionUtility.spec.ts | 1 - .../common/src/extensions/extensionUtility.ts | 14 - .../src/interfaces/cellRange.interface.ts | 16 + .../common/src/interfaces/drag.interface.ts | 16 + .../excelCopyBufferOption.interface.ts | 6 + .../externalCopyClipCommand.interface.ts | 24 + packages/common/src/interfaces/index.ts | 2 + .../src/interfaces/slickRange.interface.ts | 8 +- .../__tests__/cellExcelCopyManager.spec.ts | 355 +++++++++++++ .../__tests__/cellExternalCopyManager.spec.ts | 482 ++++++++++++++++++ .../__tests__/cellRangeDecorator.spec.ts | 83 +++ .../__tests__/cellRangeSelector.spec.ts | 361 +++++++++++++ .../__tests__/cellSelectionModel.spec.ts | 341 +++++++++++++ .../src/plugins/cellExcelCopyManager.ts | 10 +- .../src/plugins/cellExternalCopyManager.ts | 273 +++++----- .../common/src/plugins/cellRangeDecorator.ts | 33 +- .../common/src/plugins/cellRangeSelector.ts | 210 ++++---- .../common/src/plugins/cellSelectionModel.ts | 204 ++++---- packages/common/src/plugins/menuBaseClass.ts | 2 +- .../__tests__/extension.service.spec.ts | 50 +- .../common/src/services/extension.service.ts | 5 - 24 files changed, 2115 insertions(+), 393 deletions(-) create mode 100644 packages/common/src/interfaces/drag.interface.ts create mode 100644 packages/common/src/interfaces/externalCopyClipCommand.interface.ts create mode 100644 packages/common/src/plugins/__tests__/cellExcelCopyManager.spec.ts create mode 100644 packages/common/src/plugins/__tests__/cellExternalCopyManager.spec.ts create mode 100644 packages/common/src/plugins/__tests__/cellRangeDecorator.spec.ts create mode 100644 packages/common/src/plugins/__tests__/cellRangeSelector.spec.ts create mode 100644 packages/common/src/plugins/__tests__/cellSelectionModel.spec.ts diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example13.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example13.ts index f9f8a3f3c..ca7e92a19 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example13.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example13.ts @@ -65,9 +65,9 @@ export class Example13 { enableFiltering: false, enableExcelCopyBuffer: true, excelCopyBufferOptions: { - onCopyCells: (e, args) => console.log(e, args), - onPasteCells: (e, args) => console.log(e, args), - onCopyCancelled: (e, args) => console.log(e, args), + onCopyCells: (e, args) => console.log('onCopyCells', e, args), + onPasteCells: (e, args) => console.log('onPasteCells', e, args), + onCopyCancelled: (e, args) => console.log('onCopyCancelled', e, args), }, enableCellNavigation: true, gridHeight: 275, @@ -82,6 +82,8 @@ export class Example13 { ...this.gridOptions1, enableHeaderMenu: true, enableFiltering: true, + // frozenColumn: 2, + // frozenRow: 2, headerButton: { // when floating to left, you might want to inverse the icon orders inverseOrder: true, diff --git a/packages/common/src/enums/keyCode.enum.ts b/packages/common/src/enums/keyCode.enum.ts index 2fb829a00..65e6c61f1 100644 --- a/packages/common/src/enums/keyCode.enum.ts +++ b/packages/common/src/enums/keyCode.enum.ts @@ -1,4 +1,6 @@ export enum KeyCode { + C = 67, + V = 86, BACKSPACE = 8, DELETE = 46, DOWN = 40, diff --git a/packages/common/src/enums/slickPluginList.enum.ts b/packages/common/src/enums/slickPluginList.enum.ts index 1d19d4336..734457124 100644 --- a/packages/common/src/enums/slickPluginList.enum.ts +++ b/packages/common/src/enums/slickPluginList.enum.ts @@ -17,6 +17,7 @@ import { } from '../interfaces/index'; import { AutoTooltipPlugin, + CellRangeSelector, // CellExternalCopyManager, // CellRangeDecorator, // CellRangeSelector, @@ -25,6 +26,7 @@ import { export type SlickPluginList = AutoTooltipPlugin | + CellRangeSelector | SlickCellExternalCopyManager | SlickCellMenu | SlickCellRangeDecorator | diff --git a/packages/common/src/extensions/__tests__/extensionUtility.spec.ts b/packages/common/src/extensions/__tests__/extensionUtility.spec.ts index cec36a517..ff8ddbec6 100644 --- a/packages/common/src/extensions/__tests__/extensionUtility.spec.ts +++ b/packages/common/src/extensions/__tests__/extensionUtility.spec.ts @@ -16,7 +16,6 @@ const mockAddon = jest.fn().mockImplementation(() => ({ destroy: jest.fn() })); -jest.mock('slickgrid/plugins/slick.cellexternalcopymanager', () => mockAddon); jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockAddon); jest.mock('slickgrid/plugins/slick.rowdetailview', () => mockAddon); jest.mock('slickgrid/plugins/slick.rowmovemanager', () => mockAddon); diff --git a/packages/common/src/extensions/extensionUtility.ts b/packages/common/src/extensions/extensionUtility.ts index dae0e5839..0165cdf66 100644 --- a/packages/common/src/extensions/extensionUtility.ts +++ b/packages/common/src/extensions/extensionUtility.ts @@ -60,20 +60,6 @@ export class ExtensionUtility { return output; } - /** - * Loop through object provided and set to null any property found starting with "onX" - * @param {Object}: obj - */ - nullifyFunctionNameStartingWithOn(obj?: any) { - if (obj) { - for (const prop of Object.keys(obj)) { - if (prop.startsWith('on')) { - obj[prop] = null; - } - } - } - } - /** * When using ColumnPicker/GridMenu to show/hide a column, we potentially need to readjust the grid option "frozenColumn" index. * That is because SlickGrid freezes by column index and it has no knowledge of the columns themselves and won't change the index, we need to do that ourselves whenever necessary. diff --git a/packages/common/src/interfaces/cellRange.interface.ts b/packages/common/src/interfaces/cellRange.interface.ts index 2dc92f3af..fdcd5631c 100644 --- a/packages/common/src/interfaces/cellRange.interface.ts +++ b/packages/common/src/interfaces/cellRange.interface.ts @@ -1,3 +1,5 @@ +import { CellRangeDecorator } from '../plugins/cellRangeDecorator'; + export interface CellRange { /** Selection start from which cell? */ fromCell: number; @@ -11,3 +13,17 @@ export interface CellRange { /** Selection goes to which row? */ toRow: number; } + +export interface CellRangeDecoratorOption { + selectionCssClass: string; + selectionCss: CSSStyleDeclaration; + offset: { top: number; left: number; height: number; width: number; }; +} + +export interface CellRangeSelectorOption { + cellDecorator: CellRangeDecorator; + selectionCss: CSSStyleDeclaration; +} + +export type CSSStyleDeclarationReadonly = 'length' | 'parentRule' | 'getPropertyPriority' | 'getPropertyValue' | 'item' | 'removeProperty' | 'setProperty'; +export type CSSStyleDeclarationWritable = keyof Omit; diff --git a/packages/common/src/interfaces/drag.interface.ts b/packages/common/src/interfaces/drag.interface.ts new file mode 100644 index 000000000..2b6368c1b --- /dev/null +++ b/packages/common/src/interfaces/drag.interface.ts @@ -0,0 +1,16 @@ +export interface DragPosition { + startX: number; + startY: number; + range: DragRange; +} + +export interface DragRange { + start: { + row?: number; + cell?: number; + }; + end: { + row?: number; + cell?: number; + }; +} \ No newline at end of file diff --git a/packages/common/src/interfaces/excelCopyBufferOption.interface.ts b/packages/common/src/interfaces/excelCopyBufferOption.interface.ts index 4c840e4c0..1033e6e78 100644 --- a/packages/common/src/interfaces/excelCopyBufferOption.interface.ts +++ b/packages/common/src/interfaces/excelCopyBufferOption.interface.ts @@ -7,6 +7,12 @@ import { import { CellExcelCopyManager, } from '../plugins/cellExcelCopyManager'; export interface ExcelCopyBufferOption { + /** defaults to 2000(ms), delay in ms to wait before clearing the selection after a paste action */ + clearCopySelectionDelay?: number; + + /** defaults to 100(ms), delay in ms to wait before executing focus/paste */ + clipboardPasteDelay?: number; + /** defaults to "copied", sets the css className used for copied cells. */ copiedCellStyle?: string; diff --git a/packages/common/src/interfaces/externalCopyClipCommand.interface.ts b/packages/common/src/interfaces/externalCopyClipCommand.interface.ts new file mode 100644 index 000000000..f543bcb5d --- /dev/null +++ b/packages/common/src/interfaces/externalCopyClipCommand.interface.ts @@ -0,0 +1,24 @@ +import { CellExternalCopyManager } from '../plugins/cellExternalCopyManager'; +import { CellRange, Column, ExcelCopyBufferOption } from './index'; + +export interface ExternalCopyClipCommand { + activeCell: number; + activeRow: number; + cellExternalCopyManager: CellExternalCopyManager; + clippedRange: CellRange[]; + destH: number; + destW: number; + h: number; + w: number; + isClipboardCommand: boolean; + maxDestX: number; + maxDestY: number; + oldValues: any[]; + oneCellToMultiple: boolean; + _options: ExcelCopyBufferOption; + + execute: () => void; + markCopySelection: (ranges: CellRange[]) => void; + setDataItemValueForColumn: (item: any, columnDef: Column, value: any) => any | void; + undo: () => void; +} \ No newline at end of file diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 2f71be2dd..ad1b7daeb 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -37,6 +37,7 @@ export * from './currentSorter.interface'; export * from './customFooterOption.interface'; export * from './dataViewOption.interface'; export * from './domEvent.interface'; +export * from './drag.interface'; export * from './draggableGrouping.interface'; export * from './draggableGroupingOption.interface'; export * from './editCommand.interface'; @@ -58,6 +59,7 @@ export * from './excelWorkbook.interface'; export * from './excelWorksheet.interface'; export * from './extension.interface'; export * from './extensionModel.interface'; +export * from './externalCopyClipCommand.interface'; export * from './externalResource.interface'; export * from './filter.interface'; export * from './filterArguments.interface'; diff --git a/packages/common/src/interfaces/slickRange.interface.ts b/packages/common/src/interfaces/slickRange.interface.ts index f904ffe86..37618421e 100644 --- a/packages/common/src/interfaces/slickRange.interface.ts +++ b/packages/common/src/interfaces/slickRange.interface.ts @@ -13,14 +13,14 @@ export interface SlickRange extends CellRange { constructor: (fromRow: number, fromCell: number, toRow: number, toCell: number) => void; /** Returns whether a range represents a single row. */ - isSingleRow: () => boolean; + isSingleRow?: () => boolean; /** Returns whether a range represents a single cell. */ - isSingleCell: () => boolean; + isSingleCell?: () => boolean; /** Returns whether a range contains a given cell. */ - contains: (row: number, cell: number) => boolean; + contains?: (row: number, cell: number) => boolean; /** Returns a readable representation of a range. */ - toString: () => string; + toString?: () => string; } diff --git a/packages/common/src/plugins/__tests__/cellExcelCopyManager.spec.ts b/packages/common/src/plugins/__tests__/cellExcelCopyManager.spec.ts new file mode 100644 index 000000000..7de0c8cb6 --- /dev/null +++ b/packages/common/src/plugins/__tests__/cellExcelCopyManager.spec.ts @@ -0,0 +1,355 @@ +import { CellRange, EditCommand, Formatter, GridOption, SlickGrid, SlickNamespace, } from '../../interfaces/index'; +import { Formatters } from '../../formatters'; +import { SharedService } from '../../services/shared.service'; +import { CellExcelCopyManager } from '../cellExcelCopyManager'; +import { CellSelectionModel } from '../cellSelectionModel'; +import { CellExternalCopyManager } from '../../../dist/esm'; + +declare const Slick: SlickNamespace; +jest.mock('flatpickr', () => { }); + +const gridStub = { + getData: jest.fn(), + getOptions: jest.fn(), + getSelectionModel: jest.fn(), + registerPlugin: jest.fn(), + setSelectionModel: jest.fn(), + onKeyDown: new Slick.Event(), +} as unknown as SlickGrid; + +const mockCellExternalCopyManager = { + constructor: jest.fn(), + init: jest.fn(), + destroy: jest.fn(), + dispose: jest.fn(), + getHeaderValueForColumn: jest.fn(), + getDataItemValueForColumn: jest.fn(), + setDataItemValueForColumn: jest.fn(), + onCopyCells: new Slick.Event(), + onCopyCancelled: new Slick.Event(), + onPasteCells: new Slick.Event(), +} as unknown as CellExternalCopyManager; + +const mockCellSelectionModel = { + constructor: jest.fn(), + init: jest.fn(), + destroy: jest.fn(), + getSelectedRanges: jest.fn(), + setSelectedRanges: jest.fn(), + getSelectedRows: jest.fn(), + setSelectedRows: jest.fn(), + onSelectedRangesChanged: new Slick.Event(), +} as unknown as CellSelectionModel; + +jest.mock('../cellSelectionModel', () => ({ + CellSelectionModel: jest.fn().mockImplementation(() => mockCellSelectionModel), +})); +jest.mock('../cellExternalCopyManager', () => ({ + CellExternalCopyManager: jest.fn().mockImplementation(() => mockCellExternalCopyManager), +})); + +describe('CellExcelCopyManager', () => { + let queueCallback: EditCommand; + const mockEventCallback = () => { }; + const mockSelectRange = [{ fromCell: 1, fromRow: 1, toCell: 1, toRow: 1 }] as CellRange[]; + const mockSelectRangeEvent = { ranges: mockSelectRange }; + + let plugin: CellExcelCopyManager; + const gridOptionsMock = { + editable: true, + enableCheckboxSelector: true, + excelCopyBufferOptions: { + onExtensionRegistered: jest.fn(), + onCopyCells: mockEventCallback, + onCopyCancelled: mockEventCallback, + onPasteCells: mockEventCallback, + } + } as GridOption; + + beforeEach(() => { + plugin = new CellExcelCopyManager(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create the plugin', () => { + expect(plugin).toBeTruthy(); + expect(plugin.eventHandler).toBeTruthy(); + }); + + describe('registered addon', () => { + beforeEach(() => { + queueCallback = { + execute: () => { }, + undo: () => { }, + row: 0, + cell: 0, + editor: {}, + serializedValue: 'serialize', + prevSerializedValue: 'previous' + }; + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + }); + + it('should initialize CellExcelCopyManager', () => { + const setSelectionSpy = jest.spyOn(gridStub, 'setSelectionModel'); + const cellExternalCopyInitSpy = jest.spyOn(mockCellExternalCopyManager, 'init'); + + plugin.init(gridStub); + + const expectedAddonOptions = { + clipboardCommandHandler: expect.anything(), + dataItemColumnValueExtractor: expect.anything(), + newRowCreator: expect.anything(), + includeHeaderWhenCopying: false, + readOnlyMode: false, + }; + expect(plugin.addonOptions).toEqual(expectedAddonOptions); + expect(plugin.gridOptions).toEqual(gridOptionsMock); + expect(setSelectionSpy).toHaveBeenCalledWith(mockCellSelectionModel); + expect(cellExternalCopyInitSpy).toHaveBeenCalledWith(gridStub, expectedAddonOptions); + }); + + it('should call internal event handler subscribe and expect the "onCopyCells" option to be called when addon notify is called', () => { + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const mockOnCopy = jest.fn(); + const mockOnCopyCancel = jest.fn(); + const mockOnPasteCell = jest.fn(); + + plugin.init(gridStub, { onCopyCells: mockOnCopy, onCopyCancelled: mockOnCopyCancel, onPasteCells: mockOnPasteCell }); + mockCellExternalCopyManager.onCopyCells.notify(mockSelectRangeEvent, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(handlerSpy).toHaveBeenCalledWith( + { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + expect.anything() + ); + expect(mockOnCopy).toHaveBeenCalledWith(expect.anything(), mockSelectRangeEvent); + expect(mockOnCopyCancel).not.toHaveBeenCalled(); + expect(mockOnPasteCell).not.toHaveBeenCalled(); + }); + + it('should call internal event handler subscribe and expect the "onCopyCancelled" option to be called when addon notify is called', () => { + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const mockOnCopy = jest.fn(); + const mockOnCopyCancel = jest.fn(); + const mockOnPasteCell = jest.fn(); + + plugin.init(gridStub, { onCopyCells: mockOnCopy, onCopyCancelled: mockOnCopyCancel, onPasteCells: mockOnPasteCell }); + mockCellExternalCopyManager.onCopyCancelled.notify(mockSelectRangeEvent, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(handlerSpy).toHaveBeenCalledWith( + { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + expect.anything() + ); + expect(mockOnCopy).not.toHaveBeenCalledWith(expect.anything(), mockSelectRangeEvent); + expect(mockOnCopyCancel).toHaveBeenCalled(); + expect(mockOnPasteCell).not.toHaveBeenCalled(); + }); + + it('should call internal event handler subscribe and expect the "onPasteCells" option to be called when addon notify is called', () => { + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const mockOnCopy = jest.fn(); + const mockOnCopyCancel = jest.fn(); + const mockOnPasteCell = jest.fn(); + + plugin.init(gridStub, { onCopyCells: mockOnCopy, onCopyCancelled: mockOnCopyCancel, onPasteCells: mockOnPasteCell }); + mockCellExternalCopyManager.onPasteCells.notify(mockSelectRangeEvent, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(handlerSpy).toHaveBeenCalledWith( + { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + expect.anything() + ); + expect(mockOnCopy).not.toHaveBeenCalledWith(expect.anything(), mockSelectRangeEvent); + expect(mockOnCopyCancel).not.toHaveBeenCalled(); + expect(mockOnPasteCell).toHaveBeenCalled(); + }); + + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + }); + + describe('createUndoRedo private method', () => { + it('should create the UndoRedoBuffer', () => { + plugin.init(gridStub); + + expect(plugin.undoRedoBuffer).toEqual({ + queueAndExecuteCommand: expect.anything(), + undo: expect.anything(), + redo: expect.anything(), + }); + }); + + it('should have called Edit Command "execute" method after creating the UndoRedoBuffer', () => { + plugin.init(gridStub); + const undoRedoBuffer = plugin.undoRedoBuffer; + + const spy = jest.spyOn(queueCallback, 'execute'); + undoRedoBuffer.queueAndExecuteCommand(queueCallback); + + expect(spy).toHaveBeenCalled(); + }); + + it('should not have called Edit Command "undo" method when there is nothing to undo', () => { + plugin.init(gridStub); + const undoRedoBuffer = plugin.undoRedoBuffer; + + const spy = jest.spyOn(queueCallback, 'undo'); + undoRedoBuffer.undo(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should have called Edit Command "undo" method after calling it from UndoRedoBuffer', () => { + plugin.init(gridStub); + const undoRedoBuffer = plugin.undoRedoBuffer; + + const spy = jest.spyOn(queueCallback, 'undo'); + undoRedoBuffer.queueAndExecuteCommand(queueCallback); + undoRedoBuffer.undo(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should have called Edit Command "execute" method only at first queueing, the "redo" should not call the "execute" method by itself', () => { + plugin.init(gridStub); + const undoRedoBuffer = plugin.undoRedoBuffer; + + const spy = jest.spyOn(queueCallback, 'execute'); + undoRedoBuffer.queueAndExecuteCommand(queueCallback); + undoRedoBuffer.redo(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should have called Edit Command "execute" method at first queueing & then inside the "redo" since we did an "undo" just before', () => { + plugin.init(gridStub); + const undoRedoBuffer = plugin.undoRedoBuffer; + + const spy = jest.spyOn(queueCallback, 'execute'); + undoRedoBuffer.queueAndExecuteCommand(queueCallback); + undoRedoBuffer.undo(); + undoRedoBuffer.redo(); + + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('should have a single entry in the queue buffer after calling "queueAndExecuteCommand" once', () => { + plugin.init(gridStub); + plugin.undoRedoBuffer.queueAndExecuteCommand(queueCallback); + expect(plugin.commandQueue).toHaveLength(1); + }); + + it('should call a redo when Ctrl+Shift+Z keyboard event occurs', () => { + plugin.init(gridStub); + const spy = jest.spyOn(queueCallback, 'execute'); + + plugin.undoRedoBuffer.queueAndExecuteCommand(queueCallback); + const body = window.document.body; + body.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { + keyCode: 90, + ctrlKey: true, + shiftKey: true, + bubbles: true, + cancelable: true + })); + + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('should call a undo when Ctrl+Z keyboard event occurs', () => { + plugin.init(gridStub); + const spy = jest.spyOn(queueCallback, 'undo'); + + plugin.undoRedoBuffer.queueAndExecuteCommand(queueCallback); + const body = window.document.body; + body.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { + keyCode: 90, + ctrlKey: true, + shiftKey: false, + bubbles: true + })); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('addonOptions callbacks', () => { + it('should expect "queueAndExecuteCommand" to be called after calling "clipboardCommandHandler" callback', () => { + plugin.init(gridStub); + const spy = jest.spyOn(plugin.undoRedoBuffer, 'queueAndExecuteCommand'); + + plugin.addonOptions!.clipboardCommandHandler!(queueCallback); + + expect(spy).toHaveBeenCalled(); + }); + + it('should expect "addItem" method to be called after calling "newRowCreator" callback', () => { + plugin.init(gridStub); + const mockGetData = { addItem: jest.fn() }; + const getDataSpy = jest.spyOn(gridStub, 'getData').mockReturnValue(mockGetData); + const addItemSpy = jest.spyOn(mockGetData, 'addItem'); + + plugin.addonOptions!.newRowCreator!(2); + + expect(getDataSpy).toHaveBeenCalled(); + expect(addItemSpy).toHaveBeenCalledWith(expect.objectContaining({ id: 'newRow_0' })); + expect(addItemSpy).toHaveBeenCalledWith(expect.objectContaining({ id: 'newRow_1' })); + }); + + it('should expect a formatted output after calling "dataItemColumnValueExtractor" callback', () => { + plugin.init(gridStub); + const output = plugin.addonOptions!.dataItemColumnValueExtractor!({ firstName: 'John', lastName: 'Doe' }, { id: 'firstName', field: 'firstName', exportWithFormatter: true, formatter: Formatters.bold }); + expect(output).toBe('John'); + }); + + it('should expect a sanitized formatted and empty output after calling "dataItemColumnValueExtractor" callback', () => { + gridOptionsMock.textExportOptions = { sanitizeDataExport: true }; + const myBoldFormatter: Formatter = (_row, _cell, value) => value ? { text: `${value}` } : null as any; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + plugin.init(gridStub); + + const output = plugin.addonOptions!.dataItemColumnValueExtractor!({ firstName: 'John', lastName: null }, { id: 'lastName', field: 'lastName', exportWithFormatter: true, formatter: myBoldFormatter }); + + expect(output).toBe(''); + }); + + it('should expect a sanitized formatted output after calling "dataItemColumnValueExtractor" callback', () => { + gridOptionsMock.textExportOptions = { sanitizeDataExport: true }; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + plugin.init(gridStub); + + const output = plugin.addonOptions!.dataItemColumnValueExtractor!({ firstName: 'John', lastName: 'Doe' }, { id: 'firstName', field: 'firstName', exportWithFormatter: true, formatter: Formatters.bold }); + + expect(output).toBe('John'); + }); + + it('should expect a sanitized formatted output, from a Custom Formatter, after calling "dataItemColumnValueExtractor" callback', () => { + const myBoldFormatter: Formatter = (_row, _cell, value) => value ? { text: `${value}` } : ''; + gridOptionsMock.textExportOptions = { sanitizeDataExport: true }; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + plugin.init(gridStub); + + const output = plugin.addonOptions!.dataItemColumnValueExtractor!({ firstName: 'John', lastName: 'Doe' }, { id: 'firstName', field: 'firstName', exportWithFormatter: true, formatter: myBoldFormatter }); + + expect(output).toBe('John'); + }); + + it('should return null when calling "dataItemColumnValueExtractor" callback without editable', () => { + gridOptionsMock.editable = false; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + plugin.init(gridStub); + + const output = plugin.addonOptions!.dataItemColumnValueExtractor!({ firstName: 'John', lastName: 'Doe' }, { id: 'firstName', field: 'firstName' }); + + expect(output).toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/packages/common/src/plugins/__tests__/cellExternalCopyManager.spec.ts b/packages/common/src/plugins/__tests__/cellExternalCopyManager.spec.ts new file mode 100644 index 000000000..78a5a0a99 --- /dev/null +++ b/packages/common/src/plugins/__tests__/cellExternalCopyManager.spec.ts @@ -0,0 +1,482 @@ +import 'jest-extended'; + +import { Column, EditCommand, GridOption, SlickGrid, SlickNamespace, } from '../../interfaces/index'; +import { CellSelectionModel } from '../cellSelectionModel'; +import { CellExternalCopyManager } from '../cellExternalCopyManager'; +import { InputEditor } from '../../editors/inputEditor'; + +declare const Slick: SlickNamespace; +jest.mock('flatpickr', () => { }); + +const mockGetSelectionModel = { + getSelectedRanges: jest.fn(), +}; +const gridStub = { + getActiveCell: jest.fn(), + getColumns: jest.fn(), + getData: jest.fn(), + getDataItem: jest.fn(), + getDataLength: jest.fn(), + getEditorLock: () => ({ + isActive: () => false, + }), + getOptions: jest.fn(), + focus: jest.fn(), + getSelectionModel: () => mockGetSelectionModel, + registerPlugin: jest.fn(), + removeCellCssStyles: jest.fn(), + setCellCssStyles: jest.fn(), + setData: jest.fn(), + setSelectionModel: jest.fn(), + updateCell: jest.fn(), + render: jest.fn(), + onCellChange: new Slick.Event(), + onKeyDown: new Slick.Event(), +} as unknown as SlickGrid; + +const mockCellSelectionModel = { + constructor: jest.fn(), + init: jest.fn(), + destroy: jest.fn(), + getSelectedRanges: jest.fn(), + setSelectedRanges: jest.fn(), + getSelectedRows: jest.fn(), + setSelectedRows: jest.fn(), + onSelectedRangesChanged: new Slick.Event(), +} as unknown as CellSelectionModel; + +const mockTextEditor = { + constructor: jest.fn(), + init: jest.fn(), + destroy: jest.fn(), + applyValue: jest.fn(), + loadValue: jest.fn(), + serializeValue: jest.fn(), +} as unknown as InputEditor; + +const mockTextEditorImplementation = jest.fn().mockImplementation(() => mockTextEditor); + +const Editors = { + text: mockTextEditorImplementation +}; + +describe('CellExternalCopyManager', () => { + const mockEventCallback = () => { }; + const mockColumns = [ + { id: 'firstName', field: 'firstName', name: 'First Name', editor: Editors.text, internalColumnEditor: Editors.text }, + { id: 'lastName', field: 'lastName', name: 'Last Name', }, + { id: 'age', field: 'age', name: 'Age', editor: Editors.text, internalColumnEditor: Editors.text }, + ] as Column[]; + let plugin: CellExternalCopyManager; + const gridOptionsMock = { + editable: true, + enableCheckboxSelector: true, + excelCopyBufferOptions: { + onExtensionRegistered: jest.fn(), + onCopyCells: mockEventCallback, + onCopyCancelled: mockEventCallback, + onPasteCells: mockEventCallback, + } + } as GridOption; + + beforeEach(() => { + plugin = new CellExternalCopyManager(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create the plugin', () => { + expect(plugin).toBeTruthy(); + expect(plugin.eventHandler).toBeTruthy(); + }); + + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + + describe('registered addon', () => { + beforeEach(() => { + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + }); + + afterEach(() => { + plugin.dispose(); + jest.clearAllMocks(); + }); + + it('should throw an error initializing the plugin without a selection model', (done) => { + jest.spyOn(gridStub, 'getSelectionModel').mockReturnValue(null); + try { + plugin.init(gridStub); + } catch (error) { + expect(error.message).toBe('Selection model is mandatory for this plugin. Please set a selection model on the grid before adding this plugin: grid.setSelectionModel(new Slick.CellSelectionModel())'); + done(); + } + }); + + it('should focus on the grid after "onSelectedRangesChanged" is triggered', () => { + jest.spyOn(gridStub, 'getSelectionModel').mockReturnValue(mockCellSelectionModel as any); + const gridFocusSpy = jest.spyOn(gridStub, 'focus'); + + plugin.init(gridStub); + const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() }; + mockCellSelectionModel.onSelectedRangesChanged.notify({ fromCell: 0, fromRow: 0, toCell: 0, toRow: 0 }, eventData, gridStub); + + expect(gridFocusSpy).toHaveBeenCalled(); + }); + + it('should remove CSS styling when "clearCopySelection" is called', () => { + const removeStyleSpy = jest.spyOn(gridStub, 'removeCellCssStyles'); + plugin.init(gridStub); + plugin.clearCopySelection(); + expect(removeStyleSpy).toHaveBeenCalledWith('copy-manager'); + }); + + it('should call "getHeaderValueForColumn" and expect the ouput to be what "headerColumnValueExtractor" returns when it is provided', () => { + plugin.init(gridStub, { headerColumnValueExtractor: () => 'Full Name' }); + const output = plugin.getHeaderValueForColumn(mockColumns[0]); + expect(output).toEqual('Full Name'); + }); + + it('should call "getHeaderValueForColumn" and expect the column name property be returned when "headerColumnValueExtractor" is not provided', () => { + plugin.init(gridStub); + const output = plugin.getHeaderValueForColumn(mockColumns[0]); + expect(output).toEqual('First Name'); + }); + + it('should call "getDataItemValueForColumn" and expect the ouput to be what "dataItemColumnValueExtractor" returns when it is provided', () => { + plugin.init(gridStub, { dataItemColumnValueExtractor: (item, col) => col.field === 'firstName' ? 'Full Name' : 'Last Name' }); + const output = plugin.getDataItemValueForColumn({ firstName: 'John', lastName: 'Doe' }, mockColumns[0], new Event('mousedown')); + expect(output).toEqual('Full Name'); + }); + + it('should call "getDataItemValueForColumn" and expect the editor serialized value returned when an Editor is provided', () => { + jest.spyOn(mockTextEditor, 'serializeValue').mockReturnValue('serialized output'); + plugin.init(gridStub); + const output = plugin.getDataItemValueForColumn({ firstName: 'John', lastName: 'Doe' }, mockColumns[0], new Event('mousedown')); + expect(output).toEqual('serialized output'); + }); + + it('should call "getDataItemValueForColumn" and expect the column "field" value returned when there is no Editor provided', () => { + plugin.init(gridStub); + const output = plugin.getDataItemValueForColumn({ firstName: 'John', lastName: 'Doe' }, mockColumns[1], new Event('mousedown')); + expect(output).toEqual('Doe'); + }); + + it('should call "setDataItemValueForColumn" and expect the ouput to be what "dataItemColumnValueSetter" returns when it is provided', () => { + plugin.init(gridStub, { dataItemColumnValueSetter: (item, col, val) => val }); + const output = plugin.setDataItemValueForColumn({ firstName: 'John', lastName: 'Doe' }, mockColumns[1], 'some value'); + expect(output).toEqual('some value'); + }); + + it('should call "setDataItemValueForColumn" and expect the Editor load & apply value to be set when Editor is provided', () => { + const applyValSpy = jest.spyOn(mockTextEditor, 'applyValue'); + const loadValSpy = jest.spyOn(mockTextEditor, 'loadValue'); + + const mockItem = { firstName: 'John', lastName: 'Doe' }; + plugin.init(gridStub); + plugin.setDataItemValueForColumn(mockItem, mockColumns[0], 'some value'); + + expect(loadValSpy).toHaveBeenCalledWith(mockItem); + expect(applyValSpy).toHaveBeenCalledWith(mockItem, 'some value'); + }); + + it('should call "setDataItemValueForColumn" and expect item last name to change with new value when no Editor is provided', () => { + const mockItem = { firstName: 'John', lastName: 'Doe' }; + plugin.init(gridStub); + plugin.setDataItemValueForColumn(mockItem, mockColumns[1], 'some value'); + + expect(mockItem.lastName).toEqual('some value'); + }); + + it('should set "includeHeaderWhenCopying" when its SETTER is called', () => { + plugin.init(gridStub); + plugin.setIncludeHeaderWhenCopying(true); + expect(plugin.addonOptions.includeHeaderWhenCopying).toBeTruthy(); + }); + + describe('keyDown handler', () => { + beforeEach(() => { + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + jest.spyOn(gridStub, 'getDataLength').mockReturnValue(2); + jest.spyOn(gridStub, 'getData').mockReturnValue([{ firstName: 'John', lastName: 'Doe', age: 30 }, { firstName: 'Jane', lastName: 'Doe' }]); + jest.spyOn(gridStub, 'getDataItem').mockReturnValue({ firstName: 'John', lastName: 'Doe' }).mockReturnValueOnce({ firstName: 'Jane', lastName: 'Doe' }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should Copy & Paste then clear selections', (done) => { + const mockOnCopyCancelled = jest.fn(); + const mockOnCopyInit = jest.fn(); + const mockOnCopyCells = jest.fn(); + const mockOnCopySuccess = jest.fn(); + + const clearSpy = jest.spyOn(plugin, 'clearCopySelection'); + jest.spyOn(gridStub.getSelectionModel(), 'getSelectedRanges').mockReturnValue([{ fromRow: 0, fromCell: 1, toRow: 2, toCell: 2 }]); + + plugin.init(gridStub, { clearCopySelectionDelay: 1, clipboardPasteDelay: 2, includeHeaderWhenCopying: true, onCopyCancelled: mockOnCopyCancelled, onCopyInit: mockOnCopyInit, onCopyCells: mockOnCopyCells, onCopySuccess: mockOnCopySuccess }); + + const keyDownCtrlCopyEvent = new Event('keydown'); + Object.defineProperty(keyDownCtrlCopyEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlCopyEvent, 'key', { writable: true, configurable: true, value: 'c' }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlCopyEvent, gridStub); + + const keyDownEscEvent = new Event('keydown'); + Object.defineProperty(keyDownEscEvent, 'key', { writable: true, configurable: true, value: 'Escape' }); + Object.defineProperty(keyDownEscEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownEscEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownEscEvent, gridStub); + + expect(clearSpy).toHaveBeenCalled(); + expect(mockOnCopyInit).toHaveBeenCalled(); + expect(mockOnCopyCancelled).toHaveBeenCalledWith(keyDownEscEvent, { ranges: [{ fromCell: 1, fromRow: 0, toCell: 2, toRow: 2 }] }); + expect(mockOnCopyCells).toHaveBeenCalledWith(keyDownEscEvent, { ranges: expect.toBeArray() }); + + const getActiveCellSpy = jest.spyOn(gridStub, 'getActiveCell'); + const keyDownCtrlPasteEvent = new Event('keydown'); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + Object.defineProperty(keyDownCtrlPasteEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlPasteEvent, 'key', { writable: true, configurable: true, value: 'v' }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlPasteEvent, gridStub); + setTimeout(() => { + expect(getActiveCellSpy).toHaveBeenCalled(); + expect(clearSpy).toHaveBeenCalled(); + done(); + }, 2); + }); + + it('should copy selection and use window.clipboard when exist and Paste is performed', (done) => { + const mockOnCopyInit = jest.fn(); + const mockOnCopyCells = jest.fn(); + const mockSetData = jest.fn(); + const mockClipboard = () => ({ setData: mockSetData }); + Object.defineProperty(window, 'clipboardData', { writable: true, configurable: true, value: mockClipboard() }); + const clearSpy = jest.spyOn(plugin, 'clearCopySelection'); + jest.spyOn(gridStub.getSelectionModel(), 'getSelectedRanges').mockReturnValue([{ fromRow: 0, fromCell: 1, toRow: 1, toCell: 2 }]); + + plugin.init(gridStub, { clipboardPasteDelay: 1, clearCopySelectionDelay: 1, includeHeaderWhenCopying: true, onCopyInit: mockOnCopyInit, onCopyCells: mockOnCopyCells }); + + const keyDownCtrlCopyEvent = new Event('keydown'); + Object.defineProperty(keyDownCtrlCopyEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlCopyEvent, 'key', { writable: true, configurable: true, value: 'c' }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlCopyEvent, gridStub); + + expect(clearSpy).toHaveBeenCalled(); + expect(mockOnCopyInit).toHaveBeenCalled(); + expect(mockSetData).toHaveBeenCalledWith('Text', expect.toBeString()); + expect(mockSetData).toHaveBeenCalledWith('Text', expect.stringContaining(`Last Name\tAge`)); + expect(mockSetData).toHaveBeenCalledWith('Text', expect.stringContaining(`Doe\tserialized output`)); + + const getActiveCellSpy = jest.spyOn(gridStub, 'getActiveCell'); + const keyDownCtrlPasteEvent = new Event('keydown'); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + Object.defineProperty(keyDownCtrlPasteEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlPasteEvent, 'key', { writable: true, configurable: true, value: 'v' }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlPasteEvent, gridStub); + setTimeout(() => { + expect(getActiveCellSpy).toHaveBeenCalled(); + expect(clearSpy).toHaveBeenCalled(); + done(); + }, 2); + }); + + it('should Copy, Paste and run Execute clip command', (done) => { + jest.spyOn(gridStub.getSelectionModel(), 'getSelectedRanges').mockReturnValueOnce([{ fromRow: 0, fromCell: 1, toRow: 1, toCell: 2 }]).mockReturnValueOnce(null); + + plugin.init(gridStub, { clipboardPasteDelay: 1, clearCopySelectionDelay: 1, includeHeaderWhenCopying: true, }); + + const keyDownCtrlCopyEvent = new Event('keydown'); + Object.defineProperty(keyDownCtrlCopyEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlCopyEvent, 'key', { writable: true, configurable: true, value: 'c' }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlCopyEvent, gridStub); + + const updateCellSpy = jest.spyOn(gridStub, 'updateCell'); + const onCellChangeSpy = jest.spyOn(gridStub.onCellChange, 'notify'); + const getActiveCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 0, row: 1 }); + const keyDownCtrlPasteEvent = new Event('keydown'); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + Object.defineProperty(keyDownCtrlPasteEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlPasteEvent, 'key', { writable: true, configurable: true, value: 'v' }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlPasteEvent, gridStub); + document.querySelector('textarea').value = `Doe\tserialized output`; + + setTimeout(() => { + expect(getActiveCellSpy).toHaveBeenCalled(); + expect(updateCellSpy).toHaveBeenCalledWith(1, 0); + expect(updateCellSpy).toHaveBeenCalledWith(1, 1); + expect(onCellChangeSpy).toHaveBeenCalledWith({ row: 1, cell: 0, item: { firstName: 'John', lastName: 'serialized output' }, grid: gridStub, column: {} }); + const getDataItemSpy = jest.spyOn(gridStub, 'getDataItem'); + plugin.clipCommand.undo(); + expect(getDataItemSpy).toHaveBeenCalled(); + done(); + }, 2); + }); + + it('should Copy, Paste and run Execute clip command with only 1 cell to copy', (done) => { + jest.spyOn(gridStub.getSelectionModel(), 'getSelectedRanges').mockReturnValueOnce([{ fromRow: 0, fromCell: 1, toRow: 1, toCell: 2 }]).mockReturnValueOnce([{ fromRow: 0, fromCell: 1, toRow: 1, toCell: 2 }]); + + plugin.init(gridStub, { clipboardPasteDelay: 1, clearCopySelectionDelay: 1, includeHeaderWhenCopying: true, }); + + const keyDownCtrlCopyEvent = new Event('keydown'); + Object.defineProperty(keyDownCtrlCopyEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlCopyEvent, 'key', { writable: true, configurable: true, value: 'c' }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlCopyEvent, gridStub); + + const updateCellSpy = jest.spyOn(gridStub, 'updateCell'); + const onCellChangeSpy = jest.spyOn(gridStub.onCellChange, 'notify'); + const getActiveCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 0, row: 1 }); + const keyDownCtrlPasteEvent = new Event('keydown'); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + Object.defineProperty(keyDownCtrlPasteEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlPasteEvent, 'key', { writable: true, configurable: true, value: 'v' }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlPasteEvent, gridStub); + document.querySelector('textarea').value = `Smith`; + + setTimeout(() => { + expect(getActiveCellSpy).toHaveBeenCalled(); + expect(updateCellSpy).toHaveBeenCalledWith(0, 1); + expect(updateCellSpy).toHaveBeenCalledWith(0, 2); + expect(onCellChangeSpy).toHaveBeenCalledWith({ row: 1, cell: 2, item: { firstName: 'John', lastName: 'Smith' }, grid: gridStub, column: {} }); + + const getDataItemSpy = jest.spyOn(gridStub, 'getDataItem'); + const updateCell2Spy = jest.spyOn(gridStub, 'updateCell'); + const onCellChange2Spy = jest.spyOn(gridStub.onCellChange, 'notify'); + const setDataItemValSpy = jest.spyOn(plugin, 'setDataItemValueForColumn'); + plugin.clipCommand.undo(); + expect(getDataItemSpy).toHaveBeenCalled(); + expect(updateCell2Spy).toHaveBeenCalled(); + expect(onCellChangeSpy).toHaveBeenCalled(); + // expect(onCellChange2Spy).toHaveBeenCalledWith({ row: 1, cell: 2, item: { firstName: 'John', lastName: 'Smith' }, grid: gridStub, column: {} }); + expect(setDataItemValSpy).toHaveBeenCalled(); + done(); + }, 2); + }); + + it('should Copy, Paste but not execute run clipCommandHandler when defined', (done) => { + const mockClipboardCommandHandler = jest.fn(); + jest.spyOn(gridStub.getSelectionModel(), 'getSelectedRanges').mockReturnValueOnce([{ fromRow: 0, fromCell: 1, toRow: 2, toCell: 2 }]).mockReturnValueOnce(null); + + plugin.init(gridStub, { clearCopySelectionDelay: 1, clipboardPasteDelay: 1, includeHeaderWhenCopying: true, clipboardCommandHandler: mockClipboardCommandHandler }); + + const keyDownCtrlCopyEvent = new Event('keydown'); + Object.defineProperty(keyDownCtrlCopyEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlCopyEvent, 'key', { writable: true, configurable: true, value: 'c' }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlCopyEvent, gridStub); + + const getActiveCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 0, row: 1 }); + const keyDownCtrlPasteEvent = new Event('keydown'); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + Object.defineProperty(keyDownCtrlPasteEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlPasteEvent, 'key', { writable: true, configurable: true, value: 'v' }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlPasteEvent, gridStub); + document.querySelector('textarea').value = `Doe\tserialized output`; + + setTimeout(() => { + expect(getActiveCellSpy).toHaveBeenCalled(); + expect(mockClipboardCommandHandler).toHaveBeenCalled(); + done(); + }, 2); + }); + + it('should Copy, Paste without completing it because it does not know where to paste it', (done) => { + const mockClipboardCommandHandler = jest.fn(); + jest.spyOn(gridStub.getSelectionModel(), 'getSelectedRanges').mockReturnValueOnce([{ fromRow: 0, fromCell: 1, toRow: 2, toCell: 2 }]).mockReturnValueOnce(null); + + plugin.init(gridStub, { clearCopySelectionDelay: 1, clipboardPasteDelay: 1, includeHeaderWhenCopying: true, clipboardCommandHandler: mockClipboardCommandHandler }); + + const keyDownCtrlCopyEvent = new Event('keydown'); + Object.defineProperty(keyDownCtrlCopyEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlCopyEvent, 'key', { writable: true, configurable: true, value: 'c' }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlCopyEvent, gridStub); + + const getActiveCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(null); + const keyDownCtrlPasteEvent = new Event('keydown'); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + Object.defineProperty(keyDownCtrlPasteEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlPasteEvent, 'key', { writable: true, configurable: true, value: 'v' }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlPasteEvent, gridStub); + document.querySelector('textarea').value = `Doe\tserialized output`; + + setTimeout(() => { + expect(getActiveCellSpy).toHaveBeenCalled(); + expect(mockClipboardCommandHandler).not.toHaveBeenCalled(); + done(); + }, 2); + }); + + it('should Copy, Paste and run Execute clip command', (done) => { + const mockNewRowCreator = jest.fn(); + const mockOnPasteCells = jest.fn(); + const renderSpy = jest.spyOn(gridStub, 'render'); + const setDataSpy = jest.spyOn(gridStub, 'setData'); + jest.spyOn(gridStub.getSelectionModel(), 'getSelectedRanges').mockReturnValueOnce([{ fromRow: 0, fromCell: 1, toRow: 2, toCell: 2 }]).mockReturnValueOnce(null); + + plugin.init(gridStub, { clearCopySelectionDelay: 1, clipboardPasteDelay: 1, includeHeaderWhenCopying: true, newRowCreator: mockNewRowCreator, onPasteCells: mockOnPasteCells }); + + const keyDownCtrlCopyEvent = new Event('keydown'); + Object.defineProperty(keyDownCtrlCopyEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlCopyEvent, 'key', { writable: true, configurable: true, value: 'c' }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlCopyEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlCopyEvent, gridStub); + + const getActiveCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 0, row: 3 }); + const keyDownCtrlPasteEvent = new Event('keydown'); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + Object.defineProperty(keyDownCtrlPasteEvent, 'ctrlKey', { writable: true, configurable: true, value: true }); + Object.defineProperty(keyDownCtrlPasteEvent, 'key', { writable: true, configurable: true, value: 'v' }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(keyDownCtrlPasteEvent, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + gridStub.onKeyDown.notify({ cell: 0, row: 0, grid: gridStub }, keyDownCtrlPasteEvent, gridStub); + document.querySelector('textarea').value = `Doe\tserialized output`; + + setTimeout(() => { + expect(getActiveCellSpy).toHaveBeenCalled(); + expect(renderSpy).toHaveBeenCalled(); + expect(setDataSpy).toHaveBeenCalledWith([{ firstName: 'John', lastName: 'Doe', age: 30 }, { firstName: 'Jane', lastName: 'Doe' }, {}, {}]); + expect(mockNewRowCreator).toHaveBeenCalled(); + + const getDataItemSpy = jest.spyOn(gridStub, 'getDataItem'); + const setData2Spy = jest.spyOn(gridStub, 'setData'); + const render2Spy = jest.spyOn(gridStub, 'render'); + plugin.clipCommand.undo(); + expect(getDataItemSpy).toHaveBeenCalled(); + expect(setData2Spy).toHaveBeenCalledWith([{ firstName: 'John', lastName: 'Doe', age: 30 }, { firstName: 'Jane', lastName: 'Doe' }]); + expect(render2Spy).toHaveBeenCalled(); + expect(mockOnPasteCells).toHaveBeenCalledWith(expect.toBeObject(), { ranges: [{ fromCell: 0, fromRow: 3, toCell: 1, toRow: 3 }] }); + done(); + }, 2); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/common/src/plugins/__tests__/cellRangeDecorator.spec.ts b/packages/common/src/plugins/__tests__/cellRangeDecorator.spec.ts new file mode 100644 index 000000000..0e7944af8 --- /dev/null +++ b/packages/common/src/plugins/__tests__/cellRangeDecorator.spec.ts @@ -0,0 +1,83 @@ +import 'jest-extended'; + +import { GridOption, SlickGrid, SlickNamespace, } from '../../interfaces/index'; +import { CellRangeDecorator } from '../cellRangeDecorator'; + +declare const Slick: SlickNamespace; +jest.mock('flatpickr', () => { }); + +const gridStub = { + getActiveCell: jest.fn(), + getActiveCanvasNode: jest.fn(), + getCellNodeBox: jest.fn(), +} as unknown as SlickGrid; + +describe('CellRangeDecorator Plugin', () => { + const mockEventCallback = () => { }; + let plugin: CellRangeDecorator; + const gridOptionsMock = { + editable: true, + enableCheckboxSelector: true, + excelCopyBufferOptions: { + onExtensionRegistered: jest.fn(), + onCopyCells: mockEventCallback, + onCopyCancelled: mockEventCallback, + onPasteCells: mockEventCallback, + } + } as GridOption; + + beforeEach(() => { + plugin = new CellRangeDecorator(gridStub); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create the plugin', () => { + expect(plugin).toBeTruthy(); + expect(plugin.addonOptions).toEqual({ + selectionCssClass: 'slick-range-decorator', + selectionCss: { + border: '2px dashed red', + zIndex: '9999', + }, + offset: { top: -1, left: -1, height: -2, width: -2 }, + }) + }); + + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + const hideSpy = jest.spyOn(plugin, 'hide'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + expect(hideSpy).toHaveBeenCalled(); + }); + + it('should Show range when called and not return any new position when getCellNodeBox returns null', () => { + const divContainer = document.createElement('div'); + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divContainer); + + plugin = new CellRangeDecorator(gridStub, { offset: { top: 20, left: 5, width: 12, height: 33 } }); + plugin.show({ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }); + + expect(plugin.addonElement.style.top).toEqual(''); + expect(plugin.addonElement.style.left).toEqual(''); + expect(plugin.addonElement.style.height).toEqual(''); + expect(plugin.addonElement.style.width).toEqual(''); + }); + + it('should Show range when called and calculate new position when getCellNodeBox returns a cell position', () => { + const divContainer = document.createElement('div'); + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divContainer); + jest.spyOn(gridStub, 'getCellNodeBox').mockReturnValue({ top: 25, left: 26, right: 27, bottom: 12, height: 33, width: 44, visible: true }); + + plugin = new CellRangeDecorator(gridStub, { offset: { top: 20, left: 5, width: 12, height: 33 } }); + plugin.show({ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }); + + expect(plugin.addonElement.style.top).toEqual('45px'); // 25 + 20px + expect(plugin.addonElement.style.left).toEqual('31px'); // 26 + 5px + expect(plugin.addonElement.style.height).toEqual('20px'); // 12 - 25 + 33px + expect(plugin.addonElement.style.width).toEqual('13px'); // 27 - 26 + 12px + }); +}); \ No newline at end of file diff --git a/packages/common/src/plugins/__tests__/cellRangeSelector.spec.ts b/packages/common/src/plugins/__tests__/cellRangeSelector.spec.ts new file mode 100644 index 000000000..2d2f82c70 --- /dev/null +++ b/packages/common/src/plugins/__tests__/cellRangeSelector.spec.ts @@ -0,0 +1,361 @@ +import 'jest-extended'; + +import { GridOption, SlickGrid, SlickNamespace, } from '../../interfaces/index'; +import { CellRangeSelector } from '../cellRangeSelector'; + +declare const Slick: SlickNamespace; +const GRID_UID = 'slickgrid_12345'; +jest.mock('flatpickr', () => { }); + +const addJQueryEventPropagation = function (event) { + Object.defineProperty(event, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(event, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + return event; +} + +const mockGridOptions = { + frozenColumn: 1, + frozenRow: -1, +} as GridOption; + +const gridStub = { + canCellBeSelected: jest.fn(), + getActiveCell: jest.fn(), + getActiveCanvasNode: jest.fn(), + getCanvasNode: jest.fn(), + getCellFromEvent: jest.fn(), + getCellFromPoint: jest.fn(), + getCellNodeBox: jest.fn(), + getOptions: () => mockGridOptions, + getUID: () => GRID_UID, + focus: jest.fn(), + onDragInit: new Slick.Event(), + onDragStart: new Slick.Event(), + onDrag: new Slick.Event(), + onDragEnd: new Slick.Event(), + onScroll: new Slick.Event(), +} as unknown as SlickGrid; + +describe('CellRangeSelector Plugin', () => { + let plugin: CellRangeSelector; + const gridContainerElm = document.createElement('div'); + gridContainerElm.className = GRID_UID; + const viewportElm = document.createElement('div'); + viewportElm.className = 'slick-viewport'; + const canvasTL = document.createElement('div'); + canvasTL.className = 'grid-canvas grid-canvas-top grid-canvas-left'; + const canvasTR = document.createElement('div'); + canvasTR.className = 'grid-canvas grid-canvas-top grid-canvas-right'; + const canvasBL = document.createElement('div'); + canvasBL.className = 'grid-canvas grid-canvas-bottom grid-canvas-left'; + const canvasBR = document.createElement('div'); + canvasBR.className = 'grid-canvas grid-canvas-bottom grid-canvas-right'; + viewportElm.appendChild(canvasTL); + viewportElm.appendChild(canvasTR); + viewportElm.appendChild(canvasBL); + viewportElm.appendChild(canvasBR); + gridContainerElm.appendChild(viewportElm); + document.body.appendChild(gridContainerElm); + Object.defineProperty(canvasTL, 'clientHeight', { writable: true, configurable: true, value: 12 }); + Object.defineProperty(canvasTR, 'clientHeight', { writable: true, configurable: true, value: 14 }); + Object.defineProperty(canvasTL, 'clientWidth', { writable: true, configurable: true, value: 32 }); + Object.defineProperty(canvasTR, 'clientWidth', { writable: true, configurable: true, value: 33 }); + + beforeEach(() => { + plugin = new CellRangeSelector(); + }); + + afterEach(() => { + jest.clearAllMocks(); + plugin?.dispose(); + mockGridOptions.frozenColumn = -1; + mockGridOptions.frozenRow = -1; + mockGridOptions.frozenBottom = false; + }); + + it('should create the plugin', () => { + expect(plugin).toBeTruthy(); + expect(plugin.eventHandler).toBeTruthy(); + expect(plugin.addonOptions).toEqual({ + selectionCss: { + border: '2px dashed blue', + } + }); + }); + + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + + it('should create the plugin and initialize it', () => { + plugin.init(gridStub); + + expect(plugin.getCellDecorator()).toBeTruthy(); + }); + + it('should handle drag but return without executing anything when item cannot be dragged and cell cannot be selected', () => { + const divCanvas = document.createElement('div'); + divCanvas.className = 'grid-canvas-bottom grid-canvas-left'; + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(false); + jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 5 }); + const focusSpy = jest.spyOn(gridStub, 'focus'); + jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + + 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); + + const dragEventEnd = addJQueryEventPropagation(new Event('dragEnd')); + gridStub.onDragEnd.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEventEnd, gridStub); + + expect(focusSpy).not.toHaveBeenCalled(); + expect(decoratorHideSpy).not.toHaveBeenCalled(); + expect(decoratorShowSpy).not.toHaveBeenCalled(); + }); + + it('should handle drag in bottom left canvas', () => { + mockGridOptions.frozenRow = 2; + const divCanvas = document.createElement('div'); + divCanvas.className = 'grid-canvas-bottom grid-canvas-left'; + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + 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'); + jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + + plugin.init(gridStub); + 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: {} }); + }); + + it('should handle drag in bottom right canvas with decorator showing dragging range', () => { + mockGridOptions.frozenColumn = 3; + const divCanvas = document.createElement('div'); + divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + 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 onBeforeCellSpy = jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + + plugin.init(gridStub); + 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(onBeforeCellSpy).toHaveBeenCalled(); + expect(decoratorShowSpy).toHaveBeenCalledWith({ + fromCell: 2, fromRow: 3, 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: {} }); + }); + + it('should handle drag end in bottom right canvas with "onCellRangeSelected" published', () => { + mockGridOptions.frozenColumn = 3; + const divCanvas = document.createElement('div'); + divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + 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 onBeforeCellRangeSpy = jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + const onCellRangeSpy = jest.spyOn(plugin.onCellRangeSelected, 'notify').mockReturnValue(true); + + 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 dragEventEnd = addJQueryEventPropagation(new Event('dragEnd')); + gridStub.onDragEnd.notify({ startX: 3, startY: 4, range: { start: { cell: 2, row: 3 }, end: { cell: 4, row: 5 } }, grid: gridStub } as any, dragEventEnd, 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).toHaveBeenCalledWith({ + range: { + fromCell: 2, fromRow: 3, toCell: 4, toRow: 5, + contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + } + }); + expect(decoratorHideSpy).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: {} }); + }); + + it('should handle drag and return when "canCellBeSelected" returs', () => { + mockGridOptions.frozenColumn = 3; + const divCanvas = document.createElement('div'); + divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + 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 }); + const focusSpy = jest.spyOn(gridStub, 'focus'); + const onBeforeCellRangeSpy = jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + const onCellRangeSpy = jest.spyOn(plugin.onCellRangeSelected, 'notify').mockReturnValue(true); + + 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: {} }); + }); + + 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'); + divCanvas.className = 'grid-canvas-bottom grid-canvas-right'; + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); + jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 0 }); + const focusSpy = jest.spyOn(gridStub, 'focus'); + const onBeforeCellRangeSpy = jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + + plugin.init(gridStub); + 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(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(), + }); + }); + + 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'; + jest.spyOn(gridStub, 'getActiveCanvasNode').mockReturnValue(divCanvas); + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 2, row: 3 }); + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); + jest.spyOn(gridStub, 'getCellFromPoint').mockReturnValue({ cell: 4, row: 0 }); + const focusSpy = jest.spyOn(gridStub, 'focus'); + const onBeforeCellRangeSpy = jest.spyOn(plugin.onBeforeCellRangeSelected, 'notify').mockReturnValue(true); + + plugin.init(gridStub); + 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(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(), + }); + }); +}); \ No newline at end of file diff --git a/packages/common/src/plugins/__tests__/cellSelectionModel.spec.ts b/packages/common/src/plugins/__tests__/cellSelectionModel.spec.ts new file mode 100644 index 000000000..4761906a7 --- /dev/null +++ b/packages/common/src/plugins/__tests__/cellSelectionModel.spec.ts @@ -0,0 +1,341 @@ +import 'jest-extended'; +import { SlickRange } from '../../../dist/commonjs'; + +import { GridOption, SlickGrid, SlickNamespace, } from '../../interfaces/index'; +import { CellRangeSelector } from '../cellRangeSelector'; +import { CellSelectionModel } from '../cellSelectionModel'; + +declare const Slick: SlickNamespace; +const GRID_UID = 'slickgrid_12345'; +jest.mock('flatpickr', () => { }); + +const addJQueryEventPropagation = function (event, commandKey = '', keyName = '') { + Object.defineProperty(event, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(event, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + if (commandKey) { + Object.defineProperty(event, commandKey, { writable: true, configurable: true, value: true }); + } + if (keyName) { + Object.defineProperty(event, 'key', { writable: true, configurable: true, value: keyName }); + } + return event; +} + +const mockGridOptions = { + frozenColumn: 1, + frozenRow: -1, +} as GridOption; + +const getEditorLockMock = { + commitCurrentEdit: jest.fn(), + isActive: jest.fn(), +}; + +const gridStub = { + canCellBeSelected: jest.fn(), + getActiveCell: jest.fn(), + getActiveCanvasNode: jest.fn(), + getCanvasNode: jest.fn(), + getCellFromEvent: jest.fn(), + getCellFromPoint: jest.fn(), + getCellNodeBox: jest.fn(), + getEditorLock: () => getEditorLockMock, + getOptions: () => mockGridOptions, + getUID: () => GRID_UID, + focus: jest.fn(), + registerPlugin: jest.fn(), + setActiveCell: jest.fn(), + scrollCellIntoView: jest.fn(), + scrollRowIntoView: jest.fn(), + unregisterPlugin: jest.fn(), + onActiveCellChanged: new Slick.Event(), + onKeyDown: new Slick.Event(), + onCellRangeSelected: new Slick.Event(), + onBeforeCellRangeSelected: new Slick.Event(), +} as unknown as SlickGrid; + +describe('CellSelectionModel Plugin', () => { + let plugin: CellSelectionModel; + const gridContainerElm = document.createElement('div'); + gridContainerElm.className = GRID_UID; + const viewportElm = document.createElement('div'); + viewportElm.className = 'slick-viewport'; + const canvasTL = document.createElement('div'); + canvasTL.className = 'grid-canvas grid-canvas-top grid-canvas-left'; + const canvasTR = document.createElement('div'); + canvasTR.className = 'grid-canvas grid-canvas-top grid-canvas-right'; + const canvasBL = document.createElement('div'); + canvasBL.className = 'grid-canvas grid-canvas-bottom grid-canvas-left'; + const canvasBR = document.createElement('div'); + canvasBR.className = 'grid-canvas grid-canvas-bottom grid-canvas-right'; + viewportElm.appendChild(canvasTL); + viewportElm.appendChild(canvasTR); + viewportElm.appendChild(canvasBL); + viewportElm.appendChild(canvasBR); + gridContainerElm.appendChild(viewportElm); + document.body.appendChild(gridContainerElm); + Object.defineProperty(canvasTL, 'clientHeight', { writable: true, configurable: true, value: 12 }); + Object.defineProperty(canvasTR, 'clientHeight', { writable: true, configurable: true, value: 14 }); + Object.defineProperty(canvasTL, 'clientWidth', { writable: true, configurable: true, value: 32 }); + Object.defineProperty(canvasTR, 'clientWidth', { writable: true, configurable: true, value: 33 }); + jest.spyOn(gridStub, 'getCanvasNode').mockReturnValue(canvasTL); + + beforeEach(() => { + plugin = new CellSelectionModel(); + }); + + afterEach(() => { + jest.clearAllMocks(); + plugin?.dispose(); + mockGridOptions.frozenColumn = -1; + mockGridOptions.frozenRow = -1; + mockGridOptions.frozenBottom = false; + }); + + it('should create the plugin', () => { + expect(plugin).toBeTruthy(); + expect(plugin.eventHandler).toBeTruthy(); + expect(plugin.cellRangeSelector).toBeTruthy(); + }); + + it('should dispose of the addon', () => { + 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'); + + plugin.init(gridStub); + + expect(plugin.cellRangeSelector).toBeTruthy(); + expect(plugin.canvas).toBeTruthy(); + expect(plugin.addonOptions).toEqual({ selectActiveCell: true }); + expect(registerSpy).toHaveBeenCalledWith(plugin.cellRangeSelector); + }); + + it('should create the plugin and initialize it with just "selectActiveCell" option and still expect the same result', () => { + const registerSpy = jest.spyOn(gridStub, 'registerPlugin'); + + plugin = new CellSelectionModel({ selectActiveCell: false, cellRangeSelector: undefined }); + plugin.init(gridStub); + + expect(plugin.cellRangeSelector).toBeTruthy(); + expect(plugin.canvas).toBeTruthy(); + expect(plugin.addonOptions).toEqual({ selectActiveCell: false }); + expect(registerSpy).toHaveBeenCalledWith(plugin.cellRangeSelector); + }); + + it('should create the plugin and initialize it with just "selectActiveCell" option and still expect the same result', () => { + const registerSpy = jest.spyOn(gridStub, 'registerPlugin'); + + const mockCellRangeSelector = new CellRangeSelector({ selectionCss: { border: '2px solid black' } as CSSStyleDeclaration }); + plugin = new CellSelectionModel({ cellRangeSelector: mockCellRangeSelector, selectActiveCell: true }); + plugin.init(gridStub); + + expect(plugin.cellRangeSelector).toBeTruthy(); + expect(plugin.canvas).toBeTruthy(); + expect(plugin.addonOptions).toEqual({ selectActiveCell: true, cellRangeSelector: mockCellRangeSelector }); + expect(registerSpy).toHaveBeenCalledWith(plugin.cellRangeSelector); + }); + + it('should return False when onBeforeCellRangeSelected is called and getEditorLock returns False', () => { + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter')); + jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(true); + const stopPropSpy = jest.spyOn(mouseEvent, 'stopPropagation'); + + plugin.init(gridStub); + const output = plugin.cellRangeSelector.onBeforeCellRangeSelected.notify({ cell: 2, row: 3, grid: gridStub }, mouseEvent, gridStub); + + expect(output).toBeFalsy(); + expect(stopPropSpy).toHaveBeenCalled(); + }); + + it('should call "setSelectedRanges" when "onCellRangeSelected"', () => { + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter')); + jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(true); + const setActiveCellSpy = jest.spyOn(gridStub, 'setActiveCell'); + const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + + plugin.init(gridStub); + plugin.cellRangeSelector.onCellRangeSelected.notify({ range: { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 } }, mouseEvent, gridStub); + + expect(setActiveCellSpy).toHaveBeenCalledWith(2, 1, false, false, true); + expect(setSelectRangeSpy).toHaveBeenCalledWith([{ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }]); + }); + + it('should call "setSelectedRanges" with Slick Ranges when triggered by "onActiveCellChanged" and "selectActiveCell" is True', () => { + plugin = new CellSelectionModel({ selectActiveCell: true, cellRangeSelector: undefined }); + plugin.init(gridStub); + const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter')); + gridStub.onActiveCellChanged.notify({ cell: 2, row: 3, grid: gridStub }, mouseEvent, gridStub); + + expect(setSelectRangeSpy).toHaveBeenCalledWith([{ + fromCell: 2, fromRow: 3, toCell: 2, toRow: 3, + contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + }]); + }); + + it('should call "setSelectedRanges" with empty array when triggered by "onActiveCellChanged" and "selectActiveCell" is False', () => { + plugin = new CellSelectionModel({ selectActiveCell: false, cellRangeSelector: undefined }); + plugin.init(gridStub); + const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter')); + gridStub.onActiveCellChanged.notify({ cell: 2, row: 3, grid: gridStub }, mouseEvent, gridStub); + + expect(setSelectRangeSpy).toHaveBeenCalledWith([]); + }); + + it('should call "setSelectedRanges" with Slick Range with a Left direction when triggered by "onKeyDown" with key combo of Shift+ArrowLeft', () => { + jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 }); + const mockRanges = [ + { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }, + { fromCell: 2, fromRow: 3, toCell: 3, toRow: 4 } + ] as unknown as SlickRange[]; + plugin.init(gridStub); + plugin.setSelectedRanges(mockRanges); + + const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + const keyDownEvent = addJQueryEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowLeft'); + gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub); + + expect(setSelectRangeSpy).toHaveBeenCalledWith([{ + fromCell: 2, fromRow: 3, toCell: 2, toRow: 3, + contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + }]); + }); + + it('should call "setSelectedRanges" with Slick Range with a Right direction when triggered by "onKeyDown" with key combo of Shift+ArrowRight', () => { + jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 }); + + plugin.init(gridStub); + plugin.setSelectedRanges([ + { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }, + { fromCell: 2, fromRow: 3, toCell: 3, toRow: 4 } + ] as unknown as SlickRange[]); + const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + const keyDownEvent = addJQueryEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowRight'); + gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub); + + expect(setSelectRangeSpy).toHaveBeenCalledWith([{ + fromCell: 2, fromRow: 3, toCell: 2, toRow: 3, + contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + }]); + }); + + it('should call "setSelectedRanges" with Slick Range with a Right direction when triggered by "onKeyDown" with key combo of Shift+ArrowUp', () => { + jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 }); + + plugin.init(gridStub); + plugin.setSelectedRanges([ + { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }, + { fromCell: 2, fromRow: 3, toCell: 3, toRow: 4 } + ] as unknown as SlickRange[]); + const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + const keyDownEvent = addJQueryEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowUp'); + gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub); + + expect(setSelectRangeSpy).toHaveBeenCalledWith([{ + fromCell: 2, fromRow: 3, toCell: 2, toRow: 3, + contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + }]); + }); + + it('should call "setSelectedRanges" with Slick Range with a Right direction when triggered by "onKeyDown" with key combo of Shift+ArrowDown', () => { + jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 }); + + plugin.init(gridStub); + plugin.setSelectedRanges([ + { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: () => false }, + { fromCell: 2, fromRow: 3, toCell: 3, toRow: 4, contains: () => false } + ] as unknown as SlickRange[]); + const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + const keyDownEvent = addJQueryEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowDown'); + gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub); + + expect(setSelectRangeSpy).toHaveBeenCalledWith([{ + fromCell: 2, fromRow: 3, toCell: 2, toRow: 3, + contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + }]); + }); + + it('should call "setSelectedRanges" with Slick Range and expect with "canCellBeSelected" returning True', () => { + jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 }); + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); + const scrollRowSpy = jest.spyOn(gridStub, 'scrollRowIntoView'); + const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView'); + + plugin.init(gridStub); + plugin.setSelectedRanges([ + { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: () => false }, + { fromCell: 2, fromRow: 3, toCell: 3, toRow: 4, contains: () => false } + ] as unknown as SlickRange[]); + const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges'); + const keyDownEvent = addJQueryEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowDown'); + gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub); + + expect(setSelectRangeSpy).toHaveBeenCalledWith([ + { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: expect.toBeFunction(), } as unknown as SlickRange, + { + fromCell: 2, fromRow: 3, toCell: 2, toRow: 4, + contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(), + }, + ]); + expect(scrollCellSpy).toHaveBeenCalledWith(4, 2, false); + expect(scrollRowSpy).toHaveBeenCalledWith(4); + }); + + it('should call "rangesAreEqual" and expect True when both ranges are equal', () => { + jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 }); + + plugin.init(gridStub); + const output = plugin.rangesAreEqual( + [{ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }], + [{ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }] + ); + + expect(output).toBeTrue(); + }); + + it('should call "rangesAreEqual" and expect False when both ranges are not equal', () => { + jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: 3 }); + + plugin.init(gridStub); + const output = plugin.rangesAreEqual( + [{ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }], + [{ fromCell: 2, fromRow: 3, toCell: 3, toRow: 4 }] + ); + + expect(output).toBeFalse(); + }); + + it('should return an empty range array when calling "canCellBeSelected" return false on all ranges', () => { + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(false); + + plugin.init(gridStub); + plugin.setSelectedRanges([ + { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }, + { fromCell: 2, fromRow: 3, toCell: 3, toRow: 4 } + ] as unknown as SlickRange[]); + const output = plugin.removeInvalidRanges([{ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }] as SlickRange[]); + + expect(output).toEqual([]); + }); + + it('should return an same range array when calling "canCellBeSelected" return true for all ranges', () => { + jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true); + const mockRanges = [ + { fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }, + { fromCell: 2, fromRow: 3, toCell: 3, toRow: 4 } + ] as unknown as SlickRange[]; + + plugin.init(gridStub); + plugin.setSelectedRanges(mockRanges); + const output = plugin.removeInvalidRanges([{ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4 }] as SlickRange[]); + + expect(output).toEqual([mockRanges[0]]); + expect(plugin.getSelectedRanges()).toEqual(mockRanges); + }); +}); \ No newline at end of file diff --git a/packages/common/src/plugins/cellExcelCopyManager.ts b/packages/common/src/plugins/cellExcelCopyManager.ts index 59f0b6374..02693c4bb 100644 --- a/packages/common/src/plugins/cellExcelCopyManager.ts +++ b/packages/common/src/plugins/cellExcelCopyManager.ts @@ -109,8 +109,12 @@ export class CellExcelCopyManager { this._cellExternalCopyManagerPlugin?.dispose(); } + // + // protected functions + // --------------------- + /** Create an undo redo buffer used by the Excel like copy */ - private createUndoRedoBuffer() { + protected createUndoRedoBuffer() { let commandCtr = 0; this._commandQueue = []; @@ -144,7 +148,7 @@ export class CellExcelCopyManager { } /** @return default plugin (addon) options */ - private getDefaultOptions(): ExcelCopyBufferOption { + protected getDefaultOptions(): ExcelCopyBufferOption { let newRowIds = 0; return { @@ -188,7 +192,7 @@ export class CellExcelCopyManager { } /** Hook an undo shortcut key hook that will redo/undo the copy buffer using Ctrl+(Shift)+Z keyboard events */ - private handleBodyKeyDown(e: KeyboardEvent) { + protected handleBodyKeyDown(e: KeyboardEvent) { const keyCode = e.keyCode || e.code; if (keyCode === 90 && (e.ctrlKey || e.metaKey)) { if (e.shiftKey) { diff --git a/packages/common/src/plugins/cellExternalCopyManager.ts b/packages/common/src/plugins/cellExternalCopyManager.ts index 33b0f64eb..791642f7f 100644 --- a/packages/common/src/plugins/cellExternalCopyManager.ts +++ b/packages/common/src/plugins/cellExternalCopyManager.ts @@ -1,7 +1,21 @@ -import { CellRange, Column, ExcelCopyBufferOption, SlickGrid, SlickNamespace } from '../interfaces/index'; +import { KeyCode } from '../enums/index'; +import { + // TypeScript Helper + GetSlickEventType, + + CellRange, + Column, + ExcelCopyBufferOption, + ExternalCopyClipCommand, + SlickEventHandler, + SlickGrid, + SlickNamespace, +} from '../interfaces/index'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; +const CLEAR_COPY_SELECTION_DELAY = 2000; +const CLIPBOARD_PASTE_DELAY = 100; /* This manager enables users to copy/paste data from/to an external Spreadsheet application @@ -12,14 +26,15 @@ declare const Slick: SlickNamespace; where the browser copies/pastes the serialized data. */ export class CellExternalCopyManager { - protected _clipCommand: any; - protected _grid!: SlickGrid; - protected _copiedRanges!: CellRange[] | null; protected _addonOptions!: ExcelCopyBufferOption; - protected _copiedCellStyleLayerKey = 'copy-manager'; - protected _copiedCellStyle = 'copied'; - protected _clearCopyTI: any = 0; protected _bodyElement = document.body; + protected _clearCopyTI?: NodeJS.Timeout; + protected _clipCommand!: ExternalCopyClipCommand; + protected _copiedCellStyle = 'copied'; + protected _copiedCellStyleLayerKey = 'copy-manager'; + protected _copiedRanges: CellRange[] | null = null; + protected _eventHandler: SlickEventHandler; + protected _grid!: SlickGrid; protected _onCopyInit?: () => void; protected _onCopySuccess?: (rowCount: number) => void; pluginName = 'CellExternalCopyManager'; @@ -27,32 +42,44 @@ export class CellExternalCopyManager { onCopyCancelled = new Slick.Event(); onPasteCells = new Slick.Event(); - keyCodes = { - 'C': 67, - 'V': 86, - 'ESC': 27, - 'INSERT': 45 - }; + constructor() { + this._eventHandler = new Slick.EventHandler() as SlickEventHandler; + } - init(grid: SlickGrid, options: ExcelCopyBufferOption) { + get addonOptions() { + return this._addonOptions; + } + + get clipCommand(): ExternalCopyClipCommand { + return this._clipCommand; + } + + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + + init(grid: SlickGrid, options?: ExcelCopyBufferOption) { this._grid = grid; this._addonOptions = { ...this._addonOptions, ...options }; this._copiedCellStyleLayerKey = this._addonOptions.copiedCellStyleLayerKey || 'copy-manager'; this._copiedCellStyle = this._addonOptions.copiedCellStyle || 'copied'; - this._clearCopyTI = 0; this._bodyElement = this._addonOptions.bodyElement || document.body; this._onCopyInit = this._addonOptions.onCopyInit || undefined; this._onCopySuccess = this._addonOptions.onCopySuccess || undefined; - this._grid.onKeyDown.subscribe(this.handleKeyDown.bind(this)); + + const onKeyDownHandler = this._grid.onKeyDown; + (this._eventHandler as SlickEventHandler>).subscribe(onKeyDownHandler, this.handleKeyDown.bind(this)); // we need a cell selection model const cellSelectionModel = grid.getSelectionModel(); if (!cellSelectionModel) { throw new Error(`Selection model is mandatory for this plugin. Please set a selection model on the grid before adding this plugin: grid.setSelectionModel(new Slick.CellSelectionModel())`); } + // we give focus on the grid when a selection is done on it. // without this, if the user selects a range of cell without giving focus on a particular cell, the grid doesn't get the focus and key stroke handles (ctrl+c) don't work - cellSelectionModel.onSelectedRangesChanged.subscribe(() => { + const onSelectedRangesChangedHandler = cellSelectionModel.onSelectedRangesChanged; + (this._eventHandler as SlickEventHandler>).subscribe(onSelectedRangesChangedHandler, () => { this._grid.focus(); }); } @@ -63,22 +90,25 @@ export class CellExternalCopyManager { } dispose() { - this._grid.onKeyDown.unsubscribe(this.handleKeyDown.bind(this)); + this._eventHandler.unsubscribeAll(); + } + + clearCopySelection() { + this._grid.removeCellCssStyles(this._copiedCellStyleLayerKey); } getHeaderValueForColumn(columnDef: Column) { - if (this._addonOptions.headerColumnValueExtractor) { + if (typeof this._addonOptions.headerColumnValueExtractor === 'function') { const val = this._addonOptions.headerColumnValueExtractor(columnDef); if (val) { return val; } } - return columnDef.name; } getDataItemValueForColumn(item: any, columnDef: Column, event: Event) { - if (this._addonOptions.dataItemColumnValueExtractor) { + if (typeof this._addonOptions.dataItemColumnValueExtractor === 'function') { const val = this._addonOptions.dataItemColumnValueExtractor(item, columnDef); if (val) { return val; @@ -88,40 +118,40 @@ export class CellExternalCopyManager { let retVal = ''; // if a custom getter is not defined, we call serializeValue of the editor to serialize - if (columnDef.editor) { - const editorArgs = { - container: document.createElement('p'), // a dummy container - column: columnDef, - position: { top: 0, left: 0 }, // a dummy position required by some editors - grid: this._grid, - event, - }; - const editor = new (columnDef as any).editor(editorArgs); - editor.loadValue(item); - retVal = editor.serializeValue(); - editor.destroy(); - } else { - retVal = item[columnDef.field]; + if (columnDef) { + if (columnDef.editor) { + const editor = new (columnDef as any).editor({ + container: document.createElement('p'), // a dummy container + column: columnDef, + event, + position: { top: 0, left: 0 }, // a dummy position required by some editors + grid: this._grid, + }); + editor.loadValue(item); + retVal = editor.serializeValue(); + editor.destroy(); + } else { + retVal = item[columnDef.field || '']; + } } return retVal; } setDataItemValueForColumn(item: any, columnDef: Column, value: any): any | void { - if (!columnDef.denyPaste) { + if (!columnDef?.denyPaste) { if (this._addonOptions.dataItemColumnValueSetter) { return this._addonOptions.dataItemColumnValueSetter(item, columnDef, value); } // if a custom setter is not defined, we call applyValue of the editor to unserialize if (columnDef.editor) { - const editorArgs = { + const editor = new (columnDef as any).editor({ container: document.body, // a dummy container column: columnDef, - position: { 'top': 0, 'left': 0 }, // a dummy position required by some editors + position: { top: 0, left: 0 }, // a dummy position required by some editors grid: this._grid - }; - const editor = new (columnDef as any).editor(editorArgs); + }); editor.loadValue(item); editor.applyValue(item, value); editor.destroy(); @@ -131,39 +161,45 @@ export class CellExternalCopyManager { } } + setIncludeHeaderWhenCopying(includeHeaderWhenCopying: boolean) { + this._addonOptions.includeHeaderWhenCopying = includeHeaderWhenCopying; + } + + // + // protected functions + // --------------------- + protected createTextBox(innerText: string) { - const ta = document.createElement('textarea'); - ta.style.position = 'absolute'; - ta.style.left = '-1000px'; - ta.style.top = document.body.scrollTop + 'px'; - ta.value = innerText; - this._bodyElement.appendChild(ta); - ta.select(); - - return ta; + const textAreaElm = document.createElement('textarea'); + textAreaElm.style.position = 'absolute'; + textAreaElm.style.left = '-1000px'; + textAreaElm.style.top = `${document.body.scrollTop}px`; + textAreaElm.value = innerText; + this._bodyElement.appendChild(textAreaElm); + textAreaElm.select(); + + return textAreaElm; } - protected decodeTabularData(grid: SlickGrid, ta: HTMLTextAreaElement) { + protected decodeTabularData(grid: SlickGrid, textAreaElement: HTMLTextAreaElement) { const columns = grid.getColumns(); - const clipText = ta.value; + const clipText = textAreaElement.value; const clipRows = clipText.split(/[\n\f\r]/); // trim trailing CR if present - if (clipRows[clipRows.length - 1] === '') { clipRows.pop(); } + if (clipRows[clipRows.length - 1] === '') { + clipRows.pop(); + } - const clippedRange: any[] = []; let j = 0; + const clippedRange: any[] = []; + this._bodyElement.removeChild(textAreaElement); - this._bodyElement.removeChild(ta); - for (let i = 0; i < clipRows.length; i++) { - if (clipRows[i] !== '') { - clippedRange[j++] = clipRows[i].split('\t'); - } else { - clippedRange[j++] = ['']; - } + for (const clipRow of clipRows) { + clippedRange[j++] = clipRow !== '' ? clipRow.split('\t') : ['']; } const selectedCell = this._grid.getActiveCell(); const ranges = this._grid.getSelectionModel().getSelectedRanges(); - const selectedRange = ranges && ranges.length ? ranges[0] : null; // pick only one selection + const selectedRange = ranges?.length ? ranges[0] : null; // pick only one selection let activeRow: number; let activeCell: number; @@ -189,18 +225,17 @@ export class CellExternalCopyManager { let addRows = 0; // ignore new rows if we don't have a "newRowCreator" - if (availableRows < destH && this._addonOptions.newRowCreator) { + if ((availableRows < destH) && typeof this._addonOptions.newRowCreator === 'function') { const d: any[] = this._grid.getData(); - for (addRows = 1; addRows <= destH - availableRows; addRows++) { + for (addRows = 1; addRows <= (destH - availableRows); addRows++) { d.push({}); } this._grid.setData(d); this._grid.render(); } - const overflowsBottomOfGrid = activeRow + destH > this._grid.getDataLength(); - - if (this._addonOptions.newRowCreator && overflowsBottomOfGrid) { + const overflowsBottomOfGrid = (activeRow + destH) > this._grid.getDataLength(); + if (overflowsBottomOfGrid && typeof this._addonOptions.newRowCreator === 'function') { const newRowsNeeded = activeRow + destH - this._grid.getDataLength(); this._addonOptions.newRowCreator(newRowsNeeded); } @@ -251,7 +286,6 @@ export class CellExternalCopyManager { grid: this._grid, column: {} as unknown as Column, }); - } } } @@ -262,7 +296,6 @@ export class CellExternalCopyManager { toCell: activeCell + this._clipCommand.w - 1, toRow: activeRow + this._clipCommand.h - 1 }; - this.markCopySelection([bRange]); this._grid.getSelectionModel().setSelectedRanges([bRange]); this.onPasteCells.notify({ ranges: [bRange] }); @@ -304,16 +337,16 @@ export class CellExternalCopyManager { this.markCopySelection([bRange]); this._grid.getSelectionModel().setSelectedRanges([bRange]); this.onPasteCells.notify({ ranges: [bRange] }); - if (this._addonOptions.onPasteCells) { - this._addonOptions.onPasteCells.call(this, new Slick.EventData(), { ranges: [bRange] }); + if (typeof this._addonOptions.onPasteCells === 'function') { + this._addonOptions.onPasteCells(new Slick.EventData(), { ranges: [bRange] }); } if (addRows > 1) { - const d = this._grid.getData() as any[]; + const data = this._grid.getData() as any[]; for (; addRows > 1; addRows--) { - d.splice(d.length - 1, 1); + data.splice(data.length - 1, 1); } - this._grid.setData(d); + this._grid.setData(data); this._grid.render(); } } @@ -321,40 +354,38 @@ export class CellExternalCopyManager { if (this._addonOptions.clipboardCommandHandler) { this._addonOptions.clipboardCommandHandler(this._clipCommand); - } - else { + } else { this._clipCommand.execute(); } } - handleKeyDown(e: any): boolean | void { - let ranges; + protected handleKeyDown(e: any): boolean | void { + let ranges: CellRange[]; if (!this._grid.getEditorLock().isActive() || this._grid.getOptions().autoEdit) { - if (e.which === this.keyCodes.ESC) { + if (e.which === KeyCode.ESCAPE || e.key === 'Escape') { if (this._copiedRanges) { e.preventDefault(); this.clearCopySelection(); this.onCopyCancelled.notify({ ranges: this._copiedRanges }); - if (this._addonOptions.onCopyCancelled) { - this._addonOptions.onCopyCancelled.call(this, e, { ranges: this._copiedRanges }); + if (typeof this._addonOptions.onCopyCancelled === 'function') { + this._addonOptions.onCopyCancelled(e, { ranges: this._copiedRanges }); } this._copiedRanges = null; } } - if ((e.which === this.keyCodes.C || e.which === this.keyCodes.INSERT) && (e.ctrlKey || e.metaKey) && !e.shiftKey) { // CTRL+C or CTRL+INS - if (this._onCopyInit) { - // @ts-ignore - this._onCopyInit.call(); + if ((e.which === KeyCode.C || e.key === 'c' || e.which === KeyCode.INSERT || e.key === 'Insert') && (e.ctrlKey || e.metaKey) && !e.shiftKey) { // CTRL+C or CTRL+INS + if (typeof this._onCopyInit === 'function') { + this._onCopyInit.call(this); } ranges = this._grid.getSelectionModel().getSelectedRanges(); if (ranges.length !== 0) { this._copiedRanges = ranges; this.markCopySelection(ranges); this.onCopyCells.notify({ ranges }); - if (this._addonOptions.onCopyCells) { - this._addonOptions.onCopyCells.call(this, e, { ranges }); + if (typeof this._addonOptions.onCopyCells === 'function') { + this._addonOptions.onCopyCells(e, { ranges }); } const columns = this._grid.getColumns(); @@ -388,35 +419,21 @@ export class CellExternalCopyManager { if ((window as any).clipboardData) { (window as any).clipboardData.setData('Text', clipText); return true; - } - else { - const focusEl = document.activeElement as HTMLElement; - - const ta = this.createTextBox(clipText); - - ta.focus(); + } else { + const focusElm = document.activeElement as HTMLElement; + const textAreaElm = this.createTextBox(clipText); + textAreaElm.focus(); setTimeout(() => { - this._bodyElement.removeChild(ta); - // restore focus - if (focusEl) { - focusEl.focus(); - } else { - console.log('Not element to restore focus to after copy?'); - } - }, 100); + this._bodyElement.removeChild(textAreaElm); + // restore focus when possible + focusElm ? focusElm.focus() : console.log('No element to restore focus to after copy?'); + }, this.addonOptions?.clipboardPasteDelay ?? CLIPBOARD_PASTE_DELAY); - if (this._onCopySuccess) { - let rowCount = 0; + if (typeof this._onCopySuccess === 'function') { // If it's cell selection, use the toRow/fromRow fields - if (ranges.length === 1) { - rowCount = (ranges[0].toRow + 1) - ranges[0].fromRow; - } - else { - rowCount = ranges.length; - } - // @ts-ignore - this._onCopySuccess.call(this, rowCount); + const rowCount = (ranges.length === 1) ? ((ranges[0].toRow + 1) - ranges[0].fromRow) : ranges.length; + this._onCopySuccess(rowCount); } return false; @@ -425,45 +442,31 @@ export class CellExternalCopyManager { } if (!this._addonOptions.readOnlyMode && ( - (e.which === this.keyCodes.V && (e.ctrlKey || e.metaKey) && !e.shiftKey) - || (e.which === this.keyCodes.INSERT && e.shiftKey && !e.ctrlKey) + ((e.which === KeyCode.V || e.key === 'v') && (e.ctrlKey || e.metaKey) && !e.shiftKey) + || ((e.which === KeyCode.INSERT || e.key === 'Insert') && e.shiftKey && !e.ctrlKey) )) { // CTRL+V or Shift+INS - const ta = this.createTextBox(''); - - setTimeout(() => { - this.decodeTabularData(this._grid, ta); - }, 100); - + const textBoxElm = this.createTextBox(''); + setTimeout(() => this.decodeTabularData(this._grid, textBoxElm), this.addonOptions?.clipboardPasteDelay ?? CLIPBOARD_PASTE_DELAY); return false; } } } - markCopySelection(ranges: CellRange[]) { + protected markCopySelection(ranges: CellRange[]) { this.clearCopySelection(); const columns = this._grid.getColumns(); const hash: any = {}; - for (let i = 0; i < ranges.length; i++) { - for (let j = ranges[i].fromRow; j <= ranges[i].toRow; j++) { + for (const range of ranges) { + for (let j = range.fromRow; j <= range.toRow; j++) { hash[j] = {}; - for (let k = ranges[i].fromCell; k <= ranges[i].toCell && k < columns.length; k++) { + for (let k = range.fromCell; k <= range.toCell && k < columns.length; k++) { hash[j][columns[k].id] = this._copiedCellStyle; } } } this._grid.setCellCssStyles(this._copiedCellStyleLayerKey, hash); - clearTimeout(this._clearCopyTI); - this._clearCopyTI = setTimeout(() => { - this.clearCopySelection(); - }, 2000); - } - - clearCopySelection() { - this._grid.removeCellCssStyles(this._copiedCellStyleLayerKey); - } - - setIncludeHeaderWhenCopying(includeHeaderWhenCopying: boolean) { - this._addonOptions.includeHeaderWhenCopying = includeHeaderWhenCopying; + clearTimeout(this._clearCopyTI as NodeJS.Timeout); + this._clearCopyTI = setTimeout(() => this.clearCopySelection(), this.addonOptions?.clearCopySelectionDelay || CLEAR_COPY_SELECTION_DELAY); } } \ No newline at end of file diff --git a/packages/common/src/plugins/cellRangeDecorator.ts b/packages/common/src/plugins/cellRangeDecorator.ts index 07fb7e9c5..76f26575c 100644 --- a/packages/common/src/plugins/cellRangeDecorator.ts +++ b/packages/common/src/plugins/cellRangeDecorator.ts @@ -1,13 +1,4 @@ -import { CellRange, SlickGrid } from '../interfaces/index'; - -export interface CellRangeDecoratorOption { - selectionCssClass: string; - selectionCss: CSSStyleDeclaration; - offset: { top: number; left: number; height: number; width: number; }; -} - -export type CSSStyleDeclarationReadonly = 'length' | 'parentRule' | 'getPropertyPriority' | 'getPropertyValue' | 'item' | 'removeProperty' | 'setProperty'; -export type CSSStyleDeclarationWritable = keyof Omit; +import { CellRange, CellRangeDecoratorOption, CSSStyleDeclarationWritable, SlickGrid } from '../interfaces/index'; /** * Displays an overlay on top of a given cell range. @@ -30,11 +21,19 @@ export class CellRangeDecorator { } as CellRangeDecoratorOption; pluginName = 'CellRangeDecorator'; - constructor(grid: SlickGrid, options: any) { + constructor(grid: SlickGrid, options?: Partial) { this._addonOptions = { ...this._defaults, ...options }; this._grid = grid; } + get addonOptions() { + return this._addonOptions; + } + + get addonElement(): HTMLElement | null | undefined { + return this._elem; + } + /** @deprecated @use `dispose` Destroy plugin. */ destroy() { this.dispose(); @@ -45,6 +44,11 @@ export class CellRangeDecorator { this.hide(); } + hide() { + this._elem?.remove(); + this._elem = null; + } + show(range: CellRange) { if (!this._elem) { this._elem = document.createElement('div'); @@ -53,7 +57,7 @@ export class CellRangeDecorator { this._elem!.style[cssStyleKey as CSSStyleDeclarationWritable] = this._addonOptions.selectionCss[cssStyleKey as CSSStyleDeclarationWritable]; }); this._elem.style.position = 'absolute'; - this._grid?.getActiveCanvasNode().appendChild(this._elem); + this._grid?.getActiveCanvasNode()?.appendChild(this._elem); } const from = this._grid?.getCellNodeBox(range.fromRow, range.fromCell); @@ -67,9 +71,4 @@ export class CellRangeDecorator { } return this._elem; } - - hide() { - this._elem?.remove(); - this._elem = null; - } } \ No newline at end of file diff --git a/packages/common/src/plugins/cellRangeSelector.ts b/packages/common/src/plugins/cellRangeSelector.ts index 83427c722..055dd816b 100644 --- a/packages/common/src/plugins/cellRangeSelector.ts +++ b/packages/common/src/plugins/cellRangeSelector.ts @@ -1,80 +1,88 @@ import { emptyElement, getHtmlElementOffset, } from '../services/domUtilities'; -import { CellRange, DOMMouseEvent, GridOption, SlickGrid, SlickNamespace } from '../interfaces/index'; +import { CellRange, CellRangeSelectorOption, DOMMouseEvent, DragPosition, DragRange, GridOption, OnScrollEventArgs, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; import { CellRangeDecorator } from './index'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; -interface DragPosition { - startX: number; - startY: number; - range: DragRange; -} - -interface DragRange { - start: { - row?: number; - cell?: number; - }; - end: { - row?: number; - cell?: number; - }; -} - export class CellRangeSelector { - protected _addonOptions!: any; - protected _currentlySelectedRange!: DragRange; + protected _activeCanvas?: HTMLElement; + protected _addonOptions!: CellRangeSelectorOption; + protected _currentlySelectedRange: DragRange | null = null; protected _canvas!: HTMLElement; + protected _decorator!: CellRangeDecorator; + protected _dragging = false; + protected _eventHandler: SlickEventHandler; protected _grid!: SlickGrid; protected _gridOptions!: GridOption; - protected _activeCanvas?: HTMLElement; - protected _dragging = false; - protected _decorator!: CellRangeDecorator; - protected _eventHandler = new Slick.EventHandler(); + protected _gridUid = ''; // Frozen row & column constiables - protected _rowOffset: any; - protected _columnOffset: any; + protected _columnOffset = 0; + protected _rowOffset = 0; protected _isRightCanvas = false; protected _isBottomCanvas = false; // Scrollings - protected _scrollTop = 0; protected _scrollLeft = 0; + protected _scrollTop = 0; protected _defaults = { selectionCss: { border: '2px dashed blue' - } as CSSStyleDeclaration - }; + } + } as CellRangeSelectorOption; pluginName = 'CellRangeSelector'; onBeforeCellRangeSelected = new Slick.Event(); onCellRangeSelected = new Slick.Event<{ range: CellRange; }>(); - constructor(options: any) { + constructor(options?: Partial) { + this._eventHandler = new Slick.EventHandler(); this._addonOptions = { ...this._defaults, ...options }; } + get addonOptions() { + return this._addonOptions; + } + + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + + /** Getter for the grid uid */ + get gridUid(): string { + return this._gridUid || (this._grid?.getUID() ?? ''); + } + get gridUidSelector(): string { + return this.gridUid ? `.${this.gridUid}` : ''; + } + init(grid: SlickGrid) { this._grid = grid; this._decorator = this._addonOptions.cellDecorator || new CellRangeDecorator(grid, this._addonOptions); - this._grid = grid; - this._canvas = this._grid.getCanvasNode(); - this._gridOptions = this._grid.getOptions(); + this._canvas = grid.getCanvasNode(); + this._gridOptions = grid.getOptions(); + this._gridUid = grid.getUID(); + this._eventHandler - .subscribe(this._grid.onScroll, this.handleScroll.bind(this) as EventListener) + .subscribe(this._grid.onDrag, this.handleDrag.bind(this) as EventListener) .subscribe(this._grid.onDragInit, this.handleDragInit.bind(this) as EventListener) .subscribe(this._grid.onDragStart, this.handleDragStart.bind(this) as EventListener) - .subscribe(this._grid.onDrag, this.handleDrag.bind(this) as EventListener) - .subscribe(this._grid.onDragEnd, this.handleDragEnd.bind(this) as EventListener); + .subscribe(this._grid.onDragEnd, this.handleDragEnd.bind(this) as EventListener) + .subscribe(this._grid.onScroll, this.handleScroll.bind(this) as EventListener); } + /** @deprecated @use `dispose` Destroy plugin. */ destroy() { - this._eventHandler.unsubscribeAll(); + this.dispose(); + } + + /** Dispose the plugin. */ + dispose() { + this._eventHandler?.unsubscribeAll(); emptyElement(this._activeCanvas); emptyElement(this._canvas); - if (this._decorator?.destroy) { - this._decorator.destroy(); + if (this._decorator?.dispose) { + this._decorator.dispose(); } } @@ -82,12 +90,59 @@ export class CellRangeSelector { return this._decorator; } - handleScroll(_e: DOMMouseEvent, args: { scrollTop: number; scrollLeft: number; }) { - this._scrollTop = args.scrollTop; - this._scrollLeft = args.scrollLeft; + getCurrentRange() { + return this._currentlySelectedRange; + } + + // + // protected functions + // --------------------- + + protected handleDrag(e: any, dd: DragPosition) { + if (!this._dragging) { + return; + } + e.stopImmediatePropagation(); + + 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; + } + + // ... 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; + } + + dd.range.end = end; + this._decorator.show(new Slick.Range(dd.range.start.row, dd.range.start.cell, end.row, end.cell)); + } + + protected handleDragEnd(e: any, dd: DragPosition) { + if (!this._dragging) { + return; + } + + 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) + }); } - handleDragInit(e: any) { + 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); @@ -97,23 +152,24 @@ export class CellRangeSelector { this._isBottomCanvas = this._activeCanvas.classList.contains('grid-canvas-bottom'); if (this._gridOptions.frozenRow! > -1 && this._isBottomCanvas) { - this._rowOffset = (this._gridOptions.frozenBottom) ? document.querySelector('.' + this._grid.getUID() + ' .grid-canvas-bottom')?.clientHeight ?? 0 : document.querySelector('.' + this._grid.getUID() + ' .grid-canvas-top')?.clientHeight ?? 0; + const canvasSelector = `${this.gridUidSelector} .grid-canvas-${this._gridOptions.frozenBottom ? 'bottom' : 'top'}`; + this._rowOffset = document.querySelector(canvasSelector)?.clientHeight ?? 0; } this._isRightCanvas = this._activeCanvas.classList.contains('grid-canvas-right'); if (this._gridOptions.frozenColumn! > -1 && this._isRightCanvas) { - this._columnOffset = document.querySelector('.' + this._grid.getUID() + ' .grid-canvas-left')?.clientWidth ?? 0; + this._columnOffset = document.querySelector(`${this.gridUidSelector} .grid-canvas-left`)?.clientWidth ?? 0; } // prevent the grid from cancelling drag'n'drop by default e.stopImmediatePropagation(); } - handleDragStart(e: DOMMouseEvent, dd: DragPosition) { - const cell = this._grid.getCellFromEvent(e); - if (this.onBeforeCellRangeSelected.notify(cell) !== false) { - if (this._grid.canCellBeSelected(cell!.row, cell!.cell)) { + 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(); } @@ -135,63 +191,13 @@ export class CellRangeSelector { } const start = this._grid.getCellFromPoint(startX, startY); - dd.range = { start, end: {} }; this._currentlySelectedRange = dd.range; return this._decorator.show(new Slick.Range(start.row, start.cell)); } - handleDrag(e: any, dd: DragPosition) { - if (!this._dragging) { - return; - } - e.stopImmediatePropagation(); - - 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; - } - - // ... 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; - } - - dd.range.end = end; - - this._decorator.show(new Slick.Range(dd.range.start.row, dd.range.start.cell, end.row, end.cell)); - } - - handleDragEnd(e: any, dd: DragPosition) { - if (!this._dragging) { - return; - } - - 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 - ) - }); - } - - getCurrentRange() { - return this._currentlySelectedRange; + protected handleScroll(_e: DOMMouseEvent, args: OnScrollEventArgs) { + this._scrollTop = args.scrollTop; + this._scrollLeft = args.scrollLeft; } } \ No newline at end of file diff --git a/packages/common/src/plugins/cellSelectionModel.ts b/packages/common/src/plugins/cellSelectionModel.ts index fbd184216..1db4200c8 100644 --- a/packages/common/src/plugins/cellSelectionModel.ts +++ b/packages/common/src/plugins/cellSelectionModel.ts @@ -1,4 +1,5 @@ -import { CellRange, OnActiveCellChangedEventArgs, SlickGrid, SlickNamespace, SlickRange, } from '../interfaces/index'; +import { KeyCode } from '../enums/index'; +import { CellRange, OnActiveCellChangedEventArgs, SlickEventHandler, SlickGrid, SlickNamespace, SlickRange, } from '../interfaces/index'; import { CellRangeSelector } from './index'; // using external SlickGrid JS libraries @@ -6,8 +7,9 @@ declare const Slick: SlickNamespace; export class CellSelectionModel { protected _addonOptions: any; - protected _grid!: SlickGrid; protected _canvas: HTMLElement | null = null; + protected _eventHandler: SlickEventHandler; + protected _grid!: SlickGrid; protected _ranges: SlickRange[] = []; protected _selector: CellRangeSelector; protected _defaults = { @@ -16,29 +18,45 @@ export class CellSelectionModel { onSelectedRangesChanged = new Slick.Event(); pluginName = 'CellSelectionModel'; - constructor(options?: any) { - if (options === undefined || typeof options.cellRangeSelector === undefined) { - this._selector = new CellRangeSelector({ - selectionCss: { - border: '2px solid black' - } - }); + constructor(options?: { selectActiveCell: boolean; cellRangeSelector: CellRangeSelector; }) { + this._eventHandler = new Slick.EventHandler(); + if (options === undefined || options.cellRangeSelector === undefined) { + this._selector = new CellRangeSelector({ selectionCss: { border: '2px solid black' } as CSSStyleDeclaration }); } else { this._selector = options.cellRangeSelector; } this._addonOptions = options; } + get addonOptions() { + return this._addonOptions; + } + + get canvas() { + return this._canvas; + } + + get cellRangeSelector() { + return this._selector; + } + + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + init(grid: SlickGrid) { - this._addonOptions = { ...this._defaults, ...this._addonOptions }; this._grid = grid; - this._canvas = this._grid.getCanvasNode(); - this._grid.onActiveCellChanged.subscribe(this.handleActiveCellChange.bind(this)); - this._grid.onKeyDown.subscribe(this.handleKeyDown.bind(this)); + this._addonOptions = { ...this._defaults, ...this._addonOptions }; + this._eventHandler + .subscribe(this._grid.onActiveCellChanged, this.handleOnActiveCellChange.bind(this) as EventListener) + .subscribe(this._grid.onKeyDown, this.handleOnKeyDown.bind(this) as EventListener) + .subscribe(this._selector.onBeforeCellRangeSelected, this.handleOnBeforeCellRangeSelected.bind(this) as EventListener) + .subscribe(this._selector.onCellRangeSelected, this.handleOnCellRangeSelected.bind(this) as EventListener); + + // register the cell range selector plugin grid.registerPlugin(this._selector); - this._selector.onCellRangeSelected.subscribe(this.handleCellRangeSelected.bind(this)); - this._selector.onBeforeCellRangeSelected.subscribe(this.handleBeforeCellRangeSelected.bind(this)); + this._canvas = this._grid.getCanvasNode(); } /** @deprecated @use `dispose` Destroy plugin. */ @@ -47,34 +65,21 @@ export class CellSelectionModel { } dispose() { - this._grid.onActiveCellChanged.unsubscribe(this.handleActiveCellChange.bind(this)); - this._grid.onKeyDown.unsubscribe(this.handleKeyDown.bind(this)); - this._selector.onCellRangeSelected.unsubscribe(this.handleCellRangeSelected.bind(this)); - this._selector.onBeforeCellRangeSelected.unsubscribe(this.handleBeforeCellRangeSelected.bind(this)); - this._grid.unregisterPlugin(this._selector as any); this._canvas = null; - this._selector?.destroy(); + this._eventHandler.unsubscribeAll(); + this._grid?.unregisterPlugin(this._selector); + this._selector?.dispose(); } - removeInvalidRanges(ranges: any[]) { - const result = []; - - for (let i = 0; i < ranges.length; i++) { - const r = ranges[i]; - if (this._grid.canCellBeSelected(r.fromRow, r.fromCell) && this._grid.canCellBeSelected(r.toRow, r.toCell)) { - result.push(r); - } - } - - return result; + getSelectedRanges() { + return this._ranges; } - rangesAreEqual(range1: any, range2: any) { + rangesAreEqual(range1: CellRange[], range2: CellRange[]) { let areDifferent = (range1.length !== range2.length); if (!areDifferent) { for (let i = 0; i < range1.length; i++) { - if ( - range1[i].fromCell !== range2[i].fromCell + if (range1[i].fromCell !== range2[i].fromCell || range1[i].fromRow !== range2[i].fromRow || range1[i].toCell !== range2[i].toCell || range1[i].toRow !== range2[i].toRow @@ -87,9 +92,22 @@ export class CellSelectionModel { return !areDifferent; } - setSelectedRanges(ranges: any[]) { + removeInvalidRanges(ranges: SlickRange[]) { + const result = []; + for (let i = 0; i < ranges.length; i++) { + const r = ranges[i]; + if (this._grid.canCellBeSelected(r.fromRow, r.fromCell) && this._grid.canCellBeSelected(r.toRow, r.toCell)) { + result.push(r); + } + } + return result; + } + + setSelectedRanges(ranges: SlickRange[]) { // simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged - if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) { return; } + if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) { + return; + } // if range has not changed, don't fire onSelectedRangesChanged const rangeHasChanged = !this.rangesAreEqual(this._ranges, ranges); @@ -100,46 +118,42 @@ export class CellSelectionModel { } } - getSelectedRanges() { - return this._ranges; + // + // protected functions + // --------------------- + + protected handleOnActiveCellChange(_e: any, args: OnActiveCellChangedEventArgs) { + if (this._addonOptions.selectActiveCell && args.row !== null && args.cell !== null) { + this.setSelectedRanges([new Slick.Range(args.row, args.cell)]); + } else if (!this._addonOptions.selectActiveCell) { + // clear the previous selection once the cell changes + this.setSelectedRanges([]); + } } - handleBeforeCellRangeSelected(e: any): boolean | void { + protected handleOnBeforeCellRangeSelected(e: any): boolean | void { if (this._grid.getEditorLock().isActive()) { e.stopPropagation(); return false; } } - handleCellRangeSelected(_e: any, args: { range: CellRange; }) { + protected handleOnCellRangeSelected(_e: any, args: { range: CellRange; }) { this._grid.setActiveCell(args.range.fromRow, args.range.fromCell, false, false, true); - this.setSelectedRanges([args.range]); - } - - handleActiveCellChange(_e: any, args: OnActiveCellChangedEventArgs) { - if (this._addonOptions.selectActiveCell && args.row !== null && args.cell !== null) { - this.setSelectedRanges([new Slick.Range(args.row, args.cell)]); - } else if (!this._addonOptions.selectActiveCell) { - // clear the previous selection once the cell changes - this.setSelectedRanges([]); - } + this.setSelectedRanges([args.range as SlickRange]); } - handleKeyDown(e: any) { - /*** - * Кey codes - * 37 left - * 38 up - * 39 right - * 40 down - */ + protected handleOnKeyDown(e: any) { let ranges: SlickRange[]; let last: SlickRange; const active = this._grid.getActiveCell(); const metaKey = e.ctrlKey || e.metaKey; if (active && e.shiftKey && !metaKey && !e.altKey && - (e.which === 37 || e.which === 39 || e.which === 38 || e.which === 40)) { + (e.which === KeyCode.LEFT || e.key === 'ArrowLeft' + || e.which === KeyCode.RIGHT || e.key === 'ArrowRight' + || e.which === KeyCode.UP || e.key === 'ArrowUp' + || e.which === KeyCode.DOWN || e.key === 'ArrowDown')) { ranges = this.getSelectedRanges().slice(); if (!ranges.length) { @@ -148,42 +162,44 @@ export class CellSelectionModel { // keyboard can work with last range only last = ranges.pop() as SlickRange; - // can't handle selection out of active cell - if (!last.contains(active.row, active.cell)) { - last = new Slick.Range(active.row, active.cell); - } - let dRow = last.toRow - last.fromRow; - let dCell = last.toCell - last.fromCell; - // walking direction - const dirRow = active.row === last.fromRow ? 1 : -1; - const dirCell = active.cell === last.fromCell ? 1 : -1; - - if (e.which === 37) { - dCell -= dirCell; - } else if (e.which === 39) { - dCell += dirCell; - } else if (e.which === 38) { - dRow -= dirRow; - } else if (e.which === 40) { - dRow += dirRow; - } + if (typeof last?.contains === 'function') { + // can't handle selection out of active cell + if (!last.contains(active.row, active.cell)) { + last = new Slick.Range(active.row, active.cell); + } + let dRow = last.toRow - last.fromRow; + let dCell = last.toCell - last.fromCell; + + // walking direction + const dirRow = active.row === last.fromRow ? 1 : -1; + const dirCell = active.cell === last.fromCell ? 1 : -1; + + if (e.which === KeyCode.LEFT || e.key === 'ArrowLeft') { + dCell -= dirCell; + } else if (e.which === KeyCode.RIGHT || e.key === 'ArrowRight') { + dCell += dirCell; + } else if (e.which === KeyCode.UP || e.key === 'ArrowUp') { + dRow -= dirRow; + } else if (e.which === KeyCode.DOWN || e.key === 'ArrowDown') { + dRow += dirRow; + } - // define new selection range - const newLast = new Slick.Range(active.row, active.cell, active.row + dirRow * dRow, active.cell + dirCell * dCell); - if (this.removeInvalidRanges([newLast]).length) { - ranges.push(newLast); - const viewRow = dirRow > 0 ? newLast.toRow : newLast.fromRow; - const viewCell = dirCell > 0 ? newLast.toCell : newLast.fromCell; - this._grid.scrollRowIntoView(viewRow); - this._grid.scrollCellIntoView(viewRow, viewCell, false); - } - else { - ranges.push(last); - } - this.setSelectedRanges(ranges); + // define new selection range + const newLast = new Slick.Range(active.row, active.cell, active.row + dirRow * dRow, active.cell + dirCell * dCell); + if (this.removeInvalidRanges([newLast]).length) { + ranges.push(newLast); + const viewRow = dirRow > 0 ? newLast.toRow : newLast.fromRow; + const viewCell = dirCell > 0 ? newLast.toCell : newLast.fromCell; + this._grid.scrollRowIntoView(viewRow); + this._grid.scrollCellIntoView(viewRow, viewCell, false); + } else { + ranges.push(last); + } + this.setSelectedRanges(ranges); - e.preventDefault(); - e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); + } } } } \ No newline at end of file diff --git a/packages/common/src/plugins/menuBaseClass.ts b/packages/common/src/plugins/menuBaseClass.ts index c66ec9bed..f18c343e5 100644 --- a/packages/common/src/plugins/menuBaseClass.ts +++ b/packages/common/src/plugins/menuBaseClass.ts @@ -39,7 +39,7 @@ export class MenuBaseClass GRID_UID, getColumns: jest.fn(), setColumns: jest.fn(), onColumnsResized: jest.fn(), registerPlugin: jest.fn(), + setSelectionModel: jest.fn(), updateColumnHeader: jest.fn(), + onActiveCellChanged: new Slick.Event(), onBeforeDestroy: new Slick.Event(), onBeforeHeaderCellDestroy: new Slick.Event(), onBeforeSetColumns: new Slick.Event(), onClick: new Slick.Event(), - onContextMenu: new Slick.Event(), onColumnsReordered: new Slick.Event(), + onContextMenu: new Slick.Event(), onHeaderCellRendered: new Slick.Event(), + onHeaderContextMenu: new Slick.Event(), onKeyDown: new Slick.Event(), - onSetOptions: new Slick.Event(), onScroll: new Slick.Event(), - onHeaderContextMenu: new Slick.Event(), + onSetOptions: new Slick.Event(), } as unknown as SlickGrid; const filterServiceStub = { @@ -502,18 +519,23 @@ describe('ExtensionService', () => { expect(output).toEqual({ name: ExtensionName.headerMenu, instance: pluginInstance, class: pluginInstance } as ExtensionModel); }); - // it('should register the ExcelCopyBuffer addon when "enableExcelCopyBuffer" is set in the grid options', () => { - // const gridOptionsMock = { enableExcelCopyBuffer: true } as GridOption; - // const extSpy = jest.spyOn(extensionStub, 'register').mockReturnValue(instanceMock); - // const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + it('should register the ExcelCopyBuffer addon when "enableExcelCopyBuffer" is set in the grid options', () => { + const onRegisteredMock = jest.fn(); + const gridOptionsMock = { enableExcelCopyBuffer: true, excelCopyBufferOptions: { onExtensionRegistered: onRegisteredMock } } as GridOption; + const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + jest.spyOn(gridStub, 'getSelectionModel').mockReturnValue(cellSelectionModelStub); - // service.bindDifferentExtensions(); - // const output = service.getExtensionByName(ExtensionName.cellExternalCopyManager); + service.bindDifferentExtensions(); + const output = service.getExtensionByName(ExtensionName.cellExternalCopyManager); + const pluginInstance = service.getSlickgridAddonInstance(ExtensionName.cellExternalCopyManager); - // expect(gridSpy).toHaveBeenCalled(); - // expect(extSpy).toHaveBeenCalled(); - // expect(output).toEqual({ name: ExtensionName.cellExternalCopyManager, instance: instanceMock as unknown, class: extensionStub } as ExtensionModel); - // }); + expect(onRegisteredMock).toHaveBeenCalledWith(expect.toBeObject()); + expect(output.instance instanceof CellExcelCopyManager).toBeTrue(); + expect(gridSpy).toHaveBeenCalled(); + expect(pluginInstance).toBeTruthy(); + expect(output!.instance).toEqual(pluginInstance); + expect(output).toEqual({ name: ExtensionName.cellExternalCopyManager, instance: pluginInstance, class: pluginInstance } as ExtensionModel); + }); }); describe('createExtensionsBeforeGridCreation method', () => { diff --git a/packages/common/src/services/extension.service.ts b/packages/common/src/services/extension.service.ts index 345aea416..c14b17c8c 100644 --- a/packages/common/src/services/extension.service.ts +++ b/packages/common/src/services/extension.service.ts @@ -1,8 +1,3 @@ -// import common 3rd party SlickGrid plugins/libs -import 'slickgrid/plugins/slick.cellrangedecorator'; -import 'slickgrid/plugins/slick.cellrangeselector'; -import 'slickgrid/plugins/slick.cellselectionmodel'; - import { Column, Extension, ExtensionModel, GridOption, SlickRowSelectionModel, } from '../interfaces/index'; import { ColumnReorderFunction, ExtensionList, ExtensionName, SlickControlList, SlickPluginList } from '../enums/index'; import {