Manipulation: Support $el.html(selfRemovingScript) (#5378)
[jquery.git] / src / effects.js
blob3c1bd33fd0b8fb41925432dfec179c27337b37c5
1 import { jQuery } from "./core.js";
2 import { document } from "./var/document.js";
3 import { rcssNum } from "./var/rcssNum.js";
4 import { rnothtmlwhite } from "./var/rnothtmlwhite.js";
5 import { cssExpand } from "./css/var/cssExpand.js";
6 import { isHiddenWithinTree } from "./css/var/isHiddenWithinTree.js";
7 import { adjustCSS } from "./css/adjustCSS.js";
8 import { cssCamelCase } from "./css/cssCamelCase.js";
9 import { dataPriv } from "./data/var/dataPriv.js";
10 import { showHide } from "./css/showHide.js";
12 import "./core/init.js";
13 import "./queue.js";
14 import "./deferred.js";
15 import "./traversing.js";
16 import "./manipulation.js";
17 import "./css.js";
18 import "./effects/Tween.js";
20 var
21         fxNow, inProgress,
22         rfxtypes = /^(?:toggle|show|hide)$/,
23         rrun = /queueHooks$/;
25 function schedule() {
26         if ( inProgress ) {
27                 if ( document.hidden === false && window.requestAnimationFrame ) {
28                         window.requestAnimationFrame( schedule );
29                 } else {
30                         window.setTimeout( schedule, 13 );
31                 }
33                 jQuery.fx.tick();
34         }
37 // Animations created synchronously will run synchronously
38 function createFxNow() {
39         window.setTimeout( function() {
40                 fxNow = undefined;
41         } );
42         return ( fxNow = Date.now() );
45 // Generate parameters to create a standard animation
46 function genFx( type, includeWidth ) {
47         var which,
48                 i = 0,
49                 attrs = { height: type };
51         // If we include width, step value is 1 to do all cssExpand values,
52         // otherwise step value is 2 to skip over Left and Right
53         includeWidth = includeWidth ? 1 : 0;
54         for ( ; i < 4; i += 2 - includeWidth ) {
55                 which = cssExpand[ i ];
56                 attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
57         }
59         if ( includeWidth ) {
60                 attrs.opacity = attrs.width = type;
61         }
63         return attrs;
66 function createTween( value, prop, animation ) {
67         var tween,
68                 collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
69                 index = 0,
70                 length = collection.length;
71         for ( ; index < length; index++ ) {
72                 if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {
74                         // We're done with this property
75                         return tween;
76                 }
77         }
80 function defaultPrefilter( elem, props, opts ) {
81         var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,
82                 isBox = "width" in props || "height" in props,
83                 anim = this,
84                 orig = {},
85                 style = elem.style,
86                 hidden = elem.nodeType && isHiddenWithinTree( elem ),
87                 dataShow = dataPriv.get( elem, "fxshow" );
89         // Queue-skipping animations hijack the fx hooks
90         if ( !opts.queue ) {
91                 hooks = jQuery._queueHooks( elem, "fx" );
92                 if ( hooks.unqueued == null ) {
93                         hooks.unqueued = 0;
94                         oldfire = hooks.empty.fire;
95                         hooks.empty.fire = function() {
96                                 if ( !hooks.unqueued ) {
97                                         oldfire();
98                                 }
99                         };
100                 }
101                 hooks.unqueued++;
103                 anim.always( function() {
105                         // Ensure the complete handler is called before this completes
106                         anim.always( function() {
107                                 hooks.unqueued--;
108                                 if ( !jQuery.queue( elem, "fx" ).length ) {
109                                         hooks.empty.fire();
110                                 }
111                         } );
112                 } );
113         }
115         // Detect show/hide animations
116         for ( prop in props ) {
117                 value = props[ prop ];
118                 if ( rfxtypes.test( value ) ) {
119                         delete props[ prop ];
120                         toggle = toggle || value === "toggle";
121                         if ( value === ( hidden ? "hide" : "show" ) ) {
123                                 // Pretend to be hidden if this is a "show" and
124                                 // there is still data from a stopped show/hide
125                                 if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
126                                         hidden = true;
128                                 // Ignore all other no-op show/hide data
129                                 } else {
130                                         continue;
131                                 }
132                         }
133                         orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
134                 }
135         }
137         // Bail out if this is a no-op like .hide().hide()
138         propTween = !jQuery.isEmptyObject( props );
139         if ( !propTween && jQuery.isEmptyObject( orig ) ) {
140                 return;
141         }
143         // Restrict "overflow" and "display" styles during box animations
144         if ( isBox && elem.nodeType === 1 ) {
146                 // Support: IE <=9 - 11+
147                 // Record all 3 overflow attributes because IE does not infer the shorthand
148                 // from identically-valued overflowX and overflowY.
149                 opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
151                 // Identify a display type, preferring old show/hide data over the CSS cascade
152                 restoreDisplay = dataShow && dataShow.display;
153                 if ( restoreDisplay == null ) {
154                         restoreDisplay = dataPriv.get( elem, "display" );
155                 }
156                 display = jQuery.css( elem, "display" );
157                 if ( display === "none" ) {
158                         if ( restoreDisplay ) {
159                                 display = restoreDisplay;
160                         } else {
162                                 // Get nonempty value(s) by temporarily forcing visibility
163                                 showHide( [ elem ], true );
164                                 restoreDisplay = elem.style.display || restoreDisplay;
165                                 display = jQuery.css( elem, "display" );
166                                 showHide( [ elem ] );
167                         }
168                 }
170                 // Animate inline elements as inline-block
171                 if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) {
172                         if ( jQuery.css( elem, "float" ) === "none" ) {
174                                 // Restore the original display value at the end of pure show/hide animations
175                                 if ( !propTween ) {
176                                         anim.done( function() {
177                                                 style.display = restoreDisplay;
178                                         } );
179                                         if ( restoreDisplay == null ) {
180                                                 display = style.display;
181                                                 restoreDisplay = display === "none" ? "" : display;
182                                         }
183                                 }
184                                 style.display = "inline-block";
185                         }
186                 }
187         }
189         if ( opts.overflow ) {
190                 style.overflow = "hidden";
191                 anim.always( function() {
192                         style.overflow = opts.overflow[ 0 ];
193                         style.overflowX = opts.overflow[ 1 ];
194                         style.overflowY = opts.overflow[ 2 ];
195                 } );
196         }
198         // Implement show/hide animations
199         propTween = false;
200         for ( prop in orig ) {
202                 // General show/hide setup for this element animation
203                 if ( !propTween ) {
204                         if ( dataShow ) {
205                                 if ( "hidden" in dataShow ) {
206                                         hidden = dataShow.hidden;
207                                 }
208                         } else {
209                                 dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } );
210                         }
212                         // Store hidden/visible for toggle so `.stop().toggle()` "reverses"
213                         if ( toggle ) {
214                                 dataShow.hidden = !hidden;
215                         }
217                         // Show elements before animating them
218                         if ( hidden ) {
219                                 showHide( [ elem ], true );
220                         }
222                         // eslint-disable-next-line no-loop-func
223                         anim.done( function() {
225                                 // The final step of a "hide" animation is actually hiding the element
226                                 if ( !hidden ) {
227                                         showHide( [ elem ] );
228                                 }
229                                 dataPriv.remove( elem, "fxshow" );
230                                 for ( prop in orig ) {
231                                         jQuery.style( elem, prop, orig[ prop ] );
232                                 }
233                         } );
234                 }
236                 // Per-property setup
237                 propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
238                 if ( !( prop in dataShow ) ) {
239                         dataShow[ prop ] = propTween.start;
240                         if ( hidden ) {
241                                 propTween.end = propTween.start;
242                                 propTween.start = 0;
243                         }
244                 }
245         }
248 function propFilter( props, specialEasing ) {
249         var index, name, easing, value, hooks;
251         // camelCase, specialEasing and expand cssHook pass
252         for ( index in props ) {
253                 name = cssCamelCase( index );
254                 easing = specialEasing[ name ];
255                 value = props[ index ];
256                 if ( Array.isArray( value ) ) {
257                         easing = value[ 1 ];
258                         value = props[ index ] = value[ 0 ];
259                 }
261                 if ( index !== name ) {
262                         props[ name ] = value;
263                         delete props[ index ];
264                 }
266                 hooks = jQuery.cssHooks[ name ];
267                 if ( hooks && "expand" in hooks ) {
268                         value = hooks.expand( value );
269                         delete props[ name ];
271                         // Not quite $.extend, this won't overwrite existing keys.
272                         // Reusing 'index' because we have the correct "name"
273                         for ( index in value ) {
274                                 if ( !( index in props ) ) {
275                                         props[ index ] = value[ index ];
276                                         specialEasing[ index ] = easing;
277                                 }
278                         }
279                 } else {
280                         specialEasing[ name ] = easing;
281                 }
282         }
285 function Animation( elem, properties, options ) {
286         var result,
287                 stopped,
288                 index = 0,
289                 length = Animation.prefilters.length,
290                 deferred = jQuery.Deferred().always( function() {
292                         // Don't match elem in the :animated selector
293                         delete tick.elem;
294                 } ),
295                 tick = function() {
296                         if ( stopped ) {
297                                 return false;
298                         }
299                         var currentTime = fxNow || createFxNow(),
300                                 remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
302                                 percent = 1 - ( remaining / animation.duration || 0 ),
303                                 index = 0,
304                                 length = animation.tweens.length;
306                         for ( ; index < length; index++ ) {
307                                 animation.tweens[ index ].run( percent );
308                         }
310                         deferred.notifyWith( elem, [ animation, percent, remaining ] );
312                         // If there's more to do, yield
313                         if ( percent < 1 && length ) {
314                                 return remaining;
315                         }
317                         // If this was an empty animation, synthesize a final progress notification
318                         if ( !length ) {
319                                 deferred.notifyWith( elem, [ animation, 1, 0 ] );
320                         }
322                         // Resolve the animation and report its conclusion
323                         deferred.resolveWith( elem, [ animation ] );
324                         return false;
325                 },
326                 animation = deferred.promise( {
327                         elem: elem,
328                         props: jQuery.extend( {}, properties ),
329                         opts: jQuery.extend( true, {
330                                 specialEasing: {},
331                                 easing: jQuery.easing._default
332                         }, options ),
333                         originalProperties: properties,
334                         originalOptions: options,
335                         startTime: fxNow || createFxNow(),
336                         duration: options.duration,
337                         tweens: [],
338                         createTween: function( prop, end ) {
339                                 var tween = jQuery.Tween( elem, animation.opts, prop, end,
340                                         animation.opts.specialEasing[ prop ] || animation.opts.easing );
341                                 animation.tweens.push( tween );
342                                 return tween;
343                         },
344                         stop: function( gotoEnd ) {
345                                 var index = 0,
347                                         // If we are going to the end, we want to run all the tweens
348                                         // otherwise we skip this part
349                                         length = gotoEnd ? animation.tweens.length : 0;
350                                 if ( stopped ) {
351                                         return this;
352                                 }
353                                 stopped = true;
354                                 for ( ; index < length; index++ ) {
355                                         animation.tweens[ index ].run( 1 );
356                                 }
358                                 // Resolve when we played the last frame; otherwise, reject
359                                 if ( gotoEnd ) {
360                                         deferred.notifyWith( elem, [ animation, 1, 0 ] );
361                                         deferred.resolveWith( elem, [ animation, gotoEnd ] );
362                                 } else {
363                                         deferred.rejectWith( elem, [ animation, gotoEnd ] );
364                                 }
365                                 return this;
366                         }
367                 } ),
368                 props = animation.props;
370         propFilter( props, animation.opts.specialEasing );
372         for ( ; index < length; index++ ) {
373                 result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
374                 if ( result ) {
375                         if ( typeof result.stop === "function" ) {
376                                 jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
377                                         result.stop.bind( result );
378                         }
379                         return result;
380                 }
381         }
383         jQuery.map( props, createTween, animation );
385         if ( typeof animation.opts.start === "function" ) {
386                 animation.opts.start.call( elem, animation );
387         }
389         // Attach callbacks from options
390         animation
391                 .progress( animation.opts.progress )
392                 .done( animation.opts.done, animation.opts.complete )
393                 .fail( animation.opts.fail )
394                 .always( animation.opts.always );
396         jQuery.fx.timer(
397                 jQuery.extend( tick, {
398                         elem: elem,
399                         anim: animation,
400                         queue: animation.opts.queue
401                 } )
402         );
404         return animation;
407 jQuery.Animation = jQuery.extend( Animation, {
409         tweeners: {
410                 "*": [ function( prop, value ) {
411                         var tween = this.createTween( prop, value );
412                         adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
413                         return tween;
414                 } ]
415         },
417         tweener: function( props, callback ) {
418                 if ( typeof props === "function" ) {
419                         callback = props;
420                         props = [ "*" ];
421                 } else {
422                         props = props.match( rnothtmlwhite );
423                 }
425                 var prop,
426                         index = 0,
427                         length = props.length;
429                 for ( ; index < length; index++ ) {
430                         prop = props[ index ];
431                         Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
432                         Animation.tweeners[ prop ].unshift( callback );
433                 }
434         },
436         prefilters: [ defaultPrefilter ],
438         prefilter: function( callback, prepend ) {
439                 if ( prepend ) {
440                         Animation.prefilters.unshift( callback );
441                 } else {
442                         Animation.prefilters.push( callback );
443                 }
444         }
445 } );
447 jQuery.speed = function( speed, easing, fn ) {
448         var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
449                 complete: fn || easing ||
450                         typeof speed === "function" && speed,
451                 duration: speed,
452                 easing: fn && easing || easing && typeof easing !== "function" && easing
453         };
455         // Go to the end state if fx are off
456         if ( jQuery.fx.off ) {
457                 opt.duration = 0;
459         } else {
460                 if ( typeof opt.duration !== "number" ) {
461                         if ( opt.duration in jQuery.fx.speeds ) {
462                                 opt.duration = jQuery.fx.speeds[ opt.duration ];
464                         } else {
465                                 opt.duration = jQuery.fx.speeds._default;
466                         }
467                 }
468         }
470         // Normalize opt.queue - true/undefined/null -> "fx"
471         if ( opt.queue == null || opt.queue === true ) {
472                 opt.queue = "fx";
473         }
475         // Queueing
476         opt.old = opt.complete;
478         opt.complete = function() {
479                 if ( typeof opt.old === "function" ) {
480                         opt.old.call( this );
481                 }
483                 if ( opt.queue ) {
484                         jQuery.dequeue( this, opt.queue );
485                 }
486         };
488         return opt;
491 jQuery.fn.extend( {
492         fadeTo: function( speed, to, easing, callback ) {
494                 // Show any hidden elements after setting opacity to 0
495                 return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show()
497                         // Animate to the value specified
498                         .end().animate( { opacity: to }, speed, easing, callback );
499         },
500         animate: function( prop, speed, easing, callback ) {
501                 var empty = jQuery.isEmptyObject( prop ),
502                         optall = jQuery.speed( speed, easing, callback ),
503                         doAnimation = function() {
505                                 // Operate on a copy of prop so per-property easing won't be lost
506                                 var anim = Animation( this, jQuery.extend( {}, prop ), optall );
508                                 // Empty animations, or finishing resolves immediately
509                                 if ( empty || dataPriv.get( this, "finish" ) ) {
510                                         anim.stop( true );
511                                 }
512                         };
514                 doAnimation.finish = doAnimation;
516                 return empty || optall.queue === false ?
517                         this.each( doAnimation ) :
518                         this.queue( optall.queue, doAnimation );
519         },
520         stop: function( type, clearQueue, gotoEnd ) {
521                 var stopQueue = function( hooks ) {
522                         var stop = hooks.stop;
523                         delete hooks.stop;
524                         stop( gotoEnd );
525                 };
527                 if ( typeof type !== "string" ) {
528                         gotoEnd = clearQueue;
529                         clearQueue = type;
530                         type = undefined;
531                 }
532                 if ( clearQueue ) {
533                         this.queue( type || "fx", [] );
534                 }
536                 return this.each( function() {
537                         var dequeue = true,
538                                 index = type != null && type + "queueHooks",
539                                 timers = jQuery.timers,
540                                 data = dataPriv.get( this );
542                         if ( index ) {
543                                 if ( data[ index ] && data[ index ].stop ) {
544                                         stopQueue( data[ index ] );
545                                 }
546                         } else {
547                                 for ( index in data ) {
548                                         if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
549                                                 stopQueue( data[ index ] );
550                                         }
551                                 }
552                         }
554                         for ( index = timers.length; index--; ) {
555                                 if ( timers[ index ].elem === this &&
556                                         ( type == null || timers[ index ].queue === type ) ) {
558                                         timers[ index ].anim.stop( gotoEnd );
559                                         dequeue = false;
560                                         timers.splice( index, 1 );
561                                 }
562                         }
564                         // Start the next in the queue if the last step wasn't forced.
565                         // Timers currently will call their complete callbacks, which
566                         // will dequeue but only if they were gotoEnd.
567                         if ( dequeue || !gotoEnd ) {
568                                 jQuery.dequeue( this, type );
569                         }
570                 } );
571         },
572         finish: function( type ) {
573                 if ( type !== false ) {
574                         type = type || "fx";
575                 }
576                 return this.each( function() {
577                         var index,
578                                 data = dataPriv.get( this ),
579                                 queue = data[ type + "queue" ],
580                                 hooks = data[ type + "queueHooks" ],
581                                 timers = jQuery.timers,
582                                 length = queue ? queue.length : 0;
584                         // Enable finishing flag on private data
585                         data.finish = true;
587                         // Empty the queue first
588                         jQuery.queue( this, type, [] );
590                         if ( hooks && hooks.stop ) {
591                                 hooks.stop.call( this, true );
592                         }
594                         // Look for any active animations, and finish them
595                         for ( index = timers.length; index--; ) {
596                                 if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
597                                         timers[ index ].anim.stop( true );
598                                         timers.splice( index, 1 );
599                                 }
600                         }
602                         // Look for any animations in the old queue and finish them
603                         for ( index = 0; index < length; index++ ) {
604                                 if ( queue[ index ] && queue[ index ].finish ) {
605                                         queue[ index ].finish.call( this );
606                                 }
607                         }
609                         // Turn off finishing flag
610                         delete data.finish;
611                 } );
612         }
613 } );
615 jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) {
616         var cssFn = jQuery.fn[ name ];
617         jQuery.fn[ name ] = function( speed, easing, callback ) {
618                 return speed == null || typeof speed === "boolean" ?
619                         cssFn.apply( this, arguments ) :
620                         this.animate( genFx( name, true ), speed, easing, callback );
621         };
622 } );
624 // Generate shortcuts for custom animations
625 jQuery.each( {
626         slideDown: genFx( "show" ),
627         slideUp: genFx( "hide" ),
628         slideToggle: genFx( "toggle" ),
629         fadeIn: { opacity: "show" },
630         fadeOut: { opacity: "hide" },
631         fadeToggle: { opacity: "toggle" }
632 }, function( name, props ) {
633         jQuery.fn[ name ] = function( speed, easing, callback ) {
634                 return this.animate( props, speed, easing, callback );
635         };
636 } );
638 jQuery.timers = [];
639 jQuery.fx.tick = function() {
640         var timer,
641                 i = 0,
642                 timers = jQuery.timers;
644         fxNow = Date.now();
646         for ( ; i < timers.length; i++ ) {
647                 timer = timers[ i ];
649                 // Run the timer and safely remove it when done (allowing for external removal)
650                 if ( !timer() && timers[ i ] === timer ) {
651                         timers.splice( i--, 1 );
652                 }
653         }
655         if ( !timers.length ) {
656                 jQuery.fx.stop();
657         }
658         fxNow = undefined;
661 jQuery.fx.timer = function( timer ) {
662         jQuery.timers.push( timer );
663         jQuery.fx.start();
666 jQuery.fx.start = function() {
667         if ( inProgress ) {
668                 return;
669         }
671         inProgress = true;
672         schedule();
675 jQuery.fx.stop = function() {
676         inProgress = null;
679 jQuery.fx.speeds = {
680         slow: 600,
681         fast: 200,
683         // Default speed
684         _default: 400
687 export { jQuery, jQuery as $ };