/*! * jQuery UI Tabs @VERSION * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license */ //>>label: Tabs //>>group: Widgets //>>description: Transforms a set of container elements into a tab structure. //>>docs: http://api.jqueryui.com/tabs/ //>>demos: http://jqueryui.com/tabs/ //>>css.structure: ../themes/base/core.css //>>css.structure: ../themes/base/tabs.css //>>css.theme: ../themes/base/theme.css ( function( factory ) { if ( typeof define === "function" && define.amd ) { // AMD. Register as an anonymous module. define( [ "jquery", "../escape-selector", "../keycode", "../safe-active-element", "../unique-id", "../version", "../widget" ], factory ); } else { // Browser globals factory( jQuery ); } }( function( $ ) { $.widget( "ui.tabs", { version: "@VERSION", delay: 300, options: { active: null, classes: { "ui-tabs": "ui-corner-all", "ui-tabs-nav": "ui-corner-all", "ui-tab": "ui-corner-top", "ui-tabs-panel": "ui-corner-bottom" }, collapsible: false, event: "click", heightStyle: "content", hide: null, show: null, // callbacks activate: null, beforeActivate: null, beforeLoad: null, load: null }, _isLocal: ( function() { var rhash = /#.*$/; return function( anchor ) { var anchorUrl, locationUrl; anchorUrl = anchor.href.replace( rhash, "" ); locationUrl = location.href.replace( rhash, "" ); // decoding may throw an error if the URL isn't UTF-8 (#9518) try { anchorUrl = decodeURIComponent( anchorUrl ); } catch ( error ) {} try { locationUrl = decodeURIComponent( locationUrl ); } catch ( error ) {} return anchor.hash.length > 1 && anchorUrl === locationUrl; }; } )(), _create: function() { var that = this, options = this.options; this.running = false; this._addClass( "ui-tabs", "ui-widget ui-widget-content" ); this._toggleClass( "ui-tabs-collapsible", null, options.collapsible ); this._processTabs(); options.active = this._initialActive(); // Take disabling tabs via class attribute from HTML // into account and update option properly. if ( $.isArray( options.disabled ) ) { options.disabled = $.unique( options.disabled.concat( $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) { return that.tabs.index( li ); } ) ) ).sort(); } // check for length avoids error when initializing empty list if ( this.options.active !== false && this.anchors.length ) { this.active = this._findActive( options.active ); } else { this.active = $(); } this._refresh(); if ( this.active.length ) { this.load( options.active ); } }, _initialActive: function() { var active = this.options.active, collapsible = this.options.collapsible, locationHash = location.hash.substring( 1 ); if ( active === null ) { // check the fragment identifier in the URL if ( locationHash ) { this.tabs.each( function( i, tab ) { if ( $( tab ).attr( "aria-controls" ) === locationHash ) { active = i; return false; } } ); } // check for a tab marked active via a class if ( active === null ) { active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) ); } // no active tab, set to false if ( active === null || active === -1 ) { active = this.tabs.length ? 0 : false; } } // handle numbers: negative, out of range if ( active !== false ) { active = this.tabs.index( this.tabs.eq( active ) ); if ( active === -1 ) { active = collapsible ? false : 0; } } // don't allow collapsible: false and active: false if ( !collapsible && active === false && this.anchors.length ) { active = 0; } return active; }, _getCreateEventData: function() { return { tab: this.active, panel: !this.active.length ? $() : this._getPanelForTab( this.active ) }; }, _tabKeydown: function( event ) { var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ), selectedIndex = this.tabs.index( focusedTab ), goingForward = true; if ( this._handlePageNav( event ) ) { return; } switch ( event.keyCode ) { case $.ui.keyCode.RIGHT: case $.ui.keyCode.DOWN: selectedIndex++; break; case $.ui.keyCode.UP: case $.ui.keyCode.LEFT: goingForward = false; selectedIndex--; break; case $.ui.keyCode.END: selectedIndex = this.anchors.length - 1; break; case $.ui.keyCode.HOME: selectedIndex = 0; break; case $.ui.keyCode.SPACE: // Activate only, no collapsing event.preventDefault(); clearTimeout( this.activating ); this._activate( selectedIndex ); return; case $.ui.keyCode.ENTER: // Toggle (cancel delayed activation, allow collapsing) event.preventDefault(); clearTimeout( this.activating ); // Determine if we should collapse or activate this._activate( selectedIndex === this.options.active ? false : selectedIndex ); return; default: return; } // Focus the appropriate tab, based on which key was pressed event.preventDefault(); clearTimeout( this.activating ); selectedIndex = this._focusNextTab( selectedIndex, goingForward ); // Navigating with control/command key will prevent automatic activation if ( !event.ctrlKey && !event.metaKey ) { // Update aria-selected immediately so that AT think the tab is already selected. // Otherwise AT may confuse the user by stating that they need to activate the tab, // but the tab will already be activated by the time the announcement finishes. focusedTab.attr( "aria-selected", "false" ); this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" ); this.activating = this._delay( function() { this.option( "active", selectedIndex ); }, this.delay ); } }, _panelKeydown: function( event ) { if ( this._handlePageNav( event ) ) { return; } // Ctrl+up moves focus to the current tab if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { event.preventDefault(); this.active.trigger( "focus" ); } }, // Alt+page up/down moves focus to the previous/next tab (and activates) _handlePageNav: function( event ) { if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { this._activate( this._focusNextTab( this.options.active - 1, false ) ); return true; } if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { this._activate( this._focusNextTab( this.options.active + 1, true ) ); return true; } }, _findNextTab: function( index, goingForward ) { var lastTabIndex = this.tabs.length - 1; function constrain() { if ( index > lastTabIndex ) { index = 0; } if ( index < 0 ) { index = lastTabIndex; } return index; } while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { index = goingForward ? index + 1 : index - 1; } return index; }, _focusNextTab: function( index, goingForward ) { index = this._findNextTab( index, goingForward ); this.tabs.eq( index ).trigger( "focus" ); return index; }, _setOption: function( key, value ) { if ( key === "active" ) { // _activate() will handle invalid values and update this.options this._activate( value ); return; } if ( key === "disabled" ) { // don't use the widget factory's disabled handling this._setupDisabled( value ); return; } this._super( key, value ); if ( key === "collapsible" ) { this._toggleClass( "ui-tabs-collapsible", null, value ); // Setting collapsible: false while collapsed; open first panel if ( !value && this.options.active === false ) { this._activate( 0 ); } } if ( key === "event" ) { this._setupEvents( value ); } if ( key === "heightStyle" ) { this._setupHeightStyle( value ); } }, _sanitizeSelector: function( hash ) { return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; }, refresh: function() { var options = this.options, lis = this.tablist.children( ":has(a[href])" ); // get disabled tabs from class attribute from HTML // this will get converted to a boolean if needed in _refresh() options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) { return lis.index( tab ); } ); this._processTabs(); // was collapsed or no tabs if ( options.active === false || !this.anchors.length ) { options.active = false; this.active = $(); // was active, but active tab is gone } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) { // all remaining tabs are disabled if ( this.tabs.length === options.disabled.length ) { options.active = false; this.active = $(); // activate previous tab } else { this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) ); } // was active, active tab still exists } else { // make sure active index is correct options.active = this.tabs.index( this.active ); } this._refresh(); }, _refresh: function() { this._setupDisabled( this.options.disabled ); this._setupEvents( this.options.event ); this._setupHeightStyle( this.options.heightStyle ); this.tabs.not( this.active ).attr( { "aria-selected": "false", "aria-expanded": "false", tabIndex: -1 } ); this.panels.not( this._getPanelForTab( this.active ) ) .hide() .attr( { "aria-hidden": "true" } ); // Make sure one tab is in the tab order if ( !this.active.length ) { this.tabs.eq( 0 ).attr( "tabIndex", 0 ); } else { this.active .attr( { "aria-selected": "true", "aria-expanded": "true", tabIndex: 0 } ); this._addClass( this.active, "ui-tabs-active", "ui-state-active" ); this._getPanelForTab( this.active ) .show() .attr( { "aria-hidden": "false" } ); } }, _processTabs: function() { var that = this, prevTabs = this.tabs, prevAnchors = this.anchors, prevPanels = this.panels; this.tablist = this._getList().attr( "role", "tablist" ); this._addClass( this.tablist, "ui-tabs-nav", "ui-helper-reset ui-helper-clearfix ui-widget-header" ); // Prevent users from focusing disabled tabs via click this.tablist .on( "mousedown" + this.eventNamespace, "> li", function( event ) { if ( $( this ).is( ".ui-state-disabled" ) ) { event.preventDefault(); } } ) // support: IE <9 // Preventing the default action in mousedown doesn't prevent IE // from focusing the element, so if the anchor gets focused, blur. // We don't have to worry about focusing the previously focused // element since clicking on a non-focusable element should focus // the body anyway. .on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() { if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { this.blur(); } } ); this.tabs = this.tablist.find( "> li:has(a[href])" ) .attr( { role: "tab", tabIndex: -1 } ); this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" ); this.anchors = this.tabs.map( function() { return $( "a", this )[ 0 ]; } ) .attr( { role: "presentation", tabIndex: -1 } ); this._addClass( this.anchors, "ui-tabs-anchor" ); this.panels = $(); this.anchors.each( function( i, anchor ) { var selector, panel, panelId, anchorId = $( anchor ).uniqueId().attr( "id" ), tab = $( anchor ).closest( "li" ), originalAriaControls = tab.attr( "aria-controls" ); // inline tab if ( that._isLocal( anchor ) ) { selector = anchor.hash; panelId = selector.substring( 1 ); panel = that.element.find( that._sanitizeSelector( selector ) ); // remote tab } else { // If the tab doesn't already have aria-controls, // generate an id by using a throw-away element panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id; selector = "#" + panelId; panel = that.element.find( selector ); if ( !panel.length ) { panel = that._createPanel( panelId ); panel.insertAfter( that.panels[ i - 1 ] || that.tablist ); } panel.attr( "aria-live", "polite" ); } if ( panel.length ) { that.panels = that.panels.add( panel ); } if ( originalAriaControls ) { tab.data( "ui-tabs-aria-controls", originalAriaControls ); } tab.attr( { "aria-controls": panelId, "aria-labelledby": anchorId } ); panel.attr( "aria-labelledby", anchorId ); } ); this.panels.attr( "role", "tabpanel" ); this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" ); // Avoid memory leaks (#10056) if ( prevTabs ) { this._off( prevTabs.not( this.tabs ) ); this._off( prevAnchors.not( this.anchors ) ); this._off( prevPanels.not( this.panels ) ); } }, // allow overriding how to find the list for rare usage scenarios (#7715) _getList: function() { return this.tablist || this.element.find( "ol, ul" ).eq( 0 ); }, _createPanel: function( id ) { return $( "