Daniel Werner has uploaded a new change for review.
https://gerrit.wikimedia.org/r/79146
Change subject: Introduces jQuery.AnimationEvent and jQuery.fn.animateWithEvent
......................................................................
Introduces jQuery.AnimationEvent and jQuery.fn.animateWithEvent
jQuery.fn.animateWithEvent allows for efficient event handling during
animations and for simple
extensibility of widgets dealing with animations.
Change-Id: I3be9cd0fefed0152bd3a14fc51c94ff0e11387a3
---
M ValueView/ValueView.resources.mw.php
M ValueView/ValueView.tests.qunit.php
A ValueView/resources/jquery/jquery.AnimationEvent.js
A ValueView/resources/jquery/jquery.animateWithEvent.js
A ValueView/tests/qunit/jquery/jquery.AnimationEvent.tests.js
A ValueView/tests/qunit/jquery/jquery.animateWithEvent.tests.js
6 files changed, 596 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DataValues
refs/changes/46/79146/1
diff --git a/ValueView/ValueView.resources.mw.php
b/ValueView/ValueView.resources.mw.php
index 6a67c0b..f4bd0ab 100644
--- a/ValueView/ValueView.resources.mw.php
+++ b/ValueView/ValueView.resources.mw.php
@@ -61,6 +61,16 @@
),
),
+ 'jquery.animateWithEvent' => $moduleTemplate + array(
+ 'scripts' => array(
+ 'jquery/jquery.AnimationEvent.js',
+ 'jquery/jquery.animateWithEvent.js',
+ ),
+ 'dependencies' => array(
+ 'jquery.PurposedCallbacks',
+ ),
+ ),
+
'jquery.eachchange' => $moduleTemplate + array(
'scripts' => array(
'jquery/jquery.eachchange.js'
diff --git a/ValueView/ValueView.tests.qunit.php
b/ValueView/ValueView.tests.qunit.php
index 97682fa..e909c30 100644
--- a/ValueView/ValueView.tests.qunit.php
+++ b/ValueView/ValueView.tests.qunit.php
@@ -44,6 +44,16 @@
),
),
+ 'jquery.animateWithEvent.tests' => array(
+ 'scripts' => array(
+ "$bp/jquery/jquery.AnimationEvent.tests.js",
+ "$bp/jquery/jquery.animateWithEvent.tests.js",
+ ),
+ 'dependencies' => array(
+ 'jquery.animateWithEvent',
+ ),
+ ),
+
'jquery.eachchange.tests' => array(
'scripts' => array(
"$bp/jquery/jquery.eachchange.tests.js",
diff --git a/ValueView/resources/jquery/jquery.AnimationEvent.js
b/ValueView/resources/jquery/jquery.AnimationEvent.js
new file mode 100644
index 0000000..540f072
--- /dev/null
+++ b/ValueView/resources/jquery/jquery.AnimationEvent.js
@@ -0,0 +1,142 @@
+/**
+ * @licence GNU GPL v2+
+ * @version 0.1
+ * @author Daniel Werner < [email protected] >
+ *
+ * @dependency jQuery
+ */
+jQuery.AnimationEvent = ( function( $, PurposedCallbacks ) {
+ 'use strict';
+
+ /**
+ * Event for notifying about an animation which is about to be added to
an animation queue.
+ *
+ * @since 0.1
+ *
+ * @param {string} animationPurpose Will be available as
"animationPurpose" field. This will
+ * not end up as the event's "type". The "type" will always be
set to "animation".
+ * @param {Object} props Additional event properties which will be
copied into the object.
+ * @return {jQuery.AnimationEvent} Can be instantiated without "new".
+ * @constructor
+ * @extends jQuery.Event
+ */
+ var SELF = function AnimationEvent( animationPurpose, props ) {
+ if( !( this instanceof SELF ) ) {
+ return new SELF( animationPurpose, props );
+ }
+ if( typeof animationPurpose !== 'string' || $.trim(
animationPurpose ) === '' ) {
+ throw new Error( 'An animation purpose has to be
stated' );
+ }
+
+ // Apply "parent" constructor:
+ $.Event.call( this, 'animation', props );
+
+ var self = this;
+ var callbacksList = new PurposedCallbacks( SELF.ANIMATION_STEPS
);
+
+ /**
+ * The purpose stated for the animation which is about to be
started.
+ *
+ * @type {string}
+ * @since 0.1
+ */
+ this.animationPurpose = animationPurpose;
+
+ /**
+ * A jQuery.PurposedCallbacks.Facade which allows for
registering callbacks for different
+ * stages of the animation which is about to be started. The
possible stages are those
+ * defined in jQuery.AnimationEvent.ANIMATION_STEPS.
+ *
+ * @see http://api.jquery.com/animate For a detailed
description for each animation stage.
+ * @example event.animationCallbacks.add( 'step', function() {
... } );
+ *
+ * @type {jQuery.PurposedCallbacks.Facade}
+ * @since 0.1
+ */
+ this.animationCallbacks = callbacksList.facade();
+
+ /**
+ * The jQuery.Animation object associated with the event. This
will only be set if the
+ * animationOptions() object generated by this instance is used
as options for an animation
+ * and after the animation has been started (not just queued).
+ *
+ * @type {Object} A jQuery.Promise with some additional fields.
See jQuery.Animation.
+ * @since 0.1
+ */
+ this.animation = null;
+
+ /**
+ * Returns an object which can be used as options for
jQuery.animate or any shortcut
+ * version of it (e.g. jQuery.fadeIn). Defines all animation
callback fields with functions
+ * which will trigger all registered callbacks and all
callbacks still registered to the
+ * "animationCallbacks" field's jQuery.PurposedCallbacks.Facade
object in the future.
+ * Optionally, an object of options to be mixed in can be
given. If this object has
+ * callbacks defined already, then these callbacks will be
mixed in and called first.
+ *
+ * IMPORTANT: The options generated by one AnimationEvent
instance should only be used for
+ * one animation.
+ *
+ * @since 0.1
+ *
+ * @param {Object} [baseOptions]
+ * @returns {*}
+ *
+ * @throws {Error} If animationOptions() has been called
already and the returned options
+ * have been passed to some animation whose execution
has started already.
+ */
+ this.animationOptions = function( baseOptions ) {
+ if( this.animation ) {
+ throw new Error( 'The AnimationEvent
instance\'s generated animation options are ' +
+ 'used within some animation already,
they can not be used in two animations.' );
+ }
+ baseOptions = baseOptions || {};
+ var options = $.extend( {}, baseOptions );
+
+ $.each( SELF.ANIMATION_STEPS, function( i, purpose ) {
+ // Consider callbacks defined in the given
options, they should be called first.
+ var baseCallback = baseOptions[ purpose ];
+ var finalCallback = function() {
+ if( baseCallback ) {
+ baseCallback.apply( this,
arguments );
+ }
+ callbacksList.fireWith( this, [ purpose
], arguments );
+ };
+ if( purpose === 'start' ) {
+ options.start = function() {
+ // If "start" gets called, this
means the generated options are used within
+ // an jQuery.Animation. Tell
the AnimationEvent instance which has created
+ // these options which
jQuery.Animation object it is related to.
+ var animation = arguments[0];
+ if( self.animation &&
self.animation !== animation ) {
+ throw new Error( 'Can
not use the same AnimationEvent instance\'s '
+ +
'animationOptions() for two different animations.' );
+ }
+ self.animation = animation;
+ finalCallback.apply( this,
arguments );
+ };
+ } else {
+ options[ purpose ] = finalCallback;
+ }
+ } );
+ return options;
+ };
+ };
+
+ // Inherit from $.Event but remove certain fields this will create.
This should not actually
+ // matter since they will be overwritten when creating an instance, but
do it the "clean" way
+ // anyhow:
+ SELF.prototype = new $.Event();
+ delete( SELF.prototype.timeStamp );
+ delete( SELF.prototype[ jQuery.expando ] );
+
+ /**
+ * All animation step callback option names usable in
jQuery.Animation's options
+ *
+ * @type {string[]}
+ * @since 0.1
+ */
+ SELF.ANIMATION_STEPS = [ 'start', 'step', 'progress', 'complete',
'done', 'fail', 'always' ];
+
+ return SELF;
+
+}( jQuery, jQuery.PurposedCallbacks ) );
diff --git a/ValueView/resources/jquery/jquery.animateWithEvent.js
b/ValueView/resources/jquery/jquery.animateWithEvent.js
new file mode 100644
index 0000000..f45891f
--- /dev/null
+++ b/ValueView/resources/jquery/jquery.animateWithEvent.js
@@ -0,0 +1,95 @@
+/**
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ *
+ * @dependency jQuery.AnimationEvent
+ */
+jQuery.fn.animateWithEvent = ( function( $ ) {
+ 'use strict';
+
+ /**
+ * Same as jQuery.fn.animate or any other animation function with the
difference that for each
+ * element to be animated, a jQuery.AnimationEvent will be created. The
AnimationEvent instance
+ * will be available as first parameter in the "startCallback" which
will be called for each
+ * element's animation when the animation is about to start.
+ *
+ * The "startCallback" can be used to trigger an event, stating that an
animation is about to
+ * be executed. In the event the AnimationEvent can be used to allow
event listeners to add
+ * specialized callbacks per animation stage (see AnimationEvent
documentation).
+ *
+ * @example <code>
+ * $.animationWithEvent(
+ * 'mywidgetsgreatanimation',
+ * 'fadeIn',
+ * { duration: 200 },
+ * function( animationEvent ) {
+ * self._trigger( 'animation', animationEvent );
+ * }
+ * );
+ * </code>
+ *
+ * @param {string} animationPurpose Will be forwarded to
jQuery.AnimationEvent.
+ * @param {string|Object} animationProperties Name of a jQuery.fn
member which is dedicated to
+ * some animation (e.g. "fadeIn") and takes an "options"
argument. Can also be an object
+ * of properties to animate, in this case jQuery.fn.animate will
be used.
+ * @param {Object} [options] Options passed to the animation
("duration", "easing" etc.).
+ * @param {Function( jQuery.AnimationEvent event )} [startCallback]
Callback which will be fired
+ * before the animation starts. This is different from the
options.start callback since
+ * it will get a jQuery.AnimationEvent instance. Also, the
callback will be triggered
+ * before any options.start callback.
+ * @returns {*}
+ *
+ * @throws {Error} If animationProperties is a string but not a member
of jQuery.fn
+ * @throws {Error}
+ */
+ return function animateWithEvent( animationPurpose,
animationProperties, options, startCallback ) {
+ var animationFunction;
+ if( typeof animationProperties !== 'string' ) {
+ // jQuery.fn.animate( animationProperties, options )
+ animationFunction = 'animate';
+ animationProperties = animationProperties || {}; //
allow "empty" animation
+ } else {
+ // E.g. "fadeIn", properties are predefined in
jQuery.fn.fadeIn( options ).
+ animationFunction = animationProperties;
+ animationProperties = false;
+ if( !$.isFunction( $.fn[ animationFunction ] ) ) {
+ throw new Error( 'jQuery.fn."' +
animationFunction + '" is not a function.' );
+ }
+ }
+
+ if( $.isFunction( options ) || !options ) {
+ startCallback = options;
+ options = {};
+ }
+ startCallback = startCallback || $.noop;
+
+ $.each( this, function( i, elem ) {
+ var animationEvent = $.AnimationEvent( animationPurpose
);
+
+ // The animation options generated by the event will
have all animation stage fields
+ // defined with callbacks that will fire the related
callbacks registered to the
+ // animationEvent's "animationCallbacks" field in the
future.
+ var animationOptions = animationEvent.animationOptions(
$.extend( {}, options, {
+ start: function() {
+ // startCallback could for example
trigger an "animation" event within a widget.
+ // All event listeners can then
register their callbacks for different animation
+ // stages to
animationEvent.animationCallbacks.
+ startCallback.call( this,
animationEvent );
+ if( options.start ) {
+ options.start.apply( this,
arguments );
+ }
+ }
+ } ) );
+
+ if( animationProperties ) {
+ // animate()
+ $( elem )[ animationFunction ](
animationProperties, animationOptions );
+ } else {
+ // any dedicated animation function, e.g.
"fadeIn"
+ $( elem )[ animationFunction ](
animationOptions );
+ }
+ } );
+ return this;
+ };
+
+}( jQuery ) );
diff --git a/ValueView/tests/qunit/jquery/jquery.AnimationEvent.tests.js
b/ValueView/tests/qunit/jquery/jquery.AnimationEvent.tests.js
new file mode 100644
index 0000000..98f5374
--- /dev/null
+++ b/ValueView/tests/qunit/jquery/jquery.AnimationEvent.tests.js
@@ -0,0 +1,170 @@
+/**
+ * QUnit tests for 'jQuery.AnimationEvent'.
+ *
+ * @file
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ */
+( function( $, QUnit, AnimationEvent, PurposedCallbacks ) {
+ 'use strict';
+ /* jshint newcap: false */
+
+ QUnit.module( 'jquery.AnimationEvent' );
+
+ function assertSuccessfulConstruction( assert, instance, purpose ) {
+ assert.ok(
+ instance instanceof AnimationEvent,
+ 'Instantiated'
+ );
+ assert.ok(
+ instance instanceof $.Event,
+ 'Instance of jQuery.Event.'
+ );
+ assert.strictEqual(
+ instance.animationPurpose,
+ purpose,
+ 'Animation purpose got copied into "animationPurpose"
field.'
+ );
+ assert.ok(
+ instance.animationCallbacks instanceof
PurposedCallbacks.Facade,
+ '"animationCallbacks" field is instance of
jQuery.PurposedCallbacks.Facade'
+ );
+ assert.strictEqual(
+ instance.type,
+ 'animation',
+ '"type" field is set to "animation"'
+ );
+ }
+
+ QUnit.test( 'construction without "new"', function( assert ) {
+ assertSuccessfulConstruction( assert, AnimationEvent(
'nopurpose' ), 'nopurpose' );
+ } );
+
+ QUnit.test( 'construction with "new"', function( assert ) {
+ assertSuccessfulConstruction( assert, new AnimationEvent( 'foo'
), 'foo' );
+ } );
+
+ QUnit.test( 'construction with custom fields given', function( assert )
{
+ var fields = {
+ someCustomField1: 'foo',
+ someCustomField2: {}
+ };
+ var event = AnimationEvent( 'someanimation', fields );
+
+ assertSuccessfulConstruction( assert, event, 'someanimation' );
+
+ assert.ok(
+ event.foo === fields.foo,
+ 'Custom field got copied.'
+ );
+ assert.ok(
+ event.someCustomField2 === fields.someCustomField2,
+ 'Another custom field got copied, copy happens by
reference, no deep extend.'
+ );
+ } );
+
+ QUnit.test( 'animationOptions()', AnimationEvent.ANIMATION_STEPS.length
+ 2, function( assert ) {
+ var event = AnimationEvent( 'animationpurpose' );
+ var predefined = {
+ easing: 'swing',
+ queue: true,
+ duration: 200
+ };
+
+ var options = event.animationOptions( predefined );
+
+ assert.ok(
+ $.isPlainObject( options ),
+ 'Returns a plain object.'
+ );
+ assert.ok(
+ options.easing === predefined.easing
+ && options.queue === predefined.queue
+ && options.duration === predefined.duration,
+ 'Returned object holds all values of the base object
given to animationOptions().'
+ );
+
+ $.each( AnimationEvent.ANIMATION_STEPS, function( i, stepName )
{
+ assert.ok(
+ $.isFunction( options[ stepName ] ),
+ 'Returned options object\'s field "' + stepName
+ '" is a function.'
+ );
+ } );
+ } );
+
+ QUnit.test( 'ANIMATION_STEPS', function( assert ) {
+ assert.ok(
+ $.isArray( AnimationEvent.ANIMATION_STEPS ),
+ 'Is an array.'
+ );
+ // This might be kind of pointless, but simply make sure that
no one changes this without
+ // changing tests as well, being absolutely sure about it.
+ var expectedSteps =
+ [ 'start', 'step', 'progress', 'complete', 'done',
'fail', 'always' ];
+ assert.ok(
+ $( AnimationEvent.ANIMATION_STEPS ).not( expectedSteps
).length === 0
+ && $( expectedSteps ).not(
AnimationEvent.ANIMATION_STEPS ).length === 0,
+ 'Contains expected steps.'
+ );
+ } );
+
+ function testAnimationOptionsGeneratedCallbacks( assert, testStep ) {
+ var event = AnimationEvent( 'animationpurpose' );
+ var predefined = {};
+
+ var firedPredefined, firedCallbacksMember;
+ var resetFired = function() { firedPredefined =
firedCallbacksMember = 0; };
+
+ resetFired();
+
+ predefined[ testStep ] = function() {
+ firedPredefined++;
+ assert.ok(
+ !firedCallbacksMember,
+ 'Predefined "' + testStep + '" callback got
executed first.'
+ );
+ };
+ event.animationCallbacks.add( testStep, function() {
+ firedCallbacksMember++;
+ } );
+
+ var options = event.animationOptions( predefined );
+ options[ testStep ]();
+
+ assert.strictEqual(
+ firedPredefined,
+ 1,
+ 'Fired predefined callback.'
+ );
+ assert.strictEqual(
+ firedCallbacksMember,
+ 1,
+ 'Fired callback registered to event\'s
"animationCallbacks" field.'
+ );
+
+ resetFired();
+
+ // Execute all other generated step callbacks as well, verify
that they execute and that
+ // they do not trigger the testStep's callback again.
+ $.each( AnimationEvent.ANIMATION_STEPS, function( i, step ) {
+ if( step !== testStep ) {
+ options[ step ]();
+ }
+ } );
+ assert.ok(
+ firedPredefined === 0 && firedCallbacksMember === 0,
+ 'Fired callbacks generated for all other option fields,
they are independent of the "'
+ + testStep + '" one.'
+ );
+ }
+
+ $.each( AnimationEvent.ANIMATION_STEPS, function( i, step ) {
+ QUnit.test(
+ 'animationOptions(). ' + step + ' callbacks test',
+ 4,
+ function( assert ) {
+ testAnimationOptionsGeneratedCallbacks( assert,
step );
+ } );
+ } );
+
+}( jQuery, QUnit, jQuery.AnimationEvent, jQuery.PurposedCallbacks ) );
diff --git a/ValueView/tests/qunit/jquery/jquery.animateWithEvent.tests.js
b/ValueView/tests/qunit/jquery/jquery.animateWithEvent.tests.js
new file mode 100644
index 0000000..c51ab1e
--- /dev/null
+++ b/ValueView/tests/qunit/jquery/jquery.animateWithEvent.tests.js
@@ -0,0 +1,169 @@
+/**
+ * QUnit tests for 'jQuery.AnimationEvent'.
+ *
+ * @file
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ */
+( function( $, QUnit ) {
+ 'use strict';
+ /* jshint newcap: false */
+
+ QUnit.module( 'jquery.animateWithEvent' );
+
+ QUnit.test( 'special start callback execution before options.start',
function( assert ) {
+ var optionsStartCallbackDone = 0;
+ var specialStartCallbackDone = 0;
+
+ QUnit.stop();
+ $( '<div/>').animateWithEvent(
+ 'fooeventpurpose',
+ 'fadeOut',
+ {
+ start: function( animation ) {
+ optionsStartCallbackDone++;
+ QUnit.start();
+ }
+ }, function( animationEvent ) {
+ assert.ok(
+ !optionsStartCallbackDone,
+ 'last argument start callback got fired
before options.start callback.'
+ );
+ specialStartCallbackDone++;
+ }
+ );
+
+ assert.strictEqual(
+ optionsStartCallbackDone,
+ 1,
+ 'options.start callback got fired.'
+ );
+ assert.strictEqual(
+ specialStartCallbackDone,
+ 1,
+ 'Last argument start callback got fired.'
+ );
+ } );
+
+ QUnit.test( 'special start callback', function( assert ) {
+ var $elem = $( '<div/>');
+
+ $elem.animateWithEvent(
+ 'foopurpose',
+ { width: 200 },
+ {},
+ function( animationEvent ) {
+ assert.ok(
+ this === $elem.get( 0 ),
+ 'Context of the callback is the DOM
node to be animated.'
+ );
+ assert.ok(
+ animationEvent instanceof
$.AnimationEvent,
+ 'Airst argument is an instance of
jQuery.AnimationEvent.'
+ );
+
+ }
+ );
+ } );
+
+ QUnit.test( 'options.start callback', 2, function( assert ) {
+ var $elem = $( '<div/>');
+ var animationEventsAnimation;
+
+ $elem.animateWithEvent(
+ 'foopurpose',
+ { width: 200 },
+ {
+ start: function( animation ) {
+ assert.ok(
+ this === $elem.get( 0 ),
+ 'Context of the callback is the
DOM node to be animated.'
+ );
+ assert.ok(
+ animation ===
animationEventsAnimation,
+ 'First argument ist the
animation object which is set to the '
+ + 'AnimationEvent
instance\'s "animation" field in the callback '
+ + 'given as
animateWithEvent\'s last argument.'
+ );
+
+ }
+ }, function( animationEvent ) {
+ animationEventsAnimation =
animationEvent.animation;
+ }
+ );
+ } );
+
+ QUnit.test( 'On jQuery set of multiple elements', function( assert ) {
+ var $elems = $( '<div/>' ).add( $( '<span/> ') ).add( $(
'<div/> ') );
+ var $confirmedElems = $();
+ var animationEventInstances = [];
+
+ QUnit.stop();
+ $elems.animateWithEvent( 'fadesomethingin', 'fadeIn', function(
animationEvent ) {
+ var elem = animationEvent.animation.elem;
+ $confirmedElems = $confirmedElems.add( elem );
+
+ if( $.inArray( animationEvent, animationEventInstances
) < 0 ) {
+ animationEventInstances.push( animationEvent );
+ }
+
+ if( $confirmedElems.length >= $elems.length ) {
+ QUnit.start();
+ }
+ } );
+
+ assert.ok(
+ $elems.length === $confirmedElems.length
+ && $elems.not( $confirmedElems ).length === 0,
+ 'Initial callback got called for all ' + $elems.length
+ ' elements of the jQuery set.'
+ );
+
+ assert.strictEqual(
+ animationEventInstances.length,
+ $elems.length,
+ 'Each callback got its own instance of
jQuery.AnimationEvent.'
+ );
+ } );
+
+ QUnit.test( 'Error cases', function( assert ) {
+ assert.throws(
+ function() {
+ $( '<div/>').animateWithEvent(
+ 'fooeventpurpose',
+ 'fooAnimateFunction'
+ );
+ },
+ 'Can not use unknown animation function in arguments.'
+ );
+
+ assert.throws(
+ function() {
+ $( '<div/>').animateWithEvent();
+ },
+ 'Throws error if called without parameters. At least
event purpose has to be given.'
+ );
+ } );
+
+ QUnit.test( 'Two arguments are sufficient', function( assert ) {
+ var $node = $( '<div/>');
+ var result;
+
+ result = $node.animateWithEvent(
+ 'fooeventpurpose',
+ { width: 200 }
+ );
+ assert.ok(
+ result === $node,
+ 'Can call with only first two arguments'
+ );
+
+ result = $node.animateWithEvent(
+ 'xxxevent'
+ );
+ assert.ok(
+ result === $node,
+ 'Can call with only first argument'
+ );
+ } );
+
+}( jQuery, QUnit ) );
--
To view, visit https://gerrit.wikimedia.org/r/79146
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I3be9cd0fefed0152bd3a14fc51c94ff0e11387a3
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/DataValues
Gerrit-Branch: master
Gerrit-Owner: Daniel Werner <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits