UNPKG

@bokeh/slickgrid

Version:

A lightning fast JavaScript grid/spreadsheet

384 lines (321 loc) 12.9 kB
var $ = require("../slick.jquery"); var Slick = require("../slick.core"); /*** * A plugin to add drop-down menus to column headers. * * USAGE: * * Add the plugin .js & .css files and register it with the grid. * * To specify a menu in a column header, extend the column definition like so: * * var columns = [ * { * id: 'myColumn', * name: 'My column', * * // This is the relevant part * header: { * menu: { * items: [ * { * // menu item options * }, * { * // menu item options * } * ] * } * } * } * ]; * * * Available menu options: * autoAlign: Auto-align drop menu to the left when not enough viewport space to show on the right * autoAlignOffset: When drop menu is aligned to the left, it might not be perfectly aligned with the header menu icon, if that is the case you can add an offset (positive/negative number to move right/left) * buttonCssClass: an extra CSS class to add to the menu button * buttonImage: a url to the menu button image (default '../images/down.gif') * menuUsabilityOverride: Callback method that user can override the default behavior of enabling/disabling the menu from being usable (must be combined with a custom formatter) * minWidth: Minimum width that the drop menu will have * * * Available menu item options: * action: Optionally define a callback function that gets executed when item is chosen (and/or use the onCommand event) * title: Menu item text. * divider: Whether the current item is a divider, not an actual command. * disabled: Whether the item/command is disabled. * hidden: Whether the item/command is hidden. * tooltip: Item tooltip. * command: A command identifier to be passed to the onCommand event handlers. * cssClass: A CSS class to be added to the menu item container. * iconCssClass: A CSS class to be added to the menu item icon. * iconImage: A url to the icon image. * textCssClass: A CSS class to be added to the menu item text. * 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: * onAfterMenuShow: Fired after the menu is shown. You can customize the menu or dismiss it by returning false. * Event args: * grid: Reference to the grid. * column: Column definition. * menu: Menu options. Note that you can change the menu items here. * * onBeforeMenuShow: Fired before the menu is shown. You can customize the menu or dismiss it by returning false. * Event args: * grid: Reference to the grid. * column: Column definition. * menu: Menu options. Note that you can change the menu items here. * * onCommand: Fired on menu item 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: an extra CSS class to add to the menu button * buttonImage: a url to the menu button image (default '../images/down.gif') * @class Slick.Plugins.HeaderButtons * @constructor */ function HeaderMenu(options) { var _grid; var _self = this; var _handler = new Slick.EventHandler(); var _defaults = { buttonCssClass: null, buttonImage: null, minWidth: 100, autoAlign: true, autoAlignOffset: 0 }; var $menu; var $activeHeaderColumn; function init(grid) { options = $.extend(true, {}, _defaults, options); _grid = grid; _handler .subscribe(_grid.onHeaderCellRendered, handleHeaderCellRendered) .subscribe(_grid.onBeforeHeaderCellDestroy, handleBeforeHeaderCellDestroy); // Force the grid to re-render the header now that the events are hooked up. _grid.setColumns(_grid.getColumns()); // Hide the menu on outside click. $(document.body).on("mousedown", handleBodyMouseDown); } function setOptions(newOptions) { options = $.extend(true, {}, options, newOptions); } function destroy() { _handler.unsubscribeAll(); $(document.body).off("mousedown", handleBodyMouseDown); if ($menu) { $menu.remove(); } $menu = null; $activeHeaderColumn = null; $menu = null; } function handleBodyMouseDown(e) { if ($menu && $menu[0] != e.target && !$.contains($menu[0], e.target)) { hideMenu(); } } function hideMenu() { if ($menu) { $menu.remove(); $menu = null; $activeHeaderColumn .removeClass("slick-header-column-active"); } } function handleHeaderCellRendered(e, args) { var column = args.column; var menu = column.header && column.header.menu; if (menu) { // run the override function (when defined), if the result is false it won't go further if (!runOverrideFunctionWhenExists(options.menuUsabilityOverride, args)) { return; } var $el = $("<div></div>") .addClass("slick-header-menubutton") .data("column", column) .data("menu", menu); if (options.buttonCssClass) { $el.addClass(options.buttonCssClass); } if (options.buttonImage) { $el.css("background-image", "url(" + options.buttonImage + ")"); } if (options.tooltip) { $el.attr("title", options.tooltip); } $el .on("click", showMenu) .appendTo(args.node); $el = null; } } function handleBeforeHeaderCellDestroy(e, args) { var column = args.column; if (column.header && column.header.menu) { $(args.node).find(".slick-header-menubutton").remove(); } } function showMenu(e) { var $menuButton = $(this); var menu = $menuButton.data("menu"); var columnDef = $menuButton.data("column"); // Let the user modify the menu or cancel altogether, // or provide alternative menu implementation. var callbackArgs = { "grid": _grid, "column": columnDef, "menu": menu }; if (_self.onBeforeMenuShow.notify(callbackArgs, e, _self) == false) { return; } if (!$menu) { $menu = $("<div class='slick-header-menu' style='min-width: " + options.minWidth + "px'></div>") .appendTo(_grid.getContainerNode()); } $menu.empty(); // Construct the menu items. for (var i = 0; i < menu.items.length; i++) { var item = menu.items[i]; // run each override functions to know if the item is visible and usable var isItemVisible = runOverrideFunctionWhenExists(item.itemVisibilityOverride, callbackArgs); var isItemUsable = runOverrideFunctionWhenExists(item.itemUsabilityOverride, callbackArgs); // 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(item, "itemUsabilityOverride")) { item.disabled = isItemUsable ? false : true; } var $li = $("<div class='slick-header-menuitem'></div>") .data("command", item.command !== undefined ? item.command : "") .data("column", columnDef) .data("item", item) .on("click", handleMenuItemClick) .appendTo($menu); if (item.divider || item === "divider") { $li.addClass("slick-header-menuitem-divider"); continue; } if (item.disabled) { $li.addClass("slick-header-menuitem-disabled"); } if (item.hidden) { $li.addClass("slick-header-menuitem-hidden"); } if (item.cssClass) { $li.addClass(item.cssClass); } if (item.tooltip) { $li.attr("title", item.tooltip); } var $icon = $("<div class='slick-header-menuicon'></div>") .appendTo($li); if (item.iconCssClass) { $icon.addClass(item.iconCssClass); } if (item.iconImage) { $icon.css("background-image", "url(" + item.iconImage + ")"); } var $text = $("<span class='slick-header-menucontent'></span>") .text(item.title) .appendTo($li); if (item.textCssClass) { $text.addClass(item.textCssClass); } $icon = null; $text = null; $li = null; } var leftPos = $(this).offset().left; // 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 (options.autoAlign) { var gridPos = _grid.getGridPosition(); if ((leftPos + $menu.width()) >= gridPos.width) { leftPos = leftPos + $menuButton.outerWidth() - $menu.outerWidth() + options.autoAlignOffset; } } $menu .offset({ top: $(this).offset().top + $(this).height(), left: leftPos }); // Mark the header as active to keep the highlighting. $activeHeaderColumn = $menuButton.closest(".slick-header-column"); $activeHeaderColumn .addClass("slick-header-column-active"); if (_self.onAfterMenuShow.notify(callbackArgs, e, _self) == false) { return; } // Stop propagation so that it doesn't register as a header click event. e.preventDefault(); e.stopPropagation(); $menuButton = null; } function handleMenuItemClick(e) { var command = $(this).data("command"); var columnDef = $(this).data("column"); var item = $(this).data("item"); if (item.disabled || item.divider || item === "divider") { return; } if (command != null && command !== '') { var callbackArgs = { "grid": _grid, "column": columnDef, "command": command, "item": item }; _self.onCommand.notify(callbackArgs, e, _self); // execute action callback when defined if (typeof item.action === "function") { item.action.call(this, e, callbackArgs); } } if(!e.isDefaultPrevented()) { hideMenu(); } // 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) */ function runOverrideFunctionWhenExists(overrideFn, args) { if (typeof overrideFn === 'function') { return overrideFn.call(this, args); } return true; } $.extend(this, { "init": init, "destroy": destroy, "pluginName": "HeaderMenu", "setOptions": setOptions, "onAfterMenuShow": new Slick.Event(), "onBeforeMenuShow": new Slick.Event(), "onCommand": new Slick.Event() }); } module.exports = { "HeaderMenu": HeaderMenu };