Skip to content
Browse files

Start using grunt; try to get travis to build.

  • Loading branch information...
1 parent 198752d commit 90b59d2648af8f98df2efb98485b7f25905a95d4 @matthew-andrews matthew-andrews committed May 24, 2013
Showing with 938 additions and 452 deletions.
  1. +34 −0 GruntFile.js
  2. +38 −11 README.md
  3. +437 −0 build/dom-delegate.js
  4. +1 −0 build/dom-delegate.min.js
  5. +0 −1 build/logs/.gitignore
  6. +421 −438 lib/delegate.js
  7. +7 −2 package.json
View
34 GruntFile.js
@@ -0,0 +1,34 @@
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+
+ buster: {},
+
+ browserify: {
+ build: {
+ src: 'lib/delegate.js',
+ dest: 'build/<%= pkg.name %>.js'
+ },
+ options: {
+ standalone: 'Delegate'
+ }
+ },
+
+ uglify: {
+ build: {
+ src: 'build/<%= pkg.name %>.js',
+ dest: 'build/<%= pkg.name %>.min.js'
+ }
+ }
+
+ });
+
+ grunt.loadNpmTasks('grunt-buster');
+ grunt.loadNpmTasks('grunt-browserify');
+ grunt.loadNpmTasks('grunt-contrib-uglify');
+
+ // Default task.
+ grunt.registerTask('default', ['browserify', 'uglify']);
+};
View
49 README.md
@@ -1,8 +1,6 @@
-# Delegate #
+# dom-delegate [![Build Status](https://travis-ci.org/ftlabs/dom-delegate.png?branch=master)](https://travis-ci.org/ftlabs/dom-delegate)
-[![Build Status](https://travis-ci.org/ftlabs/dom-delegate.png?branch=master)](https://travis-ci.org/ftlabs/dom-delegate)
-
-Delegate is a simple, easy-to-use component for binding to events on all target elements matching the given selector, irrespective of whether they exist at registration time or not. This allows developers to implement the [event delegation pattern](http://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/).
+FT's dom-delegate is a simple, easy-to-use component for binding to events on all target elements matching the given selector, irrespective of whether they exist at registration time or not. This allows developers to implement the [event delegation pattern](http://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/).
Delegate is developed by [FT Labs](http://labs.ft.com/), part of the Financial Times.
@@ -32,11 +30,38 @@ Include delegate.js in your JavaScript bundle or add it to your HTML page like t
The script must be loaded prior to instantiating a Delegate object.
-To instantiate Delegate on the `body`:
+To instantiate Delegate on the `body` and listen to some events:
+
+```js
+function handleButtonClicks(event) {
+ // do some things
+}
+
+function handleTouchMove(event) {
+ // do some other things
+}
+
+window.addEventListener('load', function() {
+ var delegate = new Delegate(document.body);
+ delegate.on('click', 'button', handleButtonClicks);
+
+ // Listen to all touch move
+ // events that reach the body
+ delegate.on('touchmove', handleTouchMove);
+
+}, false);
+```
+
+A cool trick to handle images that fail to load:
```js
+function handleImageFail() {
+ this.style.display = 'none';
+}
+
window.addEventListener('load', function() {
- new Delegate(document.body);
+ var delegate = new Delegate(document.body);
+ delegate.on('error', 'img', handleImageFail);
}, false);
```
@@ -93,13 +118,13 @@ The report in `build/logs/jscoverage/` can be processed using `genhtml`, which i
The event to listen for e.g. `mousedown`, `mouseup`, `mouseout`, `error` or `click`.
-#### `selector (string)` ####
+#### `selector (string|function)` ####
Any kind of valid CSS selector supported by [`matchesSelector`](http://caniuse.com/matchesselector). Some selectors, like `#id` or `tag` will use optimized functions internally that check for straight matches between the ID or tag name of elements.
-Null is also accepted and will match the root element set by `root()`.
+`null` is also accepted and will match the root element set by `root()`. Passing a handler function into `.on`'s second argument (with `eventData` as an optional third parameter) is equivalent to `.on(eventType, null, handler[, eventData])`.
-#### `handler (function)` ####
+#### `handler (function|*)` ####
Function that will handle the specified event on elements matching the given selector. The function will receive two arguments: the native event object and the target element, in that order.
@@ -115,9 +140,11 @@ Calling `off` with no arguments will remove all registered listeners, effectivel
Remove handlers for events matching this type considering the other parameters.
-#### `selector (string)` ####
+#### `selector (string|function)` ####
+
+Only remove listeners registered with the given selector, among the other arguments. If null passed listeners registered to the root element will be removed. Passing in a function into `off`'s second parameter is equivalent to `.off(eventType, null, handler)` (the third parameter will be ignored).
-Only remove listeners registered with the given selector, among the other arguments.
+If `selector` is a function
#### `handler (function)` ####
View
437 build/dom-delegate.js
@@ -0,0 +1,437 @@
+(function(e){if("function"==typeof bootstrap)bootstrap("delegate",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeDelegate=e}else"undefined"!=typeof window?window.Delegate=e():global.Delegate=e()})(function(){var define,ses,bootstrap,module,exports;
+return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
+/*jshint browser:true, node:true*/
+
+'use strict';
+
+/**
+ * @preserve Create and manage a DOM event delegator.
+ *
+ * @version 0.2.1
+ * @codingstandard ftlabs-jsv2
+ * @copyright The Financial Times Limited [All Rights Reserved]
+ * @license MIT License (see LICENSE.txt)
+ */
+
+module.exports = Delegate;
+
+/**
+ * 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;
+
+/**
+ * Start listening for events
+ * on the provided DOM element
+ *
+ * @param {Node|string} [root] The root node or a selector string matching the root node
+ * @returns {Delegate} This method is chainable
+ */
+Delegate.prototype.root = function(root) {
+ var listenerMap = this.listenerMap;
+ var eventType;
+
+ if (typeof root === 'string') {
+ root = document.querySelector(root);
+ }
+
+ // 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 this;
+ }
+
+ /**
+ * The root node at which
+ * listeners are attached.
+ *
+ * @type Node
+ */
+ this.rootElement = root;
+
+ // Set up master event listeners
+ for (eventType in listenerMap) {
+ if (listenerMap.hasOwnProperty(eventType)) {
+ this.rootElement.addEventListener(eventType, this.handle, this.captureForType(eventType));
+ }
+ }
+
+ return this;
+};
+
+/**
+ * @param {string} eventType
+ * @returns boolean
+ */
+Delegate.prototype.captureForType = function(eventType) {
+ 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|undefined} selector Only handle events on elements matching this selector, if undefined match root element
+ * @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) {
+ var root, listenerMap, matcher, matcherParam, self = this;
+
+ if (!eventType) {
+ throw new TypeError('Invalid event type: ' + eventType);
+ }
+
+ // handler can be passed as
+ // the second or third argument
+ if (typeof selector === 'function') {
+ handler = selector;
+ selector = null;
+ eventData = handler;
+ }
+
+ // 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));
+ }
+ listenerMap[eventType] = [];
+ }
+
+ if (!selector) {
+ matcherParam = null;
+
+ // COMPLEX - matchesRoot needs to have access to
+ // this.rootElement, so bind the function to this.
+ matcher = this.matchesRoot.bind(this);
+
+ // Compile a matcher for the given selector
+ } else 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';
+ }
+
+ if (!Delegate.tagsCaseSensitive) {
+ matcherParam = selector.toUpperCase();
+ } else {
+ matcherParam = selector;
+ }
+
+ matcher = this.matchesTag;
+ } else if (/^#[a-z0-9\-_]+$/i.test(selector)) {
+ matcherParam = selector.slice(1);
+ matcher = this.matchesId;
+ } else {
+ matcherParam = selector;
+ matcher = this.matches;
+ }
+
+ // Add to the list of listeners
+ listenerMap[eventType].push({
+ selector: selector,
+ eventData: eventData,
+ handler: handler,
+ matcher: matcher,
+ matcherParam: matcherParam
+ });
+
+ return 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) {
+ var i, listener, listenerMap, listenerList, singleEventType, self = this;
+
+ // Handler can be passed as
+ // the second or third argument
+ if (typeof selector === 'function') {
+ handler = selector;
+ selector = null;
+ }
+
+ listenerMap = this.listenerMap;
+ if (!eventType) {
+ for (singleEventType in listenerMap) {
+ if (listenerMap.hasOwnProperty(singleEventType)) {
+ this.off(singleEventType, selector, handler);
+ }
+ }
+
+ return this;
+ }
+
+ listenerList = listenerMap[eventType];
+ if (!listenerList || !listenerList.length) {
+ return this;
+ }
+
+ // Remove only parameter matches
+ // if specified
+ for (i = listenerList.length - 1; i >= 0; i--) {
+ listener = listenerList[i];
+
+ if ((!selector || selector === listener.selector) && (!handler || handler === listener.handler)) {
+ listenerList.splice(i, 1);
+ }
+ }
+
+ // All listeners removed
+ if (!listenerList.length) {
+ delete listenerMap[eventType];
+
+ // Remove the main handler
+ if (this.rootElement) {
+ this.rootElement.removeEventListener(eventType, this.handle, this.captureForType(eventType));
+ }
+ }
+
+ return this;
+};
+
+
+/**
+ * Handle an arbitrary event.
+ *
+ * @param {Event} event
+ */
+Delegate.prototype.handle = function(event) {
+ var i, l, root, listener, returned, listenerList, target, /** @const */ EVENTIGNORE = 'ftLabsDelegateIgnore';
+
+ if (event[EVENTIGNORE] === true) {
+ return;
+ }
+
+ 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;
+ 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;
+ }
+
+ // 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);
+ }
+
+ // Stop propagation to subsequent
+ // callbacks if the callback returned
+ // false
+ if (returned === false) {
+ 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;
+ }
+
+ l = listenerList.length;
+ target = target.parentElement;
+ }
+};
+
+/**
+ * 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);
+ }
+
+ 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 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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * Short hand for off()
+ * and root(), ie both
+ * with no parameters
+ *
+ * @return void
+ */
+Delegate.prototype.destroy = function() {
+ this.off();
+ this.root();
+};
+
+},{}]},{},[1])(1)
+});
+;
View
1 build/dom-delegate.min.js
@@ -0,0 +1 @@
+(function(e){if("function"==typeof bootstrap)bootstrap("delegate",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeDelegate=e}else"undefined"!=typeof window?window.Delegate=e():global.Delegate=e()})(function(){return function(e,t,r){function n(r,i){if(!t[r]){if(!e[r]){var s="function"==typeof require&&require;if(!i&&s)return s(r,!0);if(o)return o(r,!0);throw Error("Cannot find module '"+r+"'")}var a=t[r]={exports:{}};e[r][0].call(a.exports,function(t){var o=e[r][1][t];return n(o?o:t)},a,a.exports)}return t[r].exports}for(var o="function"==typeof require&&require,i=0;r.length>i;i++)n(r[i]);return n}({1:[function(e,t){"use strict";function r(e){var t=this;e&&this.root(e),this.listenerMap={},this.handle=function(e){r.prototype.handle.call(t,e)}}t.exports=r,r.tagsCaseSensitive=null,r.prototype.root=function(e){var t,r=this.listenerMap;if("string"==typeof e&&(e=document.querySelector(e)),this.rootElement)for(t in r)r.hasOwnProperty(t)&&this.rootElement.removeEventListener(t,this.handle,this.captureForType(t));if(!e||!e.addEventListener)return this.rootElement&&delete this.rootElement,this;this.rootElement=e;for(t in r)r.hasOwnProperty(t)&&this.rootElement.addEventListener(t,this.handle,this.captureForType(t));return this},r.prototype.captureForType=function(e){return"error"===e},r.prototype.on=function(e,t,n,o){var i,s,a,l;if(!e)throw new TypeError("Invalid event type: "+e);if("function"==typeof t&&(n=t,t=null,o=n),void 0===o&&(o=null),"function"!=typeof n)throw new TypeError("Handler must be a type of Function");return i=this.rootElement,s=this.listenerMap,s[e]||(i&&i.addEventListener(e,this.handle,this.captureForType(e)),s[e]=[]),t?/^[a-z]+$/i.test(t)?(null===r.tagsCaseSensitive&&(r.tagsCaseSensitive="i"===document.createElement("i").tagName),l=r.tagsCaseSensitive?t:t.toUpperCase(),a=this.matchesTag):/^#[a-z0-9\-_]+$/i.test(t)?(l=t.slice(1),a=this.matchesId):(l=t,a=this.matches):(l=null,a=this.matchesRoot.bind(this)),s[e].push({selector:t,eventData:o,handler:n,matcher:a,matcherParam:l}),this},r.prototype.off=function(e,t,r){var n,o,i,s,a;if("function"==typeof t&&(r=t,t=null),i=this.listenerMap,!e){for(a in i)i.hasOwnProperty(a)&&this.off(a,t,r);return this}if(s=i[e],!s||!s.length)return this;for(n=s.length-1;n>=0;n--)o=s[n],t&&t!==o.selector||r&&r!==o.handler||s.splice(n,1);return s.length||(delete i[e],this.rootElement&&this.rootElement.removeEventListener(e,this.handle,this.captureForType(e))),this},r.prototype.handle=function(e){var t,r,n,o,i,s,a,l="ftLabsDelegateIgnore";if(e[l]!==!0)for(a=e.target,a.nodeType===Node.TEXT_NODE&&(a=a.parentNode),n=this.rootElement,s=this.listenerMap[e.type],r=s.length;a&&r;){for(t=0;r>t&&(o=s[t],o);t++)if(o.matcher.call(a,o.matcherParam,a)&&(i=this.fire(e,a,o)),i===!1)return e[l]=!0,void 0;if(a===n)break;r=s.length,a=a.parentElement}},r.prototype.fire=function(e,t,r){var n,o;return null!==r.eventData?(o=e.data,e.data=r.eventData,n=r.handler.call(t,e,t),e.data=o):n=r.handler.call(t,e,t),n},r.prototype.matches=function(e){return e.matchesSelector||e.webkitMatchesSelector||e.mozMatchesSelector||e.msMatchesSelector||e.oMatchesSelector}(HTMLElement.prototype),r.prototype.matchesTag=function(e,t){return e===t.tagName},r.prototype.matchesRoot=function(e,t){return this.rootElement===t},r.prototype.matchesId=function(e,t){return e===t.id},r.prototype.destroy=function(){this.off(),this.root()}},{}]},{},[1])(1)});
View
1 build/logs/.gitignore
@@ -1 +0,0 @@
-*.lcov
View
859 lib/delegate.js
@@ -1,5 +1,6 @@
-/*jslint browser:true, node:true*/
-/*global define, Node*/
+/*jshint browser:true, node:true*/
+
+'use strict';
/**
* @preserve Create and manage a DOM event delegator.
@@ -10,439 +11,421 @@
* @license MIT License (see LICENSE.txt)
*/
-;(function(){
-
- 'use strict';
-
- /**
- * 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;
-
- /**
- * Start listening for events
- * on the provided DOM element
- *
- * @param {Node|string} [root] The root node or a selector string matching the root node
- * @returns {Delegate} This method is chainable
- */
- Delegate.prototype.root = function(root) {
- var listenerMap = this.listenerMap;
- var eventType;
-
- if (typeof root === 'string') {
- root = document.querySelector(root);
- }
-
- // 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 this;
- }
-
- /**
- * The root node at which
- * listeners are attached.
- *
- * @type Node
- */
- this.rootElement = root;
-
- // Set up master event listeners
- for (eventType in listenerMap) {
- if (listenerMap.hasOwnProperty(eventType)) {
- this.rootElement.addEventListener(eventType, this.handle, this.captureForType(eventType));
- }
- }
-
- return this;
- };
-
- /**
- * @param {string} eventType
- * @returns boolean
- */
- Delegate.prototype.captureForType = function(eventType) {
- 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|undefined} selector Only handle events on elements matching this selector, if undefined match root element
- * @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) {
- var root, listenerMap, matcher, matcherParam, self = this;
-
- if (!eventType) {
- throw new TypeError('Invalid event type: ' + eventType);
- }
-
- // handler can be passed as
- // the second or third argument
- if (typeof selector === 'function') {
- handler = selector;
- selector = null;
- eventData = handler;
- }
-
- // 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));
- }
- listenerMap[eventType] = [];
- }
-
- if (!selector) {
- matcherParam = null;
-
- // COMPLEX - matchesRoot needs to have access to
- // this.rootElement, so bind the function to this.
- matcher = this.matchesRoot.bind(this);
-
- // Compile a matcher for the given selector
- } else 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';
- }
-
- if (!Delegate.tagsCaseSensitive) {
- matcherParam = selector.toUpperCase();
- } else {
- matcherParam = selector;
- }
-
- matcher = this.matchesTag;
- } else if (/^#[a-z0-9\-_]+$/i.test(selector)) {
- matcherParam = selector.slice(1);
- matcher = this.matchesId;
- } else {
- matcherParam = selector;
- matcher = this.matches;
- }
-
- // Add to the list of listeners
- listenerMap[eventType].push({
- selector: selector,
- eventData: eventData,
- handler: handler,
- matcher: matcher,
- matcherParam: matcherParam
- });
-
- return 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) {
- var i, listener, listenerMap, listenerList, singleEventType, self = this;
-
- // Handler can be passed as
- // the second or third argument
- if (typeof selector === 'function') {
- handler = selector;
- selector = null;
- }
-
- listenerMap = this.listenerMap;
- if (!eventType) {
- for (singleEventType in listenerMap) {
- if (listenerMap.hasOwnProperty(singleEventType)) {
- this.off(singleEventType, selector, handler);
- }
- }
-
- return this;
- }
-
- listenerList = listenerMap[eventType];
- if (!listenerList || !listenerList.length) {
- return this;
- }
-
- // Remove only parameter matches if specified
- for (i = listenerList.length - 1; i >= 0; i--) {
- listener = listenerList[i];
-
- if ((!selector || selector === listener.selector) && (!handler || handler === listener.handler)) {
- listenerList.splice(i, 1);
- }
- }
-
- // All listeners removed
- if (!listenerList.length) {
- delete listenerMap[eventType];
-
- // Remove the main handler
- if (this.rootElement) {
- this.rootElement.removeEventListener(eventType, this.handle, this.captureForType(eventType));
- }
- }
-
- return this;
- };
-
-
- /**
- * Handle an arbitrary event.
- *
- * @param {Event} event
- */
- Delegate.prototype.handle = function(event) {
- var i, l, root, listener, returned, listenerList, target, /** @const */ EVENTIGNORE = 'ftLabsDelegateIgnore';
-
- if (event[EVENTIGNORE] === true) {
- return;
- }
-
- 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;
- 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;
- }
-
- // 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);
- }
-
- // Stop propagation to subsequent
- // callbacks if the callback returned
- // false
- if (returned === false) {
- 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;
- }
-
- l = listenerList.length;
- target = target.parentElement;
- }
- };
-
- /**
- * 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);
- }
-
- 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 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;
- };
-
- /**
- * 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;
- };
-
- /**
- * 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;
- };
-
- /**
- * Short hand for off()
- * and root(), ie both
- * with no parameters
- *
- * @return void
- */
- Delegate.prototype.destroy = function() {
- this.off();
- this.root();
- };
-
- /**
- * 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;
- }
-
-})();
+module.exports = Delegate;
+
+/**
+ * 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;
+
+/**
+ * Start listening for events
+ * on the provided DOM element
+ *
+ * @param {Node|string} [root] The root node or a selector string matching the root node
+ * @returns {Delegate} This method is chainable
+ */
+Delegate.prototype.root = function(root) {
+ var listenerMap = this.listenerMap;
+ var eventType;
+
+ if (typeof root === 'string') {
+ root = document.querySelector(root);
+ }
+
+ // 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 this;
+ }
+
+ /**
+ * The root node at which
+ * listeners are attached.
+ *
+ * @type Node
+ */
+ this.rootElement = root;
+
+ // Set up master event listeners
+ for (eventType in listenerMap) {
+ if (listenerMap.hasOwnProperty(eventType)) {
+ this.rootElement.addEventListener(eventType, this.handle, this.captureForType(eventType));
+ }
+ }
+
+ return this;
+};
+
+/**
+ * @param {string} eventType
+ * @returns boolean
+ */
+Delegate.prototype.captureForType = function(eventType) {
+ 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|undefined} selector Only handle events on elements matching this selector, if undefined match root element
+ * @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) {
+ var root, listenerMap, matcher, matcherParam, self = this;
+
+ if (!eventType) {
+ throw new TypeError('Invalid event type: ' + eventType);
+ }
+
+ // handler can be passed as
+ // the second or third argument
+ if (typeof selector === 'function') {
+ handler = selector;
+ selector = null;
+ eventData = handler;
+ }
+
+ // 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));
+ }
+ listenerMap[eventType] = [];
+ }
+
+ if (!selector) {
+ matcherParam = null;
+
+ // COMPLEX - matchesRoot needs to have access to
+ // this.rootElement, so bind the function to this.
+ matcher = this.matchesRoot.bind(this);
+
+ // Compile a matcher for the given selector
+ } else 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';
+ }
+
+ if (!Delegate.tagsCaseSensitive) {
+ matcherParam = selector.toUpperCase();
+ } else {
+ matcherParam = selector;
+ }
+
+ matcher = this.matchesTag;
+ } else if (/^#[a-z0-9\-_]+$/i.test(selector)) {
+ matcherParam = selector.slice(1);
+ matcher = this.matchesId;
+ } else {
+ matcherParam = selector;
+ matcher = this.matches;
+ }
+
+ // Add to the list of listeners
+ listenerMap[eventType].push({
+ selector: selector,
+ eventData: eventData,
+ handler: handler,
+ matcher: matcher,
+ matcherParam: matcherParam
+ });
+
+ return 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) {
+ var i, listener, listenerMap, listenerList, singleEventType, self = this;
+
+ // Handler can be passed as
+ // the second or third argument
+ if (typeof selector === 'function') {
+ handler = selector;
+ selector = null;
+ }
+
+ listenerMap = this.listenerMap;
+ if (!eventType) {
+ for (singleEventType in listenerMap) {
+ if (listenerMap.hasOwnProperty(singleEventType)) {
+ this.off(singleEventType, selector, handler);
+ }
+ }
+
+ return this;
+ }
+
+ listenerList = listenerMap[eventType];
+ if (!listenerList || !listenerList.length) {
+ return this;
+ }
+
+ // Remove only parameter matches
+ // if specified
+ for (i = listenerList.length - 1; i >= 0; i--) {
+ listener = listenerList[i];
+
+ if ((!selector || selector === listener.selector) && (!handler || handler === listener.handler)) {
+ listenerList.splice(i, 1);
+ }
+ }
+
+ // All listeners removed
+ if (!listenerList.length) {
+ delete listenerMap[eventType];
+
+ // Remove the main handler
+ if (this.rootElement) {
+ this.rootElement.removeEventListener(eventType, this.handle, this.captureForType(eventType));
+ }
+ }
+
+ return this;
+};
+
+
+/**
+ * Handle an arbitrary event.
+ *
+ * @param {Event} event
+ */
+Delegate.prototype.handle = function(event) {
+ var i, l, root, listener, returned, listenerList, target, /** @const */ EVENTIGNORE = 'ftLabsDelegateIgnore';
+
+ if (event[EVENTIGNORE] === true) {
+ return;
+ }
+
+ 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;
+ 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;
+ }
+
+ // 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);
+ }
+
+ // Stop propagation to subsequent
+ // callbacks if the callback returned
+ // false
+ if (returned === false) {
+ 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;
+ }
+
+ l = listenerList.length;
+ target = target.parentElement;
+ }
+};
+
+/**
+ * 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);
+ }
+
+ 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 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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * Short hand for off()
+ * and root(), ie both
+ * with no parameters
+ *
+ * @return void
+ */
+Delegate.prototype.destroy = function() {
+ this.off();
+ this.root();
+};
View
9 package.json
@@ -17,7 +17,7 @@
"node": "*"
},
"scripts": {
- "test": "node_modules/.bin/buster-test -c test/buster.js"
+ "test": "./node_modules/.bin/grunt buster"
},
"keywords": [
"delegate",
@@ -26,7 +26,12 @@
],
"devDependencies": {
"buster": "~0.6.2",
- "buster-coverage": "*"
+ "buster-coverage": "*",
+ "grunt": "~0.4.1",
+ "grunt-cli": "~0.1.8",
+ "grunt-buster": "~0.1.2",
+ "grunt-browserify": "1.0.2",
+ "grunt-contrib-uglify": "~0.1.2"
},
"license": "MIT",
"homepage": "https://github.com/ftlabs/dom-delegate"

0 comments on commit 90b59d2

Please sign in to comment.
Something went wrong with that request. Please try again.