Henning Snater has submitted this change and it was merged.

Change subject: Introduces jQuery.PurposedCallbacks constructor
......................................................................


Introduces jQuery.PurposedCallbacks constructor

An instance of jQuery.PurposedCallbacks is a list of $.Callbacks instances, one 
per "purpose".
A purpose is defined by a string. When registering a callback, an associated 
purpose has to
be given as well.

Change-Id: I312c0d52979cb276d6ed0336c53347236369a64c
---
M ValueView/ValueView.resources.mw.php
M ValueView/ValueView.tests.qunit.php
A ValueView/resources/jquery/jquery.PurposedCallbacks.js
A ValueView/tests/qunit/jquery/jquery.PurposedCallbacks.tests.js
4 files changed, 558 insertions(+), 0 deletions(-)

Approvals:
  Henning Snater: Looks good to me, approved



diff --git a/ValueView/ValueView.resources.mw.php 
b/ValueView/ValueView.resources.mw.php
index 317b46c..eee38bf 100644
--- a/ValueView/ValueView.resources.mw.php
+++ b/ValueView/ValueView.resources.mw.php
@@ -37,6 +37,15 @@
                ),
 
                // Dependencies required by jQuery.valueview library:
+               'jquery.PurposedCallbacks' => $moduleTemplate + array(
+                       'scripts' => array(
+                               'jquery/jquery.PurposedCallbacks.js',
+                       ),
+                       'dependencies' => array(
+                               'jquery',
+                       ),
+               ),
+
                '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 ed91251..3c1809f 100644
--- a/ValueView/ValueView.tests.qunit.php
+++ b/ValueView/ValueView.tests.qunit.php
@@ -20,6 +20,15 @@
        $bp = 'tests/qunit';
 
        return array(
+               'jquery.PurposedCallbacks.tests' => array(
+                       'scripts' => array(
+                               "$bp/jquery/jquery.PurposedCallbacks.tests.js",
+                       ),
+                       'dependencies' => array(
+                               'jquery.PurposedCallbacks',
+                       ),
+               ),
+
                'jquery.eachchange.tests' => array(
                        'scripts' => array(
                                "$bp/jquery/jquery.eachchange.tests.js",
diff --git a/ValueView/resources/jquery/jquery.PurposedCallbacks.js 
b/ValueView/resources/jquery/jquery.PurposedCallbacks.js
new file mode 100644
index 0000000..671b585
--- /dev/null
+++ b/ValueView/resources/jquery/jquery.PurposedCallbacks.js
@@ -0,0 +1,242 @@
+/**
+ * jQuery.PurposedCallbacks and jQuery.PurposedCallbacks.Facade constructors.
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < daniel.a.r.wer...@gmail.com >
+ *
+ * @dependency jQuery
+ */
+jQuery.PurposedCallbacks = ( function( $ ) {
+       'use strict';
+
+       /**
+        * An instance of jQuery.PurposedCallbacks is a list of $.Callbacks 
instances, one per
+        * "purpose". The purposes are string identifiers for groups of 
callbacks. Callbacks can be
+        * registered for a purpose. Callbacks registered for one purpose will 
be executed together.
+        *
+        * This is conceptually similar to jQuery.Deferred but more flexible 
since it allows to define
+        * custom purposes other than "done", "fail" and "progress".
+        * Also, there is an equivalent to jQuery.Deferred.prototype.promise 
which is
+        * jQuery.PurposedCallbacks.prototype.facade.
+        *
+        * @constructor
+        *
+        * @example <code>
+        *   function someAction() {
+        *     var callbacks = $.PurposedCallbacks( [ 'done', 'fail' ] );
+        *
+        *     someAsynchronousAction(
+        *       function() {
+        *         callbacks.fire( 'done' );
+        *       },
+        *       function( errorMsg ) {
+        *         callbacks.fire( 'fail', errorMsg );
+        *       }
+        *    );
+        *
+        *     // Only expose object for registering more callbacks, but not 
for firing them:
+        *     return callbacks.facade();
+        *  }
+        *  someAction()
+        *    .add( 'done', function() { alert( 'done' ) } );
+        *    .add( 'fail', function( reason ) { alert( 'error: ' + reason ) } 
);
+        *    .add( 'fail', function( reason ) { alert( 'ERROR!' } );
+        * </code>
+        *
+        * @param {string[]} [predefinedPurposes] Allows to predefine which 
purposes are allowed.
+        * @param {string} [callbackOptions] Same options as for 
jQuery.Callbacks, will be forwarded.
+        * @return {jQuery.PurposedCallbacks} Can be instantiated without "new".
+        */
+       var SELF = function PurposedCallbacks( predefinedPurposes, 
callbackOptions ) {
+               if( !( this instanceof SELF ) ) {
+                       return new SELF( predefinedPurposes, callbackOptions );
+               }
+
+               if( typeof predefinedPurposes === 'string' ) {
+                       callbackOptions = predefinedPurposes;
+                       predefinedPurposes = undefined;
+               }
+
+               /**
+                * cache for .facade() member.
+                * @type jQuery.PurposedCallbacks.Facade
+                */
+               var facade;
+
+               /**
+                * @type {Object} Field names are purposes, each holding a 
jQuery.Callbacks instance.
+                */
+               var callbacksPerPurpose = {};
+
+               /**
+                * Registers a callback for some purpose.
+                *
+                * @param {string} purpose
+                * @param {Function|Function[]} callbacks
+                * @return {*}
+                *
+                * @throws {Error} If purposes are predefined via the 
constructor and "purpose" is not
+                *         one of them.
+                */
+               this.add = function( purpose, callbacks ) {
+                       if( predefinedPurposes && !this.has( purpose ) ) {
+                               throw new Error( 'Unknown purpose "' + purpose 
+ '".' );
+                       }
+
+                       if( !callbacksPerPurpose[ purpose ] ) {
+                               callbacksPerPurpose[ purpose ] = $.Callbacks( 
callbackOptions );
+                       }
+                       callbacksPerPurpose[ purpose ].add( callbacks );
+
+                       return this;
+               };
+
+               /**
+                * Removes a callback from a purpose.
+                *
+                * @param {string} purpose
+                * @param {Function|Function[]} callbacks
+                * @return {*}
+                */
+               this.remove = function( purpose, callbacks ) {
+                       var callbackForPurpose = callbacksPerPurpose[ purpose ];
+                       if( callbackForPurpose ) {
+                               // NOTE: We can't remove( callbacks ) even 
though it is documented to work this way.
+                               //  This is a bug (behavior not according to 
jQuery API documentation) which is
+                               //  still present in jQuery 2.0
+                               callbacks = $.isArray( callbacks ) ? callbacks 
: [ callbacks ];
+                               callbackForPurpose.remove.apply( 
callbackForPurpose, callbacks );
+                       }
+                       return this;
+               };
+
+               /**
+                * Returns whether a given purpose is known or whether a 
specific callback has been
+                * registered for a purpose.
+                *
+                * @param {string} purpose
+                * @param {Function} [callback]
+                * @return {boolean}
+                */
+               this.has = function( purpose, callback ) {
+                       if( callback ) {
+                               var callbacksForPurpose = callbacksPerPurpose[ 
purpose ];
+                               return callbacksForPurpose && 
callbacksForPurpose.has( callback );
+                       }
+                       return $.inArray( purpose, this.purposes() ) > -1;
+               };
+
+               /**
+                * Returns all purposes used for registering callbacks. If 
purposes got defined via the
+                * constructor, then all those purposes will be returned. If 
purposes were not defined via
+                * the constructor, then all purposes used to register a 
callback via "add" will be returned,
+                * even if the callback got removed again and there are no 
callbacks left for the queue of
+                * that purpose.
+                *
+                * @return {string[]}
+                */
+               this.purposes = function() {
+                       if( predefinedPurposes ) {
+                               return predefinedPurposes.slice();
+                       }
+                       var usedPurposes = [];
+                       for( var purpose in callbacksPerPurpose ) {
+                               usedPurposes.push( purpose );
+                       }
+                       return usedPurposes;
+               };
+
+               /**
+                * Fires the callbacks of the given purposes or of all purposes 
with the provided
+                * arguments. Context for the callbacks will be the 
PurposedCallbacks or the
+                * PurposedCallbacks.Facade instance the callback has been 
added with.
+                *
+                * @param {string|string[]} purposes
+                * @param {array} [args]
+                */
+               this.fire = function( purposes, args ) {
+                       this.fireWith( this, purposes, args );
+                       return this;
+               };
+
+               /**
+                * same as fire() but with the callbacks called in a given 
context.
+                *
+                * @param {*} context
+                * @param {string|string[]} purposes
+                * @param [args]
+                * @return {*}
+                *
+                * @throws {Error} In case purposes were defined in the 
constructor and a purpose given
+                *         here is not one of them.
+                */
+               this.fireWith = function( context, purposes, args ) {
+                       if( !$.isArray( purposes ) ) {
+                               purposes = [ purposes ];
+                       }
+                       args = args || [];
+                       var missingPurposes = [];
+
+                       $.each( purposes, function( i, purpose ) {
+                               var callbacksForPurpose = callbacksPerPurpose[ 
purpose ];
+                               if( callbacksForPurpose ) {
+                                       callbacksForPurpose.fireWith( context, 
args );
+                               } else {
+                                       missingPurposes.push( purpose );
+                               }
+                       } );
+
+                       if( predefinedPurposes && missingPurposes.length ) {
+                               var unknownPurposes = $( missingPurposes ).not( 
predefinedPurposes );
+                               if( unknownPurposes.length ) {
+                                       throw new Error( 'Can not fire 
callbacks for unknown purposes.' );
+                               }
+                       }
+                       return this;
+               };
+
+               /**
+                * Returns a facade to the object, only allowing for 
adding/removing new callbacks but not
+                * allowing to fire them. Similar to jQuery.Deferred's 
promise().
+                * The object returned here could for example be exposed as 
return value of a function
+                * which is initiating some asynchronous action. After the 
asynchronous action is done,
+                * the "fire" function will be called by the function. The code 
which received the facade
+                * by the function can only add and remove callbacks but not 
fire them.
+                *
+                * @return jQuery.PurposedCallbacks.Facade
+                */
+               this.facade = function() {
+                       if( !facade ) {
+                               facade = SELF.Facade( this );
+                       }
+                       return facade;
+               };
+       };
+
+       /**
+        * Facade of jQuery.PurposedCallbacks which only allows access to the 
"add", "remove", "has"
+        * and "purposes" members.
+        * This is to jQuery.PurposedCallbacks what the "Promise" is to 
jQuery.Deferred.
+        *
+        * @constructor
+        *
+        * @param {jQuery.PurposedCallbacks} base
+        * @return {jQuery.PurposedCallbacks.Facade} Can be instantiated 
without "new".
+        */
+       SELF.Facade = function PurposedCallbacksFacade( base ) {
+               if( !( this instanceof SELF.Facade ) ) {
+                       return new SELF.Facade( base );
+               }
+               var self = this;
+
+               $.each( [ 'add', 'remove', 'has', 'purposes' ], function( i, 
field ) {
+                       self[ field ] = function() {
+                               var result = base[ field ].apply( base, 
arguments );
+                               return result === base ? this : result;
+                       };
+               } );
+       };
+
+       return SELF;
+
+}( jQuery ) );
diff --git a/ValueView/tests/qunit/jquery/jquery.PurposedCallbacks.tests.js 
b/ValueView/tests/qunit/jquery/jquery.PurposedCallbacks.tests.js
new file mode 100644
index 0000000..80a887c
--- /dev/null
+++ b/ValueView/tests/qunit/jquery/jquery.PurposedCallbacks.tests.js
@@ -0,0 +1,298 @@
+/**
+ * QUnit tests for 'jQuery.PurposedCallbacks'.
+ *
+ * @file
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < daniel.wer...@wikimedia.de >
+ */
+( function( $, QUnit, PurposedCallbacks ) {
+       'use strict';
+       /* jshint newcap: false */
+
+       QUnit.module( 'jquery.PurposedCallbacks' );
+
+       QUnit.test( 'construction', function( assert ) {
+               var pc = PurposedCallbacks();
+
+               assert.ok(
+                       pc instanceof PurposedCallbacks,
+                       'Instantiated without "new".'
+               );
+       } );
+
+       QUnit.test( 'facade()', function( assert ) {
+               var pc = PurposedCallbacks();
+
+               assert.ok(
+                       pc.facade() instanceof PurposedCallbacks.Facade,
+                       'Returns instance of PurposedCallbacks.Facade.'
+               );
+
+               assert.ok(
+                       pc.facade() === pc.facade(),
+                       'Always returns the same facade instance and does not 
create a new one.'
+               );
+       } );
+
+       QUnit.test( 'puposes() on instance without predefined purposes', 
function( assert ) {
+               var pcf = PurposedCallbacks().facade();
+
+               assert.ok(
+                       $.isArray( pcf.purposes() ) && pcf.purposes().length 
=== 0,
+                       'Returns an empty array.'
+               );
+
+               pcf.add( 'foo', $.noop );
+               pcf.purposes().length = 0;
+               assert.deepEqual(
+                       pcf.purposes(),
+                       [ 'foo' ],
+                       'Returns a newly added purpose "foo", modifying the 
returned array has no effect ' +
+                               'on next returned array (no reference to 
internal object).'
+               );
+
+               pcf.add( 'bar', $.noop );
+               assert.deepEqual(
+                       pcf.purposes(),
+                       [ 'foo', 'bar' ],
+                       'Returns a newly added purpose "bar" and the old "foo".'
+               );
+
+               pcf.remove( 'foo', $.noop );
+               assert.deepEqual(
+                       pcf.purposes(),
+                       [ 'foo', 'bar' ],
+                       'Still returns "foo" as known purpose after removing 
only callback of "foo".'
+               );
+       } );
+
+       QUnit.test( 'purposes() on instance with predefined purposes', 
function( assert ) {
+               var purposes = [ 'foo', 'bar' ];
+               var pcf = PurposedCallbacks( purposes ).facade();
+
+               assert.deepEqual(
+                       pcf.purposes(),
+                       purposes,
+                       'Returns all predefined purposes initially.'
+               );
+
+               assert.ok(
+                       pcf.purposes() !== purposes,
+                       'Returned array is a copy of the predefined purposes, 
not the same object.'
+               );
+
+               pcf.add( 'foo', $.noop );
+               pcf.remove( 'foo', $.noop );
+               assert.deepEqual(
+                       pcf.purposes(),
+                       purposes,
+                       'Still returns all purposes after adding and removing a 
callback .'
+               );
+       } );
+
+       QUnit.test( 'chainable members', function( assert ) {
+               var pc = PurposedCallbacks();
+               var pcf = pc.facade();
+
+               assert.ok(
+                       pcf.add( 'foo', $.noop ) === pcf,
+                       'add() is chainable.'
+               );
+               assert.ok(
+                       pcf.remove( 'foo', $.noop ) === pcf,
+                       'remove() is chainable.'
+               );
+
+               // Chainable on base only, not a member of the facade:
+               assert.ok(
+                       pc.fire( 'foo' ),
+                       'fire() is chainable'
+               );
+               assert.ok(
+                       pc.fireWith( 'foo', this ),
+                       'fireWith() is chainable'
+               );
+       } );
+
+       QUnit.test( 'add() callback of unknown purpose', function( assert ) {
+               var pcf = PurposedCallbacks( [ 'bar' ] ).facade();
+               assert.throws(
+                       function() {
+                               pcf.add( 'foo', $.noop );
+                       },
+                       'Can not add callback for purpose not stated in list of 
predefined purposes.'
+               );
+       } );
+
+       QUnit.test( 'fire()', function( assert ) {
+               var pc = PurposedCallbacks();
+               var pcf = pc.facade();
+
+               var fired1 = 0;
+               var fired2 = 0;
+               var resetFired = function() { fired1 = fired2 = 0; };
+
+               assert.ok(
+                       pcf.fire === undefined,
+                       'fire() is not a member of the facade.'
+               );
+
+               pcf.add( '1', function() {
+                       fired1++;
+               } );
+               pcf.add( '2', function() {
+                       fired2++;
+               } );
+
+               pc.fire( '1' );
+               assert.ok(
+                       fired1 === 1 && fired2 === 0,
+                       'Fired callback of stated purpose. Purpose can be given 
as string.'
+               );
+
+               resetFired();
+
+               pc.fire( [ '2' ] );
+               assert.ok(
+                       fired1 === 0 && fired2 === 1,
+                       'Fired callback of another stated purpose. Purpose can 
be given as array of strings.'
+               );
+
+               resetFired();
+
+               pc.fire( [ '1', '2' ] );
+               assert.ok(
+                       fired1 === 1 && fired2 === 1,
+                       'Fired callback of both stated purposes together'
+               );
+       } );
+
+       QUnit.test( 'fire() with custom arguments', 1, function( assert ) {
+               var pc = PurposedCallbacks();
+               var pcf = pc.facade();
+               var args = [ {}, [] ];
+
+               pcf.add( 'foo', function( arg1, arg2 ) {
+                       assert.ok(
+                               arg1 === args[0] && arg2 === args[1],
+                               'Arguments get passed to the callback.'
+                       );
+               } );
+               pc.fire( 'foo', args );
+       } );
+
+       // Helper callbacks for the following tests:
+       var fn1 = function() { this.push( 1 ); };
+       var fn2 = function() { this.push( 2 ); };
+       var fn3 = function() { this.push( 3 ); };
+       var fn4 = function() { this.push( 4 ); };
+
+       QUnit.test( 'add() verified by fireWith()', function( assert ) {
+               var pc = PurposedCallbacks();
+               var pcf = pc.facade();
+
+               pcf.add( 'foo', fn1 );
+               pcf.add( 'bar', [ fn2, fn3 ] );
+               pcf.add( 'foo', fn4 );
+
+               var feedback = [];
+               pc.fireWith( feedback, [ 'foo', 'bar' ] );
+               assert.deepEqual(
+                       feedback,
+                       [ 1, 4, 2, 3 ],
+                       'Executed all callbacks in expected order. Verified add 
with single function and ' +
+                               'with array of functions.'
+               );
+
+               feedback = [];
+               pc.fireWith( feedback, [ 'bar', 'foo' ] );
+               assert.deepEqual(
+                       feedback,
+                       [ 2, 3, 1, 4 ],
+                       'Executed all callbacks in expected order. Verified 
callbacks of first purpose given ' +
+                               'in "fireWith" are fired first.'
+               );
+       } );
+
+       QUnit.test( 'remove() verified by fireWith()', function( assert ) {
+               var pc = PurposedCallbacks();
+               var pcf = pc.facade();
+
+               pcf.add( 'foo', fn1 );
+               pcf.add( 'foo', fn2 );
+               pcf.remove( 'foo', fn2 );
+               pcf.add( 'foo', fn3 );
+               pcf.add( 'bar', fn4 );
+               pcf.remove( 'foo', fn4 ); // Not registered for the purpose, so 
fn4 should still be called.
+               pcf.add( 'bar', fn1 );
+
+               var feedback = [];
+               pc.fireWith( feedback, [ 'foo', 'bar' ] );
+               assert.deepEqual(
+                       feedback,
+                       [ 1, 3, 4, 1 ],
+                       'Executed all callbacks registered for purpose, except 
the ones removed again.'
+               );
+
+               var feedback = [];
+               pcf.remove( 'bar', [ fn1, fn4 ] );
+               pc.fireWith( feedback, [ 'foo', 'bar' ] );
+               assert.deepEqual(
+                       feedback,
+                       [ 1, 3 ],
+                       'remove() can remove all callbacks given in an array.'
+               );
+       } );
+
+       QUnit.test( 'has()', function( assert ) {
+               var pc = PurposedCallbacks();
+               var pcf = pc.facade();
+
+               pcf.add( 'foo', $.noop );
+               assert.ok(
+                       pcf.has( 'foo' ),
+                       'Newly defined purpose exists.'
+               );
+               assert.ok(
+                       pcf.has( 'foo', $.noop ),
+                       'Callback for that purpose is recognized as existent as 
well.'
+               );
+               assert.ok(
+                       !pcf.has( 'bar' ),
+                       'Purpose without callbacks does not exist.'
+               );
+
+               pcf.remove( 'foo', $.noop );
+               assert.ok(
+                       pcf.has( 'foo' ),
+                       'Purpose still exists after removing only callback from 
it.'
+               );
+       } );
+
+       QUnit.test( 'has() on instance with predefined purposes', function( 
assert ) {
+               var pc = PurposedCallbacks( [ 'foo' ] );
+               var pcf = pc.facade();
+
+               assert.ok(
+                       pcf.has( 'foo' ),
+                       'Predefined purpose exists.'
+               );
+
+               pcf.add( 'foo', $.noop );
+               pcf.remove( 'foo', $.noop );
+               assert.ok(
+                       pcf.has( 'foo' ),
+                       'Predefined purpose exists still exists after adding 
and removing callback from it.'
+               );
+               assert.ok(
+                       !pcf.has( 'foo', $.noop ),
+                       'Callback got removed though.'
+               );
+
+               assert.ok(
+                       !pcf.has( 'bar' ),
+                       'Purpose without callbacks does not exist.'
+               );
+       } );
+
+}( jQuery, QUnit, jQuery.PurposedCallbacks ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/79145
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I312c0d52979cb276d6ed0336c53347236369a64c
Gerrit-PatchSet: 8
Gerrit-Project: mediawiki/extensions/DataValues
Gerrit-Branch: master
Gerrit-Owner: Daniel Werner <daniel.wer...@wikimedia.de>
Gerrit-Reviewer: Henning Snater <henning.sna...@wikimedia.de>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to