'use strict'; import $ from 'jquery'; import { Keyboard } from './foundation.util.keyboard'; import { Nest } from './foundation.util.nest'; import { GetYoDigits } from './foundation.core.utils'; import { Plugin } from './foundation.core.plugin'; /** * AccordionMenu module. * @module foundation.accordionMenu * @requires foundation.util.keyboard * @requires foundation.util.nest */ class AccordionMenu extends Plugin { /** * Creates a new instance of an accordion menu. * @class * @name AccordionMenu * @fires AccordionMenu#init * @param {jQuery} element - jQuery object to make into an accordion menu. * @param {Object} options - Overrides to the default plugin settings. */ _setup(element, options) { this.$element = element; this.options = $.extend({}, AccordionMenu.defaults, this.$element.data(), options); this.className = 'AccordionMenu'; // ie9 back compat this._init(); Keyboard.register('AccordionMenu', { 'ENTER': 'toggle', 'SPACE': 'toggle', 'ARROW_RIGHT': 'open', 'ARROW_UP': 'up', 'ARROW_DOWN': 'down', 'ARROW_LEFT': 'close', 'ESCAPE': 'closeAll' }); } /** * Initializes the accordion menu by hiding all nested menus. * @private */ _init() { Nest.Feather(this.$element, 'accordion'); var _this = this; this.$element.find('[data-submenu]').not('.is-active').slideUp(0);//.find('a').css('padding-left', '1rem'); this.$element.attr({ 'role': 'tree', 'aria-multiselectable': this.options.multiOpen }); this.$menuLinks = this.$element.find('.is-accordion-submenu-parent'); this.$menuLinks.each(function(){ var linkId = this.id || GetYoDigits(6, 'acc-menu-link'), $elem = $(this), $sub = $elem.children('[data-submenu]'), subId = $sub[0].id || GetYoDigits(6, 'acc-menu'), isActive = $sub.hasClass('is-active'); if(_this.options.parentLink) { let $anchor = $elem.children('a'); $anchor.clone().prependTo($sub).wrap('
'); } if(_this.options.submenuToggle) { $elem.addClass('has-submenu-toggle'); $elem.children('a').after(''); } else { $elem.attr({ 'aria-controls': subId, 'aria-expanded': isActive, 'id': linkId }); } $sub.attr({ 'aria-labelledby': linkId, 'aria-hidden': !isActive, 'role': 'group', 'id': subId }); }); this.$element.find('li').attr({ 'role': 'treeitem' }); var initPanes = this.$element.find('.is-active'); if(initPanes.length){ var _this = this; initPanes.each(function(){ _this.down($(this)); }); } this._events(); } /** * Adds event handlers for items within the menu. * @private */ _events() { var _this = this; this.$element.find('li').each(function() { var $submenu = $(this).children('[data-submenu]'); if ($submenu.length) { if(_this.options.submenuToggle) { $(this).children('.submenu-toggle').off('click.zf.accordionMenu').on('click.zf.accordionMenu', function(e) { _this.toggle($submenu); }); } else { $(this).children('a').off('click.zf.accordionMenu').on('click.zf.accordionMenu', function(e) { e.preventDefault(); _this.toggle($submenu); }); } } }).on('keydown.zf.accordionmenu', function(e){ var $element = $(this), $elements = $element.parent('ul').children('li'), $prevElement, $nextElement, $target = $element.children('[data-submenu]'); $elements.each(function(i) { if ($(this).is($element)) { $prevElement = $elements.eq(Math.max(0, i-1)).find('a').first(); $nextElement = $elements.eq(Math.min(i+1, $elements.length-1)).find('a').first(); if ($(this).children('[data-submenu]:visible').length) { // has open sub menu $nextElement = $element.find('li:first-child').find('a').first(); } if ($(this).is(':first-child')) { // is first element of sub menu $prevElement = $element.parents('li').first().find('a').first(); } else if ($prevElement.parents('li').first().children('[data-submenu]:visible').length) { // if previous element has open sub menu $prevElement = $prevElement.parents('li').find('li:last-child').find('a').first(); } if ($(this).is(':last-child')) { // is last element of sub menu $nextElement = $element.parents('li').first().next('li').find('a').first(); } return; } }); Keyboard.handleKey(e, 'AccordionMenu', { open: function() { if ($target.is(':hidden')) { _this.down($target); $target.find('li').first().find('a').first().focus(); } }, close: function() { if ($target.length && !$target.is(':hidden')) { // close active sub of this item _this.up($target); } else if ($element.parent('[data-submenu]').length) { // close currently open sub _this.up($element.parent('[data-submenu]')); $element.parents('li').first().find('a').first().focus(); } }, up: function() { $prevElement.focus(); return true; }, down: function() { $nextElement.focus(); return true; }, toggle: function() { if (_this.options.submenuToggle) { return false; } if ($element.children('[data-submenu]').length) { _this.toggle($element.children('[data-submenu]')); return true; } }, closeAll: function() { _this.hideAll(); }, handled: function(preventDefault) { if (preventDefault) { e.preventDefault(); } e.stopImmediatePropagation(); } }); });//.attr('tabindex', 0); } /** * Closes all panes of the menu. * @function */ hideAll() { this.up(this.$element.find('[data-submenu]')); } /** * Opens all panes of the menu. * @function */ showAll() { this.down(this.$element.find('[data-submenu]')); } /** * Toggles the open/close state of a submenu. * @function * @param {jQuery} $target - the submenu to toggle */ toggle($target){ if(!$target.is(':animated')) { if (!$target.is(':hidden')) { this.up($target); } else { this.down($target); } } } /** * Opens the sub-menu defined by `$target`. * @param {jQuery} $target - Sub-menu to open. * @fires AccordionMenu#down */ down($target) { if(!this.options.multiOpen) { this.up(this.$element.find('.is-active').not($target.parentsUntil(this.$element).add($target))); } $target .addClass('is-active') .attr({ 'aria-hidden': false }); if(this.options.submenuToggle) { $target.prev('.submenu-toggle').attr({'aria-expanded': true}); } else { $target.parent('.is-accordion-submenu-parent').attr({'aria-expanded': true}); } $target.slideDown(this.options.slideSpeed, () => { /** * Fires when the menu is done opening. * @event AccordionMenu#down */ this.$element.trigger('down.zf.accordionMenu', [$target]); }); } /** * Closes the sub-menu defined by `$target`. All sub-menus inside the target will be closed as well. * @param {jQuery} $target - Sub-menu to close. * @fires AccordionMenu#up */ up($target) { const $submenus = $target.find('[data-submenu]'); const $allmenus = $target.add($submenus); $submenus.slideUp(0); $allmenus .removeClass('is-active') .attr('aria-hidden', true); if(this.options.submenuToggle) { $allmenus.prev('.submenu-toggle').attr('aria-expanded', false); } else { $allmenus.parent('.is-accordion-submenu-parent').attr('aria-expanded', false); } $target.slideUp(this.options.slideSpeed, () => { /** * Fires when the menu is done collapsing up. * @event AccordionMenu#up */ this.$element.trigger('up.zf.accordionMenu', [$target]); }); } /** * Destroys an instance of accordion menu. * @fires AccordionMenu#destroyed */ _destroy() { this.$element.find('[data-submenu]').slideDown(0).css('display', ''); this.$element.find('a').off('click.zf.accordionMenu'); this.$element.find('[data-is-parent-link]').detach(); if(this.options.submenuToggle) { this.$element.find('.has-submenu-toggle').removeClass('has-submenu-toggle'); this.$element.find('.submenu-toggle').remove(); } Nest.Burn(this.$element, 'accordion'); } } AccordionMenu.defaults = { /** * Adds the parent link to the submenu. * @option * @type {boolean} * @default false */ parentLink: false, /** * Amount of time to animate the opening of a submenu in ms. * @option * @type {number} * @default 250 */ slideSpeed: 250, /** * Adds a separate submenu toggle button. This allows the parent item to have a link. * @option * @example true */ submenuToggle: false, /** * The text used for the submenu toggle if enabled. This is used for screen readers only. * @option * @example true */ submenuToggleText: 'Toggle menu', /** * Allow the menu to have multiple open panes. * @option * @type {boolean} * @default true */ multiOpen: true }; export {AccordionMenu};