﻿/*
Copyright © 2013 Adobe Systems Incorporated.
Licensed under the Apache License, Version 2.0 (the “License”);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * See <a href="http://jquery.com">http://jquery.com</a>.
 * @name jquery
 * @class
 * See the jQuery Library  (<a href="http://jquery.com">http://jquery.com</a>) for full details.  This just
 * documents the function and classes that are added to jQuery by this plug-in.
 */

/**
 * See <a href="http://jquery.com">http://jquery.com</a>
 * @name fn
 * @class
 * See the jQuery Library  (<a href="http://jquery.com">http://jquery.com</a>) for full details.  This just
 * documents the function and classes that are added to jQuery by this plug-in.
 * @memberOf jquery
 */

/**
 * @fileOverview accessibleMegaMenu plugin
 *
 *<p>Licensed under the Apache License, Version 2.0 (the “License”)
 *<br />Copyright © 2013 Adobe Systems Incorporated.
 *<br />Project page <a href="https://github.com/adobe-accessibility/Accessible-Mega-Menu">https://github.com/adobe-accessibility/Accessible-Mega-Menu</a>
 * @version 0.1
 * @author Michael Jordan
 * @requires jquery
 */

/*jslint browser: true, devel: true, plusplus: true, nomen: true */

const MegaMenuPanelState = {
  hover: "fw-mega-menu-hover",
  focus: "fw-mega-menu-focus",
  open: "fw-mega-menu-open",
  closed: "fw-mega-menu-closed",
  empty: "fw-mega-menu-empty",
  full: "fw-mega-menu-full",
}

const FW_MEGA_MENU_CSS_CLASS = "fw-mega-menu";
const FW_MEGA_MENU_ITEM_CSS_CLASS = "fw-mega-menu-item";
const FW_MEGA_MENU_ITEM_LINK_CSS_CLASS = "fw-mega-menu-item-link";
const FW_MEGA_PANEL_CSS_CLASS = "fw-mega-menu-panel";
const FW_MEGA_PANEL_GROUP_CSS_CLASS = "fw-mega-menu-panel-group";

const FW_MEGA_PANEL_STATE_HOVER = "fw-mega-menu-hover";
const FW_MEGA_PANEL_STATE_FOCUS = "fw-mega-menu-focus";
const FW_MEGA_PANEL_STATE_OPEN = "fw-mega-menu-open";

/*global jQuery */
(function ($, window, document) {
  "use strict";
  var wasOutsideMenu = false; // Dms.Css implemented variable.  See full description under _mouseOverHandlerForBody
  var pluginName = "accessibleMegaMenu",
    defaults = {
      uuidPrefix: FW_MEGA_MENU_CSS_CLASS, // unique ID's are required to indicate aria-owns, aria-controls and aria-labelledby
      menuClass: FW_MEGA_MENU_CSS_CLASS, // default css class used to define the megamenu styling
      topNavItemClass: FW_MEGA_MENU_ITEM_CSS_CLASS, // default css class for a top-level navigation item in the megamenu
      topNavItemWrapperClass: FW_MEGA_MENU_ITEM_LINK_CSS_CLASS, // Dms.Css implementated property.  Default css class for the element that contains the actual link for the navigation item
      panelClass: FW_MEGA_PANEL_CSS_CLASS, // default css class for a megamenu panel
      panelGroupClass: FW_MEGA_PANEL_GROUP_CSS_CLASS, // default css class for a group of items within a megamenu panel
      hoverClass: FW_MEGA_PANEL_STATE_HOVER, // default css class for the hover state
      focusClass: FW_MEGA_PANEL_STATE_FOCUS, // default css class for the focus state
      openClass: FW_MEGA_PANEL_STATE_OPEN, // default css class for the open state
      useHoverIntent: false, // Dms.Css implemented property.  Determines whether hover intent is used or not.
      hoverIntentSensitivity: 6, // Dms.Css implemented property.  Corresponds to the 'sensitivity' property in the siteHoverIntent.js file.  Gives a way for us to configure the hover intent functionality of megaMenu from our site.js
      hoverIntentInterval: 100, // Dms.Css implemented property.  Corresponds to the 'interval' property in the siteHoverIntent.js file.  Gives a way for us to configure the hover intent functionality of megaMenu from our site.js
      hoverIntentTimeout: 0 // Dms.Css implemented property.  Corresponds to the 'timeout' property in the siteHoverIntent.js file.  Gives a way for us to configure the hover intent functionality of megaMenu from our site.js
    },
    Keyboard = {
      BACKSPACE: 8, COMMA: 188, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, LEFT: 37, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SPACE: 32, TAB: 9, UP: 38,
      keyMap: {
        48: "0", 49: "1", 50: "2", 51: "3", 52: "4", 53: "5", 54: "6", 55: "7", 56: "8", 57: "9", 59: ";", 65: "a", 66: "b", 67: "c", 68: "d", 69: "e", 70: "f", 71: "g", 72: "h",
        73: "i", 74: "j", 75: "k", 76: "l", 77: "m", 78: "n", 79: "o", 80: "p", 81: "q", 82: "r", 83: "s", 84: "t", 85: "u", 86: "v", 87: "w", 88: "x", 89: "y", 90: "z", 96: "0",
        97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", 104: "8", 105: "9", 190: "."
      }
    };
  /**
   * @desc Creates a new accessible mega menu instance.
   * @param {jquery} element
   * @param {object} [options] Mega Menu options
   * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby
   * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling
   * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu
   * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel
   * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel
   * @param {string} [options.hoverClass=hover] - CSS class for the hover state
   * @param {string} [options.focusClass=focus] - CSS class for the focus state
   * @param {string} [options.openClass=open] - CSS class for the open state
   * @constructor
   */
  //function AccessibleMegaMenu(element, options) {
  function AccessibleMegaMenu(element) {
    this.element = element;

    // merge optional settings and defaults into settings
    //this.settings = $.extend({}, defaults, options);
    this.settings = $.extend({}, defaults);

    this._defaults = defaults;
    this._name = pluginName;

    this.mouseTimeoutID = null;
    this.focusTimeoutID = null;
    this.mouseFocused = false;
    this.justFocused = false;

    this.init();
  }

  AccessibleMegaMenu.prototype = (function () {

    /* private attributes and methods ------------------------ */
    var uuid = 0,
      keydownTimeoutDuration = 1000,
      keydownSearchString = "",
      isTouch = typeof window.hasOwnProperty === "function" && !!window.hasOwnProperty("ontouchstart"),
      _getPlugin,
      _addUniqueId,
      _togglePanel,
      _clickHandler,
      _clickOutsideHandler,
      _DOMAttrModifiedHandler,
      _focusInHandler,
      _focusOutHandler,
      _keyDownHandler,
      _mouseDownHandler,
      _mouseOverHandler,
      _mouseOutHandler,
      _toggleExpandedEventHandlers,
      _mouseOverHandlerForBody; // Dms.Css implemented variable to hold the event specified below.

    /**
     * Dms.Css implementation.  UX comp/functionality requires that if a user uses a top menu item to navigate in desktop mode, that if the user keeps their cursor on that item then the mega menu does not expand until you hover off and then hover back on.
     * This is accomplished via the following ...
     * 1) Create a global var in this IIFE that tracks whether you were outside of the menu'ing system.  This is called 'wasOutsideMegaMenu' and is declared at the top.
     * 2) Declare a public setting in accordance with how the authors have declared theirs.  This new setting allows the user to specify the wrapper class of the main top level nav item links.  This is called 'topNavItemWrapperClass' and is important for tracking when the user is inside of the top level nav items.
     * 3) Declare a variable in the accessible menu prototype in accordance with how the authors declared theirs that will hold the event for mouseover'ing inside of any body element 
     * 4) Create the handler in accordance with how the authors have created theirs.  Make sure to use the setting specified in #2 instead of hardcoding.
     * 5) Wire up the mouseover event for $("body *") that will call the handler specified in #4 above.  This is done in accordance with how other events have been wired up - in the prototype's return 'start' event using $.proxy. 
     * 6) Modify the mouseOverHandler to not toggle the menu unless wasOutsideMegaMenu is true. 
     */
    _mouseOverHandlerForBody = function (event) {
      if (event.target.parentElement != undefined) {
        if (event.target.parentElement.className != this.settings.topNavItemWrapperClass) { // If the wrapper class of the target is NOT the top nav item wrapper then we know we're outside the menu.
          event.stopPropagation();
          wasOutsideMenu = true;
          $("body *").off(`mouseover.${FW_MEGA_MENU_CSS_CLASS}`, _mouseOverHandlerForBody); // Unwire this up ASAP so that we don't fire off hundreds of events for no reason.
        }
      } else {
        event.stopPropagation();
        wasOutsideMenu = true; // If the target has no parent then we know we're ouside the menu.
        $("body *").off(`mouseover.${FW_MEGA_MENU_CSS_CLASS}`, _mouseOverHandlerForBody); // Unwire this up ASAP so that we don't fire off hundreds of events for no reason.
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_getPlugin
     * @desc Returns the parent accessibleMegaMenu instance for a given element
     * @param {jQuery} element
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _getPlugin = function (element) {
      return $(element).closest(':data(plugin_' + pluginName + ')').data("plugin_" + pluginName);
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_addUniqueId
     * @desc Adds a unique id and element.
     * The id string starts with the
     * string defined in settings.uuidPrefix.
     * @param {jQuery} element
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _addUniqueId = function (element) {
      element = $(element);
      var settings = this.settings;
      if (!element.attr("id")) {
        element.attr("id", settings.uuidPrefix + "-" + new Date().getTime() + "-" + (++uuid));
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_togglePanel
     * @desc Toggle the display of mega menu panels in response to an event.
     * The optional boolean value 'hide' forces all panels to hide.
     * @param {event} event
     * @param {Boolean} [hide] Hide all mega menu panels when true
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _togglePanel = function (event, hide) {
      var target = $(event.target),
        that = this,
        settings = this.settings,
        menu = this.menu,
        topli = target.closest('.' + settings.topNavItemClass),
        panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass),
        newfocus;

      _toggleExpandedEventHandlers.call(this, true);

      if (hide) {
        topli = menu.find('.' + settings.topNavItemClass + ' .' + settings.openClass + ':first').closest('.' + settings.topNavItemClass);
        if (!(topli.is(event.relatedTarget) || topli.has(event.relatedTarget).length > 0)) {
          if ((event.type === 'mouseout' || event.type === 'focusout') && topli.has(document.activeElement).length > 0) {
            return;
          }
          topli.find('[aria-expanded]')
            .attr('aria-expanded', 'false')
            .removeClass(settings.openClass)
            .filter('.' + settings.panelClass)
            .attr('aria-hidden', 'true');
          if ((event.type === 'keydown' && event.keyCode === Keyboard.ESCAPE) || event.type === 'DOMAttrModified') {
            newfocus = topli.find(':tabbable:first');
            setTimeout(function () {
              menu.find('[aria-expanded].' + that.settings.panelClass).off(`DOMAttrModified.${FW_MEGA_MENU_CSS_CLASS}`);
              newfocus.focus();
              that.justFocused = false;
            }, 99);
          }
        } else if (topli.length === 0) {
          menu.find('[aria-expanded=true]')
            .attr('aria-expanded', 'false')
            .removeClass(settings.openClass)
            .filter('.' + settings.panelClass)
            .attr('aria-hidden', 'true');
        }
      } else {
        clearTimeout(that.focusTimeoutID);
        topli.siblings()
          .find('[aria-expanded]')
          .attr('aria-expanded', 'false')
          .removeClass(settings.openClass)
          .filter('.' + settings.panelClass)
          .attr('aria-hidden', 'true');
        topli.find('[aria-expanded]')
          .attr('aria-expanded', 'true')
          .addClass(settings.openClass)
          .filter('.' + settings.panelClass)
          .attr('aria-hidden', 'false');
        if (event.type === 'mouseover' && target.is(':tabbable') && topli.length === 1 && panel.length === 0 && menu.has(document.activeElement).length > 0) {
          target.focus();
          that.justFocused = false;
        }

        _toggleExpandedEventHandlers.call(that);
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_clickHandler
     * @desc Handle click event on mega menu item
     * @param {event} Event object
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _clickHandler = function (event) {
      var target = $(event.currentTarget),
        topli = target.closest('.' + this.settings.topNavItemClass),
        panel = target.closest('.' + this.settings.panelClass);
      if (topli.length === 1
        && panel.length === 0
        && topli.find('.' + this.settings.panelClass).length === 1) {
        if (!target.hasClass(this.settings.openClass)) {
          event.preventDefault();
          event.stopPropagation();
          _togglePanel.call(this, event);
          this.justFocused = false;
        } else {
          if (this.justFocused) {
            event.preventDefault();
            event.stopPropagation();
            this.justFocused = false;
          } else if (isTouch) {
            event.preventDefault();
            event.stopPropagation();
            _togglePanel.call(this, event, target.hasClass(this.settings.openClass));
          }
        }
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_clickOutsideHandler
     * @desc Handle click event outside of a the megamenu
     * @param {event} Event object
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _clickOutsideHandler = function (event) {
      if ($(event.target).closest(this.menu).length === 0) {
        event.preventDefault();
        event.stopPropagation();
        _togglePanel.call(this, event, true);
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_DOMAttrModifiedHandler
     * @desc Handle DOMAttrModified event on panel to respond to Windows 8 Narrator ExpandCollapse pattern
     * @param {event} Event object
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _DOMAttrModifiedHandler = function (event) {
      if (event.originalEvent.attrName === 'aria-expanded'
        && event.originalEvent.newValue === 'false'
        && $(event.target).hasClass(this.settings.openClass)) {
        event.preventDefault();
        event.stopPropagation();
        _togglePanel.call(this, event, true);
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_focusInHandler
     * @desc Handle focusin event on mega menu item.
     * @param {event} Event object
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _focusInHandler = function (event) {
      clearTimeout(this.focusTimeoutID);
      var target = $(event.target),
        panel = target.closest('.' + this.settings.panelClass);
      target
        .addClass(this.settings.focusClass)
        .on(`click.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_clickHandler, this));
      this.justFocused = !this.mouseFocused;
      this.mouseFocused = false;
      if (this.panels.not(panel).filter('.' + this.settings.openClass).length) {
        _togglePanel.call(this, event);
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_focusOutHandler
     * @desc Handle focusout event on mega menu item.
     * @param {event} Event object
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _focusOutHandler = function (event) {
      this.justFocused = false;
      var that = this,
        target = $(event.target),
        topli = target.closest('.' + this.settings.topNavItemClass),
        keepOpen = false;
      target
        .removeClass(this.settings.focusClass)
        .off(`click.${FW_MEGA_MENU_CSS_CLASS}`);

      if (window.cvox) {
        // If ChromeVox is running...
        that.focusTimeoutID = setTimeout(function () {
          window.cvox.Api.getCurrentNode(function (node) {
            if (topli.has(node).length) {
              // and the current node being voiced is in
              // the mega menu, clearTimeout,
              // so the panel stays open.
              clearTimeout(that.focusTimeoutID);
            } else {
              that.focusTimeoutID = setTimeout(function (scope, event, hide) {
                _togglePanel.call(scope, event, hide);
              }, 275, that, event, true);
            }
          });
        }, 25);
      } else {
        that.focusTimeoutID = setTimeout(function () {
          _togglePanel.call(that, event, true);
        }, 300);
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_keyDownHandler
     * @desc Handle keydown event on mega menu.
     * @param {event} Event object
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _keyDownHandler = function (event) {
      var that = (this.constructor === AccessibleMegaMenu) ? this : _getPlugin(this), // determine the AccessibleMegaMenu plugin instance
        settings = that.settings,
        target = $($(this).is('.' + settings.hoverClass + ':tabbable') ? this : event.target), // if the element is hovered the target is this, otherwise, its the focused element
        menu = that.menu,
        topnavitems = that.topnavitems,
        topli = target.closest('.' + settings.topNavItemClass),
        tabbables = menu.find(':tabbable'),
        panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass),
        panelGroups = panel.find('.' + settings.panelGroupClass),
        currentPanelGroup = target.closest('.' + settings.panelGroupClass),
        next,
        keycode = event.keyCode || event.which,
        start,
        i,
        o,
        label,
        found = false,
        newString = Keyboard.keyMap[event.keyCode] || '',
        regex,
        isTopNavItem = (topli.length === 1 && panel.length === 0);

      if (target.is("input:focus, select:focus, textarea:focus, button:focus")) {
        // if the event target is a form element we should handle keydown normally
        return;
      }

      if (target.is('.' + settings.hoverClass + ':tabbable')) {
        $('html').off(`keydown.${FW_MEGA_MENU_CSS_CLASS}`);
      }

      switch (keycode) {
        case Keyboard.ESCAPE:
          _togglePanel.call(that, event, true);
          break;
        case Keyboard.DOWN:
          event.preventDefault();
          if (isTopNavItem) {
            _togglePanel.call(that, event);
            found = (topli.find('.' + settings.panelClass + ' :tabbable:first').focus().length === 1);
          } else {
            found = (tabbables.filter(':gt(' + tabbables.index(target) + '):first').focus().length === 1);
          }

          if (!found && window.opera && opera.toString() === "[object Opera]" && (event.ctrlKey || event.metaKey)) {
            tabbables = $(':tabbable');
            i = tabbables.index(target);
            found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1);
          }
          break;
        case Keyboard.UP:
          event.preventDefault();
          if (isTopNavItem && target.hasClass(settings.openClass)) {
            _togglePanel.call(that, event, true);
            next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last');
            if (next.children('.' + settings.panelClass).length) {
              found = (next.children()
                .attr('aria-expanded', 'true')
                .addClass(settings.openClass)
                .filter('.' + settings.panelClass)
                .attr('aria-hidden', 'false')
                .find(':tabbable:last')
                .focus() === 1);
            }
          } else if (!isTopNavItem) {
            found = (tabbables.filter(':lt(' + tabbables.index(target) + '):last').focus().length === 1);
          }

          if (!found && window.opera && opera.toString() === "[object Opera]" && (event.ctrlKey || event.metaKey)) {
            tabbables = $(':tabbable');
            i = tabbables.index(target);
            found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):first').focus().length === 1);
          }
          break;
        case Keyboard.RIGHT:
          event.preventDefault();
          if (isTopNavItem) {
            found = (topnavitems.filter(':gt(' + topnavitems.index(topli) + '):first').find(':tabbable:first').focus().length === 1);
          } else {
            if (panelGroups.length && currentPanelGroup.length) {
              // if the current panel contains panel groups, and we are able to focus the first tabbable element of the next panel group
              found = (panelGroups.filter(':gt(' + panelGroups.index(currentPanelGroup) + '):first').find(':tabbable:first').focus().length === 1);
            }

            if (!found) {
              found = (topli.find(':tabbable:first').focus().length === 1);
            }
          }
          break;
        case Keyboard.LEFT:
          event.preventDefault();
          if (isTopNavItem) {
            found = (topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last').find(':tabbable:first').focus().length === 1);
          } else {
            if (panelGroups.length && currentPanelGroup.length) {
              // if the current panel contains panel groups, and we are able to focus the first tabbable element of the previous panel group
              found = (panelGroups.filter(':lt(' + panelGroups.index(currentPanelGroup) + '):last').find(':tabbable:first').focus().length === 1);
            }

            if (!found) {
              found = (topli.find(':tabbable:first').focus().length === 1);
            }
          }
          break;
        case Keyboard.TAB:
          i = tabbables.index(target);
          if (event.shiftKey && isTopNavItem && target.hasClass(settings.openClass)) {
            _togglePanel.call(that, event, true);
            next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last');
            if (next.children('.' + settings.panelClass).length) {
              found = next.children()
                .attr('aria-expanded', 'true')
                .addClass(settings.openClass)
                .filter('.' + settings.panelClass)
                .attr('aria-hidden', 'false')
                .find(':tabbable:last')
                .focus();
            }
          } else if (event.shiftKey && i > 0) {
            found = (tabbables.filter(':lt(' + i + '):last').focus().length === 1);
          } else if (!event.shiftKey && i < tabbables.length - 1) {
            found = (tabbables.filter(':gt(' + i + '):first').focus().length === 1);
          } else if (window.opera && opera.toString() === "[object Opera]") {
            tabbables = $(':tabbable');
            i = tabbables.index(target);
            if (event.shiftKey) {
              found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):last').focus().length === 1);
            } else {
              found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1);
            }
          }

          if (found) {
            event.preventDefault();
          }
          break;
        case Keyboard.SPACE:
          if (isTopNavItem) {
            event.preventDefault();
            _clickHandler.call(that, event);
          } else {
            return true;
          }
          break;
        case Keyboard.ENTER:
          return true;
          break;
        default:
          // alphanumeric filter
          clearTimeout(this.keydownTimeoutID);

          keydownSearchString += newString !== keydownSearchString ? newString : '';

          if (keydownSearchString.length === 0) {
            return;
          }

          this.keydownTimeoutID = setTimeout(function () {
            keydownSearchString = '';
          }, keydownTimeoutDuration);

          if (isTopNavItem && !target.hasClass(settings.openClass)) {
            tabbables = tabbables.filter(':not(.' + settings.panelClass + ' :tabbable)');
          } else {
            tabbables = topli.find(':tabbable');
          }

          if (event.shiftKey) {
            tabbables = $(tabbables.get()
              .reverse());
          }

          for (i = 0; i < tabbables.length; i++) {
            o = tabbables.eq(i);
            if (o.is(target)) {
              start = (keydownSearchString.length === 1) ? i + 1 : i;
              break;
            }
          }

          regex = new RegExp('^' + keydownSearchString.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'), 'i');

          for (i = start; i < tabbables.length; i++) {
            o = tabbables.eq(i);
            label = $.trim(o.text());
            if (regex.test(label)) {
              found = true;
              o.focus();
              break;
            }
          }
          if (!found) {
            for (i = 0; i < start; i++) {
              o = tabbables.eq(i);
              label = $.trim(o.text());
              if (regex.test(label)) {
                o.focus();
                break;
              }
            }
          }
          break;
      }
      that.justFocused = false;
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_mouseDownHandler
     * @desc Handle mousedown event on mega menu.
     * @param {event} Event object
     * @memberof accessibleMegaMenu
     * @inner
     * @private
     */
    _mouseDownHandler = function (event) {
      if ($(event.target).is(this.settings.panelClass) || $(event.target).closest(":focusable").length) {
        this.mouseFocused = true;
      }
      this.mouseTimeoutID = setTimeout(function () {
        clearTimeout(this.focusTimeoutID);
      }, 1);
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_mouseOverHandler
     * @desc Handle mouseover event on mega menu.
     * @param {event} Event object
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _mouseOverHandler = function (event) {
      clearTimeout(this.mouseTimeoutID);
      $(event.target)
        .addClass(this.settings.hoverClass);
      if (wasOutsideMenu || isTouch) {
        _togglePanel.call(this, event);
      }
      if ($(event.target).is(':tabbable')) {
        $('html').on(`keydown.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_keyDownHandler, event.target));
      }
    };

    /**
     * @name jQuery.fn.accessibleMegaMenu~_mouseOutHandler
     * @desc Handle mouseout event on mega menu.
     * @param {event} Event object
     * @memberof jQuery.fn.accessibleMegaMenu
     * @inner
     * @private
     */
    _mouseOutHandler = function (event) {
      var that = this;
      $(event.target)
        .removeClass(that.settings.hoverClass);

      that.mouseTimeoutID = setTimeout(function () {
        _togglePanel.call(that, event, true);
      }, 250);
      if ($(event.target).is(':tabbable')) {
        $('html').off(`keydown.${FW_MEGA_MENU_CSS_CLASS}`);
      }
    };

    _toggleExpandedEventHandlers = function (hide) {
      var menu = this.menu;
      if (hide) {
        $('html').off(`mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-${FW_MEGA_MENU_CSS_CLASS},  pointerup.outside-${FW_MEGA_MENU_CSS_CLASS}`);

        menu.find('[aria-expanded].' + this.settings.panelClass).off(`DOMAttrModified.${FW_MEGA_MENU_CSS_CLASS}`);
      } else {
        $('html').on(`mouseup.outside-${FW_MEGA_MENU_CSS_CLASS}, touchend.outside-${FW_MEGA_MENU_CSS_CLASS}, mspointerup.outside-${FW_MEGA_MENU_CSS_CLASS},  pointerup.outside-${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_clickOutsideHandler, this));

        /* Narrator in Windows 8 automatically toggles the aria-expanded property on double tap or click.
           To respond to the change to collapse the panel, we must add a listener for a DOMAttrModified event. */
        menu.find('[aria-expanded=true].' + this.settings.panelClass).on(`DOMAttrModified.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_DOMAttrModifiedHandler, this));
      }
    };

    /* public attributes and methods ------------------------- */
    return {
      constructor: AccessibleMegaMenu,

      /**
       * @lends jQuery.fn.accessibleMegaMenu
       * @desc Initializes an instance of the accessibleMegaMenu plugins
       * @memberof jQuery.fn.accessibleMegaMenu
       * @instance
       */
      init: function () {
        var settings = this.settings,
          nav = $(this.element),
          menu = nav.children().first(),
          topnavitems = menu.children();
        this.start(settings, nav, menu, topnavitems);
        nav.show();
      },

      start: function (settings, nav, menu, topnavitems) {
        var that = this;
        this.settings = settings;
        this.menu = menu;
        this.topnavitems = topnavitems;

        nav.attr("role", "navigation");
        menu.addClass(settings.menuClass);
        topnavitems.each(function (i, topnavitem) {
          var topnavitemlink, topnavitempanel;
          topnavitem = $(topnavitem);
          topnavitem.addClass(settings.topNavItemClass);
          topnavitemlink = topnavitem.find(":tabbable:first");
          topnavitempanel = topnavitem.children(":not(:tabbable):last");
          _addUniqueId.call(that, topnavitemlink);
          if (topnavitempanel.length) {
            _addUniqueId.call(that, topnavitempanel);
            topnavitemlink.attr({
              "aria-haspopup": true,
              "aria-controls": topnavitempanel.attr("id"),
              "aria-expanded": false
            });

            topnavitempanel.attr({
              "role": "group",
              "aria-expanded": false,
              "aria-hidden": true
            })
              .addClass(settings.panelClass)
              .not("[aria-labelledby]")
              .attr("aria-labelledby", topnavitemlink.attr("id"));
          }
        });

        this.panels = menu.find("." + settings.panelClass);

        menu.on(`focusin.${FW_MEGA_MENU_CSS_CLASS}`, ":focusable, ." + settings.panelClass, $.proxy(_focusInHandler, this))
          .on(`focusout.${FW_MEGA_MENU_CSS_CLASS}`, ":focusable, ." + settings.panelClass, $.proxy(_focusOutHandler, this))
          .on(`keydown.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_keyDownHandler, this));

        // Dms.Css implementation.  If the setting 'userHoverIntent' is set to true, the mouseover and mouseout handlers will not be added directly.  Rather, a hoverIntent will be wired up to the top LIs in the menu.
        if (settings.useHoverIntent && !isTouch) {
          menu.children().hoverIntent({
            over: $.proxy(_mouseOverHandler, this),
            out: $.proxy(_mouseOutHandler, this),
            timeout: settings.hoverIntentTimeout,
            sensitivity: settings.hoverIntentSensitivity,
            interval: settings.hoverIntentInterval
          });
        } else {
          menu.on(`mouseover.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_mouseOverHandler, this))
            .on(`mouseout.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_mouseOutHandler, this));
        }

        menu.on(`mousedown.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_mouseDownHandler, this));

        if (isTouch) {
          menu.on(`touchstart.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_clickHandler, this));
        } else {
          // Dms.Css implementation.  Please see the comments for _mouseOverHandlerForBody for a full explanation.  Note that this will be unwired very quickly. 
          $("body *").on(`mouseover.${FW_MEGA_MENU_CSS_CLASS}`, $.proxy(_mouseOverHandlerForBody, this));
        }

        menu.find("hr").attr("role", "separator");

        if ($(document.activeElement).closest(menu).length) {
          $(document.activeElement).trigger(`focusin.${FW_MEGA_MENU_CSS_CLASS}`);
        }
      },

      /**
       * @desc Get default values
       * @example $(selector).accessibleMegaMenu("getDefaults");
       * @return {object}
       * @memberof jQuery.fn.accessibleMegaMenu
       * @instance
       */
      getDefaults: function () {
        return this._defaults;
      },

      /**
       * @desc Get any option set to plugin using its name (as string)
       * @example $(selector).accessibleMegaMenu("getOption", some_option);
       * @param {string} opt
       * @return {string}
       * @memberof jQuery.fn.accessibleMegaMenu
       * @instance
       */
      getOption: function (opt) {
        return this.settings[opt];
      },

      /**
       * @desc Get all options
       * @example $(selector).accessibleMegaMenu("getAllOptions");
       * @return {object}
       * @memberof jQuery.fn.accessibleMegaMenu
       * @instance
       */
      getAllOptions: function () {
        return this.settings;
      },

      /**
       * @desc Set option
       * @example $(selector).accessibleMegaMenu("setOption", "option_name",  "option_value",  reinitialize);
       * @param {string} opt - Option name
       * @param {string} val - Option value
       * @param {boolean} [reinitialize] - boolean to re-initialize the menu.
       * @memberof jQuery.fn.accessibleMegaMenu
       * @instance
       */
      setOption: function (opt, value, reinitialize) {
        this.settings[opt] = value;
        if (reinitialize) {
          this.init();
        }
      }
    };
  }());

  /* lightweight plugin wrapper around the constructor,
     to prevent against multiple instantiations */

  /**
   * @class accessibleMegaMenu
   * @memberOf jQuery.fn
   * @classdesc Implements an accessible mega menu as a jQuery plugin.
   * <p>The mega-menu It is modeled after the mega menu on {@link http://adobe.com|adobe.com} but has been simplified for use by others. A brief description of the interaction design choices can be found in a blog post at {@link http://blogs.adobe.com/accessibility/2013/05/adobe-com.html|Mega menu accessibility on adobe.com}.</p>
   * <h3>Keyboard Accessibility</h3>
   * <p>The accessible mega menu supports keyboard interaction modeled after the behavior described in the {@link http://www.w3.org/TR/wai-aria-practices/#menu|WAI-ARIA Menu or Menu bar (widget) design pattern}, however we also try to respect users' general expectations for the behavior of links in a global navigation. To this end, the accessible mega menu implementation permits tab focus on each of the six top-level menu items. When one of the menu items has focus, pressing the Enter key, Spacebar or Down arrow will open the submenu panel, and pressing the Left or Right arrow key will shift focus to the adjacent menu item. Links within the submenu panels are included in the tab order when the panel is open. They can also be navigated with the arrow keys or by typing the first character in the link name, which speeds up keyboard navigation considerably. Pressing the Escape key closes the submenu and restores focus to the parent menu item.</p>
   * <h3>Screen Reader Accessibility</h3>
   * <p>The accessible mega menu models its use of WAI-ARIA Roles, States, and Properties after those described in the {@link http://www.w3.org/TR/wai-aria-practices/#menu|WAI-ARIA Menu or Menu bar (widget) design pattern} with some notable exceptions, so that it behaves better with screen reader user expectations for global navigation. We don't use <code class="prettyprint prettyprinted" style=""><span class="pln">role</span><span class="pun">=</span><span class="str">"menu"</span></code> for the menu container and <code class="prettyprint prettyprinted" style=""><span class="pln">role</span><span class="pun">=</span><span class="str">"menuitem"</span></code> for each of the links therein, because if we do, assistive technology will no longer interpret the links as links, but instead, as menu items, and the links in our global navigation will no longer show up when a screen reader user executes a shortcut command to bring up a list of links in the page.</p>
   * @example <h4>HTML</h4><hr/>
&lt;nav&gt;
  &lt;ul class=&quot;nav-menu&quot;&gt;
      &lt;li class=&quot;nav-item&quot;&gt;
          &lt;a href=&quot;?movie&quot;&gt;Movies&lt;/a&gt;
          &lt;div class=&quot;sub-nav&quot;&gt;
              &lt;ul class=&quot;sub-nav-group&quot;&gt;
                  &lt;li&gt;&lt;a href=&quot;?movie&amp;genre=0&quot;&gt;Action &amp;amp; Adventure&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&lt;a href=&quot;?movie&amp;genre=2&quot;&gt;Children &amp;amp; Family&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&amp;#8230;&lt;/li&gt;
              &lt;/ul&gt;
              &lt;ul class=&quot;sub-nav-group&quot;&gt;
                  &lt;li&gt;&lt;a href=&quot;?movie&amp;genre=7&quot;&gt;Dramas&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&lt;a href=&quot;?movie&amp;genre=9&quot;&gt;Foreign&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&amp;#8230;&lt;/li&gt;
              &lt;/ul&gt;
              &lt;ul class=&quot;sub-nav-group&quot;&gt;
                  &lt;li&gt;&lt;a href=&quot;?movie&amp;genre=14&quot;&gt;Musicals&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&lt;a href=&quot;?movie&amp;genre=15&quot;&gt;Romance&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&amp;#8230;&lt;/li&gt;
              &lt;/ul&gt;
          &lt;/div&gt;
      &lt;/li&gt;
      &lt;li class=&quot;nav-item&quot;&gt;
          &lt;a href=&quot;?tv&quot;&gt;TV Shows&lt;/a&gt;
          &lt;div class=&quot;sub-nav&quot;&gt;
              &lt;ul class=&quot;sub-nav-group&quot;&gt;
                  &lt;li&gt;&lt;a href=&quot;?tv&amp;genre=20&quot;&gt;Classic TV&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&lt;a href=&quot;?tv&amp;genre=21&quot;&gt;Crime TV&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&amp;#8230;&lt;/li&gt;
              &lt;/ul&gt;
              &lt;ul class=&quot;sub-nav-group&quot;&gt;
                  &lt;li&gt;&lt;a href=&quot;?tv&amp;genre=27&quot;&gt;Reality TV&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&lt;a href=&quot;?tv&amp;genre=30&quot;&gt;TV Action&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&amp;#8230;&lt;/li&gt;
              &lt;/ul&gt;
              &lt;ul class=&quot;sub-nav-group&quot;&gt;
                  &lt;li&gt;&lt;a href=&quot;?tv&amp;genre=33&quot;&gt;TV Dramas&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&lt;a href=&quot;?tv&amp;genre=34&quot;&gt;TV Horror&lt;/a&gt;&lt;/li&gt;
                  &lt;li&gt;&amp;#8230;&lt;/li&gt;
              &lt;/ul&gt;
          &lt;/div&gt;
      &lt;/li&gt;
  &lt;/ul&gt;
&lt;/nav&gt;
   * @example <h4>CSS</h4><hr/>
&#47;* Rudimentary mega menu CSS for demonstration *&#47;
&#47;* mega menu list *&#47;
.nav-menu {
  display: block;
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  z-index: 15;
}
&#47;* a top level navigation item in the mega menu *&#47;
.nav-item {
  list-style: none;
  display: inline-block;
  padding: 0;
  margin: 0;
}
&#47;* first descendant link within a top level navigation item *&#47;
.nav-item &gt; a {
  position: relative;
  display: inline-block;
  padding: 0.5em 1em;
  margin: 0 0 -1px 0;
  border: 1px solid transparent;
}
&#47;* focus/open states of first descendant link within a top level
 navigation item *&#47;
.nav-item &gt; a:focus,
.nav-item &gt; a.open {
  border: 1px solid #dedede;
}
&#47;* open state of first descendant link within a top level
 navigation item *&#47;
.nav-item &gt; a.open {
  background-color: #fff;
  border-bottom: none;
  z-index: 1;
}
&#47;* sub-navigation panel *&#47;
.sub-nav {
  position: absolute;
  display: none;
  top: 2.2em;
  margin-top: -1px;
  padding: 0.5em 1em;
  border: 1px solid #dedede;
  background-color: #fff;
}
&#47;* sub-navigation panel open state *&#47;
.sub-nav.open {
  display: block;
}
&#47;* list of items within sub-navigation panel *&#47;
.sub-nav ul {
  display: inline-block;
  vertical-align: top;
  margin: 0 1em 0 0;
  padding: 0;
}
&#47;* list item within sub-navigation panel *&#47;
.sub-nav li {
  display: block;
  list-style-type: none;
  margin: 0;
  padding: 0;
}
   * @example <h4>JavaScript</h4><hr/>
&lt;!-- include jquery --&gt;
&lt;script src=&quot;http://code.jquery.com/jquery-1.10.1.min.js&quot;&gt;&lt;/script&gt;
&lt;!-- include the jquery-accessibleMegaMenu plugin script --&gt;
&lt;script src=&quot;js/jquery-accessibleMegaMenu.js&quot;&gt;&lt;/script&gt;
&lt;!-- initialize a selector as an accessibleMegaMenu --&gt;
&lt;script&gt;
  $(&quot;nav:first&quot;).accessibleMegaMenu({
      &#47;* prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby *&#47;
      uuidPrefix: &quot;accessible-megamenu&quot;,
      &#47;* css class used to define the megamenu styling *&#47;
      menuClass: &quot;nav-menu&quot;,
      &#47;* css class for a top-level navigation item in the megamenu *&#47;
      topNavItemClass: &quot;nav-item&quot;,
      &#47;* css class for a megamenu panel *&#47;
      panelClass: &quot;sub-nav&quot;,
      &#47;* css class for a group of items within a megamenu panel *&#47;
      panelGroupClass: &quot;sub-nav-group&quot;,
      &#47;* css class for the hover state *&#47;
      hoverClass: &quot;hover&quot;,
      &#47;* css class for the focus state *&#47;
      focusClass: &quot;focus&quot;,
      &#47;* css class for the open state *&#47;
      openClass: &quot;open&quot;
  });
&lt;/script&gt;
   * @param {object} [options] Mega Menu options
   * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby
   * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling
   * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu
   * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel
   * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel
   * @param {string} [options.hoverClass=hover] - CSS class for the hover state
   * @param {string} [options.focusClass=focus] - CSS class for the focus state
   * @param {string} [options.openClass=open] - CSS class for the open state
   */
  $.fn[pluginName] = function (options) {
    return this.each(function () {
      if (!$.data(this, "plugin_" + pluginName)) {
        $.data(this, "plugin_" + pluginName, new $.fn[pluginName].AccessibleMegaMenu(this, options));
      }
    });
  };

  $.fn[pluginName].AccessibleMegaMenu = AccessibleMegaMenu;

  /* :focusable and :tabbable selectors from
     https://raw.github.com/jquery/jquery-ui/master/ui/jquery.ui.core.js */

  /**
   * @private
   */
  function visible(element) {
    return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function () {
      return $.css(this, "visibility") === "hidden";
    }).length;
  }

  /**
   * @private
   */
  function focusable(element, isTabIndexNotNaN) {
    var map, mapName, img,
      nodeName = element.nodeName.toLowerCase();
    if ("area" === nodeName) {
      map = element.parentNode;
      mapName = map.name;
      if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") {
        return false;
      }
      img = $("img[usemap=#" + mapName + "]")[0];
      return !!img && visible(img);
    }
    return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled :
      "a" === nodeName ?
        element.href || isTabIndexNotNaN :
        isTabIndexNotNaN) &&
      // the element and all of its ancestors must be visible
      visible(element);
  }

  $.extend($.expr[":"], {
    data: $.expr.createPseudo ? $.expr.createPseudo(function (dataName) {
      return function (elem) {
        return !!$.data(elem, dataName);
      };
    }) : // support: jQuery <1.8
      function (elem, i, match) {
        return !!$.data(elem, match[3]);
      },

    focusable: function (element) {
      return focusable(element, !isNaN($.attr(element, "tabindex")));
    },

    tabbable: function (element) {
      var tabIndex = $.attr(element, "tabindex"),
        isTabIndexNaN = isNaN(tabIndex);
      return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN);
    }
  });
}($, window, document));