/* ========================================================================== * bootstrap-tab-history.js * Author: Michael Narayan * Repository: https://github.com/mnarayan01/bootstrap-tab-history/ * ========================================================================== * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain a * copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. * ========================================================================== */ /* ========================================================================== */ /* JSHint directives */ /* */ /* global BootstrapTabHistory: true */ /* */ /* global document */ /* global jQuery */ /* global history */ /* global window */ /* ========================================================================== */ /** * Integrate [HTML5 history state tracking](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history) * with [`bootstrap/tab.js`](http://getbootstrap.com/javascript/#tabs). To enable tracking on a tab element, simply set * the `data-tab-history` attribute to true (or a string denoting a tab grouping). * * See the README for additional information. * * Functionality based upon bootstrap/tab.js v3.1.0; reference it when making any changes. */ BootstrapTabHistory = { options: { /** * When the anchor portion of the URI is used to activate a tab, scroll down to the given offset, rather than the * element with the given `id` attribute. Set to null to disable. Only relevant if showTabsBasedOnAnchor is true. * * May be overriden on a per-element basis by the attribute `data-tab-history-anchor-y-offset`. * * @public * @type {?number} */ defaultAnchorYOffset: 0, /** * Either 'push' or 'replace', for whether to use `history.pushState` or `history.replaceState`, resp. * * May be overriden on a per-element basis by the attribute `data-tab-history-changer`. * * @public * @type {string} */ defaultChanger: 'replace', /** * If true, update the URL in onShownTab in the calls to `history.pushState` and `history.replaceState`. Otherwise, * `null` is passed as the third parameter to these calls. * * May be overriden on a per-element basis by the attribute `data-tab-history-update-url`. * * @public * @type {boolean} */ defaultUpdateURL: false, /** * Should the anchor portion of the loaded URI be used to activate a single tab if no history was present on page * load. * * @public * @type {boolean} */ showTabsBasedOnAnchor: true } }; (function () { 'use strict'; jQuery(function () { if(history && history.pushState && history.replaceState) { var bootstrapTabHistory = history.state && history.state.bootstrapTabHistory; if(bootstrapTabHistory) { showTabsBasedOnState(bootstrapTabHistory); } else { showTabsBasedOnAnchor(); } backfillHistoryState(); jQuery(document).on('shown.bs.tab', onShownTab); jQuery(window).on('popstate', onPopState); } else { showTabsBasedOnAnchor(); } }); /** * Used to prevent onShownTab from registering shown events that we triggered via showTabsBasedOnState. * * @type {boolean} */ var showingTabsBasedOnState = false; /** * Used to update `history.state` to reflect the default active tabs on initial page load. This supports proper * `history.back` handling when `data-tab-history-update-url` is true. */ function backfillHistoryState() { var newState = null; jQuery('li.active > [data-tab-history]').each(function () { var $activeTabElement = jQuery(this); var selector = getTabSelector($activeTabElement); if(selector) { var tabGroup = getTabGroup($activeTabElement); if(tabGroup) { newState = createNewHistoryState(newState || history.state, tabGroup, selector); } } }); if(newState) { history.replaceState(newState, '', null); } } /** * Clone the existing state, ensure its bootstrapTabHistory attribute is an Object, and add the provided tabGroup to * said bootstrapTabHistory attribute. * * @param {?object} existingState * @param {!string} tabGroup * @param {!string} selector * @returns {!object} */ function createNewHistoryState(existingState, tabGroup, selector) { // Clone history.state and ensure it has a bootstrapTabHistory entry. var newState = jQuery.extend(true, {}, existingState, { bootstrapTabHistory: {} }); newState.bootstrapTabHistory[tabGroup] = selector; return newState; } /** * @param {jQuery} $tab * @returns {?string} */ function getTabGroup($tab) { return parseTruthyAttributeValue($tab.data('tab-history')); } /** * @param {jQuery} $tab * @returns {?string} */ function getTabSelector($tab) { return $tab.data('target') || $tab.attr('href'); } /** * Receives the `shown.bs.tab` event. Updates `history.state` as appropriate. * * @param {jQuery.Event} shownEvt */ function onShownTab(shownEvt) { if(!showingTabsBasedOnState) { var $activatedTab = jQuery(shownEvt.target); var selector = getTabSelector($activatedTab); if(selector) { var tabGroup = getTabGroup($activatedTab); if(tabGroup) { var historyChanger = $activatedTab.data('tab-history-changer') || BootstrapTabHistory.options.defaultChanger; var newState = createNewHistoryState(history.state, tabGroup, selector); var updateURL = (function ($activatedTab) { if(selector[0] === '#') { var elementUpdateURLOption = parseTruthyAttributeValue($activatedTab.data('tab-history-update-url')); if(elementUpdateURLOption === undefined) { return BootstrapTabHistory.options.defaultUpdateURL; } else { return elementUpdateURLOption; } } else { return false; } })($activatedTab); switch(historyChanger) { case 'push': history.pushState(newState, '', updateURL ? selector : null); break; case 'replace': history.replaceState(newState, '', updateURL ? selector : null); break; default: throw new Error('Unknown tab-history-changer: ' + historyChanger); } } } } } /** * Receives the `popstate` event. Shows tabs based on the value of `history.state` as appropriate. */ function onPopState() { var bootstrapTabHistory = history.state && history.state.bootstrapTabHistory; if(bootstrapTabHistory) { showTabsBasedOnState(bootstrapTabHistory); } } /** * Returns the given value, _unless_ that value is an empty string, in which case `true` is returned. * * Rationale: HAML data attributes which are set to `true` are rendered as a blank string. * * @param {*} value * @returns {*} */ function parseTruthyAttributeValue(value) { if(value) { return value; } else if(value === '') { return true; } else { return value; } } /** * Show tabs based upon the anchor component of `window.location`. */ function showTabsBasedOnAnchor() { if(BootstrapTabHistory.options.showTabsBasedOnAnchor) { var anchor = window.location && window.location.hash; if(anchor) { var $tabElement = showTabForSelector(anchor); if($tabElement && window.addEventListener && window.removeEventListener) { var anchorYOffset = (function ($tabElement) { var elementSetting = $tabElement.data('tab-history-anchor-y-offset'); if(elementSetting === undefined) { return BootstrapTabHistory.options.defaultAnchorYOffset; } else { return elementSetting; } })($tabElement); // HACK: This prevents scrolling to the tab on page load. This relies on the fact that we should never get // here on `history.forward`, `history.back`, or `location.reload`, since in all those situations the // `history.state` object should have been used (unless the browser did not support the modern History API). if(anchorYOffset || anchorYOffset === 0) { var scrollListener = function resetAnchorScroll () { window.removeEventListener('scroll', scrollListener); window.scrollTo(0, anchorYOffset); }; window.addEventListener('scroll', scrollListener); } } } } } /** * Show a tab which corresponds to the provided selector. * * @param {string} selector - A CSS selector. * @returns {?jQuery} - The tab which was found to show (even if said tab was already active). */ function showTabForSelector(selector) { var $tabElement = (function (selector) { var $ret = null; jQuery('[data-toggle="tab"], [data-toggle="pill"]').each(function () { var $potentialTab = jQuery(this); if(($potentialTab.attr('href') === selector || $potentialTab.data('target') === selector) && getTabGroup($potentialTab)) { $ret = $potentialTab; return false; } else { return null; } }); return $ret; })(selector); if($tabElement) { $tabElement.tab('show'); } return $tabElement; } /** * Iterate through the provided set of tab tab groups, showing the tabs based on the stored selectors. * * @param {object} bootstrapTabHistory - Each of the values is passed to showTabForSelector; the keys are actually irrelevant. */ function showTabsBasedOnState(bootstrapTabHistory) { showingTabsBasedOnState = true; try { for(var k in bootstrapTabHistory) { if(bootstrapTabHistory.hasOwnProperty(k)) { showTabForSelector(bootstrapTabHistory[k]); } } } finally { showingTabsBasedOnState = false; } } })();