You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1963 lines
71 KiB

/**
* jquery-bootstrap-scrolling-tabs
* @version v2.4.0
* @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.
* 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',
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);
}
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.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) {
idx = i;
return true; // exit loop
}
});
return idx;
}
function storeDataOnLiEl($li, tabs, index) {
$li.data({
tab: $.extend({}, tabs[index]), // store a clone so we can check for changes
index: index
});
}
}()); // tabUtils
function buildNavTabsAndTabContentForTargetElementInstance($targetElInstance, settings, readyCallback) {
var tabs = settings.tabs,
propNames = {
paneId: settings.propPaneId,
title: settings.propTitle,
active: settings.propActive,
disabled: settings.propDisabled,
content: settings.propContent
},
ignoreTabPanes = settings.ignoreTabPanes,
hasTabContent = tabs.length && tabs[0][propNames.content] !== undefined,
$navTabs = tabElements.getNewElNavTabs(),
$tabContent = tabElements.getNewElTabContent(),
$scroller,
attachTabContentToDomCallback = ignoreTabPanes ? null : function() {
$scroller.after($tabContent);
};
if (!tabs.length) {
return;
}
tabs.forEach(function(tab, index) {
var options = {
forceActiveTab: true,
tabLiContent: settings.tabsLiContent && settings.tabsLiContent[index],
tabPostProcessor: settings.tabsPostProcessors && settings.tabsPostProcessors[index]
};
tabElements
.getNewElTabLi(tab, propNames, options)
.appendTo($navTabs);
// build the tab panes if we weren't told to ignore them and there's
// tab content data available
if (!ignoreTabPanes && hasTabContent) {
tabElements
.getNewElTabPane(tab, propNames, options)
.appendTo($tabContent);
}
});
$scroller = wrapNavTabsInstanceInScroller($navTabs,
settings,
readyCallback,
attachTabContentToDomCallback);
$scroller.appendTo($targetElInstance);
$targetElInstance.data({
scrtabs: {
tabs: tabs,
propNames: propNames,
ignoreTabPanes: ignoreTabPanes,
hasTabContent: hasTabContent,
tabsLiContent: settings.tabsLiContent,
tabsPostProcessors: settings.tabsPostProcessors,
scroller: $scroller
}
});
// once the nav-tabs are wrapped in the scroller, attach each tab's
// data to it for reference later; we need to wait till they're
// wrapped in the scroller because we wrap a *clone* of the nav-tabs
// we built above, not the original nav-tabs
$scroller.find('.nav-tabs > li').each(function (index) {
tabUtils.storeDataOnLiEl($(this), tabs, index);
});
return $targetElInstance;
}
function wrapNavTabsInstanceInScroller($navTabsInstance, settings, readyCallback, attachTabContentToDomCallback) {
var $scroller = tabElements.getNewElScrollerElementWrappingNavTabsInstance($navTabsInstance.clone(true), settings), // use clone because we replaceWith later
scrollingTabsControl = new ScrollingTabsControl($scroller),
navTabsInstanceData = $navTabsInstance.data('scrtabs');
if (!navTabsInstanceData) {
$navTabsInstance.data('scrtabs', {
scroller: $scroller
});
} else {
navTabsInstanceData.scroller = $scroller;
}
$navTabsInstance.replaceWith($scroller.css('visibility', 'hidden'));
if (settings.tabClickHandler && (typeof settings.tabClickHandler === 'function')) {
$scroller.hasTabClickHandler = true;
scrollingTabsControl.tabClickHandler = settings.tabClickHandler;
}
$scroller.initTabs = function () {
scrollingTabsControl.initTabs(settings,
$scroller,
readyCallback,
attachTabContentToDomCallback);
};
$scroller.scrollToActiveTab = function() {
scrollingTabsControl.scrollToActiveTab(settings);
};
$scroller.initTabs();
listenForDropdownMenuTabs($scroller, scrollingTabsControl);
return $scroller;
}
/* exported listenForDropdownMenuTabs,
refreshTargetElementInstance,
scrollToActiveTab */
function checkForTabAdded(refreshData) {
var updatedTabsArray = refreshData.updatedTabsArray,
updatedTabsLiContent = refreshData.updatedTabsLiContent || [],
updatedTabsPostProcessors = refreshData.updatedTabsPostProcessors || [],
propNames = refreshData.propNames,
ignoreTabPanes = refreshData.ignoreTabPanes,
options = refreshData.options,
$currTabLis = refreshData.$currTabLis,
$navTabs = refreshData.$navTabs,
$currTabContentPanesContainer = ignoreTabPanes ? null : refreshData.$currTabContentPanesContainer,
$currTabContentPanes = ignoreTabPanes ? null : refreshData.$currTabContentPanes,
isInitTabsRequired = false;
// make sure each tab in the updated tabs array has a corresponding DOM element
updatedTabsArray.forEach(function (tab, idx) {
var $li = $currTabLis.find('a[href="#' + tab[propNames.paneId] + '"]'),
isTabIdxPastCurrTabs = (idx >= $currTabLis.length),
$pane;
if (!$li.length) { // new tab
isInitTabsRequired = true;
// add the tab, add its pane (if necessary), and refresh the scroller
options.tabLiContent = updatedTabsLiContent[idx];
options.tabPostProcessor = updatedTabsPostProcessors[idx];
$li = tabElements.getNewElTabLi(tab, propNames, options);
tabUtils.storeDataOnLiEl($li, updatedTabsArray, idx);
if (isTabIdxPastCurrTabs) { // append to end of current tabs
$li.appendTo($navTabs);
} else { // insert in middle of current tabs
$li.insertBefore($currTabLis.eq(idx));
}
if (!ignoreTabPanes && tab[propNames.content] !== undefined) {
$pane = tabElements.getNewElTabPane(tab, propNames, options);
if (isTabIdxPastCurrTabs) { // append to end of current tabs
$pane.appendTo($currTabContentPanesContainer);
} else { // insert in middle of current tabs
$pane.insertBefore($currTabContentPanes.eq(idx));
}
}
}
});
return isInitTabsRequired;
}
function checkForTabPropertiesUpdated(refreshData) {
var tabLiData = refreshData.tabLi,
ignoreTabPanes = refreshData.ignoreTabPanes,
$li = tabLiData.$li,
$contentPane = tabLiData.$contentPane,
origTabData = tabLiData.origTabData,
newTabData = tabLiData.newTabData,
propNames = refreshData.propNames,
isInitTabsRequired = false;
// update tab title if necessary
if (origTabData[propNames.title] !== newTabData[propNames.title]) {
$li.find('a[role="tab"]')
.html(origTabData[propNames.title] = newTabData[propNames.title]);
isInitTabsRequired = true;
}
// update tab disabled state if necessary
if (origTabData[propNames.disabled] !== newTabData[propNames.disabled]) {
if (newTabData[propNames.disabled]) { // enabled -> disabled
$li.addClass('disabled');
$li.find('a[role="tab"]').attr('data-toggle', '');
} else { // disabled -> enabled
$li.removeClass('disabled');
$li.find('a[role="tab"]').attr('data-toggle', 'tab');
}
origTabData[propNames.disabled] = newTabData[propNames.disabled];
isInitTabsRequired = true;
}
// update tab active state if necessary
if (refreshData.options.forceActiveTab) {
// set the active tab based on the tabs array regardless of the current
// DOM state, which could have been changed by the user clicking a tab
// without those changes being reflected back to the tab data
$li[newTabData[propNames.active] ? 'addClass' : 'removeClass']('active');
$contentPane[newTabData[propNames.active] ? 'addClass' : 'removeClass']('active');
origTabData[propNames.active] = newTabData[propNames.active];
isInitTabsRequired = true;
}
// update tab content pane if necessary
if (!ignoreTabPanes && origTabData[propNames.content] !== newTabData[propNames.content]) {
$contentPane.html(origTabData[propNames.content] = newTabData[propNames.content]);
isInitTabsRequired = true;
}
return isInitTabsRequired;
}
function checkForTabRemoved(refreshData) {
var tabLiData = refreshData.tabLi,
ignoreTabPanes = refreshData.ignoreTabPanes,
$li = tabLiData.$li,
idxToMakeActive;
if (tabLiData.newIdx !== -1) { // tab was not removed--it has a valid index
return false;
}
// if this was the active tab, make the closest enabled tab active
if ($li.hasClass('active')) {
idxToMakeActive = tabUtils.getIndexOfClosestEnabledTab(refreshData.$currTabLis, tabLiData.currDomIdx);
if (idxToMakeActive > -1) {
refreshData.$currTabLis
.eq(idxToMakeActive)
.addClass('active');
if (!ignoreTabPanes) {
refreshData.$currTabContentPanes
.eq(idxToMakeActive)
.addClass('active');
}
}
}
$li.remove();
if (!ignoreTabPanes) {
tabLiData.$contentPane.remove();
}
return true;
}
function checkForTabsOrderChanged(refreshData) {
var $currTabLis = refreshData.$currTabLis,
updatedTabsArray = refreshData.updatedTabsArray,
propNames = refreshData.propNames,
ignoreTabPanes = refreshData.ignoreTabPanes,
newTabsCollection = [],
newTabPanesCollection = ignoreTabPanes ? null : [];
if (!tabUtils.didTabOrderChange($currTabLis, updatedTabsArray, propNames)) {
return false;
}
// the tab order changed...
updatedTabsArray.forEach(function (t) {
var paneId = t[propNames.paneId];
newTabsCollection.push(
$currTabLis
.find('a[role="tab"][href="#' + paneId + '"]')
.parent('li')
);
if (!ignoreTabPanes) {
newTabPanesCollection.push($('#' + paneId));
}
});
refreshData.$navTabs.append(newTabsCollection);
if (!ignoreTabPanes) {
refreshData.$currTabContentPanesContainer.append(newTabPanesCollection);
}
return true;
}
function checkForTabsRemovedOrUpdated(refreshData) {
var $currTabLis = refreshData.$currTabLis,
updatedTabsArray = refreshData.updatedTabsArray,
propNames = refreshData.propNames,
isInitTabsRequired = false;
$currTabLis.each(function (currDomIdx) {
var $li = $(this),
origTabData = $li.data('tab'),
newIdx = tabUtils.getTabIndexByPaneId(updatedTabsArray, propNames.paneId, origTabData[propNames.paneId]),
newTabData = (newIdx > -1) ? updatedTabsArray[newIdx] : null;
refreshData.tabLi = {
$li: $li,
currDomIdx: currDomIdx,
newIdx: newIdx,
$contentPane: tabElements.getElTabPaneForLi($li),
origTabData: origTabData,
newTabData: newTabData
};
if (checkForTabRemoved(refreshData)) {
isInitTabsRequired = true;
return; // continue to next $li in .each() since we removed this tab
}
if (checkForTabPropertiesUpdated(refreshData)) {
isInitTabsRequired = true;
}
});
return isInitTabsRequired;
}
function listenForDropdownMenuTabs($scroller, stc) {
var $ddMenu;
// for dropdown menus to show, we need to move them out of the
// scroller and append them to the body
$scroller
.on(CONSTANTS.EVENTS.DROPDOWN_MENU_SHOW, handleDropdownShow)
.on(CONSTANTS.EVENTS.DROPDOWN_MENU_HIDE, handleDropdownHide);
function handleDropdownHide(e) {
// move the dropdown menu back into its tab
$(e.target).append($ddMenu.off(CONSTANTS.EVENTS.CLICK));
}
function handleDropdownShow(e) {
var $ddParentTabLi = $(e.target),
ddLiOffset = $ddParentTabLi.offset(),
$currActiveTab = $scroller.find('li[role="presentation"].active'),
ddMenuRightX,
tabsContainerMaxX,
ddMenuTargetLeft;
$ddMenu = $ddParentTabLi
.find('.dropdown-menu')
.attr('data-' + CONSTANTS.DATA_KEY_DDMENU_MODIFIED, true);
// if the dropdown's parent tab li isn't already active,
// we need to deactivate any active menu item in the dropdown
if ($currActiveTab[0] !== $ddParentTabLi[0]) {
$ddMenu.find('li.active').removeClass('active');
}
// we need to do our own click handling because the built-in
// bootstrap handlers won't work since we moved the dropdown
// menu outside the tabs container
$ddMenu.on(CONSTANTS.EVENTS.CLICK, 'a[role="tab"]', handleClickOnDropdownMenuItem);
$('body').append($ddMenu);
// make sure the menu doesn't go off the right side of the page
ddMenuRightX = $ddMenu.width() + ddLiOffset.left;
tabsContainerMaxX = $scroller.width() - (stc.$slideRightArrow.outerWidth() + 1);
ddMenuTargetLeft = ddLiOffset.left;
if (ddMenuRightX > tabsContainerMaxX) {
ddMenuTargetLeft -= (ddMenuRightX - tabsContainerMaxX);
}
$ddMenu.css({
'display': 'block',
'top': ddLiOffset.top + $ddParentTabLi.outerHeight() - 2,
'left': ddMenuTargetLeft
});
function handleClickOnDropdownMenuItem() {
/* jshint validthis: true */
var $selectedMenuItemAnc = $(this),
$selectedMenuItemLi = $selectedMenuItemAnc.parent('li'),
$selectedMenuItemDropdownMenu = $selectedMenuItemLi.parent('.dropdown-menu'),
targetPaneId = $selectedMenuItemAnc.attr('href');
if ($selectedMenuItemLi.hasClass('active')) {
return;
}
// once we select a menu item from the dropdown, deactivate
// the current tab (unless it's our parent tab), deactivate
// any active dropdown menu item, make our parent tab active
// (if it's not already), and activate the selected menu item
$scroller
.find('li.active')
.not($ddParentTabLi)
.add($selectedMenuItemDropdownMenu.find('li.active'))
.removeClass('active');
$ddParentTabLi
.add($selectedMenuItemLi)
.addClass('active');
// manually deactivate current active pane and activate our pane
$('.tab-content .tab-pane.active').removeClass('active');
$(targetPaneId).addClass('active');
}
}
}
function refreshDataDrivenTabs($container, options) {
var instanceData = $container.data().scrtabs,
scroller = instanceData.scroller,
$navTabs = $container.find('.scrtabs-tab-container .nav-tabs'),
$currTabContentPanesContainer = $container.find('.tab-content'),
isInitTabsRequired = false,
refreshData = {
options: options,
updatedTabsArray: instanceData.tabs,
updatedTabsLiContent: instanceData.tabsLiContent,
updatedTabsPostProcessors: instanceData.tabsPostProcessors,
propNames: instanceData.propNames,
ignoreTabPanes: instanceData.ignoreTabPanes,
$navTabs: $navTabs,
$currTabLis: $navTabs.find('> li'),
$currTabContentPanesContainer: $currTabContentPanesContainer,
$currTabContentPanes: $currTabContentPanesContainer.find('.tab-pane')
};
// to preserve the tab positions if we're just adding or removing
// a tab, don't completely rebuild the tab structure, but check
// for differences between the new tabs array and the old
if (checkForTabAdded(refreshData)) {
isInitTabsRequired = true;
}
if (checkForTabsOrderChanged(refreshData)) {
isInitTabsRequired = true;
}
if (checkForTabsRemovedOrUpdated(refreshData)) {
isInitTabsRequired = true;
}
if (isInitTabsRequired) {
scroller.initTabs();
}
return isInitTabsRequired;
}
function refreshTargetElementInstance($container, options) {
if (!$container.data('scrtabs')) { // target element doesn't have plugin on it
return;
}
// force a refresh if the tabs are static html or they're data-driven
// but the data didn't change so we didn't call initTabs()
if ($container.data('scrtabs').isWrapperOnly || !refreshDataDrivenTabs($container, options)) {
$('body').trigger(CONSTANTS.EVENTS.FORCE_REFRESH);
}
}
function scrollToActiveTab() {
/* jshint validthis: true */
var $targetElInstance = $(this),
scrtabsData = $targetElInstance.data('scrtabs');
if (!scrtabsData) {
return;
}
scrtabsData.scroller.scrollToActiveTab();
}
var methods = {
destroy: function() {
var $targetEls = this;
return $targetEls.each(destroyPlugin);
},
init: function(options) {
var $targetEls = this,
targetElsLastIndex = $targetEls.length - 1,
settings = $.extend({}, $.fn.scrollingTabs.defaults, options || {});
// ---- tabs NOT data-driven -------------------------
if (!settings.tabs) {
// just wrap the selected .nav-tabs element(s) in the scroller
return $targetEls.each(function(index) {
var dataObj = {
isWrapperOnly: true
},
$targetEl = $(this).data({ scrtabs: dataObj }),
readyCallback = (index < targetElsLastIndex) ? null : function() {
$targetEls.trigger(CONSTANTS.EVENTS.TABS_READY);
};
wrapNavTabsInstanceInScroller($targetEl, settings, readyCallback);
});
}
// ---- tabs data-driven -------------------------
return $targetEls.each(function (index) {
var $targetEl = $(this),
readyCallback = (index < targetElsLastIndex) ? null : function() {
$targetEls.trigger(CONSTANTS.EVENTS.TABS_READY);
};
buildNavTabsAndTabContentForTargetElementInstance($targetEl, settings, readyCallback);
});
},
refresh: function(options) {
var $targetEls = this,
settings = $.extend({}, $.fn.scrollingTabs.defaults, options || {});
return $targetEls.each(function () {
refreshTargetElementInstance($(this), settings);
});
},
scrollToActiveTab: function() {
return this.each(scrollToActiveTab);
}
};
function destroyPlugin() {
/* jshint validthis: true */
var $targetElInstance = $(this),
scrtabsData = $targetElInstance.data('scrtabs'),
$tabsContainer;
if (!scrtabsData) {
return;
}
if (scrtabsData.enableSwipingElement === 'self') {
$targetElInstance.removeClass(CONSTANTS.CSS_CLASSES.ALLOW_SCROLLBAR);
} else if (scrtabsData.enableSwipingElement === 'parent') {
$targetElInstance.closest('.scrtabs-tab-container').parent().removeClass(CONSTANTS.CSS_CLASSES.ALLOW_SCROLLBAR);
}
scrtabsData.scroller
.off(CONSTANTS.EVENTS.DROPDOWN_MENU_SHOW)
.off(CONSTANTS.EVENTS.DROPDOWN_MENU_HIDE);
// if there were any dropdown menus opened, remove the css we added to
// them so they would display correctly
scrtabsData.scroller
.find('[data-' + CONSTANTS.DATA_KEY_DDMENU_MODIFIED + ']')
.css({
display: '',
left: '',
top: ''
})
.off(CONSTANTS.EVENTS.CLICK)
.removeAttr('data-' + CONSTANTS.DATA_KEY_DDMENU_MODIFIED);
if (scrtabsData.scroller.hasTabClickHandler) {
$targetElInstance
.find('a[data-toggle="tab"]')
.off('.scrtabs');
}
if (scrtabsData.isWrapperOnly) { // we just wrapped nav-tabs markup, so restore it
// $targetElInstance is the ul.nav-tabs
$tabsContainer = $targetElInstance.parents('.scrtabs-tab-container');
if ($tabsContainer.length) {
$tabsContainer.replaceWith($targetElInstance);
}
} else { // we generated the tabs from data so destroy everything we created
if (scrtabsData.scroller && scrtabsData.scroller.initTabs) {
scrtabsData.scroller.initTabs = null;
}
// $targetElInstance is the container for the ul.nav-tabs we generated
$targetElInstance
.find('.scrtabs-tab-container')
.add('.tab-content')
.remove();
}
$targetElInstance.removeData('scrtabs');
while(--$.fn.scrollingTabs.nextInstanceId >= 0) {
$(window).off(CONSTANTS.EVENTS.WINDOW_RESIZE + $.fn.scrollingTabs.nextInstanceId);
}
$('body').off(CONSTANTS.EVENTS.FORCE_REFRESH);
}
$.fn.scrollingTabs = function(methodOrOptions) {
if (methods[methodOrOptions]) {
return methods[methodOrOptions].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (!methodOrOptions || (typeof methodOrOptions === 'object')) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + methodOrOptions + ' does not exist on $.scrollingTabs.');
}
};
$.fn.scrollingTabs.nextInstanceId = 0;
$.fn.scrollingTabs.defaults = {
tabs: null,
propPaneId: 'paneId',
propTitle: 'title',
propActive: 'active',
propDisabled: 'disabled',
propContent: 'content',
ignoreTabPanes: false,
scrollToTabEdge: false,
disableScrollArrowsOnFullyScrolled: false,
forceActiveTab: false,
reverseScroll: false,
widthMultiplier: 1,
tabClickHandler: null,
cssClassLeftArrow: 'fa fa-chevron-left',
cssClassRightArrow: 'fa fa-chevron-right',
leftArrowContent: '',
rightArrowContent: '',
tabsLiContent: null,
tabsPostProcessors: null,
enableSwiping: false,
enableRtlSupport: false,
bootstrapVersion: 4
};
}(jQuery, window));