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