Henning Snater has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/172987

Change subject: Implemented EventSingletonManager
......................................................................

Implemented EventSingletonManager

Implemented $.util.EventSingletonManager and applied it to $.sticknode plugin.
The utility constructor will be used for solving 73300 as well.

Change-Id: I46fed73ae4ab25284d392ce19b912ee8272b5495
---
M lib/resources/Resources.php
M lib/resources/jquery/jquery.sticknode.js
A lib/resources/jquery/jquery.util.EventSingletonManager.js
A lib/tests/qunit/jquery/jquery.util.EventSingletonManager.tests.js
M lib/tests/qunit/resources.php
5 files changed, 394 insertions(+), 58 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Wikibase 
refs/changes/87/172987/1

diff --git a/lib/resources/Resources.php b/lib/resources/Resources.php
index d3fb44f..4b60548 100644
--- a/lib/resources/Resources.php
+++ b/lib/resources/Resources.php
@@ -133,6 +133,16 @@
                        ),
                        'dependencies' => array(
                                'jquery.throttle-debounce',
+                               'jquery.util.EventSingletonManager',
+                       ),
+               ),
+
+               'jquery.util.EventSingletonManager' => $moduleTemplate + array(
+                       'scripts' => array(
+                               'jquery/jquery.util.EventSingletonManager.js',
+                       ),
+                       'dependencies' => array(
+                               'jquery.throttle-debounce',
                        ),
                ),
 
diff --git a/lib/resources/jquery/jquery.sticknode.js 
b/lib/resources/jquery/jquery.sticknode.js
index 92aa88a..4650ac4 100644
--- a/lib/resources/jquery/jquery.sticknode.js
+++ b/lib/resources/jquery/jquery.sticknode.js
@@ -6,7 +6,7 @@
        'use strict';
 
 var $window = $( window ),
-       stickyInstances = [],
+       eventSingleton = new $.util.EventSingletonManager(),
        PLUGIN_NAME = 'sticknode';
 
 /**
@@ -34,65 +34,39 @@
                        return;
                }
 
-               register( new StickyNode( $( this ), options ) );
+               var stickyNode = new StickyNode( $( this ), options );
+
+               eventSingleton.register(
+                       stickyNode,
+                       window,
+                       'scroll.' + PLUGIN_NAME + ' ' + 'touchmove.' + 
PLUGIN_NAME,
+                       function( event, stickyNode ) {
+                               if( stickyNode.update( $window.scrollTop() ) ) {
+                                       stickyNode.$node.triggerHandler( 
PLUGIN_NAME + 'update' );
+                               }
+                       },
+                       {
+                               throttle: 150
+                       }
+               );
+
+               eventSingleton.register(
+                       stickyNode,
+                       window,
+                       'resize.' + PLUGIN_NAME,
+                       function( event, stickyNode ) {
+                               if( stickyNode.update( $window.scrollTop(), 
true ) ) {
+                                       stickyNode.$node.triggerHandler( 
PLUGIN_NAME + 'update' );
+                               }
+                       },
+                       {
+                               throttle: 150
+                       }
+               );
        } );
 
        return this;
 };
-
-/**
- * @param {boolean} [force]
- */
-function update( force ) {
-       force = force === true;
-
-       for( var i = 0; i < stickyInstances.length; i++ ) {
-               if( stickyInstances[i].update( $window.scrollTop(), force ) ) {
-                       stickyInstances[i].$node.triggerHandler( PLUGIN_NAME + 
'update' );
-               }
-       }
-}
-
-function updateAndForceTriggerEvent() {
-       update( true );
-}
-
-/**
- * @param {Function} fn
- * @return {Function}
- */
-function throttle( fn ) {
-       return $.throttle ? $.throttle( 150, fn ) : fn;
-}
-
-/**
- * @param {StickyNode} sticky
- */
-function register( sticky ) {
-       if( !stickyInstances.length ) {
-               $window
-               .on( 'scroll.' + PLUGIN_NAME + ' ' + 'touchmove.' + 
PLUGIN_NAME, ( function() {
-                       return throttle( update );
-               }() ) )
-               .on( 'resize.' + PLUGIN_NAME, ( function() {
-                       return throttle( updateAndForceTriggerEvent );
-               }() ) );
-       }
-       stickyInstances.push( sticky );
-}
-
-/**
- * @param {StickyNode} sticky
- */
-function deregister( sticky ) {
-       var index = $.inArray( sticky );
-       if( index ) {
-               stickyInstances.splice( index, 1 );
-       }
-       if( !stickyInstances.length ) {
-               $window.off( '.' + PLUGIN_NAME );
-       }
-}
 
 /**
  * @constructor
@@ -133,10 +107,14 @@
        _changesDocumentHeight: false,
 
        /**
-        * Destroys and deregisters the plugin.
+        * Destroys and unregisters the plugin.
         */
        destroy: function() {
-               deregister( this );
+               eventSingleton.unregister(
+                       this.$node.data( PLUGIN_NAME ),
+                       window,
+                       '.' + PLUGIN_NAME
+               );
                this.$node.removeData( PLUGIN_NAME );
        },
 
diff --git a/lib/resources/jquery/jquery.util.EventSingletonManager.js 
b/lib/resources/jquery/jquery.util.EventSingletonManager.js
new file mode 100644
index 0000000..33b1f25
--- /dev/null
+++ b/lib/resources/jquery/jquery.util.EventSingletonManager.js
@@ -0,0 +1,201 @@
+/**
+ * @licence GNU GPL v2+
+ * @author H. Snater < mediaw...@snater.com >
+ */
+( function( $ ) {
+       'use strict';
+
+$.util = $.util || {};
+
+/**
+ * Manages attaching an event handler to a target only once for a set of 
source objects.
+ * Since an event is attached only once, the initial event handler (for the 
combination of target/
+ * event name/event namespace) may not be overwritten later on.
+ * @constructor
+ */
+var SELF = $.util.EventSingletonManager = function() {
+       this._registry = [];
+};
+
+$.extend( SELF.prototype, {
+       /**
+        * @type {Object[]}
+        */
+       _registry: [],
+
+       /**
+        * Attaches an event handler to a target element unless it is not 
attached already. The event
+        * binding is registered for a source element.
+        *
+        * @param {*} source
+        * @param {HTMLElement|Object} target
+        * @param {string} event
+        * @param {Function} handler
+        *        Will receive the following arguments when the event is 
triggered:
+        *        - {jQuery.Event} Object representing the actual event 
triggered on the target.
+        *        - {*} source
+        * @param {Object} [options]
+        *        - {number} [throttle] Throttle delay
+        *          Default: undefined (no throttling)
+        *        - {number} [debounce] Debounce delay
+        *          Default: undefined (no debouncing)
+        */
+       register: function( source, target, event, handler, options ) {
+               var namespacedEvents = event.split( ' ' );
+
+               options = options || {};
+
+               for( var i = 0; i < namespacedEvents.length; i++ ) {
+                       var registration = this._getRegistration( target, 
namespacedEvents[i] );
+
+                       if( registration ) {
+                               registration.sources.push( source );
+                       } else {
+                               this._attach( source, target, 
namespacedEvents[i], handler, options );
+                       }
+               }
+       },
+
+       /**
+        * Unregisters one or multiple events attached to a target element and 
registered for a specific
+        * source.
+        *
+        * @param {*} source
+        * @param {HTMLElement|Object} target
+        * @param {string} event
+        *        Instead of a full event name, a namespace may be passed to 
remove all events attached
+        *        to target and registered on source.
+        */
+       unregister: function( source, target, event ) {
+               var registrations = [],
+                       i;
+
+               if( event.indexOf( '.' ) === 0 ) {
+                       registrations = this._getRegistrations( target, 
event.split( '.' )[1] );
+               } else {
+                       var events = event.split( ' ' );
+                       for( i = 0; i < events.length; i++ ) {
+                               var registration = this._getRegistration( 
target, events[i] );
+                               if( registration ) {
+                                       registrations.push( registration );
+                               }
+                       }
+               }
+
+               for( i = 0; i < registrations.length; i++ ) {
+                       var index = $.inArray( source, registrations[i].sources 
);
+                       if( index !== -1 ) {
+                               registrations[i].sources.splice( index, 1 );
+                       }
+                       if( !registrations[i].sources.length ) {
+                               this._detach( registrations[i] );
+                       }
+               }
+       },
+
+       /**
+        * @param {HTMLElement|Object} target
+        * @param {string} event
+        * @return {Object}
+        */
+       _getRegistration: function( target, event ) {
+               var eventSegments = event.split( '.' );
+
+               for( var i = 0; i < this._registry.length; i++ ) {
+                       if(
+                               this._registry[i].target === target
+                               && this._registry[i].event === eventSegments[0]
+                               && this._registry[i].namespace === 
eventSegments[1]
+                       ) {
+                               return this._registry[i];
+                       }
+               }
+       },
+
+       /**
+        * @param {HTMLElement|Object} target
+        * @param {string} namespace
+        * @return {Object[]}
+        */
+       _getRegistrations: function( target, namespace ) {
+               var registered = [];
+
+               for( var i = 0; i < this._registry.length; i++ ) {
+                       if( this._registry[i].target === target && 
this._registry[i].namespace === namespace ) {
+                               registered.push( this._registry[i] );
+                       }
+               }
+
+               return registered;
+       },
+
+       /**
+        * @param {*} source
+        * @param {HTMLElement|Object} target
+        * @param {string} event
+        * @param {Function} handler
+        * @param {Object} options
+        */
+       _attach: function( source, target, event, handler, options ) {
+               var self = this,
+                       eventSegments = event.split( '.' ),
+                       actualHandler = function( actualEvent ) {
+                               self._triggerHandler( target, event, 
actualEvent );
+                       },
+                       alteredHandler;
+
+               if( options.throttle ) {
+                       alteredHandler = $.throttle( options.throttle, 
actualHandler );
+               } else if( options.debounce ) {
+                       alteredHandler = $.debounce( options.debounce, 
actualHandler );
+               }
+
+               $( target ).on( event, alteredHandler || actualHandler );
+
+               this._registry.push( {
+                       sources: [source],
+                       target: target,
+                       event: eventSegments[0],
+                       namespace: eventSegments[1],
+                       handler: handler
+               } );
+       },
+
+       /**
+        * @param {Object} registration
+        */
+       _detach: function( registration ) {
+               var namespaced = registration.event;
+               if( registration.namespace ) {
+                       namespaced += '.' + registration.namespace;
+               }
+               $( registration.target ).off( namespaced );
+
+               for( var i = 0; i < this._registry.length; i++ ) {
+                       if(
+                               this._registry[i].target === registration.target
+                               && this._registry[i].event === 
registration.event
+                               && this._registry[i].namespace === 
registration.namespace
+                       ) {
+                               this._registry.splice( i, 1 );
+                       }
+               }
+       },
+
+       /**
+        * @param {HTMLElement|Object} target
+        * @param {string} event
+        * @param {jQuery.Event} actualEvent
+        */
+       _triggerHandler: function( target, event, actualEvent ) {
+               var registration = this._getRegistration( target, event );
+
+               if( registration ) {
+                       for( var i = 0; i < registration.sources.length; i++ ) {
+                               registration.handler( actualEvent, 
registration.sources[i] );
+                       }
+               }
+       }
+} );
+
+} )( jQuery );
diff --git a/lib/tests/qunit/jquery/jquery.util.EventSingletonManager.tests.js 
b/lib/tests/qunit/jquery/jquery.util.EventSingletonManager.tests.js
new file mode 100644
index 0000000..1c23ec9
--- /dev/null
+++ b/lib/tests/qunit/jquery/jquery.util.EventSingletonManager.tests.js
@@ -0,0 +1,138 @@
+/**
+ * @licence GNU GPL v2+
+ * @author H. Snater < mediaw...@snater.com >
+ */
+( function( $, QUnit ) {
+       'use strict';
+
+QUnit.module( 'jquery.util.EventSingletonManager' );
+
+QUnit.test( 'register() & unregister() (single source)', 2, function( assert ) 
{
+       var manager = new $.util.EventSingletonManager(),
+               $source = $( '<div/>' ),
+               $target = $( '<div/>' ),
+               event = $.Event( 'custom' );
+
+       manager.register(
+               $source.get( 0 ),
+               $target.get( 0 ),
+               'custom.namespace',
+               function( event, source ) {
+                       assert.ok(
+                               true,
+                               'Triggered event "' + event.type + '.'
+                       );
+
+                       assert.equal(
+                               $source.get( 0 ),
+                               source,
+                               'Verified source element being passed to the 
handler.'
+                       );
+               }
+       );
+
+       $target.trigger( event );
+
+       manager.unregister( $source.get( 0 ), $target.get( 0 ), 
'custom.namespace' );
+
+       $target.trigger( event );
+} );
+
+QUnit.test( 'register() & unregister() (multiple sources)', 4, function( 
assert ) {
+       var manager = new $.util.EventSingletonManager(),
+               $sources = $( '<div/><div/>' ),
+               sources = $sources.map( function() { return this; } ),
+               $target = $( '<div/>' ),
+               triggeredForSources = [],
+               event = $.Event( 'custom' ),
+               handler = function( event, source ) {
+                       triggeredForSources.push( source );
+               };
+
+       manager.register( $sources.get( 0 ), $target.get( 0 ), 
'custom.namespace', handler );
+       manager.register( $sources.get( 1 ), $target.get( 0 ), 
'custom.namespace', handler );
+
+       $target.trigger( event );
+
+       assert.equal(
+               triggeredForSources.length,
+               $sources.length,
+               'Handler has been called for every source.'
+       );
+
+       assert.ok(
+               $.inArray( sources[0], triggeredForSources ) !== -1,
+               'Handler was called for first source.'
+       );
+
+       assert.ok(
+               $.inArray( sources[1], triggeredForSources ) !== -1,
+               'Handler was called for second source.'
+       );
+
+       manager.unregister( $sources.get( 1 ), $target.get( 0 ), 
'custom.namespace' );
+
+       $target.trigger( event );
+
+       assert.equal(
+               triggeredForSources[2],
+               $sources.get( 0 ),
+               'Handler was called once again after unregistering a source.'
+       );
+
+       manager.unregister( $sources.get( 0 ), $target.get( 0 ), '.namespace' );
+} );
+
+QUnit.test( 'unregister() & unregister() (multiple events)', 8, function( 
assert ) {
+       var manager = new $.util.EventSingletonManager(),
+               $source = $( '<div/>' ),
+               $target = $( '<div/>' ),
+               event1 = $.Event( 'custom1' ),
+               event2 = $.Event( 'custom2' ),
+               event3 = $.Event( 'custom3' ),
+               event4 = $.Event( 'custom4' );
+
+       manager.register(
+               $source.get( 0 ),
+               $target.get( 0 ),
+               'custom1.namespace custom2.namespace custom3.namespace 
custom4.othernamespace',
+               function( event, source ) {
+                       assert.ok(
+                               true,
+                               'Triggered event "' + event.type + '".'
+                       );
+               }
+       );
+
+       $target.trigger( event1 );
+       $target.trigger( event2 );
+       $target.trigger( event3 );
+       $target.trigger( event4 );
+
+       manager.unregister(
+               $source.get( 0 ),
+               $target.get( 0 ),
+               'custom1.namespace'
+       );
+
+       $target.trigger( event1 ); // no action
+       $target.trigger( event2 );
+       $target.trigger( event3 );
+       $target.trigger( event4 );
+
+       manager.unregister( $source.get( 0 ), $target.get( 0 ), '.namespace' );
+
+       $target.trigger( event1 ); // no action
+       $target.trigger( event2 ); // no action
+       $target.trigger( event3 ); // no action
+       $target.trigger( event4 );
+
+       manager.unregister( $source.get( 0 ), $target.get( 0 ), 
'.othernamespace' );
+
+       $target.trigger( event1 ); // no action
+       $target.trigger( event2 ); // no action
+       $target.trigger( event3 ); // no action
+       $target.trigger( event4 ); // no action
+} );
+
+}( jQuery, QUnit ) );
diff --git a/lib/tests/qunit/resources.php b/lib/tests/qunit/resources.php
index 1fda429..b6e0c15 100644
--- a/lib/tests/qunit/resources.php
+++ b/lib/tests/qunit/resources.php
@@ -53,6 +53,15 @@
                        ),
                ),
 
+               'jquery.util.EventSingletonManager.tests' => $moduleBase + 
array(
+                       'scripts' => array(
+                               
'jquery/jquery.util.EventSingletonManager.tests.js',
+                       ),
+                       'dependencies' => array(
+                               'jquery.util.EventSingletonManager',
+                       ),
+               ),
+
                'jquery.ui.tagadata.tests' => $moduleBase + array(
                        'scripts' => array(
                                'jquery.ui/jquery.ui.tagadata.tests.js',

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I46fed73ae4ab25284d392ce19b912ee8272b5495
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Henning Snater <henning.sna...@wikimedia.de>

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

Reply via email to