widget.js 19.3 KB
Newer Older
1
/*!
2
 * jQuery UI Widget 1.12.1
3 4 5 6 7 8
 * http://jqueryui.com
 *
 * Copyright jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 */
9 10 11 12 13 14 15 16

//>>label: Widget
//>>group: Core
//>>description: Provides a factory for creating stateful widgets with a common API.
//>>docs: http://api.jqueryui.com/jQuery.widget/
//>>demos: http://jqueryui.com/widget/

( function( factory ) {
17 18 19
	if ( typeof define === "function" && define.amd ) {

		// AMD. Register as an anonymous module.
20
		define( [ "jquery", "./version" ], factory );
21 22 23 24 25
	} else {

		// Browser globals
		factory( jQuery );
	}
26
}( function( $ ) {
27

28 29
var widgetUuid = 0;
var widgetSlice = Array.prototype.slice;
30

31
$.cleanData = ( function( orig ) {
32 33
	return function( elems ) {
		var events, elem, i;
34
		for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) {
35 36 37 38 39 40 41 42
			try {

				// Only trigger remove when necessary to save time
				events = $._data( elem, "events" );
				if ( events && events.remove ) {
					$( elem ).triggerHandler( "remove" );
				}

43
			// Http://bugs.jquery.com/ticket/8235
44 45 46 47
			} catch ( e ) {}
		}
		orig( elems );
	};
48
} )( $.cleanData );
49 50

$.widget = function( name, base, prototype ) {
51 52 53 54 55
	var existingConstructor, constructor, basePrototype;

	// ProxiedPrototype allows the provided prototype to remain unmodified
	// so that it can be used as a mixin for multiple widgets (#8876)
	var proxiedPrototype = {};
56

57
	var namespace = name.split( "." )[ 0 ];
58
	name = name.split( "." )[ 1 ];
59
	var fullName = namespace + "-" + name;
60 61 62 63 64 65

	if ( !prototype ) {
		prototype = base;
		base = $.Widget;
	}

66 67 68 69 70
	if ( $.isArray( prototype ) ) {
		prototype = $.extend.apply( null, [ {} ].concat( prototype ) );
	}

	// Create selector for plugin
71 72 73 74 75 76 77
	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
		return !!$.data( elem, fullName );
	};

	$[ namespace ] = $[ namespace ] || {};
	existingConstructor = $[ namespace ][ name ];
	constructor = $[ namespace ][ name ] = function( options, element ) {
78 79

		// Allow instantiation without "new" keyword
80 81 82 83
		if ( !this._createWidget ) {
			return new constructor( options, element );
		}

84
		// Allow instantiation without initializing for simple inheritance
85 86 87 88 89
		// must use "new" keyword (the code above always passes args)
		if ( arguments.length ) {
			this._createWidget( options, element );
		}
	};
90 91

	// Extend with the existing constructor to carry over any static properties
92 93
	$.extend( constructor, existingConstructor, {
		version: prototype.version,
94 95

		// Copy the object used to create the prototype in case we need to
96 97
		// redefine the widget later
		_proto: $.extend( {}, prototype ),
98 99

		// Track widgets that inherit from this widget in case this widget is
100 101
		// redefined after a widget inherits from it
		_childConstructors: []
102
	} );
103 104

	basePrototype = new base();
105 106

	// We need to make the options hash a property directly on the new instance
107 108 109 110 111 112 113 114
	// otherwise we'll modify the options hash on the prototype that we're
	// inheriting from
	basePrototype.options = $.widget.extend( {}, basePrototype.options );
	$.each( prototype, function( prop, value ) {
		if ( !$.isFunction( value ) ) {
			proxiedPrototype[ prop ] = value;
			return;
		}
115 116 117 118 119 120 121 122 123
		proxiedPrototype[ prop ] = ( function() {
			function _super() {
				return base.prototype[ prop ].apply( this, arguments );
			}

			function _superApply( args ) {
				return base.prototype[ prop ].apply( this, args );
			}

124
			return function() {
125 126 127
				var __super = this._super;
				var __superApply = this._superApply;
				var returnValue;
128 129 130 131 132 133 134 135 136 137 138

				this._super = _super;
				this._superApply = _superApply;

				returnValue = value.apply( this, arguments );

				this._super = __super;
				this._superApply = __superApply;

				return returnValue;
			};
139 140
		} )();
	} );
141
	constructor.prototype = $.widget.extend( basePrototype, {
142

143 144 145
		// 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
146
		widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name
147 148 149 150 151
	}, proxiedPrototype, {
		constructor: constructor,
		namespace: namespace,
		widgetName: name,
		widgetFullName: fullName
152
	} );
153 154 155 156 157 158 159 160 161

	// 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;

162
			// Redefine the child widget using the same prototype that was
163
			// originally used, but inherit from the new version of the base
164 165 166 167 168
			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor,
				child._proto );
		} );

		// Remove the list of existing child constructors from the old constructor
169 170 171 172 173 174 175 176 177 178 179 180
		// so the old child constructors can be garbage collected
		delete existingConstructor._childConstructors;
	} else {
		base._childConstructors.push( constructor );
	}

	$.widget.bridge( name, constructor );

	return constructor;
};

$.widget.extend = function( target ) {
181 182 183 184 185 186
	var input = widgetSlice.call( arguments, 1 );
	var inputIndex = 0;
	var inputLength = input.length;
	var key;
	var value;

187 188 189 190
	for ( ; inputIndex < inputLength; inputIndex++ ) {
		for ( key in input[ inputIndex ] ) {
			value = input[ inputIndex ][ key ];
			if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
191

192 193 194 195
				// Clone objects
				if ( $.isPlainObject( value ) ) {
					target[ key ] = $.isPlainObject( target[ key ] ) ?
						$.widget.extend( {}, target[ key ], value ) :
196

197 198
						// Don't extend strings, arrays, etc. with objects
						$.widget.extend( {}, value );
199

200 201 202 203 204 205 206 207 208 209 210 211 212
				// Copy everything else by reference
				} else {
					target[ key ] = value;
				}
			}
		}
	}
	return target;
};

$.widget.bridge = function( name, object ) {
	var fullName = object.prototype.widgetFullName || name;
	$.fn[ name ] = function( options ) {
213 214 215
		var isMethodCall = typeof options === "string";
		var args = widgetSlice.call( arguments, 1 );
		var returnValue = this;
216 217

		if ( isMethodCall ) {
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253

			// If this is an empty collection, we need to have the instance method
			// return undefined instead of the jQuery instance
			if ( !this.length && options === "instance" ) {
				returnValue = undefined;
			} else {
				this.each( function() {
					var methodValue;
					var instance = $.data( this, fullName );

					if ( options === "instance" ) {
						returnValue = instance;
						return false;
					}

					if ( !instance ) {
						return $.error( "cannot call methods on " + name +
							" prior to initialization; " +
							"attempted to call method '" + options + "'" );
					}

					if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) {
						return $.error( "no such method '" + options + "' for " + name +
							" widget instance" );
					}

					methodValue = instance[ options ].apply( instance, args );

					if ( methodValue !== instance && methodValue !== undefined ) {
						returnValue = methodValue && methodValue.jquery ?
							returnValue.pushStack( methodValue.get() ) :
							methodValue;
						return false;
					}
				} );
			}
254 255 256 257
		} else {

			// Allow multiple hashes to be passed on init
			if ( args.length ) {
258
				options = $.widget.extend.apply( null, [ options ].concat( args ) );
259 260
			}

261
			this.each( function() {
262 263 264 265 266 267 268 269 270
				var instance = $.data( this, fullName );
				if ( instance ) {
					instance.option( options || {} );
					if ( instance._init ) {
						instance._init();
					}
				} else {
					$.data( this, fullName, new object( options, this ) );
				}
271
			} );
272 273 274 275 276 277 278 279 280 281 282 283 284
		}

		return returnValue;
	};
};

$.Widget = function( /* options, element */ ) {};
$.Widget._childConstructors = [];

$.Widget.prototype = {
	widgetName: "widget",
	widgetEventPrefix: "",
	defaultElement: "<div>",
285

286
	options: {
287
		classes: {},
288 289
		disabled: false,

290
		// Callbacks
291 292
		create: null
	},
293

294 295 296
	_createWidget: function( options, element ) {
		element = $( element || this.defaultElement || this )[ 0 ];
		this.element = $( element );
297
		this.uuid = widgetUuid++;
298 299 300 301 302
		this.eventNamespace = "." + this.widgetName + this.uuid;

		this.bindings = $();
		this.hoverable = $();
		this.focusable = $();
303
		this.classesElementLookup = {};
304 305 306 307 308 309 310 311 312

		if ( element !== this ) {
			$.data( element, this.widgetFullName, this );
			this._on( true, this.element, {
				remove: function( event ) {
					if ( event.target === element ) {
						this.destroy();
					}
				}
313
			} );
314
			this.document = $( element.style ?
315 316

				// Element within the document
317
				element.ownerDocument :
318 319

				// Element is window or document
320
				element.document || element );
321
			this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow );
322 323 324 325 326 327 328 329
		}

		this.options = $.widget.extend( {},
			this.options,
			this._getCreateOptions(),
			options );

		this._create();
330 331 332 333 334

		if ( this.options.disabled ) {
			this._setOptionDisabled( this.options.disabled );
		}

335 336 337
		this._trigger( "create", null, this._getCreateEventData() );
		this._init();
	},
338 339 340 341 342

	_getCreateOptions: function() {
		return {};
	},

343
	_getCreateEventData: $.noop,
344

345
	_create: $.noop,
346

347 348 349
	_init: $.noop,

	destroy: function() {
350 351
		var that = this;

352
		this._destroy();
353 354 355 356 357
		$.each( this.classesElementLookup, function( key, value ) {
			that._removeClass( value, key );
		} );

		// We can probably remove the unbind calls in 2.0
358 359
		// all event bindings should go through this._on()
		this.element
360 361
			.off( this.eventNamespace )
			.removeData( this.widgetFullName );
362
		this.widget()
363 364 365 366 367
			.off( this.eventNamespace )
			.removeAttr( "aria-disabled" );

		// Clean up events and states
		this.bindings.off( this.eventNamespace );
368
	},
369

370 371 372 373 374 375 376
	_destroy: $.noop,

	widget: function() {
		return this.element;
	},

	option: function( key, value ) {
377 378 379 380
		var options = key;
		var parts;
		var curOption;
		var i;
381 382

		if ( arguments.length === 0 ) {
383 384

			// Don't return a reference to the internal hash
385 386 387 388
			return $.widget.extend( {}, this.options );
		}

		if ( typeof key === "string" ) {
389 390

			// Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
			options = {};
			parts = key.split( "." );
			key = parts.shift();
			if ( parts.length ) {
				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
				for ( i = 0; i < parts.length - 1; i++ ) {
					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
					curOption = curOption[ parts[ i ] ];
				}
				key = parts.pop();
				if ( arguments.length === 1 ) {
					return curOption[ key ] === undefined ? null : curOption[ key ];
				}
				curOption[ key ] = value;
			} else {
				if ( arguments.length === 1 ) {
					return this.options[ key ] === undefined ? null : this.options[ key ];
				}
				options[ key ] = value;
			}
		}

		this._setOptions( options );

		return this;
	},
417

418 419 420 421 422 423 424 425 426
	_setOptions: function( options ) {
		var key;

		for ( key in options ) {
			this._setOption( key, options[ key ] );
		}

		return this;
	},
427

428
	_setOption: function( key, value ) {
429 430 431 432
		if ( key === "classes" ) {
			this._setOptionClasses( value );
		}

433 434 435
		this.options[ key ] = value;

		if ( key === "disabled" ) {
436 437 438 439 440 441 442 443
			this._setOptionDisabled( value );
		}

		return this;
	},

	_setOptionClasses: function( value ) {
		var classKey, elements, currentElements;
444

445 446 447 448 449 450
		for ( classKey in value ) {
			currentElements = this.classesElementLookup[ classKey ];
			if ( value[ classKey ] === this.options.classes[ classKey ] ||
					!currentElements ||
					!currentElements.length ) {
				continue;
451
			}
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469

			// We are doing this to create a new jQuery object because the _removeClass() call
			// on the next line is going to destroy the reference to the current elements being
			// tracked. We need to save a copy of this collection so that we can add the new classes
			// below.
			elements = $( currentElements.get() );
			this._removeClass( currentElements, classKey );

			// We don't use _addClass() here, because that uses this.options.classes
			// for generating the string of classes. We want to use the value passed in from
			// _setOption(), this is the new value of the classes option which was passed to
			// _setOption(). We pass this value directly to _classes().
			elements.addClass( this._classes( {
				element: elements,
				keys: classKey,
				classes: value,
				add: true
			} ) );
470
		}
471
	},
472

473 474 475 476 477 478 479 480
	_setOptionDisabled: function( value ) {
		this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value );

		// If the widget is becoming disabled, then nothing is interactive
		if ( value ) {
			this._removeClass( this.hoverable, null, "ui-state-hover" );
			this._removeClass( this.focusable, null, "ui-state-focus" );
		}
481 482 483
	},

	enable: function() {
484
		return this._setOptions( { disabled: false } );
485
	},
486

487
	disable: function() {
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558
		return this._setOptions( { disabled: true } );
	},

	_classes: function( options ) {
		var full = [];
		var that = this;

		options = $.extend( {
			element: this.element,
			classes: this.options.classes || {}
		}, options );

		function processClassString( classes, checkOption ) {
			var current, i;
			for ( i = 0; i < classes.length; i++ ) {
				current = that.classesElementLookup[ classes[ i ] ] || $();
				if ( options.add ) {
					current = $( $.unique( current.get().concat( options.element.get() ) ) );
				} else {
					current = $( current.not( options.element ).get() );
				}
				that.classesElementLookup[ classes[ i ] ] = current;
				full.push( classes[ i ] );
				if ( checkOption && options.classes[ classes[ i ] ] ) {
					full.push( options.classes[ classes[ i ] ] );
				}
			}
		}

		this._on( options.element, {
			"remove": "_untrackClassesElement"
		} );

		if ( options.keys ) {
			processClassString( options.keys.match( /\S+/g ) || [], true );
		}
		if ( options.extra ) {
			processClassString( options.extra.match( /\S+/g ) || [] );
		}

		return full.join( " " );
	},

	_untrackClassesElement: function( event ) {
		var that = this;
		$.each( that.classesElementLookup, function( key, value ) {
			if ( $.inArray( event.target, value ) !== -1 ) {
				that.classesElementLookup[ key ] = $( value.not( event.target ).get() );
			}
		} );
	},

	_removeClass: function( element, keys, extra ) {
		return this._toggleClass( element, keys, extra, false );
	},

	_addClass: function( element, keys, extra ) {
		return this._toggleClass( element, keys, extra, true );
	},

	_toggleClass: function( element, keys, extra, add ) {
		add = ( typeof add === "boolean" ) ? add : extra;
		var shift = ( typeof element === "string" || element === null ),
			options = {
				extra: shift ? keys : extra,
				keys: shift ? element : keys,
				element: shift ? this.element : element,
				add: add
			};
		options.element.toggleClass( this._classes( options ), add );
		return this;
559 560 561
	},

	_on: function( suppressDisabledCheck, element, handlers ) {
562 563
		var delegateElement;
		var instance = this;
564

565
		// No suppressDisabledCheck flag, shuffle arguments
566 567 568 569 570 571
		if ( typeof suppressDisabledCheck !== "boolean" ) {
			handlers = element;
			element = suppressDisabledCheck;
			suppressDisabledCheck = false;
		}

572
		// No element argument, shuffle and use this.element
573 574 575 576 577 578 579 580 581 582 583
		if ( !handlers ) {
			handlers = element;
			element = this.element;
			delegateElement = this.widget();
		} else {
			element = delegateElement = $( element );
			this.bindings = this.bindings.add( element );
		}

		$.each( handlers, function( event, handler ) {
			function handlerProxy() {
584 585

				// Allow widgets to customize the disabled handling
586 587 588 589
				// - disabled as an array instead of boolean
				// - disabled class as method for disabling individual parts
				if ( !suppressDisabledCheck &&
						( instance.options.disabled === true ||
590
						$( this ).hasClass( "ui-state-disabled" ) ) ) {
591 592 593 594 595 596
					return;
				}
				return ( typeof handler === "string" ? instance[ handler ] : handler )
					.apply( instance, arguments );
			}

597
			// Copy the guid so direct unbinding works
598 599 600 601 602
			if ( typeof handler !== "string" ) {
				handlerProxy.guid = handler.guid =
					handler.guid || handlerProxy.guid || $.guid++;
			}

603 604 605 606
			var match = event.match( /^([\w:-]*)\s*(.*)$/ );
			var eventName = match[ 1 ] + instance.eventNamespace;
			var selector = match[ 2 ];

607
			if ( selector ) {
608
				delegateElement.on( eventName, selector, handlerProxy );
609
			} else {
610
				element.on( eventName, handlerProxy );
611
			}
612
		} );
613 614 615
	},

	_off: function( element, eventName ) {
616
		eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) +
617
			this.eventNamespace;
618
		element.off( eventName ).off( eventName );
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638

		// Clear the stack to avoid memory leaks (#10056)
		this.bindings = $( this.bindings.not( element ).get() );
		this.focusable = $( this.focusable.not( element ).get() );
		this.hoverable = $( this.hoverable.not( element ).get() );
	},

	_delay: function( handler, delay ) {
		function handlerProxy() {
			return ( typeof handler === "string" ? instance[ handler ] : handler )
				.apply( instance, arguments );
		}
		var instance = this;
		return setTimeout( handlerProxy, delay || 0 );
	},

	_hoverable: function( element ) {
		this.hoverable = this.hoverable.add( element );
		this._on( element, {
			mouseenter: function( event ) {
639
				this._addClass( $( event.currentTarget ), null, "ui-state-hover" );
640 641
			},
			mouseleave: function( event ) {
642
				this._removeClass( $( event.currentTarget ), null, "ui-state-hover" );
643
			}
644
		} );
645 646 647 648 649 650
	},

	_focusable: function( element ) {
		this.focusable = this.focusable.add( element );
		this._on( element, {
			focusin: function( event ) {
651
				this._addClass( $( event.currentTarget ), null, "ui-state-focus" );
652 653
			},
			focusout: function( event ) {
654
				this._removeClass( $( event.currentTarget ), null, "ui-state-focus" );
655
			}
656
		} );
657 658 659
	},

	_trigger: function( type, event, data ) {
660 661
		var prop, orig;
		var callback = this.options[ type ];
662 663 664 665 666 667

		data = data || {};
		event = $.Event( event );
		event.type = ( type === this.widgetEventPrefix ?
			type :
			this.widgetEventPrefix + type ).toLowerCase();
668 669

		// The original event may come from any element
670 671 672
		// so we need to reset the target on the new event
		event.target = this.element[ 0 ];

673
		// Copy original event properties over to the new event
674 675 676 677 678 679 680 681 682 683 684
		orig = event.originalEvent;
		if ( orig ) {
			for ( prop in orig ) {
				if ( !( prop in event ) ) {
					event[ prop ] = orig[ prop ];
				}
			}
		}

		this.element.trigger( event, data );
		return !( $.isFunction( callback ) &&
685
			callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false ||
686 687 688 689 690 691 692 693 694
			event.isDefaultPrevented() );
	}
};

$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
	$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
		if ( typeof options === "string" ) {
			options = { effect: options };
		}
695 696 697 698 699 700 701 702

		var hasOptions;
		var effectName = !options ?
			method :
			options === true || typeof options === "number" ?
				defaultEffect :
				options.effect || defaultEffect;

703 704 705 706
		options = options || {};
		if ( typeof options === "number" ) {
			options = { duration: options };
		}
707

708 709
		hasOptions = !$.isEmptyObject( options );
		options.complete = callback;
710

711 712 713
		if ( options.delay ) {
			element.delay( options.delay );
		}
714

715 716 717 718 719
		if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
			element[ method ]( options );
		} else if ( effectName !== method && element[ effectName ] ) {
			element[ effectName ]( options.duration, options.easing, callback );
		} else {
720
			element.queue( function( next ) {
721 722 723 724 725
				$( this )[ method ]();
				if ( callback ) {
					callback.call( element[ 0 ] );
				}
				next();
726
			} );
727 728
		}
	};
729
} );
730 731 732

return $.widget;

733
} ) );