diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example13.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example13.ts index cdd84fff9..f9f8a3f3c 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example13.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example13.ts @@ -63,6 +63,12 @@ export class Example13 { container: '.demo-container', }, 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), + }, enableCellNavigation: true, gridHeight: 275, headerButton: { diff --git a/packages/common/src/enums/slickPluginList.enum.ts b/packages/common/src/enums/slickPluginList.enum.ts index 6666261aa..1d19d4336 100644 --- a/packages/common/src/enums/slickPluginList.enum.ts +++ b/packages/common/src/enums/slickPluginList.enum.ts @@ -15,7 +15,13 @@ import { SlickRowMoveManager, SlickRowSelectionModel, } from '../interfaces/index'; -import { AutoTooltipPlugin } from '../plugins/index'; +import { + AutoTooltipPlugin, + // CellExternalCopyManager, + // CellRangeDecorator, + // CellRangeSelector, + // CellSelectionModel, +} from '../plugins/index'; export type SlickPluginList = AutoTooltipPlugin | diff --git a/packages/common/src/extensions/__tests__/cellExternalCopyManagerExtension.spec.ts b/packages/common/src/extensions/__tests__/cellExternalCopyManagerExtension.spec.ts deleted file mode 100644 index 6cdf94252..000000000 --- a/packages/common/src/extensions/__tests__/cellExternalCopyManagerExtension.spec.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { CellRange, EditCommand, ExcelCopyBufferOption, Formatter, GridOption, SlickCellExternalCopyManager, SlickGrid, SlickNamespace, } from '../../interfaces/index'; -import { Formatters } from '../../formatters'; -import { CellExternalCopyManagerExtension } from '../cellExternalCopyManagerExtension'; -import { ExtensionUtility } from '../extensionUtility'; -import { SharedService } from '../../services/shared.service'; -import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; -import { BackendUtilityService } from '../../services'; - -declare const Slick: SlickNamespace; -jest.mock('flatpickr', () => { }); - -const gridStub = { - getData: jest.fn(), - getOptions: jest.fn(), - registerPlugin: jest.fn(), - setSelectionModel: jest.fn(), -} as unknown as SlickGrid; - -const addonStub = { - init: jest.fn(), - destroy: jest.fn(), - onCopyCells: new Slick.Event(), - onCopyCancelled: new Slick.Event(), - onPasteCells: new Slick.Event(), -}; -const mockAddon = jest.fn().mockImplementation(() => addonStub); -const mockSelectionModel = jest.fn().mockImplementation(() => ({ - init: jest.fn(), - destroy: jest.fn() -})); -Slick.CellExternalCopyManager = mockAddon; -Slick.CellSelectionModel = mockSelectionModel; - -describe('cellExternalCopyManagerExtension', () => { - jest.mock('slickgrid/plugins/slick.cellexternalcopymanager', () => mockAddon); - jest.mock('slickgrid/plugins/slick.cellselectionmodel', () => mockSelectionModel); - - let queueCallback: EditCommand; - const mockEventCallback = () => { }; - const mockSelectRange = [{ fromCell: 1, fromRow: 1, toCell: 1, toRow: 1 }] as CellRange[]; - const mockSelectRangeEvent = { ranges: mockSelectRange }; - - let extension: CellExternalCopyManagerExtension; - let extensionUtility: ExtensionUtility; - let backendUtilityService: BackendUtilityService; - let sharedService: SharedService; - let translateService: TranslateServiceStub; - const gridOptionsMock = { - editable: true, - enableCheckboxSelector: true, - excelCopyBufferOptions: { - onExtensionRegistered: jest.fn(), - onCopyCells: mockEventCallback, - onCopyCancelled: mockEventCallback, - onPasteCells: mockEventCallback, - } - } as GridOption; - - beforeEach(() => { - sharedService = new SharedService(); - backendUtilityService = new BackendUtilityService(); - translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); - extension = new CellExternalCopyManagerExtension(extensionUtility, sharedService); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should return null when either the grid object or the grid options is missing', () => { - const output = extension.register(); - expect(output).toBeNull(); - }); - - describe('registered addon', () => { - beforeEach(() => { - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - queueCallback = { - execute: () => { }, - undo: () => { }, - row: 0, - cell: 0, - editor: {}, - serializedValue: 'serialize', - prevSerializedValue: 'previous' - }; - }); - - it('should register the addon', () => { - const pluginSpy = jest.spyOn(SharedService.prototype.slickGrid, 'registerPlugin'); - const onRegisteredSpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onExtensionRegistered'); - - const instance = extension.register() as SlickCellExternalCopyManager; - const addonInstance = extension.getAddonInstance(); - - expect(instance).toBeTruthy(); - expect(instance).toEqual(addonInstance); - expect(onRegisteredSpy).toHaveBeenCalledWith(instance); - expect(pluginSpy).toHaveBeenCalledWith(instance); - expect(mockSelectionModel).toHaveBeenCalled(); - expect(mockAddon).toHaveBeenCalledWith({ - clipboardCommandHandler: expect.anything(), - dataItemColumnValueExtractor: expect.anything(), - newRowCreator: expect.anything(), - includeHeaderWhenCopying: false, - readOnlyMode: false, - onCopyCancelled: expect.anything(), - onCopyCells: expect.anything(), - onExtensionRegistered: expect.anything(), - onPasteCells: expect.anything(), - }); - }); - - it('should call internal event handler subscribe and expect the "onCopyCells" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onCopySpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onCopyCells'); - const onCancelSpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onCopyCancelled'); - const onPasteSpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onPasteCells'); - - const instance = extension.register() as SlickCellExternalCopyManager; - instance.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(onCopySpy).toHaveBeenCalledWith(expect.anything(), mockSelectRangeEvent); - expect(onCancelSpy).not.toHaveBeenCalled(); - expect(onPasteSpy).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(extension.eventHandler, 'subscribe'); - const onCopySpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onCopyCells'); - const onCancelSpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onCopyCancelled'); - const onPasteSpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onPasteCells'); - - const instance = extension.register() as SlickCellExternalCopyManager; - instance.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(onCopySpy).not.toHaveBeenCalled(); - expect(onCancelSpy).toHaveBeenCalledWith(expect.anything(), mockSelectRangeEvent); - expect(onPasteSpy).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(extension.eventHandler, 'subscribe'); - const onCopySpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onCopyCells'); - const onCancelSpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onCopyCancelled'); - const onPasteSpy = jest.spyOn(SharedService.prototype.gridOptions.excelCopyBufferOptions as ExcelCopyBufferOption, 'onPasteCells'); - - const instance = extension.register() as SlickCellExternalCopyManager; - instance.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(onCopySpy).not.toHaveBeenCalled(); - expect(onCancelSpy).not.toHaveBeenCalled(); - expect(onPasteSpy).toHaveBeenCalledWith(expect.anything(), mockSelectRangeEvent); - }); - - it('should dispose of the addon', () => { - const instance = extension.register() as SlickCellExternalCopyManager; - const destroySpy = jest.spyOn(instance, 'destroy'); - - extension.dispose(); - - expect(destroySpy).toHaveBeenCalled(); - }); - }); - - describe('createUndoRedo private method', () => { - it('should create the UndoRedoBuffer', () => { - extension.register(); - - expect(extension.undoRedoBuffer).toEqual({ - queueAndExecuteCommand: expect.anything(), - undo: expect.anything(), - redo: expect.anything(), - }); - }); - - it('should have called Edit Command "execute" method after creating the UndoRedoBuffer', () => { - extension.register(); - const undoRedoBuffer = extension.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', () => { - extension.register(); - const undoRedoBuffer = extension.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', () => { - extension.register(); - const undoRedoBuffer = extension.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', () => { - extension.register(); - const undoRedoBuffer = extension.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', () => { - extension.register(); - const undoRedoBuffer = extension.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', () => { - extension.register(); - extension.undoRedoBuffer.queueAndExecuteCommand(queueCallback); - expect(extension.commandQueue).toHaveLength(1); - }); - - it('should call a redo when Ctrl+Shift+Z keyboard event occurs', () => { - extension.register(); - const spy = jest.spyOn(queueCallback, 'execute'); - - extension.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', () => { - extension.register(); - const spy = jest.spyOn(queueCallback, 'undo'); - - extension.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', () => { - extension.register() as SlickCellExternalCopyManager; - const spy = jest.spyOn(extension.undoRedoBuffer, 'queueAndExecuteCommand'); - - extension.addonOptions!.clipboardCommandHandler!(queueCallback); - - expect(spy).toHaveBeenCalled(); - }); - - it('should expect "addItem" method to be called after calling "newRowCreator" callback', () => { - extension.register(); - const mockGetData = { addItem: jest.fn() }; - const getDataSpy = jest.spyOn(gridStub, 'getData').mockReturnValue(mockGetData); - const addItemSpy = jest.spyOn(mockGetData, 'addItem'); - - extension.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', () => { - extension.register(); - const output = extension.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); - extension.register(); - - const output = extension.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); - extension.register(); - - const output = extension.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); - extension.register(); - - const output = extension.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); - extension.register(); - - const output = extension.addonOptions!.dataItemColumnValueExtractor!({ firstName: 'John', lastName: 'Doe' }, { id: 'firstName', field: 'firstName' }); - - expect(output).toBeNull(); - }); - }); -}); diff --git a/packages/common/src/extensions/index.ts b/packages/common/src/extensions/index.ts index 128b46a32..b6fbc6a05 100644 --- a/packages/common/src/extensions/index.ts +++ b/packages/common/src/extensions/index.ts @@ -1,4 +1,3 @@ -export * from './cellExternalCopyManagerExtension'; export * from './checkboxSelectorExtension'; export * from './extensionUtility'; export * from './rowDetailViewExtension'; diff --git a/packages/common/src/interfaces/excelCopyBufferOption.interface.ts b/packages/common/src/interfaces/excelCopyBufferOption.interface.ts index b622c5e13..4c840e4c0 100644 --- a/packages/common/src/interfaces/excelCopyBufferOption.interface.ts +++ b/packages/common/src/interfaces/excelCopyBufferOption.interface.ts @@ -2,9 +2,9 @@ import { Column, CellRange, FormatterResultObject, - SlickCellExternalCopyManager, SlickEventData, } from './index'; +import { CellExcelCopyManager, } from '../plugins/cellExcelCopyManager'; export interface ExcelCopyBufferOption { /** defaults to "copied", sets the css className used for copied cells. */ @@ -48,7 +48,7 @@ export interface ExcelCopyBufferOption { // ------------ /** Fired after extension (plugin) is registered by SlickGrid */ - onExtensionRegistered?: (plugin: SlickCellExternalCopyManager) => void; + onExtensionRegistered?: (plugin: CellExcelCopyManager) => void; /** Fired when a copy cell is triggered */ onCopyCells?: (e: SlickEventData, args: { ranges: CellRange[] }) => void; diff --git a/packages/common/src/interfaces/slickCellRangeSelector.interface.ts b/packages/common/src/interfaces/slickCellRangeSelector.interface.ts index 6c4153e38..6475a7e55 100644 --- a/packages/common/src/interfaces/slickCellRangeSelector.interface.ts +++ b/packages/common/src/interfaces/slickCellRangeSelector.interface.ts @@ -1,6 +1,7 @@ import { SlickCellRangeDecorator, SlickGrid, SlickRange } from './index'; import { CellRange } from './cellRange.interface'; import { SlickEvent } from './slickEvent.interface'; +// import { CellRangeDecorator, } from '../plugins/slickCellRangeDecorator'; export interface SlickCellRangeSelector { pluginName: 'CellRangeSelector' diff --git a/packages/common/src/interfaces/slickRange.interface.ts b/packages/common/src/interfaces/slickRange.interface.ts index d9164b253..f904ffe86 100644 --- a/packages/common/src/interfaces/slickRange.interface.ts +++ b/packages/common/src/interfaces/slickRange.interface.ts @@ -12,33 +12,15 @@ export interface SlickRange extends CellRange { */ constructor: (fromRow: number, fromCell: number, toRow: number, toCell: number) => void; - /** - * Returns whether a range contains a given cell. - * @method contains - * @param row {Integer} - * @param cell {Integer} - * @return {Boolean} - */ - contains?: (row: number, cell: number) => boolean; + /** Returns whether a range represents a single row. */ + isSingleRow: () => boolean; - /** - * Returns whether a range represents a single cell. - * @method isSingleCell - * @return {Boolean} - */ - isSingleCell?: () => boolean; + /** Returns whether a range represents a single cell. */ + isSingleCell: () => boolean; - /** - * Returns whether a range represents a single row. - * @method isSingleRow - * @return {Boolean} - */ - isSingleRow?: () => boolean; + /** Returns whether a range contains a given cell. */ + contains: (row: number, cell: number) => boolean; - /** - * Returns a readable representation of a range. - * @method toString - * @return {String} - */ - toString?: () => string; + /** Returns a readable representation of a range. */ + toString: () => string; } diff --git a/packages/common/src/extensions/cellExternalCopyManagerExtension.ts b/packages/common/src/plugins/cellExcelCopyManager.ts similarity index 51% rename from packages/common/src/extensions/cellExternalCopyManagerExtension.ts rename to packages/common/src/plugins/cellExcelCopyManager.ts index 0d1537bd2..59f0b6374 100644 --- a/packages/common/src/extensions/cellExternalCopyManagerExtension.ts +++ b/packages/common/src/plugins/cellExcelCopyManager.ts @@ -1,38 +1,44 @@ -import 'slickgrid/plugins/slick.cellexternalcopymanager'; - import { + // TypeScript Helper + GetSlickEventType, + Column, EditCommand, EditUndoRedoBuffer, ExcelCopyBufferOption, - Extension, - SlickCellExternalCopyManager, - SlickCellSelectionModel, + GridOption, SlickDataView, SlickEventHandler, + SlickGrid, SlickNamespace, - - // TypeScript Helper - GetSlickEventType, } from '../interfaces/index'; -import { ExtensionUtility } from './extensionUtility'; import { BindingEventService } from '../services/bindingEvent.service'; -import { SharedService } from '../services/shared.service'; import { sanitizeHtmlToText } from '../services/domUtilities'; +import { CellExternalCopyManager, CellSelectionModel } from './index'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; -export class CellExternalCopyManagerExtension implements Extension { - private _addon: SlickCellExternalCopyManager | null = null; - private _addonOptions: ExcelCopyBufferOption | null = null; - private _cellSelectionModel!: SlickCellSelectionModel; - private _eventHandler: SlickEventHandler; - private _commandQueue!: EditCommand[]; - private _undoRedoBuffer!: EditUndoRedoBuffer; - private _bindingEventService: BindingEventService; - - constructor(private readonly extensionUtility: ExtensionUtility, private readonly sharedService: SharedService) { +/* + This manager enables users to copy/paste data from/to an external Spreadsheet application + such as MS-Excel® or OpenOffice-Spreadsheet. + + Since it is not possible to access directly the clipboard in javascript, the plugin uses + a trick to do it's job. After detecting the keystroke, we dynamically create a textarea + where the browser copies/pastes the serialized data. +*/ +export class CellExcelCopyManager { + protected _addonOptions!: ExcelCopyBufferOption; + protected _bindingEventService: BindingEventService; + protected _cellExternalCopyManagerPlugin!: CellExternalCopyManager; + protected _cellSelectionModel!: CellSelectionModel; + protected _commandQueue!: EditCommand[]; + protected _eventHandler: SlickEventHandler; + protected _grid!: SlickGrid; + protected _undoRedoBuffer!: EditUndoRedoBuffer; + pluginName = 'CellExcelCopyManager'; + + constructor() { this._eventHandler = new Slick.EventHandler() as SlickEventHandler; this._bindingEventService = new BindingEventService(); } @@ -49,74 +55,58 @@ export class CellExternalCopyManagerExtension implements Extension { return this._commandQueue; } - get undoRedoBuffer(): EditUndoRedoBuffer { - return this._undoRedoBuffer; - } - - /** Dispose of the 3rd party addon (plugin) */ - dispose() { - // unsubscribe all SlickGrid events - this._eventHandler.unsubscribeAll(); - if (this._addon && this._addon.destroy) { - this._addon.destroy(); - } - if (this._cellSelectionModel?.destroy) { - this._cellSelectionModel.destroy(); - } - this.extensionUtility.nullifyFunctionNameStartingWithOn(this._addonOptions); - this._addonOptions = null; - this._bindingEventService.unbindAll(); + get gridOptions(): GridOption { + return this._grid?.getOptions?.() ?? {}; } - /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance(): SlickCellExternalCopyManager | null { - return this._addon; + get undoRedoBuffer(): EditUndoRedoBuffer { + return this._undoRedoBuffer; } - /** Register the 3rd party addon (plugin) */ - register(): SlickCellExternalCopyManager | null { - if (this.sharedService && this.sharedService.slickGrid && this.sharedService.gridOptions) { - this.createUndoRedoBuffer(); - this._bindingEventService.bind(document.body, 'keydown', this.handleKeyDown.bind(this) as EventListener); - - this._addonOptions = { ...this.getDefaultOptions(), ...this.sharedService.gridOptions.excelCopyBufferOptions } as ExcelCopyBufferOption; - this._cellSelectionModel = new Slick.CellSelectionModel() as SlickCellSelectionModel; - this.sharedService.slickGrid.setSelectionModel(this._cellSelectionModel); - this._addon = new Slick.CellExternalCopyManager(this._addonOptions); - if (this._addon) { - this.sharedService.slickGrid.registerPlugin(this._addon); + init(grid: SlickGrid, options?: ExcelCopyBufferOption) { + this._grid = grid; + this.createUndoRedoBuffer(); + this._cellSelectionModel = new CellSelectionModel(); + this._grid.setSelectionModel(this._cellSelectionModel as any); + this._bindingEventService.bind(document.body, 'keydown', this.handleBodyKeyDown.bind(this) as EventListener); + this._addonOptions = { ...this.getDefaultOptions(), ...options } as ExcelCopyBufferOption; + this._cellExternalCopyManagerPlugin = new CellExternalCopyManager(); + this._cellExternalCopyManagerPlugin.init(this._grid, this._addonOptions); + + const onCopyCellsHandler = this._cellExternalCopyManagerPlugin.onCopyCells; + (this._eventHandler as SlickEventHandler>).subscribe(onCopyCellsHandler, (e, args) => { + if (this._addonOptions && typeof this._addonOptions.onCopyCells === 'function') { + this._addonOptions.onCopyCells(e, args); } + }); - // hook to all possible events - if (this.sharedService.slickGrid && this._addonOptions) { - if (this._addon && this._addonOptions.onExtensionRegistered) { - this._addonOptions.onExtensionRegistered(this._addon); - } + const onCopyCancelledHandler = this._cellExternalCopyManagerPlugin.onCopyCancelled; + (this._eventHandler as SlickEventHandler>).subscribe(onCopyCancelledHandler, (e, args) => { + if (this._addonOptions && typeof this._addonOptions.onCopyCancelled === 'function') { + this._addonOptions.onCopyCancelled(e, args); + } + }); - const onCopyCellsHandler = this._addon.onCopyCells; - (this._eventHandler as SlickEventHandler>).subscribe(onCopyCellsHandler, (e, args) => { - if (this._addonOptions && typeof this._addonOptions.onCopyCells === 'function') { - this._addonOptions.onCopyCells(e, args); - } - }); + const onPasteCellsHandler = this._cellExternalCopyManagerPlugin.onPasteCells; + (this._eventHandler as SlickEventHandler>).subscribe(onPasteCellsHandler, (e, args) => { + if (this._addonOptions && typeof this._addonOptions.onPasteCells === 'function') { + this._addonOptions.onPasteCells(e, args); + } + }); + } - const onCopyCancelledHandler = this._addon.onCopyCancelled; - (this._eventHandler as SlickEventHandler>).subscribe(onCopyCancelledHandler, (e, args) => { - if (this._addonOptions && typeof this._addonOptions.onCopyCancelled === 'function') { - this._addonOptions.onCopyCancelled(e, args); - } - }); + /** @deprecated @use `dispose` Destroy plugin. */ + destroy() { + this.dispose(); + } - const onPasteCellsHandler = this._addon.onPasteCells; - (this._eventHandler as SlickEventHandler>).subscribe(onPasteCellsHandler, (e, args) => { - if (this._addonOptions && typeof this._addonOptions.onPasteCells === 'function') { - this._addonOptions.onPasteCells(e, args); - } - }); - } - return this._addon; - } - return null; + /** Dispose of the 3rd party addon (plugin) */ + dispose() { + // unsubscribe all SlickGrid events + this._eventHandler.unsubscribeAll(); + this._bindingEventService.unbindAll(); + this._cellSelectionModel?.dispose(); + this._cellExternalCopyManagerPlugin?.dispose(); } /** Create an undo redo buffer used by the Excel like copy */ @@ -164,11 +154,11 @@ export class CellExternalCopyManagerExtension implements Extension { dataItemColumnValueExtractor: (item: any, columnDef: Column) => { // when grid or cell is not editable, we will possibly evaluate the Formatter if it was passed // to decide if we evaluate the Formatter, we will use the same flag from Export which is "exportWithFormatter" - if (!this.sharedService.gridOptions.editable || !columnDef.editor) { - const textExportOptions = { ...this.sharedService.gridOptions.exportOptions, ...this.sharedService.gridOptions.textExportOptions }; + if (!this.gridOptions.editable || !columnDef.editor) { + const textExportOptions = { ...this.gridOptions.exportOptions, ...this.gridOptions.textExportOptions }; const isEvaluatingFormatter = (columnDef.exportWithFormatter !== undefined) ? columnDef.exportWithFormatter : (textExportOptions?.exportWithFormatter); if (columnDef.formatter && isEvaluatingFormatter) { - const formattedOutput = columnDef.formatter(0, 0, item[columnDef.field], columnDef, item, this.sharedService.slickGrid); + const formattedOutput = columnDef.formatter(0, 0, item[columnDef.field], columnDef, item, this._grid); if (columnDef.sanitizeDataExport || (textExportOptions?.sanitizeDataExport)) { let outputString = formattedOutput as string; if (formattedOutput && typeof formattedOutput === 'object' && formattedOutput.hasOwnProperty('text')) { @@ -191,14 +181,14 @@ export class CellExternalCopyManagerExtension implements Extension { includeHeaderWhenCopying: false, newRowCreator: (count: number) => { for (let i = 0; i < count; i++) { - this.sharedService.slickGrid.getData().addItem({ id: `newRow_${newRowIds++}` }); + this._grid.getData().addItem({ id: `newRow_${newRowIds++}` }); } } }; } /** Hook an undo shortcut key hook that will redo/undo the copy buffer using Ctrl+(Shift)+Z keyboard events */ - private handleKeyDown(e: KeyboardEvent) { + private handleBodyKeyDown(e: KeyboardEvent) { const keyCode = e.keyCode || e.code; if (keyCode === 90 && (e.ctrlKey || e.metaKey)) { if (e.shiftKey) { @@ -208,4 +198,4 @@ export class CellExternalCopyManagerExtension implements Extension { } } } -} +} \ No newline at end of file diff --git a/packages/common/src/plugins/cellExternalCopyManager.ts b/packages/common/src/plugins/cellExternalCopyManager.ts new file mode 100644 index 000000000..33b0f64eb --- /dev/null +++ b/packages/common/src/plugins/cellExternalCopyManager.ts @@ -0,0 +1,469 @@ +import { CellRange, Column, ExcelCopyBufferOption, SlickGrid, SlickNamespace } from '../interfaces/index'; + +// using external SlickGrid JS libraries +declare const Slick: SlickNamespace; + +/* + This manager enables users to copy/paste data from/to an external Spreadsheet application + such as MS-Excel® or OpenOffice-Spreadsheet. + + Since it is not possible to access directly the clipboard in javascript, the plugin uses + a trick to do it's job. After detecting the keystroke, we dynamically create a textarea + 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 _onCopyInit?: () => void; + protected _onCopySuccess?: (rowCount: number) => void; + pluginName = 'CellExternalCopyManager'; + onCopyCells = new Slick.Event(); + onCopyCancelled = new Slick.Event(); + onPasteCells = new Slick.Event(); + + keyCodes = { + 'C': 67, + 'V': 86, + 'ESC': 27, + 'INSERT': 45 + }; + + 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)); + + // 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(() => { + this._grid.focus(); + }); + } + + /** @deprecated @use `dispose` Destroy plugin. */ + destroy() { + this.dispose(); + } + + dispose() { + this._grid.onKeyDown.unsubscribe(this.handleKeyDown.bind(this)); + } + + getHeaderValueForColumn(columnDef: Column) { + if (this._addonOptions.headerColumnValueExtractor) { + const val = this._addonOptions.headerColumnValueExtractor(columnDef); + if (val) { + return val; + } + } + + return columnDef.name; + } + + getDataItemValueForColumn(item: any, columnDef: Column, event: Event) { + if (this._addonOptions.dataItemColumnValueExtractor) { + const val = this._addonOptions.dataItemColumnValueExtractor(item, columnDef); + if (val) { + return val; + } + } + + 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]; + } + + return retVal; + } + + setDataItemValueForColumn(item: any, columnDef: Column, value: any): any | void { + 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 = { + container: document.body, // a dummy container + column: columnDef, + 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(); + } else { + item[columnDef.field] = value; + } + } + } + + 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; + } + + protected decodeTabularData(grid: SlickGrid, ta: HTMLTextAreaElement) { + const columns = grid.getColumns(); + const clipText = ta.value; + const clipRows = clipText.split(/[\n\f\r]/); + // trim trailing CR if present + if (clipRows[clipRows.length - 1] === '') { clipRows.pop(); } + + const clippedRange: any[] = []; + let j = 0; + + this._bodyElement.removeChild(ta); + for (let i = 0; i < clipRows.length; i++) { + if (clipRows[i] !== '') { + clippedRange[j++] = clipRows[i].split('\t'); + } else { + clippedRange[j++] = ['']; + } + } + const selectedCell = this._grid.getActiveCell(); + const ranges = this._grid.getSelectionModel().getSelectedRanges(); + const selectedRange = ranges && ranges.length ? ranges[0] : null; // pick only one selection + let activeRow: number; + let activeCell: number; + + if (selectedRange) { + activeRow = selectedRange.fromRow; + activeCell = selectedRange.fromCell; + } else if (selectedCell) { + activeRow = selectedCell.row; + activeCell = selectedCell.cell; + } else { + return; // we don't know where to paste + } + + let oneCellToMultiple = false; + let destH = clippedRange.length; + let destW = clippedRange.length ? clippedRange[0].length : 0; + if (clippedRange.length === 1 && clippedRange[0].length === 1 && selectedRange) { + oneCellToMultiple = true; + destH = selectedRange.toRow - selectedRange.fromRow + 1; + destW = selectedRange.toCell - selectedRange.fromCell + 1; + } + const availableRows = (this._grid.getData() as any[]).length - activeRow; + let addRows = 0; + + // ignore new rows if we don't have a "newRowCreator" + if (availableRows < destH && this._addonOptions.newRowCreator) { + const d: any[] = this._grid.getData(); + 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 newRowsNeeded = activeRow + destH - this._grid.getDataLength(); + this._addonOptions.newRowCreator(newRowsNeeded); + } + + this._clipCommand = { + isClipboardCommand: true, + clippedRange, + oldValues: [], + cellExternalCopyManager: this, + _options: this._addonOptions, + setDataItemValueForColumn: this.setDataItemValueForColumn, + markCopySelection: this.markCopySelection, + oneCellToMultiple, + activeRow, + activeCell, + destH, + destW, + maxDestY: this._grid.getDataLength(), + maxDestX: this._grid.getColumns().length, + h: 0, + w: 0, + + execute: () => { + this._clipCommand.h = 0; + for (let y = 0; y < this._clipCommand.destH; y++) { + this._clipCommand.oldValues[y] = []; + this._clipCommand.w = 0; + this._clipCommand.h++; + for (let x = 0; x < this._clipCommand.destW; x++) { + this._clipCommand.w++; + const desty = activeRow + y; + const destx = activeCell + x; + + if (desty < this._clipCommand.maxDestY && destx < this._clipCommand.maxDestX) { + // const nd = this._grid.getCellNode(desty, destx); + const dt = this._grid.getDataItem(desty); + this._clipCommand.oldValues[y][x] = dt[columns[destx]['field']]; + if (oneCellToMultiple) { + this.setDataItemValueForColumn(dt, columns[destx], clippedRange[0][0]); + } else { + this.setDataItemValueForColumn(dt, columns[destx], clippedRange[y] ? clippedRange[y][x] : ''); + } + this._grid.updateCell(desty, destx); + this._grid.onCellChange.notify({ + row: desty, + cell: destx, + item: dt, + grid: this._grid, + column: {} as unknown as Column, + }); + + } + } + } + + const bRange = { + fromCell: activeCell, + fromRow: activeRow, + 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] }); + }, + + undo: () => { + for (let y = 0; y < this._clipCommand.destH; y++) { + for (let x = 0; x < this._clipCommand.destW; x++) { + const desty = activeRow + y; + const destx = activeCell + x; + + if (desty < this._clipCommand.maxDestY && destx < this._clipCommand.maxDestX) { + // const nd = this._grid.getCellNode(desty, destx); + const dt = this._grid.getDataItem(desty); + if (oneCellToMultiple) { + this.setDataItemValueForColumn(dt, columns[destx], this._clipCommand.oldValues[0][0]); + } else { + this.setDataItemValueForColumn(dt, columns[destx], this._clipCommand.oldValues[y][x]); + } + this._grid.updateCell(desty, destx); + this._grid.onCellChange.notify({ + row: desty, + cell: destx, + item: dt, + grid: this._grid, + column: {} as unknown as Column, + }); + } + } + } + + const bRange = { + fromCell: activeCell, + fromRow: activeRow, + 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] }); + if (this._addonOptions.onPasteCells) { + this._addonOptions.onPasteCells.call(this, new Slick.EventData(), { ranges: [bRange] }); + } + + if (addRows > 1) { + const d = this._grid.getData() as any[]; + for (; addRows > 1; addRows--) { + d.splice(d.length - 1, 1); + } + this._grid.setData(d); + this._grid.render(); + } + } + }; + + if (this._addonOptions.clipboardCommandHandler) { + this._addonOptions.clipboardCommandHandler(this._clipCommand); + } + else { + this._clipCommand.execute(); + } + } + + + handleKeyDown(e: any): boolean | void { + let ranges; + if (!this._grid.getEditorLock().isActive() || this._grid.getOptions().autoEdit) { + if (e.which === this.keyCodes.ESC) { + 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 }); + } + 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(); + } + 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 }); + } + + const columns = this._grid.getColumns(); + let clipText = ''; + + for (let rg = 0; rg < ranges.length; rg++) { + const range = ranges[rg]; + const clipTextRows = []; + for (let i = range.fromRow; i < range.toRow + 1; i++) { + const clipTextCells = []; + const dt = this._grid.getDataItem(i); + + if (clipTextRows.length === 0 && this._addonOptions.includeHeaderWhenCopying) { + const clipTextHeaders = []; + for (let j = range.fromCell; j < range.toCell + 1; j++) { + if (columns[j].name!.length > 0) { + clipTextHeaders.push(this.getHeaderValueForColumn(columns[j])); + } + } + clipTextRows.push(clipTextHeaders.join('\t')); + } + + for (let j = range.fromCell; j < range.toCell + 1; j++) { + clipTextCells.push(this.getDataItemValueForColumn(dt, columns[j], e)); + } + clipTextRows.push(clipTextCells.join('\t')); + } + clipText += clipTextRows.join('\r\n') + '\r\n'; + } + + 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(); + + setTimeout(() => { + this._bodyElement.removeChild(ta); + // restore focus + if (focusEl) { + focusEl.focus(); + } else { + console.log('Not element to restore focus to after copy?'); + } + }, 100); + + if (this._onCopySuccess) { + let rowCount = 0; + // 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); + } + + return false; + } + } + } + + if (!this._addonOptions.readOnlyMode && ( + (e.which === this.keyCodes.V && (e.ctrlKey || e.metaKey) && !e.shiftKey) + || (e.which === this.keyCodes.INSERT && e.shiftKey && !e.ctrlKey) + )) { // CTRL+V or Shift+INS + const ta = this.createTextBox(''); + + setTimeout(() => { + this.decodeTabularData(this._grid, ta); + }, 100); + + return false; + } + } + } + + 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++) { + hash[j] = {}; + for (let k = ranges[i].fromCell; k <= ranges[i].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; + } +} \ No newline at end of file diff --git a/packages/common/src/plugins/cellRangeDecorator.ts b/packages/common/src/plugins/cellRangeDecorator.ts new file mode 100644 index 000000000..07fb7e9c5 --- /dev/null +++ b/packages/common/src/plugins/cellRangeDecorator.ts @@ -0,0 +1,75 @@ +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; + +/** + * Displays an overlay on top of a given cell range. + * TODO: + * Currently, it blocks mouse events to DOM nodes behind it. + * Use FF and WebKit-specific "pointer-events" CSS style, or some kind of event forwarding. + * Could also construct the borders separately using 4 individual DIVs. + */ +export class CellRangeDecorator { + protected _addonOptions!: CellRangeDecoratorOption; + protected _elem?: HTMLElement | null; + protected _grid?: SlickGrid; + protected _defaults = { + selectionCssClass: 'slick-range-decorator', + selectionCss: { + border: '2px dashed red', + zIndex: '9999', + }, + offset: { top: -1, left: -1, height: -2, width: -2 } + } as CellRangeDecoratorOption; + pluginName = 'CellRangeDecorator'; + + constructor(grid: SlickGrid, options: any) { + this._addonOptions = { ...this._defaults, ...options }; + this._grid = grid; + } + + /** @deprecated @use `dispose` Destroy plugin. */ + destroy() { + this.dispose(); + } + + /** Dispose the plugin. */ + dispose() { + this.hide(); + } + + show(range: CellRange) { + if (!this._elem) { + this._elem = document.createElement('div'); + this._elem.className = this._addonOptions.selectionCssClass; + Object.keys(this._addonOptions.selectionCss as CSSStyleDeclaration).forEach((cssStyleKey) => { + this._elem!.style[cssStyleKey as CSSStyleDeclarationWritable] = this._addonOptions.selectionCss[cssStyleKey as CSSStyleDeclarationWritable]; + }); + this._elem.style.position = 'absolute'; + this._grid?.getActiveCanvasNode().appendChild(this._elem); + } + + const from = this._grid?.getCellNodeBox(range.fromRow, range.fromCell); + const to = this._grid?.getCellNodeBox(range.toRow, range.toCell); + + if (from && to && this._addonOptions?.offset) { + this._elem.style.top = `${from.top + this._addonOptions.offset.top}px`; + this._elem.style.left = `${from.left + this._addonOptions.offset.left}px`; + this._elem.style.height = `${to.bottom - from.top + this._addonOptions.offset.height}px`; + this._elem.style.width = `${to.right - from.left + this._addonOptions.offset.width}px`; + } + 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 new file mode 100644 index 000000000..551c845a6 --- /dev/null +++ b/packages/common/src/plugins/cellRangeSelector.ts @@ -0,0 +1,197 @@ +import { CellRange, emptyElement, getHtmlElementOffset, } from '..'; +import { DOMMouseEvent, GridOption, 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 _canvas!: HTMLElement; + protected _grid!: SlickGrid; + protected _gridOptions!: GridOption; + protected _activeCanvas?: HTMLElement; + protected _dragging = false; + protected _decorator!: CellRangeDecorator; + protected _eventHandler = new Slick.EventHandler(); + + // Frozen row & column constiables + protected _rowOffset: any; + protected _columnOffset: any; + protected _isRightCanvas = false; + protected _isBottomCanvas = false; + + // Scrollings + protected _scrollTop = 0; + protected _scrollLeft = 0; + protected _defaults = { + selectionCss: { + border: '2px dashed blue' + } as CSSStyleDeclaration + }; + pluginName = 'CellRangeSelector'; + onBeforeCellRangeSelected = new Slick.Event(); + onCellRangeSelected = new Slick.Event<{ range: CellRange; }>(); + + constructor(options: any) { + this._addonOptions = { ...this._defaults, ...options }; + } + + 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._eventHandler + .subscribe(this._grid.onScroll, this.handleScroll.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); + } + + destroy() { + this._eventHandler.unsubscribeAll(); + emptyElement(this._activeCanvas); + emptyElement(this._canvas); + if (this._decorator?.destroy) { + this._decorator.destroy(); + } + } + + getCellDecorator() { + return this._decorator; + } + + handleScroll(_e: DOMMouseEvent, args: { scrollTop: number; scrollLeft: number; }) { + this._scrollTop = args.scrollTop; + this._scrollLeft = args.scrollLeft; + } + + handleDragInit(e: any) { + // Set the active canvas node because the decorator needs to append its + // box to the correct canvas + this._activeCanvas = this._grid.getActiveCanvasNode(e); + + this._rowOffset = 0; + this._columnOffset = 0; + 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; + } + + 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; + } + + // 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)) { + this._dragging = true; + e.stopImmediatePropagation(); + } + } + if (!this._dragging) { + return; + } + + this._grid.focus(); + + let startX = dd.startX - (getHtmlElementOffset(this._canvas)?.left ?? 0); + if (this._gridOptions.frozenColumn! >= 0 && this._isRightCanvas) { + startX += this._scrollLeft; + } + + let startY = dd.startY - (getHtmlElementOffset(this._canvas)?.top ?? 0); + if (this._gridOptions.frozenRow! >= 0 && this._isBottomCanvas) { + startY += this._scrollTop; + } + + 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; + } +} \ No newline at end of file diff --git a/packages/common/src/plugins/cellSelectionModel.ts b/packages/common/src/plugins/cellSelectionModel.ts new file mode 100644 index 000000000..fbd184216 --- /dev/null +++ b/packages/common/src/plugins/cellSelectionModel.ts @@ -0,0 +1,189 @@ +import { CellRange, OnActiveCellChangedEventArgs, SlickGrid, SlickNamespace, SlickRange, } from '../interfaces/index'; +import { CellRangeSelector } from './index'; + +// using external SlickGrid JS libraries +declare const Slick: SlickNamespace; + +export class CellSelectionModel { + protected _addonOptions: any; + protected _grid!: SlickGrid; + protected _canvas: HTMLElement | null = null; + protected _ranges: SlickRange[] = []; + protected _selector: CellRangeSelector; + protected _defaults = { + selectActiveCell: true, + }; + 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' + } + }); + } else { + this._selector = options.cellRangeSelector; + } + this._addonOptions = options; + } + + + 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)); + grid.registerPlugin(this._selector); + this._selector.onCellRangeSelected.subscribe(this.handleCellRangeSelected.bind(this)); + this._selector.onBeforeCellRangeSelected.subscribe(this.handleBeforeCellRangeSelected.bind(this)); + } + + /** @deprecated @use `dispose` Destroy plugin. */ + destroy() { + this.dispose(); + } + + 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(); + } + + 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; + } + + rangesAreEqual(range1: any, range2: any) { + let areDifferent = (range1.length !== range2.length); + if (!areDifferent) { + for (let i = 0; i < range1.length; i++) { + 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 + ) { + areDifferent = true; + break; + } + } + } + return !areDifferent; + } + + setSelectedRanges(ranges: any[]) { + // simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged + 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); + + this._ranges = this.removeInvalidRanges(ranges); + if (rangeHasChanged) { + this.onSelectedRangesChanged.notify(this._ranges); + } + } + + getSelectedRanges() { + return this._ranges; + } + + handleBeforeCellRangeSelected(e: any): boolean | void { + if (this._grid.getEditorLock().isActive()) { + e.stopPropagation(); + return false; + } + } + + handleCellRangeSelected(_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([]); + } + } + + handleKeyDown(e: any) { + /*** + * Кey codes + * 37 left + * 38 up + * 39 right + * 40 down + */ + 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)) { + + ranges = this.getSelectedRanges().slice(); + if (!ranges.length) { + ranges.push(new Slick.Range(active.row, active.cell)); + } + // 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; + } + + // 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(); + } + } +} \ No newline at end of file diff --git a/packages/common/src/plugins/index.ts b/packages/common/src/plugins/index.ts index c1371eef1..0c0bcf576 100644 --- a/packages/common/src/plugins/index.ts +++ b/packages/common/src/plugins/index.ts @@ -1,5 +1,10 @@ export * from './autoTooltip.plugin'; +export * from './cellExcelCopyManager'; +export * from './cellExternalCopyManager'; export * from './cellMenu.plugin'; +export * from './cellRangeDecorator'; +export * from './cellRangeSelector'; +export * from './cellSelectionModel'; export * from './contextMenu.plugin'; export * from './draggableGrouping.plugin'; export * from './headerButton.plugin'; diff --git a/packages/common/src/services/__tests__/extension.service.spec.ts b/packages/common/src/services/__tests__/extension.service.spec.ts index 47aadb520..ac387f7a0 100644 --- a/packages/common/src/services/__tests__/extension.service.spec.ts +++ b/packages/common/src/services/__tests__/extension.service.spec.ts @@ -3,7 +3,6 @@ import 'jest-extended'; import { ExtensionName } from '../../enums/index'; import { Column, ExtensionModel, GridOption, SlickGrid, SlickNamespace } from '../../interfaces/index'; import { - CellExternalCopyManagerExtension, CheckboxSelectorExtension, ExtensionUtility, RowDetailViewExtension, @@ -145,7 +144,6 @@ describe('ExtensionService', () => { sortServiceStub, treeDataServiceStub, // extensions - extensionStub as unknown as CellExternalCopyManagerExtension, extensionCheckboxSelectorStub as unknown as CheckboxSelectorExtension, extensionStub as unknown as RowDetailViewExtension, extensionRowMoveStub as unknown as RowMoveManagerExtension, @@ -504,18 +502,18 @@ 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 gridOptionsMock = { enableExcelCopyBuffer: true } as GridOption; + // const extSpy = jest.spyOn(extensionStub, 'register').mockReturnValue(instanceMock); + // const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - service.bindDifferentExtensions(); - const output = service.getExtensionByName(ExtensionName.cellExternalCopyManager); + // service.bindDifferentExtensions(); + // const output = service.getExtensionByName(ExtensionName.cellExternalCopyManager); - expect(gridSpy).toHaveBeenCalled(); - expect(extSpy).toHaveBeenCalled(); - expect(output).toEqual({ name: ExtensionName.cellExternalCopyManager, instance: instanceMock as unknown, class: extensionStub } as ExtensionModel); - }); + // expect(gridSpy).toHaveBeenCalled(); + // expect(extSpy).toHaveBeenCalled(); + // expect(output).toEqual({ name: ExtensionName.cellExternalCopyManager, instance: instanceMock as unknown, class: extensionStub } as ExtensionModel); + // }); }); describe('createExtensionsBeforeGridCreation method', () => { @@ -851,7 +849,6 @@ describe('ExtensionService', () => { sortServiceStub, treeDataServiceStub, // extensions - extensionStub as unknown as CellExternalCopyManagerExtension, extensionStub as unknown as CheckboxSelectorExtension, extensionStub as unknown as RowDetailViewExtension, extensionStub as unknown as RowMoveManagerExtension, diff --git a/packages/common/src/services/__tests__/grid.service.spec.ts b/packages/common/src/services/__tests__/grid.service.spec.ts index abcb46577..7493291a0 100644 --- a/packages/common/src/services/__tests__/grid.service.spec.ts +++ b/packages/common/src/services/__tests__/grid.service.spec.ts @@ -14,7 +14,6 @@ const mockSelectionModel = { const mockSelectionModelImplementation = jest.fn().mockImplementation(() => mockSelectionModel); jest.mock('flatpickr', () => { }); -jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockSelectionModelImplementation); Slick.RowSelectionModel = mockSelectionModelImplementation; const filterServiceStub = { @@ -95,6 +94,7 @@ const treeDataServiceStub = { } as unknown as TreeDataService; describe('Grid Service', () => { + jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockSelectionModelImplementation); let service: GridService; const sharedService = new SharedService(); const mockGridOptions = { enableAutoResize: true } as GridOption; diff --git a/packages/common/src/services/extension.service.ts b/packages/common/src/services/extension.service.ts index 9b54662ed..345aea416 100644 --- a/packages/common/src/services/extension.service.ts +++ b/packages/common/src/services/extension.service.ts @@ -6,7 +6,6 @@ import 'slickgrid/plugins/slick.cellselectionmodel'; import { Column, Extension, ExtensionModel, GridOption, SlickRowSelectionModel, } from '../interfaces/index'; import { ColumnReorderFunction, ExtensionList, ExtensionName, SlickControlList, SlickPluginList } from '../enums/index'; import { - CellExternalCopyManagerExtension, CheckboxSelectorExtension, ExtensionUtility, RowDetailViewExtension, @@ -15,8 +14,16 @@ import { } from '../extensions/index'; import { SharedService } from './shared.service'; import { TranslaterService } from './translater.service'; -import { AutoTooltipPlugin, CellMenuPlugin, ContextMenuPlugin, DraggableGroupingPlugin, HeaderButtonPlugin, HeaderMenuPlugin } from '../plugins/index'; import { ColumnPickerControl, GridMenuControl } from '../controls/index'; +import { + AutoTooltipPlugin, + CellExcelCopyManager, + CellMenuPlugin, + ContextMenuPlugin, + DraggableGroupingPlugin, + HeaderButtonPlugin, + HeaderMenuPlugin +} from '../plugins/index'; import { FilterService } from './filter.service'; import { GroupItemMetadataProviderService } from './groupItemMetadataProvider.service'; import { PubSubService } from './pubSub.service'; @@ -31,6 +38,7 @@ interface ExtensionWithColumnIndexPosition { export class ExtensionService { protected _cellMenuPlugin?: CellMenuPlugin; + protected _cellExcelCopyManagerPlugin?: CellExcelCopyManager; protected _contextMenuPlugin?: ContextMenuPlugin; protected _columnPickerControl?: ColumnPickerControl; protected _draggleGroupingPlugin?: DraggableGroupingPlugin; @@ -55,7 +63,6 @@ export class ExtensionService { protected readonly sortService: SortService, protected readonly treeDataService: TreeDataService, - protected readonly cellExternalCopyExtension: CellExternalCopyManagerExtension, protected readonly checkboxSelectorExtension: CheckboxSelectorExtension, protected readonly rowDetailViewExtension: RowDetailViewExtension, protected readonly rowMoveManagerExtension: RowMoveManagerExtension, @@ -152,11 +159,13 @@ export class ExtensionService { } // Cell External Copy Manager Plugin (Excel Like) - if (this.gridOptions.enableExcelCopyBuffer && this.cellExternalCopyExtension && this.cellExternalCopyExtension.register) { - const instance = this.cellExternalCopyExtension.register(); - if (instance) { - this._extensionList[ExtensionName.cellExternalCopyManager] = { name: ExtensionName.cellExternalCopyManager, class: this.cellExternalCopyExtension, instance }; + if (this.gridOptions.enableExcelCopyBuffer) { + this._cellExcelCopyManagerPlugin = new CellExcelCopyManager(); + this._cellExcelCopyManagerPlugin.init(this.sharedService.slickGrid, this.sharedService.gridOptions.excelCopyBufferOptions); + if (this.gridOptions.excelCopyBufferOptions?.onExtensionRegistered) { + this.gridOptions.excelCopyBufferOptions.onExtensionRegistered(this._cellExcelCopyManagerPlugin); } + this._extensionList[ExtensionName.cellExternalCopyManager] = { name: ExtensionName.cellExternalCopyManager, class: this._cellExcelCopyManagerPlugin, instance: this._cellExcelCopyManagerPlugin }; } // (Action) Cell Menu Plugin diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts index 1503a6fe2..2d5c0fadc 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts @@ -33,10 +33,8 @@ import { ServicePagination, SharedService, SlickDataView, - SlickDraggableGrouping, SlickEventHandler, SlickGrid, - SlickGroupItemMetadataProvider, SortService, TreeDataService, TranslaterService, @@ -212,12 +210,6 @@ const mockDataView = { syncGridSelection: jest.fn(), } as unknown as SlickDataView; -const mockDraggableGroupingExtension = { - constructor: jest.fn(), - init: jest.fn(), - destroy: jest.fn(), -} as unknown as SlickDraggableGrouping; - const mockEventPubSub = { notify: jest.fn(), subscribe: jest.fn(), @@ -272,16 +264,13 @@ const mockGrid = { const mockSlickEventHandlerImplementation = jest.fn().mockImplementation(() => mockSlickEventHandler); const mockDataViewImplementation = jest.fn().mockImplementation(() => mockDataView); const mockGridImplementation = jest.fn().mockImplementation(() => mockGrid); -const mockDraggableGroupingImplementation = jest.fn().mockImplementation(() => mockDraggableGroupingExtension); const template = `
`; describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () => { jest.mock('slickgrid/slick.grid', () => mockGridImplementation); - jest.mock('slickgrid/plugins/slick.draggablegrouping', () => mockDraggableGroupingImplementation); Slick.Grid = mockGridImplementation; Slick.EventHandler = slickEventHandler; Slick.Data = { DataView: mockDataViewImplementation, }; - Slick.DraggableGrouping = mockDraggableGroupingImplementation; let component: SlickVanillaGridBundle; let divContainer: HTMLDivElement; @@ -2141,10 +2130,8 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with a Hierarchical Dataset', () => { jest.mock('slickgrid/slick.grid', () => mockGridImplementation); - jest.mock('slickgrid/plugins/slick.draggablegrouping', () => mockDraggableGroupingImplementation); Slick.Grid = mockGridImplementation; Slick.Data = { DataView: mockDataViewImplementation, }; - Slick.DraggableGrouping = mockDraggableGroupingImplementation; let component: SlickVanillaGridBundle; let divContainer: HTMLDivElement; @@ -2225,11 +2212,9 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with a Slickgrid Container that already exist', () => { jest.mock('slickgrid/slick.grid', () => mockGridImplementation); - jest.mock('slickgrid/plugins/slick.draggablegrouping', () => mockDraggableGroupingImplementation); Slick.Grid = mockGridImplementation; Slick.EventHandler = mockSlickEventHandlerImplementation; Slick.Data = { DataView: mockDataViewImplementation, }; - Slick.DraggableGrouping = mockDraggableGroupingImplementation; let component: SlickVanillaGridBundle; let divContainer: HTMLDivElement; diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts index d2c316437..8371837c5 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -31,7 +31,6 @@ import { // extensions CheckboxSelectorExtension, - CellExternalCopyManagerExtension, ExtensionUtility, RowDetailViewExtension, RowSelectionExtension, @@ -350,7 +349,6 @@ export class SlickVanillaGridBundle { this.paginationService = services?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService); // extensions - const cellExternalCopyManagerExtension = new CellExternalCopyManagerExtension(this.extensionUtility, this.sharedService); const checkboxExtension = new CheckboxSelectorExtension(this.sharedService); const rowDetailViewExtension = new RowDetailViewExtension(); const rowMoveManagerExtension = new RowMoveManagerExtension(this.sharedService); @@ -362,7 +360,6 @@ export class SlickVanillaGridBundle { this._eventPubSubService, this.sortService, this.treeDataService, - cellExternalCopyManagerExtension, checkboxExtension, rowDetailViewExtension, rowMoveManagerExtension,