Daniel Werner has uploaded a new change for review.
https://gerrit.wikimedia.org/r/79145
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, 521 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DataValues
refs/changes/45/79145/1
diff --git a/ValueView/ValueView.resources.mw.php
b/ValueView/ValueView.resources.mw.php
index 8c06f91..6a67c0b 100644
--- a/ValueView/ValueView.resources.mw.php
+++ b/ValueView/ValueView.resources.mw.php
@@ -52,6 +52,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 cb502bd..97682fa 100644
--- a/ValueView/ValueView.tests.qunit.php
+++ b/ValueView/ValueView.tests.qunit.php
@@ -35,6 +35,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..fc577ef
--- /dev/null
+++ b/ValueView/resources/jquery/jquery.PurposedCallbacks.js
@@ -0,0 +1,205 @@
+/**
+ * jQuery.PurposedCallbacks and jQuery.PurposedCallbacks.Facade constructors.
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ *
+ * @dependency jQuery
+ */
+jQuery.PurposedCallbacks = ( function( $ ) {
+ 'use strict';
+
+ /**
+ * 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.
+ *
+ * @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".
+ * @constructor
+ */
+ 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]
+ * @returns {*}
+ */
+ 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().
+ *
+ * @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.
+ *
+ * @param {jQuery.PurposedCallbacks} base
+ * @return {jQuery.PurposedCallbacks.Facade} Can be instantiated
without "new".
+ * @constructor
+ */
+ 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..e5426f8
--- /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 < [email protected] >
+ */
+( 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: newchange
Gerrit-Change-Id: I312c0d52979cb276d6ed0336c53347236369a64c
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