diff --git a/README.md b/README.md index 4a18ed9e0..fe1209c27 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ You might be wondering why was this monorepo created? Here are a few of the reas The Vanilla Implementation (not associated to any framework) is built with [WebPack](https://webpack.js.org/) and is also used to test all the UI functionalities [Cypress](https://www.cypress.io/) (E2E tests). This [Vanilla bundle](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/vanilla-bundle) package is also what we use in our SalesForce implementation (with Lightning Web Component), hence the creation of this monorepo library. ### Fully Tested with [Jest](https://jestjs.io/) (Unit Tests) - [Cypress](https://www.cypress.io/) (E2E Tests) -Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +13,000 lines of code (+3,000 unit tests) that are fully tested with [Jest](https://jestjs.io/). There are also +300 Cypress E2E tests to cover all [Examples](https://ghiscoding.github.io/slickgrid-universal/) and most UI functionalities (there's also an additional +500 tests in Aurelia-Slickgrid) +Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +15,000 lines of code (+3,700 unit tests) that are fully tested with [Jest](https://jestjs.io/). There are also +400 Cypress E2E tests to cover all [Examples](https://ghiscoding.github.io/slickgrid-universal/) and most UI functionalities (there's also an additional +500 tests in Aurelia-Slickgrid) ### Available Demos diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts index 065f0b513..6732b3a0a 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts @@ -255,6 +255,7 @@ export class Example7 { singleRowMove: true, disableRowSelection: true, cancelEditOnDrag: true, + hideRowMoveShadow: false, onBeforeMoveRows: this.onBeforeMoveRow.bind(this), onMoveRows: this.onMoveRows.bind(this), diff --git a/packages/common/src/enums/index.ts b/packages/common/src/enums/index.ts index 3fdd8fa67..83725b536 100644 --- a/packages/common/src/enums/index.ts +++ b/packages/common/src/enums/index.ts @@ -23,4 +23,5 @@ export * from './slickPluginList.enum'; export * from './sortDirection.enum'; export * from './sortDirectionNumber.enum'; export * from './sortDirectionString.type'; -export * from './toggleStateChangeType'; \ No newline at end of file +export * from './toggleStateChangeType'; +export * from './usabilityOverrideFn.type'; \ No newline at end of file diff --git a/packages/common/src/enums/slickPluginList.enum.ts b/packages/common/src/enums/slickPluginList.enum.ts index 2a83b4874..5956c3b83 100644 --- a/packages/common/src/enums/slickPluginList.enum.ts +++ b/packages/common/src/enums/slickPluginList.enum.ts @@ -2,7 +2,6 @@ import { SlickEditorLock, SlickGroupItemMetadataProvider, SlickRowDetailView, - SlickRowMoveManager, } from '../interfaces/index'; import { SlickAutoTooltip, @@ -16,6 +15,7 @@ import { SlickDraggableGrouping, SlickHeaderButtons, SlickHeaderMenu, + SlickRowMoveManager, SlickRowSelectionModel, } from '../plugins/index'; diff --git a/packages/common/src/enums/usabilityOverrideFn.type.ts b/packages/common/src/enums/usabilityOverrideFn.type.ts new file mode 100644 index 000000000..4d5abae56 --- /dev/null +++ b/packages/common/src/enums/usabilityOverrideFn.type.ts @@ -0,0 +1,3 @@ +import { SlickGrid } from '../interfaces/slickGrid.interface'; + +export type UsabilityOverrideFn = (row: number, dataContext: any, grid: SlickGrid) => boolean; \ No newline at end of file diff --git a/packages/common/src/extensions/__tests__/rowMoveManagerExtension.spec.ts b/packages/common/src/extensions/__tests__/rowMoveManagerExtension.spec.ts deleted file mode 100644 index c8ff442e6..000000000 --- a/packages/common/src/extensions/__tests__/rowMoveManagerExtension.spec.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { RowMoveManagerExtension } from '../rowMoveManagerExtension'; -import { SharedService } from '../../services/shared.service'; -import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; -import { Column, GridOption, RowMoveManager, SlickGrid, SlickNamespace, SlickRowMoveManager } from '../../interfaces/index'; -import { SlickRowSelectionModel } from '../../plugins/slickRowSelectionModel'; - -declare const Slick: SlickNamespace; - -const gridStub = { - getOptions: jest.fn(), - getSelectionModel: jest.fn(), - registerPlugin: jest.fn(), - setSelectionModel: jest.fn(), -} as unknown as SlickGrid; - -const mockAddon = jest.fn().mockImplementation(() => ({ - init: jest.fn(), - destroy: jest.fn(), - getColumnDefinition: jest.fn(), - onBeforeMoveRows: new Slick.Event(), - onMoveRows: new Slick.Event(), -})); - -const mockRowSelectionModel = { - constructor: jest.fn(), - init: jest.fn(), - destroy: jest.fn(), - dispose: jest.fn(), - getSelectedRows: jest.fn(), - setSelectedRows: jest.fn(), - getSelectedRanges: jest.fn(), - setSelectedRanges: jest.fn(), - onSelectedRangesChanged: new Slick.Event(), -} as unknown as SlickRowSelectionModel; - -jest.mock('../../plugins/slickRowSelectionModel', () => ({ - SlickRowSelectionModel: jest.fn().mockImplementation(() => mockRowSelectionModel), -})); - -describe('rowMoveManagerExtension', () => { - jest.mock('slickgrid/plugins/slick.rowmovemanager', () => mockAddon); - Slick.RowMoveManager = mockAddon; - - let sharedService: SharedService; - let translateService: TranslateServiceStub; - let extension: RowMoveManagerExtension; - const gridOptionsMock = { - enableRowMoveManager: true, - rowMoveManager: { - cancelEditOnDrag: true, - singleRowMove: true, - disableRowSelection: true, - onExtensionRegistered: jest.fn(), - onBeforeMoveRows: () => { }, - onMoveRows: () => { }, - }, - } as GridOption; - - beforeEach(() => { - sharedService = new SharedService(); - translateService = new TranslateServiceStub(); - extension = new RowMoveManagerExtension(sharedService); - }); - - it('should return null after calling "create" method when either the column definitions or the grid options is missing', () => { - const output = extension.create([] as Column[], null as any); - expect(output).toBeNull(); - }); - - it('should return null after calling "loadAddonWhenNotExists" method when either the column definitions or the grid options is missing', () => { - const output = extension.loadAddonWhenNotExists([] as Column[], null as any); - expect(output).toBeNull(); - }); - - it('should return null after calling "register" method when either the grid object or the grid options is missing', () => { - const output = extension.register(); - expect(output).toBeNull(); - }); - - describe('create method', () => { - let columnsMock: Column[]; - - beforeEach(() => { - columnsMock = [ - { id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, - { id: 'field2', field: 'field2', width: 50 } - ]; - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should add a reserved column for icons in 1st column index', () => { - const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock) as SlickRowMoveManager; - const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' }); - extension.create(columnsMock, gridOptionsMock); - - expect(spy).toHaveBeenCalled(); - expect(columnsMock).toEqual([ - { - excludeFromColumnPicker: true, - excludeFromExport: true, - excludeFromGridMenu: true, - excludeFromHeaderMenu: true, - excludeFromQuery: true, - field: 'move', - id: '_move' - }, - { id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, - { id: 'field2', field: 'field2', width: 50 }, - ]); - }); - - it('should NOT add the move icon column if it already exist in the column definitions', () => { - columnsMock = [{ - id: '_move', name: '', field: 'move', width: 40, - behavior: 'selectAndMove', selectable: false, resizable: false, cssClass: '', - formatter: () => ({ addClasses: 'cell-reorder dnd' }) - }, ...columnsMock] as Column[]; - const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock) as SlickRowMoveManager; - const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' }); - extension.create(columnsMock, gridOptionsMock); - - expect(spy).toHaveBeenCalled(); - expect(columnsMock).toEqual([ - { - behavior: 'selectAndMove', - cssClass: '', - field: 'move', - formatter: expect.anything(), - id: '_move', - name: '', - resizable: false, - selectable: false, - width: 40, - excludeFromColumnPicker: true, - excludeFromExport: true, - excludeFromGridMenu: true, - excludeFromHeaderMenu: true, - excludeFromQuery: true, - }, - { id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, - { id: 'field2', field: 'field2', width: 50 }, - ]); - }); - - it('should expect the column to be at a different column index position when "columnIndexPosition" is defined', () => { - gridOptionsMock.rowMoveManager!.columnIndexPosition = 2; - const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock) as SlickRowMoveManager; - const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' }); - extension.create(columnsMock, gridOptionsMock); - - expect(spy).toHaveBeenCalled(); - expect(columnsMock).toEqual([ - { id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, - { id: 'field2', field: 'field2', width: 50 }, - { - excludeFromColumnPicker: true, - excludeFromExport: true, - excludeFromGridMenu: true, - excludeFromHeaderMenu: true, - excludeFromQuery: true, - field: 'move', - id: '_move' - }, - ]); - }); - }); - - describe('registered addon', () => { - let columnsMock: Column[]; - - beforeEach(() => { - columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }]; - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - jest.clearAllMocks(); - }); - - it('should register the addon', () => { - const onRegisteredSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager as RowMoveManager, 'onExtensionRegistered'); - const pluginSpy = jest.spyOn(SharedService.prototype.slickGrid, 'registerPlugin'); - - const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock) as SlickRowMoveManager; - extension.create(columnsMock, gridOptionsMock); - extension.register(); - const addonInstance = extension.getAddonInstance(); - - expect(instance).toBeTruthy(); - expect(instance).toEqual(addonInstance); - expect(mockAddon).toHaveBeenCalledWith({ - cancelEditOnDrag: true, - disableRowSelection: true, - singleRowMove: true, - columnIndexPosition: 2, - onExtensionRegistered: expect.anything(), - onBeforeMoveRows: expect.anything(), - onMoveRows: expect.anything(), - }); - expect(onRegisteredSpy).toHaveBeenCalledWith(instance); - expect(pluginSpy).toHaveBeenCalledWith(instance); - }); - - it('should dispose of the addon', () => { - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowMoveManager; - extension.register(); - const destroySpy = jest.spyOn(instance, 'destroy'); - - extension.dispose(); - - expect(destroySpy).toHaveBeenCalled(); - }); - - it('should provide addon options and expect them to be called in the addon constructor', () => { - const optionMock = { cancelEditOnDrag: true, singleRowMove: true, disableRowSelection: true }; - const addonOptions = { ...gridOptionsMock, rowMoveManager: optionMock }; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(addonOptions); - - extension.create(columnsMock, gridOptionsMock); - extension.register(); - - expect(mockAddon).toHaveBeenCalledWith(gridOptionsMock.rowMoveManager); - }); - - it('should call internal event handler subscribe and expect the "onBeforeMoveRows" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager as RowMoveManager, 'onBeforeMoveRows'); - const onMoveSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager as RowMoveManager, 'onMoveRows'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowMoveManager; - extension.register(); - instance.onBeforeMoveRows.notify({ insertBefore: 3, rows: [1], grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(2); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onBeforeSpy).toHaveBeenCalledWith(expect.anything(), { insertBefore: 3, rows: [1], grid: gridStub }); - expect(onBeforeSpy).toHaveReturnedWith(undefined); - expect(onMoveSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onBeforeMoveRows" option to be called AND have returned with a boolean when original callbacks returns a boolean', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - gridOptionsMock.rowMoveManager.onBeforeMoveRows = () => false; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager as RowMoveManager, 'onBeforeMoveRows'); - const onMoveSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager as RowMoveManager, 'onMoveRows'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowMoveManager; - extension.register(); - instance.onBeforeMoveRows.notify({ insertBefore: 3, rows: [1], grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(2); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onBeforeSpy).toHaveBeenCalledWith(expect.anything(), { insertBefore: 3, rows: [1], grid: gridStub }); - expect(onBeforeSpy).toHaveReturnedWith(false); - expect(onMoveSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onMoveRows" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager as RowMoveManager, 'onBeforeMoveRows'); - const onMoveSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager as RowMoveManager, 'onMoveRows'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowMoveManager; - extension.register(); - instance.onMoveRows.notify({ insertBefore: 3, rows: [1], grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(2); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onMoveSpy).toHaveBeenCalledWith(expect.anything(), { insertBefore: 3, rows: [1], grid: gridStub }); - expect(onBeforeSpy).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/common/src/extensions/index.ts b/packages/common/src/extensions/index.ts index c84a110cd..96f5604da 100644 --- a/packages/common/src/extensions/index.ts +++ b/packages/common/src/extensions/index.ts @@ -1,3 +1,2 @@ export * from './extensionUtility'; export * from './rowDetailViewExtension'; -export * from './rowMoveManagerExtension'; diff --git a/packages/common/src/extensions/rowMoveManagerExtension.ts b/packages/common/src/extensions/rowMoveManagerExtension.ts deleted file mode 100644 index 3a082959c..000000000 --- a/packages/common/src/extensions/rowMoveManagerExtension.ts +++ /dev/null @@ -1,133 +0,0 @@ -import 'slickgrid/plugins/slick.rowmovemanager'; - -import { - Column, - Extension, - GetSlickEventType, - GridOption, - SlickEventHandler, - SlickNamespace, - SlickRowMoveManager, -} from '../interfaces/index'; -import { SlickRowSelectionModel } from '../plugins/slickRowSelectionModel'; -import { SharedService } from '../services/shared.service'; - -// using external non-typed js libraries -declare const Slick: SlickNamespace; - -export class RowMoveManagerExtension implements Extension { - private _addon: SlickRowMoveManager | null = null; - private _eventHandler: SlickEventHandler; - private _rowSelectionPlugin!: SlickRowSelectionModel; - - constructor(private readonly sharedService: SharedService) { - this._eventHandler = new Slick.EventHandler(); - } - - get eventHandler(): SlickEventHandler { - return this._eventHandler; - } - - dispose() { - // unsubscribe all SlickGrid events - this._eventHandler.unsubscribeAll(); - - if (this._addon && this._addon.destroy) { - this._addon.destroy(); - this._addon = null; - } - if (this._rowSelectionPlugin?.destroy) { - this._rowSelectionPlugin.destroy(); - } - } - - /** - * Create the plugin before the Grid creation to avoid having odd behaviors. - * Mostly because the column definitions might change after the grid creation, so we want to make sure to add it before then - */ - create(columnDefinitions: Column[], gridOptions: GridOption): SlickRowMoveManager | null { - if (Array.isArray(columnDefinitions) && gridOptions) { - this._addon = this.loadAddonWhenNotExists(columnDefinitions, gridOptions); - const newRowMoveColumn: Column | undefined = this._addon?.getColumnDefinition(); - const rowMoveColDef = Array.isArray(columnDefinitions) && columnDefinitions.find((col: Column) => col && col.behavior === 'selectAndMove'); - const finalRowMoveColumn = rowMoveColDef ? rowMoveColDef : newRowMoveColumn; - - // set some exclusion properties since we don't want this column to be part of the export neither the list of column in the pickers - if (typeof finalRowMoveColumn === 'object') { - finalRowMoveColumn.excludeFromExport = true; - finalRowMoveColumn.excludeFromColumnPicker = true; - finalRowMoveColumn.excludeFromGridMenu = true; - finalRowMoveColumn.excludeFromQuery = true; - finalRowMoveColumn.excludeFromHeaderMenu = true; - } - - // only add the new column if it doesn't already exist - if (!rowMoveColDef && finalRowMoveColumn) { - // column index position in the grid - const columnPosition = gridOptions?.rowMoveManager?.columnIndexPosition ?? 0; - if (columnPosition > 0) { - columnDefinitions.splice(columnPosition, 0, finalRowMoveColumn); - } else { - columnDefinitions.unshift(finalRowMoveColumn); - } - } - return this._addon; - } - return null; - } - - loadAddonWhenNotExists(columnDefinitions: Column[], gridOptions: GridOption): SlickRowMoveManager | null { - if (Array.isArray(columnDefinitions) && gridOptions) { - if (!this._addon) { - this._addon = new Slick.RowMoveManager(gridOptions?.rowMoveManager || { cancelEditOnDrag: true }); - } - return this._addon; - } - return null; - } - - /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance(): SlickRowMoveManager | null { - return this._addon; - } - - /** Register the 3rd party addon (plugin) */ - register(rowSelectionPlugin?: SlickRowSelectionModel): SlickRowMoveManager | null { - if (this._addon && this.sharedService && this.sharedService.slickGrid && this.sharedService.gridOptions) { - // this also requires the Row Selection Model to be registered as well - if (!rowSelectionPlugin || !this.sharedService.slickGrid.getSelectionModel()) { - rowSelectionPlugin = new SlickRowSelectionModel(this.sharedService.gridOptions.rowSelectionOptions); - this.sharedService.slickGrid.setSelectionModel(rowSelectionPlugin); - } - this._rowSelectionPlugin = rowSelectionPlugin; - this.sharedService.slickGrid.registerPlugin(this._addon); - - // hook all events - if (this._addon && this.sharedService.slickGrid && this.sharedService.gridOptions.rowMoveManager) { - if (this.sharedService.gridOptions.rowMoveManager.onExtensionRegistered) { - this.sharedService.gridOptions.rowMoveManager.onExtensionRegistered(this._addon); - } - - const onBeforeMoveRowsHandler = this._addon.onBeforeMoveRows; - if (onBeforeMoveRowsHandler) { - (this._eventHandler as SlickEventHandler>).subscribe(onBeforeMoveRowsHandler, (e, args) => { - if (this.sharedService.gridOptions.rowMoveManager && typeof this.sharedService.gridOptions.rowMoveManager.onBeforeMoveRows === 'function') { - return this.sharedService.gridOptions.rowMoveManager.onBeforeMoveRows(e, args); - } - }); - } - - const onMoveRowsHandler = this._addon.onMoveRows; - if (onMoveRowsHandler) { - (this._eventHandler as SlickEventHandler>).subscribe(onMoveRowsHandler, (e, args) => { - if (this.sharedService.gridOptions.rowMoveManager && typeof this.sharedService.gridOptions.rowMoveManager.onMoveRows === 'function') { - this.sharedService.gridOptions.rowMoveManager.onMoveRows(e, args); - } - }); - } - } - return this._addon; - } - return null; - } -} diff --git a/packages/common/src/interfaces/checkboxSelectorOption.interface.ts b/packages/common/src/interfaces/checkboxSelectorOption.interface.ts index b5a664e0a..6a5c40038 100644 --- a/packages/common/src/interfaces/checkboxSelectorOption.interface.ts +++ b/packages/common/src/interfaces/checkboxSelectorOption.interface.ts @@ -1,4 +1,4 @@ -import { SlickGrid } from './slickGrid.interface'; +import { UsabilityOverrideFn } from '../enums/usabilityOverrideFn.type'; export interface CheckboxSelectorOption { /** Defaults to "_checkbox_selector", you can provide a different column id used as the column header id */ @@ -33,5 +33,5 @@ export interface CheckboxSelectorOption { width?: number; /** Override the logic for showing (or not) the expand icon (use case example: only every 2nd row is expandable) */ - selectableOverride?: (row: number, dataContext: any, grid: SlickGrid) => boolean; + selectableOverride?: UsabilityOverrideFn; } diff --git a/packages/common/src/interfaces/drag.interface.ts b/packages/common/src/interfaces/drag.interface.ts index 2b6368c1b..756162faa 100644 --- a/packages/common/src/interfaces/drag.interface.ts +++ b/packages/common/src/interfaces/drag.interface.ts @@ -1,3 +1,5 @@ +import { SlickGrid } from './index'; + export interface DragPosition { startX: number; startY: number; @@ -13,4 +15,27 @@ export interface DragRange { row?: number; cell?: number; }; +} + +export interface DragRowMove { + available: any[]; + canMove: boolean; + clonedSlickRow: HTMLElement; + deltaX: number; + deltaY: number; + drag: HTMLElement; + drop: any[]; + grid: SlickGrid; + guide: HTMLElement; + insertBefore: number; + offsetX: number; + offsetY: number; + originalX: number; + originalY: number; + proxy: HTMLElement; + selectionProxy: HTMLElement; + target: HTMLElement; + selectedRows: number[]; + startX: number; + startY: number; } \ No newline at end of file diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 72b57ea1c..00d1b0309 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -147,7 +147,6 @@ export * from './slickRange.interface'; export * from './slickRemoteModel.interface'; export * from './slickResizer.interface'; export * from './slickRowDetailView.interface'; -export * from './slickRowMoveManager.interface'; export * from './sorter.interface'; export * from './textExportOption.interface'; export * from './treeDataOption.interface'; diff --git a/packages/common/src/interfaces/rowDetailViewOption.interface.ts b/packages/common/src/interfaces/rowDetailViewOption.interface.ts index e52503fdb..587ee1a9b 100644 --- a/packages/common/src/interfaces/rowDetailViewOption.interface.ts +++ b/packages/common/src/interfaces/rowDetailViewOption.interface.ts @@ -1,5 +1,5 @@ +import { UsabilityOverrideFn } from '../index'; import { Observable, Subject } from '../services/rxjsFacade'; -import { SlickGrid } from './slickGrid.interface'; export interface RowDetailViewOption { /** Defaults to true, which will collapse all row detail views when user calls a sort. Unless user implements a sort to deal with padding */ @@ -76,5 +76,5 @@ export interface RowDetailViewOption { process: (item: any) => Promise | Observable | Subject; /** Override the logic for showing (or not) the expand icon (use case example: only every 2nd row is expandable) */ - expandableOverride?: (row: number, dataContext: any, grid: SlickGrid) => boolean; + expandableOverride?: UsabilityOverrideFn; } diff --git a/packages/common/src/interfaces/rowMoveManager.interface.ts b/packages/common/src/interfaces/rowMoveManager.interface.ts index ff3cfe176..bd1dbc620 100644 --- a/packages/common/src/interfaces/rowMoveManager.interface.ts +++ b/packages/common/src/interfaces/rowMoveManager.interface.ts @@ -1,9 +1,9 @@ import { RowMoveManagerOption, - SlickRowMoveManager, SlickEventData, SlickGrid, } from './index'; +import { SlickRowMoveManager } from '../plugins/slickRowMoveManager'; export interface RowMoveManager extends RowMoveManagerOption { // diff --git a/packages/common/src/interfaces/rowMoveManagerOption.interface.ts b/packages/common/src/interfaces/rowMoveManagerOption.interface.ts index 86535b2a7..369acddeb 100644 --- a/packages/common/src/interfaces/rowMoveManagerOption.interface.ts +++ b/packages/common/src/interfaces/rowMoveManagerOption.interface.ts @@ -1,4 +1,4 @@ -import { SlickGrid } from './slickGrid.interface'; +import { UsabilityOverrideFn } from '../enums/usabilityOverrideFn.type'; export interface RowMoveManagerOption { /** Defaults to false, option to cancel editing while dragging a row */ @@ -20,6 +20,21 @@ export interface RowMoveManagerOption { /** Defaults to False, do we want to disable the row selection? */ disableRowSelection?: boolean; + /** Defaults to True, do we want to hide the row move shadow of what we're dragging? */ + hideRowMoveShadow?: boolean; + + /** Defaults to 0, optional left margin of the row move shadown element when enabled */ + rowMoveShadowMarginLeft?: number | string; + + /** Defaults to 0, optional top margin of the row move shadown element when enabled */ + rowMoveShadowMarginTop?: number | string; + + /** Defaults to 0.9, opacity of row move shadow element (requires shadow to be shown via option: `hideRowMoveShadow: false`) */ + rowMoveShadowOpacity?: number | string; + + /** Defaults to 0.75, scale size of row move shadow element (requires shadow to be shown via option: `hideRowMoveShadow: false`) */ + rowMoveShadowScale?: number | string; + /** Defaults to False, do we want a single row move? Setting this to false means that 1 or more rows can be selected to move together. */ singleRowMove?: boolean; @@ -27,5 +42,5 @@ export interface RowMoveManagerOption { width?: number; /** Override the logic for showing (or not) the move icon (use case example: only every 2nd row is moveable) */ - usabilityOverride?: (row: number, dataContext: any, grid: SlickGrid) => boolean; + usabilityOverride?: UsabilityOverrideFn; } diff --git a/packages/common/src/interfaces/slickNamespace.interface.ts b/packages/common/src/interfaces/slickNamespace.interface.ts index 699ef52c4..e1ef09283 100644 --- a/packages/common/src/interfaces/slickNamespace.interface.ts +++ b/packages/common/src/interfaces/slickNamespace.interface.ts @@ -29,7 +29,6 @@ import { SlickRemoteModel, SlickResizer, SlickRowDetailView, - SlickRowMoveManager, } from './index'; import { SlickGridMenu, } from '../controls/index'; import { @@ -44,6 +43,7 @@ import { SlickDraggableGrouping, SlickHeaderButtons, SlickHeaderMenu, + SlickRowMoveManager, SlickRowSelectionModel, } from '../plugins/index'; diff --git a/packages/common/src/interfaces/slickRowDetailView.interface.ts b/packages/common/src/interfaces/slickRowDetailView.interface.ts index aa7e4dafa..0c937830d 100644 --- a/packages/common/src/interfaces/slickRowDetailView.interface.ts +++ b/packages/common/src/interfaces/slickRowDetailView.interface.ts @@ -1,3 +1,4 @@ +import { UsabilityOverrideFn } from '../enums/usabilityOverrideFn.type'; import { Column, RowDetailViewOption, @@ -28,7 +29,7 @@ export interface SlickRowDetailView { expandDetailView(item: any): void; /** Override the logic for showing (or not) the expand icon (use case example: only every 2nd row is expandable) */ - expandableOverride?: (row: number, dataContext: any, grid: SlickGrid) => boolean; + expandableOverride?: UsabilityOverrideFn; /** Get the Column Definition of the first column dedicated to toggling the Row Detail View */ getColumnDefinition(): Column; diff --git a/packages/common/src/interfaces/slickRowMoveManager.interface.ts b/packages/common/src/interfaces/slickRowMoveManager.interface.ts deleted file mode 100644 index 921dab540..000000000 --- a/packages/common/src/interfaces/slickRowMoveManager.interface.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - Column, - RowMoveManagerOption, - SlickEvent, - SlickGrid, -} from './index'; - -/** A plugin that allows to move/reorganize some rows with drag & drop */ -export interface SlickRowMoveManager { - pluginName: 'RowMoveManager', - - /** Constructor of the 3rd party plugin, user can optionally pass some options to the plugin */ - constructor: (options?: RowMoveManagerOption) => void; - - /** initialize the 3rd party plugin */ - init(grid: SlickGrid): void; - - /** destroy the 3rd party plugin */ - destroy(): void; - - /** Getter of the grid Column Definition for the checkbox selector column */ - getColumnDefinition(): Column; - - /** - * Change Row Move Manager options - * @options An object with configuration options. - */ - setOptions(options: RowMoveManagerOption): void; - - /** Override the logic for showing (or not) the move icon (use case example: only every 2nd row is moveable) */ - usabilityOverride?: (row: number, dataContext: any, grid: SlickGrid) => boolean; - - // -- - // Events - - /** triggered before rows are being moved */ - onBeforeMoveRows: SlickEvent<{ grid: SlickGrid; rows: number[]; insertBefore: number; }>; - - /** triggered when rows are being moved */ - onMoveRows: SlickEvent<{ grid: SlickGrid; rows: number[]; insertBefore: number; }>; -} diff --git a/packages/common/src/plugins/__tests__/slickAutoTooltip.spec.ts b/packages/common/src/plugins/__tests__/slickAutoTooltip.spec.ts index a99be7193..7651aba91 100644 --- a/packages/common/src/plugins/__tests__/slickAutoTooltip.spec.ts +++ b/packages/common/src/plugins/__tests__/slickAutoTooltip.spec.ts @@ -41,6 +41,12 @@ describe('AutoTooltip Plugin', () => { expect(plugin.eventHandler).toBeTruthy(); }); + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + it('should use default options when instantiating the plugin without passing any arguments', () => { plugin.init(gridStub); diff --git a/packages/common/src/plugins/__tests__/slickCellMenu.plugin.spec.ts b/packages/common/src/plugins/__tests__/slickCellMenu.plugin.spec.ts index ac9b85850..ee9171210 100644 --- a/packages/common/src/plugins/__tests__/slickCellMenu.plugin.spec.ts +++ b/packages/common/src/plugins/__tests__/slickCellMenu.plugin.spec.ts @@ -72,10 +72,6 @@ const gridStub = { onSort: new Slick.Event(), } as unknown as SlickGrid; -const dataViewStub = { - refresh: jest.fn(), -} as unknown as SlickDataView; - const pubSubServiceStub = { publish: jest.fn(), subscribe: jest.fn(), @@ -145,6 +141,12 @@ describe('CellMenu Plugin', () => { expect(plugin.eventHandler).toBeTruthy(); }); + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + it('should use default options when instantiating the plugin without passing any arguments', () => { plugin.init(); diff --git a/packages/common/src/plugins/__tests__/slickCheckboxSelectColumn.spec.ts b/packages/common/src/plugins/__tests__/slickCheckboxSelectColumn.spec.ts index f741f9797..accc5e167 100644 --- a/packages/common/src/plugins/__tests__/slickCheckboxSelectColumn.spec.ts +++ b/packages/common/src/plugins/__tests__/slickCheckboxSelectColumn.spec.ts @@ -348,8 +348,6 @@ describe('SlickCheckboxSelectColumn Plugin', () => { }); it('should call the "create" method and expect plugin to be created with checkbox column to be created at position 0 when using default', () => { - const checkboxSelectorElm = document.createElement('input'); - checkboxSelectorElm.type = 'checkbox'; plugin.create(mockColumns, { checkboxSelector: { columnId: 'chk-id' } }); expect(plugin).toBeTruthy(); @@ -373,8 +371,6 @@ describe('SlickCheckboxSelectColumn Plugin', () => { }); it('should call the "create" method and expect plugin to be created at position 1 when defined', () => { - const checkboxSelectorElm = document.createElement('input'); - checkboxSelectorElm.type = 'checkbox'; plugin.create(mockColumns, { checkboxSelector: { columnIndexPosition: 1 } }); expect(plugin).toBeTruthy(); @@ -398,8 +394,6 @@ describe('SlickCheckboxSelectColumn Plugin', () => { }); it('should process the "checkboxSelectionFormatter" and expect necessary Formatter to return null when selectableOverride is returning False', () => { - const checkboxSelectorElm = document.createElement('input'); - checkboxSelectorElm.type = 'checkbox'; plugin.selectableOverride(() => false); plugin.create(mockColumns, {}); const output = plugin.getColumnDefinition().formatter(0, 0, null, { id: 'checkbox_selector', field: '' } as Column, { firstName: 'John', lastName: 'Doe', age: 33 }, gridStub); @@ -409,9 +403,6 @@ describe('SlickCheckboxSelectColumn Plugin', () => { }); it('should process the "checkboxSelectionFormatter" and expect necessary Formatter to return null when selectableOverride is returning False', () => { - const checkboxSelectorElm = document.createElement('input'); - checkboxSelectorElm.type = 'checkbox'; - plugin.init(gridStub); plugin.selectableOverride(() => true); const output = plugin.getColumnDefinition().formatter(0, 0, null, { id: 'checkbox_selector', field: '' } as Column, { firstName: 'John', lastName: 'Doe', age: 33 }, gridStub); diff --git a/packages/common/src/plugins/__tests__/slickContextMenu.spec.ts b/packages/common/src/plugins/__tests__/slickContextMenu.spec.ts index b55af329d..65d61df97 100644 --- a/packages/common/src/plugins/__tests__/slickContextMenu.spec.ts +++ b/packages/common/src/plugins/__tests__/slickContextMenu.spec.ts @@ -164,6 +164,12 @@ describe('ContextMenu Plugin', () => { expect(plugin.eventHandler).toBeTruthy(); }); + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + it('should use default options when instantiating the plugin without passing any arguments', () => { plugin.init(); diff --git a/packages/common/src/plugins/__tests__/slickHeaderButtons.spec.ts b/packages/common/src/plugins/__tests__/slickHeaderButtons.spec.ts index 13979ead4..4a5117a79 100644 --- a/packages/common/src/plugins/__tests__/slickHeaderButtons.spec.ts +++ b/packages/common/src/plugins/__tests__/slickHeaderButtons.spec.ts @@ -86,6 +86,12 @@ describe('HeaderButton Plugin', () => { expect(plugin.eventHandler).toBeTruthy(); }); + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + it('should use default options when instantiating the plugin without passing any arguments', () => { plugin.init(); diff --git a/packages/common/src/plugins/__tests__/slickHeaderMenu.spec.ts b/packages/common/src/plugins/__tests__/slickHeaderMenu.spec.ts index a1e4dbaa2..decdeeee2 100644 --- a/packages/common/src/plugins/__tests__/slickHeaderMenu.spec.ts +++ b/packages/common/src/plugins/__tests__/slickHeaderMenu.spec.ts @@ -140,6 +140,12 @@ describe('HeaderMenu Plugin', () => { expect(plugin.eventHandler).toBeTruthy(); }); + it('should dispose of the addon', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + plugin.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + it('should use default options when instantiating the plugin without passing any arguments', () => { plugin.init(); diff --git a/packages/common/src/plugins/__tests__/slickRowMoveManager.spec.ts b/packages/common/src/plugins/__tests__/slickRowMoveManager.spec.ts new file mode 100644 index 000000000..f17d3da9b --- /dev/null +++ b/packages/common/src/plugins/__tests__/slickRowMoveManager.spec.ts @@ -0,0 +1,442 @@ +import 'jest-extended'; + +import { Column, GridOption, OnDragEventArgs, SlickGrid, SlickNamespace, } from '../../interfaces/index'; +import { SlickRowMoveManager } from '../slickRowMoveManager'; + +declare const Slick: SlickNamespace; +const GRID_UID = 'slickgrid_12345'; +jest.mock('flatpickr', () => { }); + +const addJQueryEventPropagation = function (event, target?: HTMLElement) { + Object.defineProperty(event, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + Object.defineProperty(event, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() }); + if (target) { + Object.defineProperty(event, 'target', { writable: true, configurable: true, value: target }); + } + return event; +} + +const mockGridOptions = { + frozenColumn: 1, + frozenRow: -1, + multiSelect: true, +} as GridOption; + +const getEditorLockMock = { + cancelCurrentEdit: jest.fn(), + commitCurrentEdit: jest.fn(), + isActive: jest.fn(), +}; + +const gridStub = { + canCellBeActive: jest.fn(), + getActiveCell: jest.fn(), + getActiveCanvasNode: jest.fn(), + getCanvasNode: jest.fn(), + getCellFromEvent: jest.fn(), + getCellFromPoint: jest.fn(), + getCellNode: jest.fn(), + getCellNodeBox: jest.fn(), + getColumns: jest.fn(), + getDataItem: jest.fn(), + getDataLength: jest.fn(), + getSelectedRows: jest.fn(), + getEditorLock: () => getEditorLockMock, + getOptions: () => mockGridOptions, + getUID: () => GRID_UID, + focus: jest.fn(), + registerPlugin: jest.fn(), + setActiveCell: jest.fn(), + setSelectedRows: jest.fn(), + scrollCellIntoView: jest.fn(), + scrollRowIntoView: jest.fn(), + unregisterPlugin: jest.fn(), + onDrag: new Slick.Event(), + onDragInit: new Slick.Event(), + onDragEnd: new Slick.Event(), + onDragStart: new Slick.Event(), +} as unknown as SlickGrid; + +describe('SlickRowMoveManager Plugin', () => { + let plugin: SlickRowMoveManager; + const mockColumns = [ + { id: 'firstName', field: 'firstName', name: 'First Name', }, + { id: 'lastName', field: 'lastName', name: 'Last Name', }, + { id: 'age', field: 'age', name: 'Age', }, + ] as Column[]; + 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(canvasTL, 'top', { 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 SlickRowMoveManager(); + }); + + afterEach(() => { + jest.clearAllMocks(); + plugin?.dispose(); + mockGridOptions.frozenColumn = -1; + mockGridOptions.frozenRow = -1; + mockGridOptions.frozenBottom = false; + mockGridOptions.multiSelect = true; + mockGridOptions.rowHeight = 25; + jest.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions); + }); + + 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(); + }); + + it('should create the plugin and initialize it', () => { + plugin.init(gridStub); + + expect(plugin.addonOptions).toEqual({ + cancelEditOnDrag: false, + columnId: '_move', + cssClass: 'slick-row-move-column', + disableRowSelection: false, + hideRowMoveShadow: true, + rowMoveShadowMarginLeft: 0, + rowMoveShadowMarginTop: 0, + rowMoveShadowOpacity: 0.9, + rowMoveShadowScale: 0.75, + singleRowMove: false, + width: 40, + }); + }); + + it('should call the "create" method and expect plugin to be created with checkbox column to be created at position 0 when using default', () => { + plugin.create(mockColumns, { rowMoveManager: { columnId: 'move-id' } }); + + expect(plugin).toBeTruthy(); + expect(mockColumns[0]).toEqual({ + cssClass: 'slick-row-move-column', + excludeFromColumnPicker: true, + excludeFromExport: true, + excludeFromGridMenu: true, + excludeFromHeaderMenu: true, + excludeFromQuery: true, + field: 'move', + formatter: expect.toBeFunction(), + id: 'move-id', + name: '', + behavior: 'selectAndMove', + resizable: false, + selectable: false, + width: 40, + }); + }); + + it('should create the plugin and call "setOptions" and expect options changed', () => { + plugin.init(gridStub); + plugin.setOptions({ cssClass: 'some-class', hideRowMoveShadow: false, rowMoveShadowMarginLeft: 2, rowMoveShadowMarginTop: 5, rowMoveShadowOpacity: 1, rowMoveShadowScale: 0.9, singleRowMove: true, width: 20 }); + + expect(plugin.addonOptions).toEqual({ + cancelEditOnDrag: false, + columnId: '_move', + cssClass: 'some-class', + disableRowSelection: false, + hideRowMoveShadow: false, + rowMoveShadowMarginLeft: 2, + rowMoveShadowMarginTop: 5, + rowMoveShadowOpacity: 1, + rowMoveShadowScale: 0.9, + singleRowMove: true, + width: 20, + }); + }); + + it('should call the "create" method and expect plugin to be created at position 1 when defined', () => { + plugin.create(mockColumns, { rowMoveManager: { columnIndexPosition: 1 } }); + + expect(plugin).toBeTruthy(); + expect(mockColumns[1]).toEqual({ + behavior: 'selectAndMove', + cssClass: 'slick-row-move-column', + excludeFromColumnPicker: true, + excludeFromExport: true, + excludeFromGridMenu: true, + excludeFromHeaderMenu: true, + excludeFromQuery: true, + field: 'move', + formatter: expect.toBeFunction(), + id: 'move-id', + name: '', + resizable: false, + selectable: false, + width: 40, + }); + }); + + it('should process the "checkboxSelectionFormatter" and expect necessary Formatter to return null when selectableOverride is returning False', () => { + plugin.usabilityOverride(() => false); + plugin.create(mockColumns, {}); + const output = plugin.getColumnDefinition().formatter(0, 0, null, { id: '_move', field: '' } as Column, { firstName: 'John', lastName: 'Doe', age: 33 }, gridStub); + + expect(plugin).toBeTruthy(); + expect(output).toEqual(''); + }); + + it('should process the "checkboxSelectionFormatter" and expect necessary Formatter to return regular formatter when usabilityOverride is returning True', () => { + plugin.init(gridStub); + plugin.usabilityOverride(() => true); + const output = plugin.getColumnDefinition().formatter(0, 0, null, { id: '_move', field: '' } as Column, { firstName: 'John', lastName: 'Doe', age: 33 }, gridStub); + + expect(plugin).toBeTruthy(); + expect(output).toEqual({ addClasses: 'cell-reorder dnd', text: '' }); + }); + + it('should process the "checkboxSelectionFormatter" and expect necessary Formatter to return regular formatter when usabilityOverride is not a function', () => { + plugin.init(gridStub); + plugin.usabilityOverride(null); + const output = plugin.getColumnDefinition().formatter(0, 0, null, { id: '_move', field: '' } as Column, { firstName: 'John', lastName: 'Doe', age: 33 }, gridStub); + + expect(plugin).toBeTruthy(); + expect(output).toEqual({ addClasses: 'cell-reorder dnd', text: '' }); + }); + + it('should create the plugin and trigger "dragInit" event and expect "stopImmediatePropagation" to be called', () => { + plugin.init(gridStub); + + const divElm = document.createElement('div'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter'), divElm); + const stopImmediatePropagationSpy = jest.spyOn(mouseEvent, 'stopImmediatePropagation'); + gridStub.onDragInit.notify({ + count: 1, deltaX: 0, deltaY: 1, offsetX: 2, offsetY: 3, proxy: document.createElement('div'), guide: document.createElement('div'), row: 2, rows: [2], + } as unknown as OnDragEventArgs, mouseEvent); + + expect(stopImmediatePropagationSpy).toHaveBeenCalled(); + }); + + it('should create the plugin and trigger "dragEnd" event and expect it to return null when we are not actually dragging any row', () => { + plugin.init(gridStub, { hideRowMoveShadow: false }); + + const divElm = document.createElement('div'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter'), divElm); + const stopImmediatePropagationSpy = jest.spyOn(mouseEvent, 'stopImmediatePropagation'); + const mockArgs = { + deltaX: 0, deltaY: 1, offsetX: 2, offsetY: 3, + row: 2, rows: [2], selectedRows: [2], insertBefore: 4, + canMove: true, + proxy: document.createElement('div'), + guide: document.createElement('div'), + selectionProxy: document.createElement('div'), + clonedSlickRow: document.createElement('div'), + } as unknown as OnDragEventArgs; + gridStub.onDragEnd.notify(mockArgs, mouseEvent); + + expect(stopImmediatePropagationSpy).not.toHaveBeenCalled(); + }); + + it('should create the plugin and trigger "dragStart" and "dragEnd" events, expect new row being moved when different and expect dragEnd to remove guide/proxy/shadow and finally onMoveRows to publish event and callback to be called', () => { + const mockOnMoveRows = jest.fn(); + const mockNewMovedRow = 0; + const mockSlickRow = document.createElement('div'); + mockSlickRow.className = 'slick-row'; + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 1, row: mockNewMovedRow }); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + jest.spyOn(gridStub, 'getCellNode').mockReturnValue(mockSlickRow); + jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue([2]); + const setSelectRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); + + plugin.init(gridStub, { hideRowMoveShadow: false, onMoveRows: mockOnMoveRows }); + const onMoveRowNotifySpy = jest.spyOn(plugin.onMoveRows, 'notify'); + + const divElm = document.createElement('div'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter'), divElm); + const stopImmediatePropagationSpy = jest.spyOn(mouseEvent, 'stopImmediatePropagation'); + const mockArgs = { + deltaX: 0, deltaY: 1, offsetX: 2, offsetY: 3, + row: 2, rows: [2], selectedRows: [2], insertBefore: 4, + canMove: true, + } as any; + gridStub.onDragStart.notify(mockArgs, mouseEvent); + + expect(stopImmediatePropagationSpy).toHaveBeenCalledTimes(1); + expect(setSelectRowSpy).toHaveBeenCalledWith([mockNewMovedRow]); + expect(mockArgs.insertBefore).toBe(-1); + expect(mockArgs.selectedRows).toEqual([mockNewMovedRow]); + expect(mockArgs.clonedSlickRow).toBeTruthy(); + expect(mockArgs.guide).toBeTruthy(); + expect(mockArgs.selectionProxy).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-guide')).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-proxy')).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-shadow-row')).toBeTruthy(); + + gridStub.onDragEnd.notify(mockArgs, mouseEvent); + expect(onMoveRowNotifySpy).toHaveBeenCalledWith({ insertBefore: -1, rows: [mockNewMovedRow], grid: gridStub, }); + expect(mockOnMoveRows).toHaveBeenCalledWith(mouseEvent, { insertBefore: -1, rows: [mockNewMovedRow], grid: gridStub, }); + expect(stopImmediatePropagationSpy).toHaveBeenCalledTimes(2); + }); + + it('should create the plugin and trigger "dragStart" and expect editor be cancelled when it is active editor', () => { + const mockNewMovedRow = 0; + const mockSlickRow = document.createElement('div'); + mockSlickRow.className = 'slick-row'; + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 1, row: mockNewMovedRow }); + + plugin.init(gridStub, { cancelEditOnDrag: true }); + jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(true); + const cancelEditorSpy = jest.spyOn(gridStub.getEditorLock(), 'cancelCurrentEdit'); + + const divElm = document.createElement('div'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter'), divElm); + const stopImmediatePropagationSpy = jest.spyOn(mouseEvent, 'stopImmediatePropagation'); + const mockArgs = { + canMove: true, deltaX: 0, deltaY: 1, offsetX: 2, offsetY: 3, + row: 2, rows: [2], selectedRows: [2], insertBefore: 4, + } as any; + const output = gridStub.onDragStart.notify(mockArgs, mouseEvent); + + expect(stopImmediatePropagationSpy).not.toHaveBeenCalled(); + expect(cancelEditorSpy).toHaveBeenCalled(); + expect(output).toBeFalsy(); + }); + + it('should create the plugin and trigger "drag" event (without "dragStart") and the handler to return right away when row is not dragging', () => { + const mockNewMovedRow = 0; + const mockSlickRow = document.createElement('div'); + mockSlickRow.className = 'slick-row'; + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 1, row: mockNewMovedRow }); + + plugin.init(gridStub, { cancelEditOnDrag: true }); + jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(true); + + const divElm = document.createElement('div'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter'), divElm); + const stopImmediatePropagationSpy = jest.spyOn(mouseEvent, 'stopImmediatePropagation'); + const mockArgs = { + canMove: true, deltaX: 0, deltaY: 1, offsetX: 2, offsetY: 3, + row: 2, rows: [2], selectedRows: [2], insertBefore: 4, + } as any; + gridStub.onDragStart.notify(mockArgs, mouseEvent); + gridStub.onDrag.notify(mockArgs, mouseEvent); + + expect(stopImmediatePropagationSpy).not.toHaveBeenCalled(); + }); + + it('should create the plugin and trigger "dragStart" and "drag" events, expect new row being moved', () => { + const mockOnBeforeMoveRows = jest.fn(); + const mockNewMovedRow = 0; + const mockSlickRow = document.createElement('div'); + mockSlickRow.className = 'slick-row'; + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 1, row: mockNewMovedRow }); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + jest.spyOn(gridStub, 'getCellNode').mockReturnValue(mockSlickRow); + jest.spyOn(gridStub, 'getDataLength').mockReturnValue(5); + jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue([2]); + const setSelectRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); + + plugin.init(gridStub, { hideRowMoveShadow: false, onBeforeMoveRows: mockOnBeforeMoveRows }); + plugin.usabilityOverride(() => true); + jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(false); + + const divElm = document.createElement('div'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter'), divElm); + const stopImmediatePropagationSpy = jest.spyOn(mouseEvent, 'stopImmediatePropagation'); + const mockArgs = { + deltaX: 0, deltaY: 1, offsetX: 2, offsetY: 3, + row: 2, rows: [2], selectedRows: [2], insertBefore: 4, + canMove: true, + } as any; + gridStub.onDragStart.notify(mockArgs, mouseEvent); + + expect(stopImmediatePropagationSpy).toHaveBeenCalled(); + expect(setSelectRowSpy).toHaveBeenCalledWith([mockNewMovedRow]); + expect(mockArgs.insertBefore).toBe(-1); + expect(mockArgs.selectedRows).toEqual([mockNewMovedRow]); + expect(mockArgs.clonedSlickRow).toBeTruthy(); + expect(mockArgs.guide).toBeTruthy(); + expect(mockArgs.selectionProxy).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-guide')).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-proxy')).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-shadow-row')).toBeTruthy(); + + Object.defineProperty(mouseEvent, 'pageY', { writable: true, configurable: true, value: 12 }); + gridStub.onDrag.notify(mockArgs, mouseEvent); + expect(mockArgs.selectionProxy.style.display).toBe('block'); + expect(mockArgs.selectionProxy.style.top).toBe('7px'); + expect(mockArgs.clonedSlickRow.style.display).toBe('block'); + expect(mockArgs.clonedSlickRow.style.top).toBe('6px'); + expect(mockArgs.canMove).toBeTrue(); + expect(mockOnBeforeMoveRows).toHaveBeenCalled(); + expect(mockArgs.guide.style.top).toBe('0px'); + }); + + it('should create the plugin and trigger "dragStart" and "drag" events, expect new row to not be to moved when "onBeforeMoveRows" returns false', () => { + const mockOnBeforeMoveRows = jest.fn(); + const mockNewMovedRow = 0; + const mockSlickRow = document.createElement('div'); + mockSlickRow.className = 'slick-row'; + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 1, row: mockNewMovedRow }); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + jest.spyOn(gridStub, 'getCellNode').mockReturnValue(mockSlickRow); + jest.spyOn(gridStub, 'getDataLength').mockReturnValue(5); + jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue([2]); + const setSelectRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); + + plugin.init(gridStub, { hideRowMoveShadow: false, onBeforeMoveRows: mockOnBeforeMoveRows }); + plugin.usabilityOverride(() => true); + jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(false); + + const divElm = document.createElement('div'); + const mouseEvent = addJQueryEventPropagation(new Event('mouseenter'), divElm); + const stopImmediatePropagationSpy = jest.spyOn(mouseEvent, 'stopImmediatePropagation'); + const mockArgs = { + deltaX: 0, deltaY: 1, offsetX: 2, offsetY: 3, + row: 2, rows: [2], selectedRows: [2], insertBefore: 4, + canMove: true, + } as any; + gridStub.onDragStart.notify(mockArgs, mouseEvent); + + expect(stopImmediatePropagationSpy).toHaveBeenCalled(); + expect(setSelectRowSpy).toHaveBeenCalledWith([mockNewMovedRow]); + expect(mockArgs.insertBefore).toBe(-1); + expect(mockArgs.selectedRows).toEqual([mockNewMovedRow]); + expect(mockArgs.clonedSlickRow).toBeTruthy(); + expect(mockArgs.guide).toBeTruthy(); + expect(mockArgs.selectionProxy).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-guide')).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-proxy')).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-shadow-row')).toBeTruthy(); + + Object.defineProperty(mouseEvent, 'pageY', { writable: true, configurable: true, value: 12 }); + mockOnBeforeMoveRows.mockReturnValue(false); + gridStub.onDrag.notify(mockArgs, mouseEvent); + expect(mockArgs.selectionProxy.style.display).toBe('block'); + expect(mockArgs.selectionProxy.style.top).toBe('7px'); + expect(mockArgs.clonedSlickRow.style.display).toBe('block'); + expect(mockArgs.clonedSlickRow.style.top).toBe('6px'); + expect(mockArgs.canMove).toBeFalse(); + expect(mockArgs.guide.style.top).toBe('-1000px'); + }); + +}); \ No newline at end of file diff --git a/packages/common/src/plugins/__tests__/slickRowSelectionModel.spec.ts b/packages/common/src/plugins/__tests__/slickRowSelectionModel.spec.ts index a705507ca..16de46747 100644 --- a/packages/common/src/plugins/__tests__/slickRowSelectionModel.spec.ts +++ b/packages/common/src/plugins/__tests__/slickRowSelectionModel.spec.ts @@ -56,7 +56,7 @@ const gridStub = { onBeforeCellRangeSelected: new Slick.Event(), } as unknown as SlickGrid; -describe('CellSelectionModel Plugin', () => { +describe('SlickRowSelectionModel Plugin', () => { let plugin: SlickRowSelectionModel; const mockColumns = [ { id: 'firstName', field: 'firstName', name: 'First Name', }, diff --git a/packages/common/src/plugins/index.ts b/packages/common/src/plugins/index.ts index 3c4d242c0..1ce662e9c 100644 --- a/packages/common/src/plugins/index.ts +++ b/packages/common/src/plugins/index.ts @@ -10,4 +10,5 @@ export * from './slickContextMenu'; export * from './slickDraggableGrouping'; export * from './slickHeaderButtons'; export * from './slickHeaderMenu'; +export * from './slickRowMoveManager'; export * from './slickRowSelectionModel'; \ No newline at end of file diff --git a/packages/common/src/plugins/menuBaseClass.ts b/packages/common/src/plugins/menuBaseClass.ts index f18c343e5..c36a1efb5 100644 --- a/packages/common/src/plugins/menuBaseClass.ts +++ b/packages/common/src/plugins/menuBaseClass.ts @@ -86,6 +86,11 @@ export class MenuBaseClass { this.grid.setColumns(this.grid.getColumns()); } + /** @deprecated @use `dispose` Destroy plugin. */ + destroy() { + this.dispose(); + } + /** Dispose (destroy) the SlickGrid 3rd party plugin */ dispose() { super.dispose(); diff --git a/packages/common/src/plugins/slickHeaderMenu.ts b/packages/common/src/plugins/slickHeaderMenu.ts index e7ad6975a..a10c2e8eb 100644 --- a/packages/common/src/plugins/slickHeaderMenu.ts +++ b/packages/common/src/plugins/slickHeaderMenu.ts @@ -88,6 +88,11 @@ export class SlickHeaderMenu extends MenuBaseClass { this._bindEventService.bind(document.body, 'mousedown', this.handleBodyMouseDown.bind(this) as EventListener); } + /** @deprecated @use `dispose` Destroy plugin. */ + destroy() { + this.dispose(); + } + /** Dispose (destroy) of the plugin */ dispose() { super.dispose(); diff --git a/packages/common/src/plugins/slickRowMoveManager.ts b/packages/common/src/plugins/slickRowMoveManager.ts new file mode 100644 index 000000000..26f0c014a --- /dev/null +++ b/packages/common/src/plugins/slickRowMoveManager.ts @@ -0,0 +1,309 @@ +import { UsabilityOverrideFn } from '../enums/usabilityOverrideFn.type'; +import { + Column, + DragRowMove, + FormatterResultObject, + GridOption, + RowMoveManager, + RowMoveManagerOption, + SlickEventData, + SlickEventHandler, + SlickGrid, + SlickNamespace, +} from '../interfaces/index'; +import { findWidthOrDefault, getHtmlElementOffset } from '../services/domUtilities'; + +// using external SlickGrid JS libraries +declare const Slick: SlickNamespace; + +/** + * Row Move Manager options: + * cssClass: A CSS class to be added to the menu item container. + * columnId: Column definition id (defaults to "_move") + * cancelEditOnDrag: Do we want to cancel any Editing while dragging a row (defaults to false) + * disableRowSelection: Do we want to disable the row selection? (defaults to false) + * singleRowMove: Do we want a single row move? Setting this to false means that it's a multple row move (defaults to false) + * width: Width of the column + * usabilityOverride: Callback method that user can override the default behavior of the row being moveable or not + * + */ +export class SlickRowMoveManager { + protected _addonOptions!: RowMoveManager; + protected _canvas!: HTMLElement; + protected _dragging = false; + protected _eventHandler: SlickEventHandler; + protected _grid!: SlickGrid; + protected _handler = new Slick.EventHandler(); + protected _usabilityOverride?: UsabilityOverrideFn; + protected _defaults = { + columnId: '_move', + cssClass: 'slick-row-move-column', + cancelEditOnDrag: false, + disableRowSelection: false, + hideRowMoveShadow: true, + rowMoveShadowMarginTop: 0, + rowMoveShadowMarginLeft: 0, + rowMoveShadowOpacity: 0.9, + rowMoveShadowScale: 0.75, + singleRowMove: false, + width: 40, + } as RowMoveManagerOption; + onBeforeMoveRows = new Slick.Event<{ grid: SlickGrid; rows: number[]; insertBefore: number; }>(); + onMoveRows = new Slick.Event<{ grid: SlickGrid; rows: number[]; insertBefore: number; }>(); + pluginName: 'RowMoveManager' = 'RowMoveManager'; + + /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */ + constructor() { + this._eventHandler = new Slick.EventHandler(); + } + + get addonOptions(): RowMoveManagerOption { + return this._addonOptions as RowMoveManagerOption; + } + + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + + /** Getter for the Grid Options pulled through the Grid Object */ + get gridOptions(): GridOption { + return this._grid?.getOptions?.() ?? {}; + } + + /** Initialize plugin. */ + init(grid: SlickGrid, options?: RowMoveManager) { + this._addonOptions = { ...this._defaults, ...options }; + this._grid = grid; + this._canvas = this._grid.getCanvasNode(); + this._eventHandler + .subscribe(this._grid.onDragInit, this.handleDragInit.bind(this)) + .subscribe(this._grid.onDragStart, this.handleDragStart.bind(this)) + .subscribe(this._grid.onDrag, this.handleDrag.bind(this)) + .subscribe(this._grid.onDragEnd, this.handleDragEnd.bind(this)); + } + + /** @deprecated @use `dispose` Destroy plugin. */ + destroy() { + this.dispose(); + } + + /** Dispose (destroy) the SlickGrid 3rd party plugin */ + dispose() { + this._eventHandler?.unsubscribeAll(); + } + + /** + * Create the plugin before the Grid creation to avoid having odd behaviors. + * Mostly because the column definitions might change after the grid creation, so we want to make sure to add it before then + */ + create(columnDefinitions: Column[], gridOptions: GridOption): SlickRowMoveManager | null { + this._addonOptions = { ...this._defaults, ...gridOptions.rowMoveManager } as RowMoveManagerOption; + if (Array.isArray(columnDefinitions) && gridOptions) { + const newRowMoveColumn: Column = this.getColumnDefinition(); + const rowMoveColDef = Array.isArray(columnDefinitions) && columnDefinitions.find((col: Column) => col && col.behavior === 'selectAndMove'); + const finalRowMoveColumn = rowMoveColDef ? rowMoveColDef : newRowMoveColumn; + + // column index position in the grid + const columnPosition = gridOptions?.rowMoveManager?.columnIndexPosition ?? 0; + if (columnPosition > 0) { + columnDefinitions.splice(columnPosition, 0, finalRowMoveColumn); + } else { + columnDefinitions.unshift(finalRowMoveColumn); + } + } + return this; + } + + getColumnDefinition(): Column { + return { + id: this._addonOptions.columnId || '_move', + name: '', + behavior: 'selectAndMove', + cssClass: this._addonOptions.cssClass, + excludeFromExport: true, + excludeFromColumnPicker: true, + excludeFromGridMenu: true, + excludeFromQuery: true, + excludeFromHeaderMenu: true, + field: 'move', + resizable: false, + selectable: false, + width: this._addonOptions.width || 40, + formatter: this.moveIconFormatter.bind(this), + }; + } + + /** + * Method that user can pass to override the default behavior or making every row moveable. + * In order word, user can choose which rows to be an available as moveable (or not) by providing his own logic show/hide icon and usability. + * @param overrideFn: override function callback + */ + usabilityOverride(overrideFn: UsabilityOverrideFn) { + this._usabilityOverride = overrideFn; + } + + setOptions(newOptions: RowMoveManagerOption) { + this._addonOptions = { ...this._addonOptions, ...newOptions }; + } + + // -- + // protected functions + // ------------------ + + protected handleDragInit(e: SlickEventData) { + // prevent the grid from cancelling drag'n'drop by default + e.stopImmediatePropagation(); + } + + protected handleDragEnd(e: SlickEventData, dd: DragRowMove) { + if (!this._dragging) { + return; + } + this._dragging = false; + e.stopImmediatePropagation(); + + dd.guide?.remove(); + dd.selectionProxy?.remove(); + dd.clonedSlickRow?.remove(); + + if (dd.canMove) { + const eventData = { + grid: this._grid, + rows: dd.selectedRows, + insertBefore: dd.insertBefore, + }; + // TODO: this._grid.remapCellCssClasses ? + if (typeof this._addonOptions.onMoveRows === 'function') { + this._addonOptions.onMoveRows(e, eventData); + } + this.onMoveRows.notify(eventData); + } + } + protected handleDrag(e: SlickEventData, dd: DragRowMove): boolean | void { + if (this._dragging) { + e.stopImmediatePropagation(); + + const top = e.pageY - (getHtmlElementOffset(this._canvas)?.top ?? 0); + dd.selectionProxy.style.top = `${top - 5}px`; + dd.selectionProxy.style.display = 'block'; + + // if the row move shadow is enabled, we'll also make it follow the mouse cursor + if (dd.clonedSlickRow) { + dd.clonedSlickRow.style.top = `${top - 6}px`; + dd.clonedSlickRow.style.display = 'block'; + } + + const insertBefore = Math.max(0, Math.min(Math.round(top / (this.gridOptions.rowHeight || 0)), this._grid.getDataLength())); + if (insertBefore !== dd.insertBefore) { + const eventData = { + grid: this._grid, + rows: dd.selectedRows, + insertBefore, + }; + + if (this._addonOptions?.onBeforeMoveRows?.(e, eventData) === false || this.onBeforeMoveRows.notify(eventData) === false) { + dd.canMove = false; + } else { + dd.canMove = true; + } + + // if there's a UsabilityOverride defined, we also need to verify that the condition is valid + if (this._usabilityOverride && dd.canMove) { + const insertBeforeDataContext = this._grid.getDataItem(insertBefore); + dd.canMove = this.checkUsabilityOverride(insertBefore, insertBeforeDataContext, this._grid); + } + + // if the new target is possible we'll display the dark blue bar (representing the acceptability) at the target position + // else it won't show up (it will be off the screen) + if (!dd.canMove) { + dd.guide.style.top = '-1000px'; + } else { + dd.guide.style.top = `${insertBefore * (this.gridOptions.rowHeight || 0)}px`; + } + + dd.insertBefore = insertBefore; + } + } + } + + protected handleDragStart(event: SlickEventData, dd: DragRowMove): boolean | void { + const cell = this._grid.getCellFromEvent(event) || { cell: -1, row: -1 }; + const currentRow = cell.row; + const dataContext = this._grid.getDataItem(currentRow); + + if (this.checkUsabilityOverride(currentRow, dataContext, this._grid)) { + if (this._addonOptions.cancelEditOnDrag && this._grid.getEditorLock().isActive()) { + this._grid.getEditorLock().cancelCurrentEdit(); + } + + if (this._grid.getEditorLock().isActive() || !/move|selectAndMove/.test(this._grid.getColumns()[cell.cell].behavior || '')) { + return false; + } + + this._dragging = true; + event.stopImmediatePropagation(); + + // optionally create a shadow element of the row item that we're moving/dragging so that we can see it all the time exactly which row is being dragged + if (!this.addonOptions.hideRowMoveShadow) { + const slickRowElm = this._grid.getCellNode(cell.row, cell.cell)?.closest('.slick-row'); + if (slickRowElm) { + dd.clonedSlickRow = slickRowElm.cloneNode(true) as HTMLDivElement; + dd.clonedSlickRow.classList.add('slick-reorder-shadow-row'); + dd.clonedSlickRow.style.display = 'none'; + dd.clonedSlickRow.style.marginLeft = findWidthOrDefault(this._addonOptions?.rowMoveShadowMarginLeft, '0px'); + dd.clonedSlickRow.style.marginTop = findWidthOrDefault(this._addonOptions?.rowMoveShadowMarginTop, '0px'); + dd.clonedSlickRow.style.opacity = `${this._addonOptions?.rowMoveShadowOpacity ?? 0.95}`; + dd.clonedSlickRow.style.transform = `scale(${this.addonOptions?.rowMoveShadowScale ?? 0.75})`; + this._canvas.appendChild(dd.clonedSlickRow); + } + } + + let selectedRows = this._addonOptions.singleRowMove ? [cell.row] : this._grid.getSelectedRows(); + if (selectedRows.length === 0 || !selectedRows.some(selectedRow => selectedRow === cell.row)) { + selectedRows = [cell.row]; + if (!this._addonOptions.disableRowSelection) { + this._grid.setSelectedRows(selectedRows); + } + } + + const rowHeight = this.gridOptions.rowHeight as number; + dd.selectedRows = selectedRows; + + const reorderProxyElm = document.createElement('div'); + reorderProxyElm.className = 'slick-reorder-proxy'; + reorderProxyElm.style.display = 'none'; + reorderProxyElm.style.position = 'absolute'; + reorderProxyElm.style.zIndex = '99999'; + reorderProxyElm.style.width = `${this._canvas.clientWidth}px`; + reorderProxyElm.style.height = `${rowHeight * selectedRows.length}px`; + dd.selectionProxy = reorderProxyElm; + this._canvas.appendChild(reorderProxyElm); + + const reorderGuideElm = document.createElement('div'); + reorderGuideElm.className = 'slick-reorder-guide'; + reorderGuideElm.style.position = 'absolute'; + reorderGuideElm.style.zIndex = '99999'; + reorderGuideElm.style.width = `${this._canvas.clientWidth}px`; + reorderGuideElm.style.top = `-1000px`; + dd.guide = reorderGuideElm; + this._canvas.appendChild(reorderGuideElm); + + dd.insertBefore = -1; + } + } + + protected checkUsabilityOverride(row: number, dataContext: any, grid: SlickGrid) { + if (typeof this._usabilityOverride === 'function') { + return this._usabilityOverride(row, dataContext, grid); + } + return true; + } + + protected moveIconFormatter(row: number, cell: number, value: any, column: Column, dataContext: any, grid: SlickGrid): FormatterResultObject | string { + if (!this.checkUsabilityOverride(row, dataContext, grid)) { + return ''; + } else { + return { addClasses: 'cell-reorder dnd', text: '' }; + } + } +} \ No newline at end of file diff --git a/packages/common/src/services/__tests__/extension.service.spec.ts b/packages/common/src/services/__tests__/extension.service.spec.ts index 1206895f1..80ab5773b 100644 --- a/packages/common/src/services/__tests__/extension.service.spec.ts +++ b/packages/common/src/services/__tests__/extension.service.spec.ts @@ -2,14 +2,10 @@ import 'jest-extended'; import { ExtensionName } from '../../enums/index'; import { Column, ExtensionModel, GridOption, SlickGrid, SlickNamespace } from '../../interfaces/index'; -import { - ExtensionUtility, - RowDetailViewExtension, - RowMoveManagerExtension, -} from '../../extensions'; +import { ExtensionUtility, RowDetailViewExtension } from '../../extensions'; import { BackendUtilityService, ExtensionService, FilterService, PubSubService, SharedService, SortService, TreeDataService } from '../index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; -import { SlickAutoTooltip, SlickCellExcelCopyManager, SlickCellMenu, SlickCheckboxSelectColumn, SlickContextMenu, SlickDraggableGrouping, SlickHeaderButtons, SlickHeaderMenu, SlickRowSelectionModel } from '../../plugins/index'; +import { SlickAutoTooltip, SlickCellExcelCopyManager, SlickCellMenu, SlickCheckboxSelectColumn, SlickContextMenu, SlickDraggableGrouping, SlickHeaderButtons, SlickHeaderMenu, SlickRowMoveManager, SlickRowSelectionModel } from '../../plugins/index'; import { SlickCellSelectionModel } from '../../plugins/slickCellSelectionModel'; import { SlickColumnPicker, SlickGridMenu } from '../../controls/index'; import { GroupItemMetadataProviderService } from '../groupItemMetadataProvider.service'; @@ -50,6 +46,7 @@ const mockRowSelectionModel = { jest.mock('../../plugins/slickRowSelectionModel', () => ({ SlickRowSelectionModel: jest.fn().mockImplementation(() => mockRowSelectionModel), })); + const mockCheckboxSelectColumn = { constructor: jest.fn(), init: jest.fn(), @@ -61,6 +58,17 @@ jest.mock('../../plugins/slickCheckboxSelectColumn', () => ({ SlickCheckboxSelectColumn: jest.fn().mockImplementation(() => mockCheckboxSelectColumn), })); +const mockRowMoveManager = { + constructor: jest.fn(), + init: jest.fn(), + create: jest.fn(), + destroy: jest.fn(), + dispose: jest.fn(), +} as unknown as SlickRowMoveManager; +jest.mock('../../plugins/slickRowMoveManager', () => ({ + SlickRowMoveManager: jest.fn().mockImplementation(() => mockRowMoveManager), +})); + const gridStub = { autosizeColumns: jest.fn(), getColumnIndex: jest.fn(), @@ -137,7 +145,6 @@ const extensionStub = { getAddonInstance: jest.fn(), register: jest.fn() }; -const extensionGroupItemMetaStub = { ...extensionStub }; const extensionGridMenuStub = { ...extensionStub, refreshBackendDataset: jest.fn(), @@ -147,11 +154,6 @@ const extensionColumnPickerStub = { ...extensionStub, translateColumnPicker: jest.fn() }; -const extensionRowMoveStub = { - ...extensionStub, - onBeforeMoveRows: jest.fn(), - onMoveRows: jest.fn() -}; describe('ExtensionService', () => { let sharedService: SharedService; @@ -174,7 +176,6 @@ describe('ExtensionService', () => { treeDataServiceStub, // extensions extensionStub as unknown as RowDetailViewExtension, - extensionRowMoveStub as unknown as RowMoveManagerExtension, sharedService, translateService, ); @@ -429,8 +430,7 @@ describe('ExtensionService', () => { it('should register the RowMoveManager addon when "enableRowMoveManager" is set in the grid options', () => { const columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }] as Column[]; const gridOptionsMock = { enableRowMoveManager: true } as GridOption; - const extCreateSpy = jest.spyOn(extensionRowMoveStub, 'create').mockReturnValue(instanceMock); - const extRegisterSpy = jest.spyOn(extensionRowMoveStub, 'register'); + const extCreateSpy = jest.spyOn(mockRowMoveManager, 'create').mockReturnValue(mockRowMoveManager); const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); service.createExtensionsBeforeGridCreation(columnsMock, gridOptionsMock); @@ -441,8 +441,7 @@ describe('ExtensionService', () => { expect(gridSpy).toHaveBeenCalled(); expect(extCreateSpy).toHaveBeenCalledWith(columnsMock, gridOptionsMock); expect(rowSelectionInstance).not.toBeNull(); - expect(extRegisterSpy).toHaveBeenCalled(); - expect(output).toEqual({ name: ExtensionName.rowMoveManager, instance: instanceMock as unknown, class: extensionRowMoveStub } as ExtensionModel); + expect(output).toEqual({ name: ExtensionName.rowMoveManager, instance: mockRowMoveManager as unknown, class: mockRowMoveManager } as ExtensionModel); }); it('should register the RowSelection addon when "enableCheckboxSelector" (false) and "enableRowSelection" (true) are set in the grid options', () => { @@ -580,7 +579,7 @@ describe('ExtensionService', () => { expect(instance).toBeTruthy(); }); - it('should register the RowSelection & RowMoveManager addons with specific "columnIndexPosition" and expect these orders to be respected regardless of when the feature is enabled/created', () => { + it('should call RowMoveManager create when "enableRowMoveManager" is set in the grid options provided', () => { const columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, { id: 'field2', field: 'field2', width: 50, }] as Column[]; const gridOptionsMock = { enableCheckboxSelector: true, enableRowSelection: true, @@ -588,15 +587,14 @@ describe('ExtensionService', () => { enableRowMoveManager: true, rowMoveManager: { columnIndexPosition: 0 } } as GridOption; - const rowMoveInstanceMock = { onBeforeMoveRows: jest.fn(), onMoveRows: jest.fn() }; - const extCheckboxSpy = jest.spyOn(mockCheckboxSelectColumn, 'create'); - const extRowMoveSpy = jest.spyOn(extensionRowMoveStub, 'create').mockReturnValue(rowMoveInstanceMock); + const extCheckSelectSpy = jest.spyOn(mockCheckboxSelectColumn, 'create'); + const extRowMoveSpy = jest.spyOn(mockRowMoveManager, 'create'); service.createExtensionsBeforeGridCreation(columnsMock, gridOptionsMock); + + expect(extCheckSelectSpy).toHaveBeenCalledWith(columnsMock, gridOptionsMock); expect(extRowMoveSpy).toHaveBeenCalledWith(columnsMock, gridOptionsMock); - expect(extCheckboxSpy).toHaveBeenCalledWith(columnsMock, gridOptionsMock); - expect(columnsMock).toEqual([{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, { id: 'field2', field: 'field2', width: 50, }]); }); }); @@ -874,7 +872,6 @@ describe('ExtensionService', () => { treeDataServiceStub, // extensions extensionStub as unknown as RowDetailViewExtension, - extensionStub as unknown as RowMoveManagerExtension, sharedService, translateService, ); diff --git a/packages/common/src/services/extension.service.ts b/packages/common/src/services/extension.service.ts index dce8fd89d..e9ef08278 100644 --- a/packages/common/src/services/extension.service.ts +++ b/packages/common/src/services/extension.service.ts @@ -3,7 +3,6 @@ import { ColumnReorderFunction, ExtensionList, ExtensionName, SlickControlList, import { ExtensionUtility, RowDetailViewExtension, - RowMoveManagerExtension, } from '../extensions/index'; import { SharedService } from './shared.service'; import { TranslaterService } from './translater.service'; @@ -17,6 +16,7 @@ import { SlickDraggableGrouping, SlickHeaderButtons, SlickHeaderMenu, + SlickRowMoveManager, SlickRowSelectionModel } from '../plugins/index'; import { FilterService } from './filter.service'; @@ -28,10 +28,13 @@ import { TreeDataService } from './treeData.service'; interface ExtensionWithColumnIndexPosition { name: ExtensionName; columnIndexPosition: number; - extension: SlickCheckboxSelectColumn | RowDetailViewExtension | RowMoveManagerExtension; + extension: SlickCheckboxSelectColumn | RowDetailViewExtension | SlickRowMoveManager; } export class ExtensionService { + protected _extensionCreatedList: ExtensionList = {} as ExtensionList; + protected _extensionList: ExtensionList = {} as ExtensionList; + protected _cellMenuPlugin?: SlickCellMenu; protected _cellExcelCopyManagerPlugin?: SlickCellExcelCopyManager; protected _checkboxSelectColumn?: SlickCheckboxSelectColumn; @@ -41,8 +44,7 @@ export class ExtensionService { protected _gridMenuControl?: SlickGridMenu; protected _groupItemMetadataProviderService?: GroupItemMetadataProviderService; protected _headerMenuPlugin?: SlickHeaderMenu; - protected _extensionCreatedList: ExtensionList = {} as ExtensionList; - protected _extensionList: ExtensionList = {} as ExtensionList; + protected _rowMoveManagerPlugin?: SlickRowMoveManager; protected _rowSelectionModel?: SlickRowSelectionModel; get extensionList() { @@ -61,7 +63,6 @@ export class ExtensionService { protected readonly treeDataService: TreeDataService, protected readonly rowDetailViewExtension: RowDetailViewExtension, - protected readonly rowMoveManagerExtension: RowMoveManagerExtension, protected readonly sharedService: SharedService, protected readonly translaterService?: TranslaterService, ) { } @@ -269,13 +270,17 @@ export class ExtensionService { } // Row Move Manager Plugin - if (this.gridOptions.enableRowMoveManager && this.rowMoveManagerExtension && this.rowMoveManagerExtension.register) { - const rowSelectionPlugin = this.getExtensionByName(ExtensionName.rowSelection); - this.rowMoveManagerExtension.register(rowSelectionPlugin?.instance as SlickRowSelectionModel); + if (this.gridOptions.enableRowMoveManager) { + this._rowMoveManagerPlugin = this._rowMoveManagerPlugin || new SlickRowMoveManager(); + this._rowMoveManagerPlugin.init(this.sharedService.slickGrid, this.sharedService.gridOptions.rowMoveManager); + if (!this._rowSelectionModel || !this.sharedService.slickGrid.getSelectionModel()) { + this._rowSelectionModel = new SlickRowSelectionModel(this.sharedService.gridOptions.rowSelectionOptions); + this.sharedService.slickGrid.setSelectionModel(this._rowSelectionModel); + } const createdExtension = this.getCreatedExtensionByName(ExtensionName.rowMoveManager); // get the instance from when it was really created earlier - const instance = createdExtension && createdExtension.instance; + const instance = createdExtension?.instance; if (instance) { - this._extensionList[ExtensionName.rowMoveManager] = { name: ExtensionName.rowMoveManager, class: this.rowMoveManagerExtension, instance }; + this._extensionList[ExtensionName.rowMoveManager] = { name: ExtensionName.rowMoveManager, class: this._rowMoveManagerPlugin, instance: this._rowMoveManagerPlugin }; } } } @@ -295,12 +300,13 @@ export class ExtensionService { if (gridOptions.enableCheckboxSelector) { if (!this.getCreatedExtensionByName(ExtensionName.checkboxSelector)) { this._checkboxSelectColumn = new SlickCheckboxSelectColumn(this.sharedService.gridOptions.checkboxSelector); - featureWithColumnIndexPositions.push({ name: ExtensionName.checkboxSelector, extension: this._checkboxSelectColumn as SlickCheckboxSelectColumn, columnIndexPosition: gridOptions?.checkboxSelector?.columnIndexPosition ?? featureWithColumnIndexPositions.length }); + featureWithColumnIndexPositions.push({ name: ExtensionName.checkboxSelector, extension: this._checkboxSelectColumn, columnIndexPosition: gridOptions?.checkboxSelector?.columnIndexPosition ?? featureWithColumnIndexPositions.length }); } } if (gridOptions.enableRowMoveManager) { if (!this.getCreatedExtensionByName(ExtensionName.rowMoveManager)) { - featureWithColumnIndexPositions.push({ name: ExtensionName.rowMoveManager, extension: this.rowMoveManagerExtension, columnIndexPosition: gridOptions?.rowMoveManager?.columnIndexPosition ?? featureWithColumnIndexPositions.length }); + this._rowMoveManagerPlugin = new SlickRowMoveManager(); + featureWithColumnIndexPositions.push({ name: ExtensionName.rowMoveManager, extension: this._rowMoveManagerPlugin, columnIndexPosition: gridOptions?.rowMoveManager?.columnIndexPosition ?? featureWithColumnIndexPositions.length }); } } if (gridOptions.enableRowDetailView) { diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index 1ed16c77b..edb5a1225 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -871,10 +871,16 @@ $pagination-text-color: #808080 !default; /* Row Move Manager Plugin */ $row-move-plugin-icon: "\f0c9" !default; +$row-move-plugin-icon-vertical-align: bottom !default; $row-move-plugin-icon-width: $icon-font-size !default; $row-move-plugin-size: $icon-font-size !default; $row-move-plugin-cursor: move !default; -$row-move-plugin-icon-vertical-align: bottom !default; +$row-move-plugin-guide-bg-color: blue !default; +$row-move-plugin-guide-height: 2px !default; +$row-move-plugin-guide-opacity: 0.7 !default; +$row-move-plugin-proxy-opacity: 0.12 !default; +$row-move-plugin-proxy-bg-color: $row-move-plugin-guide-bg-color !default; +$row-move-plugin-shadow-row-box-shadow: rgb(0 0 0 / 20%) 8px 2px 8px 4px, rgb(0 0 0 / 19%) 2px 2px 0px 0px !default; /* selector plugin */ $selector-border-right: 1px solid rgb(196, 196, 196) !default; diff --git a/packages/common/src/styles/slick-grid.scss b/packages/common/src/styles/slick-grid.scss index f9bfba56c..9a7135198 100644 --- a/packages/common/src/styles/slick-grid.scss +++ b/packages/common/src/styles/slick-grid.scss @@ -258,17 +258,23 @@ } .slick-reorder-proxy { - display: inline-block; - background: blue; - opacity: 0.15; cursor: move; + display: inline-block; + background: var(--slick-row-move-plugin-proxy-bg-color, $row-move-plugin-proxy-bg-color); + opacity: var(--row-move-plugin-proxy-opacity, $row-move-plugin-proxy-opacity); } .slick-reorder-guide { display: inline-block; - height: 2px; - background: blue; - opacity: 0.7; + height: var(--slick-row-move-plugin-guide-height, $row-move-plugin-guide-height); + background: var(--slick-row-move-plugin-guide-bg-color, $row-move-plugin-guide-bg-color); + opacity: var(--slick-row-move-plugin-guide-opacity, $row-move-plugin-guide-opacity); + } + + .slick-reorder-shadow-row { + position: absolute; + z-index: 999999; + box-shadow: var(--slick-row-move-plugin-shadow-row-box-shadow, $row-move-plugin-shadow-row-box-shadow); } .slick-selection { 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 541285398..994fb6ccb 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -47,7 +47,6 @@ import { Observable, PaginationService, ResizerService, - RowMoveManagerExtension, RxJsFacade, SharedService, SortService, @@ -348,7 +347,6 @@ export class SlickVanillaGridBundle { // extensions const rowDetailViewExtension = new RowDetailViewExtension(); - const rowMoveManagerExtension = new RowMoveManagerExtension(this.sharedService); this.extensionService = services?.extensionService ?? new ExtensionService( this.extensionUtility, @@ -357,7 +355,6 @@ export class SlickVanillaGridBundle { this.sortService, this.treeDataService, rowDetailViewExtension, - rowMoveManagerExtension, this.sharedService, this.translaterService, );