Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

dom-delegate

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dom-delegate - npm Package Compare versions

Comparing version 0.1.3 to 0.1.5

.npmignore

41

_tests/lib/delegateTest.js

@@ -422,2 +422,43 @@ /*global buster, assert, refute, Delegate*/

},
// Regression test for - https://github.com/ftlabs/dom-delegate/pull/10
'Regression test: event listener should be rebound after last event is removed and new events are added.' : function() {
var delegate, spy, element, textNode;
spy = this.spy();
delegate = new Delegate(document);
delegate.on('click', '#delegate-test-clickable', spy);
// Unbind event listeners
delegate.off();
delegate.on('click', '#delegate-test-clickable', spy);
element = document.getElementById('delegate-test-clickable');
textNode = document.createTextNode('Test text');
element.appendChild(textNode);
textNode.dispatchEvent(setupHelper.getMouseEvent('click'));
assert.called(spy);
delegate.off();
},
// Test for issue #5
'The root element, via a null selector, is supported': function() {
var delegate, spy, element;
delegate = new Delegate(document.body);
spy = this.spy();
delegate.on('click', null, spy);
element = document.body;
element.dispatchEvent(setupHelper.getMouseEvent('click'));
assert.calledOnce(spy);
delegate.off();
},
'tearDown': function() {

@@ -424,0 +465,0 @@ setupHelper.tearDown();

2

component.json
{
"name": "dom-delegate",
"description": "Create and manage a DOM event delegator.",
"version": "0.1.3",
"version": "0.1.5",
"main": "lib/delegate.js",

@@ -6,0 +6,0 @@ "scripts": [

@@ -0,5 +1,8 @@

/*jslint browser:true, node:true*/
/*global define, Node*/
/**
* @preserve Create and manage a DOM event delegator.
*
* @version 0.1.3
* @version 0.1.5
* @codingstandard ftlabs-jsv2

@@ -10,400 +13,437 @@ * @copyright The Financial Times Limited [All Rights Reserved]

/*jslint browser:true, node:true*/
/*global define, Node*/
;(function(){
/**
* DOM event delegator
*
* The delegator will listen for events that bubble up to the root node.
*
* @constructor
* @param {Node|string} root The root node or a selector string matching the root node
*/
function Delegate(root) {
'use strict';
var self = this;
if (root) {
this.root(root);
/**
* DOM event delegator
*
* The delegator will listen
* for events that bubble up
* to the root node.
*
* @constructor
* @param {Node|string} root The root node or a selector string matching the root node
*/
function Delegate(root) {
var self = this;
if (root) {
this.root(root);
}
/**
* Maintain a map of listener
* lists, keyed by event name.
*
* @type Object
*/
this.listenerMap = {};
/** @type function() */
this.handle = function(event) { Delegate.prototype.handle.call(self, event); };
}
/**
* @protected
* @type ?boolean
*/
Delegate.tagsCaseSensitive = null;
/**
* Maintain a map of listener lists, keyed by event name.
* Start listening for events
* on the provided DOM element
*
* @type Object
* @param {Node} root The root node or a selector string matching the root node
*/
this.listenerMap = {};
Delegate.prototype.root = function(root) {
var listenerMap = this.listenerMap;
var eventType;
if (typeof root === 'string') {
root = document.querySelector(root);
}
/** @type function() */
this.handle = function(event) { Delegate.prototype.handle.call(self, event); };
}
// Remove master event listeners
if (this.rootElement) {
for (eventType in listenerMap) {
if (listenerMap.hasOwnProperty(eventType)) {
this.rootElement.removeEventListener(eventType, this.handle, this.captureForType(eventType));
}
}
}
// If no root or root is not
// a dom node, then remove internal
// root reference and exit here
if (!root || !root.addEventListener) {
if (this.rootElement) {
delete this.rootElement;
}
return;
}
/**
* @protected
* @type ?boolean
*/
Delegate.tagsCaseSensitive = null;
/**
* The root node at which
* listeners are attached.
*
* @type Node
*/
this.rootElement = root;
/**
* Start listening for events on the provided DOM element
* @param {Node} root The root node or a selector string matching the root node
*/
Delegate.prototype.root = function(root) {
'use strict';
var listenerMap = this.listenerMap;
var eventType;
if (typeof root === 'string') {
root = document.querySelector(root);
}
// Remove master event listeners
if (this.rootElement) {
// Set up master event listeners
for (eventType in listenerMap) {
if (listenerMap.hasOwnProperty(eventType)) {
this.rootElement.removeEventListener(eventType, this.handle, this.captureForType(eventType));
this.rootElement.addEventListener(eventType, this.handle, this.captureForType(eventType));
}
}
}
};
// If no root or root is not a dom node, then
// remove internal root reference and exit here
if (!root || !root.addEventListener) {
if (this.rootElement) {
delete this.rootElement;
}
return;
}
/**
* @param {string} eventType
* @returns boolean
*/
Delegate.prototype.captureForType = function(eventType) {
return eventType === 'error';
};
/**
* The root node at which listeners are attached.
* Attach a handler to one
* event for all elements
* that match the selector,
* now or in the future
*
* @type Node
* The handler function receives
* three arguments: the DOM event
* object, the node that matched
* the selector while the event
* was bubbling and a reference
* to itself. Within the handler,
* 'this' is equal to the second
* argument.
*
* The node that actually received
* the event can be accessed via
* 'event.target'.
*
* @param {string} eventType Listen for these events (in a space-separated list)
* @param {string} selector Only handle events on elements matching this selector
* @param {function()} handler Handler function - event data passed here will be in event.data
* @param {Object} [eventData] Data to pass in event.data
* @returns {Delegate} This method is chainable
*/
this.rootElement = root;
Delegate.prototype.on = function(eventType, selector, handler, eventData) {
var root, listenerMap, matcher, matcherParam, self = this, /** @const */ SEPARATOR = ' ';
// Set up master event listeners
for (eventType in listenerMap) {
if (listenerMap.hasOwnProperty(eventType)) {
this.rootElement.addEventListener(eventType, this.handle, this.captureForType(eventType));
if (!eventType) {
throw new TypeError('Invalid event type: ' + eventType);
}
}
};
// Support a separated list of event types
if (eventType.indexOf(SEPARATOR) !== -1) {
eventType.split(SEPARATOR).forEach(function(singleEventType) {
self.on(singleEventType, selector, handler, eventData);
});
/**
* @param {string} eventType
* @returns boolean
*/
Delegate.prototype.captureForType = function(eventType) {
'use strict';
return eventType === 'error';
};
/**
* Attach a handler to one event for all elements that match the selector, now or in the future
*
* The handler function receives three arguments: the DOM event object, the node that matched the selector while the event was bubbling
* and a reference to itself. Within the handler, 'this' is equal to the second argument.
* The node that actually received the event can be accessed via 'event.target'.
*
* @param {string} eventType Listen for these events (in a space-separated list)
* @param {string} selector Only handle events on elements matching this selector
* @param {function()} handler Handler function - event data passed here will be in event.data
* @param {Object} [eventData] Data to pass in event.data
* @returns {Delegate} This method is chainable
*/
Delegate.prototype.on = function(eventType, selector, handler, eventData) {
'use strict';
var root, listenerMap, matcher, matcherParam, self = this, /** @const */ SEPARATOR = ' ';
if (!eventType) {
throw new TypeError('Invalid event type: ' + eventType);
}
if (!selector) {
throw new TypeError('Invalid selector: ' + selector);
}
// Support a separated list of event types
if (eventType.indexOf(SEPARATOR) !== -1) {
eventType.split(SEPARATOR).forEach(function(singleEventType) {
self.on(singleEventType, selector, handler, eventData);
});
return this;
}
// Normalise undefined eventData to null
if (eventData === undefined) {
eventData = null;
}
if (typeof handler !== 'function') {
throw new TypeError('Handler must be a type of Function');
}
root = this.rootElement;
listenerMap = this.listenerMap;
// Add master handler for type if not created yet
if (!listenerMap[eventType]) {
if (root) {
root.addEventListener(eventType, this.handle, this.captureForType(eventType));
return this;
}
listenerMap[eventType] = [];
}
// Compile a matcher for the given selector
if (/^[a-z]+$/i.test(selector)) {
// Lazily check whether tag names are case sensitive (as in XML or XHTML documents).
if (Delegate.tagsCaseSensitive === null) {
Delegate.tagsCaseSensitive = document.createElement('i').tagName === 'i';
// Normalise undefined eventData to null
if (eventData === undefined) {
eventData = null;
}
if (!Delegate.tagsCaseSensitive) {
matcherParam = selector.toUpperCase();
} else {
matcherParam = selector;
if (typeof handler !== 'function') {
throw new TypeError('Handler must be a type of Function');
}
matcher = this.matchesTag;
} else if (/^#[a-z0-9\-_]+$/i.test(selector)) {
matcherParam = selector.slice(1);
matcher = this.matchesId;
} else {
matcherParam = selector;
matcher = this.matches;
}
root = this.rootElement;
listenerMap = this.listenerMap;
// Add to the list of listeners
listenerMap[eventType].push({
selector: selector,
eventData: eventData,
handler: handler,
matcher: matcher,
matcherParam: matcherParam
});
// Add master handler for type if not created yet
if (!listenerMap[eventType]) {
if (root) {
root.addEventListener(eventType, this.handle, this.captureForType(eventType));
}
listenerMap[eventType] = [];
}
return this;
};
if (!selector) {
matcherParam = null;
// COMPLEX - matchesRoot needs to have access to
// this.rootElement, so bind the function to this.
matcher = this.matchesRoot.bind(this);
/**
* Remove an event handler for elements that match the selector, forever
*
* @param {string} eventType Remove handlers for events matching this type, considering the other parameters
* @param {string} [selector] If this parameter is omitted, only handlers which match the other two will be removed
* @param {function()} [handler] If this parameter is omitted, only handlers which match the previous two will be removed
* @returns {Delegate} This method is chainable
*/
Delegate.prototype.off = function(eventType, selector, handler) {
'use strict';
var i, listener, listenerMap, listenerList, singleEventType, self = this, /** @const */ SEPARATOR = ' ';
// Compile a matcher for the given selector
} else if (/^[a-z]+$/i.test(selector)) {
listenerMap = this.listenerMap;
if (!eventType) {
for (singleEventType in listenerMap) {
if (listenerMap.hasOwnProperty(singleEventType)) {
this.off(singleEventType, selector, handler);
// Lazily check whether tag names are case sensitive (as in XML or XHTML documents).
if (Delegate.tagsCaseSensitive === null) {
Delegate.tagsCaseSensitive = document.createElement('i').tagName === 'i';
}
}
return this;
}
if (!Delegate.tagsCaseSensitive) {
matcherParam = selector.toUpperCase();
} else {
matcherParam = selector;
}
listenerList = listenerMap[eventType];
if (!listenerList || !listenerList.length) {
return this;
}
matcher = this.matchesTag;
} else if (/^#[a-z0-9\-_]+$/i.test(selector)) {
matcherParam = selector.slice(1);
matcher = this.matchesId;
} else {
matcherParam = selector;
matcher = this.matches;
}
// Support a separated list of event types
if (eventType.indexOf(SEPARATOR) !== -1) {
eventType.split(SEPARATOR).forEach(function(singleEventType) {
self.off(singleEventType, selector, handler);
// Add to the list of listeners
listenerMap[eventType].push({
selector: selector,
eventData: eventData,
handler: handler,
matcher: matcher,
matcherParam: matcherParam
});
return this;
}
};
// Remove only parameter matches if specified
for (i = listenerList.length - 1; i >= 0; i--) {
listener = listenerList[i];
/**
* Remove an event handler
* for elements that match
* the selector, forever
*
* @param {string} eventType Remove handlers for events matching this type, considering the other parameters
* @param {string} [selector] If this parameter is omitted, only handlers which match the other two will be removed
* @param {function()} [handler] If this parameter is omitted, only handlers which match the previous two will be removed
* @returns {Delegate} This method is chainable
*/
Delegate.prototype.off = function(eventType, selector, handler) {
var i, listener, listenerMap, listenerList, singleEventType, self = this, /** @const */ SEPARATOR = ' ';
if ((!selector || selector === listener.selector) && (!handler || handler === listener.handler)) {
listenerList.splice(i, 1);
listenerMap = this.listenerMap;
if (!eventType) {
for (singleEventType in listenerMap) {
if (listenerMap.hasOwnProperty(singleEventType)) {
this.off(singleEventType, selector, handler);
}
}
return this;
}
}
// All listeners removed
if (!listenerList.length) {
delete listenerList[eventType];
// Remove the main handler
if (this.rootElement) {
this.rootElement.removeEventListener(eventType, this.handle, this.captureForType(eventType));
listenerList = listenerMap[eventType];
if (!listenerList || !listenerList.length) {
return this;
}
}
return this;
};
// Support a separated list of event types
if (eventType.indexOf(SEPARATOR) !== -1) {
eventType.split(SEPARATOR).forEach(function(singleEventType) {
self.off(singleEventType, selector, handler);
});
return this;
}
/**
* Handle an arbitrary event.
*
* @param {Event} event
*/
Delegate.prototype.handle = function(event) {
'use strict';
var i, l, root, listener, returned, listenerList, target, /** @const */ EVENTIGNORE = 'ftLabsDelegateIgnore';
// Remove only parameter matches if specified
for (i = listenerList.length - 1; i >= 0; i--) {
listener = listenerList[i];
if (event[EVENTIGNORE] === true) {
return;
}
if ((!selector || selector === listener.selector) && (!handler || handler === listener.handler)) {
listenerList.splice(i, 1);
}
}
target = event.target;
if (target.nodeType === Node.TEXT_NODE) {
target = target.parentNode;
}
// All listeners removed
if (!listenerList.length) {
delete listenerMap[eventType];
root = this.rootElement;
listenerList = this.listenerMap[event.type];
// Remove the main handler
if (this.rootElement) {
this.rootElement.removeEventListener(eventType, this.handle, this.captureForType(eventType));
}
}
// Need to continuously check that the specific list is still populated in case one of the callbacks actually causes the list to be destroyed.
l = listenerList.length;
while (target && l) {
for (i = 0; i < l; i++) {
listener = listenerList[i];
return this;
};
// Bail from this loop if the length changed and no more listeners are defined between i and l.
if (!listener) {
break;
}
// Check for match and fire the event if there's one
// TODO:MCG:20120117: Need a way to check if event#stopImmediateProgagation was called. If so, break both loops.
if (listener.matcher.call(target, listener.matcherParam, target)) {
returned = this.fire(event, target, listener);
}
/**
* Handle an arbitrary event.
*
* @param {Event} event
*/
Delegate.prototype.handle = function(event) {
var i, l, root, listener, returned, listenerList, target, /** @const */ EVENTIGNORE = 'ftLabsDelegateIgnore';
// Stop propagation to subsequent callbacks if the callback returned false
if (returned === false) {
event[EVENTIGNORE] = true;
return;
}
if (event[EVENTIGNORE] === true) {
return;
}
// TODO:MCG:20120117: Need a way to check if event#stopProgagation was called. If so, break looping through the DOM.
// Stop if the delegation root has been reached
if (target === root) {
break;
target = event.target;
if (target.nodeType === Node.TEXT_NODE) {
target = target.parentNode;
}
root = this.rootElement;
listenerList = this.listenerMap[event.type];
// Need to continuously check
// that the specific list is
// still populated in case one
// of the callbacks actually
// causes the list to be destroyed.
l = listenerList.length;
target = target.parentElement;
}
};
while (target && l) {
for (i = 0; i < l; i++) {
listener = listenerList[i];
// Bail from this loop if
// the length changed and
// no more listeners are
// defined between i and l.
if (!listener) {
break;
}
/**
* Fire a listener on a target.
*
* @param {Event} event
* @param {Node} target
* @param {Object} listener
* @returns {boolean}
*/
Delegate.prototype.fire = function(event, target, listener) {
'use strict';
var returned, oldData;
// Check for match and fire
// the event if there's one
//
// TODO:MCG:20120117: Need a way
// to check if event#stopImmediateProgagation
// was called. If so, break both loops.
if (listener.matcher.call(target, listener.matcherParam, target)) {
returned = this.fire(event, target, listener);
}
if (listener.eventData !== null) {
oldData = event.data;
event.data = listener.eventData;
returned = listener.handler.call(target, event, target);
event.data = oldData;
} else {
returned = listener.handler.call(target, event, target);
}
// Stop propagation to subsequent
// callbacks if the callback returned
// false
if (returned === false) {
event[EVENTIGNORE] = true;
return;
}
}
return returned;
};
// TODO:MCG:20120117: Need a way to
// check if event#stopProgagation
// was called. If so, break looping
// through the DOM. Stop if the
// delegation root has been reached
if (target === root) {
break;
}
l = listenerList.length;
target = target.parentElement;
}
};
/**
* Check whether an element matches a generic selector.
*
* @type function()
* @param {string} selector A CSS selector
*/
Delegate.prototype.matches = (function(p) {
'use strict';
return (p.matchesSelector || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || p.oMatchesSelector);
}(HTMLElement.prototype));
/**
* Fire a listener on a target.
*
* @param {Event} event
* @param {Node} target
* @param {Object} listener
* @returns {boolean}
*/
Delegate.prototype.fire = function(event, target, listener) {
var returned, oldData;
if (listener.eventData !== null) {
oldData = event.data;
event.data = listener.eventData;
returned = listener.handler.call(target, event, target);
event.data = oldData;
} else {
returned = listener.handler.call(target, event, target);
}
/**
* Check whether an element matches a tag selector.
*
* Tags are NOT case-sensitive, except in XML (and XML-based languages such as XHTML).
*
* @param {string} tagName The tag name to test against
* @param {Element} element The element to test with
* @returns boolean
*/
Delegate.prototype.matchesTag = function(tagName, element) {
'use strict';
return tagName === element.tagName;
};
return returned;
};
/**
* Check whether an element matches a generic selector.
*
* @type function()
* @param {string} selector A CSS selector
*/
Delegate.prototype.matches = (function(p) {
return (p.matchesSelector || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || p.oMatchesSelector);
}(HTMLElement.prototype));
/**
* Check whether the ID of the element in 'this' matches the given ID.
*
* IDs are case-sensitive.
*
* @param {string} id The ID to test against
* @param {Element} element The element to test with
* @returns boolean
*/
Delegate.prototype.matchesId = function(id, element) {
'use strict';
return id === element.id;
};
/**
* Check whether an element
* matches a tag selector.
*
* Tags are NOT case-sensitive,
* except in XML (and XML-based
* languages such as XHTML).
*
* @param {string} tagName The tag name to test against
* @param {Element} element The element to test with
* @returns boolean
*/
Delegate.prototype.matchesTag = function(tagName, element) {
return tagName === element.tagName;
};
if (typeof define !== 'undefined' && define.amd) {
/**
* Check whether an element matches the root.
*
* @param {?String} selector In this case this is always passed through as null and not used
* @param {Element} element The element to test with
* @returns boolean
*/
Delegate.prototype.matchesRoot = function(selector, element) {
return this.rootElement === element;
};
// AMD. Register as an anonymous module.
define(function() {
'use strict';
return Delegate;
});
}
/**
* Check whether the ID of
* the element in 'this'
* matches the given ID.
*
* IDs are case-sensitive.
*
* @param {string} id The ID to test against
* @param {Element} element The element to test with
* @returns boolean
*/
Delegate.prototype.matchesId = function(id, element) {
return id === element.id;
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = function(root) {
'use strict';
return new Delegate(root);
/**
* Short hand for off()
* and root(), ie both
* with no parameters
*
* @return void
*/
Delegate.prototype.destroy = function() {
this.off();
this.root();
};
module.exports.Delegate = Delegate;
}
/**
* Expose `Delegate`
*/
if (typeof module === "object") {
module.exports = function(root) {
return new Delegate(root);
};
module.exports.Delegate = Delegate;
} else if (typeof define === "function" && define.amd) {
define(function() {
return Delegate;
});
} else {
window.Delegate = Delegate;
}
/**
* Short hand for off() and root(), ie both with no parameters
*
* @return void
*/
Delegate.prototype.destroy = function() {
this.off();
this.root();
};
})();
{
"name": "dom-delegate",
"version": "0.1.3",
"version": "0.1.5",
"author": "FT Labs <enquiries@labs.ft.com> (http://labs.ft.com/)",

@@ -15,4 +15,4 @@ "description": "Create and manage a DOM event delegator.",

},
"scripts" : {
"test" : "node_modules/.bin/buster-test -c _tests/buster.js"
"scripts": {
"test": "node_modules/.bin/buster-test -c _tests/buster.js"
},

@@ -19,0 +19,0 @@ "keywords": [

@@ -98,2 +98,4 @@ # Delegate #

Null is also accepted and will match the root element set by `root()`.
#### `handler (function)` ####

@@ -100,0 +102,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc