346 lines
9.8 KiB
JavaScript
346 lines
9.8 KiB
JavaScript
'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('<li data-is-parent-link class="is-submenu-parent-item is-submenu-item is-accordion-submenu-item"></li>');
|
|
}
|
|
|
|
if(_this.options.submenuToggle) {
|
|
$elem.addClass('has-submenu-toggle');
|
|
$elem.children('a').after('<button id="' + linkId + '" class="submenu-toggle" aria-controls="' + subId + '" aria-expanded="' + isActive + '" title="' + _this.options.submenuToggleText + '"><span class="submenu-toggle-text">' + _this.options.submenuToggleText + '</span></button>');
|
|
} 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};
|