123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- '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';
- import { Touch } from './foundation.util.touch'
- /**
- * 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};
|