/*! * jQuery UI Tabs @VERSION * * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Tabs * * Depends: * jquery.ui.core.js * jquery.ui.widget.js */ (function( $, undefined ) { var tabId = 0, rhash = /#.*$/; function getNextTabId() { return ++tabId; } function isLocal( anchor ) { // clone the node to work around IE 6 not normalizing the href property // if it's manually set, i.e., a.href = "#foo" kills the normalization anchor = anchor.cloneNode( false ); return anchor.hash.length > 1 && anchor.href.replace( rhash, "" ) === location.href.replace( rhash, "" ); } $.widget( "ui.tabs", { version: "@VERSION", options: { active: null, collapsible: false, event: "click", hide: null, show: null, // callbacks activate: null, beforeActivate: null, beforeLoad: null, load: null }, _create: function() { var panel, that = this, options = that.options, active = options.active; that.running = false; that.element.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ); that._processTabs(); if ( active === null ) { // check the fragment identifier in the URL if ( location.hash ) { that.anchors.each(function( i, tab ) { if ( tab.hash === location.hash ) { active = i; return false; } }); } // check for a tab marked active via a class if ( active === null ) { active = that.lis.filter( ".ui-tabs-active" ).index(); } // no active tab, set to false if ( active === null || active === -1 ) { active = that.lis.length ? 0 : false; } } // handle numbers: negative, out of range if ( active !== false ) { active = this.lis.eq( active ).index(); if ( active === -1 ) { active = options.collapsible ? false : 0; } } options.active = active; // don't allow collapsible: false and active: false if ( !options.collapsible && options.active === false && this.anchors.length ) { options.active = 0; } // 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.lis.filter( ".ui-state-disabled" ), function( li ) { return that.lis.index( li ); }) ) ).sort(); } this._refresh(); // highlight selected tab this.panels.hide(); this.lis.removeClass( "ui-tabs-active ui-state-active" ); // check for length avoids error when initializing empty list if ( options.active !== false && this.anchors.length ) { this.active = this._findActive( options.active ); panel = that._getPanelForTab( this.active ); panel.show(); this.lis.eq( options.active ).addClass( "ui-tabs-active ui-state-active" ); this.load( options.active ); } else { this.active = $(); } }, _getCreateEventData: function() { return { tab: this.active, panel: !this.active.length ? $() : this._getPanelForTab( this.active ) }; }, _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); // setting collapsible: false while collapsed; open first panel if ( key === "collapsible" && !value && this.options.active === false ) { this._activate( 0 ); } if ( key === "event" ) { this._setupEvents( value ); } }, _tabId: function( a ) { return $( a ).attr( "aria-controls" ) || "ui-tabs-" + getNextTabId(); }, _sanitizeSelector: function( hash ) { return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; }, refresh: function() { var next, options = this.options, lis = this.list.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(); this._refresh(); this.panels.not( this._getPanelForTab( this.active ) ).hide(); // 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.list[ 0 ], this.active[ 0 ] ) ) { // activate previous tab next = options.active - 1; this._activate( next >= 0 ? next : 0 ); // was active, active tab still exists } else { // make sure active index is correct options.active = this.anchors.index( this.active ); } }, _refresh: function() { var options = this.options; this.element.toggleClass( "ui-tabs-collapsible", options.collapsible ); this.list.addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ); this.lis.addClass( "ui-state-default ui-corner-top" ); this.panels.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ); this._setupDisabled( options.disabled ); this._setupEvents( options.event ); // remove all handlers, may run on existing tabs this.lis.unbind( ".tabs" ); this._focusable( this.lis ); this._hoverable( this.lis ); }, _processTabs: function() { var that = this; this.list = this._getList(); this.lis = this.list.find( "> li:has(a[href])" ); this.anchors = this.lis.map(function() { return $( "a", this )[ 0 ]; }); this.panels = $(); this.anchors.each(function( i, a ) { var selector, panel, id; // inline tab if ( isLocal( a ) ) { selector = a.hash; panel = that.element.find( that._sanitizeSelector( selector ) ); // remote tab } else { id = that._tabId( a ); selector = "#" + id; panel = that.element.find( selector ); if ( !panel.length ) { panel = that._createPanel( id ); panel.insertAfter( that.panels[ i - 1 ] || that.list ); } } if ( panel.length) { that.panels = that.panels.add( panel ); } $( a ).attr( "aria-controls", selector.substring( 1 ) ); }); }, // allow overriding how to find the list for rare usage scenarios (#7715) _getList: function() { return this.element.find( "ol,ul" ).eq( 0 ); }, _createPanel: function( id ) { return $( "
" ) .attr( "id", id ) .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) .data( "ui-tabs-destroy", true ); }, _setupDisabled: function( disabled ) { if ( $.isArray( disabled ) ) { if ( !disabled.length ) { disabled = false; } else if ( disabled.length === this.anchors.length ) { disabled = true; } } // disable tabs for ( var i = 0, li; ( li = this.lis[ i ] ); i++ ) { $( li ).toggleClass( "ui-state-disabled", ( disabled === true || $.inArray( i, disabled ) !== -1 ) ); } this.options.disabled = disabled; }, _setupEvents: function( event ) { // attach tab event handler, unbind to avoid duplicates from former tabifying... this.anchors.unbind( ".tabs" ); // TODO: use event delegation via _bind() if ( event ) { this.anchors.bind( event.split( " " ).join( ".tabs " ) + ".tabs", $.proxy( this, "_eventHandler" ) ); } // TODO: use event delegation via _bind() // disable click in any case this.anchors.bind( "click.tabs", function( event ){ event.preventDefault(); }); }, _eventHandler: function( event ) { var that = this, options = that.options, active = that.active, clicked = $( event.currentTarget ), clickedIsActive = clicked[ 0 ] === active[ 0 ], collapsing = clickedIsActive && options.collapsible, toShow = collapsing ? $() : that._getPanelForTab( clicked ), toHide = !active.length ? $() : that._getPanelForTab( active ), tab = clicked.closest( "li" ), eventData = { oldTab: active, oldPanel: toHide, newTab: collapsing ? $() : clicked, newPanel: toShow }; event.preventDefault(); if ( tab.hasClass( "ui-state-disabled" ) || // tab is already loading tab.hasClass( "ui-tabs-loading" ) || // can't switch durning an animation that.running || // click on active header, but not collapsible ( clickedIsActive && !options.collapsible ) || // allow canceling activation ( that._trigger( "beforeActivate", event, eventData ) === false ) ) { clicked[ 0 ].blur(); return; } options.active = collapsing ? false : that.anchors.index( clicked ); that.active = clickedIsActive ? $() : clicked; if ( that.xhr ) { that.xhr.abort(); } if ( !toHide.length && !toShow.length ) { jQuery.error( "jQuery UI Tabs: Mismatching fragment identifier." ); } if ( toShow.length ) { // TODO make passing in node possible that.load( that.anchors.index( clicked ), event ); clicked[ 0 ].blur(); } that._toggle( event, eventData ); }, // handles show/hide for selecting tabs _toggle: function( event, eventData ) { var that = this, toShow = eventData.newPanel, toHide = eventData.oldPanel; this.running = true; function complete() { that.running = false; that._trigger( "activate", event, eventData ); } function show() { eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" ); if ( toShow.length && that.options.show ) { that._show( toShow, that.options.show, complete ); } else { toShow.show(); complete(); } } // start out by hiding, then showing, then completing if ( toHide.length && this.options.hide ) { this._hide( toHide, this.options.hide, function() { eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); show(); }); } else { eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); toHide.hide(); show(); } }, _activate: function( index ) { var active = this._findActive( index )[ 0 ]; // trying to activate the already active panel if ( active === this.active[ 0 ] ) { return; } // trying to collapse, simulate a click on the current active header active = active || this.active[ 0 ]; this._eventHandler({ target: active, currentTarget: active, preventDefault: $.noop }); }, _findActive: function( selector ) { return typeof selector === "number" ? this.anchors.eq( selector ) : typeof selector === "string" ? this.anchors.filter( "[href$='" + selector + "']" ) : $(); }, _getIndex: function( index ) { // meta-function to give users option to provide a href string instead of a numerical index. // also sanitizes numerical indexes to valid values. if ( typeof index === "string" ) { index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) ); } return index; }, _destroy: function() { if ( this.xhr ) { this.xhr.abort(); } this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" ); this.list.removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ); this.anchors .unbind( ".tabs" ) .removeData( "href.tabs" ) .removeData( "load.tabs" ); this.lis.unbind( ".tabs" ).add( this.panels ).each(function() { if ( $.data( this, "ui-tabs-destroy" ) ) { $( this ).remove(); } else { $( this ).removeClass([ "ui-state-default", "ui-corner-top", "ui-tabs-active", "ui-state-active", "ui-state-disabled", "ui-tabs-panel", "ui-widget-content", "ui-corner-bottom" ].join( " " ) ); } }); return this; }, enable: function( index ) { var disabled = this.options.disabled; if ( disabled === false ) { return; } if ( index === undefined ) { disabled = false; } else { index = this._getIndex( index ); if ( $.isArray( disabled ) ) { disabled = $.map( disabled, function( num ) { return num !== index ? num : null; }); } else { disabled = $.map( this.lis, function( li, num ) { return num !== index ? num : null; }); } } this._setupDisabled( disabled ); }, disable: function( index ) { var disabled = this.options.disabled; if ( disabled === true ) { return; } if ( index === undefined ) { disabled = true; } else { index = this._getIndex( index ); if ( $.inArray( index, disabled ) !== -1 ) { return; } if ( $.isArray( disabled ) ) { disabled = $.merge( [ index ], disabled ).sort(); } else { disabled = [ index ]; } } this._setupDisabled( disabled ); }, load: function( index, event ) { index = this._getIndex( index ); var that = this, anchor = this.anchors.eq( index ), panel = that._getPanelForTab( anchor ), eventData = { tab: anchor, panel: panel }; // not remote if ( isLocal( anchor[ 0 ] ) ) { return; } this.xhr = $.ajax({ url: anchor.attr( "href" ), beforeSend: function( jqXHR, settings ) { return that._trigger( "beforeLoad", event, $.extend( { jqXHR : jqXHR, ajaxSettings: settings }, eventData ) ); } }); if ( this.xhr ) { this.lis.eq( index ).addClass( "ui-tabs-loading" ); this.xhr .success(function( response ) { // TODO: IE resolves cached XHRs immediately // remove when core #10467 is fixed setTimeout(function() { panel.html( response ); that._trigger( "load", event, eventData ); }, 1 ); }) .complete(function( jqXHR, status ) { // TODO: IE resolves cached XHRs immediately // remove when core #10467 is fixed setTimeout(function() { if ( status === "abort" ) { that.panels.stop( false, true ); } that.lis.eq( index ).removeClass( "ui-tabs-loading" ); if ( jqXHR === that.xhr ) { delete that.xhr; } }, 1 ); }); } }, _getPanelForTab: function( tab ) { var id = $( tab ).attr( "aria-controls" ); return this.element.find( this._sanitizeSelector( "#" + id ) ); } }); // DEPRECATED if ( $.uiBackCompat !== false ) { // helper method for a lot of the back compat extensions $.ui.tabs.prototype._ui = function( tab, panel ) { return { tab: tab, panel: panel, index: this.anchors.index( tab ) }; }; // url method $.widget( "ui.tabs", $.ui.tabs, { url: function( index, url ) { this.anchors.eq( index ).attr( "href", url ); } }); // ajaxOptions and cache options $.widget( "ui.tabs", $.ui.tabs, { options: { ajaxOptions: null, cache: false }, _create: function() { this._super(); var that = this; this.element.bind( "tabsbeforeload.tabs", function( event, ui ) { // tab is already cached if ( $.data( ui.tab[ 0 ], "cache.tabs" ) ) { event.preventDefault(); return; } $.extend( ui.ajaxSettings, that.options.ajaxOptions, { error: function( xhr, s, e ) { try { // Passing index avoid a race condition when this method is // called after the user has selected another tab. // Pass the anchor that initiated this request allows // loadError to manipulate the tab content panel via $(a.hash) that.options.ajaxOptions.error( xhr, s, ui.tab.closest( "li" ).index(), ui.tab[ 0 ] ); } catch ( e ) {} } }); ui.jqXHR.success(function() { if ( that.options.cache ) { $.data( ui.tab[ 0 ], "cache.tabs", true ); } }); }); }, _setOption: function( key, value ) { // reset cache if switching from cached to not cached if ( key === "cache" && value === false ) { this.anchors.removeData( "cache.tabs" ); } this._super( key, value ); }, _destroy: function() { this.anchors.removeData( "cache.tabs" ); this._super(); }, url: function( index, url ){ this.anchors.eq( index ).removeData( "cache.tabs" ); this._superApply( arguments ); } }); // abort method $.widget( "ui.tabs", $.ui.tabs, { abort: function() { if ( this.xhr ) { this.xhr.abort(); } } }); // spinner $.widget( "ui.tabs", $.ui.tabs, { options: { spinner: "Loading…" }, _create: function() { this._super(); this._bind({ tabsbeforeload: function( event, ui ) { if ( !this.options.spinner ) { return; } var span = ui.tab.find( "span" ), html = span.html(); span.html( this.options.spinner ); ui.jqXHR.complete(function() { span.html( html ); }); } }); } }); // enable/disable events $.widget( "ui.tabs", $.ui.tabs, { options: { enable: null, disable: null }, enable: function( index ) { var options = this.options, trigger; if ( index && options.disabled === true || ( $.isArray( options.disabled ) && $.inArray( index, options.disabled ) !== -1 ) ) { trigger = true; } this._superApply( arguments ); if ( trigger ) { this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); } }, disable: function( index ) { var options = this.options, trigger; if ( index && options.disabled === false || ( $.isArray( options.disabled ) && $.inArray( index, options.disabled ) === -1 ) ) { trigger = true; } this._superApply( arguments ); if ( trigger ) { this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); } } }); // add/remove methods and events $.widget( "ui.tabs", $.ui.tabs, { options: { add: null, remove: null, tabTemplate: "
  • #{label}
  • " }, add: function( url, label, index ) { if ( index === undefined ) { index = this.anchors.length; } var doInsertAfter, panel, options = this.options, li = $( options.tabTemplate .replace( /#\{href\}/g, url ) .replace( /#\{label\}/g, label ) ), id = !url.indexOf( "#" ) ? url.replace( "#", "" ) : this._tabId( li.find( "a" )[ 0 ] ); li.addClass( "ui-state-default ui-corner-top" ).data( "ui-tabs-destroy", true ); li.find( "a" ).attr( "aria-controls", id ); doInsertAfter = index >= this.lis.length; // try to find an existing element before creating a new one panel = this.element.find( "#" + id ); if ( !panel.length ) { panel = this._createPanel( id ); if ( doInsertAfter ) { if ( index > 0 ) { panel.insertAfter( this.panels.eq( -1 ) ); } else { panel.appendTo( this.element ); } } else { panel.insertBefore( this.panels[ index ] ); } } panel.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ).hide(); if ( doInsertAfter ) { li.appendTo( this.list ); } else { li.insertBefore( this.lis[ index ] ); } options.disabled = $.map( options.disabled, function( n ) { return n >= index ? ++n : n; }); this.refresh(); if ( this.lis.length === 1 && options.active === false ) { this.option( "active", 0 ); } this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); return this; }, remove: function( index ) { index = this._getIndex( index ); var options = this.options, tab = this.lis.eq( index ).remove(), panel = this._getPanelForTab( tab.find( "a[aria-controls]" ) ).remove(); // If selected tab was removed focus tab to the right or // in case the last tab was removed the tab to the left. // We check for more than 2 tabs, because if there are only 2, // then when we remove this tab, there will only be one tab left // so we don't need to detect which tab to activate. if ( tab.hasClass( "ui-tabs-active" ) && this.anchors.length > 2 ) { this._activate( index + ( index + 1 < this.anchors.length ? 1 : -1 ) ); } options.disabled = $.map( $.grep( options.disabled, function( n ) { return n !== index; }), function( n ) { return n >= index ? --n : n; }); this.refresh(); this._trigger( "remove", null, this._ui( tab.find( "a" )[ 0 ], panel[ 0 ] ) ); return this; } }); // length method $.widget( "ui.tabs", $.ui.tabs, { length: function() { return this.anchors.length; } }); // panel ids (idPrefix option + title attribute) $.widget( "ui.tabs", $.ui.tabs, { options: { idPrefix: "ui-tabs-" }, _tabId: function( a ) { return $( a ).attr( "aria-controls" ) || a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF\-]/g, "" ) || this.options.idPrefix + getNextTabId(); } }); // _createPanel method $.widget( "ui.tabs", $.ui.tabs, { options: { panelTemplate: "
    " }, _createPanel: function( id ) { return $( this.options.panelTemplate ) .attr( "id", id ) .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) .data( "ui-tabs-destroy", true ); } }); // selected option $.widget( "ui.tabs", $.ui.tabs, { _create: function() { var options = this.options; if ( options.active === null && options.selected !== undefined ) { options.active = options.selected === -1 ? false : options.selected; } this._super(); options.selected = options.active; if ( options.selected === false ) { options.selected = -1; } }, _setOption: function( key, value ) { if ( key !== "selected" ) { return this._super( key, value ); } var options = this.options; this._super( "active", value === -1 ? false : value ); options.selected = options.active; if ( options.selected === false ) { options.selected = -1; } }, _eventHandler: function( event ) { this._superApply( arguments ); this.options.selected = this.options.active; if ( this.options.selected === false ) { this.options.selected = -1; } } }); // show and select event $.widget( "ui.tabs", $.ui.tabs, { options: { show: null, select: null }, _create: function() { this._super(); if ( this.options.active !== false ) { this._trigger( "show", null, this._ui( this.active[ 0 ], this._getPanelForTab( this.active )[ 0 ] ) ); } }, _trigger: function( type, event, data ) { var ret = this._superApply( arguments ); if ( !ret ) { return false; } if ( type === "beforeActivate" && data.newTab.length ) { ret = this._super( "select", event, { tab: data.newTab[ 0], panel: data.newPanel[ 0 ], index: data.newTab.closest( "li" ).index() }); } else if ( type === "activate" && data.newTab.length ) { ret = this._super( "show", event, { tab: data.newTab[ 0 ], panel: data.newPanel[ 0 ], index: data.newTab.closest( "li" ).index() }); } return ret; } }); // select method $.widget( "ui.tabs", $.ui.tabs, { select: function( index ) { index = this._getIndex( index ); if ( index === -1 ) { if ( this.options.collapsible && this.options.selected !== -1 ) { index = this.options.selected; } else { return; } } this.anchors.eq( index ).trigger( this.options.event + ".tabs" ); } }); // cookie option (function() { var listId = 0; $.widget( "ui.tabs", $.ui.tabs, { options: { cookie: null // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true } }, _create: function() { var options = this.options, active; if ( options.active == null && options.cookie ) { active = parseInt( this._cookie(), 10 ); if ( active === -1 ) { active = false; } options.active = active; } this._super(); }, _cookie: function( active ) { var cookie = [ this.cookie || ( this.cookie = this.options.cookie.name || "ui-tabs-" + (++listId) ) ]; if ( arguments.length ) { cookie.push( active === false ? -1 : active ); cookie.push( this.options.cookie ); } return $.cookie.apply( null, cookie ); }, _refresh: function() { this._super(); if ( this.options.cookie ) { this._cookie( this.options.active, this.options.cookie ); } }, _eventHandler: function( event ) { this._superApply( arguments ); if ( this.options.cookie ) { this._cookie( this.options.active, this.options.cookie ); } }, _destroy: function() { this._super(); if ( this.options.cookie ) { this._cookie( null, this.options.cookie ); } } }); })(); // load event $.widget( "ui.tabs", $.ui.tabs, { _trigger: function( type, event, data ) { var _data = $.extend( {}, data ); if ( type === "load" ) { _data.panel = _data.panel[ 0 ]; _data.tab = _data.tab[ 0 ]; } return this._super( type, event, _data ); } }); // fx option // The new animation options (show, hide) conflict with the old show callback. // The old fx option wins over show/hide anyway (always favor back-compat). // If a user wants to use the new animation API, they must give up the old API. $.widget( "ui.tabs", $.ui.tabs, { options: { fx: null // e.g. { height: "toggle", opacity: "toggle", duration: 200 } }, _getFx: function() { var hide, show, fx = this.options.fx; if ( fx ) { if ( $.isArray( fx ) ) { hide = fx[ 0 ]; show = fx[ 1 ]; } else { hide = show = fx; } } return fx ? { show: show, hide: hide } : null; }, _toggle: function( event, eventData ) { var that = this, toShow = eventData.newPanel, toHide = eventData.oldPanel, fx = this._getFx(); if ( !fx ) { return this._super( event, eventData ); } that.running = true; function complete() { that.running = false; that._trigger( "activate", event, eventData ); } function show() { eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" ); if ( toShow.length && fx.show ) { toShow .animate( fx.show, fx.show.duration, function() { complete(); }); } else { toShow.show(); complete(); } } // start out by hiding, then showing, then completing if ( toHide.length && fx.hide ) { toHide.animate( fx.hide, fx.hide.duration, function() { eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); show(); }); } else { eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); toHide.hide(); show(); } } }); } })( jQuery );