UNPKG

smartmenus-keyboard

Version:

Provides support for advanced keyboard navigation for the SmartMenus jQuery plugin (smartmenus).

215 lines (208 loc) 8.24 kB
/*! * SmartMenus jQuery Plugin Keyboard Addon - v0.4.0 - September 17, 2017 * http://www.smartmenus.org/ * * Copyright Vasil Dinkov, Vadikom Web Ltd. * http://vadikom.com * * Licensed MIT */ (function(factory) { if (typeof define === 'function' && define.amd) { // AMD define(['jquery', 'smartmenus'], factory); } else if (typeof module === 'object' && typeof module.exports === 'object') { // CommonJS module.exports = factory(require('jquery')); } else { // Global jQuery factory(jQuery); } } (function($) { function getFirstItemLink($ul) { // make sure we also allow the link to be nested deeper inside the LI's (e.g. in a heading) return $ul.find('> li > a:not(.disabled), > li > :not(ul) a:not(.disabled)').eq(0); } function getLastItemLink($ul) { return $ul.find('> li > a:not(.disabled), > li > :not(ul) a:not(.disabled)').eq(-1); } function getNextItemLink($li, noLoop) { var $a = $li.nextAll('li').find('> a:not(.disabled), > :not(ul) a:not(.disabled)').eq(0); return noLoop || $a.length ? $a : getFirstItemLink($li.parent()); } function getPreviousItemLink($li, noLoop) { // bug workaround: elements are returned in reverse order just in jQuery 1.8.x var $a = $li.prevAll('li').find('> a:not(.disabled), > :not(ul) a:not(.disabled)').eq(/^1\.8\./.test($.fn.jquery) ? 0 : -1); return noLoop || $a.length ? $a : getLastItemLink($li.parent()); } // jQuery's .focus() is unreliable in some versions, so we're going to call the links' native JS focus method $.fn.focusSM = function() { if (this.length && this[0].focus) { this[0].focus(); } return this; }; $.extend($.SmartMenus.Keyboard = {}, { docKeydown: function(e) { var keyCode = e.keyCode; if (!/^(37|38|39|40)$/.test(keyCode)) { return; } var $root = $(this), obj = $root.data('smartmenus'), $target = $(e.target); // exit if this is an A inside a mega drop-down if (!obj || !$target.is('a') || !obj.handleItemEvents($target)) { return; } var $li = $target.closest('li'), $ul = $li.parent(), level = $ul.dataSM('level'); // swap left & right keys if ($root.hasClass('sm-rtl')) { if (keyCode == 37) { keyCode = 39; } else if (keyCode == 39) { keyCode = 37; } } switch (keyCode) { case 37: // Left if (obj.isCollapsible()) { break; } if (level > 2 || level == 2 && $root.hasClass('sm-vertical')) { obj.activatedItems[level - 2].focusSM(); // move to previous non-disabled parent item (make sure we cycle so it might be the last item) } else if (!$root.hasClass('sm-vertical')) { getPreviousItemLink((obj.activatedItems[0] || $target).closest('li')).focusSM(); } break; case 38: // Up if (obj.isCollapsible()) { var $firstItem; // if this is the first item of a sub menu, move to the parent item if (level > 1 && ($firstItem = getFirstItemLink($ul)).length && $target[0] == $firstItem[0]) { obj.activatedItems[level - 2].focusSM(); } else { getPreviousItemLink($li).focusSM(); } } else { if (level == 1 && !$root.hasClass('sm-vertical') && obj.opts.bottomToTopSubMenus) { if (!obj.activatedItems[0] && $target.dataSM('sub')) { if (obj.opts.showOnClick) { obj.clickActivated = true; } obj.itemActivate($target); if ($target.dataSM('sub').is(':visible')) { obj.focusActivated = true; } } if (obj.activatedItems[0] && obj.activatedItems[0].dataSM('sub') && obj.activatedItems[0].dataSM('sub').is(':visible') && !obj.activatedItems[0].dataSM('sub').hasClass('mega-menu')) { getLastItemLink(obj.activatedItems[0].dataSM('sub')).focusSM(); } } else if (level > 1 || $root.hasClass('sm-vertical')) { getPreviousItemLink($li).focusSM(); } } break; case 39: // Right if (obj.isCollapsible()) { break; } if (level == 1 && $root.hasClass('sm-vertical')) { if (!obj.activatedItems[0] && $target.dataSM('sub')) { if (obj.opts.showOnClick) { obj.clickActivated = true; } obj.itemActivate($target); if ($target.dataSM('sub').is(':visible')) { obj.focusActivated = true; } } if (obj.activatedItems[0] && obj.activatedItems[0].dataSM('sub') && obj.activatedItems[0].dataSM('sub').is(':visible') && !obj.activatedItems[0].dataSM('sub').hasClass('mega-menu')) { getFirstItemLink(obj.activatedItems[0].dataSM('sub')).focusSM(); } // move to next non-disabled parent item (make sure we cycle so it might be the last item) } else if ((level == 1 || obj.activatedItems[level - 1] && (!obj.activatedItems[level - 1].dataSM('sub') || !obj.activatedItems[level - 1].dataSM('sub').is(':visible') || obj.activatedItems[level - 1].dataSM('sub').hasClass('mega-menu'))) && !$root.hasClass('sm-vertical')) { getNextItemLink((obj.activatedItems[0] || $target).closest('li')).focusSM(); } else if (obj.activatedItems[level - 1] && obj.activatedItems[level - 1].dataSM('sub') && obj.activatedItems[level - 1].dataSM('sub').is(':visible') && !obj.activatedItems[level - 1].dataSM('sub').hasClass('mega-menu')) { getFirstItemLink(obj.activatedItems[level - 1].dataSM('sub')).focusSM(); } break; case 40: // Down if (obj.isCollapsible()) { var $firstSubItem, $lastItem; // move to sub menu if appropriate if (obj.activatedItems[level - 1] && obj.activatedItems[level - 1].dataSM('sub') && obj.activatedItems[level - 1].dataSM('sub').is(':visible') && !obj.activatedItems[level - 1].dataSM('sub').hasClass('mega-menu') && ($firstSubItem = getFirstItemLink(obj.activatedItems[level - 1].dataSM('sub'))).length) { $firstSubItem.focusSM(); // if this is the last item of a sub menu, move to the next parent item } else if (level > 1 && ($lastItem = getLastItemLink($ul)).length && $target[0] == $lastItem[0]) { var $parentItem = obj.activatedItems[level - 2].closest('li'), $nextParentItem = null; while ($parentItem.is('li') && !($nextParentItem = getNextItemLink($parentItem, true)).length) { $parentItem = $parentItem.parent().parent(); } if ($nextParentItem.length) { $nextParentItem.focusSM(); } else { getFirstItemLink($root).focusSM(); } } else { getNextItemLink($li).focusSM(); } } else { if (level == 1 && !$root.hasClass('sm-vertical') && !obj.opts.bottomToTopSubMenus) { if (!obj.activatedItems[0] && $target.dataSM('sub')) { if (obj.opts.showOnClick) { obj.clickActivated = true; } obj.itemActivate($target); if ($target.dataSM('sub').is(':visible')) { obj.focusActivated = true; } } if (obj.activatedItems[0] && obj.activatedItems[0].dataSM('sub') && obj.activatedItems[0].dataSM('sub').is(':visible') && !obj.activatedItems[0].dataSM('sub').hasClass('mega-menu')) { getFirstItemLink(obj.activatedItems[0].dataSM('sub')).focusSM(); } } else if (level > 1 || $root.hasClass('sm-vertical')) { getNextItemLink($li).focusSM(); } } break; } e.stopPropagation(); e.preventDefault(); } }); // hook it $(document).on('keydown.smartmenus', 'ul.sm, ul.navbar-nav:not([data-sm-skip])', $.SmartMenus.Keyboard.docKeydown); $.extend($.SmartMenus.prototype, { keyboardSetHotkey: function(keyCode, modifiers) { var self = this; $(document).on('keydown.smartmenus' + this.rootId, function(e) { if (keyCode == e.keyCode) { var procede = true; if (modifiers) { if (typeof modifiers == 'string') { modifiers = [modifiers]; } $.each(['ctrlKey', 'shiftKey', 'altKey', 'metaKey'], function(index, value) { if ($.inArray(value, modifiers) >= 0 && !e[value] || $.inArray(value, modifiers) < 0 && e[value]) { procede = false; return false; } }); } if (procede) { getFirstItemLink(self.$root).focusSM(); e.stopPropagation(); e.preventDefault(); } } }); } }); return $; }));