UNPKG

13.3 kBJavaScriptView Raw
1(function ($) {
2 // register namespace
3 $.extend(true, window, {
4 "Slick": {
5 "Plugins": {
6 "HeaderMenu": HeaderMenu
7 }
8 }
9 });
10
11 /***
12 * A plugin to add drop-down menus to column headers.
13 *
14 * USAGE:
15 *
16 * Add the plugin .js & .css files and register it with the grid.
17 *
18 * To specify a menu in a column header, extend the column definition like so:
19 *
20 * var columns = [
21 * {
22 * id: 'myColumn',
23 * name: 'My column',
24 *
25 * // This is the relevant part
26 * header: {
27 * menu: {
28 * items: [
29 * {
30 * // menu item options
31 * },
32 * {
33 * // menu item options
34 * }
35 * ]
36 * }
37 * }
38 * }
39 * ];
40 *
41 *
42 * Available menu options:
43 * autoAlign: Auto-align drop menu to the left when not enough viewport space to show on the right
44 * 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)
45 * buttonCssClass: an extra CSS class to add to the menu button
46 * buttonImage: a url to the menu button image (default '../images/down.gif')
47 * 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)
48 * minWidth: Minimum width that the drop menu will have
49 *
50 *
51 * Available menu item options:
52 * action: Optionally define a callback function that gets executed when item is chosen (and/or use the onCommand event)
53 * title: Menu item text.
54 * divider: Whether the current item is a divider, not an actual command.
55 * disabled: Whether the item/command is disabled.
56 * hidden: Whether the item/command is hidden.
57 * tooltip: Item tooltip.
58 * command: A command identifier to be passed to the onCommand event handlers.
59 * cssClass: A CSS class to be added to the menu item container.
60 * iconCssClass: A CSS class to be added to the menu item icon.
61 * iconImage: A url to the icon image.
62 * textCssClass: A CSS class to be added to the menu item text.
63 * itemVisibilityOverride: Callback method that user can override the default behavior of showing/hiding an item from the list
64 * itemUsabilityOverride: Callback method that user can override the default behavior of enabling/disabling an item from the list
65 *
66 *
67 * The plugin exposes the following events:
68
69 * onAfterMenuShow: Fired after the menu is shown. You can customize the menu or dismiss it by returning false.
70 * Event args:
71 * grid: Reference to the grid.
72 * column: Column definition.
73 * menu: Menu options. Note that you can change the menu items here.
74 *
75 * onBeforeMenuShow: Fired before the menu is shown. You can customize the menu or dismiss it by returning false.
76 * Event args:
77 * grid: Reference to the grid.
78 * column: Column definition.
79 * menu: Menu options. Note that you can change the menu items here.
80 *
81 * onCommand: Fired on menu item click for buttons with 'command' specified.
82 * Event args:
83 * grid: Reference to the grid.
84 * column: Column definition.
85 * command: Button command identified.
86 * button: Button options. Note that you can change the button options in your
87 * event handler, and the column header will be automatically updated to
88 * reflect them. This is useful if you want to implement something like a
89 * toggle button.
90 *
91 *
92 * @param options {Object} Options:
93 * buttonCssClass: an extra CSS class to add to the menu button
94 * buttonImage: a url to the menu button image (default '../images/down.gif')
95 * @class Slick.Plugins.HeaderButtons
96 * @constructor
97 */
98 function HeaderMenu(options) {
99 var _grid;
100 var _self = this;
101 var _handler = new Slick.EventHandler();
102 var _defaults = {
103 buttonCssClass: null,
104 buttonImage: null,
105 minWidth: 100,
106 autoAlign: true,
107 autoAlignOffset: 0
108 };
109 var $menu;
110 var $activeHeaderColumn;
111
112
113 function init(grid) {
114 options = $.extend(true, {}, _defaults, options);
115 _grid = grid;
116 _handler
117 .subscribe(_grid.onHeaderCellRendered, handleHeaderCellRendered)
118 .subscribe(_grid.onBeforeHeaderCellDestroy, handleBeforeHeaderCellDestroy);
119
120 // Force the grid to re-render the header now that the events are hooked up.
121 _grid.setColumns(_grid.getColumns());
122
123 // Hide the menu on outside click.
124 $(document.body).on("mousedown", handleBodyMouseDown);
125 }
126
127 function setOptions(newOptions) {
128 options = $.extend(true, {}, options, newOptions);
129 }
130
131
132 function destroy() {
133 _handler.unsubscribeAll();
134 $(document.body).off("mousedown", handleBodyMouseDown);
135 if ($menu) {
136 $menu.remove();
137 }
138 $menu = null;
139 $activeHeaderColumn = null;
140 $menu = null;
141 }
142
143
144 function handleBodyMouseDown(e) {
145 if ($menu && $menu[0] != e.target && !$.contains($menu[0], e.target)) {
146 hideMenu();
147 }
148 }
149
150
151 function hideMenu() {
152 if ($menu) {
153 $menu.remove();
154 $menu = null;
155 $activeHeaderColumn
156 .removeClass("slick-header-column-active");
157 }
158 }
159
160 function handleHeaderCellRendered(e, args) {
161 var column = args.column;
162 var menu = column.header && column.header.menu;
163
164 if (menu) {
165 // run the override function (when defined), if the result is false it won't go further
166 if (!runOverrideFunctionWhenExists(options.menuUsabilityOverride, args)) {
167 return;
168 }
169
170 var $el = $("<div></div>")
171 .addClass("slick-header-menubutton")
172 .data("column", column)
173 .data("menu", menu);
174
175 if (options.buttonCssClass) {
176 $el.addClass(options.buttonCssClass);
177 }
178
179 if (options.buttonImage) {
180 $el.css("background-image", "url(" + options.buttonImage + ")");
181 }
182
183 if (menu.tooltip) {
184 $el.attr("title", menu.tooltip);
185 }
186
187 $el
188 .on("click", showMenu)
189 .appendTo(args.node);
190 $el = null;
191 }
192 }
193
194
195 function handleBeforeHeaderCellDestroy(e, args) {
196 var column = args.column;
197
198 if (column.header && column.header.menu) {
199 $(args.node).find(".slick-header-menubutton").remove();
200 }
201 }
202
203
204 function showMenu(e) {
205 var $menuButton = $(this);
206 var menu = $menuButton.data("menu");
207 var columnDef = $menuButton.data("column");
208
209 // Let the user modify the menu or cancel altogether,
210 // or provide alternative menu implementation.
211 var callbackArgs = {
212 "grid": _grid,
213 "column": columnDef,
214 "menu": menu
215 };
216 if (_self.onBeforeMenuShow.notify(callbackArgs, e, _self) == false) {
217 return;
218 }
219
220
221 if (!$menu) {
222 $menu = $("<div class='slick-header-menu' style='min-width: " + options.minWidth + "px'></div>")
223 .appendTo(_grid.getContainerNode());
224 }
225 $menu.empty();
226
227
228 // Construct the menu items.
229 for (var i = 0; i < menu.items.length; i++) {
230 var item = menu.items[i];
231
232 // run each override functions to know if the item is visible and usable
233 var isItemVisible = runOverrideFunctionWhenExists(item.itemVisibilityOverride, callbackArgs);
234 var isItemUsable = runOverrideFunctionWhenExists(item.itemUsabilityOverride, callbackArgs);
235
236 // if the result is not visible then there's no need to go further
237 if (!isItemVisible) {
238 continue;
239 }
240
241 // when the override is defined, we need to use its result to update the disabled property
242 // so that "handleMenuItemCommandClick" has the correct flag and won't trigger a command clicked event
243 if (Object.prototype.hasOwnProperty.call(item, "itemUsabilityOverride")) {
244 item.disabled = isItemUsable ? false : true;
245 }
246
247 var $li = $("<div class='slick-header-menuitem'></div>")
248 .data("command", item.command || '')
249 .data("column", columnDef)
250 .data("item", item)
251 .on("click", handleMenuItemClick)
252 .appendTo($menu);
253
254 if (item.divider || item === "divider") {
255 $li.addClass("slick-header-menuitem-divider");
256 continue;
257 }
258
259 if (item.disabled) {
260 $li.addClass("slick-header-menuitem-disabled");
261 }
262
263 if (item.hidden) {
264 $li.addClass("slick-header-menuitem-hidden");
265 }
266
267 if (item.cssClass) {
268 $li.addClass(item.cssClass);
269 }
270
271 if (item.tooltip) {
272 $li.attr("title", item.tooltip);
273 }
274
275 var $icon = $("<div class='slick-header-menuicon'></div>")
276 .appendTo($li);
277
278 if (item.iconCssClass) {
279 $icon.addClass(item.iconCssClass);
280 }
281
282 if (item.iconImage) {
283 $icon.css("background-image", "url(" + item.iconImage + ")");
284 }
285
286 var $text = $("<span class='slick-header-menucontent'></span>")
287 .text(item.title)
288 .appendTo($li);
289
290 if (item.textCssClass) {
291 $text.addClass(item.textCssClass);
292 }
293 $icon = null;
294 $text = null;
295 $li = null;
296 }
297
298 var leftPos = $(this).offset().left;
299
300 // 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)
301 // if there isn't enough space on the right, it will automatically align the drop menu to the left
302 // to simulate an align left, we actually need to know the width of the drop menu
303 if (options.autoAlign) {
304 var gridPos = _grid.getGridPosition();
305 if ((leftPos + options.minWidth) >= gridPos.width) {
306 leftPos = leftPos - options.minWidth + options.autoAlignOffset;
307 }
308 }
309
310 $menu
311 .offset({ top: $(this).offset().top + $(this).height(), left: leftPos });
312
313
314 // Mark the header as active to keep the highlighting.
315 $activeHeaderColumn = $menuButton.closest(".slick-header-column");
316 $activeHeaderColumn
317 .addClass("slick-header-column-active");
318
319 if (_self.onAfterMenuShow.notify(callbackArgs, e, _self) == false) {
320 return;
321 }
322
323 // Stop propagation so that it doesn't register as a header click event.
324 e.preventDefault();
325 e.stopPropagation();
326 $menuButton = null;
327 }
328
329
330 function handleMenuItemClick(e) {
331 var command = $(this).data("command");
332 var columnDef = $(this).data("column");
333 var item = $(this).data("item");
334
335 if (item.disabled || item.divider || item === "divider") {
336 return;
337 }
338
339 if (command != null && command !== '') {
340 var callbackArgs = {
341 "grid": _grid,
342 "column": columnDef,
343 "command": command,
344 "item": item
345 };
346 _self.onCommand.notify(callbackArgs, e, _self);
347
348 // execute action callback when defined
349 if (typeof item.action === "function") {
350 item.action.call(this, e, callbackArgs);
351 }
352 }
353
354 if(!e.isDefaultPrevented()) {
355 hideMenu();
356 }
357
358 // Stop propagation so that it doesn't register as a header click event.
359 e.preventDefault();
360 e.stopPropagation();
361 }
362
363 /**
364 * Method that user can pass to override the default behavior.
365 * In order word, user can choose or an item is (usable/visible/enable) by providing his own logic.
366 * @param overrideFn: override function callback
367 * @param args: multiple arguments provided to the override (cell, row, columnDef, dataContext, grid)
368 */
369 function runOverrideFunctionWhenExists(overrideFn, args) {
370 if (typeof overrideFn === 'function') {
371 return overrideFn.call(this, args);
372 }
373 return true;
374 }
375
376 $.extend(this, {
377 "init": init,
378 "destroy": destroy,
379 "pluginName": "HeaderMenu",
380 "setOptions": setOptions,
381
382 "onAfterMenuShow": new Slick.Event(),
383 "onBeforeMenuShow": new Slick.Event(),
384 "onCommand": new Slick.Event()
385 });
386 }
387})(jQuery);