slickgrid
Version:
A lightning fast JavaScript grid/spreadsheet
247 lines (215 loc) • 9.4 kB
text/typescript
import type {
Column,
DOMEvent,
HeaderButtonItem,
HeaderButtonOnCommandArgs,
HeaderButtonOption,
OnHeaderCellRenderedEventArgs,
SlickPlugin
} from '../models/index.js';
import { BindingEventService as BindingEventService_, EventHandler as EventHandler_, SlickEvent as SlickEvent_, type SlickEventData, Utils as Utils_ } from '../slick.core.js';
import type { SlickGrid } from '../slick.grid.js';
// for (iife) load Slick methods from global Slick object, or use imports for (esm)
const BindingEventService = IIFE_ONLY ? Slick.BindingEventService : BindingEventService_;
const SlickEvent = IIFE_ONLY ? Slick.Event : SlickEvent_;
const EventHandler = IIFE_ONLY ? Slick.EventHandler : EventHandler_;
const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
/***
* A plugin to add custom buttons to column headers.
*
* USAGE:
*
* Add the plugin .js & .css files and register it with the grid.
*
* To specify a custom button in a column header, extend the column definition like so:
*
* let columns = [
* {
* id: 'myColumn',
* name: 'My column',
*
* // This is the relevant part
* header: {
* buttons: [
* {
* // button options
* },
* {
* // button options
* }
* ]
* }
* }
* ];
*
* Available button options:
* cssClass: CSS class to add to the button.
* image: Relative button image path.
* disabled: Whether the item is disabled.
* tooltip: Button tooltip.
* showOnHover: Only show the button on hover.
* handler: Button click handler.
* command: A command identifier to be passed to the onCommand event handlers.
*
* Available menu item options:
* action: Optionally define a callback function that gets executed when item is chosen (and/or use the onCommand event)
* command: A command identifier to be passed to the onCommand event handlers.
* cssClass: CSS class to add to the button.
* handler: Button click handler.
* image: Relative button image path.
* showOnHover: Only show the button on hover.
* tooltip: Button tooltip.
* itemVisibilityOverride: Callback method that user can override the default behavior of showing/hiding an item from the list
* itemUsabilityOverride: Callback method that user can override the default behavior of enabling/disabling an item from the list
*
* The plugin exposes the following events:
* onCommand: Fired on button click for buttons with 'command' specified.
* Event args:
* grid: Reference to the grid.
* column: Column definition.
* command: Button command identified.
* button: Button options. Note that you can change the button options in your
* event handler, and the column header will be automatically updated to
* reflect them. This is useful if you want to implement something like a
* toggle button.
*
*
* @param options {Object} Options:
* buttonCssClass: a CSS class to use for buttons (default 'slick-header-button')
* @class Slick.Plugins.HeaderButtons
* @constructor
*/
export class SlickHeaderButtons implements SlickPlugin {
// --
// public API
pluginName = 'HeaderButtons' as const;
onCommand = new SlickEvent<HeaderButtonOnCommandArgs>('onCommand');
// --
// protected props
protected _grid!: SlickGrid;
protected _handler = new EventHandler();
protected _bindingEventService = new BindingEventService();
protected _defaults: HeaderButtonOption = {
buttonCssClass: 'slick-header-button'
};
protected _options: HeaderButtonOption;
constructor(options: Partial<HeaderButtonOption>) {
this._options = Utils.extend(true, {}, this._defaults, options);
}
init(grid: SlickGrid) {
this._grid = grid;
Utils.addSlickEventPubSubWhenDefined(grid.getPubSubService(), this);
this._handler
.subscribe(this._grid.onHeaderCellRendered, this.handleHeaderCellRendered.bind(this))
.subscribe(this._grid.onBeforeHeaderCellDestroy, this.handleBeforeHeaderCellDestroy.bind(this));
// Force the grid to re-render the header now that the events are hooked up.
this._grid.setColumns(this._grid.getColumns());
}
destroy() {
this._handler.unsubscribeAll();
this._bindingEventService.unbindAll();
}
protected handleHeaderCellRendered(_e: SlickEventData, args: OnHeaderCellRenderedEventArgs) {
const column = args.column;
if (column.header?.buttons) {
// Append buttons in reverse order since they are floated to the right.
let i = column.header.buttons.length;
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<typeof args>(button.itemVisibilityOverride, args);
const isItemUsable = this.runOverrideFunctionWhenExists<typeof args>(button.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 (Object.prototype.hasOwnProperty.call(button, 'itemUsabilityOverride')) {
button.disabled = isItemUsable ? false : true;
}
const btn = document.createElement('div');
btn.className = this._options.buttonCssClass || '';
btn.ariaLabel = 'Header Button';
btn.role = 'button';
if (button.disabled) {
btn.classList.add('slick-header-button-disabled');
}
if (button.showOnHover) {
btn.classList.add('slick-header-button-hidden');
}
if (button.image) {
btn.style.backgroundImage = `url(${button.image})`;
}
if (button.cssClass) {
btn.classList.add(...Utils.classNameToList(button.cssClass));
}
if (button.tooltip) {
btn.title = button.tooltip;
}
if (button.handler && !button.disabled) {
this._bindingEventService.bind(btn, 'click', button.handler);
}
this._bindingEventService.bind(btn, 'click', this.handleButtonClick.bind(this, button, args.column) as EventListener);
args.node.appendChild(btn);
}
}
}
protected handleBeforeHeaderCellDestroy(_e: SlickEventData, args: { column: Column; node: HTMLElement; }) {
const column = args.column;
if (column.header?.buttons) {
// Removing buttons via jQuery 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.
const buttonCssClass = (this._options.buttonCssClass || '').replace(/(\s+)/g, '.');
if (buttonCssClass) {
args.node.querySelectorAll(`.${buttonCssClass}`).forEach(elm => elm.remove());
}
}
}
protected handleButtonClick(button: HeaderButtonItem, columnDef: Column, e: DOMEvent<HTMLDivElement>) {
const command = button.command || '';
const callbackArgs = {
grid: this._grid,
column: columnDef,
button
} as HeaderButtonOnCommandArgs;
if (command) {
callbackArgs.command = command;
}
// execute action callback when defined
if (typeof button.action === 'function') {
button.action.call(this, e, callbackArgs);
}
if (command && !button.disabled) {
this.onCommand.notify(callbackArgs, e, this);
// Update the header in case the user updated the button definition in the handler.
this._grid.updateColumnHeader(columnDef.id);
}
// Stop propagation so that it doesn't register as a header click event.
e.preventDefault();
e.stopPropagation();
}
/**
* Method that user can pass to override the default behavior.
* In order word, user can choose or an item is (usable/visible/enable) by providing his own logic.
* @param overrideFn: override function callback
* @param args: multiple arguments provided to the override (cell, row, columnDef, dataContext, grid)
*/
protected runOverrideFunctionWhenExists<T = any>(overrideFn: ((args: any) => boolean) | undefined, args: T): boolean {
if (typeof overrideFn === 'function') {
return overrideFn.call(this, args);
}
return true;
}
}
// extend Slick namespace on window object when building as iife
if (IIFE_ONLY && window.Slick) {
Utils.extend(true, window, {
Slick: {
Plugins: {
HeaderButtons: SlickHeaderButtons
}
}
});
}