Skip to content

Commit

Permalink
feat(grouping): add onPreHeaderContextMenu for Column Picker usage (#…
Browse files Browse the repository at this point in the history
…1580)

* feat(grouping): add onPreHeaderContextMenu for Column Picker usage
- adding new events `onPreHeaderContextMenu` and `onPreHeaderClick` which is mostly to be able to open the Column Picker from either the regular column headers OR the Column Group from the Pre-Header when defined
  • Loading branch information
ghiscoding authored Jun 20, 2024
1 parent 80b682a commit c742a83
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 28 deletions.
3 changes: 1 addition & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import globals from 'globals';
import jest from 'eslint-plugin-jest';
import n from 'eslint-plugin-n';
import tseslint from 'typescript-eslint';
import tsParser from '@typescript-eslint/parser';

export default tseslint.config(
eslint.configs.recommended,
Expand Down Expand Up @@ -44,7 +43,7 @@ export default tseslint.config(
...globals.es2021,
...globals.node,
},
parser: tsParser,
parser: tseslint.parser,
parserOptions: {
project: ['./tsconfig.base.json']
}
Expand Down
4 changes: 2 additions & 2 deletions packages/binding/src/bindingEvent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class BindingEventService {

/** Bind an event listener to any element */
bind(
elementOrElements: Document | Element | NodeListOf<Element> | Window,
elementOrElements: Document | Element | NodeListOf<Element> | Array<Element> | Window,
eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject,
listenerOptions?: boolean | AddEventListenerOptions,
groupName = ''
Expand All @@ -35,7 +35,7 @@ export class BindingEventService {
this._boundedEvents.push({ element, eventName, listener, groupName });
}
});
} else {
} else if (elementOrElements) {
// single elements to bind to
for (const eventName of eventNames) {
(elementOrElements as Element).addEventListener(eventName, listener, listenerOptions);
Expand Down
31 changes: 29 additions & 2 deletions packages/common/src/core/__tests__/slickGrid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5141,9 +5141,36 @@ describe('SlickGrid core file', () => {
});
});

describe('Header Click', () => {
// TODO: need to add another test when "columnResizeDragging"
describe('Pre-Header Click', () => {
it('should trigger onPreHeaderClick notify when not column resizing', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age', editorClass: InputEditor }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true, editable: true, createPreHeaderPanel: true, showPreHeaderPanel: true });
jest.spyOn(grid, 'getCellFromEvent').mockReturnValue(null);
const onPreHeaderClickSpy = jest.spyOn(grid.onPreHeaderClick, 'notify');
const preHeaderElms = container.querySelectorAll('.slick-preheader-panel');
const event = new CustomEvent('click');
Object.defineProperty(event, 'target', { writable: true, value: preHeaderElms[0] });
preHeaderElms[0].dispatchEvent(event);

expect(onPreHeaderClickSpy).toHaveBeenCalledWith({ node: preHeaderElms[0], grid }, expect.anything(), grid);
});
});

describe('Pre-Header Context Menu', () => {
it('should trigger onHeaderClick notify grid context menu event is triggered', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age', editorClass: InputEditor }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true, editable: true, createPreHeaderPanel: true, showPreHeaderPanel: true });
const onContextSpy = jest.spyOn(grid.onPreHeaderContextMenu, 'notify');
const preHeaderElms = container.querySelectorAll('.slick-preheader-panel');
const event = new CustomEvent('contextmenu');
Object.defineProperty(event, 'target', { writable: true, value: preHeaderElms[0] });
preHeaderElms[0].dispatchEvent(event);

expect(onContextSpy).toHaveBeenCalledWith({ node: preHeaderElms[0], grid }, expect.anything(), grid);
});
});

describe('Header Click', () => {
it('should trigger onHeaderClick notify when not column resizing', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age', editorClass: InputEditor }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true, editable: true });
Expand Down
28 changes: 22 additions & 6 deletions packages/common/src/core/slickGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ import type {
OnHeaderMouseEventArgs,
OnHeaderRowCellRenderedEventArgs,
OnKeyDownEventArgs,
OnPreHeaderClickEventArgs,
OnPreHeaderContextMenuEventArgs,
OnRenderedEventArgs,
OnScrollEventArgs,
OnSelectedRowsChangedEventArgs,
Expand Down Expand Up @@ -175,6 +177,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
onKeyDown: SlickEvent<OnKeyDownEventArgs>;
onMouseEnter: SlickEvent<OnHeaderMouseEventArgs>;
onMouseLeave: SlickEvent<OnHeaderMouseEventArgs>;
onPreHeaderClick: SlickEvent<OnPreHeaderClickEventArgs>;
onPreHeaderContextMenu: SlickEvent<OnPreHeaderContextMenuEventArgs>;
onRendered: SlickEvent<OnRenderedEventArgs>;
onScroll: SlickEvent<OnScrollEventArgs>;
onSelectedRowsChanged: SlickEvent<OnSelectedRowsChangedEventArgs>;
Expand Down Expand Up @@ -548,6 +552,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
this.onKeyDown = new SlickEvent<OnKeyDownEventArgs>('onKeyDown', externalPubSub);
this.onMouseEnter = new SlickEvent<OnHeaderMouseEventArgs>('onMouseEnter', externalPubSub);
this.onMouseLeave = new SlickEvent<OnHeaderMouseEventArgs>('onMouseLeave', externalPubSub);
this.onPreHeaderClick = new SlickEvent<OnPreHeaderClickEventArgs>('onPreHeaderClick', externalPubSub);
this.onPreHeaderContextMenu = new SlickEvent<OnPreHeaderContextMenuEventArgs>('onPreHeaderContextMenu', externalPubSub);
this.onRendered = new SlickEvent<OnRenderedEventArgs>('onRendered', externalPubSub);
this.onScroll = new SlickEvent<OnScrollEventArgs>('onScroll', externalPubSub);
this.onSelectedRowsChanged = new SlickEvent<OnSelectedRowsChangedEventArgs>('onSelectedRowsChanged', externalPubSub);
Expand Down Expand Up @@ -913,6 +919,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e

if (this._options.createPreHeaderPanel) {
this._bindingEventService.bind(this._preHeaderPanelScroller, 'scroll', this.handlePreHeaderPanelScroll.bind(this) as EventListener);
this._bindingEventService.bind([this._preHeaderPanelScroller, this._preHeaderPanelScrollerR], 'contextmenu', this.handlePreHeaderContextMenu.bind(this) as EventListener);
this._bindingEventService.bind([this._preHeaderPanelScroller, this._preHeaderPanelScrollerR], 'click', this.handlePreHeaderClick.bind(this) as EventListener);
}

this._bindingEventService.bind(this._focusSink, 'keydown', this.handleKeyDown.bind(this) as EventListener);
Expand Down Expand Up @@ -5027,14 +5035,22 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
}

protected handleHeaderClick(e: MouseEvent & { target: HTMLElement; }): void {
if (this.columnResizeDragging) {
return;
if (!this.columnResizeDragging) {
const header = e.target.closest('.slick-header-column');
const column = header && Utils.storage.get(header, 'column');
if (column) {
this.triggerEvent(this.onHeaderClick, { column }, e);
}
}
}

const header = e.target.closest('.slick-header-column');
const column = header && Utils.storage.get(header, 'column');
if (column) {
this.triggerEvent(this.onHeaderClick, { column }, e);
protected handlePreHeaderContextMenu(e: MouseEvent & { target: HTMLElement; }): void {
this.triggerEvent(this.onPreHeaderContextMenu, { node: e.target }, e);
}

protected handlePreHeaderClick(e: MouseEvent & { target: HTMLElement; }): void {
if (!this.columnResizeDragging) {
this.triggerEvent(this.onPreHeaderClick, { node: e.target }, e);
}
}

Expand Down
45 changes: 36 additions & 9 deletions packages/common/src/extensions/__tests__/slickColumnPicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ExtensionUtility } from '../extensionUtility';
import { SharedService } from '../../services/shared.service';
import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
import { BackendUtilityService } from '../../services/backendUtility.service';
import { createDomElement } from '@slickgrid-universal/utils';

const gridUid = 'slickgrid_124343';

Expand All @@ -24,6 +25,8 @@ const gridStub = {
onClick: new SlickEvent(),
onColumnsReordered: new SlickEvent(),
onHeaderContextMenu: new SlickEvent(),
onPreHeaderClick: new SlickEvent(),
onPreHeaderContextMenu: new SlickEvent(),
} as unknown as SlickGrid;

const pubSubServiceStub = {
Expand Down Expand Up @@ -148,7 +151,7 @@ describe('ColumnPickerControl', () => {
gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData as any, gridStub);
control.menuElement!.querySelector('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));

expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
Expand All @@ -172,13 +175,37 @@ describe('ColumnPickerControl', () => {
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const liElmList = control.menuElement!.querySelectorAll<HTMLLIElement>('li');

expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
expect(liElmList[2].textContent).toBe('Billing - Field 3');
});

it('should open the column picker via "onPreHeaderContextMenu" and expect "Forcefit" to be checked when "hideForceFitButton" is false', () => {
const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe');
jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined as any).mockReturnValue(1);

gridOptionsMock.columnPicker!.hideForceFitButton = false;
gridOptionsMock.forceFitColumns = true;
control.columns = columnsMock;
control.init();

const groupElm = createDomElement('div', { className: 'slick-column-name' });
gridStub.onPreHeaderContextMenu.notify({ node: groupElm, grid: gridStub }, { ...new SlickEventData(), preventDefault: jest.fn(), target: groupElm } as any, gridStub);
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const inputForcefitElm = control.menuElement!.querySelector('#slickgrid_124343-colpicker-forcefit') as HTMLInputElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;

expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(control.menuElement?.style.display).not.toBe('none');
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
expect(inputForcefitElm.checked).toBeTruthy();
expect(inputForcefitElm.dataset.option).toBe('autoresize');
expect(labelSyncElm.textContent).toBe('Force fit columns');
});

it('should open the column picker via "onHeaderContextMenu" and expect "Forcefit" to be checked when "hideForceFitButton" is false', () => {
const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe');
jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined as any).mockReturnValue(1);
Expand All @@ -193,7 +220,7 @@ describe('ColumnPickerControl', () => {
const inputForcefitElm = control.menuElement!.querySelector('#slickgrid_124343-colpicker-forcefit') as HTMLInputElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;

expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
expect(inputForcefitElm.checked).toBeTruthy();
Expand All @@ -215,7 +242,7 @@ describe('ColumnPickerControl', () => {
const inputSyncElm = control.menuElement!.querySelector('#slickgrid_124343-colpicker-syncresize') as HTMLInputElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;

expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
expect(inputSyncElm.checked).toBeTruthy();
Expand Down Expand Up @@ -244,7 +271,7 @@ describe('ColumnPickerControl', () => {
visibleColumns: columnsMock,
grid: gridStub,
};
expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
expect(onColChangedMock).toBeCalledWith(expect.anything(), expectedCallbackArgs);
Expand All @@ -268,7 +295,7 @@ describe('ColumnPickerControl', () => {
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;
inputForcefitElm.dispatchEvent(new Event('click', { bubbles: true }));

expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(inputForcefitElm.checked).toBeTruthy();
expect(inputForcefitElm.dataset.option).toBe('autoresize');
Expand All @@ -294,7 +321,7 @@ describe('ColumnPickerControl', () => {
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;
inputSyncElm.dispatchEvent(new Event('click', { bubbles: true }));

expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(inputSyncElm.checked).toBeTruthy();
expect(inputSyncElm.dataset.option).toBe('syncresize');
Expand Down Expand Up @@ -340,7 +367,7 @@ describe('ColumnPickerControl', () => {
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const col4 = control.menuElement!.querySelector<HTMLInputElement>('li.hidden input[data-columnid=field4]');

expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
expect(control.columns).toEqual(columnsMock);
Expand Down Expand Up @@ -368,7 +395,7 @@ describe('ColumnPickerControl', () => {
const labelForcefitElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;

expect(handlerSpy).toHaveBeenCalledTimes(3);
expect(handlerSpy).toHaveBeenCalledTimes(4);
expect(labelForcefitElm.textContent).toBe('Ajustement forcé des colonnes');
expect(labelSyncElm.textContent).toBe('Redimension synchrone');
expect(utilitySpy).toHaveBeenCalled();
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/extensions/slickColumnPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ export class SlickColumnPicker {
this.addonOptions.forceFitTitle = this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'columnPicker');
this.addonOptions.syncResizeTitle = this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'columnPicker');

this._eventHandler.subscribe(this.grid.onPreHeaderContextMenu, (e) => {
if (['slick-column-name', 'slick-header-column'].some(className => e.target?.classList.contains(className))) {
this.handleHeaderContextMenu(e); // open picker only when preheader has column groups
}
});
this._eventHandler.subscribe(this.grid.onHeaderContextMenu, this.handleHeaderContextMenu.bind(this));
this._eventHandler.subscribe(this.grid.onColumnsReordered, updateColumnPickerOrder.bind(this));
this._eventHandler.subscribe(this.grid.onClick, this.disposeMenu.bind(this));
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/interfaces/gridEvents.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface OnHeaderContextMenuEventArgs extends SlickGridArg { column: Col
export interface OnHeaderMouseEventArgs extends SlickGridArg { column: Column; }
export interface OnHeaderRowCellRenderedEventArgs extends SlickGridArg { node: HTMLDivElement; column: Column; }
export interface OnKeyDownEventArgs extends SlickGridArg { row: number; cell: number; }
export interface OnPreHeaderClickEventArgs extends SlickGridArg { node: HTMLElement; }
export interface OnPreHeaderContextMenuEventArgs extends SlickGridArg { node: HTMLElement; }
export interface OnValidationErrorEventArgs extends SlickGridArg { row: number; cell: number; validationResults: EditorValidationResult; column: Column; editor: Editor; cellNode: HTMLDivElement; }
export interface OnRenderedEventArgs extends SlickGridArg { startRow: number; endRow: number; }
export interface OnSelectedRowsChangedEventArgs extends SlickGridArg { rows: number[]; previousSelectedRows: number[]; changedSelectedRows: number[]; changedUnselectedRows: number[]; caller: string; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ const gridStub = {
onHeaderCellRendered: new SlickEvent(),
onHeaderClick: new SlickEvent(),
onHeaderContextMenu: new SlickEvent(),
onPreHeaderClick: new SlickEvent(),
onPreHeaderContextMenu: new SlickEvent(),
onKeyDown: new SlickEvent(),
onSelectedRowsChanged: new SlickEvent(),
onScroll: new SlickEvent(),
Expand Down
6 changes: 2 additions & 4 deletions packages/common/src/services/groupingAndColspan.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,7 @@ export class GroupingAndColspanService {
preHeaderPanel.className = 'slick-header-columns';
preHeaderPanel.style.left = '-1000px';
preHeaderPanel.style.width = `${this._grid.getHeadersWidth()}px`;

if (preHeaderPanel.parentElement) {
preHeaderPanel.parentElement.classList.add('slick-header');
}
preHeaderPanel.parentElement?.classList.add('slick-header');

const headerColumnWidthDiff = this._grid.getHeaderColumnWidthDiff();

Expand All @@ -147,6 +144,7 @@ export class GroupingAndColspanService {
widthTotal = colDef.width || 0;
headerElm = createDomElement('div', {
className: `slick-state-default slick-header-column ${isFrozenGrid ? 'frozen' : ''}`,
dataset: { group: colDef.columnGroup },
style: { width: `${widthTotal - headerColumnWidthDiff}px` }
});

Expand Down
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c742a83

Please sign in to comment.