416 lines
11 KiB
JavaScript
416 lines
11 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
import $ from 'jquery';
|
||
|
import { Keyboard } from './foundation.util.keyboard';
|
||
|
import { GetYoDigits, ignoreMousedisappear } from './foundation.core.utils';
|
||
|
import { Positionable } from './foundation.positionable';
|
||
|
|
||
|
import { Triggers } from './foundation.util.triggers';
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Dropdown module.
|
||
|
* @module foundation.dropdown
|
||
|
* @requires foundation.util.keyboard
|
||
|
* @requires foundation.util.box
|
||
|
* @requires foundation.util.triggers
|
||
|
*/
|
||
|
class Dropdown extends Positionable {
|
||
|
/**
|
||
|
* Creates a new instance of a dropdown.
|
||
|
* @class
|
||
|
* @name Dropdown
|
||
|
* @param {jQuery} element - jQuery object to make into a dropdown.
|
||
|
* Object should be of the dropdown panel, rather than its anchor.
|
||
|
* @param {Object} options - Overrides to the default plugin settings.
|
||
|
*/
|
||
|
_setup(element, options) {
|
||
|
this.$element = element;
|
||
|
this.options = $.extend({}, Dropdown.defaults, this.$element.data(), options);
|
||
|
this.className = 'Dropdown'; // ie9 back compat
|
||
|
|
||
|
// Triggers init is idempotent, just need to make sure it is initialized
|
||
|
Triggers.init($);
|
||
|
|
||
|
this._init();
|
||
|
|
||
|
Keyboard.register('Dropdown', {
|
||
|
'ENTER': 'toggle',
|
||
|
'SPACE': 'toggle',
|
||
|
'ESCAPE': 'close'
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initializes the plugin by setting/checking options and attributes, adding helper variables, and saving the anchor.
|
||
|
* @function
|
||
|
* @private
|
||
|
*/
|
||
|
_init() {
|
||
|
var $id = this.$element.attr('id');
|
||
|
|
||
|
this.$anchors = $(`[data-toggle="${$id}"]`).length ? $(`[data-toggle="${$id}"]`) : $(`[data-open="${$id}"]`);
|
||
|
this.$anchors.attr({
|
||
|
'aria-controls': $id,
|
||
|
'data-is-focus': false,
|
||
|
'data-yeti-box': $id,
|
||
|
'aria-haspopup': true,
|
||
|
'aria-expanded': false
|
||
|
});
|
||
|
|
||
|
this._setCurrentAnchor(this.$anchors.first());
|
||
|
|
||
|
if(this.options.parentClass){
|
||
|
this.$parent = this.$element.parents('.' + this.options.parentClass);
|
||
|
}else{
|
||
|
this.$parent = null;
|
||
|
}
|
||
|
|
||
|
// Set [aria-labelledby] on the Dropdown if it is not set
|
||
|
if (typeof this.$element.attr('aria-labelledby') === 'undefined') {
|
||
|
// Get the anchor ID or create one
|
||
|
if (typeof this.$currentAnchor.attr('id') === 'undefined') {
|
||
|
this.$currentAnchor.attr('id', GetYoDigits(6, 'dd-anchor'));
|
||
|
};
|
||
|
|
||
|
this.$element.attr('aria-labelledby', this.$currentAnchor.attr('id'));
|
||
|
}
|
||
|
|
||
|
this.$element.attr({
|
||
|
'aria-hidden': 'true',
|
||
|
'data-yeti-box': $id,
|
||
|
'data-resize': $id,
|
||
|
});
|
||
|
|
||
|
super._init();
|
||
|
this._events();
|
||
|
}
|
||
|
|
||
|
_getDefaultPosition() {
|
||
|
// handle legacy classnames
|
||
|
var position = this.$element[0].className.match(/(top|left|right|bottom)/g);
|
||
|
if(position) {
|
||
|
return position[0];
|
||
|
} else {
|
||
|
return 'bottom'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_getDefaultAlignment() {
|
||
|
// handle legacy float approach
|
||
|
var horizontalPosition = /float-(\S+)/.exec(this.$currentAnchor.attr('class'));
|
||
|
if(horizontalPosition) {
|
||
|
return horizontalPosition[1];
|
||
|
}
|
||
|
|
||
|
return super._getDefaultAlignment();
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Sets the position and orientation of the dropdown pane, checks for collisions if allow-overlap is not true.
|
||
|
* Recursively calls itself if a collision is detected, with a new position class.
|
||
|
* @function
|
||
|
* @private
|
||
|
*/
|
||
|
_setPosition() {
|
||
|
this.$element.removeClass(`has-position-${this.position} has-alignment-${this.alignment}`);
|
||
|
super._setPosition(this.$currentAnchor, this.$element, this.$parent);
|
||
|
this.$element.addClass(`has-position-${this.position} has-alignment-${this.alignment}`);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make it a current anchor.
|
||
|
* Current anchor as the reference for the position of Dropdown panes.
|
||
|
* @param {HTML} el - DOM element of the anchor.
|
||
|
* @function
|
||
|
* @private
|
||
|
*/
|
||
|
_setCurrentAnchor(el) {
|
||
|
this.$currentAnchor = $(el);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds event listeners to the element utilizing the triggers utility library.
|
||
|
* @function
|
||
|
* @private
|
||
|
*/
|
||
|
_events() {
|
||
|
var _this = this;
|
||
|
this.$element.on({
|
||
|
'open.zf.trigger': this.open.bind(this),
|
||
|
'close.zf.trigger': this.close.bind(this),
|
||
|
'toggle.zf.trigger': this.toggle.bind(this),
|
||
|
'resizeme.zf.trigger': this._setPosition.bind(this)
|
||
|
});
|
||
|
|
||
|
this.$anchors.off('click.zf.trigger')
|
||
|
.on('click.zf.trigger', function() { _this._setCurrentAnchor(this); });
|
||
|
|
||
|
if(this.options.hover){
|
||
|
this.$anchors.off('mouseenter.zf.dropdown mouseleave.zf.dropdown')
|
||
|
.on('mouseenter.zf.dropdown', function(){
|
||
|
_this._setCurrentAnchor(this);
|
||
|
|
||
|
var bodyData = $('body').data();
|
||
|
if(typeof(bodyData.whatinput) === 'undefined' || bodyData.whatinput === 'mouse') {
|
||
|
clearTimeout(_this.timeout);
|
||
|
_this.timeout = setTimeout(function(){
|
||
|
_this.open();
|
||
|
_this.$anchors.data('hover', true);
|
||
|
}, _this.options.hoverDelay);
|
||
|
}
|
||
|
}).on('mouseleave.zf.dropdown', ignoreMousedisappear(function(){
|
||
|
clearTimeout(_this.timeout);
|
||
|
_this.timeout = setTimeout(function(){
|
||
|
_this.close();
|
||
|
_this.$anchors.data('hover', false);
|
||
|
}, _this.options.hoverDelay);
|
||
|
}));
|
||
|
if(this.options.hoverPane){
|
||
|
this.$element.off('mouseenter.zf.dropdown mouseleave.zf.dropdown')
|
||
|
.on('mouseenter.zf.dropdown', function(){
|
||
|
clearTimeout(_this.timeout);
|
||
|
}).on('mouseleave.zf.dropdown', ignoreMousedisappear(function(){
|
||
|
clearTimeout(_this.timeout);
|
||
|
_this.timeout = setTimeout(function(){
|
||
|
_this.close();
|
||
|
_this.$anchors.data('hover', false);
|
||
|
}, _this.options.hoverDelay);
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
this.$anchors.add(this.$element).on('keydown.zf.dropdown', function(e) {
|
||
|
|
||
|
var $target = $(this),
|
||
|
visibleFocusableElements = Keyboard.findFocusable(_this.$element);
|
||
|
|
||
|
Keyboard.handleKey(e, 'Dropdown', {
|
||
|
open: function() {
|
||
|
if ($target.is(_this.$anchors) && !$target.is('input, textarea')) {
|
||
|
_this.open();
|
||
|
_this.$element.attr('tabindex', -1).focus();
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
},
|
||
|
close: function() {
|
||
|
_this.close();
|
||
|
_this.$anchors.focus();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds an event handler to the body to close any dropdowns on a click.
|
||
|
* @function
|
||
|
* @private
|
||
|
*/
|
||
|
_addBodyHandler() {
|
||
|
var $body = $(document.body).not(this.$element),
|
||
|
_this = this;
|
||
|
$body.off('click.zf.dropdown')
|
||
|
.on('click.zf.dropdown', function(e){
|
||
|
if(_this.$anchors.is(e.target) || _this.$anchors.find(e.target).length) {
|
||
|
return;
|
||
|
}
|
||
|
if(_this.$element.is(e.target) || _this.$element.find(e.target).length) {
|
||
|
return;
|
||
|
}
|
||
|
_this.close();
|
||
|
$body.off('click.zf.dropdown');
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Opens the dropdown pane, and fires a bubbling event to close other dropdowns.
|
||
|
* @function
|
||
|
* @fires Dropdown#closeme
|
||
|
* @fires Dropdown#show
|
||
|
*/
|
||
|
open() {
|
||
|
// var _this = this;
|
||
|
/**
|
||
|
* Fires to close other open dropdowns, typically when dropdown is opening
|
||
|
* @event Dropdown#closeme
|
||
|
*/
|
||
|
this.$element.trigger('closeme.zf.dropdown', this.$element.attr('id'));
|
||
|
this.$anchors.addClass('hover')
|
||
|
.attr({'aria-expanded': true});
|
||
|
// this.$element/*.show()*/;
|
||
|
|
||
|
this.$element.addClass('is-opening');
|
||
|
this._setPosition();
|
||
|
this.$element.removeClass('is-opening').addClass('is-open')
|
||
|
.attr({'aria-hidden': false});
|
||
|
|
||
|
if(this.options.autoFocus){
|
||
|
var $focusable = Keyboard.findFocusable(this.$element);
|
||
|
if($focusable.length){
|
||
|
$focusable.eq(0).focus();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(this.options.closeOnClick){ this._addBodyHandler(); }
|
||
|
|
||
|
if (this.options.trapFocus) {
|
||
|
Keyboard.trapFocus(this.$element);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Fires once the dropdown is visible.
|
||
|
* @event Dropdown#show
|
||
|
*/
|
||
|
this.$element.trigger('show.zf.dropdown', [this.$element]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Closes the open dropdown pane.
|
||
|
* @function
|
||
|
* @fires Dropdown#hide
|
||
|
*/
|
||
|
close() {
|
||
|
if(!this.$element.hasClass('is-open')){
|
||
|
return false;
|
||
|
}
|
||
|
this.$element.removeClass('is-open')
|
||
|
.attr({'aria-hidden': true});
|
||
|
|
||
|
this.$anchors.removeClass('hover')
|
||
|
.attr('aria-expanded', false);
|
||
|
|
||
|
/**
|
||
|
* Fires once the dropdown is no longer visible.
|
||
|
* @event Dropdown#hide
|
||
|
*/
|
||
|
this.$element.trigger('hide.zf.dropdown', [this.$element]);
|
||
|
|
||
|
if (this.options.trapFocus) {
|
||
|
Keyboard.releaseFocus(this.$element);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Toggles the dropdown pane's visibility.
|
||
|
* @function
|
||
|
*/
|
||
|
toggle() {
|
||
|
if(this.$element.hasClass('is-open')){
|
||
|
if(this.$anchors.data('hover')) return;
|
||
|
this.close();
|
||
|
}else{
|
||
|
this.open();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Destroys the dropdown.
|
||
|
* @function
|
||
|
*/
|
||
|
_destroy() {
|
||
|
this.$element.off('.zf.trigger').hide();
|
||
|
this.$anchors.off('.zf.dropdown');
|
||
|
$(document.body).off('click.zf.dropdown');
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Dropdown.defaults = {
|
||
|
/**
|
||
|
* Class that designates bounding container of Dropdown (default: window)
|
||
|
* @option
|
||
|
* @type {?string}
|
||
|
* @default null
|
||
|
*/
|
||
|
parentClass: null,
|
||
|
/**
|
||
|
* Amount of time to delay opening a submenu on hover event.
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 250
|
||
|
*/
|
||
|
hoverDelay: 250,
|
||
|
/**
|
||
|
* Allow submenus to open on hover events
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
hover: false,
|
||
|
/**
|
||
|
* Don't close dropdown when hovering over dropdown pane
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
hoverPane: false,
|
||
|
/**
|
||
|
* Number of pixels between the dropdown pane and the triggering element on open.
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 0
|
||
|
*/
|
||
|
vOffset: 0,
|
||
|
/**
|
||
|
* Number of pixels between the dropdown pane and the triggering element on open.
|
||
|
* @option
|
||
|
* @type {number}
|
||
|
* @default 0
|
||
|
*/
|
||
|
hOffset: 0,
|
||
|
/**
|
||
|
* Position of dropdown. Can be left, right, bottom, top, or auto.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default 'auto'
|
||
|
*/
|
||
|
position: 'auto',
|
||
|
/**
|
||
|
* Alignment of dropdown relative to anchor. Can be left, right, bottom, top, center, or auto.
|
||
|
* @option
|
||
|
* @type {string}
|
||
|
* @default 'auto'
|
||
|
*/
|
||
|
alignment: 'auto',
|
||
|
/**
|
||
|
* Allow overlap of container/window. If false, dropdown will first try to position as defined by data-position and data-alignment, but reposition if it would cause an overflow.
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
allowOverlap: false,
|
||
|
/**
|
||
|
* Allow overlap of only the bottom of the container. This is the most common
|
||
|
* behavior for dropdowns, allowing the dropdown to extend the bottom of the
|
||
|
* screen but not otherwise influence or break out of the container.
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default true
|
||
|
*/
|
||
|
allowBottomOverlap: true,
|
||
|
/**
|
||
|
* Allow the plugin to trap focus to the dropdown pane if opened with keyboard commands.
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
trapFocus: false,
|
||
|
/**
|
||
|
* Allow the plugin to set focus to the first focusable element within the pane, regardless of method of opening.
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
autoFocus: false,
|
||
|
/**
|
||
|
* Allows a click on the body to close the dropdown.
|
||
|
* @option
|
||
|
* @type {boolean}
|
||
|
* @default false
|
||
|
*/
|
||
|
closeOnClick: false
|
||
|
};
|
||
|
|
||
|
export {Dropdown};
|