From 8e6eb4881741313b7d582d2e3d17ffef582ecb35 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 24 Sep 2021 21:36:43 -0400 Subject: [PATCH] feat(plugins): move external Draggable Grouping into Slickgrid-Universal --- .../assets/i18n/en.json | 2 + .../assets/i18n/fr.json | 2 + .../src/examples/example03.ts | 17 +- packages/common/src/constants.ts | 2 + .../src/enums/columnReorderFunction.type.ts | 4 +- .../draggableGroupingExtension.spec.ts | 150 ------ .../extensions/draggableGroupingExtension.ts | 84 --- .../common/src/extensions/extensionUtility.ts | 4 +- packages/common/src/extensions/index.ts | 1 - packages/common/src/global-grid-options.ts | 5 + .../src/interfaces/domEvent.interface.ts | 2 + .../interfaces/draggableGrouping.interface.ts | 7 +- .../draggableGroupingOption.interface.ts | 18 + .../common/src/interfaces/locale.interface.ts | 6 + .../src/interfaces/slickEvent.interface.ts | 2 +- .../src/interfaces/slickGrid.interface.ts | 6 +- .../draggableGrouping.plugin.spec.ts | 455 ++++++++++++++++ .../common/src/plugins/autoTooltip.plugin.ts | 14 +- .../src/plugins/draggableGrouping.plugin.ts | 501 ++++++++++++++++++ packages/common/src/plugins/index.ts | 1 + .../__tests__/bindingEvent.service.spec.ts | 5 + .../__tests__/extension.service.spec.ts | 32 +- .../src/services/__tests__/utilities.spec.ts | 18 + .../src/services/bindingEvent.service.ts | 18 +- .../common/src/services/extension.service.ts | 49 +- packages/common/src/services/utilities.ts | 12 + .../src/styles/_variables-theme-material.scss | 1 + .../styles/_variables-theme-salesforce.scss | 1 + packages/common/src/styles/_variables.scss | 23 +- packages/common/src/styles/slick-plugins.scss | 34 +- .../components/slick-vanilla-grid-bundle.ts | 3 - test/cypress/integration/example03.spec.js | 2 +- test/translateServiceStub.ts | 1 + 33 files changed, 1156 insertions(+), 326 deletions(-) delete mode 100644 packages/common/src/extensions/__tests__/draggableGroupingExtension.spec.ts delete mode 100644 packages/common/src/extensions/draggableGroupingExtension.ts create mode 100644 packages/common/src/plugins/__tests__/draggableGrouping.plugin.spec.ts create mode 100644 packages/common/src/plugins/draggableGrouping.plugin.ts diff --git a/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json b/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json index 9a407b516..fab99ab0f 100644 --- a/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json +++ b/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json @@ -15,6 +15,7 @@ "COMMANDS": "Commands", "CONTAINS": "Contains", "COPY": "Copy", + "DROP_COLUMN_HEADER_TO_GROUP_BY": "Drop a column header here to group by the column", "EMPTY_DATA_WARNING_MESSAGE": "No data to display.", "ENDS_WITH": "Ends With", "EQUALS": "Equals", @@ -57,6 +58,7 @@ "SORT_DESCENDING": "Sort Descending", "STARTS_WITH": "Starts With", "SYNCHRONOUS_RESIZE": "Synchronous resize", + "TOGGLE_ALL_GROUPS": "Toggle all Groups", "TOGGLE_FILTER_ROW": "Toggle Filter Row", "TOGGLE_PRE_HEADER_ROW": "Toggle Pre-Header Row", "X_OF_Y_SELECTED": "# of % selected", diff --git a/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json b/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json index 07abe9c68..c15de1a4c 100644 --- a/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json +++ b/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json @@ -15,6 +15,7 @@ "COMMANDS": "Commandes", "CONTAINS": "Contient", "COPY": "Copier", + "DROP_COLUMN_HEADER_TO_GROUP_BY": "Glisser une en-tête de colonne ici pour grouper par cet colonne", "EMPTY_DATA_WARNING_MESSAGE": "Aucune donnée à afficher.", "ENDS_WITH": "Se termine par", "EQUALS": "Égale", @@ -57,6 +58,7 @@ "SORT_DESCENDING": "Trier par ordre décroissant", "STARTS_WITH": "Commence par", "SYNCHRONOUS_RESIZE": "Redimension synchrone", + "TOGGLE_ALL_GROUPS": "Basculer tous les groupes", "TOGGLE_FILTER_ROW": "Basculer la ligne des filtres", "TOGGLE_PRE_HEADER_ROW": "Basculer la ligne de pré-en-tête", "X_OF_Y_SELECTED": "# de % sélectionnés", diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts index 5f10fe23f..8e8b0afcb 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts @@ -2,6 +2,7 @@ import { Aggregators, BindingEventService, Column, + DraggableGroupingPlugin, Editors, FieldType, FileType, @@ -11,7 +12,6 @@ import { Grouping, GroupingGetterFunction, GroupTotalFormatters, - SlickDraggableGrouping, SlickNamespace, SortComparers, SortDirectionNumber, @@ -45,7 +45,7 @@ export class Example3 { excelExportService: ExcelExportService; sgb: SlickVanillaGridBundle; durationOrderByCount = false; - draggableGroupingPlugin: SlickDraggableGrouping; + draggableGroupingPlugin: DraggableGroupingPlugin; loadingClass = ''; selectedGroupingFields: Array = ['', '', '']; @@ -310,6 +310,7 @@ export class Example3 { deleteIconCssClass: 'mdi mdi-close color-danger', onGroupChanged: (_e, args) => this.onGroupChanged(args), onExtensionRegistered: (extension) => this.draggableGroupingPlugin = extension, + // groupIconCssClass: 'mdi mdi-drag-vertical', }, enableCheckboxSelector: true, enableRowSelection: true, @@ -381,7 +382,7 @@ export class Example3 { } clearGrouping() { - if (this.draggableGroupingPlugin && this.draggableGroupingPlugin.setDroppedGroups) { + if (this.draggableGroupingPlugin?.setDroppedGroups) { this.draggableGroupingPlugin.clearDroppedGroups(); } this.sgb?.slickGrid.invalidate(); // invalidate all rows and re-render @@ -404,7 +405,7 @@ export class Example3 { groupByDuration() { this.clearGrouping(); - if (this.draggableGroupingPlugin && this.draggableGroupingPlugin.setDroppedGroups) { + if (this.draggableGroupingPlugin?.setDroppedGroups) { this.showPreHeader(); this.draggableGroupingPlugin.setDroppedGroups('duration'); this.sgb?.slickGrid.invalidate(); // invalidate all rows and re-render @@ -424,20 +425,16 @@ export class Example3 { groupByDurationEffortDriven() { this.clearGrouping(); - if (this.draggableGroupingPlugin && this.draggableGroupingPlugin.setDroppedGroups) { + if (this.draggableGroupingPlugin?.setDroppedGroups) { this.showPreHeader(); this.draggableGroupingPlugin.setDroppedGroups(['duration', 'effortDriven']); this.sgb?.slickGrid.invalidate(); // invalidate all rows and re-render - - // you need to manually add the sort icon(s) in UI - const sortColumns = [{ columnId: 'duration', sortAsc: true }]; - this.sgb?.slickGrid.setSortColumns(sortColumns); } } groupByFieldName(_fieldName, _index) { this.clearGrouping(); - if (this.draggableGroupingPlugin && this.draggableGroupingPlugin.setDroppedGroups) { + if (this.draggableGroupingPlugin?.setDroppedGroups) { this.showPreHeader(); // get the field names from Group By select(s) dropdown, but filter out any empty fields diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 17fb58fbc..eefd3dc41 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -18,6 +18,7 @@ export class Constants { TEXT_COLUMN_RESIZE_BY_CONTENT: 'Resize by Content', TEXT_COMMANDS: 'Commands', TEXT_COPY: 'Copy', + TEXT_DROP_COLUMN_HEADER_TO_GROUP_BY: 'Drop a column header here to group by the column', TEXT_EQUALS: 'Equals', TEXT_EQUAL_TO: 'Equal to', TEXT_ENDS_WITH: 'Ends With', @@ -61,6 +62,7 @@ export class Constants { TEXT_SORT_ASCENDING: 'Sort Ascending', TEXT_SORT_DESCENDING: 'Sort Descending', TEXT_STARTS_WITH: 'Starts With', + TEXT_TOGGLE_ALL_GROUPS: 'Toggle all Groups', TEXT_TOGGLE_FILTER_ROW: 'Toggle Filter Row', TEXT_TOGGLE_PRE_HEADER_ROW: 'Toggle Pre-Header Row', TEXT_X_OF_Y_SELECTED: '# of % selected', diff --git a/packages/common/src/enums/columnReorderFunction.type.ts b/packages/common/src/enums/columnReorderFunction.type.ts index ac8e38cfa..ec6f7f491 100644 --- a/packages/common/src/enums/columnReorderFunction.type.ts +++ b/packages/common/src/enums/columnReorderFunction.type.ts @@ -1,3 +1,3 @@ -import { Column, SlickGrid } from '../interfaces/index'; +import { Column, SlickEvent, SlickGrid } from '../interfaces/index'; -export type ColumnReorderFunction = (grid: SlickGrid, headers: HTMLElement[], headerColumnWidthDiff: any, setColumns: (col: Column) => void, setupColumnResize: () => void, columns: Column[], getColumnIndex: number, uid: string, trigger: boolean) => void; +export type ColumnReorderFunction = (grid: SlickGrid, headers: any, headerColumnWidthDiff: any, setColumns: (cols: Column[]) => void, setupColumnResize: () => void, columns: Column[], getColumnIndex: (column: Column) => number, uid: string, trigger: (slickEvent: SlickEvent, data?: any) => void) => void; diff --git a/packages/common/src/extensions/__tests__/draggableGroupingExtension.spec.ts b/packages/common/src/extensions/__tests__/draggableGroupingExtension.spec.ts deleted file mode 100644 index f8158baa2..000000000 --- a/packages/common/src/extensions/__tests__/draggableGroupingExtension.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { DraggableGrouping, GridOption, SlickDraggableGrouping, SlickGrid, SlickNamespace } from '../../interfaces/index'; -import { DraggableGroupingExtension } from '../draggableGroupingExtension'; -import { ExtensionUtility } from '../extensionUtility'; -import { SharedService } from '../../services/shared.service'; -import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; -import { BackendUtilityService } from '../../services'; -import { PubSubService } from '../../services/pubSub.service'; - -declare const Slick: SlickNamespace; - -const gridStub = { - getOptions: jest.fn(), - registerPlugin: jest.fn(), -} as unknown as SlickGrid; - -const fnCallbacks = {}; -const pubSubServiceStub = { - publish: jest.fn(), - subscribe: (eventName, fn) => fnCallbacks[eventName as string] = fn, - unsubscribe: jest.fn(), - unsubscribeAll: jest.fn(), -} as PubSubService; -jest.mock('../../services/pubSub.service', () => ({ - PubSubService: () => pubSubServiceStub -})); - -const mockAddon = jest.fn().mockImplementation(() => ({ - init: jest.fn(), - destroy: jest.fn(), - clearDroppedGroups: jest.fn(), - onGroupChanged: new Slick.Event(), -})); - -describe('draggableGroupingExtension', () => { - jest.mock('slickgrid/plugins/slick.draggablegrouping', () => mockAddon); - Slick.DraggableGrouping = mockAddon; - - let extensionUtility: ExtensionUtility; - let backendUtilityService: BackendUtilityService; - let sharedService: SharedService; - let extension: DraggableGroupingExtension; - let translateService: TranslateServiceStub; - const gridOptionsMock = { - enableDraggableGrouping: true, - draggableGrouping: { - deleteIconCssClass: 'class', - dropPlaceHolderText: 'test', - groupIconCssClass: 'group-class', - onExtensionRegistered: jest.fn(), - onGroupChanged: () => { }, - } - } as GridOption; - - beforeEach(() => { - sharedService = new SharedService(); - backendUtilityService = new BackendUtilityService(); - translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); - extension = new DraggableGroupingExtension(extensionUtility, pubSubServiceStub, sharedService); - }); - - it('should return null after calling "create" method when the grid options is missing', () => { - const output = extension.create(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('registered addon', () => { - beforeEach(() => { - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - }); - - it('should register the addon', () => { - const onRegisteredSpy = jest.spyOn(SharedService.prototype.gridOptions.draggableGrouping as DraggableGrouping, 'onExtensionRegistered'); - const pluginSpy = jest.spyOn(SharedService.prototype.slickGrid, 'registerPlugin'); - - const instance = extension.create(gridOptionsMock) as SlickDraggableGrouping; - const addon = extension.register(); - - const addonInstance = extension.getAddonInstance(); - - expect(instance).toBeTruthy(); - expect(instance).toEqual(addonInstance); - expect(addon).not.toBeNull(); - expect(mockAddon).toHaveBeenCalledWith({ - deleteIconCssClass: 'class', - dropPlaceHolderText: 'test', - groupIconCssClass: 'group-class', - onExtensionRegistered: expect.anything(), - onGroupChanged: expect.anything(), - }); - expect(onRegisteredSpy).toHaveBeenCalledWith(instance); - expect(pluginSpy).toHaveBeenCalledWith(instance); - }); - - it('should dispose of the addon', () => { - const instance = extension.create(gridOptionsMock) as SlickDraggableGrouping; - 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 = { - deleteIconCssClass: 'different-class', - dropPlaceHolderText: 'different-test', - groupIconCssClass: 'different-group-class', - }; - const addonOptions = { ...gridOptionsMock, draggableGrouping: optionMock }; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(addonOptions); - - extension.create(addonOptions); - extension.register(); - - expect(mockAddon).toHaveBeenCalledWith(optionMock); - }); - - it('should call internal event handler subscribe and expect the "onGroupChanged" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.draggableGrouping as DraggableGrouping, 'onGroupChanged'); - - const instance = extension.create(gridOptionsMock) as SlickDraggableGrouping; - extension.register(); - instance.onGroupChanged.notify({ caller: 'clear-all', groupColumns: [] }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { caller: 'clear-all', groupColumns: [] }); - }); - - it('should expect that it call the Draggable Grouping "clearDroppedGroups" when Context Menu subscribed event triggers a clear grouping', () => { - extension.create(gridOptionsMock) as SlickDraggableGrouping; - const addon = extension.register(); - const clearSpy = jest.spyOn(addon, 'clearDroppedGroups'); - - fnCallbacks['contextMenu:clearGrouping'](true); - - expect(clearSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/common/src/extensions/draggableGroupingExtension.ts b/packages/common/src/extensions/draggableGroupingExtension.ts deleted file mode 100644 index ce9fad14b..000000000 --- a/packages/common/src/extensions/draggableGroupingExtension.ts +++ /dev/null @@ -1,84 +0,0 @@ -import 'slickgrid/plugins/slick.draggablegrouping'; - -import { DraggableGrouping, Extension, GetSlickEventType, GridOption, SlickDraggableGrouping, SlickEventHandler, SlickNamespace } from '../interfaces/index'; -import { ExtensionUtility } from './extensionUtility'; -import { PubSubService } from '../services/pubSub.service'; -import { SharedService } from '../services/shared.service'; - -// using external non-typed js libraries -declare const Slick: SlickNamespace; - -export class DraggableGroupingExtension implements Extension { - private _addon: SlickDraggableGrouping | null = null; - private _draggableGroupingOptions: DraggableGrouping | null = null; - private _eventHandler: SlickEventHandler; - - constructor(private readonly extensionUtility: ExtensionUtility, private readonly pubSubService: PubSubService, 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.extensionUtility.nullifyFunctionNameStartingWithOn(this._draggableGroupingOptions); - this._addon = null; - this._draggableGroupingOptions = null; - } - - /** - * Bind/Create different plugins before the Grid creation. - * For example the multi-select have to be added to the column definition before the grid is created to work properly - */ - create(gridOptions: GridOption): SlickDraggableGrouping | null { - if (gridOptions) { - if (!this._addon) { - this._addon = new Slick.DraggableGrouping(gridOptions.draggableGrouping); - } - return this._addon; - } - return null; - } - - /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance(): SlickDraggableGrouping | null { - return this._addon; - } - - /** Register the 3rd party addon (plugin) */ - register(): SlickDraggableGrouping | null { - if (this._addon && this.sharedService && this.sharedService.slickGrid && this.sharedService.gridOptions) { - this.sharedService.slickGrid.registerPlugin(this._addon); - - // Events - if (this.sharedService.slickGrid && this.sharedService.gridOptions.draggableGrouping) { - this._draggableGroupingOptions = this.sharedService.gridOptions.draggableGrouping; - if (this._addon && this._draggableGroupingOptions.onExtensionRegistered) { - this._draggableGroupingOptions.onExtensionRegistered(this._addon); - } - - if (this._addon && this._addon.onGroupChanged) { - const onGroupChangedHandler = this._addon.onGroupChanged; - (this._eventHandler as SlickEventHandler>).subscribe(onGroupChangedHandler, (e, args) => { - if (this._draggableGroupingOptions && typeof this._draggableGroupingOptions.onGroupChanged === 'function') { - this._draggableGroupingOptions.onGroupChanged(e, args); - } - }); - } - - // we also need to subscribe to a possible user clearing the grouping via the Context Menu, we need to clear the pre-header bar as well - this.pubSubService.subscribe('contextMenu:clearGrouping', () => this._addon?.clearDroppedGroups?.()); - } - - return this._addon; - } - return null; - } -} diff --git a/packages/common/src/extensions/extensionUtility.ts b/packages/common/src/extensions/extensionUtility.ts index 7b8f13dc5..dae0e5839 100644 --- a/packages/common/src/extensions/extensionUtility.ts +++ b/packages/common/src/extensions/extensionUtility.ts @@ -134,12 +134,12 @@ export class ExtensionUtility { } } - /** Translate the an array of items from an input key and assign to the output key */ + /** Translate the array of items from an input key and assign them to their output key */ translateItems(items: T[], inputKey: string, outputKey: string) { if (Array.isArray(items)) { for (const item of items) { if ((item as any)[inputKey]) { - (item as any)[outputKey] = this.translaterService && this.translaterService.getCurrentLanguage && this.translaterService.translate && this.translaterService.translate((item as any)[inputKey]); + (item as any)[outputKey] = this.translaterService?.translate?.((item as any)[inputKey]); } } } diff --git a/packages/common/src/extensions/index.ts b/packages/common/src/extensions/index.ts index 190d1bb06..ce5bfc9ee 100644 --- a/packages/common/src/extensions/index.ts +++ b/packages/common/src/extensions/index.ts @@ -1,6 +1,5 @@ export * from './cellExternalCopyManagerExtension'; export * from './checkboxSelectorExtension'; -export * from './draggableGroupingExtension'; export * from './extensionUtility'; export * from './groupItemMetaProviderExtension'; export * from './rowDetailViewExtension'; diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index 09f09e494..68bafe386 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -105,6 +105,11 @@ export const GlobalGridOptions: GridOption = { defaultColumnSortFieldId: 'id', defaultComponentEventPrefix: '', defaultSlickgridEventPrefix: '', + draggableGrouping: { + hideToggleAllButton: false, + toggleAllButtonText: '', + dropPlaceHolderTextKey: 'DROP_COLUMN_HEADER_TO_GROUP_BY', + }, editable: false, editorTypingDebounce: 450, filterTypingDebounce: 0, diff --git a/packages/common/src/interfaces/domEvent.interface.ts b/packages/common/src/interfaces/domEvent.interface.ts index c3f36c984..356a90c3f 100644 --- a/packages/common/src/interfaces/domEvent.interface.ts +++ b/packages/common/src/interfaces/domEvent.interface.ts @@ -1,8 +1,10 @@ export interface DOMEvent extends Event { + currentTarget: T; target: T; relatedTarget: T; } export interface DOMMouseEvent extends MouseEvent { + currentTarget: T; target: T; relatedTarget: T; } diff --git a/packages/common/src/interfaces/draggableGrouping.interface.ts b/packages/common/src/interfaces/draggableGrouping.interface.ts index 949714a13..8580379cb 100644 --- a/packages/common/src/interfaces/draggableGrouping.interface.ts +++ b/packages/common/src/interfaces/draggableGrouping.interface.ts @@ -1,7 +1,6 @@ -import { Grouping } from './grouping.interface'; +import { Grouping, SlickEventData } from './index'; import { DraggableGroupingOption } from './draggableGroupingOption.interface'; -import { SlickDraggableGrouping } from './slickDraggableGrouping.interface'; -import { SlickEventData } from './slickEventData.interface'; +import { DraggableGroupingPlugin } from '../plugins/draggableGrouping.plugin'; export interface DraggableGrouping extends DraggableGroupingOption { // @@ -11,5 +10,5 @@ export interface DraggableGrouping extends DraggableGroupingOption { onGroupChanged?: (e: SlickEventData, args: { caller?: string; groupColumns: Grouping[] }) => void; /** Fired after extension (plugin) is registered by SlickGrid */ - onExtensionRegistered?: (plugin: SlickDraggableGrouping) => void; + onExtensionRegistered?: (plugin: DraggableGroupingPlugin) => void; } diff --git a/packages/common/src/interfaces/draggableGroupingOption.interface.ts b/packages/common/src/interfaces/draggableGroupingOption.interface.ts index 384232873..14f7e28e3 100644 --- a/packages/common/src/interfaces/draggableGroupingOption.interface.ts +++ b/packages/common/src/interfaces/draggableGroupingOption.interface.ts @@ -14,6 +14,9 @@ export interface DraggableGroupingOption { /** option to specify set own placeholder note text */ dropPlaceHolderText?: string; + /** Defaults to "TEXT_DROP_COLUMN_HEADER_TO_GROUP_BY", translation key of the dropbox placeholder which shows in the pre-header when using the Draggable Grouping plugin. */ + dropPlaceHolderTextKey?: string; + /** an extra CSS class to add to the grouping field hint (default undefined) */ groupIconCssClass?: string; @@ -23,6 +26,21 @@ export interface DraggableGroupingOption { */ groupIconImage?: string; + /** Defaults to False, should we display a toggle all button (typically aligned on the left before any of the column group) */ + hideToggleAllButton?: boolean; + + /** Defaults to "Toggle all Groups", placeholder of the Toggle All button that can optionally show up in the pre-header row. */ + toggleAllPlaceholderText?: string; + + /** Defaults to "TOGGLE_ALL_GROUPS", translation key of the Toggle All button placeholder that can optionally show up in the pre-header row. */ + toggleAllPlaceholderTextKey?: string; + + /** Defaults to empty string, text to show in the Toggle All button that can optionally show up in the pre-header row. */ + toggleAllButtonText?: string; + + /** Defaults to "TOGGLE_ALL_GROUPS", translation key of text to show in the Toggle All button that can optionally show up in the pre-header row. */ + toggleAllButtonTextKey?: string; + // // Methods // --------- diff --git a/packages/common/src/interfaces/locale.interface.ts b/packages/common/src/interfaces/locale.interface.ts index 03eb3302d..087f24835 100644 --- a/packages/common/src/interfaces/locale.interface.ts +++ b/packages/common/src/interfaces/locale.interface.ts @@ -47,6 +47,9 @@ export interface Locale { /** Text "Copy" shown in Context Menu to copy a cell value */ TEXT_COPY: string; + /** Text "Drop a column header here to group by the column" which shows in the pre-header when using the Draggable Grouping plugin */ + TEXT_DROP_COLUMN_HEADER_TO_GROUP_BY?: string; + /** Text "Starts With" shown in Compound Editors/Filters as an Operator */ TEXT_ENDS_WITH: string; @@ -176,6 +179,9 @@ export interface Locale { /** Text "Synchronous Resize" displayed in the Column Picker & Grid Menu (when enabled) */ TEXT_SYNCHRONOUS_RESIZE?: string; + /** Text "Toggle all Groups" which can optionally show in a button inside the Draggable Grouping pre-header row */ + TEXT_TOGGLE_ALL_GROUPS?: string; + /** Text "Toggle Filter Row" shown in Grid Menu (when enabled) */ TEXT_TOGGLE_FILTER_ROW?: string; diff --git a/packages/common/src/interfaces/slickEvent.interface.ts b/packages/common/src/interfaces/slickEvent.interface.ts index 10449a509..25a8c6182 100644 --- a/packages/common/src/interfaces/slickEvent.interface.ts +++ b/packages/common/src/interfaces/slickEvent.interface.ts @@ -25,5 +25,5 @@ export interface SlickEvent { * Removes an event handler added with subscribe(fn). * @param fn {Function} Event handler to be removed. */ - unsubscribe: (fn: (e: SlickEventData, data?: any) => void) => void; + unsubscribe: (fn?: (e: SlickEventData, data?: any) => void) => void; } diff --git a/packages/common/src/interfaces/slickGrid.interface.ts b/packages/common/src/interfaces/slickGrid.interface.ts index e48d17d99..4249e8d45 100644 --- a/packages/common/src/interfaces/slickGrid.interface.ts +++ b/packages/common/src/interfaces/slickGrid.interface.ts @@ -225,13 +225,13 @@ export interface SlickGrid { getPluginByName(name: string): T; /** Get the Pre-Header Panel DOM node element */ - getPreHeaderPanel(): HTMLElement; + getPreHeaderPanel(): HTMLDivElement; /** Get the Pre-Header Panel Left DOM node element */ - getPreHeaderPanelLeft(): HTMLElement; + getPreHeaderPanelLeft(): HTMLDivElement; /** Get the Pre-Header Panel Right DOM node element */ - getPreHeaderPanelRight(): HTMLElement; + getPreHeaderPanelRight(): HTMLDivElement; /** Get rendered range */ getRenderedRange(viewportTop?: number, viewportLeft?: number): { top: number; bottom: number; leftPx: number; rightPx: number; }; diff --git a/packages/common/src/plugins/__tests__/draggableGrouping.plugin.spec.ts b/packages/common/src/plugins/__tests__/draggableGrouping.plugin.spec.ts new file mode 100644 index 000000000..9bbfaa920 --- /dev/null +++ b/packages/common/src/plugins/__tests__/draggableGrouping.plugin.spec.ts @@ -0,0 +1,455 @@ +import 'jquery-ui/ui/widgets/sortable'; + +import { Aggregators } from '../../aggregators/aggregators.index'; +import { DraggableGroupingPlugin } from '../draggableGrouping.plugin'; +import { ExtensionUtility } from '../../extensions/extensionUtility'; +import { Column, DraggableGroupingOption, GridOption, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { BackendUtilityService, PubSubService } from '../../services'; +import { SharedService } from '../../services/shared.service'; +import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; + +declare const Slick: SlickNamespace; +const GRID_UID = 'slickgrid12345'; + +let addonOptions: DraggableGroupingOption = { + dropPlaceHolderText: 'Drop a column header here to group by the column', + hideToggleAllButton: false, + toggleAllButtonText: '', + toggleAllPlaceholderText: 'Toggle all Groups', +}; + +const gridOptionsMock = { + enableDraggableGrouping: true, + draggableGrouping: { + hideToggleAllButton: false, + }, + showHeaderRow: false, + showTopPanel: false, + showPreHeaderPanel: false +} as unknown as GridOption; + +const dataViewStub = { + collapseAllGroups: jest.fn(), + expandAllGroups: jest.fn(), + setGrouping: jest.fn(), +} + +const getEditorLockMock = { + commitCurrentEdit: jest.fn(), +}; + +const gridStub = { + getCellNode: jest.fn(), + getCellFromEvent: jest.fn(), + getColumns: jest.fn(), + getHeaderColumn: jest.fn(), + getOptions: jest.fn(), + getPreHeaderPanel: jest.fn(), + getData: () => dataViewStub, + getEditorLock: () => getEditorLockMock, + getUID: () => GRID_UID, + registerPlugin: jest.fn(), + updateColumnHeader: jest.fn(), + onColumnsReordered: new Slick.Event(), + onHeaderCellRendered: new Slick.Event(), + onHeaderMouseEnter: new Slick.Event(), + onMouseEnter: new Slick.Event(), +} as unknown as SlickGrid; + +const pubSubServiceStub = { + publish: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + unsubscribeAll: jest.fn(), +} as PubSubService; + +const mockColumns = [ // The column definitions + { id: 'firstName', name: 'First Name', field: 'firstName', width: 100 }, + { id: 'lasstName', name: 'Last Name', field: 'lasstName', width: 100 }, + { + id: 'age', name: 'Age', field: 'age', width: 50, + grouping: { + getter: 'age', aggregators: [new Aggregators.Avg('age')], + formatter: (g) => `Age: ${g.value} (${g.count} items)`, + } + }, + { + id: 'medals', name: 'Medals', field: 'medals', width: 50, + grouping: { + getter: 'medals', aggregators: [new Aggregators.Sum('medals')], + formatter: (g) => `Medals: ${g.value} (${g.count} items)`, + } + }, + { name: 'Gender', field: 'gender', width: 75 }, +] as Column[]; + +describe('Draggable Grouping Plugin', () => { + let plugin: DraggableGroupingPlugin; + let sharedService: SharedService; + let backendUtilityService: BackendUtilityService; + let extensionUtility: ExtensionUtility; + let translateService: TranslateServiceStub; + let headerDiv: HTMLDivElement; + let preHeaderDiv: HTMLDivElement; + let dragGroupDiv: HTMLDivElement; + + beforeEach(() => { + preHeaderDiv = document.createElement('div'); + headerDiv = document.createElement('div'); + dragGroupDiv = document.createElement('div'); + dragGroupDiv.className = 'ui-droppable ui-sortable'; + headerDiv.className = 'slick-header-column'; + preHeaderDiv.className = 'slick-preheader-panel'; + preHeaderDiv.appendChild(dragGroupDiv); + document.body.appendChild(preHeaderDiv); + + backendUtilityService = new BackendUtilityService(); + sharedService = new SharedService(); + translateService = new TranslateServiceStub(); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + jest.spyOn(gridStub, 'getPreHeaderPanel').mockReturnValue(preHeaderDiv); + plugin = new DraggableGroupingPlugin(extensionUtility, pubSubServiceStub, sharedService); + }); + + afterEach(() => { + plugin.dispose(); + document.body.innerHTML = ''; + }); + + it('should create the plugin', () => { + expect(plugin).toBeTruthy(); + expect(plugin.gridUid).toBe('slickgrid12345'); + expect(plugin.eventHandler).toBeTruthy(); + }); + + it('should create and dispose of the plugin', () => { + const disposeSpy = jest.spyOn(plugin, 'dispose'); + expect(plugin).toBeTruthy(); + + plugin.destroy(); + + expect(plugin.eventHandler).toBeTruthy(); + expect(disposeSpy).toHaveBeenCalled(); + }); + + it('should use default options when instantiating the plugin without passing any arguments', () => { + plugin.init(gridStub, addonOptions); + expect(plugin.addonOptions).toEqual(addonOptions); + }); + + it('should initialize the Draggable Grouping and expect optional "Toggle All" button text when provided to the plugin', () => { + plugin.init(gridStub, { ...addonOptions, toggleAllButtonText: 'Toggle all Groups' }); + + const preHeaderElm = document.querySelector('.slick-preheader-panel'); + const toggleAllTextElm = preHeaderElm.querySelector('.slick-group-toggle-all-text'); + expect(preHeaderElm).toBeTruthy(); + expect(toggleAllTextElm.textContent).toBe('Toggle all Groups'); + }); + + it('should initialize the Draggable Grouping and expect optional "Toggle All" button text with translated value when provided to the plugin with "toggleAllButtonTextKey"', () => { + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({ ...gridOptionsMock, enableTranslate: true }); + translateService.use('fr'); + plugin.init(gridStub, { ...addonOptions, toggleAllButtonTextKey: 'TOGGLE_ALL_GROUPS' }); + + const preHeaderElm = document.querySelector('.slick-preheader-panel'); + const toggleAllTextElm = preHeaderElm.querySelector('.slick-group-toggle-all-text'); + + expect(preHeaderElm).toBeTruthy(); + expect(toggleAllTextElm.textContent).toBe('Basculer tous les groupes'); + }); + + it('should initialize the Draggable Grouping and expect optional "Toggle All" button tooltip with translated value when provided to the plugin with "toggleAllPlaceholderText"', () => { + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({ ...gridOptionsMock, enableTranslate: true }); + translateService.use('fr'); + plugin.init(gridStub, { ...addonOptions, toggleAllPlaceholderTextKey: 'TOGGLE_ALL_GROUPS' }); + + const preHeaderElm = document.querySelector('.slick-preheader-panel'); + const toggleAllTextElm = preHeaderElm.querySelector('.slick-group-toggle-all') as HTMLDivElement; + + expect(preHeaderElm).toBeTruthy(); + expect(toggleAllTextElm.title).toBe('Basculer tous les groupes'); + }); + + it('should initialize the Draggable Grouping and expect optional "Toggle All" button text with translated value when provided to the plugin with "toggleAllButtonTextKey"', () => { + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({ ...gridOptionsMock, enableTranslate: true }); + translateService.use('fr'); + plugin.init(gridStub, { ...addonOptions, dropPlaceHolderTextKey: 'GROUP_BY' }); + + const preHeaderElm = document.querySelector('.slick-preheader-panel'); + const dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropbox-toggle-placeholder'); + + expect(preHeaderElm).toBeTruthy(); + expect(dropboxPlaceholderElm.textContent).toBe('Groupé par'); + }); + + it('should add an icon beside each column title when "groupIconCssClass" is provided', () => { + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({ ...gridOptionsMock, enableTranslate: true }); + translateService.use('fr'); + plugin.init(gridStub, { ...addonOptions, groupIconCssClass: 'mdi mdi-drag' }); + const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() }; + gridStub.onHeaderCellRendered.notify({ column: mockColumns[2], node: headerDiv, grid: gridStub }, eventData, gridStub); + const groupableElm = headerDiv.querySelector('.slick-column-groupable') as HTMLSpanElement; + + expect(headerDiv.style.cursor).toBe('pointer'); + expect(groupableElm.classList.contains('mdi-drag')).toBeTruthy(); + }); + + it('should add an icon beside each column title when "groupIconImage" is provided', () => { + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({ ...gridOptionsMock, enableTranslate: true }); + translateService.use('fr'); + plugin.init(gridStub, { ...addonOptions, groupIconImage: '/images/some-image.png' }); + const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() }; + gridStub.onHeaderCellRendered.notify({ column: mockColumns[2], node: headerDiv, grid: gridStub }, eventData, gridStub); + const groupableElm = headerDiv.querySelector('.slick-column-groupable') as HTMLSpanElement; + + expect(headerDiv.style.cursor).toBe('pointer'); + expect(groupableElm.style.background).toBe('url(/images/some-image.png) no-repeat center'); + }); + + describe('setupColumnReorder definition', () => { + let dropEvent; + let dropTargetElm: HTMLSpanElement; + let mockHelperElm: HTMLSpanElement; + let $headerColumnElm: any; + let $mockDivPaneContainer1: any; + let $mockDivPaneContainer2: any; + const setColumnsSpy = jest.fn(); + const setColumnResizeSpy = jest.fn(); + const getColumnIndexSpy = jest.fn(); + const triggerSpy = jest.fn(); + const setGroupingSpy = jest.spyOn(dataViewStub, 'setGrouping'); + + beforeEach(() => { + mockHelperElm = document.createElement('span'); + const mockDivPaneContainerElm = document.createElement('div'); + mockDivPaneContainerElm.className = 'slick-pane-header'; + const mockDivPaneContainerElm2 = document.createElement('div'); + mockDivPaneContainerElm2.className = 'slick-pane-header'; + const mockHeaderLeftDiv1 = document.createElement('div'); + const mockHeaderLeftDiv2 = document.createElement('div'); + mockHeaderLeftDiv1.className = 'slick-header-columns slick-header-columns-left ui-sortable'; + mockHeaderLeftDiv2.className = 'slick-header-columns slick-header-columns-right ui-sortable'; + const $mockHeaderLeftDiv1 = $(mockHeaderLeftDiv1); + const $mockHeaderLeftDiv2 = $(mockHeaderLeftDiv2); + $mockDivPaneContainer1 = $(mockDivPaneContainerElm); + $mockDivPaneContainer2 = $(mockDivPaneContainerElm2); + $mockHeaderLeftDiv1.appendTo($mockDivPaneContainer1); + $mockHeaderLeftDiv2.appendTo($mockDivPaneContainer2); + + dropTargetElm = document.createElement('div'); + dropEvent = new Event('mouseup'); + preHeaderDiv.appendChild(dropTargetElm); + Object.defineProperty(dropEvent, 'target', { writable: true, configurable: true, value: dropTargetElm }); + const headerColumnElm = document.createElement('div'); + headerColumnElm.className = 'slick-header-column'; + headerColumnElm.id = 'slickgrid12345age'; + headerColumnElm.dataset.id = 'age'; + const columnSpanElm = document.createElement('span'); + headerColumnElm.appendChild(columnSpanElm); + preHeaderDiv.appendChild(headerColumnElm); + $headerColumnElm = $(headerColumnElm); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should execute the "start" callback of the jQueryUI Sortable and expect css classes to be updated', () => { + plugin.init(gridStub, { ...addonOptions }); + const droppableOptions = $(plugin.dropboxElement).droppable('option') as any; + droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); + const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + let placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; + let dropGroupingElm = dropTargetElm.querySelector('.slick-dropped-grouping') as HTMLDivElement; + const startFn = fn.sortable('option', 'start'); + startFn(new Event('click'), { helper: mockHelperElm }); + + expect(mockHelperElm.classList.contains('slick-header-column-active')).toBeTruthy(); + expect(placeholderElm.style.display).toBe('inline-block'); + expect(dropGroupingElm.style.display).toBe('none'); + + let groupByRemoveElm = preHeaderDiv.querySelector('.slick-groupby-remove'); + const groupByRemoveImageElm = document.querySelector('.slick-groupby-remove-image'); + + expect(groupByRemoveElm).toBeTruthy(); + expect(groupByRemoveImageElm).toBeTruthy(); + + groupByRemoveElm.dispatchEvent(new Event('click')); + + groupByRemoveElm = preHeaderDiv.querySelector('.slick-groupby-remove'); + placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; + const toggleAllElm = preHeaderDiv.querySelector('.slick-group-toggle-all') as HTMLDivElement; + + expect(setGroupingSpy).toHaveBeenCalledWith([]); + expect(groupByRemoveElm).toBeFalsy(); + expect(placeholderElm.style.display).toBe('inline-block'); + expect(toggleAllElm.style.display).toBe('none'); + }); + + it('should execute the "beforeStop" callback of the jQueryUI Sortable and expect css classes to be updated', () => { + plugin.init(gridStub, { ...addonOptions, deleteIconCssClass: 'mdi mdi-close' }); + const droppableOptions = $(plugin.dropboxElement).droppable('option') as any; + droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); + const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + const beforeStopFn = fn.sortable('option', 'beforeStop'); + beforeStopFn(new Event('click'), { helper: mockHelperElm }); + + let placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; + let dropGroupingElm = dropTargetElm.querySelector('.slick-dropped-grouping') as HTMLDivElement; + expect(placeholderElm.style.display).toBe('none'); + expect(dropGroupingElm.style.display).toBe('inline-block'); + }); + + it('should execute the "stop" callback of the jQueryUI Sortable and expect sortable to be cancelled', () => { + plugin.init(gridStub, { ...addonOptions, deleteIconCssClass: 'mdi mdi-close' }); + const droppableOptions = $(plugin.dropboxElement).droppable('option') as any; + droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); + jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(false); + const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + const stopFn = fn.sortable('option', 'stop'); + + const groupByRemoveElm = document.querySelector('.slick-groupby-remove.mdi-close'); + expect(groupByRemoveElm).toBeTruthy(); + + stopFn(new Event('click'), { helper: mockHelperElm }); + + expect(setColumnsSpy).not.toHaveBeenCalled(); + expect(setColumnResizeSpy).not.toHaveBeenCalled(); + expect(triggerSpy).not.toHaveBeenCalled(); + }); + + it('should execute the "stop" callback of the jQueryUI Sortable and expect css classes to be updated', () => { + plugin.init(gridStub, { ...addonOptions, deleteIconImage: '/images/delete.png' }); + plugin.setColumns(mockColumns); + const droppableOptions = $(plugin.dropboxElement).droppable('option') as any; + droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); + jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); + getColumnIndexSpy.mockReturnValue(2); + const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1.add($mockDivPaneContainer2), {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + const stopFn = fn.sortable('option', 'stop'); + + const groupByRemoveElm = document.querySelector('.slick-groupby-remove') as HTMLDivElement; + expect(groupByRemoveElm.style.background).toBe('url(/images/delete.png) no-repeat right'); + + stopFn(new Event('click'), { helper: mockHelperElm }); + + expect(setColumnsSpy).toHaveBeenCalledWith([mockColumns[2], mockColumns[2]]); + expect(setColumnResizeSpy).toHaveBeenCalled(); + expect(triggerSpy).toHaveBeenCalledWith(gridStub.onColumnsReordered, { grid: gridStub }); + }); + + describe('setupColumnDropbox method', () => { + it('should expect denied class to be removed when "deactivate" is called', () => { + plugin.init(gridStub, { ...addonOptions }); + const deactivateFn = plugin.droppableInstance.droppable('option', 'deactivate'); + plugin.dropboxElement.classList.add('slick-header-column-denied'); + deactivateFn(); + + expect(plugin.dropboxElement.classList.contains('slick-header-column-denied')).toBeFalsy(); + }); + + it('should expect denied class to be added when calling "over" with a header column that does not have a "grouping" property', () => { + const mockHeaderColumnDiv = document.createElement('div'); + mockHeaderColumnDiv.id = 'slickgrid12345firstName'; + mockHeaderColumnDiv.className = 'slick-header-column'; + const $mockHeaderColumnDiv = $(mockHeaderColumnDiv); + + plugin.init(gridStub, { ...addonOptions }); + const overFn = plugin.droppableInstance.droppable('option', 'over'); + overFn(new Event('mouseup'), { draggable: $mockHeaderColumnDiv }); + + expect(plugin.dropboxElement.classList.contains('slick-header-column-denied')).toBeTruthy(); + }); + + describe('setupColumnDropbox update & toggler click event', () => { + let groupChangedSpy: any; + const updateEvent = new Event('mouseup'); + let mockHeaderColumnDiv1: HTMLDivElement; + let mockHeaderColumnDiv2: HTMLDivElement; + + beforeEach(() => { + groupChangedSpy = jest.spyOn(plugin.onGroupChanged, 'notify'); + mockHeaderColumnDiv1 = document.createElement('div'); + mockHeaderColumnDiv1.className = 'slick-dropped-grouping'; + mockHeaderColumnDiv1.id = 'age'; + mockHeaderColumnDiv1.dataset.id = 'age'; + + mockHeaderColumnDiv2 = document.createElement('div'); + mockHeaderColumnDiv2.className = 'slick-dropped-grouping'; + mockHeaderColumnDiv2.id = 'medals'; + mockHeaderColumnDiv2.dataset.id = 'medals'; + dragGroupDiv.appendChild(mockHeaderColumnDiv1); + dragGroupDiv.appendChild(mockHeaderColumnDiv2); + $(mockHeaderColumnDiv1).appendTo($mockDivPaneContainer1); + $(mockHeaderColumnDiv2).appendTo($mockDivPaneContainer1); + + Object.defineProperty(updateEvent, 'target', { writable: true, configurable: true, value: $mockDivPaneContainer1.get(0) }); + + plugin.init(gridStub, { ...addonOptions, deleteIconCssClass: 'mdi mdi-close' }); + const droppableOptions = $(plugin.dropboxElement).droppable('option') as any; + droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); + jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(false); + const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + const updateFn = plugin.sortableInstance.sortable('option', 'update'); + updateFn(updateEvent); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call sortable "update" from setupColumnDropbox and expect "updateGroupBy" to be called with a sort-group', () => { + expect(plugin.columnsGroupBy.length).toBeGreaterThan(0); + expect(groupChangedSpy).toHaveBeenCalledWith({ + caller: 'sort-group', + groupColumns: [{ aggregators: expect.toBeArray(), formatter: mockColumns[2].grouping.formatter, getter: 'age' }], + }); + + jest.spyOn(gridStub, 'getHeaderColumn').mockReturnValue(mockHeaderColumnDiv1); + plugin.setDroppedGroups('age'); + }); + + it('should call "clearDroppedGroups" and expect the grouping to be cleared', () => { + const preHeaderElm = document.querySelector('.slick-preheader-panel'); + let dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; + expect(dropboxPlaceholderElm.style.display).toBe('none'); + + plugin.clearDroppedGroups(); + dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; + expect(dropboxPlaceholderElm.style.display).toBe('inline-block'); + expect(groupChangedSpy).toHaveBeenCalledWith({ caller: 'clear-all', groupColumns: [], }); + }); + + it('should use the Toggle All and expect classes to be toggled and DataView to call necessary method', () => { + const dvExpandSpy = jest.spyOn(dataViewStub, 'expandAllGroups'); + const dvCollapseSpy = jest.spyOn(dataViewStub, 'collapseAllGroups'); + let toggleAllElm = document.querySelector('.slick-group-toggle-all'); + const toggleAllIconElm = toggleAllElm.querySelector('.slick-group-toggle-all-icon'); + const clickEvent = new Event('click'); + Object.defineProperty(clickEvent, 'target', { writable: true, configurable: true, value: toggleAllIconElm }); + + // initially expanded + expect(toggleAllIconElm.classList.contains('expanded')).toBeTruthy(); + expect(toggleAllIconElm.classList.contains('collapsed')).toBeFalsy(); + + // collapsed after toggle + toggleAllElm.dispatchEvent(clickEvent); + toggleAllElm = document.querySelector('.slick-group-toggle-all'); + expect(toggleAllIconElm.classList.contains('expanded')).toBeFalsy(); + expect(toggleAllIconElm.classList.contains('collapsed')).toBeTruthy(); + expect(dvCollapseSpy).toHaveBeenCalled(); + + // expanded after toggle + toggleAllElm.dispatchEvent(clickEvent); + toggleAllElm = document.querySelector('.slick-group-toggle-all'); + expect(toggleAllIconElm.classList.contains('expanded')).toBeTruthy(); + expect(toggleAllIconElm.classList.contains('collapsed')).toBeFalsy(); + expect(dvExpandSpy).toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/packages/common/src/plugins/autoTooltip.plugin.ts b/packages/common/src/plugins/autoTooltip.plugin.ts index c954ca35f..86104db7c 100644 --- a/packages/common/src/plugins/autoTooltip.plugin.ts +++ b/packages/common/src/plugins/autoTooltip.plugin.ts @@ -18,10 +18,10 @@ declare const Slick: SlickNamespace; * @param {number} [options.maxToolTipLength=null] - The maximum length for a tooltip */ export class AutoTooltipPlugin { - private _eventHandler!: SlickEventHandler; - private _grid!: SlickGrid; - private _addonOptions?: AutoTooltipOption; - private _defaults = { + protected _eventHandler!: SlickEventHandler; + protected _grid!: SlickGrid; + protected _addonOptions?: AutoTooltipOption; + protected _defaults = { enableForCells: true, enableForHeaderCells: false, maxToolTipLength: undefined, @@ -63,14 +63,14 @@ export class AutoTooltipPlugin { } // -- - // private functions + // protected functions // ------------------ /** * Handle mouse entering grid cell to add/remove tooltip. * @param {Object} event - The event */ - private handleMouseEnter(event: Event) { + protected handleMouseEnter(event: Event) { const cell = this._grid.getCellFromEvent(event); if (cell) { let node: HTMLElement | null = this._grid.getCellNode(cell.row, cell.cell); @@ -95,7 +95,7 @@ export class AutoTooltipPlugin { * @param {Object} event - The event * @param {Object} args.column - The column definition */ - private handleHeaderMouseEnter(event: Event, args: { column: Column; }) { + protected handleHeaderMouseEnter(event: Event, args: { column: Column; }) { const column = args.column; let node: HTMLDivElement | null; const targetElm = (event.target as HTMLDivElement); diff --git a/packages/common/src/plugins/draggableGrouping.plugin.ts b/packages/common/src/plugins/draggableGrouping.plugin.ts new file mode 100644 index 000000000..508bf4d37 --- /dev/null +++ b/packages/common/src/plugins/draggableGrouping.plugin.ts @@ -0,0 +1,501 @@ +import { ExtensionUtility } from '../extensions/extensionUtility'; +import { + Column, + DOMMouseEvent, + DraggableGrouping, + DraggableGroupingOption, + GetSlickEventType, + GridOption, + GroupingGetterFunction, + HtmlElementPosition, + SlickDataView, + SlickEvent, + SlickEventHandler, + SlickGrid, + SlickNamespace, +} from '../interfaces/index'; +import { BindingEventService } from '../services/bindingEvent.service'; +import { PubSubService } from '../services/pubSub.service'; +import { SharedService } from '../services/shared.service'; +import { emptyElement, isEmptyObject } from '../services/utilities'; + +// using external SlickGrid JS libraries +declare const Slick: SlickNamespace; + +export interface JQueryUiDraggableOption { + draggable: any; + helper: any; + originalPosition: { left: number; top: number; }; + placeholder?: any; + position?: any; + sender?: any; + offset?: HtmlElementPosition; +} + +export interface JQueryUiSortableOptions { + distance?: number; + cursor?: string; + tolerance?: string; + helper?: string; + placeholder?: string; + forcePlaceholderSize?: boolean; + appendTo?: string; + start?: (e: Event, ui: JQueryUiDraggableOption) => void; + beforeStop?: (e: Event, ui: JQueryUiDraggableOption) => void; + stop?: (e: any) => void; +} + +/** + * + * Draggable Grouping contributed by: Muthukumar Selconstasu + * muthukumar{dot}se{at}gmail{dot}com + * github.com/muthukumarse/Slickgrid + * + * NOTES: + * This plugin provides the Draggable Grouping feature + * + * A plugin to add drop-down menus to column headers. + * To specify a custom button in a column header, extend the column definition like so: + * this.columnDefinitions = [{ + * id: 'cost', name: 'Cost', field: 'cost', + * grouping: { + * getter: 'cost', + * formatter: (g) => `Cost: ${g.value} (${g.count} items)`, + * aggregators: [new Aggregators.Sum('cost')], + * aggregateCollapsed: true, + * collapsed: true + * } + * }]; + */ +export class DraggableGroupingPlugin { + protected _addonOptions!: DraggableGroupingOption; + protected _bindEventService: BindingEventService; + protected _droppableInstance: any; + protected _sortableInstance: any; + protected _eventHandler!: SlickEventHandler; + protected _grid?: SlickGrid; + protected _gridColumns: Column[] = []; + protected _gridUid = ''; + protected dropboxElm!: HTMLDivElement; + protected dropboxPlaceholderElm!: HTMLDivElement; + protected groupToggler!: HTMLDivElement; + protected _defaults = { + dropPlaceHolderText: 'Drop a column header here to group by the column', + hideToggleAllButton: false, + toggleAllButtonText: '', + toggleAllPlaceholderText: 'Toggle all Groups', + } as DraggableGroupingOption; + columnsGroupBy: Column[] = []; + onGroupChanged: SlickEvent; + pluginName: 'DraggableGrouping' = 'DraggableGrouping'; + + /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */ + constructor( + protected readonly extensionUtility: ExtensionUtility, + protected readonly pubSubService: PubSubService, + protected readonly sharedService: SharedService, + ) { + this._bindEventService = new BindingEventService(); + this._eventHandler = new Slick.EventHandler(); + this.onGroupChanged = new Slick.Event(); + } + + get addonOptions(): DraggableGroupingOption { + return this._addonOptions; + } + + /** Getter of SlickGrid DataView object */ + get dataView(): SlickDataView { + return this.grid?.getData?.() ?? {} as SlickDataView; + } + + get dropboxElement() { + return this.dropboxElm; + } + + get droppableInstance() { + return this._droppableInstance; + } + + get sortableInstance() { + return this._sortableInstance; + } + + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + + get grid(): SlickGrid { + return this._grid ?? this.sharedService.slickGrid ?? {} as SlickGrid; + } + + get gridOptions(): GridOption { + return this.sharedService.gridOptions ?? {}; + } + + /** Getter for the grid uid */ + get gridUid(): string { + return this._gridUid || (this.grid?.getUID() ?? ''); + } + get gridUidSelector(): string { + return this.gridUid ? `#${this.gridUid}` : ''; + } + + /** Initialize plugin. */ + init(grid: SlickGrid, groupingOptions?: DraggableGrouping) { + this._addonOptions = { ...this._defaults, ...groupingOptions }; + this._grid = grid; + if (grid) { + this._gridUid = grid.getUID(); + this._gridColumns = grid.getColumns(); + this.dropboxElm = grid.getPreHeaderPanel(); + + // add optional group "Toggle All" with its button & text when provided + if (!this._addonOptions.hideToggleAllButton) { + this.groupToggler = document.createElement('div'); + this.groupToggler.className = 'slick-group-toggle-all'; + this.groupToggler.style.display = 'none'; + const groupTogglerIconElm = document.createElement('span'); + groupTogglerIconElm.className = 'slick-group-toggle-all-icon expanded mdi mdi-close'; + this.groupToggler.appendChild(groupTogglerIconElm); + + if (this.gridOptions.enableTranslate && this._addonOptions.toggleAllButtonTextKey) { + this._addonOptions.toggleAllButtonText = this.extensionUtility.translateWhenEnabledAndServiceExist(this._addonOptions.toggleAllButtonTextKey, 'TEXT_TOGGLE_ALL_GROUPS'); + } + if (this.gridOptions.enableTranslate && this._addonOptions.toggleAllPlaceholderTextKey) { + this._addonOptions.toggleAllPlaceholderText = this.extensionUtility.translateWhenEnabledAndServiceExist(this._addonOptions.toggleAllPlaceholderTextKey, 'TEXT_TOGGLE_ALL_GROUPS'); + } + this.groupToggler.title = this._addonOptions.toggleAllPlaceholderText ?? ''; + + if (this._addonOptions.toggleAllButtonText) { + const groupTogglerTextElm = document.createElement('span'); + groupTogglerTextElm.className = 'slick-group-toggle-all-text'; + groupTogglerTextElm.textContent = this._addonOptions.toggleAllButtonText || ''; + this.groupToggler.appendChild(groupTogglerTextElm); + } + this.dropboxElm.appendChild(this.groupToggler); + } + + this.dropboxPlaceholderElm = document.createElement('div'); + this.dropboxPlaceholderElm.className = 'slick-draggable-dropbox-toggle-placeholder'; + if (this.gridOptions.enableTranslate && this._addonOptions?.dropPlaceHolderTextKey) { + this._addonOptions.dropPlaceHolderText = this.extensionUtility.translateWhenEnabledAndServiceExist(this._addonOptions.dropPlaceHolderTextKey, 'TEXT_TOGGLE_ALL_GROUPS'); + } + this.dropboxPlaceholderElm.textContent = this._addonOptions?.dropPlaceHolderText ?? this._defaults.dropPlaceHolderText ?? ''; + this.dropboxElm.appendChild(this.dropboxPlaceholderElm); + + this.setupColumnDropbox(); + + const onHeaderCellRenderedHandler = grid.onHeaderCellRendered; + (this._eventHandler as SlickEventHandler>).subscribe(onHeaderCellRenderedHandler, (_e, args) => { + const column = args.column; + const node = args.node; + + if (!isEmptyObject(column.grouping)) { + node.style.cursor = 'pointer'; // add the pointer cursor on each column title + + // also optionally add an icon beside each column title that can be dragged + if (this._addonOptions.groupIconCssClass || this._addonOptions.groupIconImage) { + const groupableIconElm = document.createElement('span'); + groupableIconElm.className = 'slick-column-groupable'; + if (this._addonOptions.groupIconCssClass) { + groupableIconElm.classList.add(...this._addonOptions.groupIconCssClass.split(' ')); + } + if (this._addonOptions.groupIconImage) { + groupableIconElm.style.background = `url(${this._addonOptions.groupIconImage}) no-repeat center center`; + } + node.appendChild(groupableIconElm); + } + } + }); + + for (const col of this._gridColumns) { + const columnId = col.field; + grid.updateColumnHeader(columnId); + } + } + return this; + } + + /** @deprecated @use `dispose` Destroy plugin. */ + destroy() { + this.dispose(); + } + + /** Dispose the plugin. */ + dispose() { + this.onGroupChanged.unsubscribe(); + this._eventHandler.unsubscribeAll(); + emptyElement(document.querySelector('.slick-preheader-panel')); + } + + clearDroppedGroups() { + this.columnsGroupBy = []; + this.updateGroupBy('clear-all'); + const allDroppedGroupingElms = this.dropboxElm.querySelectorAll('.slick-dropped-grouping'); + for (const groupElm of Array.from(allDroppedGroupingElms)) { + const groupRemoveBtnElm = document.querySelector('.slick-groupby-remove'); + groupRemoveBtnElm?.remove(); + groupElm?.remove(); + } + + // show placeholder text & hide the "Toggle All" when that later feature is enabled + this.dropboxPlaceholderElm.style.display = 'inline-block'; + if (this.groupToggler) { + this.groupToggler.style.display = 'none'; + } + } + + setColumns(cols: Column[]) { + this._gridColumns = cols; + } + + setDroppedGroups(groupingInfo: Array | string) { + const groupingInfos = Array.isArray(groupingInfo) ? groupingInfo : [groupingInfo]; + this.dropboxPlaceholderElm.style.display = 'none'; + for (const groupInfo of groupingInfos) { + const column = $(this.grid.getHeaderColumn(groupInfo as string)); + this.handleGroupByDrop(this.dropboxElm, column); + } + } + + setupColumnReorder(grid: SlickGrid, $headers: any, headerColumnWidthDiff: any, setColumns: (columns: Column[]) => void, setupColumnResize: () => void, columns: Column[], getColumnIndex: (column: Column) => number, uid: string, trigger: (slickEvent: SlickEvent, data?: any) => void) { + $headers.filter(':ui-sortable').sortable('destroy'); + const headerDraggableGroupByElm = grid.getPreHeaderPanel(); + + return $headers.sortable({ + distance: 3, + cursor: 'default', + tolerance: 'intersection', + helper: 'clone', + placeholder: 'slick-sortable-placeholder ui-state-default slick-header-column', + forcePlaceholderSize: true, + appendTo: 'body', + start: (_e: Event, ui: JQueryUiDraggableOption) => { + $(ui.helper).addClass('slick-header-column-active'); + const placeholderElm = headerDraggableGroupByElm.querySelector('.slick-draggable-dropbox-toggle-placeholder'); + if (placeholderElm) { + placeholderElm.style.display = 'inline-block'; + } + const droppedGroupingElm = headerDraggableGroupByElm.querySelector('.slick-dropped-grouping'); + if (droppedGroupingElm) { + droppedGroupingElm.style.display = 'none'; + } + }, + beforeStop: (_e: Event, ui: JQueryUiDraggableOption) => { + $(ui.helper).removeClass('slick-header-column-active'); + const hasDroppedColumn = headerDraggableGroupByElm.querySelectorAll('.slick-dropped-grouping').length; + if (hasDroppedColumn > 0) { + const placeholderElm = headerDraggableGroupByElm.querySelector('.slick-draggable-dropbox-toggle-placeholder'); + if (placeholderElm) { + placeholderElm.style.display = 'none'; + } + const droppedGroupingElm = headerDraggableGroupByElm.querySelector('.slick-dropped-grouping'); + if (droppedGroupingElm) { + droppedGroupingElm.style.display = 'inline-block'; + } + } + }, + stop: (e: DOMMouseEvent) => { + if (!grid.getEditorLock().commitCurrentEdit()) { + ($(e.target) as any).sortable('cancel'); + return; + } + const reorderedIds = $headers.sortable('toArray'); + // If frozen columns are used, headers has more than one entry and we need the ids from all of them. + // though there is only really a left and right header, this will work even if that should change. + if ($headers.length > 1) { + for (let headerIdx = 1, ln = $headers.length; headerIdx < ln; headerIdx += 1) { + const $header = $($headers[headerIdx]); + const ids = ($header as any).sortable('toArray'); + // Note: the loop below could be simplified with: + // reorderedIds.push.apply(reorderedIds,ids); + // However, the loop is more in keeping with way-backward compatibility + for (const id of ids) { + reorderedIds.push(id); + } + } + } + const reorderedColumns = []; + for (const reorderedId of reorderedIds) { + reorderedColumns.push(columns[getColumnIndex(reorderedId.replace(uid, ''))]); + } + setColumns(reorderedColumns); + trigger(grid.onColumnsReordered, { grid: this.grid }); + e.stopPropagation(); + setupColumnResize(); + } + } as JQueryUiSortableOptions); + } + + // + // protected functions + // ------------------ + + protected addColumnGroupBy(column: Column) { + this.columnsGroupBy.push(column); + this.updateGroupBy('add-group'); + } + + protected addGroupByRemoveClickHandler(id: string | number, _container: any, column: Column, entry: any) { + const text = entry; + const groupRemoveElm = document.querySelector(`${this.gridUidSelector}_${id}_entry > .slick-groupby-remove`); + if (groupRemoveElm) { + this._bindEventService.bind(groupRemoveElm, 'click', () => { + const boundedElms = this._bindEventService.boundedEvents.filter(boundedEvent => boundedEvent.element === groupRemoveElm); + for (const boundedEvent of boundedElms) { + this._bindEventService.unbind(boundedEvent.element, 'click', boundedEvent.listener); + } + this.removeGroupBy(id, column, text); + }); + } + } + + protected handleGroupByDrop(container: any, column: any) { + const columnid = column.attr('id').replace(this._gridUid, ''); + let columnAllowed = true; + this.columnsGroupBy.forEach(col => { + if (col.id === columnid) { + columnAllowed = false; + } + }); + + if (columnAllowed) { + this._gridColumns.forEach(col => { + if (col.id === columnid) { + if (col.grouping !== null && !isEmptyObject(col.grouping)) { + const entryElm = document.createElement('div'); + entryElm.id = `${this._gridUid}_${col.id}_entry`; + entryElm.dataset.id = `${col.id}`; + entryElm.className = 'slick-dropped-grouping'; + const columnName = column.children('.slick-column-name').first(); + const groupTextElm = document.createElement('div'); + groupTextElm.style.display = 'inline-flex'; + groupTextElm.textContent = columnName.length ? columnName.text() : column.text(); + entryElm.appendChild(groupTextElm); + const groupRemoveIconElm = document.createElement('div'); + groupRemoveIconElm.className = 'slick-groupby-remove'; + if (this._addonOptions.deleteIconCssClass) { + groupRemoveIconElm.classList.add(...this._addonOptions.deleteIconCssClass.split(' ')); + } + if (this._addonOptions.deleteIconImage) { + groupRemoveIconElm.style.background = `url(${this._addonOptions.deleteIconImage}) no-repeat center right`; + } + if (!this._addonOptions.deleteIconCssClass && !this._addonOptions.deleteIconImage) { + groupRemoveIconElm.classList.add('slick-groupby-remove-image'); + } + entryElm.appendChild(groupRemoveIconElm); + entryElm.appendChild(document.createElement('div')); + container.appendChild(entryElm); + this.addColumnGroupBy(col); + this.addGroupByRemoveClickHandler(col.id, container, column, entryElm); + } + } + }); + + // show the "Toggle All" when feature is enabled + if (this.groupToggler) { + this.groupToggler.style.display = 'inline-block'; + } + } + } + + protected removeFromArray(arrayToModify: any[], itemToRemove: any) { + if (Array.isArray(arrayToModify)) { + const itemIdx = arrayToModify.findIndex(a => a.id === itemToRemove.id); + if (itemIdx >= 0) { + arrayToModify.splice(itemIdx, 1); + } + } + return arrayToModify; + } + + protected removeGroupBy(id: string | number, column: Column, entry: any) { + entry.remove(); + const groupByColumns: Column[] = []; + this._gridColumns.forEach((col: any) => groupByColumns[col.id] = col); + this.removeFromArray(this.columnsGroupBy, groupByColumns[id as any]); + if (this.columnsGroupBy.length === 0) { + // show placeholder text & hide the "Toggle All" when that later feature is enabled + this.dropboxPlaceholderElm.style.display = 'inline-block'; + if (this.groupToggler) { + this.groupToggler.style.display = 'none'; + } + } + this.updateGroupBy('remove-group'); + } + + protected setupColumnDropbox() { + this._droppableInstance = ($(this.dropboxElm) as any).droppable({ + activeClass: 'ui-state-default', + hoverClass: 'ui-state-hover', + accept: ':not(.ui-sortable-helper)', + deactivate: () => { + this.dropboxElm.classList.remove('slick-header-column-denied'); + }, + drop: (e: DOMMouseEvent, ui: JQueryUiDraggableOption) => { + this.handleGroupByDrop(e.target, ui.draggable); + }, + over: (_e: Event, ui: JQueryUiDraggableOption) => { + const id = (ui.draggable).attr('id').replace(this._gridUid, ''); + for (const col of this._gridColumns) { + if (col.id === id && !col.grouping) { + this.dropboxElm.classList.add('slick-header-column-denied'); + } + } + } + }); + + this._sortableInstance = ($(this.dropboxElm) as any).sortable({ + items: 'div.slick-dropped-grouping', + cursor: 'default', + tolerance: 'pointer', + helper: 'clone', + update: (event: DOMMouseEvent) => { + const sortArray: string[] = ($(event.target) as any).sortable('toArray', { attribute: 'data-id' }); + const newGroupingOrder = []; + for (const sortGroupId of sortArray) { + for (const groupByColumn of this.columnsGroupBy) { + if (groupByColumn.id === sortGroupId) { + newGroupingOrder.push(groupByColumn); + break; + } + } + } + this.columnsGroupBy = newGroupingOrder; + this.updateGroupBy('sort-group'); + } + }); + + if (this.groupToggler) { + this._bindEventService.bind(this.groupToggler, 'click', ((event: DOMMouseEvent) => { + const target = event.target.classList.contains('slick-group-toggle-all-icon') ? event.target : event.currentTarget.querySelector('.slick-group-toggle-all-icon'); + if (target) { + if (target.classList.contains('collapsed')) { + target.classList.remove('collapsed'); + target.classList.add('expanded'); + this.dataView.expandAllGroups(); + } else { + target.classList.add('collapsed'); + target.classList.remove('expanded'); + this.dataView.collapseAllGroups(); + } + } + }) as EventListener); + } + } + + protected updateGroupBy(originator: string) { + if (this.columnsGroupBy.length === 0) { + this.dataView.setGrouping([]); + this.dropboxPlaceholderElm.style.display = 'inline-block'; + this.onGroupChanged.notify({ caller: originator, groupColumns: [] }); + return; + } + const groupingArray: any[] = []; + this.columnsGroupBy.forEach(element => groupingArray.push(element.grouping)); + this.dataView.setGrouping(groupingArray); + this.dropboxPlaceholderElm.style.display = 'none'; + this.onGroupChanged.notify({ caller: originator, groupColumns: groupingArray }); + } +} \ No newline at end of file diff --git a/packages/common/src/plugins/index.ts b/packages/common/src/plugins/index.ts index 82ce71a88..c1371eef1 100644 --- a/packages/common/src/plugins/index.ts +++ b/packages/common/src/plugins/index.ts @@ -1,5 +1,6 @@ export * from './autoTooltip.plugin'; export * from './cellMenu.plugin'; export * from './contextMenu.plugin'; +export * from './draggableGrouping.plugin'; export * from './headerButton.plugin'; export * from './headerMenu.plugin'; \ No newline at end of file diff --git a/packages/common/src/services/__tests__/bindingEvent.service.spec.ts b/packages/common/src/services/__tests__/bindingEvent.service.spec.ts index d33147a46..680c84670 100644 --- a/packages/common/src/services/__tests__/bindingEvent.service.spec.ts +++ b/packages/common/src/services/__tests__/bindingEvent.service.spec.ts @@ -25,6 +25,7 @@ describe('BindingEvent Service', () => { service.bind(mockElm, 'click', mockCallback); + expect(service.boundedEvents.length).toBe(1); expect(addEventSpy).toHaveBeenCalledWith('click', mockCallback, undefined); }); @@ -37,6 +38,7 @@ describe('BindingEvent Service', () => { service.bind(mockElm, 'click', mockCallback, { capture: true, passive: true }); + expect(service.boundedEvents.length).toBe(1); expect(addEventSpy).toHaveBeenCalledWith('click', mockCallback, { capture: true, passive: true }); }); @@ -50,8 +52,11 @@ describe('BindingEvent Service', () => { service = new BindingEventService(); service.bind(mockElm, 'keyup', mockCallback1); service.bind(mockElm, 'click', mockCallback2, { capture: true, passive: true }); + expect(service.boundedEvents.length).toBe(2); + service.unbindAll(); + expect(service.boundedEvents.length).toBe(0); expect(addEventSpy).toHaveBeenCalledWith('keyup', mockCallback1, undefined); expect(addEventSpy).toHaveBeenCalledWith('click', mockCallback2, { capture: true, passive: true }); expect(removeEventSpy).toHaveBeenCalledWith('keyup', mockCallback1); diff --git a/packages/common/src/services/__tests__/extension.service.spec.ts b/packages/common/src/services/__tests__/extension.service.spec.ts index 76d5c7df2..cd2aaa322 100644 --- a/packages/common/src/services/__tests__/extension.service.spec.ts +++ b/packages/common/src/services/__tests__/extension.service.spec.ts @@ -5,7 +5,6 @@ import { Column, ExtensionModel, GridOption, SlickGrid, SlickNamespace } from '. import { CellExternalCopyManagerExtension, CheckboxSelectorExtension, - DraggableGroupingExtension, ExtensionUtility, GroupItemMetaProviderExtension, RowDetailViewExtension, @@ -14,7 +13,7 @@ import { } from '../../extensions'; import { BackendUtilityService, ExtensionService, FilterService, PubSubService, SharedService, SortService, TreeDataService } from '../index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; -import { AutoTooltipPlugin, CellMenuPlugin, ContextMenuPlugin, HeaderButtonPlugin, HeaderMenuPlugin } from '../../plugins/index'; +import { AutoTooltipPlugin, CellMenuPlugin, ContextMenuPlugin, DraggableGroupingPlugin, HeaderButtonPlugin, HeaderMenuPlugin } from '../../plugins/index'; import { ColumnPickerControl, GridMenuControl } from '../../controls/index'; jest.mock('flatpickr', () => { }); @@ -35,11 +34,13 @@ const gridStub = { getColumnIndex: jest.fn(), getOptions: jest.fn(), getPluginByName: jest.fn(), + getPreHeaderPanel: jest.fn(), getUID: () => GRID_UID, getColumns: jest.fn(), setColumns: jest.fn(), onColumnsResized: jest.fn(), registerPlugin: jest.fn(), + updateColumnHeader: jest.fn(), onBeforeDestroy: new Slick.Event(), onBeforeHeaderCellDestroy: new Slick.Event(), onBeforeSetColumns: new Slick.Event(), @@ -145,7 +146,6 @@ describe('ExtensionService', () => { // extensions extensionStub as unknown as CellExternalCopyManagerExtension, extensionCheckboxSelectorStub as unknown as CheckboxSelectorExtension, - extensionStub as unknown as DraggableGroupingExtension, extensionGroupItemMetaStub as unknown as GroupItemMetaProviderExtension, extensionStub as unknown as RowDetailViewExtension, extensionRowMoveStub as unknown as RowMoveManagerExtension, @@ -309,19 +309,26 @@ describe('ExtensionService', () => { }); it('should register the DraggableGrouping addon when "enableDraggableGrouping" is set in the grid options', () => { - const gridOptionsMock = { enableDraggableGrouping: true } as GridOption; - const ext1Spy = jest.spyOn(extensionStub, 'register').mockReturnValue({ ...instanceMock }); - const ext2Spy = jest.spyOn(extensionGroupItemMetaStub, 'register').mockReturnValue({ ...instanceMock }); + const onRegisteredMock = jest.fn(); + const columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }] as Column[]; + jest.spyOn(gridStub, 'getPreHeaderPanel').mockReturnValue(document.createElement('div')); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock); + const gridOptionsMock = { enableDraggableGrouping: true, draggableGrouping: { onExtensionRegistered: onRegisteredMock } } as GridOption; const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + service.createExtensionsBeforeGridCreation(columnsMock, gridOptionsMock); service.bindDifferentExtensions(); - const output1 = service.getExtensionByName(ExtensionName.draggableGrouping); + + const output = service.getExtensionByName(ExtensionName.draggableGrouping); + const pluginInstance = service.getSlickgridAddonInstance(ExtensionName.draggableGrouping); const output2 = service.getExtensionByName(ExtensionName.groupItemMetaProvider); + expect(onRegisteredMock).toHaveBeenCalledWith(expect.toBeObject()); + expect(output.instance instanceof DraggableGroupingPlugin).toBeTrue(); expect(gridSpy).toHaveBeenCalled(); - expect(ext1Spy).toHaveBeenCalled(); - expect(ext2Spy).toHaveBeenCalled(); - expect(output1).toEqual({ name: ExtensionName.draggableGrouping, instance: instanceMock as unknown, class: extensionStub } as ExtensionModel); + expect(pluginInstance).toBeTruthy(); + expect(output!.instance).toEqual(pluginInstance); + expect(output).toEqual({ name: ExtensionName.draggableGrouping, instance: pluginInstance, class: pluginInstance } as ExtensionModel); expect(output2).toEqual({ name: ExtensionName.groupItemMetaProvider, instance: instanceMock as unknown, class: extensionStub } as ExtensionModel); }); @@ -537,11 +544,11 @@ describe('ExtensionService', () => { it('should call draggableGroupingExtension create when "enableDraggableGrouping" is set in the grid options provided', () => { const columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }] as Column[]; const gridOptionsMock = { enableDraggableGrouping: true } as GridOption; - const extSpy = jest.spyOn(extensionStub, 'create').mockReturnValue(instanceMock); service.createExtensionsBeforeGridCreation(columnsMock, gridOptionsMock); + const instance = service.getCreatedExtensionByName(ExtensionName.draggableGrouping); - expect(extSpy).toHaveBeenCalledWith(gridOptionsMock); + 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', () => { @@ -842,7 +849,6 @@ describe('ExtensionService', () => { // extensions extensionStub as unknown as CellExternalCopyManagerExtension, extensionStub as unknown as CheckboxSelectorExtension, - extensionStub as unknown as DraggableGroupingExtension, extensionGroupItemMetaStub as unknown as GroupItemMetaProviderExtension, extensionStub as unknown as RowDetailViewExtension, extensionStub as unknown as RowMoveManagerExtension, diff --git a/packages/common/src/services/__tests__/utilities.spec.ts b/packages/common/src/services/__tests__/utilities.spec.ts index a6fa9371e..aa8b894ab 100644 --- a/packages/common/src/services/__tests__/utilities.spec.ts +++ b/packages/common/src/services/__tests__/utilities.spec.ts @@ -24,6 +24,7 @@ import { getTranslationPrefix, htmlEncode, htmlEntityDecode, + isEmptyObject, isNumber, mapMomentDateFormatWithFieldType, mapFlatpickrDateFormatWithFieldType, @@ -129,6 +130,23 @@ describe('Service/Utilies', () => { }); }); + describe('isEmptyObject method', () => { + it('should return True when comparing against an object that has properties', () => { + const result = isEmptyObject({ firstName: 'John', lastName: 'Doe' }); + expect(result).toBeFalse(); + }); + + it('should return False when comparing against an object is either empty, null or undefined', () => { + const result1 = isEmptyObject({}); + const result2 = isEmptyObject(null); + const result3 = isEmptyObject(undefined); + + expect(result1).toBeTrue(); + expect(result2).toBeTrue(); + expect(result3).toBeTrue(); + }); + }); + describe('isNumber method', () => { it('should return True when comparing a number from a number/string variable when strict mode is disable', () => { const result1 = isNumber(22); diff --git a/packages/common/src/services/bindingEvent.service.ts b/packages/common/src/services/bindingEvent.service.ts index 241bba7a8..35c2d0d48 100644 --- a/packages/common/src/services/bindingEvent.service.ts +++ b/packages/common/src/services/bindingEvent.service.ts @@ -3,6 +3,10 @@ import { ElementEventListener } from '../interfaces/elementEventListener.interfa export class BindingEventService { protected _boundedEvents: ElementEventListener[] = []; + get boundedEvents(): ElementEventListener[] { + return this._boundedEvents; + } + /** Bind an event listener to any element */ bind(element: Element, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) { const eventNames = (Array.isArray(eventNameOrNames)) ? eventNameOrNames : [eventNameOrNames]; @@ -12,14 +16,22 @@ export class BindingEventService { } } + /** Unbind all will remove every every event handlers that were bounded earlier */ + unbind(element: Element, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject) { + const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames : [eventNameOrNames]; + for (const eventName of eventNames) { + if (element?.removeEventListener) { + element.removeEventListener(eventName, listener); + } + } + } + /** Unbind all will remove every every event handlers that were bounded earlier */ unbindAll() { while (this._boundedEvents.length > 0) { const boundedEvent = this._boundedEvents.pop() as ElementEventListener; const { element, eventName, listener } = boundedEvent; - if (element?.removeEventListener) { - element.removeEventListener(eventName, listener); - } + this.unbind(element, eventName, listener); } } } diff --git a/packages/common/src/services/extension.service.ts b/packages/common/src/services/extension.service.ts index 0d2652bf6..e57717248 100644 --- a/packages/common/src/services/extension.service.ts +++ b/packages/common/src/services/extension.service.ts @@ -4,11 +4,10 @@ import 'slickgrid/plugins/slick.cellrangeselector'; import 'slickgrid/plugins/slick.cellselectionmodel'; import { Column, Extension, ExtensionModel, GridOption, SlickRowSelectionModel, } from '../interfaces/index'; -import { ExtensionList, ExtensionName, SlickControlList, SlickPluginList } from '../enums/index'; +import { ColumnReorderFunction, ExtensionList, ExtensionName, SlickControlList, SlickPluginList } from '../enums/index'; import { CellExternalCopyManagerExtension, CheckboxSelectorExtension, - DraggableGroupingExtension, ExtensionUtility, GroupItemMetaProviderExtension, RowDetailViewExtension, @@ -17,7 +16,7 @@ import { } from '../extensions/index'; import { SharedService } from './shared.service'; import { TranslaterService } from './translater.service'; -import { AutoTooltipPlugin, CellMenuPlugin, ContextMenuPlugin, HeaderButtonPlugin, HeaderMenuPlugin } from '../plugins/index'; +import { AutoTooltipPlugin, CellMenuPlugin, ContextMenuPlugin, DraggableGroupingPlugin, HeaderButtonPlugin, HeaderMenuPlugin } from '../plugins/index'; import { ColumnPickerControl, GridMenuControl } from '../controls/index'; import { FilterService } from './filter.service'; import { PubSubService } from './pubSub.service'; @@ -34,6 +33,7 @@ export class ExtensionService { protected _cellMenuPlugin?: CellMenuPlugin; protected _contextMenuPlugin?: ContextMenuPlugin; protected _columnPickerControl?: ColumnPickerControl; + protected _draggleGroupingPlugin?: DraggableGroupingPlugin; protected _gridMenuControl?: GridMenuControl; protected _headerMenuPlugin?: HeaderMenuPlugin; protected _extensionCreatedList: ExtensionList = {} as ExtensionList; @@ -56,7 +56,6 @@ export class ExtensionService { protected readonly cellExternalCopyExtension: CellExternalCopyManagerExtension, protected readonly checkboxSelectorExtension: CheckboxSelectorExtension, - protected readonly draggableGroupingExtension: DraggableGroupingExtension, protected readonly groupItemMetaExtension: GroupItemMetaProviderExtension, protected readonly rowDetailViewExtension: RowDetailViewExtension, protected readonly rowMoveManagerExtension: RowMoveManagerExtension, @@ -96,6 +95,17 @@ export class ExtensionService { return this.sharedService.visibleColumns || []; } + /** + * Get an Extension that was created by calling its "create" method (there are only 3 extensions which uses this method) + * @param name + */ + getCreatedExtensionByName

(name: ExtensionName): ExtensionModel | undefined { + if (this._extensionCreatedList && this._extensionCreatedList.hasOwnProperty(name)) { + return this._extensionCreatedList[name]; + } + return undefined; + } + /** * Get an Extension by it's name * @param name @@ -207,11 +217,15 @@ export class ExtensionService { } // Draggable Grouping Plugin - if (this.gridOptions.enableDraggableGrouping && this.draggableGroupingExtension && this.draggableGroupingExtension.register) { - const instance = this.draggableGroupingExtension.register(); - if (instance) { - this._extensionList[ExtensionName.draggableGrouping] = { name: ExtensionName.draggableGrouping, class: this.draggableGroupingExtension, instance }; + if (this.gridOptions.enableDraggableGrouping) { + if (this._draggleGroupingPlugin) { + this._draggleGroupingPlugin.init(this.sharedService.slickGrid, this.gridOptions.draggableGrouping); + if (this.gridOptions.draggableGrouping?.onExtensionRegistered) { + this.gridOptions.draggableGrouping.onExtensionRegistered(this._draggleGroupingPlugin); + } + this._extensionList[ExtensionName.contextMenu] = { name: ExtensionName.contextMenu, class: this._draggleGroupingPlugin, instance: this._draggleGroupingPlugin }; } + this._extensionList[ExtensionName.draggableGrouping] = { name: ExtensionName.draggableGrouping, class: this._draggleGroupingPlugin, instance: this._draggleGroupingPlugin }; } // Grid Menu Control @@ -316,10 +330,10 @@ export class ExtensionService { if (gridOptions.enableDraggableGrouping) { if (!this.getCreatedExtensionByName(ExtensionName.draggableGrouping)) { - const draggableInstance = this.draggableGroupingExtension.create(gridOptions); - if (draggableInstance) { - gridOptions.enableColumnReorder = draggableInstance.getSetupColumnReorder; - this._extensionCreatedList[ExtensionName.draggableGrouping] = { name: ExtensionName.draggableGrouping, instance: draggableInstance, class: this.draggableGroupingExtension }; + this._draggleGroupingPlugin = new DraggableGroupingPlugin(this.extensionUtility, this.pubSubService, this.sharedService); + if (this._draggleGroupingPlugin) { + gridOptions.enableColumnReorder = this._draggleGroupingPlugin.setupColumnReorder as ColumnReorderFunction; + this._extensionCreatedList[ExtensionName.draggableGrouping] = { name: ExtensionName.draggableGrouping, instance: this._draggleGroupingPlugin, class: this._draggleGroupingPlugin }; } } } @@ -472,17 +486,6 @@ export class ExtensionService { }); } - /** - * Get an Extension that was created by calling its "create" method (there are only 3 extensions which uses this method) - * @param name - */ - protected getCreatedExtensionByName

(name: ExtensionName): ExtensionModel | undefined { - if (this._extensionCreatedList && this._extensionCreatedList.hasOwnProperty(name)) { - return this._extensionCreatedList[name]; - } - return undefined; - } - /** Translate an array of items from an input key and assign translated value to the output key */ protected translateItems(items: any[], inputKey: string, outputKey: string) { if (this.gridOptions?.enableTranslate && !(this.translaterService?.translate)) { diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts index b7b5a6321..d46288d40 100644 --- a/packages/common/src/services/utilities.ts +++ b/packages/common/src/services/utilities.ts @@ -254,6 +254,18 @@ export function emptyObject(obj: any) { return obj; } +/** + * Check if an object is empty + * @param obj - input object + * @returns - boolean + */ +export function isEmptyObject(obj: any): boolean { + if (obj === null || obj === undefined) { + return true; + } + return Object.entries(obj).length === 0; +} + /** * @deprecated use `findItemInTreeStructure()` instead. Find an item from a hierarchical (tree) view structure (a parent that can have children array which themseleves can children and so on) * @param treeArray diff --git a/packages/common/src/styles/_variables-theme-material.scss b/packages/common/src/styles/_variables-theme-material.scss index 6bb6fa03e..56ca9826f 100644 --- a/packages/common/src/styles/_variables-theme-material.scss +++ b/packages/common/src/styles/_variables-theme-material.scss @@ -79,6 +79,7 @@ $draggable-group-drop-border-right: 0px !default; $draggable-group-drop-width: 100% !default; $draggable-group-drop-radius: 0 !default; $draggable-group-delete-vertical-align: middle !default; +$draggable-group-toggle-all-icon-vertical-align: text-bottom !default; $draggable-group-toggle-collapsed-icon: $icon-group-collapsed !default; $draggable-group-toggle-expanded-icon: $icon-group-expanded !default; $draggable-group-title-height: 24px !default; diff --git a/packages/common/src/styles/_variables-theme-salesforce.scss b/packages/common/src/styles/_variables-theme-salesforce.scss index 071d038d0..5f467b375 100644 --- a/packages/common/src/styles/_variables-theme-salesforce.scss +++ b/packages/common/src/styles/_variables-theme-salesforce.scss @@ -102,6 +102,7 @@ $draggable-group-drop-border-top: 0px !default; $draggable-group-drop-width: 100% !default; $draggable-group-drop-radius: 0px !default; $draggable-group-delete-vertical-align: middle !default; +$draggable-group-toggle-all-icon-vertical-align: text-bottom !default; $draggable-group-toggle-collapsed-icon: $icon-group-collapsed !default; $draggable-group-toggle-expanded-icon: $icon-group-expanded !default; $draggable-group-title-height: 24px !default; diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index 9b185678a..6aec57cfd 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -718,10 +718,18 @@ $draggable-group-delete-padding-left: 5px !default; $draggable-group-delete-padding-right: 7px !default; $draggable-group-delete-font-size: 16px !default; $draggable-group-delete-vertical-align: baseline !default; +$draggable-group-toggle-all-border: 1px solid #c7c7c7 !default; +$draggable-group-toggle-all-border-radius: 3px !default; $draggable-group-toggle-all-color: $icon-group-color !default; $draggable-group-toggle-all-display: none !default; -$draggable-group-toggle-all-pos-top: 7px !default; -$draggable-group-toggle-all-pos-right: 40px !default; +$draggable-group-toggle-all-margin-right: 8px !default; +$draggable-group-toggle-all-padding: 0 2px !default; +$draggable-group-toggle-all-position: relative !default; +$draggable-group-toggle-all-top: 0px !default; +$draggable-group-toggle-all-right: unset !default; +$draggable-group-toggle-all-icon-vertical-align: middle !default; +$draggable-group-toggle-all-text-font-size: 15px !default; +$draggable-group-toggle-all-text-margin: 0 0 0 2px !default; $draggable-group-toggle-collapsed-icon: "\f0fe" !default; $draggable-group-toggle-expanded-icon: "\f146" !default; $draggable-group-title-height: $icon-group-height !default; @@ -794,17 +802,14 @@ $multiselect-icon-vertical-align: middle !default; $multiselect-icon-width: 20px !default; $multiselect-icon-unchecked: "\f096" !default; $multiselect-icon-unchecked-color: $multiselect-icon-color !default; -$multiselect-icon-radio-color: $multiselect-icon-color !default; -$multiselect-icon-radio-height: $multiselect-icon-height !default; -$multiselect-icon-radio-width: $multiselect-icon-width !default; -$multiselect-icon-radio-border: $multiselect-icon-border !default; -$multiselect-icon-radio-border-radius: $multiselect-icon-border-radius !default; -$multiselect-icon-radio-margin: $multiselect-icon-margin !default; -$multiselect-icon-radio-height: $icon-font-size !default; $multiselect-icon-radio-border: none !default; $multiselect-icon-radio-border-radius: none !default; $multiselect-icon-radio-checked: "\f192" !default; +$multiselect-icon-radio-color: $multiselect-icon-color !default; +$multiselect-icon-radio-height: $icon-font-size !default; +$multiselect-icon-radio-margin: $multiselect-icon-margin !default; $multiselect-icon-radio-unchecked: "\f10c" !default; +$multiselect-icon-radio-width: $multiselect-icon-width !default; $multiselect-icon-search-margin-right: 8px !default; $multiselect-ok-button-font-weight: 600 !default; $multiselect-label-margin-bottom: 6px !default; diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss index 345af8168..efbc4c66e 100644 --- a/packages/common/src/styles/slick-plugins.scss +++ b/packages/common/src/styles/slick-plugins.scss @@ -820,6 +820,8 @@ input.flatpickr.form-control { .slick-preheader-panel { .ui-droppable, .ui-droppable-hover { + display: flex; + align-items: center; padding: var(--slick-draggable-group-drop-padding, $draggable-group-drop-padding); height: var(--slick-draggable-group-drop-height, $draggable-group-drop-height); border-top: var(--slick-draggable-group-drop-border-top, $draggable-group-drop-border-top) !important; @@ -830,26 +832,38 @@ input.flatpickr.form-control { border-radius: var(--slick-draggable-group-drop-radius, $draggable-group-drop-radius); background-color: var(--slick-draggable-group-drop-bgcolor, $draggable-group-drop-bgcolor); - .slick-placeholder { + .slick-draggable-dropbox-toggle-placeholder { font-style: var(--slick-draggable-group-placeholder-font-style, $draggable-group-placeholder-font-style); color: var(--slick-draggable-group-placeholder-color, $draggable-group-placeholder-color); } .slick-group-toggle-all { - position: absolute; cursor: pointer; - font-family: var(--slick-icon-font-family, $icon-font-family); - color: var(--slick-draggable-group-toggle-all-color, $draggable-group-toggle-all-color); - display: var(--slick-draggable-group-toggle-all-display, $draggable-group-toggle-all-display) !important; - top: var(--slick-draggable-group-toggle-all-pos-top, $draggable-group-toggle-all-pos-top); - right: var(--slick-draggable-group-toggle-all-pos-right, $draggable-group-toggle-all-pos-right); - - &.expanded:before { + border: var(--draggable-group-toggle-all-border, $draggable-group-toggle-all-border); + border-radius: var(--draggable-group-toggle-all-border-radius, $draggable-group-toggle-all-border-radius); + display: var(--slick-draggable-group-toggle-all-display, $draggable-group-toggle-all-display); + margin-right: var(--slick-draggable-group-toggle-all-margin-right, $draggable-group-toggle-all-margin-right); + padding: var(--slick-draggable-group-toggle-all-padding, $draggable-group-toggle-all-padding); + position: var(--draggable-group-toggle-all-position, $draggable-group-toggle-all-position); + top: var(--slick-draggable-group-toggle-all-top, $draggable-group-toggle-all-top); + right: var(--slick-draggable-group-toggle-all-right, $draggable-group-toggle-all-right); + + .slick-group-toggle-all-icon.expanded:before { content: var(--slick-draggable-group-toggle-expanded-icon, $draggable-group-toggle-expanded-icon); } - &.collapsed:before { + .slick-group-toggle-all-icon.collapsed:before { content: var(--slick-draggable-group-toggle-collapsed-icon, $draggable-group-toggle-collapsed-icon); } + .slick-group-toggle-all-icon.collapsed:before, + .slick-group-toggle-all-icon.expanded:before { + color: var(--slick-draggable-group-toggle-all-color, $draggable-group-toggle-all-color); + font-family: var(--slick-icon-font-family, $icon-font-family); + vertical-align: var(--slick-draggable-group-toggle-all-icon-vertical-align, $draggable-group-toggle-all-icon-vertical-align); + } + .slick-group-toggle-all-text { + font-size: var(--slick-draggable-group-toggle-all-text-font-size, $draggable-group-toggle-all-text-font-size); + margin: var(--slick-draggable-group-toggle-all-text-margin, $draggable-group-toggle-all-text-margin); + } } .slick-dropped-grouping { 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 fe73c978f..005e32401 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -34,7 +34,6 @@ import { // extensions CheckboxSelectorExtension, CellExternalCopyManagerExtension, - DraggableGroupingExtension, ExtensionUtility, GroupItemMetaProviderExtension, RowDetailViewExtension, @@ -355,7 +354,6 @@ export class SlickVanillaGridBundle { // extensions const cellExternalCopyManagerExtension = new CellExternalCopyManagerExtension(this.extensionUtility, this.sharedService); const checkboxExtension = new CheckboxSelectorExtension(this.sharedService); - const draggableGroupingExtension = new DraggableGroupingExtension(this.extensionUtility, this._eventPubSubService, this.sharedService); const groupItemMetaProviderExtension = new GroupItemMetaProviderExtension(this.sharedService); const rowDetailViewExtension = new RowDetailViewExtension(); const rowMoveManagerExtension = new RowMoveManagerExtension(this.sharedService); @@ -369,7 +367,6 @@ export class SlickVanillaGridBundle { this.treeDataService, cellExternalCopyManagerExtension, checkboxExtension, - draggableGroupingExtension, groupItemMetaProviderExtension, rowDetailViewExtension, rowMoveManagerExtension, diff --git a/test/cypress/integration/example03.spec.js b/test/cypress/integration/example03.spec.js index a32f87a50..7f616d66c 100644 --- a/test/cypress/integration/example03.spec.js +++ b/test/cypress/integration/example03.spec.js @@ -84,7 +84,7 @@ describe('Example 03 - Draggable Grouping', { retries: 1 }, () => { it('should be able to drag and swap grouped column titles inside the pre-header', () => { cy.get('.slick-dropped-grouping:nth(0) div') .contains('Duration') - .trigger('mousedown', 'bottom', { which: 1 }); + .trigger('mousedown', 'center', { which: 1 }); cy.get('.slick-dropped-grouping:nth(1) div') .contains('Effort-Driven') diff --git a/test/translateServiceStub.ts b/test/translateServiceStub.ts index fce37e3b1..c5d01d862 100644 --- a/test/translateServiceStub.ts +++ b/test/translateServiceStub.ts @@ -87,6 +87,7 @@ export class TranslateServiceStub implements TranslaterService { case 'SALES': output = this._locale === 'en' ? 'Sales' : 'Ventes'; break; case 'SALES_REP': output = this._locale === 'en' ? 'Sales Rep.' : 'Représentant des ventes'; break; case 'SELECT_ALL': output = this._locale === 'en' ? 'Select All' : 'Sélectionner tout'; break; + case 'TOGGLE_ALL_GROUPS': output = this._locale === 'en' ? 'Toggle all Groups' : 'Basculer tous les groupes'; break; case 'FINANCE_MANAGER': output = this._locale === 'en' ? 'Finance Manager' : 'Responsable des finances'; break; case 'HUMAN_RESOURCES': output = this._locale === 'en' ? 'Human Resources' : 'Ressources humaines'; break; case 'IT_ADMIN': output = this._locale === 'en' ? 'IT Admin' : 'Administrateur IT'; break;