/* 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 http://jquery.com. * @name jquery * @class * See the jQuery Library (http://jquery.com) for full details. This just * documents the function and classes that are added to jQuery by this plug-in. */ /** * See http://jquery.com * @name fn * @class * See the jQuery Library (http://jquery.com) for full details. This just * documents the function and classes that are added to jQuery by this plug-in. * @memberOf jquery */ /** * @fileOverview accessibleMegaMenu plugin * *

Licensed under the Apache License, Version 2.0 (the “License”) *
Copyright © 2013 Adobe Systems Incorporated. *
Project page https://github.com/adobe-accessibility/Accessible-Mega-Menu * @version 0.1 * @author Michael Jordan * @requires jquery */ /*jslint browser: true, devel: true, plusplus: true, nomen: true */ /*global jQuery, window, document */ (function ($, window, document) { "use strict"; var pluginName = "accessibleMegaMenu", defaults = { uuidPrefix: "accessible-megamenu", // unique ID's are required to indicate aria-owns, aria-controls and aria-labelledby menuClass: "accessible-megamenu", // default css class used to define the megamenu styling topNavItemClass: "accessible-megamenu-top-nav-item", // default css class for a top-level navigation item in the megamenu panelClass: "accessible-megamenu-panel", // default css class for a megamenu panel panelGroupClass: "accessible-megamenu-panel-group", // default css class for a group of items within a megamenu panel hoverClass: "hover", // default css class for the hover state focusClass: "focus", // default css class for the focus state openClass: "open", // default css class for the open state, toggleButtonClass: "accessible-megamenu-toggle", // default css class responsive toggle button openDelay: 0, // default open delay when opening menu via mouseover closeDelay: 250, // default open delay when opening menu via mouseover openOnMouseover: false // default setting for whether menu should open on mouseover }, 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: "." } }, clearTimeout = window.clearTimeout, setTimeout = window.setTimeout, isOpera = window.opera && window.opera.toString() === '[object Opera]'; /** * @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) { this.element = element; // merge optional settings and defaults into settings this.settings = $.extend({}, defaults, options); 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 = 'ontouchstart' in window || window.navigator.msMaxTouchPoints, _getPlugin, _addUniqueId, _togglePanel, _clickHandler, _touchmoveHandler, _clickOutsideHandler, _DOMAttrModifiedHandler, _focusInHandler, _focusOutHandler, _keyDownHandler, _mouseDownHandler, _mouseOverHandler, _mouseOutHandler, _clickToggleHandler, _toggleExpandedEventHandlers, _addEventHandlers, _removeEventHandlers; /** * @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); // Hide all panels. if (hide) { // Get the first top level menu item. topli = menu.find('.' + settings.topNavItemClass + ' .' + settings.openClass + ':first').closest('.' + settings.topNavItemClass); // Validate event. 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; } // Close top level link. topli.find('[aria-expanded]') .attr('aria-expanded', 'false') .removeClass(settings.openClass) // Close panel. topli.find('.' + settings.panelClass) .removeClass(settings.openClass) .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.accessible-megamenu'); newfocus.focus(); that.justFocused = false; }, 99); } } else if (topli.length === 0) { menu.find('[aria-expanded=true]') .attr('aria-expanded', 'false') .removeClass(settings.openClass) .closest('.' + settings.panelClass) .removeClass(settings.openClass) .attr('aria-hidden', 'true'); } } // Toggle panels. else { clearTimeout(that.focusTimeoutID); // Close previously open top level link and its panel. var openli = menu.find('[aria-expanded=true]').parent(); if (!openli.is(topli)) { openli.find('[aria-expanded]') .attr('aria-expanded', 'false') .removeClass(settings.openClass) .siblings('.' + settings.panelClass) .removeClass(settings.openClass) .attr('aria-hidden', 'true'); } // Open current top level link and its panel. topli.find('[aria-expanded]') .attr('aria-expanded', 'true') .addClass(settings.openClass) .siblings('.' + settings.panelClass) .addClass(settings.openClass) .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.target).closest(':tabbable'), topli = target.closest('.' + this.settings.topNavItemClass), panel = target.closest('.' + this.settings.panelClass); // With panel. if (topli.length === 1 && panel.length === 0 && topli.find('.' + this.settings.panelClass).length === 1) { // Handle click event. if (!target.hasClass(this.settings.openClass)) { event.preventDefault(); event.stopPropagation(); _togglePanel.call(this, event); this.justFocused = false; } else { // Handle focus event. if (this.justFocused) { event.preventDefault(); event.stopPropagation(); this.justFocused = false; } // Handle touch/click event. else if (isTouch || !isTouch && !this.settings.openOnMouseover) { event.preventDefault(); event.stopPropagation(); _togglePanel.call(this, event, target.hasClass(this.settings.openClass)); } } } // Without panel on enter event. else if (topli.length === 1 && panel.length === 0 && event.type == "keydown") { window.location.href = target.attr("href"); } }; /** * @name jQuery.fn.accessibleMegaMenu~_touchmoveHandler * @desc Handle touch move event on menu * @memberof jQuery.fn.accessibleMegaMenu * @inner * @private */ _touchmoveHandler = function () { this.justMoved = true; }; /** * @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); this.justFocused = !this.mouseFocused || (!this.settings.openOnMouseover && this.mouseFocused); this.mouseFocused = false; if (this.justFocused && 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); target .removeClass(this.settings.focusClass); if (typeof window.cvox === 'object' && typeof window.cvox.Api !== 'object') { // 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 () { if (that.mouseFocused && event.relatedTarget === null) { return; } _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.accessible-megamenu'); } switch (keycode) { case Keyboard.ESCAPE: this.mouseFocused = false; _togglePanel.call(that, event, true); break; case Keyboard.DOWN: event.preventDefault(); this.mouseFocused = false; 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 && isOpera && (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(); this.mouseFocused = false; 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.find('[aria-expanded]') .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 && isOpera && (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(); this.mouseFocused = false; 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(); this.mouseFocused = false; 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: this.mouseFocused = false; 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 (isOpera) { 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: case Keyboard.ENTER: // Top level links. if (isTopNavItem) { event.preventDefault(); // Handle enter event on open top level link as escape event. if (target.hasClass("open")) { this.mouseFocused = false; _togglePanel.call(that, event, true); } // Handle enter event on top level link as a click. else { _clickHandler.call(that, event); } } // Sub level links. else { 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).closest(this.settings.panelClass) || $(event.target).closest(":focusable").length) { this.mouseFocused = true; if ($(event.target).closest(this.settings.menuClass)) { $('html').on('keydown.accessible-megamenu', $.proxy(_keyDownHandler, event.target)); } } clearTimeout(this.mouseTimeoutID); 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); var that = this; if (!that.settings.openOnMouseover) { return; } this.mouseTimeoutID = setTimeout(function () { $(event.target).addClass(that.settings.hoverClass); _togglePanel.call(that, event); if ($(event.target).closest(that.settings.menuClass)) { $('html').on('keydown.accessible-megamenu', $.proxy(_keyDownHandler, event.target)); } }, this.settings.openDelay); }; /** * @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) { clearTimeout(this.mouseTimeoutID); var that = this; if (!that.settings.openOnMouseover) { return; } $(event.target) .removeClass(that.settings.hoverClass); that.mouseTimeoutID = setTimeout(function () { _togglePanel.call(that, event, true); }, this.settings.closeDelay); if ($(event.target).is(':tabbable')) { $('html').off('keydown.accessible-megamenu'); } }; /** * @name jQuery.fn.accessibleMegaMenu~_clickToggleHandler * @desc Handle click event on menu toggle button. * @memberof jQuery.fn.accessibleMegaMenu * @inner * @private */ _clickToggleHandler = function () { var isExpanded = this.toggleButton.attr('aria-expanded') === 'true'; this.toggleButton.attr({'aria-expanded': !isExpanded}); }; _toggleExpandedEventHandlers = function (hide) { var menu = this.menu; if (hide) { $('html').off('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu'); menu.find('[aria-expanded].' + this.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); } else { $('html').on('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu', $.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.accessible-megamenu', $.proxy(_DOMAttrModifiedHandler, this)); } }; _addEventHandlers = function() { var menu = this.menu, toggleButton = this.toggleButton; menu.on("focusin.accessible-megamenu", ":focusable, ." + this.settings.panelClass, $.proxy(_focusInHandler, this)) .on("focusout.accessible-megamenu", ":focusable, ." + this.settings.panelClass, $.proxy(_focusOutHandler, this)) .on("keydown.accessible-megamenu", $.proxy(_keyDownHandler, this)) .on("mouseover.accessible-megamenu", $.proxy(_mouseOverHandler, this)) .on("mouseout.accessible-megamenu", $.proxy(_mouseOutHandler, this)) .on("mousedown.accessible-megamenu", $.proxy(_mouseDownHandler, this)) .on("click.accessible-megamenu", $.proxy(_clickHandler, this)); toggleButton.on('click.accessible-megamenu', $.proxy(_clickToggleHandler, this)); if (isTouch) { menu.on("touchmove.accessible-megamenu", $.proxy(_touchmoveHandler, this)); } if ($(document.activeElement).closest(menu).length) { $(document.activeElement).trigger("focusin.accessible-megamenu"); } }; _removeEventHandlers = function () { var menu = this.menu, toggleButton = this.toggleButton; menu.off('.accessible-megamenu'); if (menu.find('[aria-expanded=true].' + this.settings.panelClass).length) { _toggleExpandedEventHandlers.call(this, true); } toggleButton.off('.accessible-megamenu'); }; /* 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('ol,ul').first(), topnavitems = menu.children(), toggleButton = nav.children('button').first(); this.start(settings, nav, menu, topnavitems, toggleButton); }, start: function(settings, nav, menu, topnavitems, toggleButton) { var that = this; this.settings = settings; this.menu = menu; this.topnavitems = topnavitems; this.toggleButton = toggleButton; nav.attr("role", "navigation"); _addUniqueId.call(that, menu); menu.addClass(settings.menuClass); menu.addClass(['js', settings.menuClass].join('-')); topnavitems.each(function (i, topnavitem) { var topnavitemlink, topnavitempanel; topnavitem = $(topnavitem); topnavitem.addClass(settings.topNavItemClass); topnavitemlink = topnavitem.find("a").first(); topnavitempanel = topnavitem.find('.' + settings.panelClass); _addUniqueId.call(that, topnavitemlink); // When sub nav exists. if (topnavitempanel.length) { _addUniqueId.call(that, topnavitempanel); // Add attributes to top level link. topnavitemlink.attr({ "role": "button", "aria-controls": topnavitempanel.attr("id"), "aria-expanded": false, "tabindex": 0 }); // Add attributes to sub nav. topnavitempanel.attr({ "role": "region", "aria-hidden": true }) .addClass(settings.panelClass) .not("[aria-labelledby]") .attr("aria-labelledby", topnavitemlink.attr("id")); } }); this.panels = menu.find("." + settings.panelClass); menu.find("hr").attr("role", "separator"); toggleButton.addClass(settings.toggleButtonClass); toggleButton.attr({'aria-expanded': false, 'aria-controls': menu.attr('id')}); _addEventHandlers.call(this); }, /** * @desc Removes maga menu javascript behavior * @example $(selector).accessibleMegaMenu("destroy"); * @return {object} * @memberof jQuery.fn.accessibleMegaMenu * @instance */ destroy: function () { this.menu.removeClass(['js', this.settings.menuClass].join('-')); _removeEventHandlers.call(this, true); }, /** * @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. *

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}.

*

Keyboard Accessibility

*

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.

*

Screen Reader Accessibility

*

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 role="menu" for the menu container and role="menuitem" 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.

* @example

HTML


<nav> <ul class="nav-menu"> <li class="nav-item"> <a href="?movie">Movies</a> <div class="sub-nav"> <ul class="sub-nav-group"> <li><a href="?movie&genre=0">Action &amp; Adventure</a></li> <li><a href="?movie&genre=2">Children &amp; Family</a></li> <li>&#8230;</li> </ul> <ul class="sub-nav-group"> <li><a href="?movie&genre=7">Dramas</a></li> <li><a href="?movie&genre=9">Foreign</a></li> <li>&#8230;</li> </ul> <ul class="sub-nav-group"> <li><a href="?movie&genre=14">Musicals</a></li> <li><a href="?movie&genre=15">Romance</a></li> <li>&#8230;</li> </ul> </div> </li> <li class="nav-item"> <a href="?tv">TV Shows</a> <div class="sub-nav"> <ul class="sub-nav-group"> <li><a href="?tv&genre=20">Classic TV</a></li> <li><a href="?tv&genre=21">Crime TV</a></li> <li>&#8230;</li> </ul> <ul class="sub-nav-group"> <li><a href="?tv&genre=27">Reality TV</a></li> <li><a href="?tv&genre=30">TV Action</a></li> <li>&#8230;</li> </ul> <ul class="sub-nav-group"> <li><a href="?tv&genre=33">TV Dramas</a></li> <li><a href="?tv&genre=34">TV Horror</a></li> <li>&#8230;</li> </ul> </div> </li> </ul> </nav> * @example

CSS


/* Rudimentary mega menu CSS for demonstration */ /* mega menu list */ .nav-menu { display: block; position: relative; list-style: none; margin: 0; padding: 0; z-index: 15; } /* a top level navigation item in the mega menu */ .nav-item { list-style: none; display: inline-block; padding: 0; margin: 0; } /* first descendant link within a top level navigation item */ .nav-item > a { position: relative; display: inline-block; padding: 0.5em 1em; margin: 0 0 -1px 0; border: 1px solid transparent; } /* focus/open states of first descendant link within a top level navigation item */ .nav-item > a:focus, .nav-item > a.open { border: 1px solid #dedede; } /* open state of first descendant link within a top level navigation item */ .nav-item > a.open { background-color: #fff; border-bottom: none; z-index: 1; } /* sub-navigation panel */ .sub-nav { position: absolute; display: none; top: 2.2em; margin-top: -1px; padding: 0.5em 1em; border: 1px solid #dedede; background-color: #fff; } /* sub-navigation panel open state */ .sub-nav.open { display: block; } /* list of items within sub-navigation panel */ .sub-nav ul { display: inline-block; vertical-align: top; margin: 0 1em 0 0; padding: 0; } /* list item within sub-navigation panel */ .sub-nav li { display: block; list-style-type: none; margin: 0; padding: 0; } * @example

JavaScript


<!-- include jquery --> <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script> <!-- include the jquery-accessibleMegaMenu plugin script --> <script src="js/jquery-accessibleMegaMenu.js"></script> <!-- initialize a selector as an accessibleMegaMenu --> <script> $("nav:first").accessibleMegaMenu({ /* prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby */ uuidPrefix: "accessible-megamenu", /* css class used to define the megamenu styling */ menuClass: "nav-menu", /* css class for a top-level navigation item in the megamenu */ topNavItemClass: "nav-item", /* css class for a megamenu panel */ panelClass: "sub-nav", /* css class for a group of items within a megamenu panel */ panelGroupClass: "sub-nav-group", /* css class for the hover state */ hoverClass: "hover", /* css class for the focus state */ focusClass: "focus", /* css class for the open state */ openClass: "open" }); </script> * @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 * @param {string} [options.openDelay=0] - Open delay when opening menu via mouseover * @param {string} [options.closeDelay=250] - Open delay when opening menu via mouseover * @param {boolean} [options.openOnMouseover=false] - Should menu open on mouseover */ $.fn[pluginName] = function (options) { return this.each(function () { var pluginInstance = $.data(this, "plugin_" + pluginName); if (!pluginInstance) { $.data(this, "plugin_" + pluginName, new $.fn[pluginName].AccessibleMegaMenu(this, options)); } else if (typeof pluginInstance[options] === 'function') { pluginInstance[options].apply(pluginInstance, Array.prototype.slice.call(arguments, 1)); } }); }; $.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); } }); }(jQuery, window, document));; jQuery(document).ready(oxygen_init_megamenu); function oxygen_init_megamenu($) { var touchEvent = 'ontouchstart' in window ? 'click' : 'click'; var url = window.location; var pathname = window.location.pathname; $(".oxy-mega-dropdown_link").filter(function() { return (this.href == url || this.href + '/' == url || this.href == pathname || this.href + '/' == pathname ); }).addClass('oxy-mega-dropdown_link-current'); $('.oxy-mega-dropdown_flyout').has('.current-menu-item').siblings('.oxy-mega-dropdown_link').addClass('oxy-mega-dropdown_link-current-ancestor'); $(".oxy-mega-menu").each(function(i, oxyMegaMenu){ var $oxyMegaMenu = $( oxyMegaMenu ), inner = $oxyMegaMenu.children('.oxy-mega-menu_inner'), oxyMegaMenuID = $( oxyMegaMenu ).attr('id'), clicktrigger = inner.data('trigger'), oDelay = inner.data('odelay'), cDelay = inner.data('cdelay'), flyMenu = inner.find('.oxy-mega-dropdown_flyout-click-area').parent('.oxy-mega-dropdown_link'), slideDuration = inner.data('duration'), mouseoverReveal = inner.data('mouseover'), preventScroll = inner.data('prevent-scroll'), hashlinkClose = inner.data('hash-close'), slide_trigger_selector = clicktrigger, slideClickArea = $oxyMegaMenu.find('.oxy-mega-dropdown_icon'), auto_aria = inner.data('auto-aria'); inner.find('.oxy-mega-dropdown_link').css("cursor","pointer"); function ariaExpandToggle($state) { if ( true === auto_aria ) { $(slide_trigger_selector).each(function(i,trigger) { if ( $(trigger).hasClass('oxy-burger-trigger') && $(trigger).children('.hamburger').length ) { $(trigger).children('.hamburger').attr('aria-expanded', $state); } else { $(trigger).attr('aria-expanded', $state); $(trigger).attr('role','button'); } }); } } if ( true === auto_aria ) { $(slide_trigger_selector).each(function(i,trigger) { if ( $(trigger).hasClass('oxy-burger-trigger') && $(trigger).children('.hamburger').length ) { $(trigger).children('.hamburger').attr('aria-controls', oxyMegaMenuID); } else { $(trigger).attr('aria-controls', oxyMegaMenuID); } }); ariaExpandToggle('false'); } $(slide_trigger_selector).on( touchEvent, function(e) { e.stopPropagation(); e.preventDefault(); $oxyMegaMenu.slideToggle(slideDuration); if ( true === auto_aria ) { if ( 'true' === $(slide_trigger_selector).attr('aria-expanded') || 'true' === $(slide_trigger_selector).children('.hamburger').attr('aria-expanded') ) { ariaExpandToggle('false'); } else { ariaExpandToggle('true'); } } $oxyMegaMenu.children('.oxy-mega-menu_inner').toggleClass('oxy-mega-menu_mobile'); if (true === preventScroll) { $('body,html').toggleClass( 'oxy-nav-menu-prevent-overflow' ); } } ); var megaStatus = false; var mobileStatus = false; var megaInitialised = false; // --> Trigger accessible menu just 1x... var triggerMegaMenu = function() { if (!megaStatus) { megaStatus = true; // so only fires 1x if (!megaInitialised) { $(oxyMegaMenu).accessibleMegaMenu({ uuidPrefix: oxyMegaMenuID, menuClass: "oxy-mega-menu_inner", topNavItemClass: "oxy-mega-dropdown", panelClass: "oxy-mega-dropdown_inner", panelGroupClass: "mega-column", hoverClass: "oxy-mega-dropdown_inner-hover", focusClass: "oxy-mega-dropdown_inner-focus", openClass: "oxy-mega-dropdown_inner-open", toggleButtonClass: "oxy-burger-trigger", openDelay: oDelay, closeDelay: cDelay, openOnMouseover: mouseoverReveal }); megaInitialised = true; } else { $(oxyMegaMenu).data('plugin_accessibleMegaMenu').init(); } $('.oxy-mega-dropdown_just-link').off( "click" ); if (true === inner.data('hovertabs')) { $oxyMegaMenu.find('.oxy-tab').attr('tabindex','0'); $oxyMegaMenu.find('.oxy-tab').on('mouseenter focus', function() { $(this).click(); }); } } } // Only fire the megamenu function is menu is visible (meaning we're not on mobile) var checkMegaDisplay = function() { // Desktop if ( 'hidden' === $( oxyMegaMenu ).css("backface-visibility") ) { $(oxyMegaMenu).removeAttr("style"); $(oxyMegaMenu).find('.oxy-mega-dropdown_inner').removeAttr("style"); triggerMegaMenu(); $( oxyMegaMenu ).off( touchEvent ); mobileStatus = false; $('body,html').removeClass( 'oxy-nav-menu-prevent-overflow' ); $(oxyMegaMenu).find('.oxy-mega-menu_inner').removeClass('oxy-mega-menu_mobile'); } // Mobile else { // if MegaMenu already init, let's remove it if (megaStatus) { $( oxyMegaMenu).find('.oxy-mega-dropdown_link').removeClass('oxy-mega-dropdown_inner-open'); $( oxyMegaMenu ).data('plugin_accessibleMegaMenu').destroy(); $oxyMegaMenu.find('.oxy-tab').off('mouseenter focus'); megaStatus = false; } if (!mobileStatus) { mobileStatus = true; // so only fires 1x if ($(slide_trigger_selector).hasClass('oxy-burger-trigger')) { $(slide_trigger_selector).children('.hamburger').removeClass('is-active'); } $( oxyMegaMenu).find('.oxy-mega-dropdown_link[data-expanded=enable]').addClass('oxy-mega-dropdown_inner-open'); $( oxyMegaMenu ).off( touchEvent ); $( oxyMegaMenu ).on( touchEvent, '.oxy-mega-menu_mobile li > a', function(e) { if ($(e.target).closest('.oxy-mega-dropdown_flyout-click-area').length > 0) { e.preventDefault(); e.stopPropagation(); oxy_subMenu_toggle(this, slideDuration); } else if ($(e.target).attr("href") === "#" && $(e.target).parent().hasClass('menu-item-has-children')) { var subflyoutButton = $(e.target).find('.oxy-mega-dropdown_flyout-click-area'); e.preventDefault(); e.stopPropagation(); oxy_subMenu_toggle(subflyoutButton, slideDuration); } else if ($(e.target).closest('.oxy-mega-dropdown_link:not(.oxy-mega-dropdown_just-link) .oxy-mega-dropdown_icon').length > 0) { e.stopPropagation(); e.preventDefault(); oxy_megaMenu_toggle(this, slideDuration); } else if ($(e.target).closest('.oxy-mega-dropdown_link').is('a[href^="#"]') && $(e.target).closest('.oxy-mega-dropdown_link').parent().hasClass('oxy-mega-dropdown') && !$(e.target).closest('.oxy-mega-dropdown_link').hasClass('oxy-mega-dropdown_just-link') ) { e.stopPropagation(); e.preventDefault(); oxy_megaMenu_toggle(this, slideDuration); } else if ($(e.target).closest('.oxy-mega-dropdown_link').data('disable-link') === 'enable') { e.stopPropagation(); e.preventDefault(); oxy_megaMenu_toggle(this, slideDuration); } else if ( $(e.target).closest('.oxy-mega-dropdown_link').hasClass('oxy-mega-dropdown_just-link') && $(e.target).closest('.oxy-mega-dropdown_link').is('a[href^="#"]') ) { e.stopPropagation(); setTimeout(function() { $(slide_trigger_selector).trigger('click') }, 0); } }); } } } checkMegaDisplay(); $(window).on("load resize orientationchange",function(e){ checkMegaDisplay(); }); if (true === hashlinkClose) { inner.on('click', '.oxy-mega-dropdown_inner a[href*="#"]:not(.menu-item-has-children > a), a.oxy-mega-dropdown_just-link[href*="#"]', function() { if ('hidden' === $oxyMegaMenu.css("backface-visibility")) { // If desktop $oxyMegaMenu.find('.oxy-mega-dropdown_inner-open').removeClass('oxy-mega-dropdown_inner-open'); } else { if ( $(this).closest('.oxy-mega-dropdown_inner').siblings('.oxy-mega-dropdown_inner-open').length ) { $(this).closest('.oxy-mega-dropdown_inner').siblings('.oxy-mega-dropdown_inner-open').trigger('click'); } $(slide_trigger_selector).trigger('click'); } }); } }); // each function oxy_subMenu_toggle(trigger, durationData) { $(trigger).closest('.menu-item-has-children').children('.sub-menu').slideToggle( durationData ); $(trigger).closest('.menu-item-has-children').siblings('.menu-item-has-children').children('.sub-menu').slideUp( durationData ); $(trigger).children('.oxy-mega-dropdown_flyout-click-area').attr('aria-expanded', function (i, attr) { return attr == 'true' ? 'false' : 'true' }); $(trigger).children('.oxy-mega-dropdown_flyout-click-area').attr('aria-pressed', function (i, attr) { return attr == 'true' ? 'false' : 'true' }); } function oxy_megaMenu_toggle(trigger, durationData) { var othermenus = $(trigger).parent('.oxy-mega-dropdown').siblings('.oxy-mega-dropdown'); othermenus.find( '.oxy-mega-dropdown_inner' ).slideUp( durationData ); othermenus.find( '.oxy-mega-dropdown_link' ).removeClass( 'oxy-mega-dropdown_inner-open' ); $(trigger).next('.oxy-mega-dropdown_inner').slideToggle( durationData ); $(trigger).toggleClass( 'oxy-mega-dropdown_inner-open' ); $(trigger).attr('aria-expanded', function (i, attr) { return attr == 'true' ? 'false' : 'true' }); $(trigger).attr('aria-pressed', function (i, attr) { return attr == 'true' ? 'false' : 'true' }); $(trigger).next('oxy-slide-menu_open'); // Resize carousel as opened if found if ($(trigger).next('.oxy-mega-dropdown_inner').has('.flickity-enabled')) { setTimeout(function() { var carousel = $(trigger).next('.oxy-mega-dropdown_inner').find('.flickity-enabled'); if (carousel.length) { var flkty = Flickity.data( carousel[0] ); flkty.resize(); } }, 100); } } $('.oxy-mega-dropdown_just-link').parent('.oxy-mega-dropdown').addClass('oxy-mega-dropdown_no-dropdown') var options = { attributes: true, attributeFilter: ['class'], subtree: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (var mutation of mutations) { if (mutation.type === 'attributes') { if (($(mutation.target).closest('.oxy-mega-menu_inner').has( ".oxy-mega-dropdown_inner-open" ).length) && !($(mutation.target).closest('.oxy-mega-menu_inner').has( ".oxy-mega-dropdown_inner-open.oxy-mega-dropdown_just-link" ).length) ) { $(mutation.target).closest('.oxy-mega-menu_inner').addClass('oxy-mega-menu_active'); } else { $(mutation.target).closest('.oxy-mega-menu_inner').removeClass('oxy-mega-menu_active'); } } } } var MegaMenus = document.querySelectorAll('.oxy-mega-menu_inner[data-type=container]'); MegaMenus.forEach(MegaMenu => { observer.observe(MegaMenu, options); }); $(".oxy-mega-dropdown_flyout").each(function(i, oxyDropdown){ var icon = $(oxyDropdown).data('icon'); $(oxyDropdown).find('.menu-item-has-children > a').append(''); }); }; /** * This work is licensed under the W3C Software and Document License * (http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). */ (function() { // Return early if we're not running inside of the browser. if (typeof window === 'undefined') { return; } // Convenience function for converting NodeLists. /** @type {typeof Array.prototype.slice} */ const slice = Array.prototype.slice; /** * IE has a non-standard name for "matches". * @type {typeof Element.prototype.matches} */ const matches = Element.prototype.matches || Element.prototype.msMatchesSelector; /** @type {string} */ const _focusableElementsString = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'details', 'summary', 'iframe', 'object', 'embed', '[contenteditable]'].join(','); /** * `InertRoot` manages a single inert subtree, i.e. a DOM subtree whose root element has an `inert` * attribute. * * Its main functions are: * * - to create and maintain a set of managed `InertNode`s, including when mutations occur in the * subtree. The `makeSubtreeUnfocusable()` method handles collecting `InertNode`s via registering * each focusable node in the subtree with the singleton `InertManager` which manages all known * focusable nodes within inert subtrees. `InertManager` ensures that a single `InertNode` * instance exists for each focusable node which has at least one inert root as an ancestor. * * - to notify all managed `InertNode`s when this subtree stops being inert (i.e. when the `inert` * attribute is removed from the root node). This is handled in the destructor, which calls the * `deregister` method on `InertManager` for each managed inert node. */ class InertRoot { /** * @param {!Element} rootElement The Element at the root of the inert subtree. * @param {!InertManager} inertManager The global singleton InertManager object. */ constructor(rootElement, inertManager) { /** @type {!InertManager} */ this._inertManager = inertManager; /** @type {!Element} */ this._rootElement = rootElement; /** * @type {!Set} * All managed focusable nodes in this InertRoot's subtree. */ this._managedNodes = new Set(); // Make the subtree hidden from assistive technology if (this._rootElement.hasAttribute('aria-hidden')) { /** @type {?string} */ this._savedAriaHidden = this._rootElement.getAttribute('aria-hidden'); } else { this._savedAriaHidden = null; } this._rootElement.setAttribute('aria-hidden', 'true'); // Make all focusable elements in the subtree unfocusable and add them to _managedNodes this._makeSubtreeUnfocusable(this._rootElement); // Watch for: // - any additions in the subtree: make them unfocusable too // - any removals from the subtree: remove them from this inert root's managed nodes // - attribute changes: if `tabindex` is added, or removed from an intrinsically focusable // element, make that node a managed node. this._observer = new MutationObserver(this._onMutation.bind(this)); this._observer.observe(this._rootElement, {attributes: true, childList: true, subtree: true}); } /** * Call this whenever this object is about to become obsolete. This unwinds all of the state * stored in this object and updates the state of all of the managed nodes. */ destructor() { this._observer.disconnect(); if (this._rootElement) { if (this._savedAriaHidden !== null) { this._rootElement.setAttribute('aria-hidden', this._savedAriaHidden); } else { this._rootElement.removeAttribute('aria-hidden'); } } this._managedNodes.forEach(function(inertNode) { this._unmanageNode(inertNode.node); }, this); // Note we cast the nulls to the ANY type here because: // 1) We want the class properties to be declared as non-null, or else we // need even more casts throughout this code. All bets are off if an // instance has been destroyed and a method is called. // 2) We don't want to cast "this", because we want type-aware optimizations // to know which properties we're setting. this._observer = /** @type {?} */ (null); this._rootElement = /** @type {?} */ (null); this._managedNodes = /** @type {?} */ (null); this._inertManager = /** @type {?} */ (null); } /** * @return {!Set} A copy of this InertRoot's managed nodes set. */ get managedNodes() { return new Set(this._managedNodes); } /** @return {boolean} */ get hasSavedAriaHidden() { return this._savedAriaHidden !== null; } /** @param {?string} ariaHidden */ set savedAriaHidden(ariaHidden) { this._savedAriaHidden = ariaHidden; } /** @return {?string} */ get savedAriaHidden() { return this._savedAriaHidden; } /** * @param {!Node} startNode */ _makeSubtreeUnfocusable(startNode) { composedTreeWalk(startNode, (node) => this._visitNode(node)); let activeElement = document.activeElement; if (!document.body.contains(startNode)) { // startNode may be in shadow DOM, so find its nearest shadowRoot to get the activeElement. let node = startNode; /** @type {!ShadowRoot|undefined} */ let root = undefined; while (node) { if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { root = /** @type {!ShadowRoot} */ (node); break; } node = node.parentNode; } if (root) { activeElement = root.activeElement; } } if (startNode.contains(activeElement)) { activeElement.blur(); // In IE11, if an element is already focused, and then set to tabindex=-1 // calling blur() will not actually move the focus. // To work around this we call focus() on the body instead. if (activeElement === document.activeElement) { document.body.focus(); } } } /** * @param {!Node} node */ _visitNode(node) { if (node.nodeType !== Node.ELEMENT_NODE) { return; } const element = /** @type {!Element} */ (node); // If a descendant inert root becomes un-inert, its descendants will still be inert because of // this inert root, so all of its managed nodes need to be adopted by this InertRoot. if (element !== this._rootElement && element.hasAttribute('inert')) { this._adoptInertRoot(element); } if (matches.call(element, _focusableElementsString) || element.hasAttribute('tabindex')) { this._manageNode(element); } } /** * Register the given node with this InertRoot and with InertManager. * @param {!Node} node */ _manageNode(node) { const inertNode = this._inertManager.register(node, this); this._managedNodes.add(inertNode); } /** * Unregister the given node with this InertRoot and with InertManager. * @param {!Node} node */ _unmanageNode(node) { const inertNode = this._inertManager.deregister(node, this); if (inertNode) { this._managedNodes.delete(inertNode); } } /** * Unregister the entire subtree starting at `startNode`. * @param {!Node} startNode */ _unmanageSubtree(startNode) { composedTreeWalk(startNode, (node) => this._unmanageNode(node)); } /** * If a descendant node is found with an `inert` attribute, adopt its managed nodes. * @param {!Element} node */ _adoptInertRoot(node) { let inertSubroot = this._inertManager.getInertRoot(node); // During initialisation this inert root may not have been registered yet, // so register it now if need be. if (!inertSubroot) { this._inertManager.setInert(node, true); inertSubroot = this._inertManager.getInertRoot(node); } inertSubroot.managedNodes.forEach(function(savedInertNode) { this._manageNode(savedInertNode.node); }, this); } /** * Callback used when mutation observer detects subtree additions, removals, or attribute changes. * @param {!Array} records * @param {!MutationObserver} self */ _onMutation(records, self) { records.forEach(function(record) { const target = /** @type {!Element} */ (record.target); if (record.type === 'childList') { // Manage added nodes slice.call(record.addedNodes).forEach(function(node) { this._makeSubtreeUnfocusable(node); }, this); // Un-manage removed nodes slice.call(record.removedNodes).forEach(function(node) { this._unmanageSubtree(node); }, this); } else if (record.type === 'attributes') { if (record.attributeName === 'tabindex') { // Re-initialise inert node if tabindex changes this._manageNode(target); } else if (target !== this._rootElement && record.attributeName === 'inert' && target.hasAttribute('inert')) { // If a new inert root is added, adopt its managed nodes and make sure it knows about the // already managed nodes from this inert subroot. this._adoptInertRoot(target); const inertSubroot = this._inertManager.getInertRoot(target); this._managedNodes.forEach(function(managedNode) { if (target.contains(managedNode.node)) { inertSubroot._manageNode(managedNode.node); } }); } } }, this); } } /** * `InertNode` initialises and manages a single inert node. * A node is inert if it is a descendant of one or more inert root elements. * * On construction, `InertNode` saves the existing `tabindex` value for the node, if any, and * either removes the `tabindex` attribute or sets it to `-1`, depending on whether the element * is intrinsically focusable or not. * * `InertNode` maintains a set of `InertRoot`s which are descendants of this `InertNode`. When an * `InertRoot` is destroyed, and calls `InertManager.deregister()`, the `InertManager` notifies the * `InertNode` via `removeInertRoot()`, which in turn destroys the `InertNode` if no `InertRoot`s * remain in the set. On destruction, `InertNode` reinstates the stored `tabindex` if one exists, * or removes the `tabindex` attribute if the element is intrinsically focusable. */ class InertNode { /** * @param {!Node} node A focusable element to be made inert. * @param {!InertRoot} inertRoot The inert root element associated with this inert node. */ constructor(node, inertRoot) { /** @type {!Node} */ this._node = node; /** @type {boolean} */ this._overrodeFocusMethod = false; /** * @type {!Set} The set of descendant inert roots. * If and only if this set becomes empty, this node is no longer inert. */ this._inertRoots = new Set([inertRoot]); /** @type {?number} */ this._savedTabIndex = null; /** @type {boolean} */ this._destroyed = false; // Save any prior tabindex info and make this node untabbable this.ensureUntabbable(); } /** * Call this whenever this object is about to become obsolete. * This makes the managed node focusable again and deletes all of the previously stored state. */ destructor() { this._throwIfDestroyed(); if (this._node && this._node.nodeType === Node.ELEMENT_NODE) { const element = /** @type {!Element} */ (this._node); if (this._savedTabIndex !== null) { element.setAttribute('tabindex', this._savedTabIndex); } else { element.removeAttribute('tabindex'); } // Use `delete` to restore native focus method. if (this._overrodeFocusMethod) { delete element.focus; } } // See note in InertRoot.destructor for why we cast these nulls to ANY. this._node = /** @type {?} */ (null); this._inertRoots = /** @type {?} */ (null); this._destroyed = true; } /** * @type {boolean} Whether this object is obsolete because the managed node is no longer inert. * If the object has been destroyed, any attempt to access it will cause an exception. */ get destroyed() { return /** @type {!InertNode} */ (this)._destroyed; } /** * Throw if user tries to access destroyed InertNode. */ _throwIfDestroyed() { if (this.destroyed) { throw new Error('Trying to access destroyed InertNode'); } } /** @return {boolean} */ get hasSavedTabIndex() { return this._savedTabIndex !== null; } /** @return {!Node} */ get node() { this._throwIfDestroyed(); return this._node; } /** @param {?number} tabIndex */ set savedTabIndex(tabIndex) { this._throwIfDestroyed(); this._savedTabIndex = tabIndex; } /** @return {?number} */ get savedTabIndex() { this._throwIfDestroyed(); return this._savedTabIndex; } /** Save the existing tabindex value and make the node untabbable and unfocusable */ ensureUntabbable() { if (this.node.nodeType !== Node.ELEMENT_NODE) { return; } const element = /** @type {!Element} */ (this.node); if (matches.call(element, _focusableElementsString)) { if (/** @type {!HTMLElement} */ (element).tabIndex === -1 && this.hasSavedTabIndex) { return; } if (element.hasAttribute('tabindex')) { this._savedTabIndex = /** @type {!HTMLElement} */ (element).tabIndex; } element.setAttribute('tabindex', '-1'); if (element.nodeType === Node.ELEMENT_NODE) { element.focus = function() {}; this._overrodeFocusMethod = true; } } else if (element.hasAttribute('tabindex')) { this._savedTabIndex = /** @type {!HTMLElement} */ (element).tabIndex; element.removeAttribute('tabindex'); } } /** * Add another inert root to this inert node's set of managing inert roots. * @param {!InertRoot} inertRoot */ addInertRoot(inertRoot) { this._throwIfDestroyed(); this._inertRoots.add(inertRoot); } /** * Remove the given inert root from this inert node's set of managing inert roots. * If the set of managing inert roots becomes empty, this node is no longer inert, * so the object should be destroyed. * @param {!InertRoot} inertRoot */ removeInertRoot(inertRoot) { this._throwIfDestroyed(); this._inertRoots.delete(inertRoot); if (this._inertRoots.size === 0) { this.destructor(); } } } /** * InertManager is a per-document singleton object which manages all inert roots and nodes. * * When an element becomes an inert root by having an `inert` attribute set and/or its `inert` * property set to `true`, the `setInert` method creates an `InertRoot` object for the element. * The `InertRoot` in turn registers itself as managing all of the element's focusable descendant * nodes via the `register()` method. The `InertManager` ensures that a single `InertNode` instance * is created for each such node, via the `_managedNodes` map. */ class InertManager { /** * @param {!Document} document */ constructor(document) { if (!document) { throw new Error('Missing required argument; InertManager needs to wrap a document.'); } /** @type {!Document} */ this._document = document; /** * All managed nodes known to this InertManager. In a map to allow looking up by Node. * @type {!Map} */ this._managedNodes = new Map(); /** * All inert roots known to this InertManager. In a map to allow looking up by Node. * @type {!Map} */ this._inertRoots = new Map(); /** * Observer for mutations on `document.body`. * @type {!MutationObserver} */ this._observer = new MutationObserver(this._watchForInert.bind(this)); // Add inert style. addInertStyle(document.head || document.body || document.documentElement); // Wait for document to be loaded. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', this._onDocumentLoaded.bind(this)); } else { this._onDocumentLoaded(); } } /** * Set whether the given element should be an inert root or not. * @param {!Element} root * @param {boolean} inert */ setInert(root, inert) { if (inert) { if (this._inertRoots.has(root)) { // element is already inert return; } const inertRoot = new InertRoot(root, this); root.setAttribute('inert', ''); this._inertRoots.set(root, inertRoot); // If not contained in the document, it must be in a shadowRoot. // Ensure inert styles are added there. if (!this._document.body.contains(root)) { let parent = root.parentNode; while (parent) { if (parent.nodeType === 11) { addInertStyle(parent); } parent = parent.parentNode; } } } else { if (!this._inertRoots.has(root)) { // element is already non-inert return; } const inertRoot = this._inertRoots.get(root); inertRoot.destructor(); this._inertRoots.delete(root); root.removeAttribute('inert'); } } /** * Get the InertRoot object corresponding to the given inert root element, if any. * @param {!Node} element * @return {!InertRoot|undefined} */ getInertRoot(element) { return this._inertRoots.get(element); } /** * Register the given InertRoot as managing the given node. * In the case where the node has a previously existing inert root, this inert root will * be added to its set of inert roots. * @param {!Node} node * @param {!InertRoot} inertRoot * @return {!InertNode} inertNode */ register(node, inertRoot) { let inertNode = this._managedNodes.get(node); if (inertNode !== undefined) { // node was already in an inert subtree inertNode.addInertRoot(inertRoot); } else { inertNode = new InertNode(node, inertRoot); } this._managedNodes.set(node, inertNode); return inertNode; } /** * De-register the given InertRoot as managing the given inert node. * Removes the inert root from the InertNode's set of managing inert roots, and remove the inert * node from the InertManager's set of managed nodes if it is destroyed. * If the node is not currently managed, this is essentially a no-op. * @param {!Node} node * @param {!InertRoot} inertRoot * @return {?InertNode} The potentially destroyed InertNode associated with this node, if any. */ deregister(node, inertRoot) { const inertNode = this._managedNodes.get(node); if (!inertNode) { return null; } inertNode.removeInertRoot(inertRoot); if (inertNode.destroyed) { this._managedNodes.delete(node); } return inertNode; } /** * Callback used when document has finished loading. */ _onDocumentLoaded() { // Find all inert roots in document and make them actually inert. const inertElements = slice.call(this._document.querySelectorAll('[inert]')); inertElements.forEach(function(inertElement) { this.setInert(inertElement, true); }, this); // Comment this out to use programmatic API only. this._observer.observe(this._document.body || this._document.documentElement, {attributes: true, subtree: true, childList: true}); } /** * Callback used when mutation observer detects attribute changes. * @param {!Array} records * @param {!MutationObserver} self */ _watchForInert(records, self) { const _this = this; records.forEach(function(record) { switch (record.type) { case 'childList': slice.call(record.addedNodes).forEach(function(node) { if (node.nodeType !== Node.ELEMENT_NODE) { return; } const inertElements = slice.call(node.querySelectorAll('[inert]')); if (matches.call(node, '[inert]')) { inertElements.unshift(node); } inertElements.forEach(function(inertElement) { this.setInert(inertElement, true); }, _this); }, _this); break; case 'attributes': if (record.attributeName !== 'inert') { return; } const target = /** @type {!Element} */ (record.target); const inert = target.hasAttribute('inert'); _this.setInert(target, inert); break; } }, this); } } /** * Recursively walk the composed tree from |node|. * @param {!Node} node * @param {(function (!Element))=} callback Callback to be called for each element traversed, * before descending into child nodes. * @param {?ShadowRoot=} shadowRootAncestor The nearest ShadowRoot ancestor, if any. */ function composedTreeWalk(node, callback, shadowRootAncestor) { if (node.nodeType == Node.ELEMENT_NODE) { const element = /** @type {!Element} */ (node); if (callback) { callback(element); } // Descend into node: // If it has a ShadowRoot, ignore all child elements - these will be picked // up by the or elements. Descend straight into the // ShadowRoot. const shadowRoot = /** @type {!HTMLElement} */ (element).shadowRoot; if (shadowRoot) { composedTreeWalk(shadowRoot, callback, shadowRoot); return; } // If it is a element, descend into distributed elements - these // are elements from outside the shadow root which are rendered inside the // shadow DOM. if (element.localName == 'content') { const content = /** @type {!HTMLContentElement} */ (element); // Verifies if ShadowDom v0 is supported. const distributedNodes = content.getDistributedNodes ? content.getDistributedNodes() : []; for (let i = 0; i < distributedNodes.length; i++) { composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); } return; } // If it is a element, descend into assigned nodes - these // are elements from outside the shadow root which are rendered inside the // shadow DOM. if (element.localName == 'slot') { const slot = /** @type {!HTMLSlotElement} */ (element); // Verify if ShadowDom v1 is supported. const distributedNodes = slot.assignedNodes ? slot.assignedNodes({flatten: true}) : []; for (let i = 0; i < distributedNodes.length; i++) { composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); } return; } } // If it is neither the parent of a ShadowRoot, a element, a // element, nor a element recurse normally. let child = node.firstChild; while (child != null) { composedTreeWalk(child, callback, shadowRootAncestor); child = child.nextSibling; } } /** * Adds a style element to the node containing the inert specific styles * @param {!Node} node */ function addInertStyle(node) { if (node.querySelector('style#inert-style, link#inert-style')) { return; } const style = document.createElement('style'); style.setAttribute('id', 'inert-style'); style.textContent = '\n'+ '[inert] {\n' + ' pointer-events: none;\n' + ' cursor: default;\n' + '}\n' + '\n' + '[inert], [inert] * {\n' + ' -webkit-user-select: none;\n' + ' -moz-user-select: none;\n' + ' -ms-user-select: none;\n' + ' user-select: none;\n' + '}\n'; node.appendChild(style); } if (!Element.prototype.hasOwnProperty('inert')) { /** @type {!InertManager} */ const inertManager = new InertManager(document); Object.defineProperty(Element.prototype, 'inert', { enumerable: true, /** @this {!Element} */ get: function() { return this.hasAttribute('inert'); }, /** @this {!Element} */ set: function(inert) { inertManager.setInert(this, inert); }, }); } })();; jQuery(document).ready(oxygen_init_offcanvas); function oxygen_init_offcanvas($) { let touchEvent = 'click'; 'use strict'; $('.oxy-off-canvas .offcanvas-inner').each(function() { var offCanvas = $(this), triggerSelector = offCanvas.data('trigger-selector'), offCanvasOutside = offCanvas.data('click-outside'), offCanvasEsc = offCanvas.data('esc'), offCanvasStart = offCanvas.data('start'), offCanvasBackdrop = offCanvas.data('backdrop'), offCanvasFocusSelector = offCanvas.data('focus-selector'), backdrop = offCanvas.prev('.oxy-offcanvas_backdrop'), menuHashLink = offCanvas.find('a[href*=\\#]').not(".menu-item-has-children > a"), reset = offCanvas.data('reset'), mediaPlayer = offCanvas.find('.oxy-pro-media-player vime-player'), otherOffcanvas = offCanvas.data('second-offcanvas'), maybeHashClose = offCanvas.data('hashclose'), offcanvasPush = offCanvas.data('content-push'), offcanvasPushContent = offCanvas.data('content-selector'), offcanvasPushDuration = offCanvas.data('content-duration'), offcanvasID = offCanvas.parent('.oxy-off-canvas').attr('ID'), burgerSync = offCanvas.data('burger-sync'), maybeOverflow = offCanvas.data('overflow'); if (false !== offCanvas.data('stagger-menu')) { offCanvas.find('.oxy-slide-menu_list > .menu-item').addClass('aos-animate'); offCanvas.find('.oxy-slide-menu_list > .menu-item').attr( { "data-aos": offCanvas.data('stagger-menu') } ); } if (! $(this).hasClass('oxy-off-canvas-toggled') ) { $(this).find(".aos-animate:not(.oxy-off-canvas-toggled .aos-animate)").addClass("aos-animate-disabled").removeClass("aos-animate"); } ariaExpandToggle('false'); if ( true === offCanvas.data('auto-aria') ) { $(triggerSelector).each(function(i,trigger) { if ( $(trigger).hasClass('oxy-burger-trigger') && $(trigger).children('.hamburger').length ) { $(trigger).children('.hamburger').attr('aria-controls', offCanvas.attr('id')); } else { $(trigger).attr('aria-controls', offCanvas.attr('id')); } }); } function doOffCanvas(triggerSelector) { if ($(triggerSelector).hasClass('oxy-close-modal')) { oxyCloseModal(); } if (!offCanvas.parent().hasClass('oxy-off-canvas-toggled')) { openOffCanvas(); } else { closeOffCanvas(); } } if ( $(triggerSelector).hasClass('oxy-burger-trigger') && $(triggerSelector).children('.hamburger').length ) { let triggerSelectorTouch = $( triggerSelector ).children('.hamburger').data('touch'); let touchEvent = 'ontouchstart' in window ? triggerSelectorTouch : 'click'; $(triggerSelector).on(touchEvent, function(e) { e.stopPropagation(); e.preventDefault(); if (true === burgerSync) { $(triggerSelector).not('#' + $(this).attr('ID')).children('.hamburger').toggleClass('is-active'); } doOffCanvas(triggerSelector); }); } else { let triggerSelectorTouch = touchEvent; $(triggerSelector).on(triggerSelectorTouch, function(e) { e.stopPropagation(); e.preventDefault(); doOffCanvas(triggerSelector); }); } // Backdrop Clicked offCanvas.siblings('.oxy-offcanvas_backdrop').on(touchEvent, function(e) { e.stopPropagation(); if (offCanvasOutside === true) { closeBurger(); closeLottie(); closeOffCanvas(); } }); // Pressing ESC from inside the offcanvas will close it offCanvas.keyup(function(e) { if (offCanvasEsc === true) { if (e.keyCode === 27) { closeBurger(); closeLottie(); closeOffCanvas(); } } }); if (maybeHashClose === true) { $(document).on("click", '#' + offcanvasID + ' a[href*=\\#]', function (e) { e.stopPropagation(); if ( ( !$(this).not(".menu-item-has-children > a") ) || $(this).is('[href="#"]' ) ) { return; } if ( $(this).is(".oxy-table-of-contents_list-item > a") ) { return; } if ( $(this).is(".mm-btn") || $(this).is(".mm-navbar__title") ) { return; } if (this.pathname === window.location.pathname) { closeBurger(); closeLottie(); closeOffCanvas(); } }); } if (offcanvasPush) { $(offcanvasPushContent).attr('data-offcanvas-push', '#' + offcanvasID); $(offcanvasPushContent).css({ "--offcanvas-push" : offcanvasPush + "px", "--offcanvas-push-duration" : offcanvasPushDuration + 's', }); } function ariaExpandToggle($state) { if ( true === offCanvas.data('auto-aria') ) { $(triggerSelector).each(function(i,trigger) { if ( $(trigger).hasClass('oxy-burger-trigger') && $(trigger).children('.hamburger').length ) { $(trigger).children('.hamburger').attr('aria-expanded', $state); } else { $(trigger).attr('aria-expanded', $state); $(trigger).attr('role','button'); } }); } } function inertToggle($state) { if ( false !== offCanvas.data('inert') ) { if ('true' === $state) { offCanvas.attr('inert',''); } else { offCanvas.removeAttr('inert'); } } } function openOffCanvas() { offCanvas.parent().addClass('oxy-off-canvas-toggled'); $(offcanvasPushContent).addClass('oxy-off-canvas-toggled'); $('body,html').addClass('off-canvas-toggled'); $('body,html').addClass('toggled' + offcanvasID); if (true === offCanvasStart) { doClose(); } else { doOpen() } } function closeOffCanvas() { $('body,html').removeClass('off-canvas-toggled'); $('body,html').removeClass('toggled' + offcanvasID); offCanvas.parent().removeClass('oxy-off-canvas-toggled'); $(offcanvasPushContent).removeClass('oxy-off-canvas-toggled'); if (true === offCanvasStart) { doOpen() } else { doClose(); } } function doOpen() { offCanvas.find(".aos-animate-disabled").removeClass("aos-animate-disabled").addClass("aos-animate"); offCanvas.parent('.oxy-off-canvas').trigger('extras_offcanvas:open'); inertToggle('false') setTimeout(function() { offCanvas.attr('aria-hidden','false'); }, 0); ariaExpandToggle('true'); if (offCanvasFocusSelector) { setTimeout(function() { offCanvas.parent().find(offCanvasFocusSelector).eq(0).focus(); }, 0); } } function doClose() { inertToggle('true'); offCanvas.attr('aria-hidden','true'); setTimeout(function(){ offCanvas.find(".aos-animate").removeClass("aos-animate").addClass("aos-animate-disabled"); }, reset); // wait before removing the animate class $(offCanvas).parent('.oxy-off-canvas').trigger('extras_offcanvas:close'); if (otherOffcanvas) { $(otherOffcanvas).children('.offcanvas-inner').attr('aria-hidden','true'); $(otherOffcanvas).children('.offcanvas-inner').removeAttr('inert'); $(otherOffcanvas).removeClass('oxy-off-canvas-toggled'); } ariaExpandToggle('false'); mediaPlayer.each(function() { $(this)[0].pause(); // turn off any pro media players }); } function closeBurger() { if ( ( $(triggerSelector).children('.hamburger').length > 0) && ($(this).children('.hamburger').data('animation') !== 'disable')) { $(triggerSelector).children('.hamburger').removeClass('is-active'); } } function closeLottie() { if ( ( $(triggerSelector).children('lottie-player').length > 0) ) { $(triggerSelector).children('lottie-player').trigger('click'); } } /* For programmatically opening */ function extrasOpenOffcanvas($extras_offcanvas) { var thisOffcanvas = $($extras_offcanvas); var thisoffcanvasPushContent = thisOffcanvas.children('.offcanvas-inner').data('content-selector'); var thisoffCanvasFocusSelector = thisOffcanvas.children('.offcanvas-inner').data('focus-selector'); thisOffcanvas.addClass('oxy-off-canvas-toggled'); $(offcanvasPushContent).addClass('oxy-off-canvas-toggled'); offCanvas.attr('aria-hidden','false'); $('body,html').addClass('off-canvas-toggled'); thisOffcanvas.find(".aos-animate-disabled").removeClass("aos-animate-disabled").addClass("aos-animate"); thisOffcanvas.trigger('extras_offcanvas:open'); if (thisoffCanvasFocusSelector) { thisOffcanvas.find(thisoffCanvasFocusSelector).eq(0).focus(); } } if (true !== offCanvasStart) { offCanvas.attr('aria-hidden','true'); inertToggle('true'); } else { offCanvas.attr('aria-hidden','false'); } // Expose function window.extrasOpenOffcanvas = extrasOpenOffcanvas; var stagger = offCanvas.data('stagger'); // Make sure AOS animations are reset for elements inside after everything has loaded setTimeout(function(){ if (! offCanvas.hasClass('oxy-off-canvas-toggled') ) { offCanvas.find(".aos-animate:not(.oxy-off-canvas-toggled .aos-animate)").addClass("aos-animate-disabled").removeClass("aos-animate"); } }, 40); // wait if (stagger != null) { var delay = offCanvas.data('first-delay'); offCanvas.find('[data-aos]').not('.not-staggered').each(function() { delay = delay + stagger; $(this).attr('data-aos-delay', delay); }); } }); } ; window.WP_Grid_Builder && WP_Grid_Builder.on( 'init', onInit ); function onInit( wpgb ) { wpgb.facets && wpgb.facets.on( 'appended', onAppended ); } function onAppended( content ) { /* Lightbox */ if (typeof doExtrasLightbox == 'function' && jQuery(content).has('.oxy-lightbox')) { doExtrasLightbox(jQuery(content)); } /* Read More / Less */ if (typeof doExtrasReadmore == 'function' && jQuery(content).has('.oxy-read-more-less')) { doExtrasReadmore(jQuery(content)); } /* Tabs */ if (typeof doExtrasTabs == 'function' && jQuery(content).has('.oxy-dynamic-tabs')) { doExtrasTabs(jQuery(content)); } /* Accordion */ if (typeof doExtrasAccordion == 'function' && jQuery(content).has('.oxy-pro-accordion')) { doExtrasAccordion(jQuery(content)); } /* Carousel */ if (typeof doExtrasCarousel == 'function' && jQuery(content).has('.oxy-carousel-builder')) { doExtrasCarousel(jQuery(content)); } /* Popover */ if (typeof doExtrasPopover == 'function' && jQuery(content).has('.oxy-popover')) { doExtrasPopover(jQuery(content)); } } ;