'use strict'; import $ from 'jquery'; import { Plugin } from './foundation.core.plugin'; import { rtl as Rtl, ignoreMousedisappear } from './foundation.core.utils'; import { Keyboard } from './foundation.util.keyboard'; import { Nest } from './foundation.util.nest'; import { Box } from './foundation.util.box'; /** * DropdownMenu module. * @module foundation.dropdown-menu * @requires foundation.util.keyboard * @requires foundation.util.box * @requires foundation.util.nest */ class DropdownMenu extends Plugin { /** * Creates a new instance of DropdownMenu. * @class * @name DropdownMenu * @fires DropdownMenu#init * @param {jQuery} element - jQuery object to make into a dropdown menu. * @param {Object} options - Overrides to the default plugin settings. */ _setup(element, options) { this.$element = element; this.options = $.extend({}, DropdownMenu.defaults, this.$element.data(), options); this.className = 'DropdownMenu'; // ie9 back compat this._init(); Keyboard.register('DropdownMenu', { 'ENTER': 'open', 'SPACE': 'open', 'ARROW_RIGHT': 'next', 'ARROW_UP': 'up', 'ARROW_DOWN': 'down', 'ARROW_LEFT': 'previous', 'ESCAPE': 'close' }); } /** * Initializes the plugin, and calls _prepareMenu * @private * @function */ _init() { Nest.Feather(this.$element, 'dropdown'); var subs = this.$element.find('li.is-dropdown-submenu-parent'); this.$element.children('.is-dropdown-submenu-parent').children('.is-dropdown-submenu').addClass('first-sub'); this.$menuItems = this.$element.find('[role="menuitem"]'); this.$tabs = this.$element.children('[role="menuitem"]'); this.$tabs.find('ul.is-dropdown-submenu').addClass(this.options.verticalClass); if (this.options.alignment === 'auto') { if (this.$element.hasClass(this.options.rightClass) || Rtl() || this.$element.parents('.top-bar-right').is('*')) { this.options.alignment = 'right'; subs.addClass('opens-left'); } else { this.options.alignment = 'left'; subs.addClass('opens-right'); } } else { if (this.options.alignment === 'right') { subs.addClass('opens-left'); } else { subs.addClass('opens-right'); } } this.changed = false; this._events(); }; _isVertical() { return this.$tabs.css('display') === 'block' || this.$element.css('flex-direction') === 'column'; } _isRtl() { return this.$element.hasClass('align-right') || (Rtl() && !this.$element.hasClass('align-left')); } /** * Adds event listeners to elements within the menu * @private * @function */ _events() { var _this = this, hasTouch = 'ontouchstart' in window || (typeof window.ontouchstart !== 'undefined'), parClass = 'is-dropdown-submenu-parent'; // used for onClick and in the keyboard handlers var handleClickFn = function(e) { var $elem = $(e.target).parentsUntil('ul', `.${parClass}`), hasSub = $elem.hasClass(parClass), hasClicked = $elem.attr('data-is-click') === 'true', $sub = $elem.children('.is-dropdown-submenu'); if (hasSub) { if (hasClicked) { if (!_this.options.closeOnClick || (!_this.options.clickOpen && !hasTouch) || (_this.options.forceFollow && hasTouch)) { return; } else { e.stopImmediatePropagation(); e.preventDefault(); _this._hide($elem); } } else { e.preventDefault(); e.stopImmediatePropagation(); _this._show($sub); $elem.add($elem.parentsUntil(_this.$element, `.${parClass}`)).attr('data-is-click', true); } } }; if (this.options.clickOpen || hasTouch) { this.$menuItems.on('click.zf.dropdownmenu touchstart.zf.dropdownmenu', handleClickFn); } // Handle Leaf element Clicks if(_this.options.closeOnClickInside){ this.$menuItems.on('click.zf.dropdownmenu', function(e) { var $elem = $(this), hasSub = $elem.hasClass(parClass); if(!hasSub){ _this._hide(); } }); } if (!this.options.disableHover) { this.$menuItems.on('mouseenter.zf.dropdownmenu', function (e) { var $elem = $(this), hasSub = $elem.hasClass(parClass); if (hasSub) { clearTimeout($elem.data('_delay')); $elem.data('_delay', setTimeout(function () { _this._show($elem.children('.is-dropdown-submenu')); }, _this.options.hoverDelay)); } }).on('mouseleave.zf.dropdownMenu', ignoreMousedisappear(function (e) { var $elem = $(this), hasSub = $elem.hasClass(parClass); if (hasSub && _this.options.autoclose) { if ($elem.attr('data-is-click') === 'true' && _this.options.clickOpen) { return false; } clearTimeout($elem.data('_delay')); $elem.data('_delay', setTimeout(function () { _this._hide($elem); }, _this.options.closingTime)); } })); } this.$menuItems.on('keydown.zf.dropdownmenu', function(e) { var $element = $(e.target).parentsUntil('ul', '[role="menuitem"]'), isTab = _this.$tabs.index($element) > -1, $elements = isTab ? _this.$tabs : $element.siblings('li').add($element), $prevElement, $nextElement; $elements.each(function(i) { if ($(this).is($element)) { $prevElement = $elements.eq(i-1); $nextElement = $elements.eq(i+1); return; } }); var nextSibling = function() { $nextElement.children('a:first').focus(); e.preventDefault(); }, prevSibling = function() { $prevElement.children('a:first').focus(); e.preventDefault(); }, openSub = function() { var $sub = $element.children('ul.is-dropdown-submenu'); if ($sub.length) { _this._show($sub); $element.find('li > a:first').focus(); e.preventDefault(); } else { return; } }, closeSub = function() { //if ($element.is(':first-child')) { var close = $element.parent('ul').parent('li'); close.children('a:first').focus(); _this._hide(close); e.preventDefault(); //} }; var functions = { open: openSub, close: function() { _this._hide(_this.$element); _this.$menuItems.eq(0).children('a').focus(); // focus to first element e.preventDefault(); }, handled: function() { e.stopImmediatePropagation(); } }; if (isTab) { if (_this._isVertical()) { // vertical menu if (_this._isRtl()) { // right aligned $.extend(functions, { down: nextSibling, up: prevSibling, next: closeSub, previous: openSub }); } else { // left aligned $.extend(functions, { down: nextSibling, up: prevSibling, next: openSub, previous: closeSub }); } } else { // horizontal menu if (_this._isRtl()) { // right aligned $.extend(functions, { next: prevSibling, previous: nextSibling, down: openSub, up: closeSub }); } else { // left aligned $.extend(functions, { next: nextSibling, previous: prevSibling, down: openSub, up: closeSub }); } } } else { // not tabs -> one sub if (_this._isRtl()) { // right aligned $.extend(functions, { next: closeSub, previous: openSub, down: nextSibling, up: prevSibling }); } else { // left aligned $.extend(functions, { next: openSub, previous: closeSub, down: nextSibling, up: prevSibling }); } } Keyboard.handleKey(e, 'DropdownMenu', functions); }); } /** * Adds an event handler to the body to close any dropdowns on a click. * @function * @private */ _addBodyHandler() { var $body = $(document.body), _this = this; $body.off('mouseup.zf.dropdownmenu touchend.zf.dropdownmenu') .on('mouseup.zf.dropdownmenu touchend.zf.dropdownmenu', function(e) { var $link = _this.$element.find(e.target); if ($link.length) { return; } _this._hide(); $body.off('mouseup.zf.dropdownmenu touchend.zf.dropdownmenu'); }); } /** * Opens a dropdown pane, and checks for collisions first. * @param {jQuery} $sub - ul element that is a submenu to show * @function * @private * @fires Dropdownmenu#show */ _show($sub) { var idx = this.$tabs.index(this.$tabs.filter(function(i, el) { return $(el).find($sub).length > 0; })); var $sibs = $sub.parent('li.is-dropdown-submenu-parent').siblings('li.is-dropdown-submenu-parent'); this._hide($sibs, idx); $sub.css('visibility', 'hidden').addClass('js-dropdown-active') .parent('li.is-dropdown-submenu-parent').addClass('is-active'); var clear = Box.ImNotTouchingYou($sub, null, true); if (!clear) { var oldClass = this.options.alignment === 'left' ? '-right' : '-left', $parentLi = $sub.parent('.is-dropdown-submenu-parent'); $parentLi.removeClass(`opens${oldClass}`).addClass(`opens-${this.options.alignment}`); clear = Box.ImNotTouchingYou($sub, null, true); if (!clear) { $parentLi.removeClass(`opens-${this.options.alignment}`).addClass('opens-inner'); } this.changed = true; } $sub.css('visibility', ''); if (this.options.closeOnClick) { this._addBodyHandler(); } /** * Fires when the new dropdown pane is visible. * @event Dropdownmenu#show */ this.$element.trigger('show.zf.dropdownmenu', [$sub]); } /** * Hides a single, currently open dropdown pane, if passed a parameter, otherwise, hides everything. * @function * @param {jQuery} $elem - element with a submenu to hide * @param {Number} idx - index of the $tabs collection to hide * @private */ _hide($elem, idx) { var $toClose; if ($elem && $elem.length) { $toClose = $elem; } else if (typeof idx !== 'undefined') { $toClose = this.$tabs.not(function(i, el) { return i === idx; }); } else { $toClose = this.$element; } var somethingToClose = $toClose.hasClass('is-active') || $toClose.find('.is-active').length > 0; if (somethingToClose) { $toClose.find('li.is-active').add($toClose).attr({ 'data-is-click': false }).removeClass('is-active'); $toClose.find('ul.js-dropdown-active').removeClass('js-dropdown-active'); if (this.changed || $toClose.find('opens-inner').length) { var oldClass = this.options.alignment === 'left' ? 'right' : 'left'; $toClose.find('li.is-dropdown-submenu-parent').add($toClose) .removeClass(`opens-inner opens-${this.options.alignment}`) .addClass(`opens-${oldClass}`); this.changed = false; } /** * Fires when the open menus are closed. * @event Dropdownmenu#hide */ this.$element.trigger('hide.zf.dropdownmenu', [$toClose]); } } /** * Destroys the plugin. * @function */ _destroy() { this.$menuItems.off('.zf.dropdownmenu').removeAttr('data-is-click') .removeClass('is-right-arrow is-left-arrow is-down-arrow opens-right opens-left opens-inner'); $(document.body).off('.zf.dropdownmenu'); Nest.Burn(this.$element, 'dropdown'); } } /** * Default settings for plugin */ DropdownMenu.defaults = { /** * Disallows hover events from opening submenus * @option * @type {boolean} * @default false */ disableHover: false, /** * Allow a submenu to automatically close on a mouseleave event, if not clicked open. * @option * @type {boolean} * @default true */ autoclose: true, /** * Amount of time to delay opening a submenu on hover event. * @option * @type {number} * @default 50 */ hoverDelay: 50, /** * Allow a submenu to open/remain open on parent click event. Allows cursor to move away from menu. * @option * @type {boolean} * @default false */ clickOpen: false, /** * Amount of time to delay closing a submenu on a mouseleave event. * @option * @type {number} * @default 500 */ closingTime: 500, /** * Position of the menu relative to what direction the submenus should open. Handled by JS. Can be `'auto'`, `'left'` or `'right'`. * @option * @type {string} * @default 'auto' */ alignment: 'auto', /** * Allow clicks on the body to close any open submenus. * @option * @type {boolean} * @default true */ closeOnClick: true, /** * Allow clicks on leaf anchor links to close any open submenus. * @option * @type {boolean} * @default true */ closeOnClickInside: true, /** * Class applied to vertical oriented menus, Foundation default is `vertical`. Update this if using your own class. * @option * @type {string} * @default 'vertical' */ verticalClass: 'vertical', /** * Class applied to right-side oriented menus, Foundation default is `align-right`. Update this if using your own class. * @option * @type {string} * @default 'align-right' */ rightClass: 'align-right', /** * Boolean to force overide the clicking of links to perform default action, on second touch event for mobile. * @option * @type {boolean} * @default true */ forceFollow: true }; export {DropdownMenu};