vendor: 14,977 bytes, lines 1-551
1import $ from 'jquery'; 2import { Plugin } from './foundation.core.plugin'; 3import { onLoad } from './foundation.core.utils'; 4import { Keyboard } from './foundation.util.keyboard'; 5import { onImagesLoaded } from './foundation.util.imageLoader'; 6/** 7 * Tabs module. 8 * @module foundation.tabs 9 * @requires foundation.util.keyboard 10 * @requires foundation.util.imageLoader if tabs contain images 11 */ 12 13class Tabs extends Plugin { 14 /** 15 * Creates a new instance of tabs. 16 * @class 17 * @name Tabs 18 * @fires Tabs#init 19 * @param {jQuery} element - jQuery object to make into tabs. 20 * @param {Object} options - Overrides to the default plugin settings. 21 */ 22 _setup(element, options) { 23 this.$element = element; 24 this.options = $.extend({}, Tabs.defaults, this.$element.data(), options); 25 this.className = 'Tabs'; // ie9 back compat 26 27 this._init(); 28 Keyboard.register('Tabs', { 29 'ENTER': 'open', 30 'SPACE': 'open', 31 'ARROW_RIGHT': 'next', 32 'ARROW_UP': 'previous', 33 'ARROW_DOWN': 'next', 34 'ARROW_LEFT': 'previous' 35 // 'TAB': 'next', 36 // 'SHIFT_TAB': 'previous' 37 }); 38 } 39 40 /** 41 * Initializes the tabs by showing and focusing (if autoFocus=true) the preset active tab. 42 * @private 43 */ 44 _init() { 45 var _this = this; 46 this._isInitializing = true; 47 48 this.$element.attr({'role': 'tablist'}); 49 this.$tabTitles = this.$element.find(`.${this.options.linkClass}`); 50 this.$tabContent = $(`[data-tabs-content="${this.$element[0].id}"]`); 51 52 this.$tabTitles.each(function(){ 53 var $elem = $(this), 54 $link = $elem.find('a'), 55 isActive = $elem.hasClass(`${_this.options.linkActiveClass}`), 56 hash = $link.attr('data-tabs-target') || $link[0].hash.slice(1), 57 linkId = $link[0].id ? $link[0].id : `${hash}-label`, 58 $tabContent = $(`#${hash}`); 59 60 $elem.attr({'role': 'presentation'}); 61 62 $link.attr({ 63 'role': 'tab', 64 'aria-controls': hash, 65 'aria-selected': isActive, 66 'id': linkId, 67 'tabindex': isActive ? '0' : '-1' 68 }); 69 70 $tabContent.attr({ 71 'role': 'tabpanel', 72 'aria-labelledby': linkId 73 }); 74 75 // Save up the initial hash to return to it later when going back in history 76 if (isActive) { 77 _this._initialAnchor = `#${hash}`; 78 } 79 80 if(!isActive) { 81 $tabContent.attr('aria-hidden', 'true'); 82 } 83 84 if(isActive && _this.options.autoFocus){ 85 _this.onLoadListener = onLoad($(window), function() { 86 $('html, body').animate({ scrollTop: $elem.offset().top }, _this.options.deepLinkSmudgeDelay, () => { 87 $link.focus(); 88 }); 89 }); 90 } 91 }); 92 93 if(this.options.matchHeight) { 94 var $images = this.$tabContent.find('img'); 95 96 if ($images.length) { 97 onImagesLoaded($images, this._setHeight.bind(this)); 98 } else { 99 this._setHeight(); 100 } 101 } 102 103 // Current context-bound function to open tabs on page load or history hashchange 104 this._checkDeepLink = () => { 105 var anchor = window.location.hash; 106 107 if (!anchor.length) { 108 // If we are still initializing and there is no anchor, then there is nothing to do 109 if (this._isInitializing) return; 110 // Otherwise, move to the initial anchor 111 if (this._initialAnchor) anchor = this._initialAnchor; 112 } 113 114 var anchorNoHash = anchor.indexOf('#') >= 0 ? anchor.slice(1) : anchor; 115 var $anchor = anchorNoHash && $(`#${anchorNoHash}`); 116 var $link = anchor && this.$element.find(`[href$="${anchor}"],[data-tabs-target="${anchorNoHash}"]`).first(); 117 // Whether the anchor element that has been found is part of this element 118 var isOwnAnchor = !!($anchor.length && $link.length); 119 120 if (isOwnAnchor) { 121 // If there is an anchor for the hash, select it 122 if ($anchor && $anchor.length && $link && $link.length) { 123 this.selectTab($anchor, true); 124 } 125 // Otherwise, collapse everything 126 else { 127 this._collapse(); 128 } 129 130 // Roll up a little to show the titles 131 if (this.options.deepLinkSmudge) { 132 var offset = this.$element.offset(); 133 $('html, body').animate({ scrollTop: offset.top - this.options.deepLinkSmudgeOffset}, this.options.deepLinkSmudgeDelay); 134 } 135 136 /** 137 * Fires when the plugin has deeplinked at pageload 138 * @event Tabs#deeplink 139 */ 140 this.$element.trigger('deeplink.zf.tabs', [$link, $anchor]); 141 } 142 } 143 144 //use browser to open a tab, if it exists in this tabset 145 if (this.options.deepLink) { 146 this._checkDeepLink(); 147 } 148 149 this._events(); 150 151 this._isInitializing = false; 152 } 153 154 /** 155 * Adds event handlers for items within the tabs. 156 * @private 157 */ 158 _events() { 159 this._addKeyHandler(); 160 this._addClickHandler(); 161 this._setHeightMqHandler = null; 162 163 if (this.options.matchHeight) { 164 this._setHeightMqHandler = this._setHeight.bind(this); 165 166 $(window).on('changed.zf.mediaquery', this._setHeightMqHandler); 167 } 168 169 if(this.options.deepLink) { 170 $(window).on('hashchange', this._checkDeepLink); 171 } 172 } 173 174 /** 175 * Adds click handlers for items within the tabs. 176 * @private 177 */ 178 _addClickHandler() { 179 var _this = this; 180 181 this.$element 182 .off('click.zf.tabs') 183 .on('click.zf.tabs', `.${this.options.linkClass}`, function(e){ 184 e.preventDefault(); 185 _this._handleTabChange($(this)); 186 }); 187 } 188 189 /** 190 * Adds keyboard event handlers for items within the tabs. 191 * @private 192 */ 193 _addKeyHandler() { 194 var _this = this; 195 196 this.$tabTitles.off('keydown.zf.tabs').on('keydown.zf.tabs', function(e){ 197 if (e.which === 9) return; 198 199 200 var $element = $(this), 201 $elements = $element.parent('ul').children('li'), 202 $prevElement, 203 $nextElement; 204 205 $elements.each(function(i) { 206 if ($(this).is($element)) { 207 if (_this.options.wrapOnKeys) { 208 $prevElement = i === 0 ? $elements.last() : $elements.eq(i-1); 209 $nextElement = i === $elements.length -1 ? $elements.first() : $elements.eq(i+1); 210 } else { 211 $prevElement = $elements.eq(Math.max(0, i-1)); 212 $nextElement = $elements.eq(Math.min(i+1, $elements.length-1)); 213 } 214 return; 215 } 216 }); 217 218 // handle keyboard event with keyboard util 219 Keyboard.handleKey(e, 'Tabs', { 220 open: function() { 221 $element.find('[role="tab"]').focus(); 222 _this._handleTabChange($element); 223 }, 224 previous: function() { 225 $prevElement.find('[role="tab"]').focus(); 226 _this._handleTabChange($prevElement); 227 }, 228 next: function() { 229 $nextElement.find('[role="tab"]').focus(); 230 _this._handleTabChange($nextElement); 231 }, 232 handled: function() { 233 e.preventDefault(); 234 } 235 }); 236 }); 237 } 238 239 /** 240 * Opens the tab `$targetContent` defined by `$target`. Collapses active tab. 241 * @param {jQuery} $target - Tab to open. 242 * @param {boolean} historyHandled - browser has already handled a history update 243 * @fires Tabs#change 244 * @function 245 */ 246 _handleTabChange($target, historyHandled) { 247 248 // With `activeCollapse`, if the target is the active Tab, collapse it. 249 if ($target.hasClass(`${this.options.linkActiveClass}`)) { 250 if(this.options.activeCollapse) { 251 this._collapse(); 252 } 253 return; 254 } 255 256 var $oldTab = this.$element. 257 find(`.${this.options.linkClass}.${this.options.linkActiveClass}`), 258 $tabLink = $target.find('[role="tab"]'), 259 target = $tabLink.attr('data-tabs-target'), 260 anchor = target && target.length ? `#${target}` : $tabLink[0].hash, 261 $targetContent = this.$tabContent.find(anchor); 262 263 //close old tab 264 this._collapseTab($oldTab); 265 266 //open new tab 267 this._openTab($target); 268 269 //either replace or update browser history 270 if (this.options.deepLink && !historyHandled) { 271 if (this.options.updateHistory) { 272 history.pushState({}, '', location.pathname + location.search + anchor); 273 } else { 274 history.replaceState({}, '', location.pathname + location.search + anchor); 275 } 276 } 277 278 /** 279 * Fires when the plugin has successfully changed tabs. 280 * @event Tabs#change 281 */ 282 this.$element.trigger('change.zf.tabs', [$target, $targetContent]); 283 284 //fire to children a mutation event 285 $targetContent.find("[data-mutate]").trigger("mutateme.zf.trigger"); 286 } 287 288 /** 289 * Opens the tab `$targetContent` defined by `$target`. 290 * @param {jQuery} $target - Tab to open. 291 * @function 292 */ 293 _openTab($target) { 294 var $tabLink = $target.find('[role="tab"]'), 295 hash = $tabLink.attr('data-tabs-target') || $tabLink[0].hash.slice(1), 296 $targetContent = this.$tabContent.find(`#${hash}`); 297 298 $target.addClass(`${this.options.linkActiveClass}`); 299 300 $tabLink.attr({ 301 'aria-selected': 'true', 302 'tabindex': '0' 303 }); 304 305 $targetContent 306 .addClass(`${this.options.panelActiveClass}`).removeAttr('aria-hidden'); 307 } 308 309 /** 310 * Collapses `$targetContent` defined by `$target`. 311 * @param {jQuery} $target - Tab to collapse. 312 * @function 313 */ 314 _collapseTab($target) { 315 var $targetAnchor = $target 316 .removeClass(`${this.options.linkActiveClass}`) 317 .find('[role="tab"]') 318 .attr({ 319 'aria-selected': 'false', 320 'tabindex': -1 321 }); 322 323 $(`#${$targetAnchor.attr('aria-controls')}`) 324 .removeClass(`${this.options.panelActiveClass}`) 325 .attr({ 'aria-hidden': 'true' }) 326 } 327 328 /** 329 * Collapses the active Tab. 330 * @fires Tabs#collapse 331 * @function 332 */ 333 _collapse() { 334 var $activeTab = this.$element.find(`.${this.options.linkClass}.${this.options.linkActiveClass}`); 335 336 if ($activeTab.length) { 337 this._collapseTab($activeTab); 338 339 /** 340 * Fires when the plugin has successfully collapsed tabs. 341 * @event Tabs#collapse 342 */ 343 this.$element.trigger('collapse.zf.tabs', [$activeTab]); 344 } 345 } 346 347 /** 348 * Public method for selecting a content pane to display. 349 * @param {jQuery | String} elem - jQuery object or string of the id of the pane to display. 350 * @param {boolean} historyHandled - browser has already handled a history update 351 * @function 352 */ 353 selectTab(elem, historyHandled) { 354 var idStr, hashIdStr; 355 356 if (typeof elem === 'object') { 357 idStr = elem[0].id; 358 } else { 359 idStr = elem; 360 } 361 362 if (idStr.indexOf('#') < 0) { 363 hashIdStr = `#${idStr}`; 364 } else { 365 hashIdStr = idStr; 366 idStr = idStr.slice(1); 367 } 368 369 var $target = this.$tabTitles.has(`[href$="${hashIdStr}"],[data-tabs-target="${idStr}"]`).first(); 370 371 this._handleTabChange($target, historyHandled); 372 }; 373 374 /** 375 * Sets the height of each panel to the height of the tallest panel. 376 * If enabled in options, gets called on media query change. 377 * If loading content via external source, can be called directly or with _reflow. 378 * If enabled with `data-match-height="true"`, tabs sets to equal height 379 * @function 380 * @private 381 */ 382 _setHeight() { 383 var max = 0, 384 _this = this; // Lock down the `this` value for the root tabs object 385 386 if (!this.$tabContent) { 387 return; 388 } 389 390 this.$tabContent 391 .find(`.${this.options.panelClass}`) 392 .css('min-height', '') 393 .each(function() { 394 395 var panel = $(this), 396 isActive = panel.hasClass(`${_this.options.panelActiveClass}`); // get the options from the parent instead of trying to get them from the child 397 398 if (!isActive) { 399 panel.css({'visibility': 'hidden', 'display': 'block'}); 400 } 401 402 var temp = this.getBoundingClientRect().height; 403 404 if (!isActive) { 405 panel.css({ 406 'visibility': '', 407 'display': '' 408 }); 409 } 410 411 max = temp > max ? temp : max; 412 }) 413 .css('min-height', `${max}px`); 414 } 415 416 /** 417 * Destroys an instance of tabs. 418 * @fires Tabs#destroyed 419 */ 420 _destroy() { 421 this.$element 422 .find(`.${this.options.linkClass}`) 423 .off('.zf.tabs').hide().end() 424 .find(`.${this.options.panelClass}`) 425 .hide(); 426 427 if (this.options.matchHeight) { 428 if (this._setHeightMqHandler != null) { 429 $(window).off('changed.zf.mediaquery', this._setHeightMqHandler); 430 } 431 } 432 433 if (this.options.deepLink) { 434 $(window).off('hashchange', this._checkDeepLink); 435 } 436 437 if (this.onLoadListener) { 438 $(window).off(this.onLoadListener); 439 } 440 } 441} 442 443Tabs.defaults = { 444 /** 445 * Link the location hash to the active pane. 446 * Set the location hash when the active pane changes, and open the corresponding pane when the location changes. 447 * @option 448 * @type {boolean} 449 * @default false 450 */ 451 deepLink: false, 452 453 /** 454 * If `deepLink` is enabled, adjust the deep link scroll to make sure the top of the tab panel is visible 455 * @option 456 * @type {boolean} 457 * @default false 458 */ 459 deepLinkSmudge: false, 460 461 /** 462 * If `deepLinkSmudge` is enabled, animation time (ms) for the deep link adjustment 463 * @option 464 * @type {number} 465 * @default 300 466 */ 467 deepLinkSmudgeDelay: 300, 468 469 /** 470 * If `deepLinkSmudge` is enabled, animation offset from the top for the deep link adjustment 471 * @option 472 * @type {number} 473 * @default 0 474 */ 475 deepLinkSmudgeOffset: 0, 476 477 /** 478 * If `deepLink` is enabled, update the browser history with the open tab 479 * @option 480 * @type {boolean} 481 * @default false 482 */ 483 updateHistory: false, 484 485 /** 486 * Allows the window to scroll to content of active pane on load. 487 * Not recommended if more than one tab panel per page. 488 * @option 489 * @type {boolean} 490 * @default false 491 */ 492 autoFocus: false, 493 494 /** 495 * Allows keyboard input to 'wrap' around the tab links. 496 * @option 497 * @type {boolean} 498 * @default true 499 */ 500 wrapOnKeys: true, 501 502 /** 503 * Allows the tab content panes to match heights if set to true. 504 * @option 505 * @type {boolean} 506 * @default false 507 */ 508 matchHeight: false, 509 510 /** 511 * Allows active tabs to collapse when clicked. 512 * @option 513 * @type {boolean} 514 * @default false 515 */ 516 activeCollapse: false, 517 518 /** 519 * Class applied to `li`'s in tab link list. 520 * @option 521 * @type {string} 522 * @default 'tabs-title' 523 */ 524 linkClass: 'tabs-title', 525 526 /** 527 * Class applied to the active `li` in tab link list. 528 * @option 529 * @type {string} 530 * @default 'is-active' 531 */ 532 linkActiveClass: 'is-active', 533 534 /** 535 * Class applied to the content containers. 536 * @option 537 * @type {string} 538 * @default 'tabs-panel' 539 */ 540 panelClass: 'tabs-panel', 541 542 /** 543 * Class applied to the active content container. 544 * @option 545 * @type {string} 546 * @default 'is-active' 547 */ 548 panelActiveClass: 'is-active' 549}; 550 551export {Tabs};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.