user: {
options: new Map(),
tokens: new Map()
- }
+ },
+
+ /**
+ * Registry and firing of events.
+ *
+ * MediaWiki has various interface components that are extended, enhanced
+ * or manipulated in some other way by extensions, gadgets and even
+ * in core itself.
+ *
+ * This framework helps streamlining the timing of when these other
+ * code paths fire their plugins (instead of using document-ready,
+ * which can and should be limited to firing only once).
+ *
+ * Features like navigating to other wiki pages, previewing an edit
+ * and editing itself – without a refresh – can then retrigger these
+ * hooks accordingly to ensure everything still works as expected.
+ *
+ * Example usage:
+ *
+ * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
+ * mw.hook( 'wikipage.content' ).fire( $content );
+ *
+ * Handlers can be added and fired for arbitrary event names at any time. The same
+ * event can be fired multiple times. The last run of an event is memorized
+ * (similar to `$(document).ready` and `$.Deferred().done`).
+ * This means if an event is fired, and a handler added afterwards, the added
+ * function will be fired right away with the last given event data.
+ *
+ * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
+ * Thus allowing flexible use and optimal maintainability and authority control.
+ * You can pass around the `add` and/or `fire` method to another piece of code
+ * without it having to know the event name (or `mw.hook` for that matter).
+ *
+ * var h = mw.hook( 'bar.ready' );
+ * new mw.Foo( .. ).fetch( { callback: h.fire } );
+ *
+ * @class mw.hook
+ */
+ hook: ( function () {
+ var lists = {};
+
+ /**
+ * @method hook
+ * @member mw
+ * @param {string} name Name of hook.
+ * @return {mw.hook}
+ */
+ return function ( name ) {
+ var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) );
+
+ return {
+ /**
+ * Register a hook handler
+ * @param {Function...} handler Function to bind.
+ * @chainable
+ */
+ add: list.add,
+
+ /**
+ * Unregister a hook handler
+ * @param {Function...} handler Function to unbind.
+ * @chainable
+ */
+ remove: list.remove,
+
+ /**
+ * Run a hook.
+ * @param {Mixed...} data
+ * @chainable
+ */
+ fire: function () {
+ return list.fireWith( null, slice.call( arguments ) );
+ }
+ };
+ };
+ }() )
};
}( jQuery ) );
} );
+ QUnit.test( 'mw.hook', 10, function ( assert ) {
+ var hook, add, fire, chars, callback;
+
+ mw.hook( 'test.hook.unfired' ).add( function () {
+ assert.ok( false, 'Unfired hook' );
+ } );
+
+ mw.hook( 'test.hook.basic' ).add( function () {
+ assert.ok( true, 'Basic callback' );
+ } );
+ mw.hook( 'test.hook.basic' ).fire();
+
+ mw.hook( 'test.hook.data' ).add( function ( data1, data2 ) {
+ assert.equal( data1, 'example', 'Fire with data (string param)' );
+ assert.deepEqual( data2, ['two'], 'Fire with data (array param)' );
+ } );
+ mw.hook( 'test.hook.data' ).fire( 'example', ['two'] );
+
+ mw.hook( 'test.hook.chainable' ).add( function () {
+ assert.ok( true, 'Chainable' );
+ } ).fire();
+
+ hook = mw.hook( 'test.hook.detach' );
+ add = hook.add;
+ fire = hook.fire;
+ add( function ( x, y ) {
+ assert.deepEqual( [x, y], ['x', 'y'], 'Detached (contextless) with data' );
+ } );
+ fire( 'x', 'y' );
+
+ mw.hook( 'test.hook.fireBefore' ).fire().add( function () {
+ assert.ok( true, 'Invoke handler right away if it was fired before' );
+ } );
+
+ mw.hook( 'test.hook.fireTwiceBefore' ).fire().fire().add( function () {
+ assert.ok( true, 'Invoke handler right away if it was fired before (only last one)' );
+ } );
+
+ chars = [];
+
+ mw.hook( 'test.hook.many' )
+ .add( function ( chr ) {
+ chars.push( chr );
+ } )
+ .fire( 'x' ).fire( 'y' ).fire( 'z' )
+ .add( function ( chr ) {
+ assert.equal( chr, 'z', 'Adding callback later invokes right away with last data' );
+ } );
+
+ assert.deepEqual( chars, ['x', 'y', 'z'], 'Multiple callbacks with multiple fires' );
+
+ chars = [];
+ callback = function ( chr ) {
+ chars.push( chr );
+ };
+
+ mw.hook( 'test.hook.variadic' )
+ .add(
+ callback,
+ callback,
+ function ( chr ) {
+ chars.push( chr );
+ },
+ callback
+ )
+ .fire( 'x' )
+ .remove(
+ function () {
+ 'not-added';
+ },
+ callback
+ )
+ .fire( 'y' )
+ .remove( callback )
+ .fire( 'z' );
+
+ assert.deepEqual(
+ chars,
+ ['x', 'x', 'x', 'x', 'y', 'z'],
+ '"add" and "remove" support variadic arguments. ' +
+ '"add" does not filter unique. ' +
+ '"remove" removes all equal by reference. ' +
+ '"remove" is silent if the function is not found'
+ );
+ } );
+
}( mediaWiki, jQuery ) );