};
- };
+ header?: HeaderButtonsOrMenu;
/** CSS class that can be added to the column header */
headerCssClass?: string;
diff --git a/packages/common/src/interfaces/extensionModel.interface.ts b/packages/common/src/interfaces/extensionModel.interface.ts
index 7d280fb9e..e0cce0ec0 100644
--- a/packages/common/src/interfaces/extensionModel.interface.ts
+++ b/packages/common/src/interfaces/extensionModel.interface.ts
@@ -8,6 +8,9 @@ export interface ExtensionModel;
+ menu?: HeaderMenuItems;
+}
\ No newline at end of file
diff --git a/packages/common/src/interfaces/headerMenu.interface.ts b/packages/common/src/interfaces/headerMenu.interface.ts
index 2da9779a9..a780c16dd 100644
--- a/packages/common/src/interfaces/headerMenu.interface.ts
+++ b/packages/common/src/interfaces/headerMenu.interface.ts
@@ -1,11 +1,22 @@
+import { MenuCommandItem } from '..';
+import { HeaderMenuPlugin } from '../plugins';
import {
Column,
HeaderMenuOption,
MenuCommandItemCallbackArgs,
- SlickEventData,
SlickGrid,
} from './index';
-import { SlickHeaderMenu } from './slickHeaderMenu.interface';
+
+export interface HeaderMenuCommandItemCallbackArgs {
+ /** Column definition */
+ column: Column;
+
+ /** Slick Grid object */
+ grid: SlickGrid;
+
+ /** html DOM element of the menu */
+ menu: Array;
+}
export interface HeaderMenu extends HeaderMenuOption {
// --
@@ -13,14 +24,14 @@ export interface HeaderMenu extends HeaderMenuOption {
// ------------
/** Fired after extension (plugin) is registered by SlickGrid */
- onExtensionRegistered?: (plugin: SlickHeaderMenu) => void;
+ onExtensionRegistered?: (plugin: HeaderMenuPlugin) => void;
/** Fired After the header menu shows up. */
- onAfterMenuShow?: (e: SlickEventData, args: { grid: SlickGrid; column: Column; menu: HTMLElement; }) => void;
+ onAfterMenuShow?: (e: Event, args: HeaderMenuCommandItemCallbackArgs) => boolean | void;
/** Fired Before the header menu shows up. */
- onBeforeMenuShow?: (e: SlickEventData, args: { grid: SlickGrid; column: Column; menu: HTMLElement; }) => void;
+ onBeforeMenuShow?: (e: Event, args: HeaderMenuCommandItemCallbackArgs) => boolean | void;
/** Fired when a command is clicked */
- onCommand?: (e: SlickEventData, args: MenuCommandItemCallbackArgs) => void;
+ onCommand?: (e: Event, args: MenuCommandItemCallbackArgs) => void;
}
diff --git a/packages/common/src/interfaces/headerMenuItems.interface.ts b/packages/common/src/interfaces/headerMenuItems.interface.ts
new file mode 100644
index 000000000..26e2acfcb
--- /dev/null
+++ b/packages/common/src/interfaces/headerMenuItems.interface.ts
@@ -0,0 +1,5 @@
+import { MenuCommandItem } from './menuCommandItem.interface';
+
+export interface HeaderMenuItems {
+ items: Array;
+}
\ No newline at end of file
diff --git a/packages/common/src/interfaces/headerMenuOption.interface.ts b/packages/common/src/interfaces/headerMenuOption.interface.ts
index e4ad2dfb8..026fc23fc 100644
--- a/packages/common/src/interfaces/headerMenuOption.interface.ts
+++ b/packages/common/src/interfaces/headerMenuOption.interface.ts
@@ -1,5 +1,4 @@
-import { SlickGrid } from './slickGrid.interface';
-import { Column } from './column.interface';
+import { Column, SlickGrid } from './index';
export interface HeaderMenuOption {
/** Auto-align drop menu to the left when not enough viewport space to show on the right */
@@ -17,9 +16,6 @@ export interface HeaderMenuOption {
*/
buttonImage?: string;
- /** A command identifier to be passed to the onCommand event handlers. */
- command?: string;
-
/** Defaults to false, which will hide the "Column Resize by Content" command in the Header Menu (Grid Option "enableColumnResizeOnDoubleClick" has to also be enabled) */
hideColumnResizeByContentCommand?: boolean;
@@ -77,6 +73,9 @@ export interface HeaderMenuOption {
/** icon for the "Sort Descending" command */
iconSortDescCommand?: string;
+ /** Header Menu dropdown offset top */
+ menuOffsetTop?: number;
+
/** Minimum width that the drop menu will have */
minWidth?: number;
diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts
index 478224025..c354b124c 100644
--- a/packages/common/src/interfaces/index.ts
+++ b/packages/common/src/interfaces/index.ts
@@ -90,7 +90,9 @@ export * from './groupTotalsFormatter.interface';
export * from './headerButton.interface';
export * from './headerButtonItem.interface';
export * from './headerButtonOnCommandArgs.interface';
+export * from './headerButtonsOrMenu.interface';
export * from './headerMenu.interface';
+export * from './headerMenuItems.interface';
export * from './headerMenuOption.interface';
export * from './hideColumnOption.interface';
export * from './htmlElementPosition.interface';
diff --git a/packages/common/src/interfaces/slickGrid.interface.ts b/packages/common/src/interfaces/slickGrid.interface.ts
index 1e2bf0b22..960b91084 100644
--- a/packages/common/src/interfaces/slickGrid.interface.ts
+++ b/packages/common/src/interfaces/slickGrid.interface.ts
@@ -497,6 +497,7 @@ export interface SlickGrid {
onBeforeHeaderCellDestroy: SlickEvent;
onBeforeHeaderRowCellDestroy: SlickEvent;
onBeforeFooterRowCellDestroy: SlickEvent;
+ onBeforeSetColumns: SlickEvent;
onCellChange: SlickEvent;
onCellCssStylesChanged: SlickEvent;
onClick: SlickEvent;
@@ -543,6 +544,7 @@ export interface OnBeforeEditCellEventArgs extends SlickGridEventData { row: num
export interface OnBeforeHeaderCellDestroyEventArgs extends SlickGridEventData { node: HTMLElement; column: Column; }
export interface OnBeforeHeaderRowCellDestroyEventArgs extends SlickGridEventData { node: HTMLElement; column: Column; }
export interface OnBeforeFooterRowCellDestroyEventArgs extends SlickGridEventData { node: HTMLElement; column: Column; }
+export interface OnBeforeSetColumnsEventArgs extends SlickGridEventData { previousColumns: Column[]; newColumns: Column[]; }
export interface OnCellChangeEventArgs extends SlickGridEventData { row: number; cell: number; item: any; column: Column; }
export interface OnCellCssStylesChangedEventArgs extends SlickGridEventData { key: string; hash: string; }
export interface OnColumnsDragEventArgs extends SlickGridEventData { triggeredByColumn: string; resizeHandle: HTMLElement; }
diff --git a/packages/common/src/plugins/__tests__/headerButton.plugin.spec.ts b/packages/common/src/plugins/__tests__/headerButton.plugin.spec.ts
index ba701a173..b10ab5099 100644
--- a/packages/common/src/plugins/__tests__/headerButton.plugin.spec.ts
+++ b/packages/common/src/plugins/__tests__/headerButton.plugin.spec.ts
@@ -1,7 +1,9 @@
import { Column, GridOption, SlickGrid, SlickNamespace, } from '../../interfaces/index';
import { HeaderButtonPlugin } from '../headerButton.plugin';
-import { PubSubService } from '../../services';
+import { BackendUtilityService, PubSubService } from '../../services';
import { SharedService } from '../../services/shared.service';
+import { ExtensionUtility } from '../../extensions/extensionUtility';
+import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
declare const Slick: SlickNamespace;
@@ -50,6 +52,9 @@ const columnsMock: Column[] = [
describe('HeaderButton Plugin', () => {
const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockReturnValue();
+ let backendUtilityService: BackendUtilityService;
+ let extensionUtility: ExtensionUtility;
+ let translateService: TranslateServiceStub;
let plugin: HeaderButtonPlugin;
let sharedService: SharedService;
const mockEventCallback = () => { };
@@ -62,10 +67,13 @@ describe('HeaderButton Plugin', () => {
} as GridOption;
beforeEach(() => {
+ backendUtilityService = new BackendUtilityService();
sharedService = new SharedService();
+ translateService = new TranslateServiceStub();
+ extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService);
jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub);
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
- plugin = new HeaderButtonPlugin(pubSubServiceStub, sharedService);
+ plugin = new HeaderButtonPlugin(extensionUtility, pubSubServiceStub, sharedService);
});
afterEach(() => {
diff --git a/packages/common/src/plugins/__tests__/headerMenu.plugin.spec.ts b/packages/common/src/plugins/__tests__/headerMenu.plugin.spec.ts
new file mode 100644
index 000000000..bf490b432
--- /dev/null
+++ b/packages/common/src/plugins/__tests__/headerMenu.plugin.spec.ts
@@ -0,0 +1,1040 @@
+import { Column, ColumnSort, ElementPosition, GridOption, MenuCommandItem, SlickDataView, SlickEventData, SlickGrid, SlickNamespace, } from '../../interfaces/index';
+import { HeaderMenuPlugin } from '../headerMenu.plugin';
+import { BackendUtilityService, FilterService, PubSubService, SharedService, SortService } from '../../services';
+import { ExtensionUtility } from '../../extensions/extensionUtility';
+import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
+
+declare const Slick: SlickNamespace;
+
+const removeExtraSpaces = (textS) => `${textS}`.replace(/[\n\r]\s+/g, '');
+
+const mockEventCallback = () => { };
+const gridOptionsMock = {
+ enableAutoSizeColumns: true,
+ enableColumnResizeOnDoubleClick: true,
+ enableHeaderMenu: true,
+ enableTranslate: true,
+ backendServiceApi: {
+ service: {
+ buildQuery: jest.fn(),
+ },
+ internalPostProcess: jest.fn(),
+ preProcess: jest.fn(),
+ process: jest.fn(),
+ postProcess: jest.fn(),
+ },
+ headerMenu: {
+ buttonCssClass: 'mdi mdi-chevron-down',
+ hideFreezeColumnsCommand: false,
+ hideColumnResizeByContentCommand: false,
+ hideForceFitButton: false,
+ hideSyncResizeButton: true,
+ onExtensionRegistered: jest.fn(),
+ onCommand: mockEventCallback,
+ },
+ multiColumnSort: true,
+ pagination: {
+ totalItems: 0
+ },
+ showHeaderRow: false,
+ showTopPanel: false,
+ showPreHeaderPanel: false
+} as unknown as GridOption;
+
+const gridStub = {
+ autosizeColumns: jest.fn(),
+ getCellNode: jest.fn(),
+ getCellFromEvent: jest.fn(),
+ getColumns: jest.fn(),
+ getColumnIndex: jest.fn(),
+ getContainerNode: jest.fn(),
+ getGridPosition: jest.fn(),
+ getOptions: () => gridOptionsMock,
+ registerPlugin: jest.fn(),
+ setColumns: jest.fn(),
+ setOptions: jest.fn(),
+ setSortColumns: jest.fn(),
+ updateColumnHeader: jest.fn(),
+ onBeforeSetColumns: new Slick.Event(),
+ onBeforeHeaderCellDestroy: new Slick.Event(),
+ onHeaderCellRendered: new Slick.Event(),
+ onHeaderMouseEnter: new Slick.Event(),
+ onMouseEnter: new Slick.Event(),
+ onSort: new Slick.Event(),
+} as unknown as SlickGrid;
+
+const dataViewStub = {
+ refresh: jest.fn(),
+} as unknown as SlickDataView;
+
+const filterServiceStub = {
+ clearFilterByColumnId: jest.fn(),
+ clearFilters: jest.fn(),
+} as unknown as FilterService;
+
+const pubSubServiceStub = {
+ publish: jest.fn(),
+ subscribe: jest.fn(),
+ unsubscribe: jest.fn(),
+ unsubscribeAll: jest.fn(),
+} as PubSubService;
+
+const sortServiceStub = {
+ clearSortByColumnId: jest.fn(),
+ clearSorting: jest.fn(),
+ emitSortChanged: jest.fn(),
+ getCurrentColumnSorts: jest.fn(),
+ onBackendSortChanged: jest.fn(),
+ onLocalSortChanged: jest.fn(),
+} as unknown as SortService;
+
+const headerMock = {
+ menu: {
+ items: [
+ {
+ cssClass: 'mdi mdi-lightbulb-outline',
+ command: 'show-positive-numbers',
+ },
+ {
+ cssClass: 'mdi mdi-lightbulb-on',
+ command: 'show-negative-numbers',
+ tooltip: 'Highlight negative numbers.',
+ },
+ ]
+ }
+};
+
+const columnsMock: Column[] = [
+ { id: 'field1', field: 'field1', name: 'Field 1', width: 100, header: headerMock, },
+ { id: 'field2', field: 'field2', name: 'Field 2', width: 75, nameKey: 'TITLE', sortable: true, filterable: true },
+ { id: 'field3', field: 'field3', name: 'Field 3', width: 75, columnGroup: 'Billing' },
+];
+
+describe('HeaderMenu Plugin', () => {
+ const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockReturnValue();
+ let backendUtilityService: BackendUtilityService;
+ let extensionUtility: ExtensionUtility;
+ let translateService: TranslateServiceStub;
+ let plugin: HeaderMenuPlugin;
+ let sharedService: SharedService;
+
+ beforeEach(() => {
+ backendUtilityService = new BackendUtilityService();
+ sharedService = new SharedService();
+ translateService = new TranslateServiceStub();
+ extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService);
+ jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub);
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
+ jest.spyOn(SharedService.prototype, 'columnDefinitions', 'get').mockReturnValue(columnsMock);
+ jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock.slice(0, 2));
+ plugin = new HeaderMenuPlugin(extensionUtility, filterServiceStub, pubSubServiceStub, sharedService, sortServiceStub);
+ });
+
+ afterEach(() => {
+ plugin.dispose();
+ });
+
+ it('should create the plugin', () => {
+ expect(plugin).toBeTruthy();
+ expect(plugin.eventHandler).toBeTruthy();
+ });
+
+ it('should use default options when instantiating the plugin without passing any arguments', () => {
+ plugin.init();
+
+ expect(plugin.options).toEqual({
+ autoAlign: true,
+ autoAlignOffset: 0,
+ buttonCssClass: null,
+ buttonImage: null,
+ hideColumnHideCommand: false,
+ hideSortCommands: false,
+ minWidth: 100,
+ title: '',
+ });
+ });
+
+ it('should be able to change Header Menu options', () => {
+ plugin.init();
+ plugin.options = {
+ buttonCssClass: 'some-class'
+ };
+
+ expect(plugin.options).toEqual({
+ buttonCssClass: 'some-class',
+ });
+ });
+
+ describe('plugins - Header Menu', () => {
+ let gridContainerDiv: HTMLDivElement;
+ let headerDiv: HTMLDivElement;
+
+ beforeEach(() => {
+ jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub);
+ columnsMock[0].header.menu.items[1] = undefined;
+ columnsMock[0].header.menu.items[1] = {
+ cssClass: 'mdi mdi-lightbulb-on',
+ command: 'show-negative-numbers',
+ tooltip: 'Highlight negative numbers.',
+ } as MenuCommandItem;
+ headerDiv = document.createElement('div');
+ headerDiv.className = 'slick-header-column';
+ gridContainerDiv = document.createElement('div');
+ gridContainerDiv.className = 'slickgrid-container';
+ jest.spyOn(gridStub, 'getContainerNode').mockReturnValue(gridContainerDiv);
+ jest.spyOn(gridStub, 'getGridPosition').mockReturnValue({ top: 10, bottom: 5, left: 15, right: 22, width: 225 } as ElementPosition);
+ });
+
+ afterEach(() => {
+ plugin.dispose();
+ });
+
+ it('should populate a Header Menu with extra button css classes when header menu option "buttonCssClass" and cell is being rendered', () => {
+ plugin.dispose();
+ plugin.init({ buttonCssClass: 'mdi mdi-chevron-down' });
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => undefined;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+
+ expect(removeExtraSpaces(headerDiv.innerHTML)).toBe(removeExtraSpaces(
+ ``));
+ });
+
+ it('should populate a Header Menu with extra button image when header menu option "buttonImage" and cell is being rendered', () => {
+ plugin.dispose();
+ plugin.init({ buttonImage: '/image.png' });
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => undefined;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+
+ expect(removeExtraSpaces(headerDiv.innerHTML)).toBe(removeExtraSpaces(
+ ``));
+ });
+
+ it('should populate a Header Menu with extra tooltip title attribute when header menu option "tooltip" and cell is being rendered', () => {
+ plugin.dispose();
+ plugin.init({ tooltip: 'some tooltip text' });
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => undefined;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+
+ expect(removeExtraSpaces(headerDiv.innerHTML)).toBe(removeExtraSpaces(
+ ``));
+ });
+
+ it('should populate a Header Menu when cell is being rendered and a 2nd button item visibility callback returns undefined', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => undefined;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+
+ // add Header Menu which is visible
+ expect(removeExtraSpaces(headerDiv.innerHTML)).toBe(removeExtraSpaces(
+ ``));
+
+ gridStub.onBeforeHeaderCellDestroy.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ expect(headerDiv.innerHTML).toBe('');
+ });
+
+ it('should populate a Header Menu when cell is being rendered and a 2nd button item visibility callback returns false', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => false;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+
+ // add Header Menu which is visible
+ expect(removeExtraSpaces(headerDiv.innerHTML)).toBe(removeExtraSpaces(
+ ``));
+ });
+
+ it('should populate a Header Menu when cell is being rendered and a 2nd button item visibility & usability callbacks returns true', () => {
+ plugin.dispose();
+ plugin.init({ hideFreezeColumnsCommand: false, hideFilterCommand: false });
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => true;
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemUsabilityOverride = () => true;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+
+ // add Header Menu which is visible
+ expect(removeExtraSpaces(headerDiv.innerHTML)).toBe(removeExtraSpaces(``));
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ });
+
+ it('should populate a Header Menu and a 2nd button item usability callback returns false and expect button to be disabled', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => true;
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemUsabilityOverride = () => false;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton:nth-child(1)') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem.slick-header-menuitem-disabled');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ });
+
+ it('should populate a Header Menu and a 2nd button is "disabled" and expect button to be disabled', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = undefined;
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).disabled = true;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem.slick-header-menuitem-disabled');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ });
+
+ it('should populate a Header Menu and expect button to be disabled when command property is disabled', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).hidden = true;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem.slick-header-menuitem-hidden');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ });
+
+ it('should populate a Header Menu and a 2nd button and property "iconImage" is filled and expect button to include an image background', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).iconImage = '/images/some-gridmenu-image.png';
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem[data-command="show-negative-numbers"]');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ expect(consoleWarnSpy).toHaveBeenCalledWith('[Slickgrid-Universal] The "iconImage" property of a Header Menu item is now deprecated and will be removed in future version, consider using "iconCssClass" instead.');
+ });
+
+ it('should populate a Header Menu and a 2nd button and property "image" is filled and expect button to include an image background', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).title = 'Some Title';
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).textCssClass = 'bold red';
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem[data-command="show-negative-numbers"]');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ expect(consoleWarnSpy).toHaveBeenCalledWith('[Slickgrid-Universal] The "iconImage" property of a Header Menu item is now deprecated and will be removed in future version, consider using "iconCssClass" instead.');
+ });
+
+ it('should populate a Header Menu and a 2nd button and property "tooltip" is filled and expect button to include a "title" attribute for the tooltip', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).tooltip = 'Some Tooltip';
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem[data-command="show-negative-numbers"]');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ });
+
+ it('should populate a Header Menu and a 2nd button and expect the button click handler & action callback to be executed when defined', () => {
+ const actionMock = jest.fn();
+
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).action = actionMock;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem[data-command="show-negative-numbers"]');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+
+ gridContainerDiv.querySelector('.slick-header-menuitem.mdi-lightbulb-on').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ expect(actionMock).toHaveBeenCalled();
+ expect(gridContainerDiv.innerHTML).toBe('');
+ });
+
+ it('should populate a Header Menu and a 2nd button and expect the "onCommand" handler to be executed when defined', () => {
+ const onCommandMock = jest.fn();
+
+ plugin.dispose();
+ plugin.init();
+ plugin.options.onCommand = onCommandMock;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem[data-command="show-negative-numbers"]');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+
+ gridContainerDiv.querySelector('.slick-header-menuitem.mdi-lightbulb-on').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ expect(onCommandMock).toHaveBeenCalled();
+ expect(gridContainerDiv.innerHTML).toBe('');
+ });
+
+ it('should populate a Header Menu and a 2nd button is "disabled" but still expect the button NOT to be disabled because the "itemUsabilityOverride" has priority over the "disabled" property', () => {
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({ ...gridOptionsMock, enableSorting: true, });
+
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => true;
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemUsabilityOverride = () => true;
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).disabled = true;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem[data-command="show-negative-numbers"]');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ });
+
+ it('should "autoAlign" and expect menu to aligned left with a calculate offset when showing menu', () => {
+ plugin.dispose();
+ plugin.init({ autoAlign: true });
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const buttonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ buttonElm.dispatchEvent(new Event('click'));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem[data-command="show-negative-numbers"]');
+ const menuElm = gridContainerDiv.querySelector('.slick-header-menu') as HTMLDivElement;
+ const clickEvent = new MouseEvent('click');
+ Object.defineProperty(buttonElm, 'clientWidth', { writable: true, configurable: true, value: 350 });
+ Object.defineProperty(plugin.menuElement, 'clientWidth', { writable: true, configurable: true, value: 275 });
+ Object.defineProperty(clickEvent, 'target', { writable: true, configurable: true, value: buttonElm });
+ plugin.showMenu(clickEvent, columnsMock[0], columnsMock[0].header.menu);
+
+ expect(menuElm).toBeTruthy();
+ expect(menuElm.clientWidth).toBe(275);
+ expect(menuElm.style.left).toBe('75px');
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+ });
+
+ it('should not populate a Header Menu when 2nd button item visibility callback returns false', () => {
+ plugin.dispose();
+ plugin.init();
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => false;
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemUsabilityOverride = () => false;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton:nth-child(1)') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem.slick-header-menuitem-disabled');
+
+ expect(commandElm).toBeFalsy();
+ });
+
+ it('should not populate a Header Menu when "menuUsabilityOverride" is defined and returns False', () => {
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({ ...gridOptionsMock, enableSorting: true, });
+
+ plugin.dispose();
+ plugin.init({ menuUsabilityOverride: () => false });
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemVisibilityOverride = () => true;
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).itemUsabilityOverride = () => true;
+ (columnsMock[0].header.menu.items[1] as MenuCommandItem).disabled = true;
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+
+ expect(headerButtonElm).toBeFalsy();
+ });
+
+ it('should open the Header Menu and then expect it to hide when clicking anywhere in the DOM body', () => {
+ const hideMenuSpy = jest.spyOn(plugin, 'hideMenu');
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({ ...gridOptionsMock, enableSorting: true, });
+
+ plugin.dispose();
+ plugin.init();
+
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const commandElm = gridContainerDiv.querySelector('.slick-header-menuitem[data-command="show-negative-numbers"]');
+
+ expect(commandElm).toBeTruthy();
+ expect(removeExtraSpaces(commandElm.outerHTML)).toBe(removeExtraSpaces(
+ ``
+ ));
+
+ const bodyElm = document.body;
+ bodyElm.dispatchEvent(new Event('mousedown', { bubbles: true }));
+ expect(hideMenuSpy).toHaveBeenCalled();
+ });
+
+ describe('hideColumn method', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ columnsMock[0].header.menu = undefined;
+ columnsMock[1].header.menu = undefined;
+ columnsMock[2].header.menu = undefined;
+ const mockColumn = { id: 'field1', field: 'field1', width: 100, nameKey: 'TITLE', sortable: true, filterable: true } as any;
+ jest.spyOn(SharedService.prototype, 'columnDefinitions', 'get').mockReturnValue([mockColumn]);
+ jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock);
+ });
+
+ it('should call hideColumn and expect "visibleColumns" to be updated accordingly', () => {
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock,
+ headerMenu: { hideFreezeColumnsCommand: false, hideColumnResizeByContentCommand: true, }
+ });
+ jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub);
+ jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(1);
+ jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock);
+ const setColumnsSpy = jest.spyOn(gridStub, 'setColumns');
+ const setOptionSpy = jest.spyOn(gridStub, 'setOptions');
+ const visibleSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set');
+ const updatedColumnsMock = [
+ { id: 'field1', field: 'field1', name: 'Field 1', width: 100, header: { menu: undefined, }, },
+ { id: 'field3', field: 'field3', name: 'Field 3', columnGroup: 'Billing', header: { menu: undefined, }, width: 75, },
+ ] as Column[];
+
+ plugin.hideColumn(columnsMock[1]);
+
+ expect(setOptionSpy).not.toHaveBeenCalled();
+ expect(visibleSpy).toHaveBeenCalledWith(updatedColumnsMock);
+ expect(setColumnsSpy).toHaveBeenCalledWith(updatedColumnsMock);
+ });
+
+ it('should call hideColumn and expect "setOptions" to be called with new "frozenColumn" index when the grid is detected to be a frozen grid', () => {
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, frozenColumn: 1,
+ headerMenu: { hideFreezeColumnsCommand: false, hideColumnResizeByContentCommand: true, }
+ });
+
+ jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub);
+ jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(1);
+ jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock);
+ const setColumnsSpy = jest.spyOn(gridStub, 'setColumns');
+ const setOptionSpy = jest.spyOn(gridStub, 'setOptions');
+ const visibleSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set');
+ const updatedColumnsMock = [
+ { id: 'field1', field: 'field1', name: 'Field 1', width: 100, header: { menu: undefined, }, },
+ { id: 'field3', field: 'field3', name: 'Field 3', columnGroup: 'Billing', header: { menu: undefined, }, width: 75, },
+ ] as Column[];
+
+ plugin.hideColumn(columnsMock[1]);
+
+ expect(setOptionSpy).toHaveBeenCalledWith({ frozenColumn: 0 });
+ expect(visibleSpy).toHaveBeenCalledWith(updatedColumnsMock);
+ expect(setColumnsSpy).toHaveBeenCalledWith(updatedColumnsMock);
+ });
+ });
+
+ describe('Internal Custom Commands', () => {
+ let eventData: SlickEventData;
+
+ beforeEach(() => {
+ columnsMock[1].header.menu = undefined;
+ columnsMock[2].header.menu = undefined;
+ headerDiv = document.createElement('div');
+ headerDiv.className = 'slick-header-column';
+ eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should expect menu to show and "onBeforeMenuShow" callback to run when defined', () => {
+ const originalColumnDefinitions = [{ id: 'field1', field: 'field1', width: 100, nameKey: 'TITLE' }, { id: 'field2', field: 'field2', width: 75 }];
+ jest.spyOn(gridStub, 'getColumns').mockReturnValue(originalColumnDefinitions);
+ jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(originalColumnDefinitions);
+ jest.spyOn(SharedService.prototype, 'hasColumnsReordered', 'get').mockReturnValue(true);
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock,
+ headerMenu: {
+ hideFreezeColumnsCommand: false, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true,
+ }
+ });
+
+ plugin.init({ onBeforeMenuShow: () => false });
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: originalColumnDefinitions, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: originalColumnDefinitions[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="freeze-columns"]') as HTMLDivElement;
+ expect((originalColumnDefinitions[1] as any).header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-thumb-tack', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
+ { divider: true, command: '', positionOrder: 49 },
+ ]);
+ expect(commandDivElm).toBeFalsy();
+ });
+
+ it('should expect menu to show and "onAfterMenuShow" callback to run when defined', () => {
+ const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, enableFiltering: true,
+ headerMenu: { hideFilterCommand: false, hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ plugin.init({ onAfterMenuShow: () => false });
+ const onAfterSpy = jest.spyOn(plugin.options, 'onAfterMenuShow').mockReturnValue(false);
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const clearFilterSpy = jest.spyOn(filterServiceStub, 'clearFilterByColumnId');
+
+ const headerMenuExpected = [{ iconCssClass: 'fa fa-filter', title: 'Remove Filter', titleKey: 'REMOVE_FILTER', command: 'clear-filter', positionOrder: 53 }];
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="clear-filter"]') as HTMLDivElement;
+ const commandIconElm = commandDivElm.querySelector('.slick-header-menuicon') as HTMLDivElement;
+ const commandLabelElm = commandDivElm.querySelector('.slick-header-menucontent') as HTMLDivElement;
+ expect(columnsMock[1].header.menu.items).toEqual(headerMenuExpected);
+ expect(commandIconElm.classList.contains('fa-filter')).toBeTruthy();
+ expect(commandLabelElm.textContent).toBe('Remove Filter');
+
+ const clickEvent = new Event('click');
+ commandDivElm.dispatchEvent(clickEvent);
+
+ expect(clearFilterSpy).toHaveBeenCalledWith(clickEvent, 'field2');
+ expect(onAfterSpy).toHaveBeenCalled();
+ expect(pubSubSpy).toHaveBeenCalledWith('headerMenu:onAfterMenuShow', {
+ grid: gridStub,
+ menu: { items: headerMenuExpected },
+ column: columnsMock[1]
+ });
+ });
+
+ it('should have the commands "column-resize-by-content" and "hide-column" in the header menu list and also expect the command to execute necessary callback', () => {
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock,
+ headerMenu: { hideFreezeColumnsCommand: true, hideColumnResizeByContentCommand: false, }
+ });
+
+ // calling `onBeforeSetColumns` 2x times shouldn't duplicate any column menus
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');
+
+ const headerMenuExpected = [
+ { iconCssClass: 'fa fa-arrows-h', title: 'Resize by Content', titleKey: 'COLUMN_RESIZE_BY_CONTENT', command: 'column-resize-by-content', positionOrder: 48 },
+ { divider: true, command: '', positionOrder: 49 },
+ { iconCssClass: 'fa fa-times', title: 'Hide Column', titleKey: 'HIDE_COLUMN', command: 'hide-column', positionOrder: 55 }
+ ];
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="column-resize-by-content"]') as HTMLDivElement;
+ const commandIconElm = commandDivElm.querySelector('.slick-header-menuicon') as HTMLDivElement;
+ const commandLabelElm = commandDivElm.querySelector('.slick-header-menucontent') as HTMLDivElement;
+ expect(columnsMock[1].header.menu.items).toEqual(headerMenuExpected);
+ expect(commandIconElm.classList.contains('fa-arrows-h')).toBeTruthy();
+ expect(commandLabelElm.textContent).toBe('Resize by Content');
+
+ const clickEvent = new Event('click');
+ commandDivElm.dispatchEvent(clickEvent);
+ expect(pubSubSpy).toHaveBeenCalledWith('onHeaderMenuColumnResizeByContent', { columnId: 'field2' });
+ });
+
+ it('should expect only the "hide-column" command in the menu when "enableSorting" and "hideSortCommands" are set and also expect the command to execute necessary callback', () => {
+ jest.spyOn(SharedService.prototype.slickGrid, 'getColumnIndex').mockReturnValue(1);
+ jest.spyOn(SharedService.prototype.slickGrid, 'getColumns').mockReturnValue(columnsMock);
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, enableSorting: true, enableColumnResizeOnDoubleClick: false,
+ headerMenu: { hideColumnHideCommand: false, hideSortCommands: true, }
+ });
+
+ // calling `onBeforeSetColumns` 2x times shouldn't duplicate hide column menu
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const autosizeSpy = jest.spyOn(gridStub, 'autosizeColumns');
+
+ const headerMenuExpected = [
+ { iconCssClass: 'fa fa-thumb-tack', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
+ { divider: true, command: '', positionOrder: 49 },
+ { iconCssClass: 'fa fa-times', title: 'Hide Column', titleKey: 'HIDE_COLUMN', command: 'hide-column', positionOrder: 55 }
+ ];
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="hide-column"]') as HTMLDivElement;
+ const commandIconElm = commandDivElm.querySelector('.slick-header-menuicon') as HTMLDivElement;
+ const commandLabelElm = commandDivElm.querySelector('.slick-header-menucontent') as HTMLDivElement;
+ expect(columnsMock[1].header.menu.items).toEqual(headerMenuExpected);
+ expect(commandIconElm.classList.contains('fa-times')).toBeTruthy();
+ expect(commandLabelElm.textContent).toBe('Hide Column');
+
+ commandDivElm.dispatchEvent(new Event('click'));
+ expect(autosizeSpy).toHaveBeenCalled();
+ });
+
+ it('should expect all menu related to Filtering when "enableFiltering" is set', () => {
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, enableFiltering: true,
+ headerMenu: { hideFilterCommand: false, hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ // calling `onBeforeSetColumns` 2x times shouldn't duplicate clear filter menu
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const clearFilterSpy = jest.spyOn(filterServiceStub, 'clearFilterByColumnId');
+
+ const headerMenuExpected = [{ iconCssClass: 'fa fa-filter', title: 'Remove Filter', titleKey: 'REMOVE_FILTER', command: 'clear-filter', positionOrder: 53 }];
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="clear-filter"]') as HTMLDivElement;
+ const commandIconElm = commandDivElm.querySelector('.slick-header-menuicon') as HTMLDivElement;
+ const commandLabelElm = commandDivElm.querySelector('.slick-header-menucontent') as HTMLDivElement;
+ expect(columnsMock[1].header.menu.items).toEqual(headerMenuExpected);
+ expect(commandIconElm.classList.contains('fa-filter')).toBeTruthy();
+ expect(commandLabelElm.textContent).toBe('Remove Filter');
+
+ const clickEvent = new Event('click');
+ commandDivElm.dispatchEvent(clickEvent);
+ expect(clearFilterSpy).toHaveBeenCalledWith(clickEvent, 'field2');
+ });
+
+ it('should expect all menu related to Sorting when "enableSorting" is set', () => {
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, enableSorting: true,
+ headerMenu: { hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ // calling `onBeforeSetColumns` 2x times shouldn't duplicate clear sort menu
+ const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ const clearSortSpy = jest.spyOn(sortServiceStub, 'clearSortByColumnId');
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="clear-sort"]') as HTMLDivElement;
+ const commandIconElm = commandDivElm.querySelector('.slick-header-menuicon') as HTMLDivElement;
+ const commandLabelElm = commandDivElm.querySelector('.slick-header-menucontent') as HTMLDivElement;
+ expect(columnsMock[1].header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-sort-asc', title: 'Sort Ascending', titleKey: 'SORT_ASCENDING', command: 'sort-asc', positionOrder: 50 },
+ { iconCssClass: 'fa fa-sort-desc', title: 'Sort Descending', titleKey: 'SORT_DESCENDING', command: 'sort-desc', positionOrder: 51 },
+ { divider: true, command: '', positionOrder: 52 },
+ { iconCssClass: 'fa fa-unsorted', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 54 },
+ ]);
+ expect(commandIconElm.classList.contains('fa-unsorted')).toBeTruthy();
+ expect(commandLabelElm.textContent).toBe('Remove Sort');
+
+ translateService.use('fr');
+ plugin.translateHeaderMenu();
+ expect(columnsMock[1].header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-sort-asc', title: 'Trier par ordre croissant', titleKey: 'SORT_ASCENDING', command: 'sort-asc', positionOrder: 50 },
+ { iconCssClass: 'fa fa-sort-desc', title: 'Trier par ordre décroissant', titleKey: 'SORT_DESCENDING', command: 'sort-desc', positionOrder: 51 },
+ { divider: true, command: '', positionOrder: 52 },
+ { iconCssClass: 'fa fa-unsorted', title: 'Supprimer le tri', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 54 },
+ ]);
+
+ const clickEvent = new Event('click');
+ commandDivElm.dispatchEvent(clickEvent);
+ expect(clearSortSpy).toHaveBeenCalledWith(clickEvent, 'field2');
+ });
+
+ it('should expect menu related to Freeze Columns when "hideFreezeColumnsCommand" is disabled and also expect grid "setOptions" method to be called with current column position', () => {
+ const setOptionsSpy = jest.spyOn(gridStub, 'setOptions');
+ const setColSpy = jest.spyOn(gridStub, 'setColumns');
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock,
+ headerMenu: { hideFreezeColumnsCommand: false, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ // calling `onBeforeSetColumns` 2x times shouldn't duplicate clear sort menu
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="freeze-columns"]') as HTMLDivElement;
+ const commandIconElm = commandDivElm.querySelector('.slick-header-menuicon') as HTMLDivElement;
+ const commandLabelElm = commandDivElm.querySelector('.slick-header-menucontent') as HTMLDivElement;
+ expect(columnsMock[1].header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-thumb-tack', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
+ { divider: true, command: '', positionOrder: 49 },
+ ]);
+ expect(commandIconElm.classList.contains('fa-thumb-tack')).toBeTruthy();
+ expect(commandLabelElm.textContent).toBe('Freeze Columns');
+
+ translateService.use('fr');
+ plugin.translateHeaderMenu();
+ expect(columnsMock[1].header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-thumb-tack', title: 'Geler les colonnes', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
+ { divider: true, command: '', positionOrder: 49 },
+ ]);
+
+ commandDivElm.dispatchEvent(new Event('click')); // execute command
+ expect(setOptionsSpy).toHaveBeenCalledWith({ frozenColumn: 1, enableMouseWheelScrollHandler: true }, false, true);
+ expect(setColSpy).toHaveBeenCalledWith(columnsMock);
+ });
+
+ it('should expect menu related to Freeze Columns when "hideFreezeColumnsCommand" is disabled and also expect grid "setOptions" method to be called with frozen column of -1 because the column found is not visible', () => {
+ const setOptionsSpy = jest.spyOn(gridStub, 'setOptions');
+ const setColSpy = jest.spyOn(gridStub, 'setColumns');
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock,
+ headerMenu: { hideFreezeColumnsCommand: false, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[2], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="freeze-columns"]') as HTMLDivElement;
+ expect(columnsMock[2].header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-thumb-tack', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
+ { divider: true, command: '', positionOrder: 49 },
+ ]);
+
+ commandDivElm.dispatchEvent(new Event('click')); // execute command
+ expect(setOptionsSpy).toHaveBeenCalledWith({ frozenColumn: -1, enableMouseWheelScrollHandler: true }, false, true);
+ expect(setColSpy).toHaveBeenCalledWith(columnsMock);
+ });
+
+ it('should expect menu related to Freeze Columns when "hideFreezeColumnsCommand" is disabled and also expect "setColumns" to be called with same as original even when the column definitions list did not change', () => {
+ const originalColumnDefinitions = [{ id: 'field1', field: 'field1', width: 100, nameKey: 'TITLE' }, { id: 'field2', field: 'field2', width: 75 }];
+ const setOptionsSpy = jest.spyOn(gridStub, 'setOptions');
+ const setColSpy = jest.spyOn(gridStub, 'setColumns');
+ jest.spyOn(gridStub, 'getColumns').mockReturnValue(originalColumnDefinitions);
+ jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(originalColumnDefinitions);
+ jest.spyOn(SharedService.prototype, 'hasColumnsReordered', 'get').mockReturnValue(false);
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock,
+ headerMenu: { hideFreezeColumnsCommand: false, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: originalColumnDefinitions, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: originalColumnDefinitions[0], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="freeze-columns"]') as HTMLDivElement;
+ expect((originalColumnDefinitions[1] as any).header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-thumb-tack', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
+ { divider: true, command: '', positionOrder: 49 },
+ ]);
+
+ commandDivElm.dispatchEvent(new Event('click')); // execute command
+ expect(setOptionsSpy).toHaveBeenCalledWith({ frozenColumn: 0, enableMouseWheelScrollHandler: true }, false, true);
+ expect(setColSpy).toHaveBeenCalledWith(originalColumnDefinitions);
+ });
+
+ it('should trigger the command "sort-asc" and expect Sort Service to call "onBackendSortChanged" being called without the sorted column', () => {
+ const mockSortedCols: ColumnSort[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: false, sortCol: { id: 'field2', field: 'field2' } }];
+ const mockSortedOuput: ColumnSort[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: true, sortCol: { id: 'field2', field: 'field2' } }];
+ const previousSortSpy = jest.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[0]]);
+ const backendSortSpy = jest.spyOn(sortServiceStub, 'onBackendSortChanged');
+ const setSortSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setSortColumns');
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, enableSorting: true,
+ headerMenu: { hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="sort-asc"]') as HTMLDivElement;
+ expect(columnsMock[1].header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-sort-asc', title: 'Sort Ascending', titleKey: 'SORT_ASCENDING', command: 'sort-asc', positionOrder: 50 },
+ { iconCssClass: 'fa fa-sort-desc', title: 'Sort Descending', titleKey: 'SORT_DESCENDING', command: 'sort-desc', positionOrder: 51 },
+ { divider: true, command: '', positionOrder: 52 },
+ { iconCssClass: 'fa fa-unsorted', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 54 },
+ ]);
+
+ const clickEvent = new Event('click');
+ commandDivElm.dispatchEvent(clickEvent);
+ expect(previousSortSpy).toHaveBeenCalled();
+ mockSortedOuput[1].sortCol = { ...columnsMock[1], ...mockSortedOuput[1].sortCol }; // merge with column header menu
+ expect(backendSortSpy).toHaveBeenCalledWith(expect.anything(), { multiColumnSort: true, sortCols: mockSortedOuput, grid: gridStub });
+ expect(setSortSpy).toHaveBeenCalled();
+ });
+
+ it('should trigger the command "sort-desc" and expect Sort Service to call "onBackendSortChanged" being called without the sorted column', () => {
+ const mockSortedCols: ColumnSort[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: true, sortCol: { id: 'field2', field: 'field2' } }];
+ const mockSortedOuput: ColumnSort[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: false, sortCol: { id: 'field2', field: 'field2' } }];
+ const previousSortSpy = jest.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[0]]);
+ const backendSortSpy = jest.spyOn(sortServiceStub, 'onBackendSortChanged');
+ const setSortSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setSortColumns');
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, enableSorting: true,
+ headerMenu: { hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+
+ const commandDivElm = gridContainerDiv.querySelector('[data-command="sort-desc"]') as HTMLDivElement;
+ expect(columnsMock[1].header.menu.items).toEqual([
+ { iconCssClass: 'fa fa-sort-asc', title: 'Sort Ascending', titleKey: 'SORT_ASCENDING', command: 'sort-asc', positionOrder: 50 },
+ { iconCssClass: 'fa fa-sort-desc', title: 'Sort Descending', titleKey: 'SORT_DESCENDING', command: 'sort-desc', positionOrder: 51 },
+ { divider: true, command: '', positionOrder: 52 },
+ { iconCssClass: 'fa fa-unsorted', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 54 },
+ ]);
+
+ const clickEvent = new Event('click');
+ commandDivElm.dispatchEvent(clickEvent);
+ expect(previousSortSpy).toHaveBeenCalled();
+ mockSortedOuput[1].sortCol = { ...columnsMock[1], ...mockSortedOuput[1].sortCol }; // merge with column header menu
+ expect(backendSortSpy).toHaveBeenCalledWith(expect.anything(), { multiColumnSort: true, sortCols: mockSortedOuput, grid: gridStub });
+ expect(setSortSpy).toHaveBeenCalled();
+ });
+
+ it('should trigger the command "sort-desc" and expect Sort Service to call "onLocalSortChanged" being called without the sorted column', () => {
+ jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(dataViewStub);
+ const mockSortedCols: ColumnSort[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: true, sortCol: { id: 'field2', field: 'field2' } }];
+ const mockSortedOuput: ColumnSort[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: false, sortCol: { id: 'field2', field: 'field2' } }];
+ const previousSortSpy = jest.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[0]]);
+ const localSortSpy = jest.spyOn(sortServiceStub, 'onLocalSortChanged');
+ const setSortSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setSortColumns');
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, enableSorting: true, backendServiceApi: undefined,
+ headerMenu: { hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ gridContainerDiv.querySelector('[data-command="sort-desc"]').dispatchEvent(new Event('click'));
+ expect(previousSortSpy).toHaveBeenCalled();
+ mockSortedOuput[1].sortCol = { ...columnsMock[1], ...mockSortedOuput[1].sortCol }; // merge with column header menu
+ expect(previousSortSpy).toHaveBeenCalled();
+ expect(localSortSpy).toHaveBeenCalledWith(gridStub, mockSortedOuput);
+ expect(setSortSpy).toHaveBeenCalled();
+ });
+
+ it('should trigger the command "sort-desc" and expect "onSort" event triggered when no DataView is provided', () => {
+ jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(undefined as any);
+ const mockSortedCols: ColumnSort[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: true, sortCol: { id: 'field2', field: 'field2' } }];
+ const mockSortedOuput: ColumnSort[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: false, sortCol: { id: 'field2', field: 'field2' } }];
+ const previousSortSpy = jest.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[0]]);
+ const setSortSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setSortColumns');
+ const gridSortSpy = jest.spyOn(gridStub.onSort, 'notify');
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
+ ...gridOptionsMock, enableSorting: true, backendServiceApi: undefined,
+ headerMenu: { hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true, }
+ });
+
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menubutton') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+ gridContainerDiv.querySelector('[data-command="sort-desc"]').dispatchEvent(new Event('click'));
+ expect(previousSortSpy).toHaveBeenCalled();
+ mockSortedOuput[1].sortCol = { ...columnsMock[1], ...mockSortedOuput[1].sortCol }; // merge with column header menu
+ expect(previousSortSpy).toHaveBeenCalled();
+ expect(gridSortSpy).toHaveBeenCalledWith(mockSortedOuput);
+ expect(setSortSpy).toHaveBeenCalled();
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/common/src/plugins/headerButton.plugin.ts b/packages/common/src/plugins/headerButton.plugin.ts
index 4873fb196..9415cd317 100644
--- a/packages/common/src/plugins/headerButton.plugin.ts
+++ b/packages/common/src/plugins/headerButton.plugin.ts
@@ -12,6 +12,7 @@ import {
SlickNamespace,
} from '../interfaces/index';
import { BindingEventService } from '../services/bindingEvent.service';
+import { ExtensionUtility } from '../extensions/extensionUtility';
import { PubSubService } from '../services/pubSub.service';
import { SharedService } from '../services/shared.service';
@@ -20,6 +21,13 @@ declare const Slick: SlickNamespace;
/**
* A plugin to add custom buttons to column headers.
+ * To specify a custom button in a column header, extend the column definition like so:
+ * this.columnDefinitions = [{
+ * id: 'myColumn', name: 'My column',
+ * header: {
+ * buttons: [{ ...button options... }, { ...button options... }]
+ * }
+ * }];
*/
export class HeaderButtonPlugin {
protected _bindEventService: BindingEventService;
@@ -32,7 +40,7 @@ export class HeaderButtonPlugin {
pluginName: 'HeaderButtons' = 'HeaderButtons';
/** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */
- constructor(protected readonly pubSubService: PubSubService, protected readonly sharedService: SharedService) {
+ constructor(protected readonly extensionUtility: ExtensionUtility, protected readonly pubSubService: PubSubService, protected readonly sharedService: SharedService) {
this._bindEventService = new BindingEventService();
this._eventHandler = new Slick.EventHandler();
this.init(sharedService.gridOptions.headerButton);
@@ -92,8 +100,8 @@ export class HeaderButtonPlugin {
while (i--) {
const button = column.header.buttons[i];
// run each override functions to know if the item is visible and usable
- const isItemVisible = this.runOverrideFunctionWhenExists(button.itemVisibilityOverride, args);
- const isItemUsable = this.runOverrideFunctionWhenExists(button.itemUsabilityOverride, args);
+ const isItemVisible = this.extensionUtility.runOverrideFunctionWhenExists(button.itemVisibilityOverride, args);
+ const isItemUsable = this.extensionUtility.runOverrideFunctionWhenExists(button.itemUsabilityOverride, args);
// if the result is not visible then there's no need to go further
if (!isItemVisible) {
@@ -197,16 +205,4 @@ export class HeaderButtonPlugin {
event.preventDefault();
event.stopPropagation();
}
-
- // --
- // protected functions
- // ------------------
-
- /** Run the Override function when it exists, if it returns True then it is usable/visible */
- protected runOverrideFunctionWhenExists(overrideFn: any, args: T): boolean {
- if (typeof overrideFn === 'function') {
- return overrideFn.call(this, args);
- }
- return true;
- }
}
\ No newline at end of file
diff --git a/packages/common/src/plugins/headerMenu.plugin.ts b/packages/common/src/plugins/headerMenu.plugin.ts
new file mode 100644
index 000000000..8141bb250
--- /dev/null
+++ b/packages/common/src/plugins/headerMenu.plugin.ts
@@ -0,0 +1,664 @@
+import { EmitterType } from '../enums/index';
+import {
+ Column,
+ CurrentSorter,
+ DOMEvent,
+ GetSlickEventType,
+ HeaderMenu,
+ HeaderMenuCommandItemCallbackArgs,
+ HeaderMenuItems,
+ HeaderMenuOption,
+ MenuCommandItem,
+ MenuCommandItemCallbackArgs,
+ OnHeaderCellRenderedEventArgs,
+ SlickEventHandler,
+ SlickGrid,
+ SlickNamespace,
+} from '../interfaces/index';
+import { arrayRemoveItemByIndex, emptyElement, getElementOffsetRelativeToParent, } from '../services/index';
+import { BindingEventService } from '../services/bindingEvent.service';
+import { ExtensionUtility } from '../extensions/extensionUtility';
+import { FilterService } from '../services/filter.service';
+import { PubSubService } from '../services/pubSub.service';
+import { SharedService } from '../services/shared.service';
+import { SortService } from '../services/sort.service';
+
+// using external SlickGrid JS libraries
+declare const Slick: SlickNamespace;
+
+/**
+ * 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: 'myColumn', name: 'My column',
+ * header: {
+ * menu: {
+ * items: [{ ...menu item options... }, { ...menu item options... }]
+ * }
+ * }
+ * }];
+ */
+export class HeaderMenuPlugin {
+ protected _activeHeaderColumnElm?: HTMLDivElement;
+ protected _bindEventService: BindingEventService;
+ protected _eventHandler!: SlickEventHandler;
+ protected _options?: HeaderMenu;
+ protected _menuElm?: HTMLDivElement;
+ protected _defaults = {
+ autoAlign: true,
+ autoAlignOffset: 0,
+ buttonCssClass: null,
+ buttonImage: null,
+ minWidth: 100,
+ hideColumnHideCommand: false,
+ hideSortCommands: false,
+ title: '',
+ } as unknown as HeaderMenuOption;
+ pluginName: 'HeaderMenu' = 'HeaderMenu';
+
+ /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */
+ constructor(
+ protected readonly extensionUtility: ExtensionUtility,
+ protected readonly filterService: FilterService,
+ protected readonly pubSubService: PubSubService,
+ protected readonly sharedService: SharedService,
+ protected readonly sortService: SortService,
+ ) {
+ this._bindEventService = new BindingEventService();
+ this._eventHandler = new Slick.EventHandler();
+ this.sharedService.gridOptions.headerMenu = this.addHeaderMenuCustomCommands(this.sharedService.columnDefinitions);
+ this.init(sharedService.gridOptions.headerMenu);
+ }
+
+ get eventHandler(): SlickEventHandler {
+ return this._eventHandler;
+ }
+
+ get grid(): SlickGrid {
+ return this.sharedService.slickGrid;
+ }
+
+ get menuElement(): HTMLDivElement | undefined {
+ return this._menuElm;
+ }
+
+ get options(): HeaderMenu {
+ return this._options as HeaderMenu;
+ }
+ set options(newOptions: HeaderMenu) {
+ this._options = newOptions;
+ }
+
+ /** Initialize plugin. */
+ init(headerMenuOptions?: HeaderMenu) {
+ this._options = { ...this._defaults, ...headerMenuOptions };
+
+ // when setColumns is called (could be via toggle filtering/sorting or anything else),
+ // we need to recreate header menu items custom commands array before the `onHeaderCellRendered` gets called
+ const onBeforeSetColumnsHandler = this.grid.onBeforeSetColumns;
+ (this._eventHandler as SlickEventHandler>).subscribe(onBeforeSetColumnsHandler, (e, args) => {
+ this.sharedService.gridOptions.headerMenu = this.addHeaderMenuCustomCommands(args.newColumns);
+ });
+
+ const onHeaderCellRenderedHandler = this.grid.onHeaderCellRendered;
+ (this._eventHandler as SlickEventHandler>).subscribe(onHeaderCellRenderedHandler, this.handleHeaderCellRendered.bind(this));
+
+ const onBeforeHeaderCellDestroyHandler = this.grid.onBeforeHeaderCellDestroy;
+ (this._eventHandler as SlickEventHandler>).subscribe(onBeforeHeaderCellDestroyHandler, this.handleBeforeHeaderCellDestroy.bind(this));
+
+ // force the grid to re-render the header after the events are hooked up.
+ this.grid.setColumns(this.grid.getColumns());
+
+ // hide the menu when clicking outside the grid
+ this._bindEventService.bind(document.body, 'mousedown', this.handleBodyMouseDown.bind(this) as EventListener);
+ }
+
+ /** Dispose (destroy) of the plugin */
+ dispose() {
+ this._eventHandler?.unsubscribeAll();
+ this._bindEventService.unbindAll();
+ this.pubSubService.unsubscribeAll();
+ this._menuElm?.remove();
+ this._menuElm = undefined;
+ this._activeHeaderColumnElm = undefined;
+ }
+
+ /** Hide a column from the grid */
+ hideColumn(column: Column) {
+ if (this.sharedService?.slickGrid?.getColumnIndex) {
+ const columnIndex = this.sharedService.slickGrid.getColumnIndex(column.id);
+ const currentVisibleColumns = this.sharedService.slickGrid.getColumns();
+
+ // if we're using frozen columns, we need to readjust pinning when the new hidden column is on the left pinning container
+ // we need to do this because SlickGrid freezes by index and has no knowledge of the columns themselves
+ const frozenColumnIndex = this.sharedService.gridOptions.frozenColumn ?? -1;
+ if (frozenColumnIndex >= 0 && frozenColumnIndex >= columnIndex) {
+ this.sharedService.gridOptions.frozenColumn = frozenColumnIndex - 1;
+ this.sharedService.slickGrid.setOptions({ frozenColumn: this.sharedService.gridOptions.frozenColumn });
+ }
+
+ // then proceed with hiding the column in SlickGrid & trigger an event when done
+ const visibleColumns = arrayRemoveItemByIndex(currentVisibleColumns, columnIndex);
+ this.sharedService.visibleColumns = visibleColumns;
+ this.sharedService.slickGrid.setColumns(visibleColumns);
+ this.pubSubService.publish('onHeaderMenuHideColumns', { columns: visibleColumns, hiddenColumn: column });
+ }
+ }
+
+ /** Hide the Header Menu */
+ hideMenu() {
+ this._menuElm?.remove();
+ this._menuElm = undefined;
+ this._activeHeaderColumnElm?.classList.remove('slick-header-column-active');
+ }
+
+ showMenu(e: MouseEvent, columnDef: Column, menu: HeaderMenuItems) {
+ // let the user modify the menu or cancel altogether,
+ // or provide alternative menu implementation.
+ const callbackArgs = {
+ grid: this.grid,
+ column: columnDef,
+ menu
+ } as unknown as HeaderMenuCommandItemCallbackArgs;
+
+ // execute optional callback method defined by the user, if it returns false then we won't go further and not open the grid menu
+ if (typeof e.stopPropagation === 'function') {
+ this.pubSubService.publish('headerMenu:onBeforeMenuShow', callbackArgs);
+ if (typeof this.options?.onBeforeMenuShow === 'function' && this.options?.onBeforeMenuShow(e, callbackArgs) === false) {
+ return;
+ }
+ }
+
+ if (!this._menuElm) {
+ this._menuElm = document.createElement('div');
+ this._menuElm.className = 'slick-header-menu';
+ this._menuElm.style.minWidth = `${this.options.minWidth}px`;
+ this.grid.getContainerNode()?.appendChild(this._menuElm);
+ }
+
+ // make sure the menu element is an empty div besore adding all list of commands
+ emptyElement(this._menuElm);
+ this.populateHeaderMenuCommandList(e, columnDef, menu, callbackArgs);
+ }
+
+ /** Translate the Header Menu titles, we need to loop through all column definition to re-translate them */
+ translateHeaderMenu() {
+ if (this.sharedService.gridOptions?.headerMenu) {
+ this.resetHeaderMenuTranslations(this.sharedService.visibleColumns);
+ }
+ }
+
+ // --
+ // event handlers
+ // ------------------
+
+ /**
+ * Event handler when column title header are being rendered
+ * @param {Object} event - The event
+ * @param {Object} args - object arguments
+ */
+ protected handleHeaderCellRendered(_e: Event, args: OnHeaderCellRenderedEventArgs) {
+ const column = args.column;
+ const menu = column.header?.menu as HeaderMenuItems;
+
+ if (menu && args.node) {
+ // run the override function (when defined), if the result is false we won't go further
+ if (!this.extensionUtility.runOverrideFunctionWhenExists(this.options.menuUsabilityOverride, args)) {
+ return;
+ }
+
+ const headerButtonDivElm = document.createElement('div');
+ headerButtonDivElm.className = 'slick-header-menubutton';
+
+ if (this.options.buttonCssClass) {
+ headerButtonDivElm.classList.add(...this.options.buttonCssClass.split(' '));
+ }
+
+ if (this.options.buttonImage) {
+ headerButtonDivElm.style.backgroundImage = `url(${this.options.buttonImage})`;
+ }
+
+ if (this.options.tooltip) {
+ headerButtonDivElm.title = this.options.tooltip;
+ }
+ args.node.appendChild(headerButtonDivElm);
+
+ // show the header menu dropdown list of commands
+ this._bindEventService.bind(headerButtonDivElm, 'click', ((e: MouseEvent) => this.showMenu(e, column, menu)) as EventListener);
+ }
+ }
+
+ /**
+ * Event handler before the header cell is being destroyed
+ * @param {Object} event - The event
+ * @param {Object} args.column - The column definition
+ */
+ protected handleBeforeHeaderCellDestroy(_e: Event, args: { column: Column; node: HTMLElement; }) {
+ const column = args.column;
+
+ if (column.header?.menu) {
+ // Removing buttons will also clean up any event handlers and data.
+ // NOTE: If you attach event handlers directly or using a different framework,
+ // you must also clean them up here to avoid memory leaks.
+ args.node.querySelectorAll('.slick-header-menubutton').forEach(elm => elm.remove());
+ }
+ }
+
+ /** Mouse down handler when clicking anywhere in the DOM body */
+ protected handleBodyMouseDown(e: DOMEvent) {
+ if ((this._menuElm !== e.target && !this._menuElm?.contains(e.target)) || e.target.className === 'close') {
+ this.hideMenu();
+ }
+ }
+
+ protected handleMenuItemClick(event: DOMEvent, item: MenuCommandItem, columnDef: Column) {
+ if (item?.command && !item.disabled && !item.divider) {
+
+ const callbackArgs = {
+ grid: this.grid,
+ command: item.command,
+ column: columnDef,
+ item,
+ } as MenuCommandItemCallbackArgs;
+
+ // execute Grid Menu callback with command,
+ // we'll also execute optional user defined onCommand callback when provided
+ this.executeHeaderMenuInternalCommands(event, callbackArgs);
+ this.pubSubService.publish('headerMenu:onCommand', callbackArgs);
+ if (typeof this.options?.onCommand === 'function') {
+ this.options.onCommand(event, callbackArgs);
+ }
+
+ // execute action callback when defined
+ if (typeof item.action === 'function') {
+ item.action.call(this, event, callbackArgs);
+ }
+ }
+
+ // does the user want to leave open the Grid Menu after executing a command?
+ if (!event.defaultPrevented) {
+ this.hideMenu();
+ }
+
+ // Stop propagation so that it doesn't register as a header click event.
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ // --
+ // protected functions
+ // ------------------
+
+ /**
+ * Create Header Menu with Custom Commands if user has enabled Header Menu
+ * @param gridOptions
+ * @param columnDefinitions
+ * @return header menu
+ */
+ protected addHeaderMenuCustomCommands(columnDefinitions: Column[]): HeaderMenu {
+ const gridOptions = this.sharedService.gridOptions;
+ const headerMenuOptions = gridOptions.headerMenu || {};
+
+ if (Array.isArray(columnDefinitions) && gridOptions.enableHeaderMenu) {
+ columnDefinitions.forEach((columnDef: Column) => {
+ if (columnDef && !columnDef.excludeFromHeaderMenu) {
+ if (!columnDef.header || !columnDef.header.menu) {
+ columnDef.header = {
+ menu: {
+ items: []
+ }
+ };
+ }
+ const columnHeaderMenuItems: Array = columnDef?.header?.menu?.items ?? [];
+
+ // Freeze Column (pinning)
+ let hasFrozenOrResizeCommand = false;
+ if (headerMenuOptions && !headerMenuOptions.hideFreezeColumnsCommand) {
+ hasFrozenOrResizeCommand = true;
+ if (!columnHeaderMenuItems.some(item => item !== 'divider' && item?.command === 'freeze-columns')) {
+ columnHeaderMenuItems.push({
+ iconCssClass: headerMenuOptions.iconFreezeColumns || 'fa fa-thumb-tack',
+ titleKey: 'FREEZE_COLUMNS',
+ command: 'freeze-columns',
+ positionOrder: 47
+ });
+ }
+ }
+
+ // Column Resize by Content (column autofit)
+ if (headerMenuOptions && !headerMenuOptions.hideColumnResizeByContentCommand && this.sharedService.gridOptions.enableColumnResizeOnDoubleClick) {
+ hasFrozenOrResizeCommand = true;
+ if (!columnHeaderMenuItems.some(item => item !== 'divider' && item?.command === 'column-resize-by-content')) {
+ columnHeaderMenuItems.push({
+ iconCssClass: headerMenuOptions.iconColumnResizeByContentCommand || 'fa fa-arrows-h',
+ titleKey: `COLUMN_RESIZE_BY_CONTENT`,
+ command: 'column-resize-by-content',
+ positionOrder: 48
+ });
+ }
+ }
+
+ // add a divider (separator) between the top freeze columns commands and the rest of the commands
+ if (hasFrozenOrResizeCommand && !columnHeaderMenuItems.some(item => item !== 'divider' && item.positionOrder === 49)) {
+ columnHeaderMenuItems.push({ divider: true, command: '', positionOrder: 49 });
+ }
+
+ // Sorting Commands
+ if (gridOptions.enableSorting && columnDef.sortable && headerMenuOptions && !headerMenuOptions.hideSortCommands) {
+ if (!columnHeaderMenuItems.some(item => item !== 'divider' && item?.command === 'sort-asc')) {
+ columnHeaderMenuItems.push({
+ iconCssClass: headerMenuOptions.iconSortAscCommand || 'fa fa-sort-asc',
+ titleKey: 'SORT_ASCENDING',
+ command: 'sort-asc',
+ positionOrder: 50
+ });
+ }
+ if (!columnHeaderMenuItems.some(item => item !== 'divider' && item?.command === 'sort-desc')) {
+ columnHeaderMenuItems.push({
+ iconCssClass: headerMenuOptions.iconSortDescCommand || 'fa fa-sort-desc',
+ titleKey: 'SORT_DESCENDING',
+ command: 'sort-desc',
+ positionOrder: 51
+ });
+ }
+
+ // add a divider (separator) between the top sort commands and the other clear commands
+ if (!columnHeaderMenuItems.some(item => item !== 'divider' && item.positionOrder === 52)) {
+ columnHeaderMenuItems.push({ divider: true, command: '', positionOrder: 52 });
+ }
+
+ if (!headerMenuOptions.hideClearSortCommand && !columnHeaderMenuItems.some(item => item !== 'divider' && item?.command === 'clear-sort')) {
+ columnHeaderMenuItems.push({
+ iconCssClass: headerMenuOptions.iconClearSortCommand || 'fa fa-unsorted',
+ titleKey: 'REMOVE_SORT',
+ command: 'clear-sort',
+ positionOrder: 54
+ });
+ }
+ }
+
+ // Filtering Commands
+ if (gridOptions.enableFiltering && columnDef.filterable && headerMenuOptions && !headerMenuOptions.hideFilterCommand) {
+ if (!headerMenuOptions.hideClearFilterCommand && !columnHeaderMenuItems.some(item => item !== 'divider' && item?.command === 'clear-filter')) {
+ columnHeaderMenuItems.push({
+ iconCssClass: headerMenuOptions.iconClearFilterCommand || 'fa fa-filter',
+ titleKey: 'REMOVE_FILTER',
+ command: 'clear-filter',
+ positionOrder: 53
+ });
+ }
+ }
+
+ // Hide Column Command
+ if (headerMenuOptions && !headerMenuOptions.hideColumnHideCommand && !columnHeaderMenuItems.some(item => item !== 'divider' && item?.command === 'hide-column')) {
+ columnHeaderMenuItems.push({
+ iconCssClass: headerMenuOptions.iconColumnHideCommand || 'fa fa-times',
+ titleKey: 'HIDE_COLUMN',
+ command: 'hide-column',
+ positionOrder: 55
+ });
+ }
+
+ this.extensionUtility.translateMenuItemsFromTitleKey(columnHeaderMenuItems);
+ this.extensionUtility.sortItems(columnHeaderMenuItems, 'positionOrder');
+ }
+ });
+ }
+
+ return headerMenuOptions;
+ }
+
+ /** Clear the Filter on the current column (if it's actually filtered) */
+ protected clearColumnFilter(event: Event, args: MenuCommandItemCallbackArgs) {
+ if (args?.column) {
+ this.filterService.clearFilterByColumnId(event, args.column.id);
+ }
+ }
+
+ /** Clear the Sort on the current column (if it's actually sorted) */
+ protected clearColumnSort(event: Event, args: MenuCommandItemCallbackArgs) {
+ if (args?.column && this.sharedService) {
+ this.sortService.clearSortByColumnId(event, args.column.id);
+ }
+ }
+
+ /** Execute the Header Menu Commands that was triggered by the onCommand subscribe */
+ protected executeHeaderMenuInternalCommands(event: Event, args: MenuCommandItemCallbackArgs) {
+ if (args?.command) {
+ switch (args.command) {
+ case 'hide-column':
+ this.hideColumn(args.column);
+ if (this.sharedService.gridOptions?.enableAutoSizeColumns) {
+ this.sharedService.slickGrid.autosizeColumns();
+ }
+ break;
+ case 'clear-filter':
+ this.clearColumnFilter(event, args);
+ break;
+ case 'clear-sort':
+ this.clearColumnSort(event, args);
+ break;
+ case 'column-resize-by-content':
+ this.pubSubService.publish('onHeaderMenuColumnResizeByContent', { columnId: args.column.id });
+ break;
+ case 'freeze-columns':
+ const visibleColumns = [...this.sharedService.visibleColumns];
+ const columnPosition = visibleColumns.findIndex(col => col.id === args.column.id);
+ const newGridOptions = { frozenColumn: columnPosition, enableMouseWheelScrollHandler: true };
+
+ // to circumvent a bug in SlickGrid core lib, let's keep the columns positions ref and re-apply them after calling setOptions
+ // the bug is highlighted in this issue comment:: https://github.com/6pac/SlickGrid/issues/592#issuecomment-822885069
+ const previousColumnDefinitions = this.sharedService.slickGrid.getColumns();
+
+ this.sharedService.slickGrid.setOptions(newGridOptions, false, true); // suppress the setColumns (3rd argument) since we'll do that ourselves
+ this.sharedService.gridOptions.frozenColumn = newGridOptions.frozenColumn;
+ this.sharedService.gridOptions.enableMouseWheelScrollHandler = newGridOptions.enableMouseWheelScrollHandler;
+ this.sharedService.frozenVisibleColumnId = args.column.id;
+
+ // to freeze columns, we need to take only the visible columns and we also need to use setColumns() when some of them are hidden
+ // to make sure that we only use the visible columns, not doing this will have the undesired effect of showing back some of the hidden columns
+ if (this.sharedService.hasColumnsReordered || (Array.isArray(visibleColumns) && Array.isArray(this.sharedService.allColumns) && visibleColumns.length !== this.sharedService.allColumns.length)) {
+ this.sharedService.slickGrid.setColumns(visibleColumns);
+ } else {
+ // to circumvent a bug in SlickGrid core lib re-apply same column definitions that were backend up before calling setOptions()
+ this.sharedService.slickGrid.setColumns(previousColumnDefinitions);
+ }
+
+ // we also need to autosize columns if the option is enabled
+ const gridOptions = this.sharedService.slickGrid.getOptions();
+ if (gridOptions.enableAutoSizeColumns) {
+ this.sharedService.slickGrid.autosizeColumns();
+ }
+ break;
+ case 'sort-asc':
+ case 'sort-desc':
+ const isSortingAsc = (args.command === 'sort-asc');
+ this.sortColumn(event, args, isSortingAsc);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ protected populateHeaderMenuCommandList(e: MouseEvent, columnDef: Column, menu: HeaderMenuItems, args: HeaderMenuCommandItemCallbackArgs) {
+ const menuItems = menu.items;
+
+ // Construct the menu items.
+ for (const item of menuItems) {
+ // run each override functions to know if the item is visible and usable
+ let isItemVisible = true;
+ let isItemUsable = true;
+ if (typeof item === 'object') {
+ isItemVisible = this.extensionUtility.runOverrideFunctionWhenExists(item.itemVisibilityOverride, args);
+ isItemUsable = this.extensionUtility.runOverrideFunctionWhenExists(item.itemUsabilityOverride, args);
+ }
+
+ // if the result is not visible then there's no need to go further
+ if (!isItemVisible) {
+ continue;
+ }
+
+ // when the override is defined, we need to use its result to update the disabled property
+ // so that "handleMenuItemCommandClick" has the correct flag and won't trigger a command clicked event
+ if (typeof item === 'object' && Object.prototype.hasOwnProperty.call(item, 'itemUsabilityOverride')) {
+ item.disabled = isItemUsable ? false : true;
+ }
+
+ const liElm = document.createElement('div');
+ liElm.className = 'slick-header-menuitem';
+ if (typeof item === 'object' && item.command) {
+ liElm.dataset.command = typeof item === 'object' && item.command || '';
+ }
+ this._menuElm?.appendChild(liElm);
+
+ if ((typeof item === 'object' && item.divider) || item === 'divider') {
+ liElm.classList.add('slick-header-menuitem-divider');
+ continue;
+ }
+
+ if (item.disabled) {
+ liElm.classList.add('slick-header-menuitem-disabled');
+ }
+
+ if (item.hidden) {
+ liElm.classList.add('slick-header-menuitem-hidden');
+ }
+
+ if (item.cssClass) {
+ liElm.classList.add(...item.cssClass.split(' '));
+ }
+
+ if (item.tooltip) {
+ liElm.title = item.tooltip;
+ }
+
+ const iconElm = document.createElement('div');
+ iconElm.className = 'slick-header-menuicon';
+ liElm.appendChild(iconElm);
+
+ if (item.iconCssClass) {
+ iconElm.classList.add(...item.iconCssClass.split(' '));
+ }
+
+ if (item.iconImage) {
+ console.warn('[Slickgrid-Universal] The "iconImage" property of a Header Menu item is now deprecated and will be removed in future version, consider using "iconCssClass" instead.');
+ iconElm.style.backgroundImage = `url(${item.iconImage})`;
+ }
+
+ const textElm = document.createElement('span');
+ textElm.className = 'slick-header-menucontent';
+ textElm.textContent = typeof item === 'object' && item.title || '';
+ liElm.appendChild(textElm);
+
+ if (item.textCssClass) {
+ textElm.classList.add(...item.textCssClass.split(' '));
+ }
+
+ // execute command on menu item clicked
+ this._bindEventService.bind(liElm, 'click', ((clickEvent: DOMEvent) => this.handleMenuItemClick(clickEvent, item, columnDef)) as EventListener);
+ }
+
+ this.repositionMenu(e);
+
+ // execute optional callback method defined by the user
+ this.pubSubService.publish('headerMenu:onAfterMenuShow', args);
+ if (typeof this.options?.onAfterMenuShow === 'function' && this.options?.onAfterMenuShow(e, args) === false) {
+ return;
+ }
+
+ // Stop propagation so that it doesn't register as a header click event.
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ protected repositionMenu(e: MouseEvent) {
+ const buttonElm = e.target as HTMLDivElement; // get header button createElement
+ if (this._menuElm && buttonElm.classList.contains('slick-header-menubutton')) {
+ const relativePos = getElementOffsetRelativeToParent(this.sharedService.gridContainerElement, buttonElm);
+ let leftPos = relativePos?.left ?? 0;
+
+ // when auto-align is set, it will calculate whether it has enough space in the viewport to show the drop menu on the right (default)
+ // if there isn't enough space on the right, it will automatically align the drop menu to the left
+ // to simulate an align left, we actually need to know the width of the drop menu
+ if (this.options.autoAlign) {
+ const gridPos = this.grid.getGridPosition();
+ if (gridPos?.width && (leftPos + (this._menuElm.clientWidth ?? 0)) >= gridPos.width) {
+ leftPos = leftPos + buttonElm.clientWidth - this._menuElm.clientWidth + (this.options?.autoAlignOffset ?? 0);
+ }
+ }
+
+ this._menuElm.style.top = `${(relativePos?.top ?? 0) + (this.options?.menuOffsetTop ?? 0) + buttonElm.clientHeight}px`;
+ this._menuElm.style.left = `${leftPos}px`;
+
+ // mark the header as active to keep the highlighting.
+ this._activeHeaderColumnElm = this._menuElm.closest('.slick-header-column') as HTMLDivElement;
+ this._activeHeaderColumnElm?.classList.add('slick-header-column-active');
+ }
+ }
+
+ /**
+ * Reset all the internal Menu options which have text to translate
+ * @param header menu object
+ */
+ protected resetHeaderMenuTranslations(columnDefinitions: Column[]) {
+ columnDefinitions.forEach((columnDef: Column) => {
+ if (columnDef?.header?.menu?.items && !columnDef.excludeFromHeaderMenu) {
+ const columnHeaderMenuItems: Array = columnDef.header.menu.items || [];
+ this.extensionUtility.translateMenuItemsFromTitleKey(columnHeaderMenuItems);
+ }
+ });
+ }
+
+ /** Sort the current column */
+ protected sortColumn(event: Event, args: MenuCommandItemCallbackArgs, isSortingAsc = true) {
+ if (args?.column) {
+ // get previously sorted columns
+ const columnDef = args.column;
+
+ // 1- get the sort columns without the current column, in the case of a single sort that would equal to an empty array
+ const tmpSortedColumns = !this.sharedService.gridOptions.multiColumnSort ? [] : this.sortService.getCurrentColumnSorts(columnDef.id + '');
+
+ let emitterType = EmitterType.local;
+
+ // 2- add to the column array, the new sorted column by the header menu
+ tmpSortedColumns.push({ columnId: columnDef.id, sortCol: columnDef, sortAsc: isSortingAsc });
+
+ if (this.sharedService.gridOptions.backendServiceApi) {
+ this.sortService.onBackendSortChanged(event, { multiColumnSort: true, sortCols: tmpSortedColumns, grid: this.sharedService.slickGrid });
+ emitterType = EmitterType.remote;
+ } else if (this.sharedService.dataView) {
+ this.sortService.onLocalSortChanged(this.sharedService.slickGrid, tmpSortedColumns);
+ emitterType = EmitterType.local;
+ } else {
+ // when using customDataView, we will simply send it as a onSort event with notify
+ args.grid.onSort.notify(tmpSortedColumns);
+ }
+
+ // update the sharedService.slickGrid sortColumns array which will at the same add the visual sort icon(s) on the UI
+ const newSortColumns = tmpSortedColumns.map(col => {
+ return {
+ columnId: col?.sortCol?.id ?? '',
+ sortAsc: col?.sortAsc ?? true,
+ };
+ });
+
+ // add sort icon in UI
+ this.sharedService.slickGrid.setSortColumns(newSortColumns);
+
+ // if we have an emitter type set, we will emit a sort changed
+ // for the Grid State Service to see the change.
+ // We also need to pass current sorters changed to the emitSortChanged method
+ if (emitterType) {
+ const currentLocalSorters: CurrentSorter[] = [];
+ newSortColumns.forEach((sortCol) => {
+ currentLocalSorters.push({
+ columnId: `${sortCol.columnId}`,
+ direction: sortCol.sortAsc ? 'ASC' : 'DESC'
+ });
+ });
+ this.sortService.emitSortChanged(emitterType, currentLocalSorters);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/common/src/plugins/index.ts b/packages/common/src/plugins/index.ts
index b13e1f69f..a9bb1a7c1 100644
--- a/packages/common/src/plugins/index.ts
+++ b/packages/common/src/plugins/index.ts
@@ -1,2 +1,3 @@
export * from './autoTooltip.plugin';
-export * from './headerButton.plugin';
\ No newline at end of file
+export * from './headerButton.plugin';
+export * from './headerMenu.plugin';
\ No newline at end of file
diff --git a/packages/common/src/services/__tests__/extension.service.spec.ts b/packages/common/src/services/__tests__/extension.service.spec.ts
index 5e32fd272..4cd768ee5 100644
--- a/packages/common/src/services/__tests__/extension.service.spec.ts
+++ b/packages/common/src/services/__tests__/extension.service.spec.ts
@@ -10,14 +10,13 @@ import {
DraggableGroupingExtension,
ExtensionUtility,
GroupItemMetaProviderExtension,
- HeaderMenuExtension,
RowDetailViewExtension,
RowMoveManagerExtension,
RowSelectionExtension,
} from '../../extensions';
import { BackendUtilityService, ExtensionService, FilterService, PubSubService, SharedService, SortService } from '..';
import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
-import { AutoTooltipPlugin, HeaderButtonPlugin } from '../../plugins/index';
+import { AutoTooltipPlugin, HeaderButtonPlugin, HeaderMenuPlugin } from '../../plugins/index';
import { ColumnPickerControl, GridMenuControl } from '../../controls/index';
jest.mock('flatpickr', () => { });
@@ -44,6 +43,7 @@ const gridStub = {
registerPlugin: jest.fn(),
onBeforeDestroy: new Slick.Event(),
onBeforeHeaderCellDestroy: new Slick.Event(),
+ onBeforeSetColumns: new Slick.Event(),
onHeaderCellRendered: new Slick.Event(),
onSetOptions: new Slick.Event(),
onColumnsReordered: new Slick.Event(),
@@ -149,7 +149,6 @@ describe('ExtensionService', () => {
extensionContextMenuStub as unknown as ContextMenuExtension,
extensionStub as unknown as DraggableGroupingExtension,
extensionGroupItemMetaStub as unknown as GroupItemMetaProviderExtension,
- extensionHeaderMenuStub as unknown as HeaderMenuExtension,
extensionStub as unknown as RowDetailViewExtension,
extensionRowMoveStub as unknown as RowMoveManagerExtension,
extensionStub as unknown as RowSelectionExtension,
@@ -202,7 +201,7 @@ describe('ExtensionService', () => {
it('should return extension addon when method is called with a valid and instantiated addon', () => {
const instanceMock = { onColumnsChanged: jest.fn() };
- const extensionMock = { name: ExtensionName.columnPicker, instance: instanceMock as unknown, class: {} } as ExtensionModel;
+ const extensionMock = { name: ExtensionName.columnPicker, instance: instanceMock as unknown, class: instanceMock } as ExtensionModel;
const spy = jest.spyOn(service, 'getExtensionByName').mockReturnValue(extensionMock);
const output = service.getSlickgridAddonInstance(ExtensionName.columnPicker);
@@ -213,7 +212,7 @@ describe('ExtensionService', () => {
it('should return Row Detail extension addon when method is called with a valid and instantiated addon', () => {
const instanceMock = { onColumnsChanged: jest.fn() };
- const extensionMock = { name: ExtensionName.rowDetailView, instance: instanceMock as unknown, class: {} } as ExtensionModel;
+ const extensionMock = { name: ExtensionName.rowDetailView, instance: instanceMock as unknown, class: instanceMock } as ExtensionModel;
const spy = jest.spyOn(service, 'getExtensionByName').mockReturnValue(extensionMock);
const output = service.getSlickgridAddonInstance(ExtensionName.rowDetailView);
@@ -234,7 +233,7 @@ describe('ExtensionService', () => {
expect(gridSpy).toHaveBeenCalled();
expect(gridMenuInstance).toBeTruthy();
expect(output!.instance).toEqual(instance);
- expect(output).toEqual({ name: ExtensionName.gridMenu, instance: gridMenuInstance as unknown, class: {} } as ExtensionModel);
+ expect(output).toEqual({ name: ExtensionName.gridMenu, instance: gridMenuInstance as unknown, class: gridMenuInstance } as ExtensionModel);
});
});
@@ -273,9 +272,10 @@ describe('ExtensionService', () => {
service.bindDifferentExtensions();
const output = service.getExtensionByName(ExtensionName.autoTooltip);
+ const pluginInstance = service.getSlickgridAddonInstance(ExtensionName.autoTooltip);
expect(extSpy).toHaveBeenCalled();
- expect(output).toEqual({ name: ExtensionName.autoTooltip, instance: expect.anything(), class: {} } as ExtensionModel);
+ expect(output).toEqual({ name: ExtensionName.autoTooltip, instance: pluginInstance, class: pluginInstance } as ExtensionModel);
expect(output.instance instanceof AutoTooltipPlugin).toBeTrue();
});
@@ -286,9 +286,10 @@ describe('ExtensionService', () => {
service.bindDifferentExtensions();
const output = service.getExtensionByName(ExtensionName.columnPicker);
+ const pluginInstance = service.getSlickgridAddonInstance(ExtensionName.columnPicker);
expect(gridSpy).toHaveBeenCalled();
- expect(output).toEqual({ name: ExtensionName.columnPicker, instance: expect.anything(), class: {} } as ExtensionModel);
+ expect(output).toEqual({ name: ExtensionName.columnPicker, instance: pluginInstance, class: pluginInstance } as ExtensionModel);
expect(output.instance instanceof ColumnPickerControl).toBeTrue();
});
@@ -454,38 +455,36 @@ describe('ExtensionService', () => {
it('should register the HeaderButton addon when "enableHeaderButton" is set in the grid options', () => {
const onRegisteredMock = jest.fn();
- const gridOptionsMock = {
- enableHeaderButton: true,
- headerButton: {
- onExtensionRegistered: onRegisteredMock
- }
- } as GridOption;
+ const gridOptionsMock = { enableHeaderButton: true, headerButton: { onExtensionRegistered: onRegisteredMock } } as GridOption;
const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
service.bindDifferentExtensions();
- const headerButtonInstance = service.getSlickgridAddonInstance(ExtensionName.headerButton);
const output = service.getExtensionByName(ExtensionName.headerButton);
- const instance = service.getSlickgridAddonInstance(ExtensionName.headerButton);
+ const pluginInstance = service.getSlickgridAddonInstance(ExtensionName.headerButton);
expect(onRegisteredMock).toHaveBeenCalledWith(expect.toBeObject());
expect(output.instance instanceof HeaderButtonPlugin).toBeTrue();
expect(gridSpy).toHaveBeenCalled();
- expect(headerButtonInstance).toBeTruthy();
- expect(output!.instance).toEqual(instance);
- expect(output).toEqual({ name: ExtensionName.headerButton, instance: headerButtonInstance as unknown, class: {} } as ExtensionModel);
+ expect(pluginInstance).toBeTruthy();
+ expect(output!.instance).toEqual(pluginInstance);
+ expect(output).toEqual({ name: ExtensionName.headerButton, instance: pluginInstance, class: pluginInstance } as ExtensionModel);
});
it('should register the HeaderMenu addon when "enableHeaderMenu" is set in the grid options', () => {
- const gridOptionsMock = { enableHeaderMenu: true } as GridOption;
- const extSpy = jest.spyOn(extensionHeaderMenuStub, 'register').mockReturnValue(instanceMock);
+ const onRegisteredMock = jest.fn();
+ const gridOptionsMock = { enableHeaderMenu: true, headerMenu: { onExtensionRegistered: onRegisteredMock } } as GridOption;
const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
service.bindDifferentExtensions();
const output = service.getExtensionByName(ExtensionName.headerMenu);
+ const pluginInstance = service.getSlickgridAddonInstance(ExtensionName.headerMenu);
+ expect(onRegisteredMock).toHaveBeenCalledWith(expect.toBeObject());
+ expect(output.instance instanceof HeaderMenuPlugin).toBeTrue();
expect(gridSpy).toHaveBeenCalled();
- expect(extSpy).toHaveBeenCalled();
- expect(output).toEqual({ name: ExtensionName.headerMenu, instance: instanceMock as unknown as SlickHeaderMenu, class: extensionHeaderMenuStub } as ExtensionModel);
+ expect(pluginInstance).toBeTruthy();
+ expect(output!.instance).toEqual(pluginInstance);
+ expect(output).toEqual({ name: ExtensionName.headerMenu, instance: pluginInstance, class: pluginInstance } as ExtensionModel);
});
it('should register the ExcelCopyBuffer addon when "enableExcelCopyBuffer" is set in the grid options', () => {
@@ -657,17 +656,24 @@ describe('ExtensionService', () => {
service.bindDifferentExtensions();
service.renderColumnHeaders(columnsMock);
const gridMenuInstance = service.getSlickgridAddonInstance(ExtensionName.gridMenu);
- const extSpy = jest.spyOn(gridMenuInstance, 'translateGridMenu');
+ const translateSpy = jest.spyOn(gridMenuInstance, 'translateGridMenu');
service.translateGridMenu();
- expect(extSpy).toHaveBeenCalled();
+ expect(translateSpy).toHaveBeenCalled();
expect(gridMenuInstance.columns).toEqual(columnsMock);
});
it('should call the translateHeaderMenu method on the HeaderMenu Extension when service with same method name is called', () => {
- const extSpy = jest.spyOn(extensionHeaderMenuStub, 'translateHeaderMenu');
+ const gridOptionsMock = { enableHeaderMenu: true, headerMenu: {} } as GridOption;
+ jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
+ jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue([]);
+
+ service.bindDifferentExtensions();
+ const pluginInstance = service.getSlickgridAddonInstance(ExtensionName.headerMenu);
+ const translateSpy = jest.spyOn(pluginInstance, 'translateHeaderMenu');
service.translateHeaderMenu();
- expect(extSpy).toHaveBeenCalled();
+
+ expect(translateSpy).toHaveBeenCalled();
});
describe('translateColumnHeaders method', () => {
@@ -800,36 +806,6 @@ describe('ExtensionService', () => {
expect(setColumnsSpy).toHaveBeenCalledWith(columnsMock);
expect(service.getExtensionByName(ExtensionName.gridMenu).instance.columns).toEqual(columnsMock);
});
-
- it('should re-register the Header Menu when enable and method is called with new column definition collection provided as argument', () => {
- const instanceMock = { onColumnsChanged: jest.fn() };
- const extensionMock = { name: ExtensionName.headerMenu, addon: null, instance: null, class: null } as ExtensionModel;
- const expectedExtension = { name: ExtensionName.headerMenu, instance: instanceMock as unknown, class: null } as ExtensionModel;
- const gridOptionsMock = { enableHeaderMenu: true } as GridOption;
- const columnsMock = [
- { id: 'field1', field: 'field1', nameKey: 'HELLO' },
- { id: 'field2', field: 'field2', nameKey: 'WORLD' }
- ] as Column[];
-
- jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
- jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub);
- const spyGetExt = jest.spyOn(service, 'getExtensionByName').mockReturnValue(extensionMock);
- const spyGmDispose = jest.spyOn(extensionHeaderMenuStub, 'dispose');
- const spyGmRegister = jest.spyOn(extensionHeaderMenuStub, 'register').mockReturnValue(instanceMock);
- const spyAllCols = jest.spyOn(SharedService.prototype, 'allColumns', 'set');
- const setColumnsSpy = jest.spyOn(gridStub, 'setColumns');
-
- service.renderColumnHeaders(columnsMock);
-
- expect(expectedExtension).toEqual(expectedExtension);
- expect(spyGetExt).toHaveBeenCalled();
- expect(expectedExtension).toEqual(expectedExtension);
- expect(spyGetExt).toHaveBeenCalled();
- expect(spyGmDispose).toHaveBeenCalled();
- expect(spyGmRegister).toHaveBeenCalled();
- expect(spyAllCols).toHaveBeenCalledWith(columnsMock);
- expect(setColumnsSpy).toHaveBeenCalledWith(columnsMock);
- });
});
});
@@ -848,7 +824,6 @@ describe('ExtensionService', () => {
extensionStub as unknown as ContextMenuExtension,
extensionStub as unknown as DraggableGroupingExtension,
extensionGroupItemMetaStub as unknown as GroupItemMetaProviderExtension,
- extensionHeaderMenuStub as unknown as HeaderMenuExtension,
extensionStub as unknown as RowDetailViewExtension,
extensionStub as unknown as RowMoveManagerExtension,
extensionStub as unknown as RowSelectionExtension,
diff --git a/packages/common/src/services/__tests__/shared.service.spec.ts b/packages/common/src/services/__tests__/shared.service.spec.ts
index b0f36546b..661cbd014 100644
--- a/packages/common/src/services/__tests__/shared.service.spec.ts
+++ b/packages/common/src/services/__tests__/shared.service.spec.ts
@@ -216,6 +216,13 @@ describe('Shared Service', () => {
expect(service.frozenVisibleColumnId).toEqual('field1');
});
+ it('should call "gridContainerElement" GETTER and SETTER expect same value to be returned', () => {
+ const divMock = document.createElement('div');
+ divMock.className = 'some-class';
+ service.gridContainerElement = divMock;
+ expect(service.gridContainerElement).toEqual(divMock);
+ });
+
it('should call "hasColumnsReordered" GETTER and expect a boolean value to be returned', () => {
const flag = service.hasColumnsReordered;
expect(flag).toEqual(false);
diff --git a/packages/common/src/services/__tests__/utilities.spec.ts b/packages/common/src/services/__tests__/utilities.spec.ts
index 060fb2b98..dc95b0129 100644
--- a/packages/common/src/services/__tests__/utilities.spec.ts
+++ b/packages/common/src/services/__tests__/utilities.spec.ts
@@ -21,6 +21,7 @@ import {
findOrDefault,
formatNumber,
getDescendantProperty,
+ getElementOffsetRelativeToParent,
getHtmlElementOffset,
getTranslationPrefix,
htmlEncode,
@@ -588,6 +589,33 @@ describe('Service/Utilies', () => {
});
});
+ describe('getElementOffsetRelativeToParent method', () => {
+ const parentDiv = document.createElement('div');
+ const childDiv = document.createElement('div');
+ parentDiv.innerHTML = ``;
+ document.body.appendChild(parentDiv);
+
+ it('should return undefined when element if not a valid html element', () => {
+ const output = getElementOffsetRelativeToParent(null, null);
+ expect(output).toEqual(undefined);
+ });
+
+ it('should return top/left 0 when creating a new element in the document without positions', () => {
+ const output = getElementOffsetRelativeToParent(parentDiv, childDiv);
+ expect(output).toEqual({ top: 0, left: 0, bottom: 0, right: 0 });
+ });
+
+ it('should return same top/left positions as defined in the document/window', () => {
+ jest.spyOn(parentDiv, 'getBoundingClientRect').mockReturnValue({ top: 20, bottom: 33, left: 25, right: 44 } as any);
+ jest.spyOn(childDiv, 'getBoundingClientRect').mockReturnValue({ top: 130, bottom: 70, left: 250, right: 66 } as any);
+ parentDiv.style.top = '10px';
+ parentDiv.style.left = '25px';
+
+ const output = getElementOffsetRelativeToParent(parentDiv, childDiv);
+ expect(output).toEqual({ top: 110, left: 225, bottom: 37, right: 22 });
+ });
+ });
+
describe('getHtmlElementOffset method', () => {
const div = document.createElement('div');
div.innerHTML = ``;
diff --git a/packages/common/src/services/extension.service.ts b/packages/common/src/services/extension.service.ts
index d5dc874b2..0fbb7c516 100644
--- a/packages/common/src/services/extension.service.ts
+++ b/packages/common/src/services/extension.service.ts
@@ -13,14 +13,13 @@ import {
DraggableGroupingExtension,
ExtensionUtility,
GroupItemMetaProviderExtension,
- HeaderMenuExtension,
RowDetailViewExtension,
RowMoveManagerExtension,
RowSelectionExtension,
} from '../extensions/index';
import { SharedService } from './shared.service';
import { TranslaterService } from './translater.service';
-import { AutoTooltipPlugin, HeaderButtonPlugin } from '../plugins/index';
+import { AutoTooltipPlugin, HeaderButtonPlugin, HeaderMenuPlugin } from '../plugins/index';
import { ColumnPickerControl, GridMenuControl } from '../controls/index';
import { FilterService } from './filter.service';
import { PubSubService } from './pubSub.service';
@@ -35,7 +34,7 @@ interface ExtensionWithColumnIndexPosition {
export class ExtensionService {
protected _columnPickerControl?: ColumnPickerControl;
protected _gridMenuControl?: GridMenuControl;
- protected _headerButtonPlugin?: HeaderButtonPlugin;
+ protected _headerMenuPlugin?: HeaderMenuPlugin;
protected _extensionCreatedList: ExtensionList = {} as ExtensionList;
protected _extensionList: ExtensionList = {} as ExtensionList;
@@ -59,7 +58,6 @@ export class ExtensionService {
protected readonly contextMenuExtension: ContextMenuExtension,
protected readonly draggableGroupingExtension: DraggableGroupingExtension,
protected readonly groupItemMetaExtension: GroupItemMetaProviderExtension,
- protected readonly headerMenuExtension: HeaderMenuExtension,
protected readonly rowDetailViewExtension: RowDetailViewExtension,
protected readonly rowMoveManagerExtension: RowMoveManagerExtension,
protected readonly rowSelectionExtension: RowSelectionExtension,
@@ -141,7 +139,7 @@ export class ExtensionService {
const instance = new AutoTooltipPlugin(this.gridOptions?.autoTooltipOptions);
if (instance) {
this.sharedService.slickGrid.registerPlugin(instance);
- this._extensionList[ExtensionName.autoTooltip] = { name: ExtensionName.autoTooltip, class: {}, instance };
+ this._extensionList[ExtensionName.autoTooltip] = { name: ExtensionName.autoTooltip, class: instance, instance };
}
}
@@ -190,7 +188,7 @@ export class ExtensionService {
if (this.gridOptions.columnPicker?.onExtensionRegistered) {
this.gridOptions.columnPicker.onExtensionRegistered(this._columnPickerControl);
}
- this._extensionList[ExtensionName.columnPicker] = { name: ExtensionName.columnPicker, class: {}, instance: this._columnPickerControl };
+ this._extensionList[ExtensionName.columnPicker] = { name: ExtensionName.columnPicker, class: this._columnPickerControl, instance: this._columnPickerControl };
}
}
@@ -217,7 +215,7 @@ export class ExtensionService {
if (this.gridOptions.gridMenu?.onExtensionRegistered) {
this.gridOptions.gridMenu.onExtensionRegistered(this._gridMenuControl);
}
- this._extensionList[ExtensionName.gridMenu] = { name: ExtensionName.gridMenu, class: {}, instance: this._gridMenuControl };
+ this._extensionList[ExtensionName.gridMenu] = { name: ExtensionName.gridMenu, class: this._gridMenuControl, instance: this._gridMenuControl };
}
}
@@ -234,20 +232,23 @@ export class ExtensionService {
// Header Button Plugin
if (this.gridOptions.enableHeaderButton) {
- this._headerButtonPlugin = new HeaderButtonPlugin(this.pubSubService, this.sharedService);
- if (this._headerButtonPlugin) {
+ const headerButtonPlugin = new HeaderButtonPlugin(this.extensionUtility, this.pubSubService, this.sharedService);
+ if (headerButtonPlugin) {
if (this.gridOptions.headerButton?.onExtensionRegistered) {
- this.gridOptions.headerButton.onExtensionRegistered(this._headerButtonPlugin);
+ this.gridOptions.headerButton.onExtensionRegistered(headerButtonPlugin);
}
- this._extensionList[ExtensionName.headerButton] = { name: ExtensionName.headerButton, class: {}, instance: this._headerButtonPlugin };
+ this._extensionList[ExtensionName.headerButton] = { name: ExtensionName.headerButton, class: headerButtonPlugin, instance: headerButtonPlugin };
}
}
// Header Menu Plugin
- if (this.gridOptions.enableHeaderMenu && this.headerMenuExtension && this.headerMenuExtension.register) {
- const instance = this.headerMenuExtension.register();
- if (instance) {
- this._extensionList[ExtensionName.headerMenu] = { name: ExtensionName.headerMenu, class: this.headerMenuExtension, instance };
+ if (this.gridOptions.enableHeaderMenu) {
+ this._headerMenuPlugin = new HeaderMenuPlugin(this.extensionUtility, this.filterService, this.pubSubService, this.sharedService, this.sortService);
+ if (this._headerMenuPlugin) {
+ if (this.gridOptions.headerMenu?.onExtensionRegistered) {
+ this.gridOptions.headerMenu.onExtensionRegistered(this._headerMenuPlugin);
+ }
+ this._extensionList[ExtensionName.headerMenu] = { name: ExtensionName.headerMenu, class: this._headerMenuPlugin, instance: this._headerMenuPlugin };
}
}
@@ -320,7 +321,7 @@ export class ExtensionService {
/** Hide a column from the grid */
hideColumn(column: Column) {
- if (this.sharedService && this.sharedService.slickGrid && this.sharedService.slickGrid.getColumns && this.sharedService.slickGrid.setColumns) {
+ if (typeof this.sharedService?.slickGrid?.getColumns === 'function') {
const columnIndex = this.sharedService.slickGrid.getColumnIndex(column.id);
this.sharedService.visibleColumns = this.removeColumnByIndex(this.sharedService.slickGrid.getColumns(), columnIndex);
this.sharedService.slickGrid.setColumns(this.sharedService.visibleColumns);
@@ -386,9 +387,7 @@ export class ExtensionService {
* Translate the Header Menu titles, we need to loop through all column definition to re-translate them
*/
translateHeaderMenu() {
- if (this.headerMenuExtension && this.headerMenuExtension.translateHeaderMenu) {
- this.headerMenuExtension.translateHeaderMenu();
- }
+ this._headerMenuPlugin?.translateHeaderMenu?.();
}
/**
@@ -444,11 +443,6 @@ export class ExtensionService {
if (this._gridMenuControl) {
this._gridMenuControl.columns = this.sharedService.allColumns ?? [];
}
-
- // recreate the Header Menu when enabled
- if (this.gridOptions.enableHeaderMenu) {
- this.recreateExternalAddon(this.headerMenuExtension, ExtensionName.headerMenu);
- }
}
//
@@ -487,20 +481,6 @@ export class ExtensionService {
return undefined;
}
- /**
- * Dispose of previous extension/addon instance, then re-register it and don't forget to overwrite previous instance ref
- * @param externalExtension - extension instance
- * @param extensionName - extension name
- */
- protected recreateExternalAddon(externalExtension: Extension, extensionName: ExtensionName) {
- externalExtension.dispose();
- const instance = externalExtension.register();
- const extension = this.getExtensionByName(extensionName);
- if (extension) {
- extension.instance = instance;
- }
- }
-
/** 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/shared.service.ts b/packages/common/src/services/shared.service.ts
index e1f49b828..a4903b7d4 100644
--- a/packages/common/src/services/shared.service.ts
+++ b/packages/common/src/services/shared.service.ts
@@ -5,6 +5,7 @@ export class SharedService {
protected _dataView!: SlickDataView;
protected _groupItemMetadataProvider!: SlickGroupItemMetadataProvider;
protected _grid!: SlickGrid;
+ protected _gridContainerElm!: HTMLElement;
protected _gridOptions!: GridOption;
protected _hasColumnsReordered = false;
protected _currentPagination!: CurrentPagination;
@@ -77,6 +78,16 @@ export class SharedService {
this._grid = grid;
}
+ /** Getter for the Grid Options pulled through the Grid Object */
+ get gridContainerElement(): HTMLElement {
+ return this._gridContainerElm;
+ }
+
+ /** Setter for the Grid Options pulled through the Grid Object */
+ set gridContainerElement(gridContainerElm: HTMLElement) {
+ this._gridContainerElm = gridContainerElm;
+ }
+
/** Getter for the Grid Options pulled through the Grid Object */
get gridOptions(): GridOption {
return this._gridOptions || this._grid?.getOptions && this._grid.getOptions() || {};
diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts
index dee34b5fa..859e4d7a8 100644
--- a/packages/common/src/services/utilities.ts
+++ b/packages/common/src/services/utilities.ts
@@ -6,7 +6,7 @@ const moment = (moment_ as any)['default'] || moment_; // patch to fix rollup "m
import { Constants } from '../constants';
import { FieldType, OperatorString, OperatorType } from '../enums/index';
-import { EventSubscription, GridOption } from '../interfaces/index';
+import { EventSubscription, GridOption, HtmlElementPosition } from '../interfaces/index';
import { Observable, RxJsFacade, Subject, Subscription } from './rxjsFacade';
/**
@@ -1017,8 +1017,23 @@ export function findOrDefault(array: T[], logic: (item: T) => boolean,
return array;
}
-/** Get HTML Element position offset (without jQuery) */
-export function getHtmlElementOffset(element: HTMLElement): { top: number; bottom: number; left: number; right: number; } | undefined {
+/** Get offset of HTML element relative to a parent element */
+export function getElementOffsetRelativeToParent(parentElm: HTMLElement | null, childElm: HTMLElement | null) {
+ if (!parentElm || !childElm) {
+ return undefined;
+ }
+ const parentPos = parentElm.getBoundingClientRect();
+ const childPos = childElm.getBoundingClientRect();
+ return {
+ top: childPos.top - parentPos.top,
+ right: childPos.right - parentPos.right,
+ bottom: childPos.bottom - parentPos.bottom,
+ left: childPos.left - parentPos.left,
+ };
+}
+
+/** Get HTML element offset with pure JS */
+export function getHtmlElementOffset(element: HTMLElement): HtmlElementPosition | undefined {
if (!element) {
return undefined;
}
@@ -1028,7 +1043,7 @@ export function getHtmlElementOffset(element: HTMLElement): { top: number; botto
let bottom = 0;
let right = 0;
- if (rect && rect.top !== undefined && rect.left !== undefined) {
+ if (rect?.top !== undefined && rect.left !== undefined) {
top = rect.top + window.pageYOffset;
left = rect.left + window.pageXOffset;
right = rect.right;
diff --git a/packages/common/src/styles/_variables-theme-material.scss b/packages/common/src/styles/_variables-theme-material.scss
index da7b7cb64..a5aaf1721 100644
--- a/packages/common/src/styles/_variables-theme-material.scss
+++ b/packages/common/src/styles/_variables-theme-material.scss
@@ -97,7 +97,7 @@ $header-button-hidden-margin-right: -6px !default;
$header-button-height: 18px !default;
$header-button-width: 18px !default;
$header-button-margin: -7px 0 100px 0 !default;
-$header-menu-button-height: 20px !default;
+$header-menu-button-height: 25px !default;
$header-menu-button-icon-font-size: 24px !default;
$header-menu-button-icon-width: 24px !default;
$header-menu-button-icon-color: $icon-color !default;
diff --git a/packages/common/src/styles/_variables-theme-salesforce.scss b/packages/common/src/styles/_variables-theme-salesforce.scss
index 68744ffa0..a404a3e76 100644
--- a/packages/common/src/styles/_variables-theme-salesforce.scss
+++ b/packages/common/src/styles/_variables-theme-salesforce.scss
@@ -117,7 +117,7 @@ $header-button-hidden-margin-right: -6px !default;
$header-button-height: 18px !default;
$header-button-width: 18px !default;
$header-button-margin: -7px 0 100px 0 !default;
-$header-menu-button-height: 20px !default;
+$header-menu-button-height: 25px !default;
$header-menu-button-icon-font-size: 26px !default;
$header-menu-button-icon-color: #706e6b !default;
$header-menu-button-icon: url('data:image/svg+xml,') !default;
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 7ab9e1f4b..649dbf68d 100644
--- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts
+++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts
@@ -39,7 +39,6 @@ import {
DraggableGroupingExtension,
ExtensionUtility,
GroupItemMetaProviderExtension,
- HeaderMenuExtension,
RowDetailViewExtension,
RowSelectionExtension,
@@ -362,7 +361,6 @@ export class SlickVanillaGridBundle {
const checkboxExtension = new CheckboxSelectorExtension(this.sharedService);
const draggableGroupingExtension = new DraggableGroupingExtension(this.extensionUtility, this.sharedService);
const groupItemMetaProviderExtension = new GroupItemMetaProviderExtension(this.sharedService);
- const headerMenuExtension = new HeaderMenuExtension(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.translaterService);
const rowDetailViewExtension = new RowDetailViewExtension();
const rowMoveManagerExtension = new RowMoveManagerExtension(this.sharedService);
const rowSelectionExtension = new RowSelectionExtension(this.sharedService);
@@ -378,7 +376,6 @@ export class SlickVanillaGridBundle {
contextMenuExtension,
draggableGroupingExtension,
groupItemMetaProviderExtension,
- headerMenuExtension,
rowDetailViewExtension,
rowMoveManagerExtension,
rowSelectionExtension,
@@ -552,6 +549,7 @@ export class SlickVanillaGridBundle {
this.slickGrid = new Slick.Grid(gridContainerElm, this.dataView, this._columnDefinitions, this._gridOptions);
this.sharedService.dataView = this.dataView;
this.sharedService.slickGrid = this.slickGrid;
+ this.sharedService.gridContainerElement = this._gridContainerElm;
this.extensionService.bindDifferentExtensions();
this.bindDifferentHooks(this.slickGrid, this._gridOptions, this.dataView);
diff --git a/test/cypress/integration/example07.spec.js b/test/cypress/integration/example07.spec.js
index 8d6172d37..b5ac5aa25 100644
--- a/test/cypress/integration/example07.spec.js
+++ b/test/cypress/integration/example07.spec.js
@@ -201,6 +201,9 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries
.children()
.each(($child, index) => expect($child.text()).to.eq(updatedTitles[index]));
+ cy.get('.slick-header-menubutton')
+ .should('have.length', 9);
+
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`).should('contain', 'Task 0');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(9)`).should('contain', 'Task 0');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(10)`).should('contain', 'Task 0');
@@ -388,12 +391,14 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries
it('should be able to toggle Sorting functionality (disable) and expect all header menu Sorting commands to be hidden and also not show Sort hint while hovering a column', () => {
const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column'];
+ cy.get('[data-test="toggle-sorting-btn"] .mdi-toggle-switch').should('exist');
cy.get('.slick-sort-indicator').should('have.length.greaterThan', 0); // sort icon hints
cy.get('[data-test="toggle-sorting-btn"]').click(); // disable it
cy.get('.slick-sort-indicator').should('have.length', 0);
+ cy.get('[data-test="toggle-sorting-btn"] .mdi-toggle-switch-off-outline').should('exist');
cy.get('.grid7')
- .find('.slick-header-column:nth(5)')
+ .find('.slick-header-column:nth(8)')
.trigger('mouseover')
.children('.slick-header-menubutton')
.click();
@@ -432,15 +437,17 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries
});
});
- it('should be able to toggle Sorting functionality (re-enable) and expect all Sorting header menu commands to be hidden and also not show Sort hint while hovering a column', () => {
+ it('should be able to toggle Sorting functionality (re-enable) and expect all Sorting header menu commands to be visible and also Sort hints to show up also', () => {
const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column'];
+ cy.get('[data-test="toggle-sorting-btn"] .mdi-toggle-switch-off-outline').should('exist');
cy.get('.slick-sort-indicator').should('have.length', 0); // sort icon hints
cy.get('[data-test="toggle-sorting-btn"]').click(); // enable it back
+ cy.get('[data-test="toggle-sorting-btn"] .mdi-toggle-switch').should('exist');
cy.get('.slick-sort-indicator').should('have.length.greaterThan', 0);
cy.get('.grid7')
- .find('.slick-header-column:nth(5)')
+ .find('.slick-header-column:nth(8)')
.trigger('mouseover')
.children('.slick-header-menubutton')
.click();
@@ -454,7 +461,7 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries
});
});
- it('should expect "Clear Sorting" command to be hidden in the Grid Menu', () => {
+ it('should expect "Clear Sorting" command to be visible again in the Grid Menu', () => {
const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel'];
cy.get('.grid7')
@@ -478,9 +485,11 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries
it('should be able to click disable Sorting functionality button and expect all Sorting commands to be hidden and also not show Sort hint while hovering a column', () => {
const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column'];
+ cy.get('[data-test="toggle-sorting-btn"] .mdi-toggle-switch').should('exist');
cy.get('.slick-sort-indicator').should('have.length.greaterThan', 0); // sort icon hints
cy.get('[data-test="disable-sorting-btn"]').click().click(); // even clicking twice should have same result
cy.get('.slick-sort-indicator').should('have.length', 0);
+ cy.get('[data-test="toggle-sorting-btn"] .mdi-toggle-switch-off-outline').should('exist');
cy.get('.grid7')
.find('.slick-header-column:nth(5)')
@@ -769,6 +778,28 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries
.contains('2 de 501 éléments');
});
+ it('should re-open Header Menu of last "Titre" column and expect all commands to be translated to French', () => {
+ const expectedFullHeaderMenuCommands = ['Redimensionner par contenu', '', 'Trier par ordre croissant', 'Trier par ordre décroissant', '', 'Supprimer le filtre', 'Supprimer le tri', 'Cacher la colonne'];
+
+ cy.get('.grid7')
+ .find('.slick-header-column:nth(8)')
+ .trigger('mouseover')
+ .children('.slick-header-menubutton')
+ .click();
+
+ cy.get('.slick-header-menu')
+ .children()
+ .each(($child, index) => {
+ const commandTitle = $child.text();
+ expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]);
+
+ // expect all Sorting commands to be hidden
+ if (commandTitle === 'Trier par ordre croissant' || commandTitle === 'Trier par ordre décroissant' || commandTitle === 'Supprimer le tri') {
+ expect($child).not.to.be.visible;
+ }
+ });
+ });
+
it('should open Grid Menu and expect new columns to be added to the column picker section, also "Duration" to be unchecked while "Finish" to be at new position', () => {
const updatedTitles = ['', '', 'Titre', 'Durée', '% Achevée', 'Fin', 'Terminé', 'Début', 'Prerequisites', 'Titre'];
@@ -794,5 +825,9 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries
}
});
});
+
+ cy.get('.slick-gridmenu')
+ .find('span.close')
+ .click();
});
});