Widget: Allow redefining a widget after other widgets have inherited from it.

This commit is contained in:
Scott González 2012-02-01 16:59:26 -05:00
parent e496cde384
commit 8cd4a8330c
4 changed files with 220 additions and 171 deletions

View File

@ -281,7 +281,7 @@ test( "enable", function() {
var element = $( "#tabs1" ).tabs({ var element = $( "#tabs1" ).tabs({
disabled: [ 0, 1 ], disabled: [ 0, 1 ],
enable: function ( event, ui ) { enable: function( event, ui ) {
equals( ui.tab, element.find( ".ui-tabs-nav a" )[ 1 ], "ui.tab" ); equals( ui.tab, element.find( ".ui-tabs-nav a" )[ 1 ], "ui.tab" );
equals( ui.panel, element.find( ".ui-tabs-panel" )[ 1 ], "ui.panel" ); equals( ui.panel, element.find( ".ui-tabs-panel" )[ 1 ], "ui.panel" );
equals( ui.index, 1, "ui.index" ); equals( ui.index, 1, "ui.index" );
@ -296,10 +296,10 @@ test( "disable", function() {
expect( 3 ); expect( 3 );
var element = $( "#tabs1" ).tabs({ var element = $( "#tabs1" ).tabs({
disable: function ( event, ui ) { disable: function( event, ui ) {
equals( ui.tab, element.find( ".ui-tabs-nav a" )[ 1 ], "ui.tab" ); equals( ui.tab, element.find( ".ui-tabs-nav a" )[ 1 ], "ui.tab" );
equals( ui.panel, element.find( ".ui-tabs-panel" )[ 1 ], "ui.panel" ); equals( ui.panel, element.find( ".ui-tabs-panel" )[ 1 ], "ui.panel" );
equals( ui.index, 1, "ui.index" ); equals( ui.index, 1, "ui.index" );
} }
}); });
element.tabs( "disable", 1 ); element.tabs( "disable", 1 );

View File

@ -1050,11 +1050,54 @@ test( "redefine", function() {
} }
}); });
var instance = new $.ui.testWidget(); var instance = new $.ui.testWidget({});
instance.method( "foo" ); instance.method( "foo" );
equal( $.ui.testWidget.foo, "bar", "static properties remain" ); equal( $.ui.testWidget.foo, "bar", "static properties remain" );
}); });
test( "redefine deep prototype chain", function() {
expect( 8 );
$.widget( "ui.testWidget", {
method: function( str ) {
strictEqual( this, instance, "original invoked with correct this" );
equal( str, "level 4", "original invoked with correct parameter" );
}
});
$.widget( "ui.testWidget2", $.ui.testWidget, {
method: function( str ) {
strictEqual( this, instance, "testWidget2 invoked with correct this" );
equal( str, "level 2", "testWidget2 invoked with correct parameter" );
this._super( "level 3" );
}
});
$.widget( "ui.testWidget3", $.ui.testWidget2, {
method: function( str ) {
strictEqual( this, instance, "testWidget3 invoked with correct this" );
equal( str, "level 1", "testWidget3 invoked with correct parameter" );
this._super( "level 2" );
}
});
// redefine testWidget after other widgets have inherited from it
// this tests whether the inheriting widgets get updated prototype chains
$.widget( "ui.testWidget", $.ui.testWidget, {
method: function( str ) {
strictEqual( this, instance, "new invoked with correct this" );
equal( str, "level 3", "new invoked with correct parameter" );
this._super( "level 4" );
}
});
// redefine testWidget3 after it has been automatically redefined
// this tests whether we properly handle _super() when the topmost prototype
// doesn't have the method defined
$.widget( "ui.testWidget3", $.ui.testWidget3, {} );
var instance = new $.ui.testWidget3({});
instance.method( "level 1" );
delete $.ui.testWidget3;
delete $.ui.testWidget2;
});
asyncTest( "_delay", function() { asyncTest( "_delay", function() {
expect( 6 ); expect( 6 );
var order = 0, var order = 0,

262
ui/jquery.ui.tabs.js vendored
View File

@ -596,86 +596,79 @@ if ( $.uiBackCompat !== false ) {
}; };
// url method // url method
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
prototype.url = function( index, url ) { url: function( index, url ) {
this.anchors.eq( index ).attr( "href", url ); this.anchors.eq( index ).attr( "href", url );
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// ajaxOptions and cache options // ajaxOptions and cache options
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
$.extend( prototype.options, { options: {
ajaxOptions: null, ajaxOptions: null,
cache: false cache: false
}); },
var _create = prototype._create, _create: function() {
_setOption = prototype._setOption, this._super();
_destroy = prototype._destroy,
oldurl = prototype.url || $.noop;
$.extend( prototype, { var self = this;
_create: function() {
_create.call( this );
var self = this; this.element.bind( "tabsbeforeload.tabs", function( event, ui ) {
// tab is already cached
this.element.bind( "tabsbeforeload.tabs", function( event, ui ) { if ( $.data( ui.tab[ 0 ], "cache.tabs" ) ) {
// tab is already cached event.preventDefault();
if ( $.data( ui.tab[ 0 ], "cache.tabs" ) ) { return;
event.preventDefault();
return;
}
$.extend( ui.ajaxSettings, self.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)
self.options.ajaxOptions.error( xhr, s, ui.tab.closest( "li" ).index(), ui.tab[ 0 ] );
}
catch ( e ) {}
}
});
ui.jqXHR.success(function() {
if ( self.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" );
} }
_setOption.apply( this, arguments );
},
_destroy: function() { $.extend( ui.ajaxSettings, self.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)
self.options.ajaxOptions.error( xhr, s, ui.tab.closest( "li" ).index(), ui.tab[ 0 ] );
}
catch ( e ) {}
}
});
ui.jqXHR.success(function() {
if ( self.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.anchors.removeData( "cache.tabs" );
_destroy.call( this );
},
url: function( index, url ){
this.anchors.eq( index ).removeData( "cache.tabs" );
oldurl.apply( this, arguments );
} }
}); this._super( key, value );
}( jQuery, jQuery.ui.tabs.prototype ) ); },
_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 // abort method
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
prototype.abort = function() { abort: function() {
if ( this.xhr ) { if ( this.xhr ) {
this.xhr.abort(); this.xhr.abort();
} }
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// spinner // spinner
$.widget( "ui.tabs", $.ui.tabs, { $.widget( "ui.tabs", $.ui.tabs, {
@ -702,16 +695,13 @@ if ( $.uiBackCompat !== false ) {
}); });
// enable/disable events // enable/disable events
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
$.extend( prototype.options, { options: {
enable: null, enable: null,
disable: null disable: null
}); },
var enable = prototype.enable, enable: function( index ) {
disable = prototype.disable;
prototype.enable = function( index ) {
var options = this.options, var options = this.options,
trigger; trigger;
@ -720,14 +710,14 @@ if ( $.uiBackCompat !== false ) {
trigger = true; trigger = true;
} }
enable.apply( this, arguments ); this._superApply( arguments );
if ( trigger ) { if ( trigger ) {
this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
} }
}; },
prototype.disable = function( index ) { disable: function( index ) {
var options = this.options, var options = this.options,
trigger; trigger;
@ -736,23 +726,23 @@ if ( $.uiBackCompat !== false ) {
trigger = true; trigger = true;
} }
disable.apply( this, arguments ); this._superApply( arguments );
if ( trigger ) { if ( trigger ) {
this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
} }
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// add/remove methods and events // add/remove methods and events
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
$.extend( prototype.options, { options: {
add: null, add: null,
remove: null, remove: null,
tabTemplate: "<li><a href='#{href}'><span>#{label}</span></a></li>" tabTemplate: "<li><a href='#{href}'><span>#{label}</span></a></li>"
}); },
prototype.add = function( url, label, index ) { add: function( url, label, index ) {
if ( index === undefined ) { if ( index === undefined ) {
index = this.anchors.length; index = this.anchors.length;
} }
@ -803,9 +793,9 @@ if ( $.uiBackCompat !== false ) {
this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
return this; return this;
}; },
prototype.remove = function( index ) { remove: function( index ) {
index = this._getIndex( index ); index = this._getIndex( index );
var options = this.options, var options = this.options,
tab = this.lis.eq( index ).remove(), tab = this.lis.eq( index ).remove(),
@ -832,125 +822,117 @@ if ( $.uiBackCompat !== false ) {
this._trigger( "remove", null, this._ui( tab.find( "a" )[ 0 ], panel[ 0 ] ) ); this._trigger( "remove", null, this._ui( tab.find( "a" )[ 0 ], panel[ 0 ] ) );
return this; return this;
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// length method // length method
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
prototype.length = function() { length: function() {
return this.anchors.length; return this.anchors.length;
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// panel ids (idPrefix option + title attribute) // panel ids (idPrefix option + title attribute)
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
$.extend( prototype.options, { options: {
idPrefix: "ui-tabs-" idPrefix: "ui-tabs-"
}); },
var _tabId = prototype._tabId; _tabId: function( a ) {
prototype._tabId = function( a ) {
return $( a ).attr( "aria-controls" ) || return $( a ).attr( "aria-controls" ) ||
a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF-]/g, "" ) || a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF-]/g, "" ) ||
this.options.idPrefix + getNextTabId(); this.options.idPrefix + getNextTabId();
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// _createPanel method // _createPanel method
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
$.extend( prototype.options, { options: {
panelTemplate: "<div></div>" panelTemplate: "<div></div>"
}); },
var _createPanel = prototype._createPanel; _createPanel: function( id ) {
prototype._createPanel = function( id ) {
return $( this.options.panelTemplate ) return $( this.options.panelTemplate )
.attr( "id", id ) .attr( "id", id )
.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
.data( "destroy.tabs", true ); .data( "destroy.tabs", true );
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// selected option // selected option
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
var _create = prototype._create, _create: function() {
_setOption = prototype._setOption,
_eventHandler = prototype._eventHandler;
prototype._create = function() {
var options = this.options; var options = this.options;
if ( options.active === null && options.selected !== undefined ) { if ( options.active === null && options.selected !== undefined ) {
options.active = options.selected === -1 ? false : options.selected; options.active = options.selected === -1 ? false : options.selected;
} }
_create.call( this ); this._super();
options.selected = options.active; options.selected = options.active;
if ( options.selected === false ) { if ( options.selected === false ) {
options.selected = -1; options.selected = -1;
} }
}; },
prototype._setOption = function( key, value ) { _setOption: function( key, value ) {
if ( key !== "selected" ) { if ( key !== "selected" ) {
return _setOption.apply( this, arguments ); return this._super( key, value );
} }
var options = this.options; var options = this.options;
_setOption.call( this, "active", value === -1 ? false : value ); this._super( "active", value === -1 ? false : value );
options.selected = options.active; options.selected = options.active;
if ( options.selected === false ) { if ( options.selected === false ) {
options.selected = -1; options.selected = -1;
} }
}; },
prototype._eventHandler = function( event ) { _eventHandler: function( event ) {
_eventHandler.apply( this, arguments ); this._superApply( arguments );
this.options.selected = this.options.active; this.options.selected = this.options.active;
if ( this.options.selected === false ) { if ( this.options.selected === false ) {
this.options.selected = -1; this.options.selected = -1;
} }
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// show and select event // show and select event
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
$.extend( prototype.options, { options: {
show: null, show: null,
select: null select: null
}); },
var _create = prototype._create, _create: function() {
_trigger = prototype._trigger; this._super();
prototype._create = function() {
_create.call( this );
if ( this.options.active !== false ) { if ( this.options.active !== false ) {
this._trigger( "show", null, this._ui( this._trigger( "show", null, this._ui(
this.active[ 0 ], this._getPanelForTab( this.active )[ 0 ] ) ); this.active[ 0 ], this._getPanelForTab( this.active )[ 0 ] ) );
} }
}; },
prototype._trigger = function( type, event, data ) { _trigger: function( type, event, data ) {
var ret = _trigger.apply( this, arguments ); var ret = this._superApply( arguments );
if ( !ret ) { if ( !ret ) {
return false; return false;
} }
if ( type === "beforeActivate" && data.newTab.length ) { if ( type === "beforeActivate" && data.newTab.length ) {
ret = _trigger.call( this, "select", event, { ret = this._super( "select", event, {
tab: data.newTab[ 0], tab: data.newTab[ 0],
panel: data.newPanel[ 0 ], panel: data.newPanel[ 0 ],
index: data.newTab.closest( "li" ).index() index: data.newTab.closest( "li" ).index()
}); });
} else if ( type === "activate" && data.newTab.length ) { } else if ( type === "activate" && data.newTab.length ) {
ret = _trigger.call( this, "show", event, { ret = this._super( "show", event, {
tab: data.newTab[ 0 ], tab: data.newTab[ 0 ],
panel: data.newPanel[ 0 ], panel: data.newPanel[ 0 ],
index: data.newTab.closest( "li" ).index() index: data.newTab.closest( "li" ).index()
}); });
} }
}; return ret;
}( jQuery, jQuery.ui.tabs.prototype ) ); }
});
// select method // select method
(function( $, prototype ) { $.widget( "ui.tabs", $.ui.tabs, {
prototype.select = function( index ) { select: function( index ) {
index = this._getIndex( index ); index = this._getIndex( index );
if ( index === -1 ) { if ( index === -1 ) {
if ( this.options.collapsible && this.options.selected !== -1 ) { if ( this.options.collapsible && this.options.selected !== -1 ) {
@ -960,8 +942,8 @@ if ( $.uiBackCompat !== false ) {
} }
} }
this.anchors.eq( index ).trigger( this.options.event + ".tabs" ); this.anchors.eq( index ).trigger( this.options.event + ".tabs" );
}; }
}( jQuery, jQuery.ui.tabs.prototype ) ); });
// cookie option // cookie option
var listId = 0; var listId = 0;

View File

@ -23,8 +23,9 @@ $.cleanData = function( elems ) {
}; };
$.widget = function( name, base, prototype ) { $.widget = function( name, base, prototype ) {
var namespace = name.split( "." )[ 0 ], var fullName, existingConstructor, constructor, basePrototype,
fullName; namespace = name.split( "." )[ 0 ];
name = name.split( "." )[ 1 ]; name = name.split( "." )[ 1 ];
fullName = namespace + "-" + name; fullName = namespace + "-" + name;
@ -39,12 +40,11 @@ $.widget = function( name, base, prototype ) {
}; };
$[ namespace ] = $[ namespace ] || {}; $[ namespace ] = $[ namespace ] || {};
// create the constructor using $.extend() so we can carry over any existingConstructor = $[ namespace ][ name ];
// static properties stored on the existing constructor (if there is one) constructor = $[ namespace ][ name ] = function( options, element ) {
$[ namespace ][ name ] = $.extend( function( options, element ) {
// allow instantiation without "new" keyword // allow instantiation without "new" keyword
if ( !this._createWidget ) { if ( !this._createWidget ) {
return new $[ namespace ][ name ]( options, element ); return new constructor( options, element );
} }
// allow instantiation without initializing for simple inheritance // allow instantiation without initializing for simple inheritance
@ -52,9 +52,19 @@ $.widget = function( name, base, prototype ) {
if ( arguments.length ) { if ( arguments.length ) {
this._createWidget( options, element ); this._createWidget( options, element );
} }
}, $[ namespace ][ name ], { version: prototype.version } ); };
// extend with the existing constructor to carry over any static properties
$.extend( constructor, existingConstructor, {
version: prototype.version,
// copy the object used to create the prototype in case we need to
// redefine the widget later
_proto: $.extend( {}, prototype ),
// track widgets that inherit from this widget in case this widget is
// redefined after a widget inherits from it
_childConstructors: []
});
var basePrototype = new base(); basePrototype = new base();
// we need to make the options hash a property directly on the new instance // we need to make the options hash a property directly on the new instance
// otherwise we'll modify the options hash on the prototype that we're // otherwise we'll modify the options hash on the prototype that we're
// inheriting from // inheriting from
@ -83,17 +93,41 @@ $.widget = function( name, base, prototype ) {
return returnValue; return returnValue;
}; };
}()); })();
} }
}); });
$[ namespace ][ name ].prototype = $.widget.extend( basePrototype, { constructor.prototype = $.widget.extend( basePrototype, {
// TODO: remove support for widgetEventPrefix
// always use the name + a colon as the prefix, e.g., draggable:start
// don't prefix for widgets that aren't DOM-based
widgetEventPrefix: name
}, prototype, {
constructor: constructor,
namespace: namespace, namespace: namespace,
widgetName: name, widgetName: name,
widgetEventPrefix: name,
widgetBaseClass: fullName widgetBaseClass: fullName
}, prototype ); });
$.widget.bridge( name, $[ namespace ][ name ] ); // If this widget is being redefined then we need to find all widgets that
// are inheriting from it and redefine all of them so that they inherit from
// the new version of this widget. We're essentially trying to replace one
// level in the prototype chain.
if ( existingConstructor ) {
$.each( existingConstructor._childConstructors, function( i, child ) {
var childPrototype = child.prototype;
// redefine the child widget using the same prototype that was
// originally used, but inherit from the new version of the base
$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
});
// remove the list of existing child constructors from the old constructor
// so the old child constructors can be garbage collected
delete existingConstructor._childConstructors;
} else {
base._childConstructors.push( constructor );
}
$.widget.bridge( name, constructor );
}; };
$.widget.extend = function( target ) { $.widget.extend = function( target ) {
@ -157,18 +191,8 @@ $.widget.bridge = function( name, object ) {
}; };
}; };
$.Widget = function( options, element ) { $.Widget = function( options, element ) {};
// allow instantiation without "new" keyword $.Widget._childConstructors = [];
if ( !this._createWidget ) {
return new $[ namespace ][ name ]( options, element );
}
// allow instantiation without initializing for simple inheritance
// must use "new" keyword (the code above always passes args)
if ( arguments.length ) {
this._createWidget( options, element );
}
};
$.Widget.prototype = { $.Widget.prototype = {
widgetName: "widget", widgetName: "widget",