From 20c3ae609c29ea7fc0bd7583f6b3361dd65329d2 Mon Sep 17 00:00:00 2001 From: Cam Song Date: Mon, 23 Nov 2015 00:27:40 +0800 Subject: [PATCH] open source --- .babelrc | 3 + .editorconfig | 12 ++ .eslintrc | 17 ++ .gitignore | 29 +--- .npmignore | 5 + .travis.yml | 7 + README.md | 3 + build/index.js | 389 +++++++++++++++++++++++++++++++++++++++++++ karma.conf.js | 95 +++++++++++ package.json | 53 ++++++ src/index.js | 205 +++++++++++++++++++++++ test/index.spec.js | 296 ++++++++++++++++++++++++++++++++ test/triggerEvent.js | 164 ++++++++++++++++++ 13 files changed, 1253 insertions(+), 25 deletions(-) create mode 100644 .babelrc create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 build/index.js create mode 100644 karma.conf.js create mode 100644 package.json create mode 100644 src/index.js create mode 100644 test/index.spec.js create mode 100644 test/triggerEvent.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..e2472af --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + presets: ["es2015", "stage-0"] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e376cf7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig helps developers define and maintain +# consistent coding styles between different editors and IDEs. + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..fcc7e3d --- /dev/null +++ b/.eslintrc @@ -0,0 +1,17 @@ +{ + "extends": "eslint-config-airbnb", + "env": { + "browser": true, + "mocha": true, + "node": true + }, + "rules": { + "valid-jsdoc": 2, + "no-param-reassign": 0, + "comma-dangle": 0, + "one-var": 0, + "no-else-return": 1, + "no-unused-expressions": 0, + "indent": 1 + } +} diff --git a/.gitignore b/.gitignore index 123ae94..7c9b99c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,6 @@ -# Logs -logs +.DS_Store *.log - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules +lib +coverage +logs diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f5c0a99 --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +.DS_Store +*.log +examples +test +coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..eab377c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - "5" + - "4" +script: + - npm run lint + - npm test diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d3f340 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# oui-dom-events [![Build Status](https://travis-ci.org/oneuijs/oui-dom-events.svg)](https://travis-ci.org/oneuijs/oui-dom-events) [![npm version](https://badge.fury.io/js/oui-dom-events.svg)](http://badge.fury.io/js/oui-dom-events) + +DOM events manager with namespace support diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..15f47ba --- /dev/null +++ b/build/index.js @@ -0,0 +1,389 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +/* eslint no-unused-expressions: 0 */ +var reUnit = /width|height|top|left|right|bottom|margin|padding/i; + +exports.default = { + // el can be an Element, NodeList or selector + + addClass: function addClass(el, className) { + var _this = this; + + if (typeof el === 'string') el = document.querySelectorAll(el); + var els = el instanceof NodeList ? [].slice.call(el) : [el]; + + els.forEach(function (e) { + if (_this.hasClass(e, className)) { + return; + } + + if (e.classList) { + e.classList.add(className); + } else { + e.className += ' ' + className; + } + }); + }, + + // el can be an Element, NodeList or selector + removeClass: function removeClass(el, className) { + if (typeof el === 'string') el = document.querySelectorAll(el); + var els = el instanceof NodeList ? [].slice.call(el) : [el]; + + els.forEach(function (e) { + if (e.classList) { + e.classList.remove(className); + } else { + e.className = e.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } + }); + }, + + // el can be an Element or selector + hasClass: function hasClass(el, className) { + if (typeof el === 'string') el = document.querySelector(el); + if (el.classList) { + return el.classList.contains(className); + } else { + return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); + } + }, + insertAfter: function insertAfter(newEl, targetEl) { + var parent = targetEl.parentNode; + + if (parent.lastChild === targetEl) { + parent.appendChild(newEl, targetEl); + } else { + parent.insertBefore(newEl, targetEl.nextSibling); + } + }, + + /** + * el can be an Element, NodeList or query string + */ + remove: function remove(el) { + if (typeof el === 'string') { + [].forEach.call(document.querySelectorAll(el), function (node) { + node.parentNode.removeChild(node); + }); + } else if (el.parentNode) { + // it's an Element + el.parentNode.removeChild(el); + } else if (el instanceof NodeList) { + // it's an array of elements + [].forEach.call(el, function (node) { + node.parentNode.removeChild(node); + }); + } else { + console.error('you can only pass Element, array of Elements or query string as argument'); + } + }, + forceReflow: function forceReflow(el) { + el.offsetHeight; + }, + getDocumentScrollTop: function getDocumentScrollTop() { + // IE8 used `document.documentElement` + return document.documentElement && document.documentElement.scrollTop || document.body.scrollTop; + }, + + // Set the current vertical position of the scroll bar for document + // Note: do not support fixed position of body + setDocumentScrollTop: function setDocumentScrollTop(value) { + window.scrollTo(0, value); + return value; + }, + outerHeight: function outerHeight(el) { + return el.offsetHeight; + }, + outerHeightWithMargin: function outerHeightWithMargin(el) { + var height = el.offsetHeight; + var style = getComputedStyle(el); + + height += (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0); + return height; + }, + outerWidth: function outerWidth(el) { + return el.offsetWidth; + }, + outerWidthWithMargin: function outerWidthWithMargin(el) { + var width = el.offsetWidth; + var style = getComputedStyle(el); + + width += (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0); + return width; + }, + getComputedStyles: function getComputedStyles(el) { + return el.ownerDocument.defaultView.getComputedStyle(el, null); + }, + getOffset: function getOffset(el) { + var html = el.ownerDocument.documentElement; + var box = { top: 0, left: 0 }; + + // If we don't have gBCR, just use 0,0 rather than error + // BlackBerry 5, iOS 3 (original iPhone) + if (typeof el.getBoundingClientRect !== 'undefined') { + box = el.getBoundingClientRect(); + } + + return { + top: box.top + window.pageYOffset - html.clientTop, + left: box.left + window.pageXOffset - html.clientLeft + }; + }, + getPosition: function getPosition(el) { + if (!el) { + return { + left: 0, + top: 0 + }; + } + + return { + left: el.offsetLeft, + top: el.offsetTop + }; + }, + setStyle: function setStyle(node, att, val, style) { + style = style || node.style; + + if (style) { + if (val === null || val === '') { + // normalize unsetting + val = ''; + } else if (!isNaN(Number(val)) && reUnit.test(att)) { + // number values may need a unit + val += 'px'; + } + + if (att === '') { + att = 'cssText'; + val = ''; + } + + style[att] = val; + } + }, + setStyles: function setStyles(node, hash) { + var _this2 = this; + + var HAS_CSSTEXT_FEATURE = typeof node.style.cssText != 'undefined'; + function trim(str) { + return str.replace(/^\s+|\s+$/g, ''); + } + var originStyleText = undefined; + var originStyleObj = {}; + if (!!HAS_CSSTEXT_FEATURE) { + originStyleText = node.style.cssText; + } else { + originStyleText = node.getAttribute('style', styleText); + } + originStyleText.split(';').forEach(function (item) { + if (item.indexOf(':') != -1) { + var obj = item.split(':'); + originStyleObj[trim(obj[0])] = trim(obj[1]); + } + }); + + var styleObj = {}; + Object.keys(hash).forEach(function (item) { + _this2.setStyle(node, item, hash[item], styleObj); + }); + var mergedStyleObj = Object.assign({}, originStyleObj, styleObj); + var styleText = Object.keys(mergedStyleObj).map(function (item) { + return item + ': ' + mergedStyleObj[item] + ';'; + }).join(' '); + + if (!!HAS_CSSTEXT_FEATURE) { + node.style.cssText = styleText; + } else { + node.setAttribute('style', styleText); + } + }, + getStyle: function getStyle(node, att, style) { + style = style || node.style; + + var val = ''; + + if (style) { + val = style[att]; + + if (val === '') { + val = this.getComputedStyle(node, att); + } + } + + return val; + }, + + // NOTE: Known bug, will return 'auto' if style value is 'auto' + getComputedStyle: function getComputedStyle(el, att) { + var win = el.ownerDocument.defaultView; + // null means not return presudo styles + var computed = win.getComputedStyle(el, null); + + return attr ? computed.attr : computed; + }, + getPageSize: function getPageSize() { + var xScroll = undefined, + yScroll = undefined; + + if (window.innerHeight && window.scrollMaxY) { + xScroll = window.innerWidth + window.scrollMaxX; + yScroll = window.innerHeight + window.scrollMaxY; + } else { + if (document.body.scrollHeight > document.body.offsetHeight) { + // all but Explorer Mac + xScroll = document.body.scrollWidth; + yScroll = document.body.scrollHeight; + } else { + // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari + xScroll = document.body.offsetWidth; + yScroll = document.body.offsetHeight; + } + } + + var windowWidth = undefined, + windowHeight = undefined; + + if (self.innerHeight) { + // all except Explorer + if (document.documentElement.clientWidth) { + windowWidth = document.documentElement.clientWidth; + } else { + windowWidth = self.innerWidth; + } + windowHeight = self.innerHeight; + } else { + if (document.documentElement && document.documentElement.clientHeight) { + // Explorer 6 Strict Mode + windowWidth = document.documentElement.clientWidth; + windowHeight = document.documentElement.clientHeight; + } else { + if (document.body) { + // other Explorers + windowWidth = document.body.clientWidth; + windowHeight = document.body.clientHeight; + } + } + } + + var pageHeight = undefined, + pageWidth = undefined; + + // for small pages with total height less then height of the viewport + if (yScroll < windowHeight) { + pageHeight = windowHeight; + } else { + pageHeight = yScroll; + } + // for small pages with total width less then width of the viewport + if (xScroll < windowWidth) { + pageWidth = xScroll; + } else { + pageWidth = windowWidth; + } + + return { + pageWidth: pageWidth, + pageHeight: pageHeight, + windowWidth: windowWidth, + windowHeight: windowHeight + }; + }, + get: function get(selector) { + return document.querySelector(selector) || {}; + }, + getAll: function getAll(selector) { + return document.querySelectorAll(selector); + }, + + /** + * selector 可选。字符串值,规定在何处停止对祖先元素进行匹配的选择器表达式。 + * filter 可选。字符串值,包含用于匹配元素的选择器表达式。 + */ + parentsUntil: function parentsUntil(el, selector, filter) { + var result = []; + var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (filter == null) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + }, + + // 获得匹配选择器的第一个祖先元素,从当前元素开始沿 DOM 树向上 + closest: function closest(el, selector) { + var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + }, + + /** + * @param {number} to assign the scrollTop value + * @param {number} duration assign the animate duration + */ + scrollTo: function scrollTo() { + var _this3 = this; + + var to = arguments.length <= 0 || arguments[0] === undefined ? 0 : arguments[0]; + var duration = arguments.length <= 1 || arguments[1] === undefined ? 16 : arguments[1]; + + if (duration < 0) { + return; + } + var diff = to - this.getDocumentScrollTop(); + var perTick = diff / duration * 10; + requestAnimationFrame(function () { + if (Math.abs(perTick) > Math.abs(diff)) { + _this3.setDocumentScrollTop(_this3.getDocumentScrollTop() + diff); + return; + } + _this3.setDocumentScrollTop(_this3.getDocumentScrollTop() + perTick); + if (diff > 0 && _this3.getDocumentScrollTop() >= to || diff < 0 && _this3.getDocumentScrollTop() <= to) { + return; + } + _this3.scrollTo(to, duration - 16); + }); + }, + + // matches(el, '.my-class'); 这里不能使用伪类选择器 + is: function is(el, selector) { + return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); + }, + width: function width(el) { + var styles = this.getComputedStyles(el); + var width = el.offsetWidth; + var borderLeftWidth = parseFloat(styles.borderLeftWidth); + var borderRightWidth = parseFloat(styles.borderRightWidth); + var paddingLeft = parseFloat(styles.paddingLeft); + var paddingRight = parseFloat(styles.paddingRight); + return width - borderRightWidth - borderLeftWidth - paddingLeft - paddingRight; + }, + height: function height(el) { + var styles = this.getComputedStyles(el); + var height = el.offsetHeight; + var borderTopWidth = parseFloat(styles.borderTopWidth); + var borderBottomWidth = parseFloat(styles.borderBottomWidth); + var paddingTop = parseFloat(styles.paddingTop); + var paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } +}; \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..fd6ca3c --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,95 @@ +// Karma configuration +// Generated on Sun Nov 22 2015 22:10:47 GMT+0800 (CST) +require('babel-core/register'); + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '.', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + + // list of files / patterns to load in the browser + files: [ + './test/**/*.spec.js' + ], + + // list of files to exclude + exclude: [ + ], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test/**/*.spec.js': ['webpack', 'sourcemap'] + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + coverageReporter: { + reporters: [ + {type: 'text'}, + {type: 'html', dir: 'coverage'}, + ] + }, + + webpack: { + cache: true, + devtool: 'inline-source-map', + + stats: { + colors: true, + reasons: true, + chunks: false + }, + + module: { + loaders: [{ + test: /\.jsx?$/, + loader: 'babel-loader', + exclude: /node_modules/ + }], + postLoaders: [{ + test: /\.js/, + exclude: /(test|node_modules)/, + loader: 'istanbul-instrumenter' + }], + }, + resolve: { + extensions: ['', '.js', '.jsx'] + } + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + // singleRun: false, + + // Concurrency level + // how many browser should be started simultanous + // concurrency: Infinity, + + // plugins: ['karma-phantomjs-launcher', 'karma-sourcemap-loader', 'karma-webpack'] + }) +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4ff6a50 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "oui-dom-events", + "version": "0.1.0", + "description": "DOM Utils of OneUI", + "main": "build/index.js", + "author": "oneui group", + "keywords": [ + "OneUI", + "DOM" + ], + "repository": { + "type": "git", + "url": "https://github.com/oneuijs/oui-dom-events.git" + }, + "bugs": { + "url": "https://github.com/oneuijs/oui-dom-events/issues" + }, + "homepage": "https://github.com/oneuijs/oui-dom-events", + "scripts": { + "test": "karma start --single-run", + "tdd": "karma start --auto-watch --no-single-run", + "test-cov": "karma start --auto-watch --single-run --reporters progress,coverage", + "build": "babel src --out-dir build", + "clean": "rm -rf build", + "lint": "eslint src test" + }, + "dependencies": {}, + "devDependencies": { + "babel-cli": "^6.2.0", + "babel-core": "^6.1.21", + "babel-eslint": "^4.1.5", + "babel-loader": "^6.2.0", + "babel-preset-es2015": "^6.1.18", + "babel-preset-stage-0": "^6.1.18", + "chai": "^3.4.1", + "chai-spies": "^0.7.1", + "eslint": "^1.9.0", + "eslint-config-airbnb": "^1.0.0", + "eslint-plugin-react": "^3.10.0", + "isparta": "^4.0.0", + "istanbul-instrumenter-loader": "^0.1.3", + "karma": "^0.13.15", + "karma-coverage": "^0.5.3", + "karma-mocha": "^0.2.1", + "karma-phantomjs-launcher": "^0.2.1", + "karma-sourcemap-loader": "^0.3.6", + "karma-webpack": "^1.7.0", + "mocha": "^2.3.4", + "phantomjs": "^1.9.18", + "webpack": "^1.12.8" + }, + "license": "MIT" +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..5b869eb --- /dev/null +++ b/src/index.js @@ -0,0 +1,205 @@ +// IE10+ Support +// inspired by zepto event https://github.com/madrobby/zepto/blob/master/src/event.js + +let handlers = {}; + +let specialEvents = {}; +specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents' + +// every element and callback function will have an unique dtId +let _dtId = 1; + +/** + * Get dtId of Element or callback function + * @param {Object|Function} obj Element or callback function + * @return {Number} unique dtId + */ +function getDtId(obj) { + return obj._dtId || (obj._dtId = _dtId++); +} + +/** + * Get event object of event string, the first `.` is used to split event and namespace + * + * @param {String} event Event type string with namespace or not + * @return {Object} An Object with `e` and `ns` key + */ +function parse(event) { + let dotIndex = event.indexOf('.'); + if (dotIndex > 0) { + return { + e: event.substring(0, event.indexOf('.')), + ns: event.substring(dotIndex + 1, event.length) + } + } else { + return { + e: event + } + } +} + +/** + * Find matched event handlers + * @param {Element} el + * @param {String} selector Used by event delegation, null if not + * @param {String} event Event string may with namespace + * @param {Function} callback + * @return {Array} Array of handlers bind to el + */ +function findHandlers(el, selector, event, callback) { + event = parse(event); + return (handlers[getDtId(el)] || []).filter(handler => { + return handler + && (!event.e || handler.e === event.e) + && (!event.ns || handler.ns === event.ns) + && (!callback || handler.callback === callback) + && (!selector || handler.selector === selector) + }); +} + +/** + * @param {Element} + * @param {[type]} + * @param {[type]} + * @param {Function} + * @return {[type]} + */ +function removeEvent(el, selector, event, callback) { + let eventName = parse(event).e; + + let handlers = findHandlers(el, selector, event, callback); + handlers.forEach(handler => { + if (el.removeEventListener) { + el.removeEventListener(eventName, handler.delegator || handler.callback); + } else if (el.detachEvent) { + el.detachEvent('on' + eventName, handler.delegator || handler.callback); + } + handler = null; + }); +} + +// delegator 只用于 delegate 时有用。 +function bindEvent(el, selector, event, callback, delegator) { + let eventName = parse(event).e; + let ns = parse(event).ns; + + if (el.addEventListener) { + el.addEventListener(eventName, delegator || callback, false); + } else if (el.attachEvent) { + el.attachEvent('on' + eventName, delegator || callback); + } + + // push events to handlers + let id = getDtId(el); + let elHandlers = (handlers[id] || (handlers[id] = [])); + elHandlers.push({ + delegator: delegator, + callback: callback, + e: eventName, + ns: ns, + selector: selector + }); +} + +const Events = { + /** + * Register a callback + * + * @param {Element} el + * @param {String} eventType + * @param {Function} callback + * @return {Null} + */ + on(el, eventType, callback) { + bindEvent(el, null, eventType, callback); + }, + + /** + * Unregister a callback + * + * @param {Element} el + * @param {String} eventType + * @param {Function} callback Optional + * @return {Null} + */ + off(el, eventType, callback) { + // find callbacks + removeEvent(el, null, eventType, callback); + }, + + /** + * Register a callback that will execute exactly once + * + * @param {Element} el + * @param {String} eventType + * @param {Function} callback + * @return {Null} + */ + once(el, eventType, callback) { + let recursiveFunction = e => { + Events.off(e.currentTarget, e.type, recursiveFunction); + return callback(e); + }; + + this.on(el, eventType, recursiveFunction); + }, + + /** + * Delegate a callback to selector under el + * + * @param {Element} el + * @param {String} selector + * @param {String} eventType + * @param {Function} callback + * @return {Null} + */ + delegate(el, selector, eventType, callback) { + // bind event to el. and check if selector match + let delegator = e => { + let els = el.querySelectorAll(selector); + let matched = false; + for(let i = 0; i < els.length; i++) { + let _el = els[i]; + if (_el === e.target || _el.contains(e.target)) { + matched = _el; + break; + } + } + if (matched) { + callback.apply(matched, [].slice.call(arguments)); + } + } + + bindEvent(el, selector, eventType, callback, delegator); + }, + + /** + * Undelegate a callback to selector under el + * + * @param {Element} el + * @param {String} selector + * @param {String} eventType + * @param {Function} callback + * @return {Null} + */ + undelegate(el, selector, eventType, callback) { + removeEvent(el, selector, eventType, callback); + }, + + /** + * Dispatch an event with props to el + * @param {Element} el + * @param {String} eventType + * @param {Object} props Optional + * @return {Null} + */ + trigger(el, eventType, props) { + let event = document.createEvent(specialEvents[eventType] || 'Events'); + let bubbles = true; + if (props) for (let name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]) + event.initEvent(eventType, bubbles, true); + el.dispatchEvent(event); + } +}; + +export default Events; diff --git a/test/index.spec.js b/test/index.spec.js new file mode 100644 index 0000000..9e2fc00 --- /dev/null +++ b/test/index.spec.js @@ -0,0 +1,296 @@ +import spies from 'chai-spies'; +import chai, { expect } from 'chai'; +import triggerEvent from './triggerEvent.js'; +import Events from '../src/index.js'; + +chai.use(spies); + +function injectHTML() { + document.body.innerHTML = ` +
+ +
+ `; +}; + +function uninjectHTML() { + var el = document.querySelector('#event-test'); + el.parentNode.removeChild(el); +}; + +describe('Events', () => { + describe('.on and .off', () => { + beforeEach(() => { + injectHTML(); + }); + + afterEach(() => { + uninjectHTML(); + }); + + it('on should invoke callback when event fired', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click', callback); + + triggerEvent(el, 'click'); + expect(callback).to.have.been.called.once; + }); + + it('on bind same callback twice will only invoke once', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click', callback); + Events.on(el, 'click', callback); + + triggerEvent(el, 'click'); + expect(callback).to.have.been.called.once; + }); + + it('on can bind two events', () => { + let callback1 = chai.spy(); + let callback2 = chai.spy(); + let callback3 = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click', callback1); + Events.on(el, 'click', callback2); + + triggerEvent(el, 'click'); + expect(callback1).to.have.been.called.once; + expect(callback2).to.have.been.called.once; + expect(callback3).to.have.not.been.called; + }); + + it('off can remove on event', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click', callback); + Events.off(el, 'click', callback); + + triggerEvent(el, 'click'); + expect(callback).to.have.not.been.called; + }); + }); + + describe('once', () => { + beforeEach(() => { + injectHTML(); + }); + + afterEach(() => { + uninjectHTML(); + }); + + it('once will only invoke callback once', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.once(el, 'click', callback); + + triggerEvent(el, 'click'); + triggerEvent(el, 'click'); + expect(callback).to.have.been.called.once; + }); + + it('on will invoke callback many times as you trigger', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click', callback); + + triggerEvent(el, 'click'); + triggerEvent(el, 'click'); + triggerEvent(el, 'click'); + expect(callback).to.have.been.called.exactly(3); + }); + + it('off can unbind once', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.once(el, 'click', callback); + Events.off(el, 'click'); + + triggerEvent(el, 'click'); + expect(callback).to.have.not.been.called; + }); + }); + + describe('with namespace', () => { + beforeEach(() => { + injectHTML(); + }); + + afterEach(() => { + uninjectHTML(); + }); + + it('on can bind with namespace', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click.testns', callback); + + triggerEvent(el, 'click'); + expect(callback).to.have.been.called.once; + }); + + it('off can remove event with namespace', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click.testns', callback); + Events.off(el, 'click', callback); + + triggerEvent(el, 'click'); + expect(callback).to.have.not.been.called; + }); + + it('off with namespace only remove that namespace', () => { + let callback1 = chai.spy(); + let callback2 = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click.testns', callback1); + Events.on(el, 'click.anotherns', callback2); + Events.off(el,'click.anotherns'); + + triggerEvent(el, 'click'); + expect(callback1).to.have.been.called.once; + expect(callback2).to.have.not.been.called; + }); + + it('off without namespace will remove all events', () => { + let callback1 = chai.spy(); + let callback2 = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click.testns', callback1); + Events.on(el, 'click.anotherns', callback2); + Events.off(el,'click'); + + triggerEvent(el, 'click'); + expect(callback1).to.have.not.been.called; + expect(callback2).to.have.not.been.called; + }); + }); + + + describe('event delegation', () => { + beforeEach(() => { + injectHTML(); + }); + + afterEach(() => { + uninjectHTML(); + }); + + it('delegate can bind event', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.delegate(el, 'li.red', 'click', callback); + + triggerEvent(document.querySelector('#event-test ul li.red'), 'click'); + expect(callback).to.have.been.called.once; + + triggerEvent(document.querySelector('#event-test ul li.green'), 'click'); + expect(callback).to.have.not.been.called; + }); + + it('undelegate can remove event delegation', () => { + let el = document.querySelector('#event-test ul'); + let callback1 = chai.spy(); + let callback2 = chai.spy(); + Events.delegate(el, 'li.red', 'click', callback1); + Events.delegate(el, 'li.green', 'click', callback2); + + Events.undelegate(el, 'li.red', 'click'); + + triggerEvent(document.querySelector('#event-test ul li.red'), 'click'); + triggerEvent(document.querySelector('#event-test ul li.green'), 'click'); + expect(callback1).to.have.not.been.called; + expect(callback2).to.have.been.called.once; + }); + + it('off can also remove event delegation', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.delegate(el, 'li.red', 'click', callback); + Events.off(el, 'li.red', 'click'); + + triggerEvent(el, 'click'); + expect(callback).to.have.not.been.called; + }); + + it('off with namespace only remove that namespace', () => { + let callback1 = chai.spy(); + let callback2 = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.delegate(el, 'li.red', 'click.testns', callback1); + Events.delegate(el, 'li.red', 'click.anotherns', callback2); + Events.off(el,'click.anotherns'); + + triggerEvent(el.querySelector('li.red'), 'click'); + expect(callback1).to.have.been.called.once; + expect(callback2).to.have.not.been.called; + }); + + it('off without namespace will remove all events', () => { + let callback1 = chai.spy(); + let callback2 = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.delegate(el, 'li.red', 'click.testns', callback1); + Events.delegate(el, 'li.red', 'click.anotherns', callback2); + Events.off(el,'click.anotherns'); + + triggerEvent(el.querySelector('li.red'), 'click'); + expect(callback1).to.have.not.been.called; + expect(callback2).to.have.not.been.called; + }); + }); + + describe('trigger', () => { + beforeEach(() => { + injectHTML(); + }); + + afterEach(() => { + uninjectHTML(); + }); + + it('can trigger events', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'click', callback); + + Events.trigger(el, 'click'); + expect(callback).to.have.been.called.once; + }); + + it('can trigger scroll events', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'scroll', callback); + + Events.trigger(el, 'scroll'); + expect(callback).to.have.been.called.once; + }); + + it('can trigger resize events', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'resize', callback); + + Events.trigger(el, 'resize'); + expect(callback).to.have.been.called.once; + }); + + it('can trigger with params', () => { + let callback = chai.spy(); + let el = document.querySelector('#event-test ul'); + Events.on(el, 'resize', callback); + + Events.trigger(el, 'resize', {foo: 'bar'}); + expect(callback).to.have.been.called.once; + }); + }); +}); diff --git a/test/triggerEvent.js b/test/triggerEvent.js new file mode 100644 index 0000000..5f90ca6 --- /dev/null +++ b/test/triggerEvent.js @@ -0,0 +1,164 @@ +/** + * @see https://github.com/adamsanderson/trigger-event + */ +module.exports = trigger; + +/** + Event type mappings. + This is not an exhaustive list. +*/ +var eventTypes = { + load: 'HTMLEvents', + unload: 'HTMLEvents', + abort: 'HTMLEvents', + error: 'HTMLEvents', + select: 'HTMLEvents', + change: 'HTMLEvents', + submit: 'HTMLEvents', + reset: 'HTMLEvents', + focus: 'HTMLEvents', + blur: 'HTMLEvents', + resize: 'HTMLEvents', + scroll: 'HTMLEvents', + input: 'HTMLEvents', + + keyup: 'KeyboardEvent', + keydown: 'KeyboardEvent', + + click: 'MouseEvents', + dblclick: 'MouseEvents', + mousedown: 'MouseEvents', + mouseup: 'MouseEvents', + mouseover: 'MouseEvents', + mousemove: 'MouseEvents', + mouseout: 'MouseEvents', + contextmenu: 'MouseEvents' +}; + +// Default event properties: +var defaults = { + clientX: 0, + clientY: 0, + button: 0, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + bubbles: true, + cancelable: true, + view: document.defaultView, + key: '', + location: 0, + modifiers: '', + repeat: 0, + locale: '' +}; + +/** + * Trigger a DOM event. + * + * trigger(document.body, "click", {clientX: 10, clientY: 35}); + * + * Where sensible, sane defaults will be filled in. See the list of event + * types for supported events. + * + * Loosely based on: + * https://github.com/kangax/protolicious/blob/master/event.simulate.js + */ +function trigger(el, name, options){ + var event, type; + + options = options || {}; + for (var attr in defaults) { + if (!options.hasOwnProperty(attr)) { + options[attr] = defaults[attr]; + } + } + + if (document.createEvent) { + // Standard Event + type = eventTypes[name] || 'CustomEvent'; + event = document.createEvent(type); + initializers[type](el, name, event, options); + el.dispatchEvent(event); + } else { + // IE Event + event = document.createEventObject(); + for (var key in options){ + event[key] = options[key]; + } + el.fireEvent('on' + name, event); + } +} + +var initializers = { + HTMLEvents: function(el, name, event, o){ + return event.initEvent(name, o.bubbles, o.cancelable); + }, + + KeyboardEvent: function(el, name, event, o){ + // Use a blank key if not defined and initialize the charCode + var key = ('key' in o) ? o.key : ""; + var charCode; + var modifiers; + + // 0 is the default location + var location = ('location' in o) ? o.location : 0; + + if (event.initKeyboardEvent) { + // Chrome and IE9+ uses initKeyboardEvent + if (! 'modifiers' in o) { + modifiers = []; + if (o.ctrlKey) modifiers.push("Ctrl"); + if (o.altKey) modifiers.push("Alt"); + if (o.ctrlKey && o.altKey) modifiers.push("AltGraph"); + if (o.shiftKey) modifiers.push("Shift"); + if (o.metaKey) modifiers.push("Meta"); + modifiers = modifiers.join(" "); + } else { + modifiers = o.modifiers; + } + + return event.initKeyboardEvent( + name, o.bubbles, o.cancelable, o.view, + key, location, modifiers, o.repeat, o.locale + ); + } else { + // Mozilla uses initKeyEvent + charCode = ('charCode' in o) ? o.charCode : key.charCodeAt(0) || 0; + return event.initKeyEvent( + name, o.bubbles, o.cancelable, o.view, + o.ctrlKey, o.altKey, o.shiftKey, + o.metaKey, charCode, key + ); + } + }, + + MouseEvents: function(el, name, event, o){ + var screenX = ('screenX' in o) ? o.screenX : o.clientX; + var screenY = ('screenY' in o) ? o.screenY : o.clientY; + var clicks; + var button; + + if ('detail' in o) { + clicks = o.detail; + } else if (name === 'dblclick') { + clicks = 2; + } else { + clicks = 1; + } + + // Default context menu to be a right click + if (name === 'contextmenu') { + button = button = o.button || 2; + } + + return event.initMouseEvent(name, o.bubbles, o.cancelable, o.view, + clicks, screenX, screenY, o.clientX, o.clientY, + o.ctrlKey, o.altKey, o.shiftKey, o.metaKey, button, el); + }, + + CustomEvent: function(el, name, event, o){ + return event.initCustomEvent(name, o.bubbles, o.cancelable, o.detail); + } +};