/*!

* Tabby v11.2.0: Simple, mobile-first toggle tabs.
* (c) 2016 Chris Ferdinandi
* MIT License
* http://github.com/cferdinandi/tabby
*/

(function (root, factory) {

if (typeof define === 'function' && define.amd) {
        define([], factory(root));
} else if (typeof exports === 'object') {
        module.exports = factory(root);
} else {
        root.tabby = factory(root);
}

})(typeof global !== 'undefined' ? global : this.window || this.global, (function (root) {

'use strict';

//
// Variables
//

var tabby = {}; // Object for public APIs
var supports = 'querySelector' in document && 'addEventListener' in root && 'classList' in document.createElement('_') && 'onhashchange' in root; // Feature test
var settings, tab;

// Default settings
var defaults = {
        selectorToggle: '[data-tab]',
        selectorToggleGroup: '[data-tabs]',
        selectorContent: '[data-tabs-pane]',
        selectorContentGroup: '[data-tabs-content]',
        toggleActiveClass: 'active',
        contentActiveClass: 'active',
        initClass: 'js-tabby',
        stopVideo: true,
        callback: function () {}
};

//
// Methods
//

/**
 * A simple forEach() implementation for Arrays, Objects and NodeLists
 * @private
 * @param {Array|Object|NodeList} collection Collection of items to iterate
 * @param {Function} callback Callback function for each iteration
 * @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`)
 */
var forEach = function (collection, callback, scope) {
        if (Object.prototype.toString.call(collection) === '[object Object]') {
                for (var prop in collection) {
                        if (Object.prototype.hasOwnProperty.call(collection, prop)) {
                                callback.call(scope, collection[prop], prop, collection);
                        }
                }
        } else {
                for (var i = 0, len = collection.length; i < len; i++) {
                        callback.call(scope, collection[i], i, collection);
                }
        }
};

/**
 * Merge defaults with user options
 * @private
 * @param {Object} defaults Default settings
 * @param {Object} options User options
 * @returns {Object} Merged values of defaults and options
 */
var extend = function () {

        // Variables
        var extended = {};
        var deep = false;
        var i = 0;
        var length = arguments.length;

        // Check if a deep merge
        if (Object.prototype.toString.call(arguments[0]) === '[object Boolean]') {
                deep = arguments[0];
                i++;
        }

        // Merge the object into the extended object
        var merge = function (obj) {
                for (var prop in obj) {
                        if (Object.prototype.hasOwnProperty.call(obj, prop)) {
                                // If deep merge and property is an object, merge properties
                                if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
                                        extended[prop] = extend(true, extended[prop], obj[prop]);
                                } else {
                                        extended[prop] = obj[prop];
                                }
                        }
                }
        };

        // Loop through each object and conduct a merge
        for (; i < length; i++) {
                var obj = arguments[i];
                merge(obj);
        }

        return extended;

};

/**
 * Get the closest matching element up the DOM tree.
 * @private
 * @param  {Element} elem     Starting element
 * @param  {String}  selector Selector to match against
 * @return {Boolean|Element}  Returns null if not match found
 */
var getClosest = function (elem, selector) {

        // Element.matches() polyfill
        if (!Element.prototype.matches) {
                Element.prototype.matches =
                        Element.prototype.matchesSelector ||
                        Element.prototype.mozMatchesSelector ||
                        Element.prototype.msMatchesSelector ||
                        Element.prototype.oMatchesSelector ||
                        Element.prototype.webkitMatchesSelector ||
                        function (s) {
                                var matches = (this.document || this.ownerDocument).querySelectorAll(s),
                                        i = matches.length;
                                while (--i >= 0 && matches.item(i) !== this) {}
                                return i > -1;
                        };
        }

        // Get closest match
        for (; elem && elem !== document; elem = elem.parentNode) {
                if (elem.matches(selector)) return elem;
        }

        return null;

};

/**
 * Escape special characters for use with querySelector
 * @public
 * @param {String} id The anchor ID to escape
 * @author Mathias Bynens
 * @link https://github.com/mathiasbynens/CSS.escape
 */
var escapeCharacters = function (id) {

        // Remove leading hash
        if (id.charAt(0) === '#') {
                id = id.substr(1);
        }

        var string = String(id);
        var length = string.length;
        var index = -1;
        var codeUnit;
        var result = '';
        var firstCodeUnit = string.charCodeAt(0);
        while (++index < length) {
                codeUnit = string.charCodeAt(index);
                // Note: there’s no need to special-case astral symbols, surrogate
                // pairs, or lone surrogates.

                // If the character is NULL (U+0000), then throw an
                // `InvalidCharacterError` exception and terminate these steps.
                if (codeUnit === 0x0000) {
                        throw new InvalidCharacterError(
                                'Invalid character: the input contains U+0000.'
                        );
                }

                if (
                        // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
                        // U+007F, […]
                        (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
                        // If the character is the first character and is in the range [0-9]
                        // (U+0030 to U+0039), […]
                        (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
                        // If the character is the second character and is in the range [0-9]
                        // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
                        (
                                index === 1 &&
                                codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
                                firstCodeUnit === 0x002D
                        )
                ) {
                        // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point
                        result += '\\' + codeUnit.toString(16) + ' ';
                        continue;
                }

                // If the character is not handled by one of the above rules and is
                // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
                // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
                // U+005A), or [a-z] (U+0061 to U+007A), […]
                if (
                        codeUnit >= 0x0080 ||
                        codeUnit === 0x002D ||
                        codeUnit === 0x005F ||
                        codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
                        codeUnit >= 0x0041 && codeUnit <= 0x005A ||
                        codeUnit >= 0x0061 && codeUnit <= 0x007A
                ) {
                        // the character itself
                        result += string.charAt(index);
                        continue;
                }

                // Otherwise, the escaped character.
                // http://dev.w3.org/csswg/cssom/#escape-a-character
                result += '\\' + string.charAt(index);

        }

        return '#' + result;

};

/**
 * Stop YouTube, Vimeo, and HTML5 videos from playing when leaving the slide
 * @private
 * @param  {Element} content The content container the video is in
 * @param  {String} activeClass The class asigned to expanded content areas
 */
var stopVideos = function (content, settings) {

        // Check if stop video enabled
        if (!settings.stopVideo) return;

        // Only run if content container is closed
        if (content.classList.contains(settings.contentActiveClass)) return;

        // Check if the video is an iframe or HTML5 video
        var iframe = content.querySelector('iframe');
        var video = content.querySelector('video');

        // Stop the video
        if (iframe) {
                var iframeSrc = iframe.src;
                iframe.src = iframeSrc;
        }
        if (video) {
                video.pause();
        }

};

/**
 * Add focus to tab
 * @private
 * @param  {node}   tab      The content to bring into focus
 * @param  {object} settings Options
 */
var adjustFocus = function (tab, settings) {

        if (tab.hasAttribute('data-tab-no-focus')) return;

        // If tab is closed, remove tabindex
        if (!tab.classList.contains(settings.contentActiveClass)) {
                if (tab.hasAttribute('data-tab-focused')) {
                        tab.removeAttribute('tabindex');
                }
                return;
        }

        // Get current position on the page
        var position = {
                x: root.pageXOffset,
                y: root.pageYOffset
        };

        // Set focus and reset position to account for page jump on focus
        tab.focus();
        if (document.activeElement.id !== tab.id) {
                tab.setAttribute('tabindex', '-1');
                tab.setAttribute('data-tab-focused', true);
                tab.focus();
        }
        root.scrollTo(position.x, position.y);

};

/**
 * Toggle tab toggle active state
 * @private
 * @param  {Node}   toggle   The toggle element
 * @param  {Object} settings
 */
var toggleToggles = function (toggle, settings) {

        // Variables
        var toggleGroup = getClosest(toggle, settings.selectorToggleGroup); // The parent for the toggle group
        if (!toggleGroup) return;
        var toggles = toggleGroup.querySelectorAll(settings.selectorToggle); // The toggles in the group
        var toggleList;

        // Show or hide each toggle
        // @todo Start here
        forEach(toggles, (function (item) {

                // If this is the selected toggle, activate it
                if (item.hash === toggle.hash) {

                        // Add active class
                        item.classList.add(settings.toggleActiveClass);

                        // If toggle is a list item, activate <li> element, too
                        toggleList = getClosest(item, 'li');
                        if (toggleList) {
                                toggleList.classList.add(settings.toggleActiveClass);
                        }

                        return;

                }

                // Otherwise, deactivate it
                item.classList.remove(settings.toggleActiveClass);
                toggleList = getClosest(item, 'li');
                if (toggleList) {
                        toggleList.classList.remove(settings.toggleActiveClass);
                }

        }));

};

/**
 * Toggle tab active state
 * @private
 * @param  {String} tabID    The ID of the tab to activate
 * @param  {Object} settings
 */
var toggleTabs = function (tabID, settings) {

        // Variables
        var tab = document.querySelector(escapeCharacters(tabID)); // The selected tab
        if (!tab) return;
        var tabGroup = getClosest(tab, settings.selectorContentGroup); // The parent for the tab group
        if (!tabGroup) return;
        var tabs = tabGroup.querySelectorAll(settings.selectorContent); // The tabs in the group

        // Show or hide each tab
        forEach(tabs, (function (tab) {

                // If this is the selected tab, show it
                if (tab.id === tabID.substring(1)) {
                        tab.classList.add(settings.contentActiveClass);
                        adjustFocus(tab, settings);
                        return;
                }

                // Otherwise, hide it
                tab.classList.remove(settings.contentActiveClass);
                stopVideos(tab, settings);
                adjustFocus(tab, settings);

        }));

};

/**
 * Show a tab and hide all others
 * @public
 * @param  {Element} toggle The element that toggled the show tab event
 * @param  {String}  tabID The ID of the tab to show
 * @param  {Object}  options
 */
tabby.toggleTab = function (tabID, toggle, options) {

        // Selectors and variables
        var localSettings = extend(settings || defaults, options || {}); // Merge user options with defaults
        var tabs = document.querySelectorAll(escapeCharacters(tabID)); // Get tab content

        // Toggle visibility of the toggles and tabs
        toggleTabs(tabID, localSettings);
        if (toggle) {
                toggleToggles(toggle, localSettings);
        }

        // Run callbacks after toggling tab
        localSettings.callback(tabs, toggle);

};

/**
 * Handle has change event
 * @private
 */
var hashChangeHandler = function (event) {

        // Get hash from URL
        var hash = root.location.hash;

        // If clicked tab is cached, reset it's ID
        if (tab) {
                tab.id = tab.getAttribute('data-tab-id');
                tab = null;
        }

        // If there's a URL hash, activate tab with matching ID
        if (!hash) return;
        var toggle = document.querySelector(settings.selectorToggle + '[href*="' + hash + '"]');
        tabby.toggleTab(hash, toggle);

};

/**
 * Handle toggle click events
 * @private
 */
var clickHandler = function (event) {

        // Don't run if right-click or command/control + click
        if (event.button !== 0 || event.metaKey || event.ctrlKey) return;

        // Check if event target is a tab toggle
        var toggle = getClosest(event.target, settings.selectorToggle);
        if (!toggle || !toggle.hash) return;

        // Don't run if toggle points to currently open tab
        if (toggle.hash === root.location.hash) {
                event.preventDefault();
                return;
        }

        // Get the tab content
        tab = document.querySelector(toggle.hash);

        // If tab content exists, save the ID as a data attribute and remove it (prevents scroll jump)
        if (!tab) return;
        tab.setAttribute('data-tab-id', tab.id);
        tab.id = '';

};

/**
 * Handle content focus events
 * @private
 */
var focusHandler = function (event) {

        // Only run if the focused content is in a tab
        tab = getClosest(event.target, settings.selectorContent);
        if (!tab) return;

        // Don't run if the content area is already open
        if (tab.classList.contains(settings.contentActiveClass)) return;

        // Store tab ID to variable and remove it from the tab
        var hash = tab.id;
        tab.setAttribute('data-tab-id', hash);
        tab.setAttribute('data-tab-no-focus', true);
        tab.id = '';

        // Change the hash
        location.hash = hash;

};

/**
 * Destroy the current initialization.
 * @public
 */
tabby.destroy = function () {
        if (!settings) return;
        document.documentElement.classList.remove(settings.initClass);
        document.removeEventListener('click', clickHandler, false);
        document.removeEventListener('focus', focusHandler, true);
        root.removeEventListener('hashchange', hashChangeHandler, false);
        settings = null;
        tab = null;
};

/**
 * Initialize Tabby
 * @public
 * @param {Object} options User settings
 */
tabby.init = function (options) {

        // feature test
        if (!supports) return;

        // Destroy any existing initializations
        tabby.destroy();

        // Merge user options with defaults
        settings = extend(defaults, options || {});

        // Add class to HTML element to activate conditional CSS
        document.documentElement.classList.add(settings.initClass);

        // Listen for all click events
        document.addEventListener('click', clickHandler, false);
        document.addEventListener('focus', focusHandler, true);
        root.addEventListener('hashchange', hashChangeHandler, false);

        // If URL has a hash, activate hashed tab by default
        hashChangeHandler();

};

//
// Public APIs
//

return tabby;

}));