/** * Dropdown Content component. * * @author Htmlstream * @version 1.0 */ ; (function ($) { 'use strict'; $.HSCore.components.HSDropdown = { /** * Base configuration of the component. * * @private */ _baseConfig: { dropdownEvent: 'click', dropdownType: 'simple', dropdownDuration: 300, dropdownEasing: 'linear', dropdownAnimationIn: 'fadeIn', dropdownAnimationOut: 'fadeOut', dropdownHideOnScroll: true, dropdownHideOnBlur: false, dropdownDelay: 350, dropdownOpenedElement: 'init', afterOpen: function (invoker) { }, beforeClose: function (invoker) { }, afterClose: function (invoker) { } }, /** * Collection of all initialized items on the page. * * @private */ _pageCollection: $(), /** * Initialization. * * @param {jQuery} collection * @param {Object} config * * @public * @return {jQuery} */ init: function (collection, config) { var self; if (!collection || !collection.length) return; self = this; var fieldsQty; collection.each(function (i, el) { var $this = $(el), itemConfig; if ($this.data('HSDropdown')) return; itemConfig = config && $.isPlainObject(config) ? $.extend(true, {}, self._baseConfig, config, $this.data()) : $.extend(true, {}, self._baseConfig, $this.data()); switch (itemConfig.dropdownType) { case 'css-animation' : $this.data('HSDropdown', new DropdownCSSAnimation($this, itemConfig)); break; case 'jquery-slide' : $this.data('HSDropdown', new DropdownJSlide($this, itemConfig)); break; default : $this.data('HSDropdown', new DropdownSimple($this, itemConfig)); } self._pageCollection = self._pageCollection.add($this); self._bindEvents($this, itemConfig.dropdownEvent, itemConfig.dropdownDelay); var UnFold = $(el).data('HSDropdown'); fieldsQty = $(UnFold.target).find('input, textarea').length; if ($(UnFold.target).find('[data-dropdown-target]').length) { $this.addClass('target-of-invoker-has-dropdowns'); } }); $(document).on('click touchstart', 'body', function (e) { if (e.target.id == self._baseConfig.dropdownOpenedElement) return; if ($(e.target).closest('#' + self._baseConfig.dropdownOpenedElement).length) return; self._pageCollection.each(function (i, el) { var windW = window.innerWidth, optIsMobileOnly = Boolean($(el).data('is-mobile-only')); if (!optIsMobileOnly) { $(el).data('HSDropdown').hide(); } else if (optIsMobileOnly && windW < 769) { $(el).data('HSDropdown').hide(); } $(el).data('HSDropdown').config.beforeClose.call(self.target, self.element); }); }); $(window).on('scroll.HSDropdown', function () { self._pageCollection.each(function (i, el) { var UnFold = $(el).data('HSDropdown'); if (UnFold.getOption('dropdownHideOnScroll') && fieldsQty === 0) { UnFold.hide(); } else if (UnFold.getOption('dropdownHideOnScroll') && !(/iPhone|iPad|iPod/i.test(navigator.userAgent))) { UnFold.hide(); } }); }); $(window).on('resize.HSDropdown', function () { if (self._resizeTimeOutId) clearTimeout(self._resizeTimeOutId); self._resizeTimeOutId = setTimeout(function () { self._pageCollection.each(function (i, el) { var UnFold = $(el).data('HSDropdown'); UnFold.smartPosition(UnFold.target); }); }, 50); }); $(document).on('keydown.HSDropdown', function (e) { if ($('body').hasClass('u-dropdown-opened')) { if (e.keyCode && e.keyCode === 38 || e.keyCode && e.keyCode === 40) { e.preventDefault(); } } }); return collection; }, /** * Binds necessary events. * * @param {jQuery} $invoker * @param {String} eventType * @param {Number} delay * @private */ _bindEvents: function ($invoker, eventType, delay) { var self = this, $dropdown = $($invoker.data('dropdown-target')); if (eventType === 'hover' && !_isTouch()) { $invoker.on('mouseenter.HSDropdown', function () { var $invoker = $(this), HSDropdown = $invoker.data('HSDropdown'); if (!HSDropdown) return; if (HSDropdown.dropdownTimeOut) clearTimeout(HSDropdown.dropdownTimeOut); HSDropdown.show(); $('body').addClass('u-dropdown-opened'); }) .on('mouseleave.HSDropdown', function () { var $invoker = $(this), HSDropdown = $invoker.data('HSDropdown'); if (!HSDropdown) return; HSDropdown.dropdownTimeOut = setTimeout(function () { HSDropdown.hide(); $('body').removeClass('u-dropdown-opened'); }, delay); }); if ($dropdown.length) { $dropdown.on('mouseenter.HSDropdown', function () { var HSDropdown = $invoker.data('HSDropdown'); if (HSDropdown.dropdownTimeOut) clearTimeout(HSDropdown.dropdownTimeOut); HSDropdown.show(); }) .on('mouseleave.HSDropdown', function () { var HSDropdown = $invoker.data('HSDropdown'); HSDropdown.dropdownTimeOut = setTimeout(function () { HSDropdown.hide(); }, delay); }); } } else { $invoker.on('click.HSDropdown', function (e) { var $curInvoker = $(this), $dropdownNotHasInnerDropdowns = $('[data-dropdown-target].active:not(.target-of-invoker-has-dropdowns)'), $dropdownHasInnerDropdown = $('[data-dropdown-target].active.target-of-invoker-has-dropdowns'); self._baseConfig.dropdownOpenedElement = $curInvoker.data('HSDropdown').target[0].id; if (!$curInvoker.data('HSDropdown')) return; if (!$curInvoker.hasClass('target-of-invoker-has-dropdowns')) { if ($dropdownNotHasInnerDropdowns.length) { $dropdownNotHasInnerDropdowns.data('HSDropdown').toggle(); } } else { if ($dropdownHasInnerDropdown.length) { $dropdownHasInnerDropdown.data('HSDropdown').toggle(); } } $curInvoker.data('HSDropdown').toggle(); e.stopPropagation(); e.preventDefault(); }); if (Boolean($invoker.data('dropdown-target-is-menu'))) { var $target = $($invoker.data('dropdown-target')), $targetItems = $target.children(); $targetItems.on('click', function() { $invoker.data('HSDropdown').toggle(); }); } } } }; function _isTouch() { return 'ontouchstart' in window; } /** * Abstract Dropdown class. * * @param {jQuery} element * @param {Object} config * @abstract */ function AbstractDropdown(element, config) { if (!element.length) return false; this.element = element; this.config = config; this.target = $(this.element.data('dropdown-target')); this.allInvokers = $('[data-dropdown-target="' + this.element.data('dropdown-target') + '"]'); this.toggle = function () { if (!this.target.length) return this; if (this.defaultState) { this.show(); } else { this.hide(); } return this; }; this.smartPosition = function (target) { if (target.data('baseDirection')) { target.css( target.data('baseDirection').direction, target.data('baseDirection').value ); } // target.removeClass('u-dropdown--reverse-y'); var $w = $(window), styles = getComputedStyle(target.get(0)), direction = Math.abs(parseInt(styles.left, 10)) < 40 ? 'left' : 'right', targetOuterGeometry = target.offset(); // horizontal axis if (direction === 'right') { if (!target.data('baseDirection')) target.data('baseDirection', { direction: 'right', value: parseInt(styles.right, 10) }); if (targetOuterGeometry.left < 0) { target.css( 'right', (parseInt(target.css('right'), 10) - (targetOuterGeometry.left - 10)) * -1 ); } } else { if (!target.data('baseDirection')) target.data('baseDirection', { direction: 'left', value: parseInt(styles.left, 10) }); if (targetOuterGeometry.left + target.outerWidth() > $w.width()) { target.css( 'left', (parseInt(target.css('left'), 10) - (targetOuterGeometry.left + target.outerWidth() + 10 - $w.width())) ); } } // vertical axis if (targetOuterGeometry.top + target.outerHeight() - $w.scrollTop() > $w.height()) { // target.addClass('u-dropdown--reverse-y'); } }; this.getOption = function (option) { return this.config[option] ? this.config[option] : null; }; return true; } /** * DropdownSimple constructor. * * @param {jQuery} element * @param {Object} config * @constructor */ function DropdownSimple(element, config) { if (!AbstractDropdown.call(this, element, config)) return; Object.defineProperty(this, 'defaultState', { get: function () { return this.target.hasClass('u-dropdown--hidden'); } }); this.target.addClass('u-dropdown--simple'); this.hide(); } /** * Shows Dropdown. * * @public * @return {DropdownSimple} */ DropdownSimple.prototype.show = function () { var activeEls = $(this)[0].config.dropdownTarget; $('[data-dropdown-target="' + activeEls + '"]').addClass('active'); this.smartPosition(this.target); this.target.removeClass('u-dropdown--hidden'); if (this.allInvokers.length) this.allInvokers.attr('aria-expanded', 'true'); this.config.afterOpen.call(this.target, this.element); return this; }; /** * Hides Dropdown. * * @public * @return {DropdownSimple} */ DropdownSimple.prototype.hide = function () { var activeEls = $(this)[0].config.dropdownTarget; $('[data-dropdown-target="' + activeEls + '"]').removeClass('active'); this.target.addClass('u-dropdown--hidden'); if (this.allInvokers.length) this.allInvokers.attr('aria-expanded', 'false'); this.config.afterClose.call(this.target, this.element); return this; }; /** * DropdownCSSAnimation constructor. * * @param {jQuery} element * @param {Object} config * @constructor */ function DropdownCSSAnimation(element, config) { if (!AbstractDropdown.call(this, element, config)) return; var self = this; this.target .addClass('u-dropdown--css-animation u-dropdown--hidden') .css('animation-duration', self.config.dropdownDuration + 'ms'); Object.defineProperty(this, 'defaultState', { get: function () { return this.target.hasClass('u-dropdown--hidden'); } }); if (this.target.length) { this.target.on('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function (e) { if (self.target.hasClass(self.config.dropdownAnimationOut)) { self.target.removeClass(self.config.dropdownAnimationOut) .addClass('u-dropdown--hidden'); if (self.allInvokers.length) self.allInvokers.attr('aria-expanded', 'false'); self.config.afterClose.call(self.target, self.element); } if (self.target.hasClass(self.config.dropdownAnimationIn)) { if (self.allInvokers.length) self.allInvokers.attr('aria-expanded', 'true'); self.config.afterOpen.call(self.target, self.element); } e.preventDefault(); e.stopPropagation(); }); } } /** * Shows Dropdown. * * @public * @return {DropdownCSSAnimation} */ DropdownCSSAnimation.prototype.show = function () { var activeEls = $(this)[0].config.dropdownTarget; $('[data-dropdown-target="' + activeEls + '"]').addClass('active'); this.smartPosition(this.target); this.target.removeClass('u-dropdown--hidden') .removeClass(this.config.dropdownAnimationOut) .addClass(this.config.dropdownAnimationIn); }; /** * Hides Dropdown. * * @public * @return {DropdownCSSAnimation} */ DropdownCSSAnimation.prototype.hide = function () { var activeEls = $(this)[0].config.dropdownTarget; $('[data-dropdown-target="' + activeEls + '"]').removeClass('active'); this.target.removeClass(this.config.dropdownAnimationIn) .addClass(this.config.dropdownAnimationOut); }; /** * DropdownSlide constructor. * * @param {jQuery} element * @param {Object} config * @constructor */ function DropdownJSlide(element, config) { if (!AbstractDropdown.call(this, element, config)) return; this.target.addClass('u-dropdown--jquery-slide u-dropdown--hidden').hide(); Object.defineProperty(this, 'defaultState', { get: function () { return this.target.hasClass('u-dropdown--hidden'); } }); } /** * Shows Dropdown. * * @public * @return {DropdownJSlide} */ DropdownJSlide.prototype.show = function () { var self = this; var activeEls = $(this)[0].config.dropdownTarget; $('[data-dropdown-target="' + activeEls + '"]').addClass('active'); this.smartPosition(this.target); this.target.removeClass('u-dropdown--hidden').stop().slideDown({ duration: self.config.dropdownDuration, easing: self.config.dropdownEasing, complete: function () { self.config.afterOpen.call(self.target, self.element); } }); }; /** * Hides Dropdown. * * @public * @return {DropdownJSlide} */ DropdownJSlide.prototype.hide = function () { var self = this; var activeEls = $(this)[0].config.dropdownTarget; $('[data-dropdown-target="' + activeEls + '"]').removeClass('active'); this.target.slideUp({ duration: self.config.dropdownDuration, easing: self.config.dropdownEasing, complete: function () { self.config.afterClose.call(self.target, self.element); self.target.addClass('u-dropdown--hidden'); } }); } })(jQuery);