├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── bower.json ├── dist ├── jquery.scrolling-tabs.css ├── jquery.scrolling-tabs.js ├── jquery.scrolling-tabs.min.css └── jquery.scrolling-tabs.min.js ├── gulpfile.js ├── package.json ├── run ├── data-driven-mult.html ├── data-driven-rtl.html ├── data-driven.html ├── index.html ├── markup-only-mult.html ├── markup-only-rtl.html ├── markup-only-svg.html ├── markup-only-with-click-target.html └── markup-only.html ├── src ├── js │ ├── _main.js │ ├── api.js │ ├── buildTabs.js │ ├── constants.js │ ├── elementsHandler.js │ ├── eventHandlers.js │ ├── header.js │ ├── scrollMovement.js │ ├── scrollingTabsControl.js │ ├── smartresize.js │ ├── tabListeners.js │ └── usage.js └── scss │ └── jquery.scrolling-tabs.scss └── st-screenshot1.png /.gitignore: -------------------------------------------------------------------------------- 1 | .jshintrc 2 | .project 3 | .sass-cache 4 | *.rb 5 | .DS_Store 6 | node_modules 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mike Jacobson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jquery-bootstrap-scrolling-tabs 2 | ================================ 3 | 4 | jQuery plugin for making Bootstrap 3 Tabs scroll horizontally rather than wrap. 5 | 6 | Here's what they look like: 7 | 8 | ![](https://raw.githubusercontent.com/mikejacobson/jquery-bootstrap-scrolling-tabs/master/st-screenshot1.png) 9 | 10 | 11 | And here are plunks showing them working with: 12 | 13 | * HTML-defined Tabs 14 | * Data-driven Tabs 15 | 16 | Or if you prefer CodePen: 17 | 18 | * HTML-defined Tabs 19 | * Data-driven Tabs 20 | 21 | 22 | 23 | Use Cases 24 | --------- 25 | * [Use Case #1: Wrap HTML-defined Tabs](#uc1) 26 | * [Use Case #2: Create Data-driven Tabs](#uc2) 27 | 28 | 29 | 30 | Optional Features 31 | ----------------- 32 | There are also optional features available: 33 | * [Reverse Scroll](#reverseScroll) 34 | * [Force Scroll to Tab Edge](#scrollToTabEdge) 35 | * [Disable Scroll Arrows on Fully Scrolled](#disableScrollArrowsOnFullyScrolled) 36 | * [Handle Delayed Scrollbar](#handleDelayedScrollbar) 37 | * [Width Multiplier](#widthMultiplier) 38 | * [Tab Click Handler](#tabClickHandler) 39 | * [Custom Scroll Arrow classes](#cssClassArrows) 40 | * [Custom Scroll Arrow content](#customArrowsContent) 41 | * [Custom Tab LI content](#customTabLiContent) 42 | * [Tab LI and Anchor Post-Processors](#postProcessors) 43 | * [Enable Horizontal Swiping for Touch Screens](#enableSwiping) 44 | * [Enable Right-to-Left Language Support](#enableRtlSupport) 45 | 46 | 47 | Usage 48 | ----- 49 | 1. Download it or install it using bower: `bower install jquery-bootstrap-scrolling-tabs` or npm: `npm install jquery-bootstrap-scrolling-tabs` 50 | 2. Include `dist/jquery.scrolling-tabs.min.css` (or `dist/jquery.scrolling-tabs.css`) on your page *after* Bootstrap's CSS 51 | 3. Include `dist/jquery.scrolling-tabs.min.js` (or `dist/jquery.scrolling-tabs.js`) on your page (make sure that jQuery is included before it, of course) 52 | 53 | 54 | Breaking Change for v2.0.0: Only affects swiping for touch screens 55 | -- 56 | Swiping functionality is now handled via code rather than the browser's built-in scrolling with a scrollbar due to conflicts that occurred if, on a touch screen, you performed a combination of swiping and pressing the scroll arrows. 57 | If you enabled swiping by passing in option `enableSwiping: true`, this change should not break anything, and should, in fact, fix a problem. However, if instead you enabled swiping by manually adding CSS class `scrtabs-allow-scrollbar` to a parent element of the scrolling tabs, that will no longer trigger a horizontal scrollbar for the tabs, so swiping will not be active. To enable swiping, you will need to pass in option `enableSwiping: true`. 58 | 59 | Breaking Change for v1.0.0 60 | -- 61 | * The plugin files, `jquery.scrolling-tabs.js` and `jquery.scrolling-tabs.css`, have been moved from the project root into `/dist/`. 62 | 63 | Possible Breaking Change for v1.0.0 64 | -- 65 | * The jQuery dependency has been bumped from `<3.0.0` to `<4.0.0`. 66 | 67 | Overview 68 | -------- 69 | If you're using Bootstrap Tabs (`nav-tabs`) and you don't want them to wrap if the page is too narrow to accommodate them all in one row, you can use this plugin to keep them in a row that scrolls horizontally without a scrollbar. 70 | 71 | It adjusts itself on window resize (debounced to prevent resize event wackiness), so if the window is widened enough to accommodate all tabs, scrolling will deactivate and the scroll arrows will disappear. (And, of course, vice versa if the window is narrowed.) 72 | 73 | Note: Similar to [Bootstrap tabs](https://getbootstrap.com/docs/3.3/javascript/#tabs), nested tabs are not supported. 74 | 75 | Use Cases 76 | --------- 77 | #### Use Case #1: HTML-defined Tabs 78 | 79 | If your `nav-tabs` markup looks like this: 80 | ```html 81 | 82 | 89 | 90 | 91 |
92 |
Tab 1 content...
93 |
Tab 2 content...
94 |
Tab 3 content...
95 |
Tab 4 content...
96 |
Tab 5 content...
97 |
98 | ``` 99 | 100 | you can wrap the tabs in the scroller like this: 101 | ```javascript 102 | $('.nav-tabs').scrollingTabs(); 103 | ``` 104 | 105 | 106 | ##### Bootstrap 4 Support 107 | 108 | If you're using Bootstrap 4, you need to pass in option `bootstrapVersion: 4` (the default is `3`): 109 | ```javascript 110 | $('.nav-tabs').scrollingTabs({ 111 | bootstrapVersion: 4 112 | }); 113 | ``` 114 | 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) 115 | 116 | ##### reverseScroll Option 117 | 118 | You can also pass in the `reverseScroll` option: 119 | ```javascript 120 | $('.nav-tabs').scrollingTabs({ 121 | reverseScroll: true 122 | }); 123 | ``` 124 | This will reverse the direction the tabs slide when you click a scroll arrow. More details [here](#reverseScroll). 125 | 126 | 127 | ##### scrollToTabEdge Option 128 | 129 | You can also pass in the `scrollToTabEdge` option: 130 | ```javascript 131 | $('.nav-tabs').scrollingTabs({ 132 | scrollToTabEdge: true 133 | }); 134 | ``` 135 | This will force the scrolling to always end with a tab edge aligned with the left scroll arrow. More details [here](#scrollToTabEdge). 136 | 137 | 138 | ##### disableScrollArrowsOnFullyScrolled Option 139 | 140 | You can also pass in the `disableScrollArrowsOnFullyScrolled` option: 141 | ```javascript 142 | $('.nav-tabs').scrollingTabs({ 143 | disableScrollArrowsOnFullyScrolled: true 144 | }); 145 | ``` 146 | If you're using the default scroll setting, this will cause the left scroll arrow to disable when the tabs are scrolled fully left (on page load, for example), and the right scroll arrow to disable when the tabs are scrolled fully right. 147 | 148 | If you have the `reverseScroll` option set to `true`, the opposite arrows will disable. 149 | 150 | 151 | ##### widthMultiplier Option 152 | 153 | You can also pass in the `widthMultiplier` option: 154 | ```javascript 155 | $('.nav-tabs').scrollingTabs({ 156 | widthMultiplier: 0.5 157 | }); 158 | ``` 159 | Pass in a value less than 1 if you want the tabs container to be less than the full width of its parent element. For example, if you want the tabs container to be half the width of its parent, pass in `0.5`. 160 | 161 | 162 | ##### tabClickHandler Option 163 | 164 | You can also use the `tabClickHandler` option to pass in a callback function to execute any time a tab is clicked: 165 | ```javascript 166 | $('.nav-tabs').scrollingTabs({ 167 | tabClickHandler: function (e) { 168 | var clickedTabElement = this; 169 | } 170 | }); 171 | ``` 172 | The callback function is simply passed as the event handler to jQuery's .on(), so the function will receive the jQuery event as an argument, and 'this' inside the function will be the clicked tab's anchor element. 173 | 174 | 175 | 176 | ##### Preventing Flash of Unwrapped Tabs 177 | 178 | To prevent a flash of the tabs on page load/refresh before they get wrapped 179 | inside the scroller, you can hide your `.nav-tabs` and `.tab-content` with 180 | some CSS (`display: none`). 181 | 182 | The plugin will automatically unhide the `.nav-tabs` when they're ready, 183 | and you can hook into the [`ready.scrtabs` event](#events) (which gets fired when 184 | the tabs are ready) to unhide your `.tab-content`: 185 | 186 | ```javascript 187 | $('.nav-tabs') 188 | .scrollingTabs() 189 | .on('ready.scrtabs', function() { 190 | $('.tab-content').show(); 191 | }); 192 | ``` 193 | 194 | 195 | 196 | ##### Forcing a Refresh 197 | 198 | The scrolling container should automatically refresh itself on window resize, but to manually force a refresh you can call the plugin's `refresh` method: 199 | ```javascript 200 | $('.nav-tabs').scrollingTabs('refresh'); 201 | ``` 202 | 203 | ##### Forcing a Scroll to the Active Tab 204 | 205 | On window resize, the scrolling container should automatically scroll to the active tab if it's 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 plugin's `scrollToActiveTab` method: 206 | ```javascript 207 | $('.nav-tabs').scrollingTabs('scrollToActiveTab'); 208 | ``` 209 | 210 | 211 | #### Use Case #2: Data-driven Tabs 212 | 213 | If your tabs are data-driven rather than defined in your markup, you just need to pass your tabs array to the plugin and it will generate the tab elements for you. 214 | 215 | So your tabs data should look something like this (note that the tab titles can contain HTML): 216 | ```javascript 217 | var myTabs = [ 218 | { paneId: 'tab01', title: 'Tab 1 of 12', content: 'Tab Number 1 Content', active: true, disabled: false }, 219 | { paneId: 'tab02', title: 'Tab 2 of 12', content: 'Tab Number 2 Content', active: false, disabled: false }, 220 | { paneId: 'tab03', title: 'Tab 3 of 12', content: 'Tab Number 3 Content', active: false, disabled: false }, 221 | { paneId: 'tab04', title: 'Tab 4 of 12', content: 'Tab Number 4 Content', active: false, disabled: false }, 222 | { paneId: 'tab05', title: 'Tab 5 of 12', content: 'Tab Number 5 Content', active: false, disabled: false } 223 | ]; 224 | 225 | ``` 226 | 227 | You would then need a target element to append the tabs and panes to, like this: 228 | 229 | ```html 230 | 231 |
232 | ``` 233 | 234 | 235 | Then just call the plugin on that element, passing a settings object with a `tabs` property pointing to your tabs array: 236 | 237 | ```javascript 238 | $('#tabs-inside-here').scrollingTabs({ 239 | tabs: myTabs 240 | }); 241 | 242 | ``` 243 | 244 | ##### Required Tab Data Properties 245 | 246 | Each tab object in the array must have a property for 247 | * the tab title (text or HTML) 248 | * the ID of its target pane (text) 249 | * its active state (bool) 250 | 251 | Optionally, it can also have a boolean for its `disabled` state. 252 | 253 | 254 | ##### 'Content' Property for Tab Panes 255 | 256 | If you want the plugin to generate the tab panes also, include a `content` property on the tab objects. 257 | 258 | If your tab objects have a `content` property but you *don't* want the plugin to generate the panes, pass in plugin option `ignoreTabPanes: true` when calling the plugin. 259 | 260 | ```javascript 261 | $('#tabs-inside-here').scrollingTabs({ 262 | tabs: myTabs, 263 | ignoreTabPanes: true 264 | }); 265 | 266 | ``` 267 | 268 | ##### Custom Tab Data Property Names 269 | 270 | By default, the plugin assumes those properties will be named `title`, `paneId`, `active`, `disabled`, and `content`, but if you want to use different property names, you can pass your property names in as properties on the settings object: 271 | 272 | ```javascript 273 | $('#tabs-inside-here').scrollingTabs({ 274 | tabs: myTabs, // required, 275 | propTitle: 'myTitle', // required if not 'title' 276 | propPaneId: 'myPaneId', // required if not 'paneId' 277 | propActive: 'myActive', // required if not 'active' 278 | propDisabled: 'myDisabled', // required if not 'disabled' 279 | propContent: 'myContent' // required if not 'content' 280 | }); 281 | ```javascript 282 | 283 | 284 | 285 | So, for example, if your tab objects used the property name `label` for their titles instead of `title`, you would need to pass property `propTitle: 'label'` in your settings object. 286 | 287 | ```javascript 288 | $('#tabs-inside-here').scrollingTabs({ 289 | tabs: myTabs, 290 | propTitle: 'label' 291 | }); 292 | 293 | ``` 294 | 295 | ##### scrollToTabEdge Option 296 | 297 | And just like in Use Case #1, you can also pass in the `scrollToTabEdge` option if you want to force the scrolling to always end with a tab edge aligned with the left scroll arrow: 298 | 299 | ```javascript 300 | $('#tabs-inside-here').scrollingTabs({ 301 | tabs: myTabs, 302 | scrollToTabEdge: true 303 | }); 304 | ``` 305 | 306 | You can also pass in the `disableScrollArrowsOnFullyScrolled` option if you want the scroll arrow to disable when the tabs are fully scrolled in that direction: 307 | 308 | ```javascript 309 | $('#tabs-inside-here').scrollingTabs({ 310 | tabs: myTabs, 311 | disableScrollArrowsOnFullyScrolled: true 312 | }); 313 | ``` 314 | 315 | 316 | ##### Refreshing after Tab Data Change 317 | 318 | On `tabs` data change, just call the plugin's `refresh` method to refresh the tabs on the page: 319 | ```javascript 320 | $('#tabs-inside-here').scrollingTabs('refresh'); 321 | ``` 322 | 323 | 324 | ##### forceActiveTab Option 325 | 326 | 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, pass the `forceActiveTab` flag on refresh: 327 | ```javascript 328 | $('#tabs-inside-here').scrollingTabs('refresh', { 329 | forceActiveTab: true 330 | }); 331 | ``` 332 | 333 | #### Reverse Scroll 334 | 335 | By default, on page load, if there are tabs hidden off the right side of the page, you would click the right scroll arrow to slide those tabs into view (and vice versa for the left scroll arrow, of course, if there are tabs hidden off the left side of the page). 336 | 337 | You can reverse the direction the tabs slide when an arrow is clicked by passing in option `reverseScroll: true`: 338 | 339 | ```javascript 340 | $('#tabs-inside-here').scrollingTabs({ 341 | tabs: myTabs, 342 | reverseScroll: true 343 | }); 344 | ``` 345 | 346 | This might be the more intuitive behavior for mobile devices. 347 | 348 | 349 | #### Force Scroll to Tab Edge 350 | 351 | If you want to ensure the scrolling always ends with a tab edge aligned with the left scroll arrow so there won't be a partially hidden tab, pass in option `scrollToTabEdge: true`: 352 | 353 | ```javascript 354 | $('#tabs-inside-here').scrollingTabs({ 355 | tabs: myTabs, 356 | scrollToTabEdge: true 357 | }); 358 | ``` 359 | 360 | There's no way to guarantee the left *and* right edges will be full tabs because that's dependent on the the width of the tabs and the window. So this just makes sure the left side will be a full tab. 361 | 362 | 363 | #### Disable Scroll Arrows on Fully Scrolled 364 | 365 | If you want the left scroll arrow to disable when the tabs are scrolled fully left (the way they would be on page load, for example), and the right scroll arrow to disable when the tabs are scrolled fully right, pass in option `disableScrollArrowsOnFullyScrolled: true`: 366 | 367 | ```javascript 368 | $('#tabs-inside-here').scrollingTabs({ 369 | tabs: myTabs, 370 | disableScrollArrowsOnFullyScrolled: true 371 | }); 372 | ``` 373 | 374 | Note that if you have the `reverseScroll` option set to `true`, the opposite arrows will disable. 375 | 376 | 377 | #### Handle Delayed Scrollbar 378 | 379 | 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, pass in option `handleDelayedScrollbar: true`: 380 | 381 | ```javascript 382 | $('.nav-tabs').scrollingTabs({ 383 | handleDelayedScrollbar: true 384 | }); 385 | ``` 386 | 387 | 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. 388 | 389 | It would also occur if a vertical scrollbar only appeared on selection of a particular tab that had more content than the default tab. 390 | 391 | This is not enabled by default because, since a window resize event is not triggered by a vertical scrollbar appearing, it requires adding an iframe to the page that listens for resize events because a scrollbar appearing in the parent window *does* trigger a resize event in the iframe, which then dispatches a resize event to the parent, triggering the plugin to recalculate its width. 392 | 393 | 394 | #### Width Multiplier 395 | 396 | If you want the tabs container to be less than the full width of its parent element, pass in a `widthMultiplier` value that's less than 1. For example, if you want the tabs container to be half the width of its parent, pass in option `widthMultiplier: 0.5`: 397 | 398 | ```javascript 399 | $('#tabs-inside-here').scrollingTabs({ 400 | tabs: myTabs, 401 | widthMultiplier: 0.5 402 | }); 403 | ``` 404 | 405 | #### Tab Click Handler 406 | 407 | You can pass in a callback function that executes any time a tab is clicked using the `tabClickHandler` option. 408 | The callback function is simply passed as the event handler to jQuery's .on(), so the function will receive the jQuery event as an argument, and 'this' inside the function will be the clicked tab's anchor element. 409 | 410 | ```javascript 411 | $('#tabs-inside-here').scrollingTabs({ 412 | tabs: myTabs, 413 | tabClickHandler: function (e) { 414 | var clickedTabElement = this; 415 | } 416 | }); 417 | ``` 418 | 419 | #### Custom Scroll Arrow classes 420 | 421 | You can pass in custom values for the class attributes for the left- and right scroll arrows using options `cssClassLeftArrow` and `cssClassRightArrow`. 422 | 423 | The defaults are `glyphicon glyphicon-chevron-left` and `glyphicon glyphicon-chevron-right`. 424 | 425 | 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: 426 | 427 | `.scrtabs-tab-scroll-arrow` 428 | 429 | `.scrtabs-tab-scroll-arrow-left` 430 | 431 | `.scrtabs-tab-scroll-arrow-right` 432 | 433 | 434 | ```javascript 435 | $('#tabs-inside-here').scrollingTabs({ 436 | tabs: myTabs, 437 | cssClassLeftArrow: 'fa fa-chevron-left', 438 | cssClassRightArrow: 'fa fa-chevron-right' 439 | }); 440 | ``` 441 | 442 | #### Custom Scroll Arrow content 443 | 444 | You can pass in custom values for the left- and right scroll arrow HTML using options `leftArrowContent` and `rightArrowContent`. This will override any custom `cssClassLeftArrow` and `cssClassRightArrow` settings. 445 | 446 | For example, if you wanted to use svg icons, you could set them like so: 447 | 448 | ```javascript 449 | $('#tabs-inside-here').scrollingTabs({ 450 | tabs: myTabs, 451 | leftArrowContent: [ 452 | '
', 453 | ' ', 454 | ' ', 455 | ' ', 456 | '
' 457 | ].join(''), 458 | rightArrowContent: [ 459 | '
', 460 | ' ', 461 | ' ', 462 | ' ', 463 | '
' 464 | ].join('') 465 | }); 466 | ``` 467 | 468 | 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: 469 | http://plnkr.co/edit/2MdZCAnLyeU40shxaol3?p=preview 470 | 471 | 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: 472 | 473 | ```javascript 474 | $('#tabs-inside-here').scrollingTabs({ 475 | tabs: myTabs, 476 | leftArrowContent: [ 477 | '
', 478 | ' ', 481 | '
' 482 | ].join(''), 483 | rightArrowContent: [ 484 | '
', 485 | ' ', 488 | '
' 489 | ].join('') 490 | }); 491 | ``` 492 | 493 | 494 | #### Custom Tab LI content 495 | 496 | To specify custom HTML for the tabs' LI elements, you can pass in option `tabsLiContent`. 497 | 498 | It must be a string array, each entry being an HTML string that defines the tab LI element for the corresponding tab (i.e., same index) in the `tabs` array. 499 | 500 | These entries will override the default `
  • `. 501 | 502 | 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 (although you would probably build the array dynamically using the `myTabs` data): 503 | 504 | ```javascript 505 | $('#tabs-inside-here').scrollingTabs({ 506 | tabs: myTabs, 507 | tabsLiContent: [ 508 | '', 509 | '', 510 | '' 511 | ] 512 | }); 513 | ``` 514 | 515 | This plunk demonstrates its usage (in conjunction with `tabsPostProcessors`): 516 | http://plnkr.co/edit/ugJLMk7lmDCuZQziQ0k0 517 | 518 | 519 | #### Tab LI and Anchor Post-Processors 520 | 521 | To perform additional processing on the tab LI and/or Anchor elements after they've been created, you can pass in option `tabsPostProcessors`. 522 | 523 | This is an 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. 524 | 525 | This allows you to, for example, attach a custom click handler to each anchor tag. 526 | 527 | ```javascript 528 | $('#tabs-inside-here').scrollingTabs({ 529 | tabs: myTabs, 530 | tabsPostProcessors: [ 531 | function ($li, $a) { console.log("Tab 1 clicked. $a.href: ", $a.attr('href')); }, 532 | function ($li, $a) { console.log("Tab 2 clicked. $a.href: ", $a.attr('href')); }, 533 | function ($li, $a) { console.log("Tab 3 clicked. $a.href: ", $a.attr('href')); } 534 | ] 535 | }); 536 | ``` 537 | This plunk demonstrates its usage (in conjunction with `tabsLiContent`): 538 | http://plnkr.co/edit/ugJLMk7lmDCuZQziQ0k0 539 | 540 | 541 | 542 | #### Enable Horizontal Swiping for Touch Screens 543 | 544 | To enable horizontal swiping for touch screens, pass in option `enableSwiping: true` when initializing the plugin: 545 | ```javascript 546 | $('.nav-tabs').scrollingTabs({ 547 | enableSwiping: true 548 | }); 549 | ``` 550 | This will enable swiping for any browser that supports [touch events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events). 551 | 552 | #### Enable Right-to-Left Language Support 553 | 554 | To enable support for right-to-left languages, pass in option `enableRtlSupport: true` when initializing the plugin: 555 | ```javascript 556 | $('.nav-tabs').scrollingTabs({ 557 | enableRtlSupport: true 558 | }); 559 | ``` 560 | 561 | With this option enabled, the plugin will check the page's `` tag for attribute `dir="rtl"` and will adjust its behavior accordingly. 562 | 563 | 564 | 565 | #### Setting Defaults 566 | Any options that can be passed into the plugin can also be set on the plugin's `defaults` object: 567 | ```javascript 568 | $.fn.scrollingTabs.defaults.tabs = myTabs; 569 | $.fn.scrollingTabs.defaults.forceActiveTab = true; 570 | $.fn.scrollingTabs.defaults.scrollToTabEdge = true; 571 | $.fn.scrollingTabs.defaults.disableScrollArrowsOnFullyScrolled = true; 572 | $.fn.scrollingTabs.defaults.handleDelayedScrollbar = true; 573 | $.fn.scrollingTabs.defaults.reverseScroll = true; 574 | $.fn.scrollingTabs.defaults.widthMultiplier = 0.5; 575 | $.fn.scrollingTabs.defaults.enableSwiping = true; 576 | $.fn.scrollingTabs.defaults.enableRtlSupport = true; 577 | ``` 578 | 579 | 580 | 581 | #### Events 582 | The plugin triggers event `ready.scrtabs` when the tabs have been wrapped in 583 | the scroller and are ready for viewing: 584 | 585 | ```javascript 586 | $('.nav-tabs') 587 | .scrollingTabs() 588 | .on('ready.scrtabs', function() { 589 | // tabs ready, do my other stuff... 590 | }); 591 | 592 | $('#tabs-inside-here') 593 | .scrollingTabs({ tabs: tabs }) 594 | .on('ready.scrtabs', function() { 595 | // tabs ready, do my other stuff... 596 | }); 597 | ``` 598 | 599 | 600 | #### Destroying the Plugin 601 | To destroy the plugin, call its `destroy` method: 602 | ```javascript 603 | $('#tabs-inside-here').scrollingTabs('destroy'); 604 | ``` 605 | 606 | 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. 607 | 608 | 609 | Custom SCSS 610 | ----------- 611 | Customise the SCSS by including `jquery.scrolling-tabs.scss` and overriding the following variables: 612 | * `$scrtabs-tabs-height` - The height of the tabs. 613 | * `$scrtabs-border-color` - The tabs border color. 614 | * `$scrtabs-foreground-color` - The text color. 615 | * `$scrtabs-background-color-hover` - The background color of the tabs when hovered over. 616 | 617 | 618 | License 619 | ------- 620 | MIT License. 621 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-bootstrap-scrolling-tabs", 3 | "version": "2.6.1", 4 | "main": [ 5 | "./dist/jquery.scrolling-tabs.js", 6 | "./dist/jquery.scrolling-tabs.css" 7 | ], 8 | "description": "jQuery plugin for scrollable Bootstrap Tabs", 9 | "homepage": "https://github.com/mikejacobson/jquery-bootstrap-scrolling-tabs", 10 | "author": "Mike Jacobson ", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mikejacobson/jquery-bootstrap-scrolling-tabs.git" 14 | }, 15 | "license": "MIT", 16 | "ignore": [ 17 | ".gitignore", 18 | "package.json", 19 | "bower.json", 20 | "README.md", 21 | "*.scss", 22 | "config.rb", 23 | "st-screenshot.png" 24 | ], 25 | "dependencies": { 26 | "bootstrap": "^3.1.1", 27 | "jquery": ">=1.9.0 <4.0.0" 28 | }, 29 | "keywords": [ 30 | "jquery", 31 | "bootstrap", 32 | "tabs", 33 | "scrolling", 34 | "scrollable" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /dist/jquery.scrolling-tabs.css: -------------------------------------------------------------------------------- 1 | /** 2 | * jquery-bootstrap-scrolling-tabs 3 | * @version v2.6.1 4 | * @link https://github.com/mikejacobson/jquery-bootstrap-scrolling-tabs 5 | * @author Mike Jacobson 6 | * @license MIT License, http://www.opensource.org/licenses/MIT 7 | */ 8 | .scrtabs-tab-container * { 9 | box-sizing: border-box; } 10 | 11 | .scrtabs-tab-container { 12 | height: 42px; } 13 | .scrtabs-tab-container .tab-content { 14 | clear: left; } 15 | 16 | .scrtabs-tab-container.scrtabs-bootstrap4 .scrtabs-tabs-movable-container > .navbar-nav { 17 | -ms-flex-direction: row; 18 | flex-direction: row; } 19 | 20 | .scrtabs-tabs-fixed-container { 21 | float: left; 22 | height: 42px; 23 | overflow: hidden; 24 | width: 100%; } 25 | 26 | .scrtabs-tabs-movable-container { 27 | position: relative; } 28 | .scrtabs-tabs-movable-container .tab-content { 29 | display: none; } 30 | 31 | .scrtabs-tab-container.scrtabs-rtl .scrtabs-tabs-movable-container > ul.nav-tabs { 32 | padding-right: 0; } 33 | 34 | .scrtabs-tab-scroll-arrow { 35 | border: 1px solid #dddddd; 36 | border-top: none; 37 | color: #428bca; 38 | display: none; 39 | float: left; 40 | font-size: 12px; 41 | height: 42px; 42 | margin-bottom: -1px; 43 | padding-left: 2px; 44 | padding-top: 13px; 45 | width: 20px; } 46 | .scrtabs-tab-scroll-arrow:hover { 47 | background-color: #eeeeee; } 48 | 49 | .scrtabs-tab-scroll-arrow, 50 | .scrtabs-tab-scroll-arrow .scrtabs-click-target { 51 | cursor: pointer; } 52 | 53 | .scrtabs-tab-scroll-arrow.scrtabs-with-click-target { 54 | cursor: default; } 55 | 56 | .scrtabs-tab-scroll-arrow.scrtabs-disable, 57 | .scrtabs-tab-scroll-arrow.scrtabs-disable .scrtabs-click-target { 58 | color: #ddd; 59 | cursor: default; } 60 | 61 | .scrtabs-tab-scroll-arrow.scrtabs-disable:hover { 62 | background-color: initial; } 63 | 64 | .scrtabs-tabs-fixed-container ul.nav-tabs > li { 65 | white-space: nowrap; } 66 | -------------------------------------------------------------------------------- /dist/jquery.scrolling-tabs.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * jquery-bootstrap-scrolling-tabs 3 | * @version v2.6.1 4 | * @link https://github.com/mikejacobson/jquery-bootstrap-scrolling-tabs 5 | * @author Mike Jacobson 6 | * @license MIT License, http://www.opensource.org/licenses/MIT 7 | */ 8 | .scrtabs-tab-container *{box-sizing:border-box}.scrtabs-tab-container{height:42px}.scrtabs-tab-container .tab-content{clear:left}.scrtabs-tab-container.scrtabs-bootstrap4 .scrtabs-tabs-movable-container>.navbar-nav{-ms-flex-direction:row;flex-direction:row}.scrtabs-tabs-fixed-container{float:left;height:42px;overflow:hidden;width:100%}.scrtabs-tabs-movable-container{position:relative}.scrtabs-tabs-movable-container .tab-content{display:none}.scrtabs-tab-container.scrtabs-rtl .scrtabs-tabs-movable-container>ul.nav-tabs{padding-right:0}.scrtabs-tab-scroll-arrow{border:1px solid #ddd;border-top:none;color:#428bca;display:none;float:left;font-size:12px;height:42px;margin-bottom:-1px;padding-left:2px;padding-top:13px;width:20px}.scrtabs-tab-scroll-arrow:hover{background-color:#eee}.scrtabs-tab-scroll-arrow,.scrtabs-tab-scroll-arrow .scrtabs-click-target{cursor:pointer}.scrtabs-tab-scroll-arrow.scrtabs-with-click-target{cursor:default}.scrtabs-tab-scroll-arrow.scrtabs-disable,.scrtabs-tab-scroll-arrow.scrtabs-disable .scrtabs-click-target{color:#ddd;cursor:default}.scrtabs-tab-scroll-arrow.scrtabs-disable:hover{background-color:initial}.scrtabs-tabs-fixed-container ul.nav-tabs>li{white-space:nowrap} -------------------------------------------------------------------------------- /dist/jquery.scrolling-tabs.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jquery-bootstrap-scrolling-tabs 3 | * @version v2.6.1 4 | * @link https://github.com/mikejacobson/jquery-bootstrap-scrolling-tabs 5 | * @author Mike Jacobson 6 | * @license MIT License, http://www.opensource.org/licenses/MIT 7 | */ 8 | !function(e,t){"use strict";function n(e){this.stc=e}function r(e){this.stc=e}function a(e){this.stc=e}function o(t){var o=this;o.$tabsContainer=t,o.instanceId=e.fn.scrollingTabs.nextInstanceId++,o.movableContainerLeftPos=0,o.scrollArrowsVisible=!1,o.scrollToTabEdge=!1,o.disableScrollArrowsOnFullyScrolled=!1,o.reverseScroll=!1,o.widthMultiplier=1,o.scrollMovement=new a(o),o.eventHandlers=new r(o),o.elementsHandler=new n(o)}function i(t,n,r){var a,o=n.tabs,i={paneId:n.propPaneId,title:n.propTitle,active:n.propActive,disabled:n.propDisabled,content:n.propContent},l=n.ignoreTabPanes,c=o.length&&void 0!==o[0][i.content],d=A.getNewElNavTabs(),b=A.getNewElTabContent(),f=l?null:function(){a.after(b)};if(o.length)return o.forEach(function(e,t){var r={forceActiveTab:!0,tabLiContent:n.tabsLiContent&&n.tabsLiContent[t],tabPostProcessor:n.tabsPostProcessors&&n.tabsPostProcessors[t]};A.getNewElTabLi(e,i,r).appendTo(d),!l&&c&&A.getNewElTabPane(e,i,r).appendTo(b)}),a=s(d,n,r,f),a.appendTo(t),t.data({scrtabs:{tabs:o,propNames:i,ignoreTabPanes:l,hasTabContent:c,tabsLiContent:n.tabsLiContent,tabsPostProcessors:n.tabsPostProcessors,scroller:a}}),a.find(".nav-tabs > li").each(function(t){L.storeDataOnLiEl(e(this),o,t)}),t}function s(e,t,n,r){e.find('a[data-toggle="tab"]').removeData(T.DATA_KEY_BOOTSTRAP_TAB);var a=A.getNewElScrollerElementWrappingNavTabsInstance(e.clone(!0),t),i=new o(a),s=e.data("scrtabs");return s?s.scroller=a:e.data("scrtabs",{scroller:a}),e.replaceWith(a.css("visibility","hidden")),t.tabClickHandler&&"function"==typeof t.tabClickHandler&&(a.hasTabClickHandler=!0,i.tabClickHandler=t.tabClickHandler),a.initTabs=function(){i.initTabs(t,a,n,r)},a.scrollToActiveTab=function(){i.scrollToActiveTab(t)},a.initTabs(),C(a,i),a}function l(e){var t=e.updatedTabsArray,n=e.updatedTabsLiContent||[],r=e.updatedTabsPostProcessors||[],a=e.propNames,o=e.ignoreTabPanes,i=e.options,s=e.$currTabLis,l=e.$navTabs,c=o?null:e.$currTabContentPanesContainer,d=o?null:e.$currTabContentPanes,b=!1;return t.forEach(function(e,f){var C,v=s.find('a[href="#'+e[a.paneId]+'"]'),u=f>=s.length;v.length||(b=!0,i.tabLiContent=n[f],i.tabPostProcessor=r[f],v=A.getNewElTabLi(e,a,i),L.storeDataOnLiEl(v,t,f),u?v.appendTo(l):v.insertBefore(s.eq(f)),o||void 0===e[a.content]||(C=A.getNewElTabPane(e,a,i),u?C.appendTo(c):C.insertBefore(d.eq(f))))}),b}function c(e){var t=e.tabLi,n=e.ignoreTabPanes,r=t.$li,a=t.$contentPane,o=t.origTabData,i=t.newTabData,s=e.propNames,l=!1;return o[s.title]!==i[s.title]&&(r.find('a[role="tab"]').html(o[s.title]=i[s.title]),l=!0),o[s.disabled]!==i[s.disabled]&&(i[s.disabled]?(r.addClass("disabled"),r.find('a[role="tab"]').attr("data-toggle","")):(r.removeClass("disabled"),r.find('a[role="tab"]').attr("data-toggle","tab")),o[s.disabled]=i[s.disabled],l=!0),e.options.forceActiveTab&&(r[i[s.active]?"addClass":"removeClass"]("active"),a[i[s.active]?"addClass":"removeClass"]("active"),o[s.active]=i[s.active],l=!0),n||o[s.content]===i[s.content]||(a.html(o[s.content]=i[s.content]),l=!0),l}function d(e){var t,n=e.tabLi,r=e.ignoreTabPanes,a=n.$li;return-1===n.newIdx&&(a.hasClass("active")&&(t=L.getIndexOfClosestEnabledTab(e.$currTabLis,n.currDomIdx))>-1&&(e.$currTabLis.eq(t).addClass("active"),r||e.$currTabContentPanes.eq(t).addClass("active")),a.remove(),r||n.$contentPane.remove(),!0)}function b(t){var n=t.$currTabLis,r=t.updatedTabsArray,a=t.propNames,o=t.ignoreTabPanes,i=[],s=o?null:[];return!!L.didTabOrderChange(n,r,a)&&(r.forEach(function(t){var r=t[a.paneId];i.push(n.find('a[role="tab"][href="#'+r+'"]').parent("li")),o||s.push(e("#"+r))}),t.$navTabs.append(i),o||t.$currTabContentPanesContainer.append(s),!0)}function f(t){var n=t.$currTabLis,r=t.updatedTabsArray,a=t.propNames,o=!1;return n.each(function(n){var i=e(this),s=i.data("tab"),l=L.getTabIndexByPaneId(r,a.paneId,s[a.paneId]),b=l>-1?r[l]:null;if(t.tabLi={$li:i,currDomIdx:n,newIdx:l,$contentPane:A.getElTabPaneForLi(i),origTabData:s,newTabData:b},d(t))return void(o=!0);c(t)&&(o=!0)}),o}function C(t,n){function r(t){e(t.target).append(o.off(T.EVENTS.CLICK))}function a(r){function a(){var n=e(this),r=n.parent("li"),a=r.parent(".dropdown-menu"),o=n.attr("href");r.hasClass("active")||(t.find("li.active").not(c).add(a.find("li.active")).removeClass("active"),c.add(r).addClass("active"),e(".tab-content .tab-pane.active").removeClass("active"),e(o).addClass("active"))}var i,s,l,c=e(r.target),d=c.offset(),b=t.find('li[role="presentation"].active');o=c.find(".dropdown-menu").attr("data-"+T.DATA_KEY_DDMENU_MODIFIED,!0),b[0]!==c[0]&&o.find("li.active").removeClass("active"),o.on(T.EVENTS.CLICK,'a[role="tab"]',a),e("body").append(o),i=o.width()+d.left,s=t.width()-(n.$slideRightArrow.outerWidth()+1),l=d.left,i>s&&(l-=i-s),o.css({display:"block",top:d.top+c.outerHeight()-2,left:l})}var o;t.on(T.EVENTS.DROPDOWN_MENU_SHOW,a).on(T.EVENTS.DROPDOWN_MENU_HIDE,r)}function v(e,t){var n=e.data().scrtabs,r=n.scroller,a=e.find(".scrtabs-tab-container .nav-tabs"),o=e.find(".tab-content"),i=!1,s={options:t,updatedTabsArray:n.tabs,updatedTabsLiContent:n.tabsLiContent,updatedTabsPostProcessors:n.tabsPostProcessors,propNames:n.propNames,ignoreTabPanes:n.ignoreTabPanes,$navTabs:a,$currTabLis:a.find("> li"),$currTabContentPanesContainer:o,$currTabContentPanes:o.find(".tab-pane")};return l(s)&&(i=!0),b(s)&&(i=!0),f(s)&&(i=!0),i&&r.initTabs(),i}function u(t,n){t.data("scrtabs")&&(!t.data("scrtabs").isWrapperOnly&&v(t,n)||e("body").trigger(T.EVENTS.FORCE_REFRESH))}function h(){var t=e(this),n=t.data("scrtabs");n&&n.scroller.scrollToActiveTab()}function S(){var n,r=e(this),a=r.data("scrtabs");if(a){for("self"===a.enableSwipingElement?r.removeClass(T.CSS_CLASSES.ALLOW_SCROLLBAR):"parent"===a.enableSwipingElement&&r.closest(".scrtabs-tab-container").parent().removeClass(T.CSS_CLASSES.ALLOW_SCROLLBAR),a.scroller.off(T.EVENTS.DROPDOWN_MENU_SHOW).off(T.EVENTS.DROPDOWN_MENU_HIDE),a.scroller.find("[data-"+T.DATA_KEY_DDMENU_MODIFIED+"]").css({display:"",left:"",top:""}).off(T.EVENTS.CLICK).removeAttr("data-"+T.DATA_KEY_DDMENU_MODIFIED),a.scroller.hasTabClickHandler&&r.find('a[data-toggle="tab"]').off(".scrtabs"),a.isWrapperOnly?(n=r.parents(".scrtabs-tab-container"),n.length&&n.replaceWith(r)):(a.scroller&&a.scroller.initTabs&&(a.scroller.initTabs=null),r.find(".scrtabs-tab-container").add(".tab-content").remove()),r.removeData("scrtabs");--e.fn.scrollingTabs.nextInstanceId>=0;)e(t).off(T.EVENTS.WINDOW_RESIZE+e.fn.scrollingTabs.nextInstanceId);e("body").off(T.EVENTS.FORCE_REFRESH)}}var T={CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL:50,SCROLL_OFFSET_FRACTION:6,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"}};!function(t){var n=function(e,t,n){var r;return function(){function a(){n||e.apply(o,i),r=null}var o=this,i=arguments;r?clearTimeout(r):n&&e.apply(o,i),r=setTimeout(a,t||100)}};e.fn[t]=function(e,r){var a=r||T.EVENTS.WINDOW_RESIZE;return e?this.bind(a,n(e)):this.trigger(t)}}("smartresizeScrtabs"),function(n){n.initElements=function(e){var t=this;t.setElementReferences(e),t.setEventListeners(e)},n.listenForTouchEvents=function(){var e,t,n,r=this,a=r.stc,o=a.scrollMovement,i=T.EVENTS,s=!1;a.$movableContainer.on(i.TOUCH_START,function(n){s=!0,t=a.movableContainerLeftPos,e=n.originalEvent.changedTouches[0].pageX}).on(i.TOUCH_END,function(){s=!1}).on(i.TOUCH_MOVE,function(r){if(s){var i=r.originalEvent.changedTouches[0].pageX,l=i-e;a.rtl&&(l=-l);var c;n=t+l,n>0?n=0:(c=o.getMinPos(),n li"),l.$slideLeftArrow=l.reverseScroll?r:a,l.$slideLeftArrowClickTarget=l.reverseScroll?o:i,l.$slideRightArrow=l.reverseScroll?a:r,l.$slideRightArrowClickTarget=l.reverseScroll?i:o,l.$scrollArrows=l.$slideLeftArrow.add(l.$slideRightArrow),l.$win=e(t)},n.setElementWidths=function(){var e=this,t=e.stc;t.winWidth=t.$win.width(),t.scrollArrowsCombinedWidth=t.$slideLeftArrow.outerWidth()+t.$slideRightArrow.outerWidth(),e.setFixedContainerWidth(),e.setMovableContainerWidth()},n.setEventListeners=function(t){var n=this,r=n.stc,a=r.eventHandlers,o=T.EVENTS,i=o.WINDOW_RESIZE+r.instanceId;t.enableSwiping&&n.listenForTouchEvents(),r.$slideLeftArrowClickTarget.off(".scrtabs").on(o.MOUSEDOWN,function(e){a.handleMousedownOnSlideMovContainerLeftArrow.call(a,e)}).on(o.MOUSEUP,function(e){a.handleMouseupOnSlideMovContainerLeftArrow.call(a,e)}).on(o.CLICK,function(e){a.handleClickOnSlideMovContainerLeftArrow.call(a,e)}),r.$slideRightArrowClickTarget.off(".scrtabs").on(o.MOUSEDOWN,function(e){a.handleMousedownOnSlideMovContainerRightArrow.call(a,e)}).on(o.MOUSEUP,function(e){a.handleMouseupOnSlideMovContainerRightArrow.call(a,e)}).on(o.CLICK,function(e){a.handleClickOnSlideMovContainerRightArrow.call(a,e)}),r.tabClickHandler&&r.$tabsLiCollection.find('a[data-toggle="tab"]').off(o.CLICK).on(o.CLICK,r.tabClickHandler),t.handleDelayedScrollbar&&n.listenForDelayedScrollbar(),r.$win.off(i).smartresizeScrtabs(function(e){a.handleWindowResize.call(a,e)},i),e("body").on(T.EVENTS.FORCE_REFRESH,r.elementsHandler.refreshAllElementSizes.bind(r.elementsHandler))},n.listenForDelayedScrollbar=function(){var n=document.createElement("iframe");n.id="scrtabs-scrollbar-resize-listener",n.style.cssText="height: 0; background-color: transparent; margin: 0; padding: 0; overflow: hidden; border-width: 0; position: absolute; width: 100%;",n.onload=function(){function r(){try{e(t).trigger("resize"),a=null}catch(e){}}var a;n.contentWindow.addEventListener("resize",function(){a&&clearTimeout(a),a=setTimeout(r,100)})},document.body.appendChild(n)},n.setFixedContainerWidth=function(){var e=this,t=e.stc,n=t.$tabsContainer.get(0).getBoundingClientRect();t.fixedContainerWidth=n.width||n.right-n.left,t.fixedContainerWidth=t.fixedContainerWidth*t.widthMultiplier,t.$fixedContainer.width(t.fixedContainerWidth)},n.setFixedContainerWidthForHiddenScrollArrows=function(){var e=this,t=e.stc;t.$fixedContainer.width(t.fixedContainerWidth)},n.setFixedContainerWidthForVisibleScrollArrows=function(){var e=this,t=e.stc;t.$fixedContainer.width(t.fixedContainerWidth-t.scrollArrowsCombinedWidth)},n.setMovableContainerWidth=function(){var t=this,n=t.stc,r=n.$tabsUl.find("> li");n.movableContainerWidth=0,r.length&&(r.each(function(){var t=e(this),r=0;n.isNavPills&&(r=parseInt(t.css("margin-left"),10)+parseInt(t.css("margin-right"),10)),n.movableContainerWidth+=t.outerWidth()+r}),n.movableContainerWidth+=1,n.movableContainerWidtht.fixedContainerWidth;n&&!t.scrollArrowsVisible?(t.$scrollArrows.show(),t.scrollArrowsVisible=!0):!n&&t.scrollArrowsVisible&&(t.$scrollArrows.hide(),t.scrollArrowsVisible=!1),t.scrollArrowsVisible?e.setFixedContainerWidthForVisibleScrollArrows():e.setFixedContainerWidthForHiddenScrollArrows()}}(n.prototype),function(e){e.handleClickOnSlideMovContainerLeftArrow=function(){this.stc.scrollMovement.incrementMovableContainerLeft()},e.handleClickOnSlideMovContainerRightArrow=function(){this.stc.scrollMovement.incrementMovableContainerRight()},e.handleMousedownOnSlideMovContainerLeftArrow=function(){var e=this,t=e.stc;t.$slideLeftArrowClickTarget.data(T.DATA_KEY_IS_MOUSEDOWN,!0),t.scrollMovement.continueSlideMovableContainerLeft()},e.handleMousedownOnSlideMovContainerRightArrow=function(){var e=this,t=e.stc;t.$slideRightArrowClickTarget.data(T.DATA_KEY_IS_MOUSEDOWN,!0),t.scrollMovement.continueSlideMovableContainerRight()},e.handleMouseupOnSlideMovContainerLeftArrow=function(){this.stc.$slideLeftArrowClickTarget.data(T.DATA_KEY_IS_MOUSEDOWN,!1)},e.handleMouseupOnSlideMovContainerRightArrow=function(){this.stc.$slideRightArrowClickTarget.data(T.DATA_KEY_IS_MOUSEDOWN,!1)},e.handleWindowResize=function(){var e=this,t=e.stc,n=t.$win.width();if(n===t.winWidth)return!1;t.winWidth=n,t.elementsHandler.refreshAllElementSizes()}}(r.prototype),function(t){t.continueSlideMovableContainerLeft=function(){var e=this,t=e.stc;setTimeout(function(){t.movableContainerLeftPos<=e.getMinPos()||!t.$slideLeftArrowClickTarget.data(T.DATA_KEY_IS_MOUSEDOWN)||e.incrementMovableContainerLeft()||e.continueSlideMovableContainerLeft()},T.CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL)},t.continueSlideMovableContainerRight=function(){var e=this,t=e.stc;setTimeout(function(){t.movableContainerLeftPos>=0||!t.$slideRightArrowClickTarget.data(T.DATA_KEY_IS_MOUSEDOWN)||e.incrementMovableContainerRight()||e.continueSlideMovableContainerRight()},T.CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL)},t.decrementMovableContainerLeftPos=function(e){var t=this,n=t.stc;n.movableContainerLeftPos-=n.fixedContainerWidth/T.SCROLL_OFFSET_FRACTION,n.movableContainerLeftPos0?n.movableContainerLeftPos=0:n.scrollToTabEdge&&t.setMovableContainerLeftPosToTabEdge(T.SLIDE_DIRECTION.RIGHT)),t.slideMovableContainerToLeftPos(),t.enableSlideLeftArrow(),0===n.movableContainerLeftPos},t.refreshScrollArrowsDisabledState=function(){var e=this,t=e.stc;if(t.disableScrollArrowsOnFullyScrolled&&t.scrollArrowsVisible){if(t.movableContainerLeftPos>=0)return e.disableSlideRightArrow(),void e.enableSlideLeftArrow();if(t.movableContainerLeftPos<=e.getMinPos())return e.disableSlideLeftArrow(),void e.enableSlideRightArrow();e.enableSlideLeftArrow(),e.enableSlideRightArrow()}},t.scrollToActiveTab=function(){var e,t,n,r,a,o,i,s,l,c=this,d=c.stc;if(d.scrollArrowsVisible&&(d.usingBootstrap4?(t=d.$tabsUl.find("li > .nav-link.active"),t.length&&(e=t.parent())):e=d.$tabsUl.find("li.active"),e&&e.length)){if(l=d.$slideRightArrow.outerWidth(),o=e.outerWidth(),n=e.offset().left-d.$fixedContainer.offset().left,r=n+o,a=d.fixedContainerWidth-l,d.rtl){if(d.$slideLeftArrow.outerWidth(),n<0)return d.movableContainerLeftPos+=n,c.slideMovableContainerToLeftPos(),!0;if(r>a)return d.movableContainerLeftPos+=r-a+2*l,c.slideMovableContainerToLeftPos(),!0}else{if(r>a)return i=r-a+l,s=d.fixedContainerWidth/2,i+=s-o/2,d.movableContainerLeftPos-=i,c.slideMovableContainerToLeftPos(),!0;if(d.$slideLeftArrow.outerWidth(),n<0)return s=d.fixedContainerWidth/2,d.movableContainerLeftPos+=-n+s-o/2,c.slideMovableContainerToLeftPos(),!0}return!1}},t.setMovableContainerLeftPosToTabEdge=function(t){var n=this,r=n.stc,a=-r.movableContainerLeftPos,o=0;r.$tabsLiCollection.each(function(){var n=e(this).width();if((o+=n)>a)return r.movableContainerLeftPos=t===T.SLIDE_DIRECTION.RIGHT?-(o-n):-o,!1})},t.slideMovableContainerToLeftPos=function(){var e,t=this,n=t.stc,r=t.getMinPos();n.movableContainerLeftPos>0?n.movableContainerLeftPos=0:n.movableContainerLeftPos')}function r(t,n){var r=e('
    '),a=n.leftArrowContent||'
    ',o=e(a),i=n.rightArrowContent||'
    ',s=e(i),l=e('
    '),c=e('
    ');return n.disableScrollArrowsOnFullyScrolled&&o.add(s).addClass(T.CSS_CLASSES.SCROLL_ARROW_DISABLE),r.append(o,l.append(c.append(t)),s)}function a(t,n){return e('').attr("href","#"+t[n.paneId]).html(t[n.title])}function o(){return e('
    ')}function i(t,n,r){var o=r.tabLiContent||'
  • ',i=e(o),s=a(t,n).appendTo(i);return t[n.disabled]?(i.addClass("disabled"),s.attr("data-toggle","")):r.forceActiveTab&&t[n.active]&&i.addClass("active"),r.tabPostProcessor&&r.tabPostProcessor(i,s),i}function s(t,n,r){var a=e('
    ').attr("id",t[n.paneId]).html(t[n.content]);return r.forceActiveTab&&t[n.active]&&a.addClass("active"),a}return{getElTabPaneForLi:t,getNewElNavTabs:n,getNewElScrollerElementWrappingNavTabsInstance:r,getNewElTabAnchor:a,getNewElTabContent:o,getNewElTabLi:i,getNewElTabPane:s}}(),L=function(){function t(t,n,a){var o=!1;return t.each(function(t){var i=r(n,a.paneId,e(this).data("tab")[a.paneId]);if(i>-1&&i!==t)return o=!0,!1}),o}function n(e,t){for(var n=e.length-1,r=-1,a=0,o=0;-1===r&&o>=0;)((o=t+ ++a)<=n&&!e.eq(o).hasClass("disabled")||(o=t-a)>=0&&!e.eq(o).hasClass("disabled"))&&(r=o);return r}function r(e,t,n){var r=-1;return e.some(function(e,a){if(e[t]===n)return r=a,!0}),r}function a(t,n,r){t.data({tab:e.extend({},n[r]),index:r})}return{didTabOrderChange:t,getIndexOfClosestEnabledTab:n,getTabIndexByPaneId:r,storeDataOnLiEl:a}}(),E={destroy:function(){return this.each(S)},init:function(t){var n=this,r=n.length-1,a=e.extend({},e.fn.scrollingTabs.defaults,t||{});return a.tabs?n.each(function(t){i(e(this),a,t", 8 | "main": "./dist/jquery.scrolling-tabs.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mikejacobson/jquery-bootstrap-scrolling-tabs.git" 12 | }, 13 | "license": "MIT", 14 | "ignore": [ 15 | ".gitignore", 16 | "bower.json", 17 | "README.md", 18 | "*.scss", 19 | "config.rb" 20 | ], 21 | "dependencies": { 22 | "bootstrap": "^3.1.1", 23 | "jquery": ">=1.9.0 <4.0.0" 24 | }, 25 | "keywords": [ 26 | "jquery", 27 | "bootstrap", 28 | "tabs", 29 | "scrolling", 30 | "scrollable" 31 | ], 32 | "devDependencies": { 33 | "browser-sync": "^2.18.8", 34 | "gulp": "^3.9.1", 35 | "gulp-clean-css": "^3.0.3", 36 | "gulp-header": "^1.8.8", 37 | "gulp-include": "^2.3.1", 38 | "gulp-jshint": "^2.0.4", 39 | "gulp-rename": "^1.2.2", 40 | "gulp-sass": "^3.1.0", 41 | "gulp-uglify": "^2.1.0", 42 | "gulp-util": "^3.0.8", 43 | "jshint": "^2.9.4", 44 | "jshint-stylish": "^2.2.1", 45 | "run-sequence": "^1.2.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /run/data-driven-mult.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 43 | 44 | 45 | 46 | 47 |
    48 |
    jquery-bootstrap-scrolling-tabs Demo - Data Driven
    49 |
    Using Tabs Array
    50 |
    51 | 52 | 53 | 54 | 55 | 56 |
    57 | 58 |
    59 | 60 | 61 |
    62 |

    63 |
    64 | 65 | 66 | 67 | 68 | 69 | 70 | 212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /run/data-driven-rtl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 43 | 44 | 45 | 46 | 47 |
    48 |
    jquery-bootstrap-scrolling-tabs Demo - Data Driven
    49 |
    Using Tabs Array
    50 |
    51 | 52 | 53 | 54 | 55 | 56 |
    57 | 58 |
    59 | 60 | 61 |
    62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /run/data-driven.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 51 | 52 | 53 | 54 | 55 |
    56 |
    jquery-bootstrap-scrolling-tabs Demo - Data Driven
    57 |
    Using Tabs Array
    58 |
    59 | 60 | 61 | 62 | 63 | 64 |
    65 | 66 |
    67 | 68 | 69 |
    70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /run/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 25 | 26 | 27 | 28 | 29 |
    30 |
    jquery-bootstrap-scrolling-tabs Demos
    31 |
    32 | 33 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /run/markup-only-mult.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 31 | 32 | 33 | 34 | 35 |
    jquery-bootstrap-scrolling-tabs Demo - Markup Only 36 | 37 | 38 |
    39 | 40 | 41 | 42 | 76 | 77 | 78 | 102 | 103 |
    104 |
    105 | 106 | 107 | 141 | 142 | 143 | 167 | 168 | 169 | 170 | 171 | 172 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /run/markup-only-rtl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 31 | 32 | 33 | 34 | 35 |
    jquery-bootstrap-scrolling-tabs Demo - Markup Only 36 | 37 | 38 |
    39 | 40 | 41 | 75 | 76 | 77 | 101 | 102 | 103 | 104 | 105 | 106 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /run/markup-only-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | point-left 61 | 62 | 63 | 64 | point-right 65 | 66 | 67 | 68 | 69 | 70 |
    jquery-bootstrap-scrolling-tabs Demo - Markup Only 71 | 72 | 73 |
    74 | 75 | 76 | 77 | 111 | 112 | 113 | 137 | 138 | 139 | 140 | 141 | 142 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /run/markup-only-with-click-target.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 31 | 32 | 33 | 34 | 35 |
    jquery-bootstrap-scrolling-tabs Demo - Markup Only 36 | 37 | 38 | 39 |
    40 | 41 | 42 | 43 | 77 | 78 | 79 | 103 | 104 | 105 | 106 | 107 | 108 | 116 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /run/markup-only.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 28 | 29 | 30 | 31 | 32 |
    jquery-bootstrap-scrolling-tabs Demo - Markup Only 33 | 34 | 35 |
    36 | 37 | 38 | 39 | 73 | 74 | 75 | 99 | 100 | 101 | 102 | 103 | 104 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/js/_main.js: -------------------------------------------------------------------------------- 1 | //=include usage.js 2 | ;(function ($, window) { 3 | 'use strict'; 4 | /* jshint unused:false */ 5 | 6 | //=include constants.js 7 | //=include smartresize.js 8 | //=include elementsHandler.js 9 | //=include eventHandlers.js 10 | //=include scrollMovement.js 11 | //=include scrollingTabsControl.js 12 | 13 | //=include buildTabs.js 14 | //=include tabListeners.js 15 | //=include api.js 16 | 17 | 18 | }(jQuery, window)); 19 | -------------------------------------------------------------------------------- /src/js/api.js: -------------------------------------------------------------------------------- 1 | var methods = { 2 | destroy: function() { 3 | var $targetEls = this; 4 | 5 | return $targetEls.each(destroyPlugin); 6 | }, 7 | 8 | init: function(options) { 9 | var $targetEls = this, 10 | targetElsLastIndex = $targetEls.length - 1, 11 | settings = $.extend({}, $.fn.scrollingTabs.defaults, options || {}); 12 | 13 | // ---- tabs NOT data-driven ------------------------- 14 | if (!settings.tabs) { 15 | 16 | // just wrap the selected .nav-tabs element(s) in the scroller 17 | return $targetEls.each(function(index) { 18 | var dataObj = { 19 | isWrapperOnly: true 20 | }, 21 | $targetEl = $(this).data({ scrtabs: dataObj }), 22 | readyCallback = (index < targetElsLastIndex) ? null : function() { 23 | $targetEls.trigger(CONSTANTS.EVENTS.TABS_READY); 24 | }; 25 | 26 | wrapNavTabsInstanceInScroller($targetEl, settings, readyCallback); 27 | }); 28 | 29 | } 30 | 31 | // ---- tabs data-driven ------------------------- 32 | return $targetEls.each(function (index) { 33 | var $targetEl = $(this), 34 | readyCallback = (index < targetElsLastIndex) ? null : function() { 35 | $targetEls.trigger(CONSTANTS.EVENTS.TABS_READY); 36 | }; 37 | 38 | buildNavTabsAndTabContentForTargetElementInstance($targetEl, settings, readyCallback); 39 | }); 40 | }, 41 | 42 | refresh: function(options) { 43 | var $targetEls = this, 44 | settings = $.extend({}, $.fn.scrollingTabs.defaults, options || {}); 45 | 46 | return $targetEls.each(function () { 47 | refreshTargetElementInstance($(this), settings); 48 | }); 49 | }, 50 | 51 | scrollToActiveTab: function() { 52 | return this.each(scrollToActiveTab); 53 | } 54 | }; 55 | 56 | function destroyPlugin() { 57 | /* jshint validthis: true */ 58 | var $targetElInstance = $(this), 59 | scrtabsData = $targetElInstance.data('scrtabs'), 60 | $tabsContainer; 61 | 62 | if (!scrtabsData) { 63 | return; 64 | } 65 | 66 | if (scrtabsData.enableSwipingElement === 'self') { 67 | $targetElInstance.removeClass(CONSTANTS.CSS_CLASSES.ALLOW_SCROLLBAR); 68 | } else if (scrtabsData.enableSwipingElement === 'parent') { 69 | $targetElInstance.closest('.scrtabs-tab-container').parent().removeClass(CONSTANTS.CSS_CLASSES.ALLOW_SCROLLBAR); 70 | } 71 | 72 | scrtabsData.scroller 73 | .off(CONSTANTS.EVENTS.DROPDOWN_MENU_SHOW) 74 | .off(CONSTANTS.EVENTS.DROPDOWN_MENU_HIDE); 75 | 76 | // if there were any dropdown menus opened, remove the css we added to 77 | // them so they would display correctly 78 | scrtabsData.scroller 79 | .find('[data-' + CONSTANTS.DATA_KEY_DDMENU_MODIFIED + ']') 80 | .css({ 81 | display: '', 82 | left: '', 83 | top: '' 84 | }) 85 | .off(CONSTANTS.EVENTS.CLICK) 86 | .removeAttr('data-' + CONSTANTS.DATA_KEY_DDMENU_MODIFIED); 87 | 88 | if (scrtabsData.scroller.hasTabClickHandler) { 89 | $targetElInstance 90 | .find('a[data-toggle="tab"]') 91 | .off('.scrtabs'); 92 | } 93 | 94 | if (scrtabsData.isWrapperOnly) { // we just wrapped nav-tabs markup, so restore it 95 | // $targetElInstance is the ul.nav-tabs 96 | $tabsContainer = $targetElInstance.parents('.scrtabs-tab-container'); 97 | 98 | if ($tabsContainer.length) { 99 | $tabsContainer.replaceWith($targetElInstance); 100 | } 101 | 102 | } else { // we generated the tabs from data so destroy everything we created 103 | if (scrtabsData.scroller && scrtabsData.scroller.initTabs) { 104 | scrtabsData.scroller.initTabs = null; 105 | } 106 | 107 | // $targetElInstance is the container for the ul.nav-tabs we generated 108 | $targetElInstance 109 | .find('.scrtabs-tab-container') 110 | .add('.tab-content') 111 | .remove(); 112 | } 113 | 114 | $targetElInstance.removeData('scrtabs'); 115 | 116 | while(--$.fn.scrollingTabs.nextInstanceId >= 0) { 117 | $(window).off(CONSTANTS.EVENTS.WINDOW_RESIZE + $.fn.scrollingTabs.nextInstanceId); 118 | } 119 | 120 | $('body').off(CONSTANTS.EVENTS.FORCE_REFRESH); 121 | } 122 | 123 | 124 | $.fn.scrollingTabs = function(methodOrOptions) { 125 | 126 | if (methods[methodOrOptions]) { 127 | return methods[methodOrOptions].apply(this, Array.prototype.slice.call(arguments, 1)); 128 | } else if (!methodOrOptions || (typeof methodOrOptions === 'object')) { 129 | return methods.init.apply(this, arguments); 130 | } else { 131 | $.error('Method ' + methodOrOptions + ' does not exist on $.scrollingTabs.'); 132 | } 133 | }; 134 | 135 | $.fn.scrollingTabs.nextInstanceId = 0; 136 | 137 | $.fn.scrollingTabs.defaults = { 138 | tabs: null, 139 | propPaneId: 'paneId', 140 | propTitle: 'title', 141 | propActive: 'active', 142 | propDisabled: 'disabled', 143 | propContent: 'content', 144 | ignoreTabPanes: false, 145 | scrollToTabEdge: false, 146 | disableScrollArrowsOnFullyScrolled: false, 147 | forceActiveTab: false, 148 | reverseScroll: false, 149 | widthMultiplier: 1, 150 | tabClickHandler: null, 151 | cssClassLeftArrow: 'glyphicon glyphicon-chevron-left', 152 | cssClassRightArrow: 'glyphicon glyphicon-chevron-right', 153 | leftArrowContent: '', 154 | rightArrowContent: '', 155 | tabsLiContent: null, 156 | tabsPostProcessors: null, 157 | enableSwiping: false, 158 | enableRtlSupport: false, 159 | handleDelayedScrollbar: false, 160 | bootstrapVersion: 3 161 | }; 162 | -------------------------------------------------------------------------------- /src/js/buildTabs.js: -------------------------------------------------------------------------------- 1 | /* exported buildNavTabsAndTabContentForTargetElementInstance */ 2 | var tabElements = (function () { 3 | 4 | return { 5 | getElTabPaneForLi: getElTabPaneForLi, 6 | getNewElNavTabs: getNewElNavTabs, 7 | getNewElScrollerElementWrappingNavTabsInstance: getNewElScrollerElementWrappingNavTabsInstance, 8 | getNewElTabAnchor: getNewElTabAnchor, 9 | getNewElTabContent: getNewElTabContent, 10 | getNewElTabLi: getNewElTabLi, 11 | getNewElTabPane: getNewElTabPane 12 | }; 13 | 14 | /////////////////// 15 | 16 | // ---- retrieve existing elements from the DOM ---------- 17 | function getElTabPaneForLi($li) { 18 | return $($li.find('a').attr('href')); 19 | } 20 | 21 | 22 | // ---- create new elements ---------- 23 | function getNewElNavTabs() { 24 | return $(''); 25 | } 26 | 27 | function getNewElScrollerElementWrappingNavTabsInstance($navTabsInstance, settings) { 28 | var $tabsContainer = $('
    '), 29 | leftArrowContent = settings.leftArrowContent || '
    ', 30 | $leftArrow = $(leftArrowContent), 31 | rightArrowContent = settings.rightArrowContent || '
    ', 32 | $rightArrow = $(rightArrowContent), 33 | $fixedContainer = $('
    '), 34 | $movableContainer = $('
    '); 35 | 36 | if (settings.disableScrollArrowsOnFullyScrolled) { 37 | $leftArrow.add($rightArrow).addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); 38 | } 39 | 40 | return $tabsContainer 41 | .append($leftArrow, 42 | $fixedContainer.append($movableContainer.append($navTabsInstance)), 43 | $rightArrow); 44 | } 45 | 46 | function getNewElTabAnchor(tab, propNames) { 47 | return $('') 48 | .attr('href', '#' + tab[propNames.paneId]) 49 | .html(tab[propNames.title]); 50 | } 51 | 52 | function getNewElTabContent() { 53 | return $('
    '); 54 | } 55 | 56 | function getNewElTabLi(tab, propNames, options) { 57 | var liContent = options.tabLiContent || '
  • ', 58 | $li = $(liContent), 59 | $a = getNewElTabAnchor(tab, propNames).appendTo($li); 60 | 61 | if (tab[propNames.disabled]) { 62 | $li.addClass('disabled'); 63 | $a.attr('data-toggle', ''); 64 | } else if (options.forceActiveTab && tab[propNames.active]) { 65 | $li.addClass('active'); 66 | } 67 | 68 | if (options.tabPostProcessor) { 69 | options.tabPostProcessor($li, $a); 70 | } 71 | 72 | return $li; 73 | } 74 | 75 | function getNewElTabPane(tab, propNames, options) { 76 | var $pane = $('
    ') 77 | .attr('id', tab[propNames.paneId]) 78 | .html(tab[propNames.content]); 79 | 80 | if (options.forceActiveTab && tab[propNames.active]) { 81 | $pane.addClass('active'); 82 | } 83 | 84 | return $pane; 85 | } 86 | 87 | 88 | }()); // tabElements 89 | 90 | var tabUtils = (function () { 91 | 92 | return { 93 | didTabOrderChange: didTabOrderChange, 94 | getIndexOfClosestEnabledTab: getIndexOfClosestEnabledTab, 95 | getTabIndexByPaneId: getTabIndexByPaneId, 96 | storeDataOnLiEl: storeDataOnLiEl 97 | }; 98 | 99 | /////////////////// 100 | 101 | function didTabOrderChange($currTabLis, updatedTabs, propNames) { 102 | var isTabOrderChanged = false; 103 | 104 | $currTabLis.each(function (currDomIdx) { 105 | var newIdx = getTabIndexByPaneId(updatedTabs, propNames.paneId, $(this).data('tab')[propNames.paneId]); 106 | 107 | if ((newIdx > -1) && (newIdx !== currDomIdx)) { // tab moved 108 | isTabOrderChanged = true; 109 | return false; // exit .each() loop 110 | } 111 | }); 112 | 113 | return isTabOrderChanged; 114 | } 115 | 116 | function getIndexOfClosestEnabledTab($currTabLis, startIndex) { 117 | var lastIndex = $currTabLis.length - 1, 118 | closestIdx = -1, 119 | incrementFromStartIndex = 0, 120 | testIdx = 0; 121 | 122 | // expand out from the current tab looking for an enabled tab; 123 | // we prefer the tab after us over the tab before 124 | while ((closestIdx === -1) && (testIdx >= 0)) { 125 | 126 | if ( (((testIdx = startIndex + (++incrementFromStartIndex)) <= lastIndex) && 127 | !$currTabLis.eq(testIdx).hasClass('disabled')) || 128 | (((testIdx = startIndex - incrementFromStartIndex) >= 0) && 129 | !$currTabLis.eq(testIdx).hasClass('disabled')) ) { 130 | 131 | closestIdx = testIdx; 132 | 133 | } 134 | } 135 | 136 | return closestIdx; 137 | } 138 | 139 | function getTabIndexByPaneId(tabs, paneIdPropName, paneId) { 140 | var idx = -1; 141 | 142 | tabs.some(function (tab, i) { 143 | if (tab[paneIdPropName] === paneId) { 144 | idx = i; 145 | return true; // exit loop 146 | } 147 | }); 148 | 149 | return idx; 150 | } 151 | 152 | function storeDataOnLiEl($li, tabs, index) { 153 | $li.data({ 154 | tab: $.extend({}, tabs[index]), // store a clone so we can check for changes 155 | index: index 156 | }); 157 | } 158 | 159 | }()); // tabUtils 160 | 161 | function buildNavTabsAndTabContentForTargetElementInstance($targetElInstance, settings, readyCallback) { 162 | var tabs = settings.tabs, 163 | propNames = { 164 | paneId: settings.propPaneId, 165 | title: settings.propTitle, 166 | active: settings.propActive, 167 | disabled: settings.propDisabled, 168 | content: settings.propContent 169 | }, 170 | ignoreTabPanes = settings.ignoreTabPanes, 171 | hasTabContent = tabs.length && tabs[0][propNames.content] !== undefined, 172 | $navTabs = tabElements.getNewElNavTabs(), 173 | $tabContent = tabElements.getNewElTabContent(), 174 | $scroller, 175 | attachTabContentToDomCallback = ignoreTabPanes ? null : function() { 176 | $scroller.after($tabContent); 177 | }; 178 | 179 | if (!tabs.length) { 180 | return; 181 | } 182 | 183 | tabs.forEach(function(tab, index) { 184 | var options = { 185 | forceActiveTab: true, 186 | tabLiContent: settings.tabsLiContent && settings.tabsLiContent[index], 187 | tabPostProcessor: settings.tabsPostProcessors && settings.tabsPostProcessors[index] 188 | }; 189 | 190 | tabElements 191 | .getNewElTabLi(tab, propNames, options) 192 | .appendTo($navTabs); 193 | 194 | // build the tab panes if we weren't told to ignore them and there's 195 | // tab content data available 196 | if (!ignoreTabPanes && hasTabContent) { 197 | tabElements 198 | .getNewElTabPane(tab, propNames, options) 199 | .appendTo($tabContent); 200 | } 201 | }); 202 | 203 | $scroller = wrapNavTabsInstanceInScroller($navTabs, 204 | settings, 205 | readyCallback, 206 | attachTabContentToDomCallback); 207 | 208 | $scroller.appendTo($targetElInstance); 209 | 210 | $targetElInstance.data({ 211 | scrtabs: { 212 | tabs: tabs, 213 | propNames: propNames, 214 | ignoreTabPanes: ignoreTabPanes, 215 | hasTabContent: hasTabContent, 216 | tabsLiContent: settings.tabsLiContent, 217 | tabsPostProcessors: settings.tabsPostProcessors, 218 | scroller: $scroller 219 | } 220 | }); 221 | 222 | // once the nav-tabs are wrapped in the scroller, attach each tab's 223 | // data to it for reference later; we need to wait till they're 224 | // wrapped in the scroller because we wrap a *clone* of the nav-tabs 225 | // we built above, not the original nav-tabs 226 | $scroller.find('.nav-tabs > li').each(function (index) { 227 | tabUtils.storeDataOnLiEl($(this), tabs, index); 228 | }); 229 | 230 | return $targetElInstance; 231 | } 232 | 233 | 234 | function wrapNavTabsInstanceInScroller($navTabsInstance, settings, readyCallback, attachTabContentToDomCallback) { 235 | // Remove tab data stored by Bootstrap in order to fix tabs that were already visited 236 | $navTabsInstance 237 | .find('a[data-toggle="tab"]') 238 | .removeData(CONSTANTS.DATA_KEY_BOOTSTRAP_TAB); 239 | 240 | var $scroller = tabElements.getNewElScrollerElementWrappingNavTabsInstance($navTabsInstance.clone(true), settings), // use clone because we replaceWith later 241 | scrollingTabsControl = new ScrollingTabsControl($scroller), 242 | navTabsInstanceData = $navTabsInstance.data('scrtabs'); 243 | 244 | if (!navTabsInstanceData) { 245 | $navTabsInstance.data('scrtabs', { 246 | scroller: $scroller 247 | }); 248 | } else { 249 | navTabsInstanceData.scroller = $scroller; 250 | } 251 | 252 | $navTabsInstance.replaceWith($scroller.css('visibility', 'hidden')); 253 | 254 | if (settings.tabClickHandler && (typeof settings.tabClickHandler === 'function')) { 255 | $scroller.hasTabClickHandler = true; 256 | scrollingTabsControl.tabClickHandler = settings.tabClickHandler; 257 | } 258 | 259 | $scroller.initTabs = function () { 260 | scrollingTabsControl.initTabs(settings, 261 | $scroller, 262 | readyCallback, 263 | attachTabContentToDomCallback); 264 | }; 265 | 266 | $scroller.scrollToActiveTab = function() { 267 | scrollingTabsControl.scrollToActiveTab(settings); 268 | }; 269 | 270 | $scroller.initTabs(); 271 | 272 | listenForDropdownMenuTabs($scroller, scrollingTabsControl); 273 | 274 | return $scroller; 275 | } 276 | -------------------------------------------------------------------------------- /src/js/constants.js: -------------------------------------------------------------------------------- 1 | /* exported CONSTANTS */ 2 | var CONSTANTS = { 3 | CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL: 50, // timeout interval for repeatedly moving the tabs container 4 | // by one increment while the mouse is held down--decrease to 5 | // make mousedown continous scrolling faster 6 | SCROLL_OFFSET_FRACTION: 6, // each click moves the container this fraction of the fixed container--decrease 7 | // to make the tabs scroll farther per click 8 | 9 | DATA_KEY_DDMENU_MODIFIED: 'scrtabsddmenumodified', 10 | DATA_KEY_IS_MOUSEDOWN: 'scrtabsismousedown', 11 | DATA_KEY_BOOTSTRAP_TAB: 'bs.tab', 12 | 13 | CSS_CLASSES: { 14 | BOOTSTRAP4: 'scrtabs-bootstrap4', 15 | RTL: 'scrtabs-rtl', 16 | SCROLL_ARROW_CLICK_TARGET: 'scrtabs-click-target', 17 | SCROLL_ARROW_DISABLE: 'scrtabs-disable', 18 | SCROLL_ARROW_WITH_CLICK_TARGET: 'scrtabs-with-click-target' 19 | }, 20 | 21 | SLIDE_DIRECTION: { 22 | LEFT: 1, 23 | RIGHT: 2 24 | }, 25 | 26 | EVENTS: { 27 | CLICK: 'click.scrtabs', 28 | DROPDOWN_MENU_HIDE: 'hide.bs.dropdown.scrtabs', 29 | DROPDOWN_MENU_SHOW: 'show.bs.dropdown.scrtabs', 30 | FORCE_REFRESH: 'forcerefresh.scrtabs', 31 | MOUSEDOWN: 'mousedown.scrtabs', 32 | MOUSEUP: 'mouseup.scrtabs', 33 | TABS_READY: 'ready.scrtabs', 34 | TOUCH_END: 'touchend.scrtabs', 35 | TOUCH_MOVE: 'touchmove.scrtabs', 36 | TOUCH_START: 'touchstart.scrtabs', 37 | WINDOW_RESIZE: 'resize.scrtabs' 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/js/elementsHandler.js: -------------------------------------------------------------------------------- 1 | /* *********************************************************************************** 2 | * ElementsHandler - Class that each instance of ScrollingTabsControl will instantiate 3 | * **********************************************************************************/ 4 | function ElementsHandler(scrollingTabsControl) { 5 | var ehd = this; 6 | 7 | ehd.stc = scrollingTabsControl; 8 | } 9 | 10 | // ElementsHandler prototype methods 11 | (function (p) { 12 | p.initElements = function (options) { 13 | var ehd = this; 14 | 15 | ehd.setElementReferences(options); 16 | ehd.setEventListeners(options); 17 | }; 18 | 19 | p.listenForTouchEvents = function () { 20 | var ehd = this, 21 | stc = ehd.stc, 22 | smv = stc.scrollMovement, 23 | ev = CONSTANTS.EVENTS; 24 | 25 | var touching = false; 26 | var touchStartX; 27 | var startingContainerLeftPos; 28 | var newLeftPos; 29 | 30 | stc.$movableContainer 31 | .on(ev.TOUCH_START, function (e) { 32 | touching = true; 33 | startingContainerLeftPos = stc.movableContainerLeftPos; 34 | touchStartX = e.originalEvent.changedTouches[0].pageX; 35 | }) 36 | .on(ev.TOUCH_END, function () { 37 | touching = false; 38 | }) 39 | .on(ev.TOUCH_MOVE, function (e) { 40 | if (!touching) { 41 | return; 42 | } 43 | 44 | var touchPageX = e.originalEvent.changedTouches[0].pageX; 45 | var diff = touchPageX - touchStartX; 46 | if (stc.rtl) { 47 | diff = -diff; 48 | } 49 | var minPos; 50 | 51 | newLeftPos = startingContainerLeftPos + diff; 52 | if (newLeftPos > 0) { 53 | newLeftPos = 0; 54 | } else { 55 | minPos = smv.getMinPos(); 56 | if (newLeftPos < minPos) { 57 | newLeftPos = minPos; 58 | } 59 | } 60 | stc.movableContainerLeftPos = newLeftPos; 61 | 62 | var leftOrRight = stc.rtl ? 'right' : 'left'; 63 | stc.$movableContainer.css(leftOrRight, smv.getMovableContainerCssLeftVal()); 64 | smv.refreshScrollArrowsDisabledState(); 65 | }); 66 | }; 67 | 68 | p.refreshAllElementSizes = function () { 69 | var ehd = this, 70 | stc = ehd.stc, 71 | smv = stc.scrollMovement, 72 | scrollArrowsWereVisible = stc.scrollArrowsVisible, 73 | actionsTaken = { 74 | didScrollToActiveTab: false 75 | }, 76 | isPerformingSlideAnim = false, 77 | minPos; 78 | 79 | ehd.setElementWidths(); 80 | ehd.setScrollArrowVisibility(); 81 | 82 | // this could have been a window resize or the removal of a 83 | // dynamic tab, so make sure the movable container is positioned 84 | // correctly because, if it is far to the left and we increased the 85 | // window width, it's possible that the tabs will be too far left, 86 | // beyond the min pos. 87 | if (stc.scrollArrowsVisible) { 88 | // make sure container not too far left 89 | minPos = smv.getMinPos(); 90 | 91 | isPerformingSlideAnim = smv.scrollToActiveTab({ 92 | isOnWindowResize: true 93 | }); 94 | 95 | if (!isPerformingSlideAnim) { 96 | smv.refreshScrollArrowsDisabledState(); 97 | 98 | if (stc.rtl) { 99 | if (stc.movableContainerRightPos < minPos) { 100 | smv.incrementMovableContainerLeft(minPos); 101 | } 102 | } else { 103 | if (stc.movableContainerLeftPos < minPos) { 104 | smv.incrementMovableContainerRight(minPos); 105 | } 106 | } 107 | } 108 | 109 | actionsTaken.didScrollToActiveTab = true; 110 | 111 | } else if (scrollArrowsWereVisible) { 112 | // scroll arrows went away after resize, so position movable container at 0 113 | stc.movableContainerLeftPos = 0; 114 | smv.slideMovableContainerToLeftPos(); 115 | } 116 | 117 | return actionsTaken; 118 | }; 119 | 120 | p.setElementReferences = function (settings) { 121 | var ehd = this, 122 | stc = ehd.stc, 123 | $tabsContainer = stc.$tabsContainer, 124 | $leftArrow, 125 | $rightArrow, 126 | $leftArrowClickTarget, 127 | $rightArrowClickTarget; 128 | 129 | stc.isNavPills = false; 130 | 131 | if (stc.rtl) { 132 | $tabsContainer.addClass(CONSTANTS.CSS_CLASSES.RTL); 133 | } 134 | 135 | if (stc.usingBootstrap4) { 136 | $tabsContainer.addClass(CONSTANTS.CSS_CLASSES.BOOTSTRAP4); 137 | } 138 | 139 | stc.$fixedContainer = $tabsContainer.find('.scrtabs-tabs-fixed-container'); 140 | $leftArrow = stc.$fixedContainer.prev(); 141 | $rightArrow = stc.$fixedContainer.next(); 142 | 143 | // if we have custom arrow content, we might have a click target defined 144 | if (settings.leftArrowContent) { 145 | $leftArrowClickTarget = $leftArrow.find('.' + CONSTANTS.CSS_CLASSES.SCROLL_ARROW_CLICK_TARGET); 146 | } 147 | 148 | if (settings.rightArrowContent) { 149 | $rightArrowClickTarget = $rightArrow.find('.' + CONSTANTS.CSS_CLASSES.SCROLL_ARROW_CLICK_TARGET); 150 | } 151 | 152 | if ($leftArrowClickTarget && $leftArrowClickTarget.length) { 153 | $leftArrow.addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_WITH_CLICK_TARGET); 154 | } else { 155 | $leftArrowClickTarget = $leftArrow; 156 | } 157 | 158 | if ($rightArrowClickTarget && $rightArrowClickTarget.length) { 159 | $rightArrow.addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_WITH_CLICK_TARGET); 160 | } else { 161 | $rightArrowClickTarget = $rightArrow; 162 | } 163 | 164 | stc.$movableContainer = $tabsContainer.find('.scrtabs-tabs-movable-container'); 165 | stc.$tabsUl = $tabsContainer.find('.nav-tabs'); 166 | 167 | // check for pills 168 | if (!stc.$tabsUl.length) { 169 | stc.$tabsUl = $tabsContainer.find('.nav-pills'); 170 | 171 | if (stc.$tabsUl.length) { 172 | stc.isNavPills = true; 173 | } 174 | } 175 | 176 | stc.$tabsLiCollection = stc.$tabsUl.find('> li'); 177 | 178 | stc.$slideLeftArrow = stc.reverseScroll ? $leftArrow : $rightArrow; 179 | stc.$slideLeftArrowClickTarget = stc.reverseScroll ? $leftArrowClickTarget : $rightArrowClickTarget; 180 | stc.$slideRightArrow = stc.reverseScroll ? $rightArrow : $leftArrow; 181 | stc.$slideRightArrowClickTarget = stc.reverseScroll ? $rightArrowClickTarget : $leftArrowClickTarget; 182 | stc.$scrollArrows = stc.$slideLeftArrow.add(stc.$slideRightArrow); 183 | 184 | stc.$win = $(window); 185 | }; 186 | 187 | p.setElementWidths = function () { 188 | var ehd = this, 189 | stc = ehd.stc; 190 | 191 | stc.winWidth = stc.$win.width(); 192 | stc.scrollArrowsCombinedWidth = stc.$slideLeftArrow.outerWidth() + stc.$slideRightArrow.outerWidth(); 193 | 194 | ehd.setFixedContainerWidth(); 195 | ehd.setMovableContainerWidth(); 196 | }; 197 | 198 | p.setEventListeners = function (settings) { 199 | var ehd = this, 200 | stc = ehd.stc, 201 | evh = stc.eventHandlers, 202 | ev = CONSTANTS.EVENTS, 203 | resizeEventName = ev.WINDOW_RESIZE + stc.instanceId; 204 | 205 | if (settings.enableSwiping) { 206 | ehd.listenForTouchEvents(); 207 | } 208 | 209 | stc.$slideLeftArrowClickTarget 210 | .off('.scrtabs') 211 | .on(ev.MOUSEDOWN, function (e) { evh.handleMousedownOnSlideMovContainerLeftArrow.call(evh, e); }) 212 | .on(ev.MOUSEUP, function (e) { evh.handleMouseupOnSlideMovContainerLeftArrow.call(evh, e); }) 213 | .on(ev.CLICK, function (e) { evh.handleClickOnSlideMovContainerLeftArrow.call(evh, e); }); 214 | 215 | stc.$slideRightArrowClickTarget 216 | .off('.scrtabs') 217 | .on(ev.MOUSEDOWN, function (e) { evh.handleMousedownOnSlideMovContainerRightArrow.call(evh, e); }) 218 | .on(ev.MOUSEUP, function (e) { evh.handleMouseupOnSlideMovContainerRightArrow.call(evh, e); }) 219 | .on(ev.CLICK, function (e) { evh.handleClickOnSlideMovContainerRightArrow.call(evh, e); }); 220 | 221 | if (stc.tabClickHandler) { 222 | stc.$tabsLiCollection 223 | .find('a[data-toggle="tab"]') 224 | .off(ev.CLICK) 225 | .on(ev.CLICK, stc.tabClickHandler); 226 | } 227 | 228 | if (settings.handleDelayedScrollbar) { 229 | ehd.listenForDelayedScrollbar(); 230 | } 231 | 232 | stc.$win 233 | .off(resizeEventName) 234 | .smartresizeScrtabs(function (e) { evh.handleWindowResize.call(evh, e); }, resizeEventName); 235 | 236 | $('body').on(CONSTANTS.EVENTS.FORCE_REFRESH, stc.elementsHandler.refreshAllElementSizes.bind(stc.elementsHandler)); 237 | }; 238 | 239 | p.listenForDelayedScrollbar = function () { 240 | var iframe = document.createElement('iframe'); 241 | iframe.id = "scrtabs-scrollbar-resize-listener"; 242 | iframe.style.cssText = 'height: 0; background-color: transparent; margin: 0; padding: 0; overflow: hidden; border-width: 0; position: absolute; width: 100%;'; 243 | iframe.onload = function() { 244 | var timeout; 245 | 246 | function handleResize() { 247 | try { 248 | $(window).trigger('resize'); 249 | timeout = null; 250 | } catch(e) {} 251 | } 252 | 253 | iframe.contentWindow.addEventListener('resize', function() { 254 | if (timeout) { 255 | clearTimeout(timeout); 256 | } 257 | 258 | timeout = setTimeout(handleResize, 100); 259 | }); 260 | }; 261 | 262 | document.body.appendChild(iframe); 263 | }; 264 | 265 | p.setFixedContainerWidth = function () { 266 | var ehd = this, 267 | stc = ehd.stc, 268 | tabsContainerRect = stc.$tabsContainer.get(0).getBoundingClientRect(); 269 | /** 270 | * @author poletaew 271 | * It solves problem with rounding by jQuery.outerWidth 272 | * If we have real width 100.5 px, jQuery.outerWidth returns us 101 px and we get layout's fail 273 | */ 274 | stc.fixedContainerWidth = tabsContainerRect.width || (tabsContainerRect.right - tabsContainerRect.left); 275 | stc.fixedContainerWidth = stc.fixedContainerWidth * stc.widthMultiplier; 276 | 277 | stc.$fixedContainer.width(stc.fixedContainerWidth); 278 | }; 279 | 280 | p.setFixedContainerWidthForHiddenScrollArrows = function () { 281 | var ehd = this, 282 | stc = ehd.stc; 283 | 284 | stc.$fixedContainer.width(stc.fixedContainerWidth); 285 | }; 286 | 287 | p.setFixedContainerWidthForVisibleScrollArrows = function () { 288 | var ehd = this, 289 | stc = ehd.stc; 290 | 291 | stc.$fixedContainer.width(stc.fixedContainerWidth - stc.scrollArrowsCombinedWidth); 292 | }; 293 | 294 | p.setMovableContainerWidth = function () { 295 | var ehd = this, 296 | stc = ehd.stc, 297 | $tabLi = stc.$tabsUl.find('> li'); 298 | 299 | stc.movableContainerWidth = 0; 300 | 301 | if ($tabLi.length) { 302 | 303 | $tabLi.each(function () { 304 | var $li = $(this), 305 | totalMargin = 0; 306 | 307 | if (stc.isNavPills) { // pills have a margin-left, tabs have no margin 308 | totalMargin = parseInt($li.css('margin-left'), 10) + parseInt($li.css('margin-right'), 10); 309 | } 310 | 311 | stc.movableContainerWidth += ($li.outerWidth() + totalMargin); 312 | }); 313 | 314 | stc.movableContainerWidth += 1; 315 | 316 | // if the tabs don't span the width of the page, force the 317 | // movable container width to full page width so the bottom 318 | // border spans the page width instead of just spanning the 319 | // width of the tabs 320 | if (stc.movableContainerWidth < stc.fixedContainerWidth) { 321 | stc.movableContainerWidth = stc.fixedContainerWidth; 322 | } 323 | } 324 | 325 | stc.$movableContainer.width(stc.movableContainerWidth); 326 | }; 327 | 328 | p.setScrollArrowVisibility = function () { 329 | var ehd = this, 330 | stc = ehd.stc, 331 | shouldBeVisible = stc.movableContainerWidth > stc.fixedContainerWidth; 332 | 333 | if (shouldBeVisible && !stc.scrollArrowsVisible) { 334 | stc.$scrollArrows.show(); 335 | stc.scrollArrowsVisible = true; 336 | } else if (!shouldBeVisible && stc.scrollArrowsVisible) { 337 | stc.$scrollArrows.hide(); 338 | stc.scrollArrowsVisible = false; 339 | } 340 | 341 | if (stc.scrollArrowsVisible) { 342 | ehd.setFixedContainerWidthForVisibleScrollArrows(); 343 | } else { 344 | ehd.setFixedContainerWidthForHiddenScrollArrows(); 345 | } 346 | }; 347 | 348 | }(ElementsHandler.prototype)); 349 | -------------------------------------------------------------------------------- /src/js/eventHandlers.js: -------------------------------------------------------------------------------- 1 | /* *********************************************************************************** 2 | * EventHandlers - Class that each instance of ScrollingTabsControl will instantiate 3 | * **********************************************************************************/ 4 | function EventHandlers(scrollingTabsControl) { 5 | var evh = this; 6 | 7 | evh.stc = scrollingTabsControl; 8 | } 9 | 10 | // prototype methods 11 | (function (p){ 12 | p.handleClickOnSlideMovContainerLeftArrow = function () { 13 | var evh = this, 14 | stc = evh.stc; 15 | 16 | stc.scrollMovement.incrementMovableContainerLeft(); 17 | }; 18 | 19 | p.handleClickOnSlideMovContainerRightArrow = function () { 20 | var evh = this, 21 | stc = evh.stc; 22 | 23 | stc.scrollMovement.incrementMovableContainerRight(); 24 | }; 25 | 26 | p.handleMousedownOnSlideMovContainerLeftArrow = function () { 27 | var evh = this, 28 | stc = evh.stc; 29 | 30 | stc.$slideLeftArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, true); 31 | stc.scrollMovement.continueSlideMovableContainerLeft(); 32 | }; 33 | 34 | p.handleMousedownOnSlideMovContainerRightArrow = function () { 35 | var evh = this, 36 | stc = evh.stc; 37 | 38 | stc.$slideRightArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, true); 39 | stc.scrollMovement.continueSlideMovableContainerRight(); 40 | }; 41 | 42 | p.handleMouseupOnSlideMovContainerLeftArrow = function () { 43 | var evh = this, 44 | stc = evh.stc; 45 | 46 | stc.$slideLeftArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, false); 47 | }; 48 | 49 | p.handleMouseupOnSlideMovContainerRightArrow = function () { 50 | var evh = this, 51 | stc = evh.stc; 52 | 53 | stc.$slideRightArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, false); 54 | }; 55 | 56 | p.handleWindowResize = function () { 57 | var evh = this, 58 | stc = evh.stc, 59 | newWinWidth = stc.$win.width(); 60 | 61 | if (newWinWidth === stc.winWidth) { 62 | return false; 63 | } 64 | 65 | stc.winWidth = newWinWidth; 66 | stc.elementsHandler.refreshAllElementSizes(); 67 | }; 68 | 69 | }(EventHandlers.prototype)); 70 | -------------------------------------------------------------------------------- /src/js/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jquery-bootstrap-scrolling-tabs 3 | * @version v2.6.1 4 | * @link https://github.com/mikejacobson/jquery-bootstrap-scrolling-tabs 5 | * @author Mike Jacobson 6 | * @license MIT License, http://www.opensource.org/licenses/MIT 7 | */ 8 | -------------------------------------------------------------------------------- /src/js/scrollMovement.js: -------------------------------------------------------------------------------- 1 | /* *********************************************************************************** 2 | * ScrollMovement - Class that each instance of ScrollingTabsControl will instantiate 3 | * **********************************************************************************/ 4 | function ScrollMovement(scrollingTabsControl) { 5 | var smv = this; 6 | 7 | smv.stc = scrollingTabsControl; 8 | } 9 | 10 | // prototype methods 11 | (function (p) { 12 | 13 | p.continueSlideMovableContainerLeft = function () { 14 | var smv = this, 15 | stc = smv.stc; 16 | 17 | setTimeout(function() { 18 | if (stc.movableContainerLeftPos <= smv.getMinPos() || 19 | !stc.$slideLeftArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN)) { 20 | return; 21 | } 22 | 23 | if (!smv.incrementMovableContainerLeft()) { // haven't reached max left 24 | smv.continueSlideMovableContainerLeft(); 25 | } 26 | }, CONSTANTS.CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL); 27 | }; 28 | 29 | p.continueSlideMovableContainerRight = function () { 30 | var smv = this, 31 | stc = smv.stc; 32 | 33 | setTimeout(function() { 34 | if (stc.movableContainerLeftPos >= 0 || 35 | !stc.$slideRightArrowClickTarget.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN)) { 36 | return; 37 | } 38 | 39 | if (!smv.incrementMovableContainerRight()) { // haven't reached max right 40 | smv.continueSlideMovableContainerRight(); 41 | } 42 | }, CONSTANTS.CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL); 43 | }; 44 | 45 | p.decrementMovableContainerLeftPos = function (minPos) { 46 | var smv = this, 47 | stc = smv.stc; 48 | 49 | stc.movableContainerLeftPos -= (stc.fixedContainerWidth / CONSTANTS.SCROLL_OFFSET_FRACTION); 50 | if (stc.movableContainerLeftPos < minPos) { 51 | stc.movableContainerLeftPos = minPos; 52 | } else if (stc.scrollToTabEdge) { 53 | smv.setMovableContainerLeftPosToTabEdge(CONSTANTS.SLIDE_DIRECTION.LEFT); 54 | 55 | if (stc.movableContainerLeftPos < minPos) { 56 | stc.movableContainerLeftPos = minPos; 57 | } 58 | } 59 | }; 60 | 61 | p.disableSlideLeftArrow = function () { 62 | var smv = this, 63 | stc = smv.stc; 64 | 65 | if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { 66 | return; 67 | } 68 | 69 | stc.$slideLeftArrow.addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); 70 | }; 71 | 72 | p.disableSlideRightArrow = function () { 73 | var smv = this, 74 | stc = smv.stc; 75 | 76 | if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { 77 | return; 78 | } 79 | 80 | stc.$slideRightArrow.addClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); 81 | }; 82 | 83 | p.enableSlideLeftArrow = function () { 84 | var smv = this, 85 | stc = smv.stc; 86 | 87 | if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { 88 | return; 89 | } 90 | 91 | stc.$slideLeftArrow.removeClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); 92 | }; 93 | 94 | p.enableSlideRightArrow = function () { 95 | var smv = this, 96 | stc = smv.stc; 97 | 98 | if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { 99 | return; 100 | } 101 | 102 | stc.$slideRightArrow.removeClass(CONSTANTS.CSS_CLASSES.SCROLL_ARROW_DISABLE); 103 | }; 104 | 105 | p.getMinPos = function () { 106 | var smv = this, 107 | stc = smv.stc; 108 | 109 | return stc.scrollArrowsVisible ? (stc.fixedContainerWidth - stc.movableContainerWidth - stc.scrollArrowsCombinedWidth) : 0; 110 | }; 111 | 112 | p.getMovableContainerCssLeftVal = function () { 113 | var smv = this, 114 | stc = smv.stc; 115 | 116 | return (stc.movableContainerLeftPos === 0) ? '0' : stc.movableContainerLeftPos + 'px'; 117 | }; 118 | 119 | p.incrementMovableContainerLeft = function () { 120 | var smv = this, 121 | stc = smv.stc, 122 | minPos = smv.getMinPos(); 123 | 124 | smv.decrementMovableContainerLeftPos(minPos); 125 | smv.slideMovableContainerToLeftPos(); 126 | smv.enableSlideRightArrow(); 127 | 128 | // return true if we're fully left, false otherwise 129 | return (stc.movableContainerLeftPos === minPos); 130 | }; 131 | 132 | p.incrementMovableContainerRight = function (minPos) { 133 | var smv = this, 134 | stc = smv.stc; 135 | 136 | // if minPos passed in, the movable container was beyond the minPos 137 | if (minPos) { 138 | stc.movableContainerLeftPos = minPos; 139 | } else { 140 | stc.movableContainerLeftPos += (stc.fixedContainerWidth / CONSTANTS.SCROLL_OFFSET_FRACTION); 141 | 142 | if (stc.movableContainerLeftPos > 0) { 143 | stc.movableContainerLeftPos = 0; 144 | } else if (stc.scrollToTabEdge) { 145 | smv.setMovableContainerLeftPosToTabEdge(CONSTANTS.SLIDE_DIRECTION.RIGHT); 146 | } 147 | } 148 | 149 | smv.slideMovableContainerToLeftPos(); 150 | smv.enableSlideLeftArrow(); 151 | 152 | // return true if we're fully right, false otherwise 153 | // left pos of 0 is the movable container's max position (farthest right) 154 | return (stc.movableContainerLeftPos === 0); 155 | }; 156 | 157 | p.refreshScrollArrowsDisabledState = function() { 158 | var smv = this, 159 | stc = smv.stc; 160 | 161 | if (!stc.disableScrollArrowsOnFullyScrolled || !stc.scrollArrowsVisible) { 162 | return; 163 | } 164 | 165 | if (stc.movableContainerLeftPos >= 0) { // movable container fully right 166 | smv.disableSlideRightArrow(); 167 | smv.enableSlideLeftArrow(); 168 | return; 169 | } 170 | 171 | if (stc.movableContainerLeftPos <= smv.getMinPos()) { // fully left 172 | smv.disableSlideLeftArrow(); 173 | smv.enableSlideRightArrow(); 174 | return; 175 | } 176 | 177 | smv.enableSlideLeftArrow(); 178 | smv.enableSlideRightArrow(); 179 | }; 180 | 181 | p.scrollToActiveTab = function () { 182 | var smv = this, 183 | stc = smv.stc, 184 | $activeTab, 185 | $activeTabAnchor, 186 | activeTabLeftPos, 187 | activeTabRightPos, 188 | rightArrowLeftPos, 189 | activeTabWidth, 190 | leftPosOffset, 191 | offsetToMiddle, 192 | leftScrollArrowWidth, 193 | rightScrollArrowWidth; 194 | 195 | if (!stc.scrollArrowsVisible) { 196 | return; 197 | } 198 | 199 | if (stc.usingBootstrap4) { 200 | $activeTabAnchor = stc.$tabsUl.find('li > .nav-link.active'); 201 | if ($activeTabAnchor.length) { 202 | $activeTab = $activeTabAnchor.parent(); 203 | } 204 | } else { 205 | $activeTab = stc.$tabsUl.find('li.active'); 206 | } 207 | 208 | if (!$activeTab || !$activeTab.length) { 209 | return; 210 | } 211 | 212 | rightScrollArrowWidth = stc.$slideRightArrow.outerWidth(); 213 | activeTabWidth = $activeTab.outerWidth(); 214 | 215 | /** 216 | * @author poletaew 217 | * We need relative offset (depends on $fixedContainer), don't absolute 218 | */ 219 | activeTabLeftPos = $activeTab.offset().left - stc.$fixedContainer.offset().left; 220 | activeTabRightPos = activeTabLeftPos + activeTabWidth; 221 | 222 | rightArrowLeftPos = stc.fixedContainerWidth - rightScrollArrowWidth; 223 | 224 | if (stc.rtl) { 225 | leftScrollArrowWidth = stc.$slideLeftArrow.outerWidth(); 226 | 227 | if (activeTabLeftPos < 0) { // active tab off left side 228 | stc.movableContainerLeftPos += activeTabLeftPos; 229 | smv.slideMovableContainerToLeftPos(); 230 | return true; 231 | } else { // active tab off right side 232 | if (activeTabRightPos > rightArrowLeftPos) { 233 | stc.movableContainerLeftPos += (activeTabRightPos - rightArrowLeftPos) + (2 * rightScrollArrowWidth); 234 | smv.slideMovableContainerToLeftPos(); 235 | return true; 236 | } 237 | } 238 | } else { 239 | if (activeTabRightPos > rightArrowLeftPos) { // active tab off right side 240 | leftPosOffset = activeTabRightPos - rightArrowLeftPos + rightScrollArrowWidth; 241 | offsetToMiddle = stc.fixedContainerWidth / 2; 242 | leftPosOffset += offsetToMiddle - (activeTabWidth / 2); 243 | stc.movableContainerLeftPos -= leftPosOffset; 244 | smv.slideMovableContainerToLeftPos(); 245 | return true; 246 | } else { 247 | leftScrollArrowWidth = stc.$slideLeftArrow.outerWidth(); 248 | if (activeTabLeftPos < 0) { // active tab off left side 249 | offsetToMiddle = stc.fixedContainerWidth / 2; 250 | stc.movableContainerLeftPos += (-activeTabLeftPos) + offsetToMiddle - (activeTabWidth / 2); 251 | smv.slideMovableContainerToLeftPos(); 252 | return true; 253 | } 254 | } 255 | } 256 | 257 | return false; 258 | }; 259 | 260 | p.setMovableContainerLeftPosToTabEdge = function (slideDirection) { 261 | var smv = this, 262 | stc = smv.stc, 263 | offscreenWidth = -stc.movableContainerLeftPos, 264 | totalTabWidth = 0; 265 | 266 | // make sure LeftPos is set so that a tab edge will be against the 267 | // left scroll arrow so we won't have a partial, cut-off tab 268 | stc.$tabsLiCollection.each(function () { 269 | var tabWidth = $(this).width(); 270 | 271 | totalTabWidth += tabWidth; 272 | 273 | if (totalTabWidth > offscreenWidth) { 274 | stc.movableContainerLeftPos = (slideDirection === CONSTANTS.SLIDE_DIRECTION.RIGHT) ? -(totalTabWidth - tabWidth) : -totalTabWidth; 275 | return false; // exit .each() loop 276 | } 277 | 278 | }); 279 | }; 280 | 281 | p.slideMovableContainerToLeftPos = function () { 282 | var smv = this, 283 | stc = smv.stc, 284 | minPos = smv.getMinPos(), 285 | leftOrRightVal; 286 | 287 | if (stc.movableContainerLeftPos > 0) { 288 | stc.movableContainerLeftPos = 0; 289 | } else if (stc.movableContainerLeftPos < minPos) { 290 | stc.movableContainerLeftPos = minPos; 291 | } 292 | 293 | stc.movableContainerLeftPos = stc.movableContainerLeftPos / 1; 294 | leftOrRightVal = smv.getMovableContainerCssLeftVal(); 295 | 296 | smv.performingSlideAnim = true; 297 | 298 | var targetPos = stc.rtl ? { right: leftOrRightVal } : { left: leftOrRightVal }; 299 | 300 | stc.$movableContainer.stop().animate(targetPos, 'slow', function __slideAnimComplete() { 301 | var newMinPos = smv.getMinPos(); 302 | 303 | smv.performingSlideAnim = false; 304 | 305 | // if we slid past the min pos--which can happen if you resize the window 306 | // quickly--move back into position 307 | if (stc.movableContainerLeftPos < newMinPos) { 308 | smv.decrementMovableContainerLeftPos(newMinPos); 309 | 310 | targetPos = stc.rtl ? { right: smv.getMovableContainerCssLeftVal() } : { left: smv.getMovableContainerCssLeftVal() }; 311 | 312 | stc.$movableContainer.stop().animate(targetPos, 'fast', function() { 313 | smv.refreshScrollArrowsDisabledState(); 314 | }); 315 | } else { 316 | smv.refreshScrollArrowsDisabledState(); 317 | } 318 | }); 319 | }; 320 | 321 | }(ScrollMovement.prototype)); 322 | -------------------------------------------------------------------------------- /src/js/scrollingTabsControl.js: -------------------------------------------------------------------------------- 1 | /* ********************************************************************** 2 | * ScrollingTabsControl - Class that each directive will instantiate 3 | * **********************************************************************/ 4 | function ScrollingTabsControl($tabsContainer) { 5 | var stc = this; 6 | 7 | stc.$tabsContainer = $tabsContainer; 8 | stc.instanceId = $.fn.scrollingTabs.nextInstanceId++; 9 | 10 | stc.movableContainerLeftPos = 0; 11 | stc.scrollArrowsVisible = false; 12 | stc.scrollToTabEdge = false; 13 | stc.disableScrollArrowsOnFullyScrolled = false; 14 | stc.reverseScroll = false; 15 | stc.widthMultiplier = 1; 16 | 17 | stc.scrollMovement = new ScrollMovement(stc); 18 | stc.eventHandlers = new EventHandlers(stc); 19 | stc.elementsHandler = new ElementsHandler(stc); 20 | } 21 | 22 | // prototype methods 23 | (function (p) { 24 | p.initTabs = function (options, $scroller, readyCallback, attachTabContentToDomCallback) { 25 | var stc = this, 26 | elementsHandler = stc.elementsHandler, 27 | num; 28 | 29 | if (options.enableRtlSupport && $('html').attr('dir') === 'rtl') { 30 | stc.rtl = true; 31 | } 32 | 33 | if (options.scrollToTabEdge) { 34 | stc.scrollToTabEdge = true; 35 | } 36 | 37 | if (options.disableScrollArrowsOnFullyScrolled) { 38 | stc.disableScrollArrowsOnFullyScrolled = true; 39 | } 40 | 41 | if (options.reverseScroll) { 42 | stc.reverseScroll = true; 43 | } 44 | 45 | if (options.widthMultiplier !== 1) { 46 | num = Number(options.widthMultiplier); // handle string value 47 | 48 | if (!isNaN(num)) { 49 | stc.widthMultiplier = num; 50 | } 51 | } 52 | 53 | if (options.bootstrapVersion.toString().charAt(0) === '4') { 54 | stc.usingBootstrap4 = true; 55 | } 56 | 57 | setTimeout(initTabsAfterTimeout, 100); 58 | 59 | function initTabsAfterTimeout() { 60 | var actionsTaken; 61 | 62 | // if we're just wrapping non-data-driven tabs, the user might 63 | // have the .nav-tabs hidden to prevent the clunky flash of 64 | // multi-line tabs on page refresh, so we need to make sure 65 | // they're visible before trying to wrap them 66 | $scroller.find('.nav-tabs').show(); 67 | 68 | elementsHandler.initElements(options); 69 | actionsTaken = elementsHandler.refreshAllElementSizes(); 70 | 71 | $scroller.css('visibility', 'visible'); 72 | 73 | if (attachTabContentToDomCallback) { 74 | attachTabContentToDomCallback(); 75 | } 76 | 77 | if (readyCallback) { 78 | readyCallback(); 79 | } 80 | } 81 | }; 82 | 83 | p.scrollToActiveTab = function(options) { 84 | var stc = this, 85 | smv = stc.scrollMovement; 86 | 87 | smv.scrollToActiveTab(options); 88 | }; 89 | }(ScrollingTabsControl.prototype)); 90 | -------------------------------------------------------------------------------- /src/js/smartresize.js: -------------------------------------------------------------------------------- 1 | // smartresize from Paul Irish (debounced window resize) 2 | (function (sr) { 3 | var debounce = function (func, threshold, execAsap) { 4 | var timeout; 5 | 6 | return function debounced() { 7 | var obj = this, args = arguments; 8 | function delayed() { 9 | if (!execAsap) { 10 | func.apply(obj, args); 11 | } 12 | timeout = null; 13 | } 14 | 15 | if (timeout) { 16 | clearTimeout(timeout); 17 | } else if (execAsap) { 18 | func.apply(obj, args); 19 | } 20 | 21 | timeout = setTimeout(delayed, threshold || 100); 22 | }; 23 | }; 24 | $.fn[sr] = function (fn, customEventName) { 25 | var eventName = customEventName || CONSTANTS.EVENTS.WINDOW_RESIZE; 26 | return fn ? this.bind(eventName, debounce(fn)) : this.trigger(sr); 27 | }; 28 | 29 | })('smartresizeScrtabs'); 30 | -------------------------------------------------------------------------------- /src/js/tabListeners.js: -------------------------------------------------------------------------------- 1 | /* exported listenForDropdownMenuTabs, 2 | refreshTargetElementInstance, 3 | scrollToActiveTab */ 4 | function checkForTabAdded(refreshData) { 5 | var updatedTabsArray = refreshData.updatedTabsArray, 6 | updatedTabsLiContent = refreshData.updatedTabsLiContent || [], 7 | updatedTabsPostProcessors = refreshData.updatedTabsPostProcessors || [], 8 | propNames = refreshData.propNames, 9 | ignoreTabPanes = refreshData.ignoreTabPanes, 10 | options = refreshData.options, 11 | $currTabLis = refreshData.$currTabLis, 12 | $navTabs = refreshData.$navTabs, 13 | $currTabContentPanesContainer = ignoreTabPanes ? null : refreshData.$currTabContentPanesContainer, 14 | $currTabContentPanes = ignoreTabPanes ? null : refreshData.$currTabContentPanes, 15 | isInitTabsRequired = false; 16 | 17 | // make sure each tab in the updated tabs array has a corresponding DOM element 18 | updatedTabsArray.forEach(function (tab, idx) { 19 | var $li = $currTabLis.find('a[href="#' + tab[propNames.paneId] + '"]'), 20 | isTabIdxPastCurrTabs = (idx >= $currTabLis.length), 21 | $pane; 22 | 23 | if (!$li.length) { // new tab 24 | isInitTabsRequired = true; 25 | 26 | // add the tab, add its pane (if necessary), and refresh the scroller 27 | options.tabLiContent = updatedTabsLiContent[idx]; 28 | options.tabPostProcessor = updatedTabsPostProcessors[idx]; 29 | $li = tabElements.getNewElTabLi(tab, propNames, options); 30 | tabUtils.storeDataOnLiEl($li, updatedTabsArray, idx); 31 | 32 | if (isTabIdxPastCurrTabs) { // append to end of current tabs 33 | $li.appendTo($navTabs); 34 | } else { // insert in middle of current tabs 35 | $li.insertBefore($currTabLis.eq(idx)); 36 | } 37 | 38 | if (!ignoreTabPanes && tab[propNames.content] !== undefined) { 39 | $pane = tabElements.getNewElTabPane(tab, propNames, options); 40 | if (isTabIdxPastCurrTabs) { // append to end of current tabs 41 | $pane.appendTo($currTabContentPanesContainer); 42 | } else { // insert in middle of current tabs 43 | $pane.insertBefore($currTabContentPanes.eq(idx)); 44 | } 45 | } 46 | 47 | } 48 | 49 | }); 50 | 51 | return isInitTabsRequired; 52 | } 53 | 54 | function checkForTabPropertiesUpdated(refreshData) { 55 | var tabLiData = refreshData.tabLi, 56 | ignoreTabPanes = refreshData.ignoreTabPanes, 57 | $li = tabLiData.$li, 58 | $contentPane = tabLiData.$contentPane, 59 | origTabData = tabLiData.origTabData, 60 | newTabData = tabLiData.newTabData, 61 | propNames = refreshData.propNames, 62 | isInitTabsRequired = false; 63 | 64 | // update tab title if necessary 65 | if (origTabData[propNames.title] !== newTabData[propNames.title]) { 66 | $li.find('a[role="tab"]') 67 | .html(origTabData[propNames.title] = newTabData[propNames.title]); 68 | 69 | isInitTabsRequired = true; 70 | } 71 | 72 | // update tab disabled state if necessary 73 | if (origTabData[propNames.disabled] !== newTabData[propNames.disabled]) { 74 | if (newTabData[propNames.disabled]) { // enabled -> disabled 75 | $li.addClass('disabled'); 76 | $li.find('a[role="tab"]').attr('data-toggle', ''); 77 | } else { // disabled -> enabled 78 | $li.removeClass('disabled'); 79 | $li.find('a[role="tab"]').attr('data-toggle', 'tab'); 80 | } 81 | 82 | origTabData[propNames.disabled] = newTabData[propNames.disabled]; 83 | isInitTabsRequired = true; 84 | } 85 | 86 | // update tab active state if necessary 87 | if (refreshData.options.forceActiveTab) { 88 | // set the active tab based on the tabs array regardless of the current 89 | // DOM state, which could have been changed by the user clicking a tab 90 | // without those changes being reflected back to the tab data 91 | $li[newTabData[propNames.active] ? 'addClass' : 'removeClass']('active'); 92 | 93 | $contentPane[newTabData[propNames.active] ? 'addClass' : 'removeClass']('active'); 94 | 95 | origTabData[propNames.active] = newTabData[propNames.active]; 96 | 97 | isInitTabsRequired = true; 98 | } 99 | 100 | // update tab content pane if necessary 101 | if (!ignoreTabPanes && origTabData[propNames.content] !== newTabData[propNames.content]) { 102 | $contentPane.html(origTabData[propNames.content] = newTabData[propNames.content]); 103 | isInitTabsRequired = true; 104 | } 105 | 106 | return isInitTabsRequired; 107 | } 108 | 109 | function checkForTabRemoved(refreshData) { 110 | var tabLiData = refreshData.tabLi, 111 | ignoreTabPanes = refreshData.ignoreTabPanes, 112 | $li = tabLiData.$li, 113 | idxToMakeActive; 114 | 115 | if (tabLiData.newIdx !== -1) { // tab was not removed--it has a valid index 116 | return false; 117 | } 118 | 119 | // if this was the active tab, make the closest enabled tab active 120 | if ($li.hasClass('active')) { 121 | 122 | idxToMakeActive = tabUtils.getIndexOfClosestEnabledTab(refreshData.$currTabLis, tabLiData.currDomIdx); 123 | if (idxToMakeActive > -1) { 124 | refreshData.$currTabLis 125 | .eq(idxToMakeActive) 126 | .addClass('active'); 127 | 128 | if (!ignoreTabPanes) { 129 | refreshData.$currTabContentPanes 130 | .eq(idxToMakeActive) 131 | .addClass('active'); 132 | } 133 | } 134 | } 135 | 136 | $li.remove(); 137 | 138 | if (!ignoreTabPanes) { 139 | tabLiData.$contentPane.remove(); 140 | } 141 | 142 | return true; 143 | } 144 | 145 | function checkForTabsOrderChanged(refreshData) { 146 | var $currTabLis = refreshData.$currTabLis, 147 | updatedTabsArray = refreshData.updatedTabsArray, 148 | propNames = refreshData.propNames, 149 | ignoreTabPanes = refreshData.ignoreTabPanes, 150 | newTabsCollection = [], 151 | newTabPanesCollection = ignoreTabPanes ? null : []; 152 | 153 | if (!tabUtils.didTabOrderChange($currTabLis, updatedTabsArray, propNames)) { 154 | return false; 155 | } 156 | 157 | // the tab order changed... 158 | updatedTabsArray.forEach(function (t) { 159 | var paneId = t[propNames.paneId]; 160 | 161 | newTabsCollection.push( 162 | $currTabLis 163 | .find('a[role="tab"][href="#' + paneId + '"]') 164 | .parent('li') 165 | ); 166 | 167 | if (!ignoreTabPanes) { 168 | newTabPanesCollection.push($('#' + paneId)); 169 | } 170 | }); 171 | 172 | refreshData.$navTabs.append(newTabsCollection); 173 | 174 | if (!ignoreTabPanes) { 175 | refreshData.$currTabContentPanesContainer.append(newTabPanesCollection); 176 | } 177 | 178 | return true; 179 | } 180 | 181 | function checkForTabsRemovedOrUpdated(refreshData) { 182 | var $currTabLis = refreshData.$currTabLis, 183 | updatedTabsArray = refreshData.updatedTabsArray, 184 | propNames = refreshData.propNames, 185 | isInitTabsRequired = false; 186 | 187 | 188 | $currTabLis.each(function (currDomIdx) { 189 | var $li = $(this), 190 | origTabData = $li.data('tab'), 191 | newIdx = tabUtils.getTabIndexByPaneId(updatedTabsArray, propNames.paneId, origTabData[propNames.paneId]), 192 | newTabData = (newIdx > -1) ? updatedTabsArray[newIdx] : null; 193 | 194 | refreshData.tabLi = { 195 | $li: $li, 196 | currDomIdx: currDomIdx, 197 | newIdx: newIdx, 198 | $contentPane: tabElements.getElTabPaneForLi($li), 199 | origTabData: origTabData, 200 | newTabData: newTabData 201 | }; 202 | 203 | if (checkForTabRemoved(refreshData)) { 204 | isInitTabsRequired = true; 205 | return; // continue to next $li in .each() since we removed this tab 206 | } 207 | 208 | if (checkForTabPropertiesUpdated(refreshData)) { 209 | isInitTabsRequired = true; 210 | } 211 | }); 212 | 213 | return isInitTabsRequired; 214 | } 215 | 216 | function listenForDropdownMenuTabs($scroller, stc) { 217 | var $ddMenu; 218 | 219 | // for dropdown menus to show, we need to move them out of the 220 | // scroller and append them to the body 221 | $scroller 222 | .on(CONSTANTS.EVENTS.DROPDOWN_MENU_SHOW, handleDropdownShow) 223 | .on(CONSTANTS.EVENTS.DROPDOWN_MENU_HIDE, handleDropdownHide); 224 | 225 | function handleDropdownHide(e) { 226 | // move the dropdown menu back into its tab 227 | $(e.target).append($ddMenu.off(CONSTANTS.EVENTS.CLICK)); 228 | } 229 | 230 | function handleDropdownShow(e) { 231 | var $ddParentTabLi = $(e.target), 232 | ddLiOffset = $ddParentTabLi.offset(), 233 | $currActiveTab = $scroller.find('li[role="presentation"].active'), 234 | ddMenuRightX, 235 | tabsContainerMaxX, 236 | ddMenuTargetLeft; 237 | 238 | $ddMenu = $ddParentTabLi 239 | .find('.dropdown-menu') 240 | .attr('data-' + CONSTANTS.DATA_KEY_DDMENU_MODIFIED, true); 241 | 242 | // if the dropdown's parent tab li isn't already active, 243 | // we need to deactivate any active menu item in the dropdown 244 | if ($currActiveTab[0] !== $ddParentTabLi[0]) { 245 | $ddMenu.find('li.active').removeClass('active'); 246 | } 247 | 248 | // we need to do our own click handling because the built-in 249 | // bootstrap handlers won't work since we moved the dropdown 250 | // menu outside the tabs container 251 | $ddMenu.on(CONSTANTS.EVENTS.CLICK, 'a[role="tab"]', handleClickOnDropdownMenuItem); 252 | 253 | $('body').append($ddMenu); 254 | 255 | // make sure the menu doesn't go off the right side of the page 256 | ddMenuRightX = $ddMenu.width() + ddLiOffset.left; 257 | tabsContainerMaxX = $scroller.width() - (stc.$slideRightArrow.outerWidth() + 1); 258 | ddMenuTargetLeft = ddLiOffset.left; 259 | 260 | if (ddMenuRightX > tabsContainerMaxX) { 261 | ddMenuTargetLeft -= (ddMenuRightX - tabsContainerMaxX); 262 | } 263 | 264 | $ddMenu.css({ 265 | 'display': 'block', 266 | 'top': ddLiOffset.top + $ddParentTabLi.outerHeight() - 2, 267 | 'left': ddMenuTargetLeft 268 | }); 269 | 270 | function handleClickOnDropdownMenuItem() { 271 | /* jshint validthis: true */ 272 | var $selectedMenuItemAnc = $(this), 273 | $selectedMenuItemLi = $selectedMenuItemAnc.parent('li'), 274 | $selectedMenuItemDropdownMenu = $selectedMenuItemLi.parent('.dropdown-menu'), 275 | targetPaneId = $selectedMenuItemAnc.attr('href'); 276 | 277 | if ($selectedMenuItemLi.hasClass('active')) { 278 | return; 279 | } 280 | 281 | // once we select a menu item from the dropdown, deactivate 282 | // the current tab (unless it's our parent tab), deactivate 283 | // any active dropdown menu item, make our parent tab active 284 | // (if it's not already), and activate the selected menu item 285 | $scroller 286 | .find('li.active') 287 | .not($ddParentTabLi) 288 | .add($selectedMenuItemDropdownMenu.find('li.active')) 289 | .removeClass('active'); 290 | 291 | $ddParentTabLi 292 | .add($selectedMenuItemLi) 293 | .addClass('active'); 294 | 295 | // manually deactivate current active pane and activate our pane 296 | $('.tab-content .tab-pane.active').removeClass('active'); 297 | $(targetPaneId).addClass('active'); 298 | } 299 | 300 | } 301 | } 302 | 303 | function refreshDataDrivenTabs($container, options) { 304 | var instanceData = $container.data().scrtabs, 305 | scroller = instanceData.scroller, 306 | $navTabs = $container.find('.scrtabs-tab-container .nav-tabs'), 307 | $currTabContentPanesContainer = $container.find('.tab-content'), 308 | isInitTabsRequired = false, 309 | refreshData = { 310 | options: options, 311 | updatedTabsArray: instanceData.tabs, 312 | updatedTabsLiContent: instanceData.tabsLiContent, 313 | updatedTabsPostProcessors: instanceData.tabsPostProcessors, 314 | propNames: instanceData.propNames, 315 | ignoreTabPanes: instanceData.ignoreTabPanes, 316 | $navTabs: $navTabs, 317 | $currTabLis: $navTabs.find('> li'), 318 | $currTabContentPanesContainer: $currTabContentPanesContainer, 319 | $currTabContentPanes: $currTabContentPanesContainer.find('.tab-pane') 320 | }; 321 | 322 | // to preserve the tab positions if we're just adding or removing 323 | // a tab, don't completely rebuild the tab structure, but check 324 | // for differences between the new tabs array and the old 325 | if (checkForTabAdded(refreshData)) { 326 | isInitTabsRequired = true; 327 | } 328 | 329 | if (checkForTabsOrderChanged(refreshData)) { 330 | isInitTabsRequired = true; 331 | } 332 | 333 | if (checkForTabsRemovedOrUpdated(refreshData)) { 334 | isInitTabsRequired = true; 335 | } 336 | 337 | if (isInitTabsRequired) { 338 | scroller.initTabs(); 339 | } 340 | 341 | return isInitTabsRequired; 342 | } 343 | 344 | function refreshTargetElementInstance($container, options) { 345 | if (!$container.data('scrtabs')) { // target element doesn't have plugin on it 346 | return; 347 | } 348 | 349 | // force a refresh if the tabs are static html or they're data-driven 350 | // but the data didn't change so we didn't call initTabs() 351 | if ($container.data('scrtabs').isWrapperOnly || !refreshDataDrivenTabs($container, options)) { 352 | $('body').trigger(CONSTANTS.EVENTS.FORCE_REFRESH); 353 | } 354 | } 355 | 356 | function scrollToActiveTab() { 357 | /* jshint validthis: true */ 358 | var $targetElInstance = $(this), 359 | scrtabsData = $targetElInstance.data('scrtabs'); 360 | 361 | if (!scrtabsData) { 362 | return; 363 | } 364 | 365 | scrtabsData.scroller.scrollToActiveTab(); 366 | } 367 | -------------------------------------------------------------------------------- /src/js/usage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery plugin version of Angular directive angular-bootstrap-scrolling-tabs: 3 | * https://github.com/mikejacobson/angular-bootstrap-scrolling-tabs 4 | * 5 | * Usage: 6 | * 7 | * Use case #1: HTML-defined tabs 8 | * ------------------------------ 9 | * Demo: http://plnkr.co/edit/thyD0grCxIjyU4PoTt4x?p=preview 10 | * 11 | * Sample HTML: 12 | * 13 | * 14 | * 20 | * 21 | * 22 | *
    23 | *
    Tab 1 content...
    24 | *
    Tab 2 content...
    25 | *
    Tab 3 content...
    26 | *
    Tab 4 content...
    27 | *
    28 | * 29 | * 30 | * JavaScript: 31 | * 32 | * $('.nav-tabs').scrollingTabs(); 33 | * 34 | * 35 | * Use Case #2: Data-driven tabs 36 | * ----------------------------- 37 | * Demo: http://plnkr.co/edit/MWBjLnTvJeetjU3NEimg?p=preview 38 | * 39 | * Sample HTML: 40 | * 41 | * 42 | *
    43 | * 44 | * 45 | * JavaScript: 46 | * 47 | * $('#tabs-inside-here').scrollingTabs({ 48 | * tabs: tabs, // required 49 | * propPaneId: 'paneId', // optional 50 | * propTitle: 'title', // optional 51 | * propActive: 'active', // optional 52 | * propDisabled: 'disabled', // optional 53 | * propContent: 'content', // optional 54 | * ignoreTabPanes: false, // optional 55 | * scrollToTabEdge: false, // optional 56 | * disableScrollArrowsOnFullyScrolled: false, // optional 57 | * reverseScroll: false // optional 58 | * }); 59 | * 60 | * Settings/Options: 61 | * 62 | * tabs: tabs data array 63 | * prop*: name of your tab object's property name that 64 | * corresponds to that required tab property if 65 | * your property name is different than the 66 | * standard name (paneId, title, etc.) 67 | * tabsLiContent: 68 | * optional string array used to define custom HTML 69 | * for each tab's
  • element. Each entry is an HTML 70 | * string defining the tab
  • element for the 71 | * corresponding tab in the tabs array. 72 | * The default for a tab is: 73 | * '
  • ' 74 | * So, for example, if you had 3 tabs and you needed 75 | * a custom 'tooltip' attribute on each one, your 76 | * tabsLiContent array might look like this: 77 | * [ 78 | * '', 79 | * '', 80 | * '' 81 | * ] 82 | * This plunk demonstrates its usage (in conjunction 83 | * with tabsPostProcessors): 84 | * http://plnkr.co/edit/ugJLMk7lmDCuZQziQ0k0 85 | * tabsPostProcessors: 86 | * optional array of functions, each one associated 87 | * with an entry in the tabs array. When a tab element 88 | * has been created, its associated post-processor 89 | * function will be called with two arguments: the 90 | * newly created $li and $a jQuery elements for that tab. 91 | * This allows you to, for example, attach a custom 92 | * event listener to each anchor tag. 93 | * This plunk demonstrates its usage (in conjunction 94 | * with tabsLiContent): 95 | * http://plnkr.co/edit/ugJLMk7lmDCuZQziQ0k0 96 | * ignoreTabPanes: relevant for data-driven tabs only--set to true if 97 | * you want the plugin to only touch the tabs 98 | * and to not generate the tab pane elements 99 | * that go in .tab-content. By default, the plugin 100 | * will generate the tab panes based on the content 101 | * property in your tab data, if a content property 102 | * is present. 103 | * scrollToTabEdge: set to true if you want to force full-width tabs 104 | * to display at the left scroll arrow. i.e., if the 105 | * scrolling stops with only half a tab showing, 106 | * it will snap the tab to its edge so the full tab 107 | * shows. 108 | * disableScrollArrowsOnFullyScrolled: 109 | * set to true if you want the left scroll arrow to 110 | * disable when the tabs are scrolled fully left, 111 | * and the right scroll arrow to disable when the tabs 112 | * are scrolled fully right. 113 | * reverseScroll: 114 | * set to true if you want the left scroll arrow to 115 | * slide the tabs left instead of right, and the right 116 | * scroll arrow to slide the tabs right. 117 | * enableSwiping: 118 | * set to true if you want to enable horizontal swiping 119 | * for touch screens. 120 | * widthMultiplier: 121 | * set to a value less than 1 if you want the tabs 122 | * container to be less than the full width of its 123 | * parent element. For example, set it to 0.5 if you 124 | * want the tabs container to be half the width of 125 | * its parent. 126 | * tabClickHandler: 127 | * a callback function to execute any time a tab is clicked. 128 | * The function is simply passed as the event handler 129 | * to jQuery's .on(), so the function will receive 130 | * the jQuery event as an argument, and the 'this' 131 | * inside the function will be the clicked tab's anchor 132 | * element. 133 | * cssClassLeftArrow, cssClassRightArrow: 134 | * custom values for the class attributes for the 135 | * left and right scroll arrows. The defaults are 136 | * 'glyphicon glyphicon-chevron-left' and 137 | * 'glyphicon glyphicon-chevron-right'. 138 | * Using different icons might require you to add 139 | * custom styling to the arrows to position the icons 140 | * correctly; the arrows can be targeted with these 141 | * selectors: 142 | * .scrtabs-tab-scroll-arrow 143 | * .scrtabs-tab-scroll-arrow-left 144 | * .scrtabs-tab-scroll-arrow-right 145 | * leftArrowContent, rightArrowContent: 146 | * custom HTML string for the left and right scroll 147 | * arrows. This will override any custom cssClassLeftArrow 148 | * and cssClassRightArrow settings. 149 | * For example, if you wanted to use svg icons, you 150 | * could set them like so: 151 | * 152 | * leftArrowContent: [ 153 | * '
    ', 154 | * ' ', 155 | * ' ', 156 | * ' ', 157 | * '
    ' 158 | * ].join(''), 159 | * rightArrowContent: [ 160 | * '
    ', 161 | * ' ', 162 | * ' ', 163 | * ' ', 164 | * '
    ' 165 | * ].join('') 166 | * 167 | * You would then need to add some CSS to make them 168 | * work correctly if you don't give them the 169 | * default scrtabs-tab-scroll-arrow classes. 170 | * This plunk shows it working with svg icons: 171 | * http://plnkr.co/edit/2MdZCAnLyeU40shxaol3?p=preview 172 | * 173 | * When using this option, you can also mark a child 174 | * element within the arrow content as the click target 175 | * if you don't want the entire content to be 176 | * clickable. You do that my adding the CSS class 177 | * 'scrtabs-click-target' to the element that should 178 | * be clickable, like so: 179 | * 180 | * leftArrowContent: [ 181 | * '
    ', 182 | * ' ', 185 | * '
    ' 186 | * ].join(''), 187 | * rightArrowContent: [ 188 | * '
    ', 189 | * ' ', 192 | * '
    ' 193 | * ].join('') 194 | * 195 | * enableRtlSupport: 196 | * set to true if you want your site to support 197 | * right-to-left languages. If true, the plugin will 198 | * check the page's tag for attribute dir="rtl" 199 | * and will adjust its behavior accordingly. 200 | * handleDelayedScrollbar: 201 | * set to true if you experience a situation where the 202 | * right scroll arrow wraps to the next line due to a 203 | * vertical scrollbar coming into existence on the page 204 | * after the plugin already calculated its width without 205 | * a scrollbar present. This would occur if, for example, 206 | * the bulk of the page's content loaded after a delay, 207 | * and only then did a vertical scrollbar become necessary. 208 | * It would also occur if a vertical scrollbar only appeared 209 | * on selection of a particular tab that had more content 210 | * than the default tab. 211 | * bootstrapVersion: 212 | * set to 4 if you're using Boostrap 4. Default is 3. 213 | * Bootstrap 4 handles some things differently than 3 214 | * (e.g., the 'active' class gets applied to the tab's 215 | * 'li > a' element rather than the 'li' itself). 216 | * 217 | * 218 | * On tabs data change: 219 | * 220 | * $('#tabs-inside-here').scrollingTabs('refresh'); 221 | * 222 | * On tabs data change, if you want the active tab to be set based on 223 | * the updated tabs data (i.e., you want to override the current 224 | * active tab setting selected by the user), for example, if you 225 | * added a new tab and you want it to be the active tab: 226 | * 227 | * $('#tabs-inside-here').scrollingTabs('refresh', { 228 | * forceActiveTab: true 229 | * }); 230 | * 231 | * Any options that can be passed into the plugin can be set on the 232 | * plugin's 'defaults' object instead so you don't have to pass them in: 233 | * 234 | * $.fn.scrollingTabs.defaults.tabs = tabs; 235 | * $.fn.scrollingTabs.defaults.forceActiveTab = true; 236 | * $.fn.scrollingTabs.defaults.scrollToTabEdge = true; 237 | * $.fn.scrollingTabs.defaults.disableScrollArrowsOnFullyScrolled = true; 238 | * $.fn.scrollingTabs.defaults.reverseScroll = true; 239 | * $.fn.scrollingTabs.defaults.widthMultiplier = 0.5; 240 | * $.fn.scrollingTabs.defaults.tabClickHandler = function () { }; 241 | * 242 | * 243 | * Methods 244 | * ----------------------------- 245 | * - refresh 246 | * On window resize, the tabs should refresh themselves, but to force a refresh: 247 | * 248 | * $('.nav-tabs').scrollingTabs('refresh'); 249 | * 250 | * - scrollToActiveTab 251 | * On window resize, the active tab will automatically be scrolled to 252 | * if it ends up offscreen, but you can also programmatically force a 253 | * scroll to the active tab any time (if, for example, you're 254 | * programmatically setting the active tab) by calling the 255 | * 'scrollToActiveTab' method: 256 | * 257 | * $('.nav-tabs').scrollingTabs('scrollToActiveTab'); 258 | * 259 | * 260 | * Events 261 | * ----------------------------- 262 | * The plugin triggers event 'ready.scrtabs' when the tabs have 263 | * been wrapped in the scroller and are ready for viewing: 264 | * 265 | * $('.nav-tabs') 266 | * .scrollingTabs() 267 | * .on('ready.scrtabs', function() { 268 | * // tabs ready, do my other stuff... 269 | * }); 270 | * 271 | * $('#tabs-inside-here') 272 | * .scrollingTabs({ tabs: tabs }) 273 | * .on('ready.scrtabs', function() { 274 | * // tabs ready, do my other stuff... 275 | * }); 276 | * 277 | * 278 | * Destroying 279 | * ----------------------------- 280 | * To destroy: 281 | * 282 | * $('.nav-tabs').scrollingTabs('destroy'); 283 | * 284 | * $('#tabs-inside-here').scrollingTabs('destroy'); 285 | * 286 | * If you were wrapping markup, the markup will be restored; if your tabs 287 | * were data-driven, the tabs will be destroyed along with the plugin. 288 | * 289 | */ 290 | -------------------------------------------------------------------------------- /src/scss/jquery.scrolling-tabs.scss: -------------------------------------------------------------------------------- 1 | // Tabs height 2 | $scrtabs-tabs-height: 42px !default; 3 | 4 | // Border color (bootstrap lt gray) 5 | $scrtabs-border-color: 1px solid rgb(221, 221, 221) !default; 6 | 7 | // Foreground color (bootstrap blue) 8 | $scrtabs-foreground-color: rgb(66, 139, 202) !default; 9 | 10 | // Background color on hover (bootstrap gray) 11 | $scrtabs-background-color-hover: rgb(238, 238, 238) !default; 12 | 13 | .scrtabs-tab-container * { 14 | box-sizing: border-box; 15 | } 16 | 17 | .scrtabs-tab-container { 18 | height: $scrtabs-tabs-height; 19 | .tab-content { 20 | clear: left; 21 | } 22 | } 23 | 24 | .scrtabs-tab-container.scrtabs-bootstrap4 .scrtabs-tabs-movable-container > .navbar-nav { 25 | -ms-flex-direction: row; 26 | flex-direction: row; 27 | } 28 | 29 | .scrtabs-tabs-fixed-container { 30 | float: left; 31 | height: $scrtabs-tabs-height; 32 | overflow: hidden; 33 | width: 100%; 34 | } 35 | 36 | .scrtabs-tabs-movable-container { 37 | position: relative; 38 | .tab-content { 39 | display: none; 40 | } 41 | } 42 | 43 | // override user agent padding-start (which translates to padding-right for 44 | // RTL sites) of 40px applied to
      elements (Bootstrap already overrides 45 | // it for LTR sites by setting padding-left to 0) 46 | .scrtabs-tab-container.scrtabs-rtl .scrtabs-tabs-movable-container > ul.nav-tabs { 47 | padding-right: 0; 48 | } 49 | 50 | .scrtabs-tab-scroll-arrow { 51 | border: $scrtabs-border-color; 52 | border-top: none; 53 | color: $scrtabs-foreground-color; 54 | display: none; 55 | float: left; 56 | font-size: 12px; 57 | height: $scrtabs-tabs-height; 58 | margin-bottom: -1px; 59 | padding-left: 2px; 60 | padding-top: 13px; 61 | width: 20px; 62 | &:hover { 63 | background-color: $scrtabs-background-color-hover; 64 | } 65 | } 66 | 67 | .scrtabs-tab-scroll-arrow, 68 | .scrtabs-tab-scroll-arrow .scrtabs-click-target { 69 | cursor: pointer; 70 | } 71 | 72 | .scrtabs-tab-scroll-arrow.scrtabs-with-click-target { 73 | cursor: default; 74 | } 75 | 76 | .scrtabs-tab-scroll-arrow.scrtabs-disable, 77 | .scrtabs-tab-scroll-arrow.scrtabs-disable .scrtabs-click-target { 78 | color: #ddd; 79 | cursor: default; 80 | } 81 | 82 | .scrtabs-tab-scroll-arrow.scrtabs-disable:hover { 83 | background-color: initial; 84 | } 85 | 86 | .scrtabs-tabs-fixed-container ul.nav-tabs > li { 87 | white-space: nowrap; 88 | } 89 | -------------------------------------------------------------------------------- /st-screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikejacobson/jquery-bootstrap-scrolling-tabs/68487cdc81f7c2a06613466748cc455ba8a513cc/st-screenshot1.png --------------------------------------------------------------------------------