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);
|