/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A delayed callback that pegs to the next animation frame
* instead of a user-configurable timeout.
*/
goog.provide('goog.async.AnimationDelay');
goog.require('goog.Disposable');
goog.require('goog.events');
goog.require('goog.functions');
// TODO(nicksantos): Should we factor out the common code between this and
// goog.async.Delay? I'm not sure if there's enough code for this to really
// make sense. Subclassing seems like the wrong approach for a variety of
// reasons. Maybe there should be a common interface?
/**
* A delayed callback that pegs to the next animation frame
* instead of a user configurable timeout. By design, this should have
* the same interface as goog.async.Delay.
*
* Uses requestAnimationFrame and friends when available, but falls
* back to a timeout of goog.async.AnimationDelay.TIMEOUT.
*
* For more on requestAnimationFrame and how you can use it to create smoother
* animations, see:
* @see http://paulirish.com/2011/requestanimationframe-for-smart-animating/
*
* @param {function(this:THIS, number)} listener Function to call
* when the delay completes. Will be passed the timestamp when it's called,
* in unix ms.
* @param {Window=} opt_window The window object to execute the delay in.
* Defaults to the global object.
* @param {THIS=} opt_handler The object scope to invoke the function in.
* @template THIS
* @constructor
* @struct
* @extends {goog.Disposable}
* @final
*/
goog.async.AnimationDelay = function(listener, opt_window, opt_handler) {
'use strict';
goog.async.AnimationDelay.base(this, 'constructor');
/**
* Identifier of the active delay timeout, or event listener,
* or null when inactive.
* @private {?goog.events.Key|number}
*/
this.id_ = null;
/**
* If we're using dom listeners.
* @private {?boolean}
*/
this.usingListeners_ = false;
/**
* The function that will be invoked after a delay.
* @const
* @private
*/
this.listener_ = listener;
/**
* The object context to invoke the callback in.
* @const
* @private {(THIS|undefined)}
*/
this.handler_ = opt_handler;
/**
* @private {Window}
*/
this.win_ = opt_window || window;
/**
* Cached callback function invoked when the delay finishes.
* @private {function()}
*/
this.callback_ = goog.bind(this.doAction_, this);
};
goog.inherits(goog.async.AnimationDelay, goog.Disposable);
/**
* Default wait timeout for animations (in milliseconds). Only used for timed
* animation, which uses a timer (setTimeout) to schedule animation.
*
* @type {number}
* @const
*/
goog.async.AnimationDelay.TIMEOUT = 20;
/**
* Name of event received from the requestAnimationFrame in Firefox.
*
* @type {string}
* @const
* @private
*/
goog.async.AnimationDelay.MOZ_BEFORE_PAINT_EVENT_ = 'MozBeforePaint';
/**
* Starts the delay timer. The provided listener function will be called
* before the next animation frame.
*/
goog.async.AnimationDelay.prototype.start = function() {
'use strict';
this.stop();
this.usingListeners_ = false;
var raf = this.getRaf_();
var cancelRaf = this.getCancelRaf_();
if (raf && !cancelRaf && this.win_.mozRequestAnimationFrame) {
// Because Firefox (Gecko) runs animation in separate threads, it also saves
// time by running the requestAnimationFrame callbacks in that same thread.
// Sadly this breaks the assumption of implicit thread-safety in JS, and can
// thus create thread-based inconsistencies on counters etc.
//
// Calling cycleAnimations_ using the MozBeforePaint event instead of as
// callback fixes this.
//
// Trigger this condition only if the mozRequestAnimationFrame is available,
// but not the W3C requestAnimationFrame function (as in draft) or the
// equivalent cancel functions.
this.id_ = goog.events.listen(
this.win_, goog.async.AnimationDelay.MOZ_BEFORE_PAINT_EVENT_,
this.callback_);
this.win_.mozRequestAnimationFrame(null);
this.usingListeners_ = true;
} else if (raf && cancelRaf) {
this.id_ = raf.call(this.win_, this.callback_);
} else {
this.id_ = this.win_.setTimeout(
// Prior to Firefox 13, Gecko passed a non-standard parameter
// to the callback that we want to ignore.
goog.functions.lock(this.callback_), goog.async.AnimationDelay.TIMEOUT);
}
};
/**
* Starts the delay timer if it's not already active.
*/
goog.async.AnimationDelay.prototype.startIfNotActive = function() {
'use strict';
if (!this.isActive()) {
this.start();
}
};
/**
* Stops the delay timer if it is active. No action is taken if the timer is not
* in use.
*/
goog.async.AnimationDelay.prototype.stop = function() {
'use strict';
if (this.isActive()) {
var raf = this.getRaf_();
var cancelRaf = this.getCancelRaf_();
if (raf && !cancelRaf && this.win_.mozRequestAnimationFrame) {
goog.events.unlistenByKey(this.id_);
} else if (raf && cancelRaf) {
cancelRaf.call(this.win_, /** @type {number} */ (this.id_));
} else {
this.win_.clearTimeout(/** @type {number} */ (this.id_));
}
}
this.id_ = null;
};
/**
* Fires delay's action even if timer has already gone off or has not been
* started yet; guarantees action firing. Stops the delay timer.
*/
goog.async.AnimationDelay.prototype.fire = function() {
'use strict';
this.stop();
this.doAction_();
};
/**
* Fires delay's action only if timer is currently active. Stops the delay
* timer.
*/
goog.async.AnimationDelay.prototype.fireIfActive = function() {
'use strict';
if (this.isActive()) {
this.fire();
}
};
/**
* @return {boolean} True if the delay is currently active, false otherwise.
*/
goog.async.AnimationDelay.prototype.isActive = function() {
'use strict';
return this.id_ != null;
};
/**
* Invokes the callback function after the delay successfully completes.
* @private
*/
goog.async.AnimationDelay.prototype.doAction_ = function() {
'use strict';
if (this.usingListeners_ && this.id_) {
goog.events.unlistenByKey(this.id_);
}
this.id_ = null;
// We are not using the timestamp returned by requestAnimationFrame
// because it may be either a Date.now-style time or a
// high-resolution time (depending on browser implementation). Using
// goog.now() will ensure that the timestamp used is consistent and
// compatible with goog.fx.Animation.
this.listener_.call(this.handler_, goog.now());
};
/** @override */
goog.async.AnimationDelay.prototype.disposeInternal = function() {
'use strict';
this.stop();
goog.async.AnimationDelay.base(this, 'disposeInternal');
};
/**
* @return {?function(function(number)): number} The requestAnimationFrame
* function, or null if not available on this browser.
* @private
*/
goog.async.AnimationDelay.prototype.getRaf_ = function() {
'use strict';
var win = this.win_;
return win.requestAnimationFrame || win.webkitRequestAnimationFrame ||
win.mozRequestAnimationFrame || win.oRequestAnimationFrame ||
win.msRequestAnimationFrame || null;
};
/**
* @return {?function(number): undefined} The cancelAnimationFrame function,
* or null if not available on this browser.
* @private
*/
goog.async.AnimationDelay.prototype.getCancelRaf_ = function() {
'use strict';
var win = this.win_;
return win.cancelAnimationFrame || win.cancelRequestAnimationFrame ||
win.webkitCancelRequestAnimationFrame ||
win.mozCancelRequestAnimationFrame || win.oCancelRequestAnimationFrame ||
win.msCancelRequestAnimationFrame || null;
};