PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.6
VikBooking Hotel Booking Engine & PMS v1.6.6
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / resources / contextmenu.js

contextmenu.js in VikBooking Hotel Booking Engine & PMS 1.6.6, at admin/resources/contextmenu.js

836 lines 25.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * jQuery add-on used to support context menus.
3 * Here's a list of supported options.
4 *
5 * @author E4J srl
6 *
7 * - trigger string The command that should trigger the popup menu. Accepts the
8 * following values: click|doubleclick|rightclick|hover.
9 * Click will be used by default.
10 * - placement string Where the popup should be displayed in relation to the target.
11 * Accepts the following values: auto|top|right|bottom|left.
12 * Auto will be used by default (at the mouse coordinates).
13 * - class string An optional class to use for individual styling.
14 * - buttons object[] A list of buttons to include within the popup menu. See the options
15 * of the buttons for further details.
16 * - onShow function An optional callback to invoke when the popup menu is displayed.
17 * - onHide function An optional callback to invoke when the popup menu is dismissed.
18 * - darkMode mixed Flag for dark mode layout, which accepts 3 possible values:
19 * true|false|null. Pass true to always force the dark mode, false to
20 * always use the light mode, null to auto-detect the proper mode
21 * according to the preferred theme of the browser.
22 * - clickable boolean Flag used to check whether the root element should prevent the
23 * browser selection by applying specific CSS rules. False by default.
24 * - lockScroll boolean Flag used to prevent the document scroll when the context menu
25 * pops up. True by default.
26 * - hideOnEsc boolean Choose whether the context menu should be closed when ESC key is
27 * pressed. Always true by default.
28 * - formatShortcut mixed An optional callback that can be used to format the shortcut symbols.
29 *
30 * Here's a list of options supported by the buttons. Any other property of the button will
31 * be accessible by the internal methods.
32 *
33 * - icon mixed Either a function, an image URL, an image instance or a font icon
34 * to display before the button text. In case of a function, it will be
35 * used as callback to define an image/icon at runtime.
36 * - text string The text to display for the popup menu button.
37 * - action function The callback to dispatch when the button gets clicked.
38 * - class string An optional class to use for individual styling.
39 * - disabled mixed Either a function or a boolean to check whether the button should
40 * be clicked or not. The button is never disabled by default.
41 * - visible mixed Either a function or a boolean to check whether the button should
42 * be displayed or not. The button is always visible by default.
43 * - separator boolean Flag used to check whether the popup should include a separator after the
44 * button. False by default.
45 * - shortcut mixed An array of commands to represent the shortcut that will trigger the action
46 * via keyboard. The array must contain one and only one character or symbol.
47 * The array may contain one ore more modifiers, which must be specified first.
48 *
49 * List of methods supported by the add-on.
50 *
51 * - show Manually displays the popup menu.
52 * - hide Manually disposes the popup menu.
53 * - destroy Destroys the popup attached to the element.
54 * - config Returns the configuration of the popup.
55 * - buttons Getter/setter of the popup buttons.
56 *
57 * It is possible to update each setting configuration by using the same
58 * name of the property and the related value to set. Leave the set argument
59 * empty to simply access the current property value. In example:
60 *
61 * jQuery(target).vboContextMenu( 'trigger', 'click');
62 * jQuery(target).vboContextMenu('placement', 'auto');
63 */
64 (function($) {
65 /**
66 * Popup menu trigger setup.
67 *
68 * @param object root The selector element.
69 * @param string trigger The trigger to use.
70 * @param mixed prev The previous trigger.
71 *
72 * @return string The trigger event.
73 */
74 var vikPopupMenuTrigger = function(root, trigger, prev) {
75 // check if the trigger was already registered
76 if (prev) {
77 // detach previous trigger
78 $(root).off(prev.toLowerCase());
79 }
80
81 if (!trigger) {
82 // abort in case of missing trigger
83 return false;
84 }
85
86 // normalize trigger event
87 switch (trigger.toLowerCase()) {
88 case 'mouseover':
89 case 'hover':
90 trigger = 'mouseover';
91 break;
92
93 case 'dblclick':
94 case 'doubleclick':
95 case 'double-click':
96 trigger = 'dblclick';
97 break;
98
99 case 'contextmenu':
100 case 'rightclick':
101 case 'right-click':
102 trigger = 'contextmenu';
103 break;
104
105 default:
106 trigger = 'click';
107 };
108
109 // scan all the registered elements
110 $(root).each(function() {
111 // register new trigger
112 $(this).on(trigger, function(event) {
113 // always prevent default event
114 event.preventDefault();
115
116 // open popup
117 vikPopupMenuShow(this, event);
118 });
119 });
120
121 return trigger;
122 };
123
124 /**
125 * Popup menu clickable setup.
126 *
127 * @param object root The selector element.
128 * @param boolean flag True to make the root clickable.
129 * @param mixed prev The flag previously set, if any.
130 *
131 * @return self
132 */
133 var vikPopupMenuClickable = function(root, flag, prev) {
134 if (prev) {
135 // remove CSS class used to disable the selection from root element
136 $(root).removeClass('vik-context-menu-disable-selection');
137 }
138
139 if (flag) {
140 // add CSS class to root element to disable the selection
141 $(root).addClass('vik-context-menu-disable-selection');
142 }
143
144 return root;
145 };
146
147 /**
148 * Initializes the popup menu.
149 *
150 * @param object root The selector element.
151 * @param object options A configuration object.
152 *
153 * @return self
154 */
155 var vikPopupMenuInit = function(root, options) {
156 // inject the specified options within the default configuration
157 options = $.extend($.vboContextMenu.defaults, options);
158
159 // register the popup configuration for being used later
160 vikPopupMenuConfig(root, options);
161
162 // register trigger to show the popup menu
163 options.trigger = vikPopupMenuTrigger(root, options.trigger);
164
165 // normalize buttons
166 vikPopupMenuButtons(root, options.buttons);
167
168 // handle clickable property
169 vikPopupMenuClickable(root, options.clickable);
170
171 // register callback to dispatch the action of a button when its shortcut is pressed
172 $(document).on('keydown.contextmenu.vikbooking', function(event) {
173 // ignore the event with this namespace because it will end up
174 // to catch also the plain keydown event
175 if (event.namespace == 'contextmenu.vikbooking') {
176 return true;
177 }
178
179 // go ahead only in case the focus is not help by a text field
180 if ($(document.activeElement).is('input,textarea') == true) {
181 // prevent shortcuts from catching typed characters
182 return true;
183 }
184
185 // retrieve popup configuration
186 var config = vikPopupMenuConfig(root);
187
188 // in case ESC was pressed, check if we should hide the popup
189 if (config.hideOnEsc && event.keyCode == 27) {
190 // auto-close the context menu
191 vikPopupMenuHide(root);
192 return true;
193 }
194
195 // iterate all registered buttons
196 $.each(config.buttons, (i, btn) => {
197 // make sure we have a shortcut and an action to execute
198 if (!btn.shortcut || !btn.action) {
199 // nothing to do here, go ahead
200 return true;
201 }
202
203 // check whether the shortcut is pressed
204 if (event.originalEvent.shortcut(btn.shortcut)) {
205 // launch callback to check whether the button is disabled
206 // or simply rely on the specified boolean
207 var disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled;
208
209 // trigger action only in case the button is not disabled
210 if (!disabled) {
211 // stop event propagation
212 event.preventDefault();
213 event.stopPropagation();
214
215 // dispatch button action
216 btn.action(root, event);
217 }
218
219 return false;
220 }
221 });
222 });
223
224 return root;
225 };
226
227 /**
228 * Getter and setter of the popup configuration.
229 *
230 * @param object root The selector element.
231 * @param mixed data The popup configuration to set. When omitted,
232 * the method will act as a getter.
233 *
234 * @param mixed Returns the configuration when the data argument is
235 * missing. Otherwise itself will be returned.
236 */
237 var vikPopupMenuConfig = function(root, data) {
238 if (typeof data === 'undefined') {
239 // GETTER: return popup configuration.
240 // Clone the object in order to prevent manual edits to
241 // the configuration properties.
242 return Object.assign({}, $(root).data('popupConfiguration'));
243 }
244
245 // SETTER: update popup configuration
246 return $(root).data('popupConfiguration', data);
247 };
248
249 /**
250 * Creates and shows the popup menu.
251 *
252 * @param object root The selector element.
253 * @param Event event The dispatcher DOM event.
254 *
255 * @return self
256 */
257 var vikPopupMenuShow = function(root, event) {
258 if ($('.vik-context-menu').length) {
259 // do not go ahead in case a context menu is visible
260 return root;
261 }
262
263 // retrieve configuration
264 var config = vikPopupMenuConfig(root);
265
266 // prepare context menu structure
267 var popup = $('<div class="vik-context-menu"><ul></ul></div>');
268
269 // in case of a custom class, add it
270 if (config.class) {
271 popup.addClass(config.class);
272 }
273
274 // look for dark mode
275 if (config.darkMode === true) {
276 // turn dark mode on
277 popup.addClass('dark-mode');
278 } else if (config.darkMode === false) {
279 // suppress dark mode
280 popup.addClass('light-mode');
281 }
282
283 // iterate registered buttons and append them one by one
284 $.each(config.buttons, function(i, btn) {
285 // launch callback to check whether the button should be displayed
286 // or simply rely on the specified boolean
287 var visible = typeof btn.visible === 'function' ? btn.visible(root, config) : btn.visible;
288
289 if (!visible) {
290 // skip button and go ahead
291 return true;
292 }
293
294 // prepare button structure
295 var popupBtn = $('<a></a>');
296
297 if (btn.icon) {
298 var icon;
299
300 if (typeof btn.icon === 'function') {
301 // we have a function, launch the callback
302 // to extract the image at runtime
303 icon = btn.icon(root, config);
304 } else {
305 // use it plain
306 icon = btn.icon;
307 }
308
309 if (icon instanceof Image) {
310 // we have an image instance
311 icon = $(icon);
312 } else if (typeof icon === 'string') {
313 if (icon.indexOf('/') !== -1) {
314 // we have an image URL
315 icon = $('<img>').attr('src', icon);
316 } else {
317 // we probably have a font icon
318 icon = $('<i></i>').addClass(icon);
319 }
320 }
321
322 // leave as is in case a jQuery instance was passed
323
324 // wrap icon in a parent and append all to button
325 popupBtn.append($('<span class="button-icon"></span>').append(icon));
326 }
327
328 // insert text button
329 popupBtn.append($('<span class="button-text"></span>').html(btn.text));
330
331 // check if the button specified a shortcut
332 if (btn.shortcut) {
333 // map shortcut elements
334 var cmd = btn.shortcut.map(function(k) {
335 var keyCode = k;
336
337 switch (k) {
338 case 'alt': k = "&#8997;"; break;
339 case 'ctrl': k = "&#8963;"; break;
340 case 'shift': k = "&#8679;"; break;
341 case 'meta': k = "&#8984;"; break;
342 // backspace
343 case 8: k = '<i class="fas fa-backspace"></i>'; break;
344 // enter
345 case 13: k = '&#9166;'; break;
346 // space
347 case 32: k = 'Space'; break;
348 // arrow up
349 case 37: k = '<i class="fas fa-long-arrow-alt-left"></i>'; break;
350 // arrow up
351 case 38: k = '<i class="fas fa-long-arrow-alt-up"></i>'; break;
352 // arrow right
353 case 39: k = '<i class="fas fa-long-arrow-alt-right"></i>'; break;
354 // arrow down
355 case 40: k = '<i class="fas fa-long-arrow-alt-down"></i>'; break;
356 // character
357 default: k = typeof k === 'string' ? k.toUpperCase() : '';
358 }
359
360 // look for a custom function used to format shortcuts
361 if (typeof config.formatShortcut === 'function') {
362 // launch the callback
363 k = config.formatShortcut(keyCode, k);
364 }
365
366 return k;
367 });
368
369 cmd = cmd.join('');
370
371 // wrap the shortcut between parenthesis in case of no modifiers
372 if (cmd.length == 1) {
373 cmd = '(' + cmd + ')';
374 }
375
376 // insert shortcut button
377 popupBtn.append($('<span class="button-shortcut"></span>').html(cmd));
378 }
379
380 // launch callback to check whether the button should be disabled
381 // or simply rely on the specified boolean
382 var disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled;
383
384 // check whether the button is disabled
385 if (disabled) {
386 popupBtn.addClass('disabled');
387 } else {
388 // register button click event
389 popupBtn.on('click', function(event) {
390 // look for an action callback
391 if (btn.action) {
392 // dispatch callback
393 btn.action(root, event);
394 }
395
396 // always dismiss the popup when a button gets clicked
397 vikPopupMenuHide(root);
398 });
399 }
400
401 // in case of a custom class, add it
402 if (btn.class) {
403 popupBtn.addClass(btn.class);
404 }
405
406 // wrap button within a parent for <ul> compliance
407 var popupItem = $('<li></li>').append(popupBtn);
408
409 // in case of a separator, add a specific class
410 if (btn.separator) {
411 popupItem.addClass('separator');
412 }
413
414 // wrap button within a parent and add to popup
415 popup.find('ul').append(popupItem);
416 });
417
418 // hide the popup before appending it
419 popup.hide();
420
421 // append button to body
422 $('body').append(popup);
423
424 // calculate popup position
425 vikPopupMenuCalcPosition(root, popup, event);
426
427 if (config.lockScroll) {
428 // prevent document from scrolling
429 $('body').addClass('lock-scroll');
430 }
431
432 // show popup
433 popup.show();
434
435 // look for a specific callback to be triggered on opening
436 if (config.onShow) {
437 // trigger show callback
438 config.onShow(root, popup);
439 }
440
441 // Register callback to auto dismiss the popup when clicked outside.
442 // Use mousedown event because it will be execured before any other
443 // supported trigger, so that the context menus can be shown on cascade.
444 $(document).on('mousedown.contextmenu.vikbooking', function(event) {
445 // ignore the event with this namespace because it will end up
446 // to catch also the plain mousedown event
447 if (event.namespace == 'contextmenu.vikbooking') {
448 return false;
449 }
450
451 if (!popup.is(':visible')) {
452 // dialog not visible
453 return false;
454 }
455
456 // get list of buttons
457 var links = popup.find('a');
458
459 // make sure we haven't clicked the popup or a link
460 if (!popup.is(event.target) && popup.has(event.target).length === 0
461 && !links.is(event.target) && links.has(event.target).length === 0) {
462 // auto close popup when clicked outside
463 vikPopupMenuHide(root);
464
465 event.stopPropagation();
466 event.preventDefault();
467
468 return false;
469 }
470 });
471
472 return root;
473 };
474
475 /**
476 * Hides the popup menu.
477
478 * @param object root The selector element.
479 *
480 * @return self
481 */
482 var vikPopupMenuHide = function(root) {
483 // go ahead only in case a popup is open
484 if ($('.vik-context-menu').length) {
485 // destroy any existing popup menu because we do not support
486 // more than a popup per time
487 $('.vik-context-menu').remove();
488
489 // always restore scroll functions
490 $('body').removeClass('lock-scroll');
491
492 // turn off proxy
493 $(document).off('mousedown.contextmenu.vikbooking');
494
495 // get popup configuration
496 var config = vikPopupMenuConfig(root);
497
498 // look for a specific callback to be triggered on dismiss
499 if (config.onHide) {
500 // trigger hide callback
501 config.onHide(root);
502 }
503 }
504
505 return root;
506 };
507
508 /**
509 * Destroys the popup menu.
510
511 * @param object root The selector element.
512 *
513 * @return self
514 */
515 var vikPopupMenuDestroy = function(root) {
516 // in case the popup was open, close it first
517 vikPopupMenuHide(root);
518
519 // detach keyboard listener
520 $(document).off('keydown.contextmenu.vikbooking');
521
522 // remove CSS class used to disable the selection from root element
523 $(root).removeClass('vik-context-menu-disable-selection');
524
525 // get popup configuration
526 var config = vikPopupMenuConfig(root);
527
528 // detach previous event without attaching a new one
529 vikPopupMenuTrigger(root, null, config.trigger);
530
531 // detach clickable property
532 vikPopupMenuClickable(root, null, config.clickable);
533
534 // then destroy the registered data
535 return vikPopupMenuConfig(root, null);
536 };
537
538 /**
539 * Getter and setter of the popup buttons.
540 *
541 * @param object root The selector element.
542 * @param mixed data The popup buttons to set. When omitted,
543 * the method will act as a getter.
544 *
545 * @param mixed Returns the buttons list when the data argument is
546 * missing. Otherwise itself will be returned.
547 */
548 var vikPopupMenuButtons = function(root, data) {
549 // get configuration
550 var config = vikPopupMenuConfig(root);
551
552 if (typeof data === 'undefined') {
553 // return popup buttons
554 return config.buttons;
555 }
556
557 // make sure the buttons property is an Array
558 if (!Array.isArray(data)) {
559 throw 'Invalid buttons, an Array was expected.';
560 }
561
562 // set specified buttons
563 config.buttons = data;
564
565 // iterate all buttons
566 for (var i = 0; i < config.buttons.length; i++) {
567 // create default button properties
568 config.buttons[i] = $.extend({
569 icon: null,
570 text: '',
571 action: null,
572 shortcut: null,
573 class: '',
574 disabled: false,
575 visible: true,
576 separator: false,
577 }, config.buttons[i]);
578
579 // make sure we have an array
580 if (!Array.isArray(config.buttons[i].shortcut)) {
581 // invalid shortcut
582 config.buttons[i].shortcut = null;
583 }
584 }
585
586 // register configuration
587 return vikPopupMenuConfig(root, config);
588 };
589
590 /**
591 * Calculates and sets the proper position of the popup.
592 *
593 * @param object root The selector element.
594 * @param mixed popup The popup element.
595 * @param mixed event The dispatcher DOM event.
596 *
597 * @param self
598 */
599 var vikPopupMenuCalcPosition = function(root, popup, event) {
600 // get popup configuration
601 var config = vikPopupMenuConfig(root);
602
603 // in case of "auto" placement, we need to make sure
604 // that we own an event to access the mouse coordinates
605 if (config.placement == 'auto' && !event) {
606 // no event was passed, fallback to right
607 config.placement = 'right';
608 }
609
610 // calculate root offset
611 var rootOffset = $(root).offset();
612 // calculate root size
613 var rootWidth = $(root).outerWidth();
614 var rootHeight = $(root).outerHeight();
615 // calculate popup size
616 var popupWidth = $(popup).outerWidth();
617 var popupHeight = $(popup).outerHeight();
618
619 var x, y;
620
621 // display popup above the root, centered
622 if (config.placement == 'top') {
623 x = rootOffset.left + rootWidth / 2 - popupWidth / 2;
624 y = rootOffset.top - popupHeight - 4;
625 }
626 // display popup above the root, to the right
627 else if (config.placement == 'top-right') {
628 x = rootOffset.left + rootWidth - popupWidth;
629 y = rootOffset.top - popupHeight - 4;
630 }
631 // display popup above the root, to the left
632 else if (config.placement == 'top-left') {
633 x = rootOffset.left;
634 y = rootOffset.top - popupHeight - 4;
635 }
636 // display the popup below the root, centered
637 else if (config.placement == 'bottom') {
638 x = rootOffset.left + rootWidth / 2 - popupWidth / 2;
639 y = rootOffset.top + rootHeight + 4;
640 }
641 // display the popup below the root, to the right
642 else if (config.placement == 'bottom-right') {
643 x = rootOffset.left + rootWidth - popupWidth;
644 y = rootOffset.top + rootHeight + 4;
645 }
646 // display the popup below the root, to the left
647 else if (config.placement == 'bottom-left') {
648 x = rootOffset.left;
649 y = rootOffset.top + rootHeight + 4;
650 }
651 // display the popup before the root
652 else if (config.placement == 'left') {
653 x = rootOffset.left - popupWidth - 4;
654 y = rootOffset.top + rootHeight / 2 - popupHeight / 2;
655 }
656 // display the popup after the root
657 else if (config.placement == 'right') {
658 x = rootOffset.left + rootWidth + 4;
659 y = rootOffset.top + rootHeight / 2 - popupHeight / 2;
660 }
661 // display the popup at the mouse coordinates
662 else {
663 x = event.pageX;
664 y = event.pageY;
665 }
666
667 // calculate screen size
668 var screenWidth = $(window).width();
669 var screenHeight = $(window).height();
670 // calculate window scrolls
671 var windowScrollLeft = $(window).scrollLeft();
672 var windowScrollTop = $(window).scrollTop();
673
674 // use 4 pixel as minimum value
675 x = Math.max(4, x);
676 y = Math.max(4, y);
677
678 // make sure the popup doesn't exceed the screen width
679 if (x + popupWidth + 4 > screenWidth) {
680 x = screenWidth - popupWidth - 4;
681 }
682
683 // make sure the popup doesn't exceed the screen height
684 if (y + popupHeight + 4 > screenHeight + windowScrollTop) {
685 y = screenHeight - popupHeight - 4 + windowScrollTop;
686 }
687
688 $(popup).css('top', y).css('left', x);
689
690 return root;
691 };
692
693 // register listener to auto-close the popup when clicked outside
694 $(document).on('mousedown', function() {
695 // we need to propagate the event with a proxy so that we can safely
696 // detach the registered callbacks when the popup gets closed
697 $(document).trigger('mousedown.contextmenu.vikbooking');
698 });
699
700 // register listener to dispatch the actions of the buttons via keyboard
701 $(document).on('keydown', function() {
702 // we need to propagate the event with a proxy so that we can safely
703 // detach the registered callbacks when the popup gets destroyed
704 $(document).trigger('keydown.contextmenu.vikbooking');
705 });
706
707 // register the jQuery callback
708 $.fn.vboContextMenu = function(method, data) {
709 if (!method) {
710 method = {};
711 }
712
713 // immediately exit in case of no elements found
714 if ($(this).length == 0) {
715 return this;
716 }
717
718 // initialize popup events
719 if (typeof method === 'object') {
720 return vikPopupMenuInit(this, method);
721 }
722 // check if we should dismiss the popup
723 else if (typeof method === 'string' && method.match(/^(close|dismiss|hide)$/i)) {
724 return vikPopupMenuHide(this);
725 }
726 // check if we should open the popup
727 else if (typeof method === 'string' && method.match(/^(show|open)$/i)) {
728 return vikPopupMenuShow(this);
729 }
730 // check if we destroy the popup
731 else if (typeof method === 'string' && method.match(/^(destroy)$/i)) {
732 return vikPopupMenuDestroy(this);
733 }
734 // check if we should return the popup configuration
735 else if (typeof method === 'string' && method.match(/^(config|configuration|options)$/i)) {
736 return vikPopupMenuConfig(this);
737 }
738 // check if we should handle with the popup buttons
739 else if (typeof method === 'string' && method.match(/^(buttons)$/i)) {
740 // use getter/setter according to the specified arguments
741 return vikPopupMenuButtons(this, data);
742 }
743 // fallback to configuration setting getter/setter
744 else {
745 // access configuration
746 var config = vikPopupMenuConfig(this);
747
748 // check if the second argument was passed
749 if (typeof data !== 'undefined') {
750 if (method == 'trigger') {
751 // register trigger before updating the configuration
752 data = vikPopupMenuTrigger(this, data, config.trigger);
753 } else if (method == 'clickable') {
754 // handle clickable property
755 vikPopupMenuClickable(this, data, config.clickable);
756 }
757
758 // register argument within configuration
759 config[method] = data;
760
761 // refresh configuration and return self instance
762 return vikPopupMenuConfig(this, config);
763 }
764
765 // return configuration setting
766 return config[method];
767 }
768
769 return this;
770 };
771
772 // define the default configuration to use for the context menu
773 $.vboContextMenu = {
774 defaults: {
775 trigger: 'click',
776 placement: 'auto',
777 class: '',
778 buttons: [],
779 onShow: null,
780 onHide: null,
781 clickable: false,
782 lockScroll: true,
783 darkMode: null,
784 hideOnEsc: true,
785 formatShortcut: null,
786 },
787 };
788
789 /**
790 * Checks if the KeyBoard event matches the given shortcut.
791 *
792 * @param array keys The shortcut representation.
793 *
794 * @return boolean True if matches, otherwise false.
795 */
796 KeyboardEvent.prototype.shortcut = function(keys) {
797 // get modifiers list
798 var modifiers = keys.slice(0);
799 // pop character from modifiers
800 var keyCode = modifiers.pop();
801
802 if (typeof keyCode === 'string') {
803 // get ASCII
804 keyCode = keyCode.toUpperCase().charCodeAt(0);
805 }
806
807 // make sure the modifiers are lower case
808 modifiers = modifiers.map(function(mod) {
809 return mod.toLowerCase();
810 });
811
812 var ok = false;
813
814 // validate key code
815 if (this.keyCode == keyCode) {
816 // validate modifiers
817 ok = true;
818 var lookup = ['meta', 'shift', 'alt', 'ctrl'];
819
820 for (var i = 0; i < lookup.length && ok; i++) {
821 // check if modifiers is pressed
822 var mod = this[lookup[i] + 'Key'];
823
824 if (mod) {
825 // if pressed, the shortcut must specify it
826 ok &= modifiers.indexOf(lookup[i]) !== -1;
827 } else {
828 // if not pressed, the shortcut must not include it
829 ok &= modifiers.indexOf(lookup[i]) === -1;
830 }
831 }
832 }
833
834 return ok;
835 }
836 })(jQuery);