UNPKG

jquery-bootstrap-scrolling-tabs

Version:
1,347 lines (1,124 loc) 72.4 kB
/** * jquery-bootstrap-scrolling-tabs * @version v2.6.1 * @link https://github.com/mikejacobson/jquery-bootstrap-scrolling-tabs * @author Mike Jacobson <michaeljjacobson1@gmail.com> * @license MIT License, http://www.opensource.org/licenses/MIT */ /** * jQuery plugin version of Angular directive angular-bootstrap-scrolling-tabs: * https://github.com/mikejacobson/angular-bootstrap-scrolling-tabs * * Usage: * * Use case #1: HTML-defined tabs * ------------------------------ * Demo: http://plnkr.co/edit/thyD0grCxIjyU4PoTt4x?p=preview * * Sample HTML: * * <!-- Nav tabs --> * <ul class="nav nav-tabs" role="tablist"> * <li role="presentation" class="active"><a href="#tab1" role="tab" data-toggle="tab">Tab Number 1</a></li> * <li role="presentation"><a href="#tab2" role="tab" data-toggle="tab">Tab Number 2</a></li> * <li role="presentation"><a href="#tab3" role="tab" data-toggle="tab">Tab Number 3</a></li> * <li role="presentation"><a href="#tab4" role="tab" data-toggle="tab">Tab Number 4</a></li> * </ul> * * <!-- Tab panes --> * <div class="tab-content"> * <div role="tabpanel" class="tab-pane active" id="tab1">Tab 1 content...</div> * <div role="tabpanel" class="tab-pane" id="tab2">Tab 2 content...</div> * <div role="tabpanel" class="tab-pane" id="tab3">Tab 3 content...</div> * <div role="tabpanel" class="tab-pane" id="tab4">Tab 4 content...</div> * </div> * * * JavaScript: * * $('.nav-tabs').scrollingTabs(); * * * Use Case #2: Data-driven tabs * ----------------------------- * Demo: http://plnkr.co/edit/MWBjLnTvJeetjU3NEimg?p=preview * * Sample HTML: * * <!-- build .nav-tabs and .tab-content in here --> * <div id="tabs-inside-here"></div> * * * JavaScript: * * $('#tabs-inside-here').scrollingTabs({ * tabs: tabs, // required * propPaneId: 'paneId', // optional * propTitle: 'title', // optional * propActive: 'active', // optional * propDisabled: 'disabled', // optional * propContent: 'content', // optional * ignoreTabPanes: false, // optional * scrollToTabEdge: false, // optional * disableScrollArrowsOnFullyScrolled: false, // optional * reverseScroll: false // optional * }); * * Settings/Options: * * tabs: tabs data array * prop*: name of your tab object's property name that * corresponds to that required tab property if * your property name is different than the * standard name (paneId, title, etc.) * tabsLiContent: * optional string array used to define custom HTML * for each tab's <li> element. Each entry is an HTML * string defining the tab <li> element for the * corresponding tab in the tabs array. * The default for a tab is: * '<li role="presentation" class=""></li>' * So, for example, if you had 3 tabs and you needed * a custom 'tooltip' attribute on each one, your * tabsLiContent array might look like this: * [ * '<li role="presentation" tooltip="Custom TT 1" class="custom-li"></li>', * '<li role="presentation" tooltip="Custom TT 2" class="custom-li"></li>', * '<li role="presentation" tooltip="Custom TT 3" class="custom-li"></li>' * ] * This plunk demonstrates its usage (in conjunction * with tabsPostProcessors): * http://plnkr.co/edit/ugJLMk7lmDCuZQziQ0k0 * tabsPostProcessors: * optional array of functions, each one associated * with an entry in the tabs array. When a tab element * has been created, its associated post-processor * function will be called with two arguments: the * newly created $li and $a jQuery elements for that tab. * This allows you to, for example, attach a custom * event listener to each anchor tag. * This plunk demonstrates its usage (in conjunction * with tabsLiContent): * http://plnkr.co/edit/ugJLMk7lmDCuZQziQ0k0 * ignoreTabPanes: relevant for data-driven tabs only--set to true if * you want the plugin to only touch the tabs * and to not generate the tab pane elements * that go in .tab-content. By default, the plugin * will generate the tab panes based on the content * property in your tab data, if a content property * is present. * scrollToTabEdge: set to true if you want to force full-width tabs * to display at the left scroll arrow. i.e., if the * scrolling stops with only half a tab showing, * it will snap the tab to its edge so the full tab * shows. * disableScrollArrowsOnFullyScrolled: * set to true if you want the left scroll arrow to * disable when the tabs are scrolled fully left, * and the right scroll arrow to disable when the tabs * are scrolled fully right. * reverseScroll: * set to true if you want the left scroll arrow to * slide the tabs left instead of right, and the right * scroll arrow to slide the tabs right. * enableSwiping: * set to true if you want to enable horizontal swiping * for touch screens. * widthMultiplier: * set to a value less than 1 if you want the tabs * container to be less than the full width of its * parent element. For example, set it to 0.5 if you * want the tabs container to be half the width of * its parent. * tabClickHandler: * a callback function to execute any time a tab is clicked. * The function is simply passed as the event handler * to jQuery's .on(), so the function will receive * the jQuery event as an argument, and the 'this' * inside the function will be the clicked tab's anchor * element. * cssClassLeftArrow, cssClassRightArrow: * custom values for the class attributes for the * left and right scroll arrows. The defaults are * 'glyphicon glyphicon-chevron-left' and * 'glyphicon glyphicon-chevron-right'. * Using different icons might require you to add * custom styling to the arrows to position the icons * correctly; the arrows can be targeted with these * selectors: * .scrtabs-tab-scroll-arrow * .scrtabs-tab-scroll-arrow-left * .scrtabs-tab-scroll-arrow-right * leftArrowContent, rightArrowContent: * custom HTML string for the left and right scroll * arrows. This will override any custom cssClassLeftArrow * and cssClassRightArrow settings. * For example, if you wanted to use svg icons, you * could set them like so: * * leftArrowContent: [ * '<div class="custom-arrow">', * ' <svg class="icon icon-point-left">', * ' <use xlink:href="#icon-point-left"></use>', * ' </svg>', * '</div>' * ].join(''), * rightArrowContent: [ * '<div class="custom-arrow">', * ' <svg class="icon icon-point-right">', * ' <use xlink:href="#icon-point-right"></use>', * ' </svg>', * '</div>' * ].join('') * * You would then need to add some CSS to make them * work correctly if you don't give them the * default scrtabs-tab-scroll-arrow classes. * This plunk shows it working with svg icons: * http://plnkr.co/edit/2MdZCAnLyeU40shxaol3?p=preview * * When using this option, you can also mark a child * element within the arrow content as the click target * if you don't want the entire content to be * clickable. You do that my adding the CSS class * 'scrtabs-click-target' to the element that should * be clickable, like so: * * leftArrowContent: [ * '<div class="scrtabs-tab-scroll-arrow scrtabs-tab-scroll-arrow-left">', * ' <button class="scrtabs-click-target" type="button">', * ' <i class="custom-chevron-left"></i>', * ' </button>', * '</div>' * ].join(''), * rightArrowContent: [ * '<div class="scrtabs-tab-scroll-arrow scrtabs-tab-scroll-arrow-right">', * ' <button class="scrtabs-click-target" type="button">', * ' <i class="custom-chevron-right"></i>', * ' </button>', * '</div>' * ].join('') * * enableRtlSupport: * set to true if you want your site to support * right-to-left languages. If true, the plugin will * check the page's <html> tag for attribute dir="rtl" * and will adjust its behavior accordingly. * handleDelayedScrollbar: * set to true if you experience a situation where the * right scroll arrow wraps to the next line due to a * vertical scrollbar coming into existence on the page * after the plugin already calculated its width without * a scrollbar present. This would occur if, for example, * the bulk of the page's content loaded after a delay, * and only then did a vertical scrollbar become necessary. * It would also occur if a vertical scrollbar only appeared * on selection of a particular tab that had more content * than the default tab. * bootstrapVersion: * set to 4 if you're using Boostrap 4. Default is 3. * Bootstrap 4 handles some things differently than 3 * (e.g., the 'active' class gets applied to the tab's * 'li > a' element rather than the 'li' itself). * * * On tabs data change: * * $('#tabs-inside-here').scrollingTabs('refresh'); * * On tabs data change, if you want the active tab to be set based on * the updated tabs data (i.e., you want to override the current * active tab setting selected by the user), for example, if you * added a new tab and you want it to be the active tab: * * $('#tabs-inside-here').scrollingTabs('refresh', { * forceActiveTab: true * }); * * Any options that can be passed into the plugin can be set on the * plugin's 'defaults' object instead so you don't have to pass them in: * * $.fn.scrollingTabs.defaults.tabs = tabs; * $.fn.scrollingTabs.defaults.forceActiveTab = true; * $.fn.scrollingTabs.defaults.scrollToTabEdge = true; * $.fn.scrollingTabs.defaults.disableScrollArrowsOnFullyScrolled = true; * $.fn.scrollingTabs.defaults.reverseScroll = true; * $.fn.scrollingTabs.defaults.widthMultiplier = 0.5; * $.fn.scrollingTabs.defaults.tabClickHandler = function () { }; * * * Methods * ----------------------------- * - refresh * On window resize, the tabs should refresh themselves, but to force a refresh: * * $('.nav-tabs').scrollingTabs('refresh'); * * - scrollToActiveTab * On window resize, the active tab will automatically be scrolled to * if it ends up offscreen, but you can also programmatically force a * scroll to the active tab any time (if, for example, you're * programmatically setting the active tab) by calling the * 'scrollToActiveTab' method: * * $('.nav-tabs').scrollingTabs('scrollToActiveTab'); * * * Events * ----------------------------- * The plugin triggers event 'ready.scrtabs' when the tabs have * been wrapped in the scroller and are ready for viewing: * * $('.nav-tabs') * .scrollingTabs() * .on('ready.scrtabs', function() { * // tabs ready, do my other stuff... * }); * * $('#tabs-inside-here') * .scrollingTabs({ tabs: tabs }) * .on('ready.scrtabs', function() { * // tabs ready, do my other stuff... * }); * * * Destroying * ----------------------------- * To destroy: * * $('.nav-tabs').scrollingTabs('destroy'); * * $('#tabs-inside-here').scrollingTabs('destroy'); * * If you were wrapping markup, the markup will be restored; if your tabs * were data-driven, the tabs will be destroyed along with the plugin. * */ ;(function ($, window) { 'use strict'; /* jshint unused:false */ /* exported CONSTANTS */ var CONSTANTS = { CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL: 50, // timeout interval for repeatedly moving the tabs container // by one increment while the mouse is held down--decrease to // make mousedown continous scrolling faster SCROLL_OFFSET_FRACTION: 6, // each click moves the container this fraction of the fixed container--decrease // to make the tabs scroll farther per click DATA_KEY_DDMENU_MODIFIED: 'scrtabsddmenumodified', DATA_KEY_IS_MOUSEDOWN: 'scrtabsismousedown', DATA_KEY_BOOTSTRAP_TAB: 'bs.tab', CSS_CLASSES: { BOOTSTRAP4: 'scrtabs-bootstrap4', RTL: 'scrtabs-rtl', SCROLL_ARROW_CLICK_TARGET: 'scrtabs-click-target', SCROLL_ARROW_DISABLE: 'scrtabs-disable', SCROLL_ARROW_WITH_CLICK_TARGET: 'scrtabs-with-click-target' }, SLIDE_DIRECTION: { LEFT: 1, RIGHT: 2 }, EVENTS: { CLICK: 'click.scrtabs', DROPDOWN_MENU_HIDE: 'hide.bs.dropdown.scrtabs', DROPDOWN_MENU_SHOW: 'show.bs.dropdown.scrtabs', FORCE_REFRESH: 'forcerefresh.scrtabs', MOUSEDOWN: 'mousedown.scrtabs', MOUSEUP: 'mouseup.scrtabs', TABS_READY: 'ready.scrtabs', TOUCH_END: 'touchend.scrtabs', TOUCH_MOVE: 'touchmove.scrtabs', TOUCH_START: 'touchstart.scrtabs', WINDOW_RESIZE: 'resize.scrtabs' } }; // smartresize from Paul Irish (debounced window resize) (function (sr) { var debounce = function (func, threshold, execAsap) { var timeout; return function debounced() { var obj = this, args = arguments; function delayed() { if (!execAsap) { func.apply(obj, args); } timeout = null; } if (timeout) { clearTimeout(timeout); } else if (execAsap) { func.apply(obj, args); } timeout = setTimeout(delayed, threshold || 100); }; }; $.fn[sr] = function (fn, customEventName) { var eventName = customEventName || CONSTANTS.EVENTS.WINDOW_RESIZE; return fn ? this.bind(eventName, debounce(fn)) : this.trigger(sr); }; })('smartresizeScrtabs'); /* *********************************************************************************** * ElementsHandler - Class that each instance of ScrollingTabsControl will instantiate * **********************************************************************************/ function ElementsHandler(scrollingTabsControl) { var ehd = this; ehd.stc = scrollingTabsControl; } // ElementsHandler prototype methods (function (p) { p.initElements = function (options) { var ehd = this; ehd.setElementReferences(options); ehd.setEventListeners(options); }; p.listenForTouchEvents = function () { var ehd = this, stc = ehd.stc, smv = stc.scrollMovement, ev = CONSTANTS.EVENTS; var touching = false; var touchStartX; var startingContainerLeftPos; var newLeftPos; stc.$movableContainer .on(ev.TOUCH_START, function (e) { touching = true; startingContainerLeftPos = stc.movableContainerLeftPos; touchStartX = e.originalEvent.changedTouches[0].pageX; }) .on(ev.TOUCH_END, function () { touching = false; }) .on(ev.TOUCH_MOVE, function (e) { if (!touching) { return; } var touchPageX = e.originalEvent.changedTouches[0].pageX; var diff = touchPageX - touchStartX; if (stc.rtl) { diff = -diff; } var minPos; newLeftPos = startingContainerLeftPos + diff; if (newLeftPos > 0) { newLeftPos = 0; } else { minPos = smv.getMinPos(); if (newLeftPos < minPos) { newLeftPos = minPos; } } stc.movableContainerLeftPos = newLeftPos; var leftOrRight = stc.rtl ? 'right' : 'left'; stc.$movableContainer.css(leftOrRight, smv.getMovableContainerCssLeftVal()); smv.refreshScrollArrowsDisabledState(); }); }; p.refreshAllElementSizes = function () { var ehd = this, stc = ehd.stc, smv = stc.scrollMovement, scrollArrowsWereVisible = stc.scrollArrowsVisible, actionsTaken = { didScrollToActiveTab: false }, isPerformingSlideAnim = false, minPos; ehd.setElementWidths(); ehd.setScrollArrowVisibility(); // this could have been a window resize or the removal of a // dynamic tab, so make sure the movable container is positioned // correctly because, if it is far to the left and we increased the // window width, it's possible that the tabs will be too far left, // beyond the min pos. if (stc.scrollArrowsVisible) { // make sure container not too far left minPos = smv.getMinPos(); isPerformingSlideAnim = smv.scrollToActiveTab({ isOnWindowResize: true }); if (!isPerformingSlideAnim) { smv.refreshScrollArrowsDisabledState(); if (stc.rtl) { if (stc.movableContainerRightPos < minPos) { smv.incrementMovableContainerLeft(minPos); } } else { if (stc.movableContainerLeftPos < minPos) { smv.incrementMovableContainerRight(minPos); } } } actionsTaken.didScrollToActiveTab = true; } else if (scrollArrowsWereVisible) { // scroll arrows went away after resize, so position movable container at 0 stc.movableContainerLeftPos = 0; smv.slideMovableContainerToLeftPos(); } return actionsTaken; }; p.setElementReferences = function (settings) { var ehd = this, stc = ehd.stc, $tabsContainer = stc.$tabsContainer, $leftArrow, $rightArrow, $leftArrowClickTarget, $rightArrowClickTarget; stc.isNavPills = false; if (stc.rtl) { $tabsContainer.addClass(CONSTANTS.CSS_CLASSES.RTL); } if (stc.usingBootstrap4) { $tabsContainer.addClass(CONSTANTS.CSS_CLASSES.BOOTSTRAP4); } stc.$fixedContainer = $tabsContainer.find('.scrtabs-tabs-fixed-container'); $leftArrow = stc.$fixedContainer.prev(); $rightArrow = stc.$fixedContainer.next(); // if we have custom arrow content, we might have a click target defined if (settings.leftArrowContent) { $leftArrowClickTarget = $leftArrow.find('.' + CONSTANTS.CSS_CLASSES.SCROLL_ARROW_CLICK_TARGET); } if (settings.rightArrowContent) { $rightArrowClickTarget = $rightArrow.find('.' + CONSTANTS.CSS_CLASSES.SCROLL_ARROW_CLICK_TARGET); } if ($leftArrowClickTarget && $leftArrowClickTarget.length) { $leftArrow.addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_WITH_CLICK_TARGET); } else { $leftArrowClickTarget = $leftArrow; } if ($rightArrowClickTarget && $rightArrowClickTarget.length) { $rightArrow.addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_WITH_CLICK_TARGET); } else { $rightArrowClickTarget = $rightArrow; } stc.$movableContainer = $tabsContainer.find('.scrtabs-tabs-movable-container'); stc.$tabsUl = $tabsContainer.find('.nav-tabs'); // check for pills if (!stc.$tabsUl.length) { stc.$tabsUl = $tabsContainer.find('.nav-pills'); if (stc.$tabsUl.length) { stc.isNavPills = true; } } stc.$tabsLiCollection = stc.$tabsUl.find('> li'); stc.$slideLeftArrow = stc.reverseScroll ? $leftArrow : $rightArrow; stc.$slideLeftArrowClickTarget = stc.reverseScroll ? $leftArrowClickTarget : $rightArrowClickTarget; stc.$slideRightArrow = stc.reverseScroll ? $rightArrow : $leftArrow; stc.$slideRightArrowClickTarget = stc.reverseScroll ? $rightArrowClickTarget : $leftArrowClickTarget; stc.$scrollArrows = stc.$slideLeftArrow.add(stc.$slideRightArrow); stc.$win = $(window); }; p.setElementWidths = function () { var ehd = this, stc = ehd.stc; stc.winWidth = stc.$win.width(); stc.scrollArrowsCombinedWidth = stc.$slideLeftArrow.outerWidth() + stc.$slideRightArrow.outerWidth(); ehd.setFixedContainerWidth(); ehd.setMovableContainerWidth(); }; p.setEventListeners = function (settings) { var ehd = this, stc = ehd.stc, evh = stc.eventHandlers, ev = CONSTANTS.EVENTS, resizeEventName = ev.WINDOW_RESIZE + stc.instanceId; if (settings.enableSwiping) { ehd.listenForTouchEvents(); } stc.$slideLeftArrowClickTarget .off('.scrtabs') .on(ev.MOUSEDOWN, function (e) { evh.handleMousedownOnSlideMovContainerLeftArrow.call(evh, e); }) .on(ev.MOUSEUP, function (e) { evh.handleMouseupOnSlideMovContainerLeftArrow.call(evh, e); }) .on(ev.CLICK, function (e) { evh.handleClickOnSlideMovContainerLeftArrow.call(evh, e); }); stc.$slideRightArrowClickTarget .off('.scrtabs') .on(ev.MOUSEDOWN, function (e) { evh.handleMousedownOnSlideMovContainerRightArrow.call(evh, e); }) .on(ev.MOUSEUP, function (e) { evh.handleMouseupOnSlideMovContainerRightArrow.call(evh, e); }) .on(ev.CLICK, function (e) { evh.handleClickOnSlideMovContainerRightArrow.call(evh, e); }); if (stc.tabClickHandler) { stc.$tabsLiCollection .find('a[data-toggle="tab"]') .off(ev.CLICK) .on(ev.CLICK, stc.tabClickHandler); } if (settings.handleDelayedScrollbar) { ehd.listenForDelayedScrollbar(); } stc.$win .off(resizeEventName) .smartresizeScrtabs(function (e) { evh.handleWindowResize.call(evh, e); }, resizeEventName); $('body').on(CONSTANTS.EVENTS.FORCE_REFRESH, stc.elementsHandler.refreshAllElementSizes.bind(stc.elementsHandler)); }; p.listenForDelayedScrollbar = function () { var iframe = document.createElement('iframe'); iframe.id = "scrtabs-scrollbar-resize-listener"; iframe.style.cssText = 'height: 0; background-color: transparent; margin: 0; padding: 0; overflow: hidden; border-width: 0; position: absolute; width: 100%;'; iframe.onload = function() { var timeout; function handleResize() { try { $(window).trigger('resize'); timeout = null; } catch(e) {} } iframe.contentWindow.addEventListener('resize', function() { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(handleResize, 100); }); }; document.body.appendChild(iframe); }; p.setFixedContainerWidth = function () { var ehd = this, stc = ehd.stc, tabsContainerRect = stc.$tabsContainer.get(0).getBoundingClientRect(); /** * @author poletaew * It solves problem with rounding by jQuery.outerWidth * If we have real width 100.5 px, jQuery.outerWidth returns us 101 px and we get layout's fail */ stc.fixedContainerWidth = tabsContainerRect.width || (tabsContainerRect.right - tabsContainerRect.left); stc.fixedContainerWidth = stc.fixedContainerWidth * stc.widthMultiplier; stc.$fixedContainer.width(stc.fixedContainerWidth); }; p.setFixedContainerWidthForHiddenScrollArrows = function () { var ehd = this, stc = ehd.stc; stc.$fixedContainer.width(stc.fixedContainerWidth); }; p.setFixedContainerWidthForVisibleScrollArrows = function () { var ehd = this, stc = ehd.stc; stc.$fixedContainer.width(stc.fixedContainerWidth - stc.scrollArrowsCombinedWidth); }; p.setMovableContainerWidth = function () { var ehd = this, stc = ehd.stc, $tabLi = stc.$tabsUl.find('> li'); stc.movableContainerWidth = 0; if ($tabLi.length) { $tabLi.each(function () { var $li = $(this), totalMargin = 0; if (stc.isNavPills) { // pills have a margin-left, tabs have no margin totalMargin = parseInt($li.css('margin-left'), 10) + parseInt($li.css('margin-right'), 10); } stc.movableContainerWidth += ($li.outerWidth() + totalMargin); }); stc.movableContainerWidth += 1; // if the tabs don't span the width of the page, force the // movable container width to full page width so the bottom // border spans the page width instead of just spanning the // width of the tabs if (stc.movableContainerWidth < stc.fixedContainerWidth) { stc.movableContainerWidth = stc.fixedContainerWidth; } } stc.$movableContainer.width(stc.movableContainerWidth); }; p.setScrollArrowVisibility = function () { var ehd = this, stc = ehd.stc, shouldBeVisible = stc.movableContainerWidth > stc.fixedContainerWidth; if (shouldBeVisible && !stc.scrollArrowsVisible) { stc.$scrollArrows.show(); stc.scrollArrowsVisible = true; } else if (!shouldBeVisible && stc.scrollArrowsVisible) { stc.$scrollArrows.hide(); stc.scrollArrowsVisible = false; } if (stc.scrollArrowsVisible) { ehd.setFixedContainerWidthForVisibleScrollArrows(); } else { ehd.setFixedContainerWidthForHiddenScrollArrows(); } }; }(ElementsHandler.prototype)); /* *********************************************************************************** * EventHandlers - Class that each instance of ScrollingTabsControl will instantiate * **********************************************************************************/ function EventHandlers(scrollingTabsControl) { var evh = this; evh.stc = scrollingTabsControl; } // prototype methods (function (p){ p.handleClickOnSlideMovContainerLeftArrow = function () { var evh = this, stc = evh.stc; stc.scrollMovement.incrementMovableContainerLeft(); }; p.handleClickOnSlideMovContainerRightArrow = function () { var evh = this, stc = evh.stc; stc.scrollMovement.incrementMovableContainerRight(); }; p.handleMousedownOnSlideMovContainerLeftArrow = function () { var evh = this, stc = evh.stc; stc.$slideLeftArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, true); stc.scrollMovement.continueSlideMovableContainerLeft(); }; p.handleMousedownOnSlideMovContainerRightArrow = function () { var evh = this, stc = evh.stc; stc.$slideRightArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, true); stc.scrollMovement.continueSlideMovableContainerRight(); }; p.handleMouseupOnSlideMovContainerLeftArrow = function () { var evh = this, stc = evh.stc; stc.$slideLeftArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, false); }; p.handleMouseupOnSlideMovContainerRightArrow = function () { var evh = this, stc = evh.stc; stc.$slideRightArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, false); }; p.handleWindowResize = function () { var evh = this, stc = evh.stc, newWinWidth = stc.$win.width(); if (newWinWidth === stc.winWidth) { return false; } stc.winWidth = newWinWidth; stc.elementsHandler.refreshAllElementSizes(); }; }(EventHandlers.prototype)); /* *********************************************************************************** * ScrollMovement - Class that each instance of ScrollingTabsControl will instantiate * **********************************************************************************/ function ScrollMovement(scrollingTabsControl) { var smv = this; smv.stc = scrollingTabsControl; } // prototype methods (function (p) { p.continueSlideMovableContainerLeft = function () { var smv = this, stc = smv.stc; setTimeout(function() { if (stc.movableContainerLeftPos <= smv.getMinPos() || !stc.$slideLeftArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN)) { return; } if (!smv.incrementMovableContainerLeft()) { // haven't reached max left smv.continueSlideMovableContainerLeft(); } }, CONSTANTS.CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL); }; p.continueSlideMovableContainerRight = function () { var smv = this, stc = smv.stc; setTimeout(function() { if (stc.movableContainerLeftPos >= 0 || !stc.$slideRightArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN)) { return; } if (!smv.incrementMovableContainerRight()) { // haven't reached max right smv.continueSlideMovableContainerRight(); } }, CONSTANTS.CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL); }; p.decrementMovableContainerLeftPos = function (minPos) { var smv = this, stc = smv.stc; stc.movableContainerLeftPos -= (stc.fixedContainerWidth / CONSTANTS.SCROLL_OFFSET_FRACTION); if (stc.movableContainerLeftPos < minPos) { stc.movableContainerLeftPos = minPos; } else if (stc.scrollToTabEdge) { smv.setMovableContainerLeftPosToTabEdge(CONSTANTS.SLIDE_DIRECTION.LEFT); if (stc.movableContainerLeftPos < minPos) { stc.movableContainerLeftPos = minPos; } } }; p.disableSlideLeftArrow = function () { var smv = this, stc = smv.stc; if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { return; } stc.$slideLeftArrow.addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); }; p.disableSlideRightArrow = function () { var smv = this, stc = smv.stc; if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { return; } stc.$slideRightArrow.addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); }; p.enableSlideLeftArrow = function () { var smv = this, stc = smv.stc; if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { return; } stc.$slideLeftArrow.removeClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); }; p.enableSlideRightArrow = function () { var smv = this, stc = smv.stc; if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { return; } stc.$slideRightArrow.removeClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); }; p.getMinPos = function () { var smv = this, stc = smv.stc; return stc.scrollArrowsVisible ? (stc.fixedContainerWidth - stc.movableContainerWidth - stc.scrollArrowsCombinedWidth) : 0; }; p.getMovableContainerCssLeftVal = function () { var smv = this, stc = smv.stc; return (stc.movableContainerLeftPos === 0) ? '0' : stc.movableContainerLeftPos + 'px'; }; p.incrementMovableContainerLeft = function () { var smv = this, stc = smv.stc, minPos = smv.getMinPos(); smv.decrementMovableContainerLeftPos(minPos); smv.slideMovableContainerToLeftPos(); smv.enableSlideRightArrow(); // return true if we're fully left, false otherwise return (stc.movableContainerLeftPos === minPos); }; p.incrementMovableContainerRight = function (minPos) { var smv = this, stc = smv.stc; // if minPos passed in, the movable container was beyond the minPos if (minPos) { stc.movableContainerLeftPos = minPos; } else { stc.movableContainerLeftPos += (stc.fixedContainerWidth / CONSTANTS.SCROLL_OFFSET_FRACTION); if (stc.movableContainerLeftPos > 0) { stc.movableContainerLeftPos = 0; } else if (stc.scrollToTabEdge) { smv.setMovableContainerLeftPosToTabEdge(CONSTANTS.SLIDE_DIRECTION.RIGHT); } } smv.slideMovableContainerToLeftPos(); smv.enableSlideLeftArrow(); // return true if we're fully right, false otherwise // left pos of 0 is the movable container's max position (farthest right) return (stc.movableContainerLeftPos === 0); }; p.refreshScrollArrowsDisabledState = function() { var smv = this, stc = smv.stc; if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { return; } if (stc.movableContainerLeftPos >= 0) { // movable container fully right smv.disableSlideRightArrow(); smv.enableSlideLeftArrow(); return; } if (stc.movableContainerLeftPos <= smv.getMinPos()) { // fully left smv.disableSlideLeftArrow(); smv.enableSlideRightArrow(); return; } smv.enableSlideLeftArrow(); smv.enableSlideRightArrow(); }; p.scrollToActiveTab = function () { var smv = this, stc = smv.stc, $activeTab, $activeTabAnchor, activeTabLeftPos, activeTabRightPos, rightArrowLeftPos, activeTabWidth, leftPosOffset, offsetToMiddle, leftScrollArrowWidth, rightScrollArrowWidth; if (!stc.scrollArrowsVisible) { return; } if (stc.usingBootstrap4) { $activeTabAnchor = stc.$tabsUl.find('li > .nav-link.active'); if ($activeTabAnchor.length) { $activeTab = $activeTabAnchor.parent(); } } else { $activeTab = stc.$tabsUl.find('li.active'); } if (!$activeTab || !$activeTab.length) { return; } rightScrollArrowWidth = stc.$slideRightArrow.outerWidth(); activeTabWidth = $activeTab.outerWidth(); /** * @author poletaew * We need relative offset (depends on $fixedContainer), don't absolute */ activeTabLeftPos = $activeTab.offset().left - stc.$fixedContainer.offset().left; activeTabRightPos = activeTabLeftPos + activeTabWidth; rightArrowLeftPos = stc.fixedContainerWidth - rightScrollArrowWidth; if (stc.rtl) { leftScrollArrowWidth = stc.$slideLeftArrow.outerWidth(); if (activeTabLeftPos < 0) { // active tab off left side stc.movableContainerLeftPos += activeTabLeftPos; smv.slideMovableContainerToLeftPos(); return true; } else { // active tab off right side if (activeTabRightPos > rightArrowLeftPos) { stc.movableContainerLeftPos += (activeTabRightPos - rightArrowLeftPos) + (2 * rightScrollArrowWidth); smv.slideMovableContainerToLeftPos(); return true; } } } else { if (activeTabRightPos > rightArrowLeftPos) { // active tab off right side leftPosOffset = activeTabRightPos - rightArrowLeftPos + rightScrollArrowWidth; offsetToMiddle = stc.fixedContainerWidth / 2; leftPosOffset += offsetToMiddle - (activeTabWidth / 2); stc.movableContainerLeftPos -= leftPosOffset; smv.slideMovableContainerToLeftPos(); return true; } else { leftScrollArrowWidth = stc.$slideLeftArrow.outerWidth(); if (activeTabLeftPos < 0) { // active tab off left side offsetToMiddle = stc.fixedContainerWidth / 2; stc.movableContainerLeftPos += (-activeTabLeftPos) + offsetToMiddle - (activeTabWidth / 2); smv.slideMovableContainerToLeftPos(); return true; } } } return false; }; p.setMovableContainerLeftPosToTabEdge = function (slideDirection) { var smv = this, stc = smv.stc, offscreenWidth = -stc.movableContainerLeftPos, totalTabWidth = 0; // make sure LeftPos is set so that a tab edge will be against the // left scroll arrow so we won't have a partial, cut-off tab stc.$tabsLiCollection.each(function () { var tabWidth = $(this).width(); totalTabWidth += tabWidth; if (totalTabWidth > offscreenWidth) { stc.movableContainerLeftPos = (slideDirection === CONSTANTS.SLIDE_DIRECTION.RIGHT) ? -(totalTabWidth - tabWidth) : -totalTabWidth; return false; // exit .each() loop } }); }; p.slideMovableContainerToLeftPos = function () { var smv = this, stc = smv.stc, minPos = smv.getMinPos(), leftOrRightVal; if (stc.movableContainerLeftPos > 0) { stc.movableContainerLeftPos = 0; } else if (stc.movableContainerLeftPos < minPos) { stc.movableContainerLeftPos = minPos; } stc.movableContainerLeftPos = stc.movableContainerLeftPos / 1; leftOrRightVal = smv.getMovableContainerCssLeftVal(); smv.performingSlideAnim = true; var targetPos = stc.rtl ? { right: leftOrRightVal } : { left: leftOrRightVal }; stc.$movableContainer.stop().animate(targetPos, 'slow', function __slideAnimComplete() { var newMinPos = smv.getMinPos(); smv.performingSlideAnim = false; // if we slid past the min pos--which can happen if you resize the window // quickly--move back into position if (stc.movableContainerLeftPos < newMinPos) { smv.decrementMovableContainerLeftPos(newMinPos); targetPos = stc.rtl ? { right: smv.getMovableContainerCssLeftVal() } : { left: smv.getMovableContainerCssLeftVal() }; stc.$movableContainer.stop().animate(targetPos, 'fast', function() { smv.refreshScrollArrowsDisabledState(); }); } else { smv.refreshScrollArrowsDisabledState(); } }); }; }(ScrollMovement.prototype)); /* ********************************************************************** * ScrollingTabsControl - Class that each directive will instantiate * **********************************************************************/ function ScrollingTabsControl($tabsContainer) { var stc = this; stc.$tabsContainer = $tabsContainer; stc.instanceId = $.fn.scrollingTabs.nextInstanceId++; stc.movableContainerLeftPos = 0; stc.scrollArrowsVisible = false; stc.scrollToTabEdge = false; stc.disableScrollArrowsOnFullyScrolled = false; stc.reverseScroll = false; stc.widthMultiplier = 1; stc.scrollMovement = new ScrollMovement(stc); stc.eventHandlers = new EventHandlers(stc); stc.elementsHandler = new ElementsHandler(stc); } // prototype methods (function (p) { p.initTabs = function (options, $scroller, readyCallback, attachTabContentToDomCallback) { var stc = this, elementsHandler = stc.elementsHandler, num; if (options.enableRtlSupport && $('html').attr('dir') === 'rtl') { stc.rtl = true; } if (options.scrollToTabEdge) { stc.scrollToTabEdge = true; } if (options.disableScrollArrowsOnFullyScrolled) { stc.disableScrollArrowsOnFullyScrolled = true; } if (options.reverseScroll) { stc.reverseScroll = true; } if (options.widthMultiplier !== 1) { num = Number(options.widthMultiplier); // handle string value if (!isNaN(num)) { stc.widthMultiplier = num; } } if (options.bootstrapVersion.toString().charAt(0) === '4') { stc.usingBootstrap4 = true; } setTimeout(initTabsAfterTimeout, 100); function initTabsAfterTimeout() { var actionsTaken; // if we're just wrapping non-data-driven tabs, the user might // have the .nav-tabs hidden to prevent the clunky flash of // multi-line tabs on page refresh, so we need to make sure // they're visible before trying to wrap them $scroller.find('.nav-tabs').show(); elementsHandler.initElements(options); actionsTaken = elementsHandler.refreshAllElementSizes(); $scroller.css('visibility', 'visible'); if (attachTabContentToDomCallback) { attachTabContentToDomCallback(); } if (readyCallback) { readyCallback(); } } }; p.scrollToActiveTab = function(options) { var stc = this, smv = stc.scrollMovement; smv.scrollToActiveTab(options); }; }(ScrollingTabsControl.prototype)); /* exported buildNavTabsAndTabContentForTargetElementInstance */ var tabElements = (function () { return { getElTabPaneForLi: getElTabPaneForLi, getNewElNavTabs: getNewElNavTabs, getNewElScrollerElementWrappingNavTabsInstance: getNewElScrollerElementWrappingNavTabsInstance, getNewElTabAnchor: getNewElTabAnchor, getNewElTabContent: getNewElTabContent, getNewElTabLi: getNewElTabLi, getNewElTabPane: getNewElTabPane }; /////////////////// // ---- retrieve existing elements from the DOM ---------- function getElTabPaneForLi($li) { return $($li.find('a').attr('href')); } // ---- create new elements ---------- function getNewElNavTabs() { return $('<ul class="nav nav-tabs" role="tablist"></ul>'); } function getNewElScrollerElementWrappingNavTabsInstance($navTabsInstance, settings) { var $tabsContainer = $('<div class="scrtabs-tab-container"></div>'), leftArrowContent = settings.leftArrowContent || '<div class="scrtabs-tab-scroll-arrow scrtabs-tab-scroll-arrow-left"><span class="' + settings.cssClassLeftArrow + '"></span></div>', $leftArrow = $(leftArrowContent), rightArrowContent = settings.rightArrowContent || '<div class="scrtabs-tab-scroll-arrow scrtabs-tab-scroll-arrow-right"><span class="' + settings.cssClassRightArrow + '"></span></div>', $rightArrow = $(rightArrowContent), $fixedContainer = $('<div class="scrtabs-tabs-fixed-container"></div>'), $movableContainer = $('<div class="scrtabs-tabs-movable-container"></div>'); if (settings.disableScrollArrowsOnFullyScrolled) { $leftArrow.add($rightArrow).addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); } return $tabsContainer .append($leftArrow, $fixedContainer.append($movableContainer.append($navTabsInstance)), $rightArrow); } function getNewElTabAnchor(tab, propNames) { return $('<a role="tab" data-toggle="tab"></a>') .attr('href', '#' + tab[propNames.paneId]) .html(tab[propNames.title]); } function getNewElTabContent() { return $('<div class="tab-content"></div>'); } function getNewElTabLi(tab, propNames, options) { var liContent = options.tabLiContent || '<li role="presentation" class=""></li>', $li = $(liContent), $a = getNewElTabAnchor(tab, propNames).appendTo($li); if (tab[propNames.disabled]) { $li.addClass('disabled'); $a.attr('data-toggle', ''); } else if (options.forceActiveTab && tab[propNames.active]) { $li.addClass('active'); } if (options.tabPostProcessor) { options.tabPostProcessor($li, $a); } return $li; } function getNewElTabPane(tab, propNames, options) { var $pane = $('<div role="tabpanel" class="tab-pane"></div>') .attr('id', tab[propNames.paneId]) .html(tab[propNames.content]); if (options.forceActiveTab && tab[propNames.active]) { $pane.addClass('active'); } return $pane; } }()); // tabElements var tabUtils = (function () { return { didTabOrderChange: didTabOrderChange, getIndexOfClosestEnabledTab: getIndexOfClosestEnabledTab, getTabIndexByPaneId: getTabIndexByPaneId, storeDataOnLiEl: storeDataOnLiEl }; /////////////////// function didTabOrderChange($currTabLis, updatedTabs, propNames) { var isTabOrderChanged = false; $currTabLis.each(function (currDomIdx) { var newIdx = getTabIndexByPaneId(updatedTabs, propNames.paneId, $(this).data('tab')[propNames.paneId]); if ((newIdx > -1) && (newIdx !== currDomIdx)) { // tab moved isTabOrderChanged = true; return false; // exit .each() loop } }); return isTabOrderChanged; } function getIndexOfClosestEnabledTab($currTabLis, startIndex) { var lastIndex = $currTabLis.length - 1, closestIdx = -1, incrementFromStartIndex = 0, testIdx = 0; // expand out from the current tab looking for an enabled tab; // we prefer the tab after us over the tab before while ((closestIdx === -1) && (testIdx >= 0)) { if ( (((testIdx = startIndex + (++incrementFromStartIndex)) <= lastIndex) && !$currTabLis.eq(testIdx).hasClass('disabled')) || (((testIdx = startIndex - incrementFromStartIndex) >= 0) && !$currTabLis.eq(testIdx).hasClass('disabled')) ) { closestIdx = testIdx; } } return closestIdx; } function getTabIndexByPaneId(tabs, paneIdPropName, paneId) { var idx = -1; tabs.some(function (tab, i) { if (tab[paneIdPropName] === paneId) {