├── .babelrc ├── .npmignore ├── .gitignore ├── .travis.yml ├── .editorconfig ├── CHANGELOG.md ├── .eslintrc ├── LICENSE ├── package.json ├── README.md ├── karma.conf.js ├── test ├── triggerEvent.js └── index.spec.js └── src └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | examples 4 | test 5 | coverage 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | lib 5 | coverage 6 | logs 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "4" 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | script: 9 | - npm run lint 10 | - npm test 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.2.2 / 2017-06-14 2 | ================== 3 | - fix the removeEvent func 4 | 5 | 0.2.1 / 2015-12-01 6 | ================== 7 | - Update README API docs 8 | 9 | 0.2.0 / 2015-12-01 10 | ================== 11 | - Fix event delegation arguments 12 | 13 | 0.1.0 / 2015-11-23 14 | ================== 15 | - Initial release 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "valid-jsdoc": 2, 10 | "no-param-reassign": 0, 11 | "comma-dangle": 0, 12 | "one-var": 0, 13 | "no-else-return": 1, 14 | "no-unused-expressions": 0, 15 | "indent": 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 oneuijs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-dom-events", 3 | "version": "0.2.2", 4 | "description": "DOM Utils of OneUI", 5 | "main": "build/index.js", 6 | "author": "oneui group", 7 | "keywords": [ 8 | "OneUI", 9 | "DOM" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/oneuijs/oui-dom-events.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/oneuijs/oui-dom-events/issues" 17 | }, 18 | "homepage": "https://github.com/oneuijs/oui-dom-events", 19 | "scripts": { 20 | "test": "karma start --single-run", 21 | "tdd": "karma start --auto-watch --no-single-run", 22 | "test-cov": "karma start --auto-watch --single-run --reporters progress,coverage", 23 | "build": "babel src --out-dir build", 24 | "clean": "rm -rf build", 25 | "lint": "eslint src test" 26 | }, 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "babel-cli": "^6.2.0", 30 | "babel-core": "^6.1.21", 31 | "babel-eslint": "^4.1.5", 32 | "babel-loader": "^6.2.0", 33 | "babel-preset-es2015": "^6.1.18", 34 | "babel-preset-stage-0": "^6.1.18", 35 | "chai": "^3.4.1", 36 | "chai-spies": "^0.7.1", 37 | "eslint": "^1.9.0", 38 | "eslint-config-airbnb": "^1.0.0", 39 | "eslint-plugin-react": "^3.10.0", 40 | "isparta": "^4.0.0", 41 | "istanbul-instrumenter-loader": "^0.1.3", 42 | "karma": "^0.13.15", 43 | "karma-coverage": "^0.5.3", 44 | "karma-firefox-launcher": "^0.1.7", 45 | "karma-mocha": "^0.2.1", 46 | "karma-sourcemap-loader": "^0.3.6", 47 | "karma-webpack": "^1.7.0", 48 | "mocha": "^2.3.4", 49 | "webpack": "^1.12.8" 50 | }, 51 | "license": "MIT" 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oui-dom-events [](https://travis-ci.org/oneuijs/oui-dom-events) [](http://badge.fury.io/js/oui-dom-events) 2 | 3 | DOM events manager with namespace support 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install oui-dom-events 9 | ``` 10 | 11 | ## Usage 12 | 13 | ### .on(element, eventName, fn) 14 | Bind `fn` to be called when `eventName` is triggered on `element` 15 | 16 | ```js 17 | import E from 'oui-dom-events' 18 | 19 | let el = document.querySelector('div'); 20 | let fn = function() { /*...*/ } 21 | 22 | E.on(el, 'click', fn); 23 | 24 | // bind with namespace 25 | E.on(el, 'click.slider', fn); 26 | ``` 27 | 28 | ### .off(element, eventName) 29 | Remove all event callbacks bing called when `eventName` is triggered on `element` 30 | 31 | ```js 32 | E.off(el, 'click'); 33 | 34 | // unbind with namespace 35 | E.off(el, 'click.slider'); 36 | ``` 37 | 38 | ### .off(element, eventName, fn) 39 | Remove `fn` from being called when `eventName` is triggered on `element` 40 | 41 | ```js 42 | // this also unbind events with namespace 43 | E.off(el, 'click', fn); 44 | 45 | // only unbind fn with namespace 46 | E.off(el, 'click.slider', fn); 47 | ``` 48 | 49 | ### .delegate(element, selector, eventName, fn) 50 | Delegate `fn` to be called when `eventName` is triggered on all elements that match `selector` under `element` 51 | 52 | ```js 53 | E.delegate(el, '.item', 'click', fn); 54 | 55 | // delegate with namespace 56 | E.delegate(el, '.item', 'click.slider', fn); 57 | 58 | ``` 59 | 60 | ### .undelegate(element, selector, eventName, fn) 61 | Delegate `fn` to be called when `eventName` is triggered on all elements that match `selector` under `element` 62 | 63 | ```js 64 | E.undelegate(el, '.item', 'click', fn); 65 | 66 | // undelegate with namespace 67 | E.undelegate(el, '.item', 'click.slider', fn); 68 | 69 | E.undelegate(el, '.item', 'click.slider'); 70 | 71 | // off also unbind all delegated events 72 | E.off(el, 'click'); 73 | ``` 74 | 75 | ### .trigger(element, eventName, dataObject) 76 | Trigger an `eventName` with `dataObject` on `element` 77 | 78 | ```js 79 | E.trigger(el, 'click', {key1: 'data1'}) 80 | ``` 81 | 82 | ## Caveats 83 | 84 | * `mouseenter` doesn't bubble, use `mouseover` and `mouseout` instead. 85 | 86 | ## Liscense 87 | 88 | MIT 89 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Nov 22 2015 22:10:47 GMT+0800 (CST) 3 | require('babel-core/register'); 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '.', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['mocha'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | './test/**/*.spec.js' 17 | ], 18 | 19 | // list of files to exclude 20 | exclude: [ 21 | ], 22 | 23 | // preprocess matching files before serving them to the browser 24 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 25 | preprocessors: { 26 | 'test/**/*.spec.js': ['webpack', 'sourcemap'] 27 | }, 28 | 29 | // test results reporter to use 30 | // possible values: 'dots', 'progress' 31 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 32 | reporters: ['progress'], 33 | 34 | coverageReporter: { 35 | reporters: [ 36 | {type: 'text'}, 37 | {type: 'html', dir: 'coverage'}, 38 | ] 39 | }, 40 | 41 | webpackMiddleware: { 42 | stats: 'minimal' 43 | }, 44 | 45 | webpack: { 46 | cache: true, 47 | devtool: 'inline-source-map', 48 | module: { 49 | loaders: [{ 50 | test: /\.jsx?$/, 51 | loader: 'babel-loader', 52 | exclude: /node_modules/ 53 | }], 54 | postLoaders: [{ 55 | test: /\.js/, 56 | exclude: /(test|node_modules)/, 57 | loader: 'istanbul-instrumenter' 58 | }], 59 | }, 60 | resolve: { 61 | extensions: ['', '.js', '.jsx'] 62 | } 63 | }, 64 | 65 | // web server port 66 | port: 9876, 67 | 68 | // enable / disable colors in the output (reporters and logs) 69 | colors: true, 70 | 71 | // level of logging 72 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 73 | logLevel: config.LOG_INFO, 74 | 75 | // enable / disable watching file and executing tests whenever any file changes 76 | autoWatch: true, 77 | 78 | // start these browsers 79 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 80 | browsers: ['Firefox'], 81 | 82 | // Continuous Integration mode 83 | // if true, Karma captures browsers, runs the tests and exits 84 | // singleRun: false, 85 | 86 | // Concurrency level 87 | // how many browser should be started simultanous 88 | // concurrency: Infinity, 89 | 90 | // plugins: ['karma-phantomjs-launcher', 'karma-sourcemap-loader', 'karma-webpack'] 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /test/triggerEvent.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | /** 3 | * @see https://github.com/adamsanderson/trigger-event 4 | */ 5 | module.exports = trigger; 6 | 7 | /** 8 | Event type mappings. 9 | This is not an exhaustive list. 10 | */ 11 | var eventTypes = { 12 | load: 'HTMLEvents', 13 | unload: 'HTMLEvents', 14 | abort: 'HTMLEvents', 15 | error: 'HTMLEvents', 16 | select: 'HTMLEvents', 17 | change: 'HTMLEvents', 18 | submit: 'HTMLEvents', 19 | reset: 'HTMLEvents', 20 | focus: 'HTMLEvents', 21 | blur: 'HTMLEvents', 22 | resize: 'HTMLEvents', 23 | scroll: 'HTMLEvents', 24 | input: 'HTMLEvents', 25 | 26 | keyup: 'KeyboardEvent', 27 | keydown: 'KeyboardEvent', 28 | 29 | click: 'MouseEvents', 30 | dblclick: 'MouseEvents', 31 | mousedown: 'MouseEvents', 32 | mouseup: 'MouseEvents', 33 | mouseover: 'MouseEvents', 34 | mousemove: 'MouseEvents', 35 | mouseout: 'MouseEvents', 36 | contextmenu: 'MouseEvents' 37 | }; 38 | 39 | // Default event properties: 40 | var defaults = { 41 | clientX: 0, 42 | clientY: 0, 43 | button: 0, 44 | ctrlKey: false, 45 | altKey: false, 46 | shiftKey: false, 47 | metaKey: false, 48 | bubbles: true, 49 | cancelable: true, 50 | view: document.defaultView, 51 | key: '', 52 | location: 0, 53 | modifiers: '', 54 | repeat: 0, 55 | locale: '' 56 | }; 57 | 58 | /** 59 | * Trigger a DOM event. 60 | * 61 | * trigger(document.body, "click", {clientX: 10, clientY: 35}); 62 | * 63 | * Where sensible, sane defaults will be filled in. See the list of event 64 | * types for supported events. 65 | * 66 | * Loosely based on: 67 | * https://github.com/kangax/protolicious/blob/master/event.simulate.js 68 | */ 69 | function trigger(el, name, options){ 70 | var event, type; 71 | 72 | options = options || {}; 73 | for (var attr in defaults) { 74 | if (!options.hasOwnProperty(attr)) { 75 | options[attr] = defaults[attr]; 76 | } 77 | } 78 | 79 | if (document.createEvent) { 80 | // Standard Event 81 | type = eventTypes[name] || 'CustomEvent'; 82 | event = document.createEvent(type); 83 | initializers[type](el, name, event, options); 84 | el.dispatchEvent(event); 85 | } else { 86 | // IE Event 87 | event = document.createEventObject(); 88 | for (var key in options){ 89 | event[key] = options[key]; 90 | } 91 | el.fireEvent('on' + name, event); 92 | } 93 | } 94 | 95 | var initializers = { 96 | HTMLEvents: function(el, name, event, o){ 97 | return event.initEvent(name, o.bubbles, o.cancelable); 98 | }, 99 | 100 | KeyboardEvent: function(el, name, event, o){ 101 | // Use a blank key if not defined and initialize the charCode 102 | var key = ('key' in o) ? o.key : ""; 103 | var charCode; 104 | var modifiers; 105 | 106 | // 0 is the default location 107 | var location = ('location' in o) ? o.location : 0; 108 | 109 | if (event.initKeyboardEvent) { 110 | // Chrome and IE9+ uses initKeyboardEvent 111 | if (! 'modifiers' in o) { 112 | modifiers = []; 113 | if (o.ctrlKey) modifiers.push("Ctrl"); 114 | if (o.altKey) modifiers.push("Alt"); 115 | if (o.ctrlKey && o.altKey) modifiers.push("AltGraph"); 116 | if (o.shiftKey) modifiers.push("Shift"); 117 | if (o.metaKey) modifiers.push("Meta"); 118 | modifiers = modifiers.join(" "); 119 | } else { 120 | modifiers = o.modifiers; 121 | } 122 | 123 | return event.initKeyboardEvent( 124 | name, o.bubbles, o.cancelable, o.view, 125 | key, location, modifiers, o.repeat, o.locale 126 | ); 127 | } else { 128 | // Mozilla uses initKeyEvent 129 | charCode = ('charCode' in o) ? o.charCode : key.charCodeAt(0) || 0; 130 | return event.initKeyEvent( 131 | name, o.bubbles, o.cancelable, o.view, 132 | o.ctrlKey, o.altKey, o.shiftKey, 133 | o.metaKey, charCode, key 134 | ); 135 | } 136 | }, 137 | 138 | MouseEvents: function(el, name, event, o){ 139 | var screenX = ('screenX' in o) ? o.screenX : o.clientX; 140 | var screenY = ('screenY' in o) ? o.screenY : o.clientY; 141 | var clicks; 142 | var button; 143 | 144 | if ('detail' in o) { 145 | clicks = o.detail; 146 | } else if (name === 'dblclick') { 147 | clicks = 2; 148 | } else { 149 | clicks = 1; 150 | } 151 | 152 | // Default context menu to be a right click 153 | if (name === 'contextmenu') { 154 | button = button = o.button || 2; 155 | } 156 | 157 | return event.initMouseEvent(name, o.bubbles, o.cancelable, o.view, 158 | clicks, screenX, screenY, o.clientX, o.clientY, 159 | o.ctrlKey, o.altKey, o.shiftKey, o.metaKey, button, el); 160 | }, 161 | 162 | CustomEvent: function(el, name, event, o){ 163 | return event.initCustomEvent(name, o.bubbles, o.cancelable, o.detail); 164 | } 165 | }; 166 | /*eslint-enable */ 167 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // IE10+ Support 2 | // inspired by zepto event https://github.com/madrobby/zepto/blob/master/src/event.js 3 | 4 | const handlers = {}; 5 | 6 | const specialEvents = {}; 7 | specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'; 8 | 9 | // every element and callback function will have an unique dtId 10 | let _dtId = 1; 11 | 12 | /** 13 | * Get dtId of Element or callback function 14 | * @param {Object|Function} obj Element or callback function 15 | * @return {Number} unique dtId 16 | */ 17 | function getDtId(obj) { 18 | return obj._dtId || (obj._dtId = _dtId++); 19 | } 20 | 21 | /** 22 | * Get event object of event string, the first `.` is used to split event and namespace 23 | * 24 | * @param {String} event Event type string with namespace or not 25 | * @return {Object} An Object with `e` and `ns` key 26 | */ 27 | function parse(event) { 28 | const dotIndex = event.indexOf('.'); 29 | if (dotIndex > 0) { 30 | return { 31 | e: event.substring(0, event.indexOf('.')), 32 | ns: event.substring(dotIndex + 1, event.length) 33 | }; 34 | } 35 | 36 | return { e: event }; 37 | } 38 | 39 | /** 40 | * Find matched event handlers 41 | * @param {Element} el the element to find 42 | * @param {String} selector Used by event delegation, null if not 43 | * @param {String} event Event string may with namespace 44 | * @param {Function} callback the callback to find, optional 45 | * @return {Array} Array of handlers bind to el 46 | */ 47 | function findHandlers(el, selector, event, callback) { 48 | event = parse(event); 49 | return (handlers[getDtId(el)] || []).filter(handler => { 50 | return handler 51 | && (!event.e || handler.e === event.e) 52 | && (!event.ns || handler.ns === event.ns) 53 | && (!callback || handler.callback === callback) 54 | && (!selector || handler.selector === selector); 55 | }); 56 | } 57 | 58 | function removeEvent(el, selector, event, callback) { 59 | const eventName = parse(event).e; 60 | 61 | if (!el._dtId) return false; 62 | const elHandlers = handlers[getDtId(el)]; 63 | const matchedHandlers = findHandlers(el, selector, event, callback); 64 | matchedHandlers.forEach(handler => { 65 | if (el.removeEventListener) { 66 | el.removeEventListener(eventName, handler.delegator || handler.callback); 67 | } else if (el.detachEvent) { 68 | el.detachEvent('on' + eventName, handler.delegator || handler.callback); 69 | } 70 | elHandlers.splice(elHandlers.indexOf(handler), 1); 71 | }); 72 | } 73 | 74 | // delegator 只用于 delegate 时有用。 75 | function bindEvent(el, selector, event, callback, delegator) { 76 | const eventName = parse(event).e; 77 | const ns = parse(event).ns; 78 | 79 | if (el.addEventListener) { 80 | el.addEventListener(eventName, delegator || callback, false); 81 | } else if (el.attachEvent) { 82 | el.attachEvent('on' + eventName, delegator || callback); 83 | } 84 | 85 | // push events to handlers 86 | const id = getDtId(el); 87 | const elHandlers = (handlers[id] || (handlers[id] = [])); 88 | elHandlers.push({ 89 | delegator: delegator, 90 | callback: callback, 91 | e: eventName, 92 | ns: ns, 93 | selector: selector 94 | }); 95 | } 96 | 97 | const Events = { 98 | /** 99 | * Register a callback 100 | * 101 | * @param {Element} el the element to bind event to 102 | * @param {String} eventType event type, can with namesapce 103 | * @param {Function} callback callback to invoke 104 | * @return {Null} return null 105 | */ 106 | on(el, eventType, callback) { 107 | bindEvent(el, null, eventType, callback); 108 | }, 109 | 110 | /** 111 | * Unregister a callback 112 | * 113 | * @param {Element} el the element to bind event to 114 | * @param {String} eventType event type, can with namesapce 115 | * @param {Function} callback optional, callback to invoke 116 | * @return {Null} return null 117 | */ 118 | off(el, eventType, callback) { 119 | // find callbacks 120 | removeEvent(el, null, eventType, callback); 121 | }, 122 | 123 | /** 124 | * Register a callback that will execute exactly once 125 | * 126 | * @param {Element} el the element to bind event to 127 | * @param {String} eventType event type, can with namesapce 128 | * @param {Function} callback callback to invoke 129 | * @return {Null} return null 130 | */ 131 | once(el, eventType, callback) { 132 | const recursiveFunction = e => { 133 | Events.off(e.currentTarget, e.type, recursiveFunction); 134 | return callback(e); 135 | }; 136 | 137 | this.on(el, eventType, recursiveFunction); 138 | }, 139 | 140 | // Delegate a callback to selector under el 141 | delegate(el, selector, eventType, callback) { 142 | // bind event to el. and check if selector match 143 | const delegator = function(e) { 144 | const els = el.querySelectorAll(selector); 145 | let matched = false; 146 | for (let i = 0; i < els.length; i++) { 147 | const _el = els[i]; 148 | if (_el === e.target || _el.contains(e.target)) { 149 | matched = _el; 150 | break; 151 | } 152 | } 153 | if (matched) { 154 | callback.apply(matched, [].slice.call(arguments)); 155 | } 156 | }; 157 | 158 | bindEvent(el, selector, eventType, callback, delegator); 159 | }, 160 | 161 | // Undelegate a callback to selector under el 162 | undelegate(el, selector, eventType, callback) { 163 | removeEvent(el, selector, eventType, callback); 164 | }, 165 | 166 | // Dispatch an event with props to el 167 | trigger(el, eventType, props) { 168 | const event = document.createEvent(specialEvents[eventType] || 'Events'); 169 | let bubbles = true; 170 | if (props) { 171 | for (const name in props) { 172 | if ({}.hasOwnProperty.call(props, name)) { 173 | (name === 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]); 174 | } 175 | } 176 | } 177 | event.initEvent(eventType, bubbles, true); 178 | el.dispatchEvent(event); 179 | } 180 | }; 181 | 182 | export default Events; 183 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import spies from 'chai-spies'; 2 | import chai, { expect } from 'chai'; 3 | import triggerEvent from './triggerEvent.js'; 4 | import Events from '../src/index.js'; 5 | 6 | chai.use(spies); 7 | 8 | function injectHTML() { 9 | document.body.innerHTML = ` 10 |