diff --git a/.gitignore b/.gitignore index b512c09..1bd7226 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +*.swp diff --git a/.gitmodules b/.gitmodules index 93b0dce..625ef37 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "support/qwery"] path = support/qwery url = git@github.com:ded/qwery.git +[submodule "support/nwevents"] + path = support/nwevents + url = https://github.com/dperini/nwevents.git diff --git a/README.md b/README.md index 6126f4d..d532bef 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ one() remove() ------ -bean.remove() is how you get rid of listeners once you no longer want them. It's also a good idea to call remove on elements before you remove elements from your dom (this gives bean a chance to clean up some things and prevents memory leaks) +bean.remove() is how you get rid of listeners once you no longer want them. It's also a good idea to call remove on elements before you remove elements from your dom (this gives Bean a chance to clean up some things and prevents memory leaks) ```javascript // remove a single event handlers @@ -108,11 +108,17 @@ bean.remove(element, 'click', handler); // remove all click handlers bean.remove(element, 'click'); +// remove handler for all events +bean.remove(element, handler); + // remove multiple events bean.remove(element, 'mousedown mouseup'); // remove all events bean.remove(element); + +// remove handlers for events using object literal +bean.remove(element, { click: clickHandler, keyup: keyupHandler }) ``` clone() @@ -142,7 +148,7 @@ bean.fire(element, 'mousedown mouseup'); custom events ------------- -Bean uses methods similar to [Dean Edward's event model](http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/) to ensure custom events behave like real events, rather than just callbacks. +Bean uses methods similar to [Dean Edwards' event model](http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/) to ensure custom events behave like real events, rather than just callbacks. For all intents and purposes, you can just think of them as native events, which will bubble up, and everything else you would expect... @@ -163,7 +169,7 @@ use them like regular events: object support -------------- -Good news, everything you can do in bean with an element, you can also do with an object! This is particularly useful for working with classes or plugins. +Good news, everything you can do in Bean with an element, you can also do with an object! This is particularly useful for working with classes or plugins. ```javascript var inst = new Klass(); @@ -193,6 +199,12 @@ bean.add(el, 'click', function (e) { e.stopPropagation(); }); +// a simple shortcut, since you usually want preventDefault() and stopPropagation() at the same time +// (works the same as the previous example) +bean.add(el, 'click', function (e) { + e.stop(); +}); + // DOMContentLoaded bean.add(document, 'DOMContentLoaded', fn); @@ -220,9 +232,9 @@ point your browser at bean/tests/index.html Ender Integration API --------------------- -If you use bean with ender it's API is greatly extended through it's Bridge file. This extension aims to give bean the look and feel of jQuery, but at the tiny size of bean. +If you use Bean with Ender its API is greatly extended through its bridge file. This extension aims to give Bean the look and feel of jQuery, but at the tiny size of Bean. -Here's the run down of the method alias's added... +Here's the run down of the method alias' added... **ADD EVENTS** diff --git a/bean.js b/bean.js index 318fefe..ff835a1 100644 --- a/bean.js +++ b/bean.js @@ -12,7 +12,9 @@ else if (typeof define == 'function' && typeof define.amd == 'object') define(definition); else this[name] = definition(); }('bean', function () { - var win = window, + var context = this, + old = this.bean, + win = window, __uid = 1, registry = {}, collected = {}, @@ -346,4 +348,4 @@ }; return bean; -}); \ No newline at end of file +}); diff --git a/make/build.js b/make/build.js index ef0501c..2133562 100644 --- a/make/build.js +++ b/make/build.js @@ -1,24 +1,31 @@ require('smoosh').config({ - "JAVASCRIPT": { - "DIST_DIR": "./", - "bean": [ - "./src/copyright.js", - "./src/bean.js" - ] - }, - "JSHINT_OPTS": { - "boss": true, - "forin": false, - "curly": false, - "debug": false, - "devel": false, - "evil": false, - "regexp": false, - "undef": false, - "sub": true, - "white": false, - "indent": 2, - "whitespace": true, - "asi": true - } -}).run().build().analyze(); \ No newline at end of file + 'JAVASCRIPT': { + 'DIST_DIR': './' + , 'bean': [ + './src/copyright.js' + , './src/bean.js' + ] + } + , 'JSHINT_OPTS': { + 'boss': true + , 'forin': true + , 'curly': false + , 'debug': true + , 'devel': false + , 'evil': false + , 'regexp': false + , 'undef': true + , 'sub': true + , 'white': false + , 'indent': 2 + , 'whitespace': true + , 'asi': true + , 'trailing': true + , 'latedef': true + , 'laxbreak': true + , 'browser': true + , 'eqeqeq': true + , 'bitwise': false + , 'loopfunc': false + } +}).run().build().analyze() diff --git a/src/bean.js b/src/bean.js index ccf42f0..c5c2a1c 100644 --- a/src/bean.js +++ b/src/bean.js @@ -1,385 +1,468 @@ +/*global module:true, define:true*/ !function (name, context, definition) { - if (typeof module != 'undefined') module.exports = definition(name, context); - else if (typeof define == 'function' && typeof define.amd == 'object') define(definition); + if (typeof module !== 'undefined') module.exports = definition(name, context); + else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); else context[name] = definition(name, context); }('bean', this, function (name, context) { - var win = window, - __uid = 1, - old = context[name], - registry = {}, - collected = {}, - overOut = /over|out/, - namespace = /[^\.]*(?=\..*)\.|.*/, - stripName = /\..*/, - addEvent = 'addEventListener', - attachEvent = 'attachEvent', - removeEvent = 'removeEventListener', - detachEvent = 'detachEvent', - doc = document || {}, - root = doc.documentElement || {}, - W3C_MODEL = root[addEvent], - eventSupport = W3C_MODEL ? addEvent : attachEvent, - - isDescendant = function (parent, child) { - var node = child.parentNode; - while (node !== null) { - if (node == parent) { - return true; + var win = window + , old = context[name] + , overOut = /over|out/ + , namespaceRegex = /[^\.]*(?=\..*)\.|.*/ + , nameRegex = /\..*/ + , addEvent = 'addEventListener' + , attachEvent = 'attachEvent' + , removeEvent = 'removeEventListener' + , detachEvent = 'detachEvent' + , doc = document || {} + , root = doc.documentElement || {} + , W3C_MODEL = root[addEvent] + , eventSupport = W3C_MODEL ? addEvent : attachEvent + , slice = Array.prototype.slice + , ONE = { one: 1 } // singleton for quick matching making add() do one() + + , nativeEvents = { + click: 1, dblclick: 1, mouseup: 1, mousedown: 1, contextmenu: 1 // mouse buttons + , mousewheel: 1, DOMMouseScroll: 1 // mouse wheel + , mouseover: 1, mouseout: 1, mousemove: 1, selectstart: 1, selectend: 1 // mouse movement + , keydown: 1, keypress: 1, keyup: 1 // keyboard + , orientationchange: 1 // mobile + , touchstart: 1, touchmove: 1, touchend: 1, touchcancel: 1 // touch + , gesturestart: 1, gesturechange: 1, gestureend: 1 // gesture + , focus: 1, blur: 1, change: 1, reset: 1, select: 1, submit: 1 // form elements + , load: 1, unload: 1, beforeunload: 1, resize: 1, move: 1, DOMContentLoaded: 1, readystatechange: 1 // window + , error: 1, abort: 1, scroll: 1 // misc } - node = node.parentNode; - } - }, - - retrieveUid = function (obj, uid) { - return (obj.__uid = uid && (uid + '::' + __uid++) || obj.__uid || __uid++); - }, - - retrieveEvents = function (element) { - var uid = retrieveUid(element); - return (registry[uid] = registry[uid] || {}); - }, - - listener = W3C_MODEL ? function (element, type, fn, add) { - element[add ? addEvent : removeEvent](type, fn, false); - } : function (element, type, fn, add, custom) { - if (custom && add && element['_on' + custom] === null) { - element['_on' + custom] = 0; - } - element[add ? attachEvent : detachEvent]('on' + type, fn); - }, - - nativeHandler = function (element, fn, args) { - return function (event) { - event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event); - return fn.apply(element, [event].concat(args)); - }; - }, - - customHandler = function (element, fn, type, condition, args) { - return function (event) { - if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName == '_on' + type || !event) { - event = event ? fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event) : null; - fn.apply(element, Array.prototype.slice.call(arguments, event ? 0 : 1).concat(args)); - } - }; - }, - - targetElement = function (element, isNative) { - return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element; - }, - - addListener = function (element, orgType, fn, args) { - var type = orgType.replace(stripName, ''), - events = retrieveEvents(element), - handlers = events[type] || (events[type] = {}), - originalFn = fn, - uid = retrieveUid(fn, orgType.replace(namespace, '')); - if (handlers[uid]) { - return element; - } - var custom = customEvents[type]; - if (custom) { - fn = custom.condition ? customHandler(element, fn, type, custom.condition) : fn; - type = custom.base || type; - } - var isNative = nativeEvents[type]; - fn = isNative ? nativeHandler(element, fn, args) : customHandler(element, fn, type, false, args); - isNative = W3C_MODEL || isNative; - if (type == 'unload') { - var org = fn; - fn = function () { - removeListener(element, type, fn) && org(); - }; - } - element = targetElement(element, isNative); - element[eventSupport] && listener(element, isNative ? type : 'propertychange', fn, true, !isNative && type); - handlers[uid] = fn; - fn.__uid = uid; - fn.__originalFn = originalFn; - return type == 'unload' ? element : (collected[retrieveUid(element)] = element); - }, - - removeListener = function (element, orgType, handler) { - var uid = element.__uid, names, uids, i, events = retrieveEvents(element), type = orgType.replace(stripName, ''); - - if (!events || !events[type]) { - return element; - } - handler && handler.__one && (handler = handler.__one) - names = orgType.replace(namespace, ''); - uids = names ? names.split('.') : [handler.__uid]; + , customEvents = (function () { + function isDescendant(parent, node) { + while ((node = node.parentNode) !== null) { + if (node === parent) return true + } + return false + } - function destroyHandler(uid) { - handler = events[type][uid]; - if (!handler) { - return; - } - delete events[type][uid]; - if (element[eventSupport]) { - type = customEvents[type] ? customEvents[type].base : type; - var isNative = W3C_MODEL || nativeEvents[type]; - element = targetElement(element, isNative); - listener(element, isNative ? type : 'propertychange', handler, false, !isNative && type); + function check(event) { + var related = event.relatedTarget + if (!related) return related === null + return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related)) + } + + return { + mouseenter: { base: 'mouseover', condition: check } + , mouseleave: { base: 'mouseout', condition: check } + , mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' } + } + })() + + , fixEvent = (function () { + var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ') + , mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' ')) + , keyProps = commonProps.concat('char charCode key keyCode'.split(' ')) + , preventDefault = 'preventDefault' + , createPreventDefault = function (e) { + return function () { + if (e[preventDefault]) + e[preventDefault]() + else + e.returnValue = false + } + } + , stopPropagation = 'stopPropagation' + , createStopPropagation = function (e) { + return function () { + if (e[stopPropagation]) + e[stopPropagation]() + else + e.cancelBubble = true + } + } + , createStop = function (e) { + return function () { + e[preventDefault]() + e[stopPropagation]() + e.stopped = true + } + } + , copyProps = function (event, result, props) { + var i, p + for (i = props.length; i--;) { + p = props[i] + if (!(p in result) && p in event) result[p] = event[p] + } + } + + return function (event, isNative) { + var result = { originalEvent: event, isNative: isNative } + if (!event) + return result + + var props + , type = event.type + , target = event.target || event.srcElement + + result[preventDefault] = createPreventDefault(event) + result[stopPropagation] = createStopPropagation(event) + result.stop = createStop(event) + result.target = target && target.nodeType === 3 ? target.parentNode : target + + if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive + if (type.indexOf('key') !== -1) { + props = keyProps + result.keyCode = event.which || event.keyCode + } else if ((/click|mouse|menu/i).test(type)) { + props = mouseProps + result.rightClick = event.which === 3 || event.button === 2 + result.pos = { x: 0, y: 0 } + if (event.pageX || event.pageY) { + result.clientX = event.pageX + result.clientY = event.pageY + } else if (event.clientX || event.clientY) { + result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft + result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop + } + if (overOut.test(type)) + result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element'] + } + copyProps(event, result, props || commonProps) + } + return result + } + })() + + // if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both + , targetElement = function (element, isNative) { + return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element } - } - destroyHandler(names); //get combos - for (i = uids.length; i--; destroyHandler(uids[i])) {} //get singles + // we use one of these per listener, of any type + , RegEntry = (function () { + function entry(element, type, handler, original, namespaces) { + this.element = element + this.type = type + this.handler = handler + this.original = original + this.namespaces = namespaces + this.custom = customEvents[type] + this.isNative = nativeEvents[type] && element[eventSupport] + this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange' + this.customType = !W3C_MODEL && !this.isNative && type + this.target = targetElement(element, this.isNative) + this.eventSupport = this.target[eventSupport] + } - if (isEmpty(events[type])) { - delete events[type]; - } + entry.prototype = { + // given a list of namespaces, is our entry in any of them? + inNamespaces: function (checkNamespaces) { + var i, j + if (!checkNamespaces) + return true + if (!this.namespaces) + return false + for (i = checkNamespaces.length; i--;) { + for (j = this.namespaces.length; j--;) { + if (checkNamespaces[i] === this.namespaces[j]) + return true + } + } + return false + } - if (isEmpty(registry[uid])) { - delete registry[uid]; - delete collected[uid]; - } + // match by element, original fn (opt), handler fn (opt) + , matches: function (checkElement, checkOriginal, checkHandler) { + return this.element === checkElement && + (!checkOriginal || this.original === checkOriginal) && + (!checkHandler || this.handler === checkHandler) + } + } + + return entry + })() + + , registry = (function () { + // our map stores arrays by event type, just because it's better than storing + // everything in a single array + var map = {} + + // generic functional search of our registry for matching listeners, + // `fn` returns false to break out of the loop + , forAll = function (element, type, original, handler, fn) { + if (!type || type === '*') { + // search the whole registry + for (var t in map) { + if (map.hasOwnProperty(t)) + forAll(element, t, original, handler, fn) + } + } else { + var i = 0, l, list = map[type], all = element === '*' + if (!list) + return + for (l = list.length; i < l; i++) { + if (all || list[i].matches(element, original, handler)) + if (!fn(list[i], list, i, type)) + return + } + } + } + + , has = function (element, type, original) { + // we're not using forAll here simply because it's a bit slower and this + // needs to be fast + var i, list = map[type] + if (list) { + for (i = list.length; i--;) { + if (list[i].matches(element, original, null)) + return true + } + } + return false + } + + , get = function (element, type, original) { + var entries = [] + forAll(element, type, original, null, function (entry) { return entries.push(entry) }) + return entries + } + + , put = function (entry) { + (map[entry.type] || (map[entry.type] = [])).push(entry) + return entry + } + + , del = function (entry) { + forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) { + list.splice(i, 1) + return false + }) + } + + // dump all entries, used for onunload + , entries = function () { + var t, entries = [] + for (t in map) { + if (map.hasOwnProperty(t)) + entries = entries.concat(map[t]) + } + return entries + } + + return { has: has, get: get, put: put, del: del, entries: entries } + })() + + // add and remove listeners to DOM elements + , listener = W3C_MODEL ? function (element, type, fn, add) { + element[add ? addEvent : removeEvent](type, fn, false) + } : function (element, type, fn, add, custom) { + if (custom && add && element['_on' + custom] === null) + element['_on' + custom] = 0 + element[add ? attachEvent : detachEvent]('on' + type, fn) + } - return element; - }, + , nativeHandler = function (element, fn, args) { + return function (event) { + event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true) + return fn.apply(element, [event].concat(args)) + } + } - del = function (selector, fn, $) { - return function (e) { - var array = typeof selector == 'string' ? $(selector, this) : selector; - for (var target = e.target; target && target != this; target = target.parentNode) { - for (var i = array.length; i--;) { - if (array[i] == target) { - return fn.apply(target, arguments); + , customHandler = function (element, fn, type, condition, args, isNative) { + return function (event) { + if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) { + if (event) + event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative) + fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args)) } } } - }; - }, - _add = function (meth, element, events, fn, delfn, $) { - if (typeof events == 'object' && !fn) { - for (var type in events) { - events.hasOwnProperty(type) && _add(meth, element, type, events[type]); + , once = function (rm, element, type, fn, originalFn) { + // wrap the handler in a handler that does a remove as well + return function () { + rm(element, type, originalFn) + fn.apply(this, arguments) + } } - } else { - var isDel = typeof fn == 'string', types = (isDel ? fn : events).split(' '); - fn = isDel ? del(events, delfn, $) : meth == 'one' ? - function(fn) { - var one = function() { - remove(element, events, one) - fn.apply(this, arguments) + + , removeListener = function (element, orgType, handler, namespaces) { + var i, l, entry + , type = (orgType && orgType.replace(nameRegex, '')) + , handlers = registry.get(element, type, handler) + + for (i = 0, l = handlers.length; i < l; i++) { + if (handlers[i].inNamespaces(namespaces)) { + if ((entry = handlers[i]).eventSupport) + listener(entry.target, entry.eventType, entry.handler, false, entry.type) + // TODO: this is problematic, we have a registry.get() and registry.del() that + // both do registry searches so we waste cycles doing this. Needs to be rolled into + // a single registry.forAll(fn) that removes while finding, but the catch is that + // we'll be splicing the arrays that we're iterating over. Needs extra tests to + // make sure we don't screw it up. @rvagg + registry.del(entry) } - return (fn.__one = one) - }(fn) : fn - for (var i = types.length; i--;) { - addListener(element, types[i], fn, Array.prototype.slice.call(arguments, isDel ? 5 : 4)); + } } - } - return element; - }, - - add = function () { - return _add.apply(this, ['add'].concat(Array.prototype.slice.call(arguments, 0))) - }, - - one = function () { - return _add.apply(this, ['one'].concat(Array.prototype.slice.call(arguments, 0))) - }, - - remove = function (element, orgEvents, fn) { - var k, m, type, events, i, - isString = typeof(orgEvents) == 'string', - names = isString && orgEvents.replace(namespace, ''), - rm = removeListener, - attached = retrieveEvents(element); - names = names && names.split('.'); - if (isString && /\s/.test(orgEvents)) { - orgEvents = orgEvents.split(' '); - i = orgEvents.length - 1; - while (remove(element, orgEvents[i]) && i--) {} - return element; - } - events = isString ? orgEvents.replace(stripName, '') : orgEvents; - if (!attached || names || (isString && !attached[events])) { - for (k in attached) { - if (attached.hasOwnProperty(k)) { - for (i in attached[k]) { - for (m = names.length; m--;) { - attached[k].hasOwnProperty(i) && - new RegExp('^' + names[m] + '::\\d*(\\..*)?$').test(i) && - rm(element, [k, i].join('.')); + + , addListener = function (element, orgType, fn, originalFn, args) { + var entry + , type = orgType.replace(nameRegex, '') + , namespaces = orgType.replace(namespaceRegex, '').split('.') + + if (registry.has(element, type, fn)) + return element // no dupe + if (type === 'unload') + fn = once(removeListener, element, type, fn, originalFn) // self clean-up + if (customEvents[type]) { + if (customEvents[type].condition) + fn = customHandler(element, fn, type, customEvents[type].condition, true) + type = customEvents[type].base || type + } + entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces)) + entry.handler = entry.isNative ? + nativeHandler(element, entry.handler, args) : + customHandler(element, entry.handler, type, false, args, false) + if (entry.eventSupport) + listener(entry.target, entry.eventType, entry.handler, true, entry.customType) + } + + , del = function (selector, fn, $) { + return function (e) { + var target, i, array = typeof selector === 'string' ? $(selector, this) : selector + for (target = e.target; target && target !== this; target = target.parentNode) { + for (i = array.length; i--;) { + if (array[i] === target) { + return fn.apply(target, arguments) + } } } } } - return element; - } - if (typeof fn == 'function') { - rm(element, events, fn); - } else if (names) { - rm(element, orgEvents); - } else { - rm = events ? rm : remove; - type = isString && events; - events = events ? (fn || attached[events] || events) : attached; - for (k in events) { - if (events.hasOwnProperty(k)) { - rm(element, type || k, events[k]); - delete events[k]; // remove unused leaf keys + + , remove = function (element, typeSpec, fn) { + var k, m, type, namespaces, i + , rm = removeListener + , isString = typeSpec && typeof typeSpec === 'string' + + if (isString && typeSpec.indexOf(' ') > 0) { + // remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3') + typeSpec = typeSpec.split(' ') + for (i = typeSpec.length; i--;) + remove(element, typeSpec[i], fn) + return element } - } - } - return element; - }, - - fire = function (element, type, args) { - var evt, k, i, m, types = type.split(' '); - for (i = types.length; i--;) { - type = types[i].replace(stripName, ''); - var isNative = nativeEvents[type], - isNamespace = types[i].replace(namespace, ''), - handlers = retrieveEvents(element)[type]; - if (isNamespace) { - isNamespace = isNamespace.split('.'); - for (k = isNamespace.length; k--;) { - for (m in handlers) { - handlers.hasOwnProperty(m) && new RegExp('^' + isNamespace[k] + '::\\d*(\\..*)?$').test(m) && handlers[m].apply(element, [false].concat(args)); + type = isString && typeSpec.replace(nameRegex, '') + if (type && customEvents[type]) + type = customEvents[type].type + if (!typeSpec || isString) { + // remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3) + if (namespaces = isString && typeSpec.replace(namespaceRegex, '')) + namespaces = namespaces.split('.') + rm(element, type, fn, namespaces) + } else if (typeof typeSpec === 'function') { + // remove(el, fn) + rm(element, null, typeSpec) + } else { + // remove(el, { t1: fn1, t2, fn2 }) + for (k in typeSpec) { + if (typeSpec.hasOwnProperty(k)) + remove(element, k, typeSpec[k]) } } - } else if (!args && element[eventSupport]) { - fireListener(isNative, type, element); - } else { - for (k in handlers) { - handlers.hasOwnProperty(k) && handlers[k].apply(element, [false].concat(args)); - } + return element } - } - return element; - }, - - fireListener = W3C_MODEL ? function (isNative, type, element) { - evt = document.createEvent(isNative ? "HTMLEvents" : "UIEvents"); - evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1); - element.dispatchEvent(evt); - } : function (isNative, type, element) { - element = targetElement(element, isNative); - isNative ? element.fireEvent('on' + type, document.createEventObject()) : element['_on' + type]++; - }, - - clone = function (element, from, type) { - var events = retrieveEvents(from), obj, k; - var uid = retrieveUid(element); - obj = type ? events[type] : events; - for (k in obj) { - obj.hasOwnProperty(k) && (type ? add : clone)(element, type || from, type ? obj[k].__originalFn : k); - } - return element; - }, - fixEvent = function (e) { - var result = {}; - if (!e) { - return result; - } - var type = e.type, target = e.target || e.srcElement; - result.preventDefault = fixEvent.preventDefault(e); - result.stopPropagation = fixEvent.stopPropagation(e); - result.target = target && target.nodeType == 3 ? target.parentNode : target; - if (~type.indexOf('key')) { - result.keyCode = e.which || e.keyCode; - } else if ((/click|mouse|menu/i).test(type)) { - result.rightClick = e.which == 3 || e.button == 2; - result.pos = { x: 0, y: 0 }; - if (e.pageX || e.pageY) { - result.clientX = e.pageX; - result.clientY = e.pageY; - } else if (e.clientX || e.clientY) { - result.clientX = e.clientX + doc.body.scrollLeft + root.scrollLeft; - result.clientY = e.clientY + doc.body.scrollTop + root.scrollTop; - } - overOut.test(type) && (result.relatedTarget = e.relatedTarget || e[(type == 'mouseover' ? 'from' : 'to') + 'Element']); - } - for (var k in e) { - if (!(k in result)) { - result[k] = e[k]; - } - } - return result; - }, + , add = function (element, events, fn, delfn, $) { + var type, types, i, args + , originalFn = fn + , isDel = fn && typeof fn === 'string' - isEmpty = function (obj) { - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) return false; - } - return true; - } + if (events && !fn && typeof events === 'object') { + for (type in events) { + if (events.hasOwnProperty(type)) + add.apply(this, [ element, type, events[type] ]) + } + } else { + args = arguments.length > 3 ? slice.call(arguments, 3) : [] + types = (isDel ? fn : events).split(' ') + isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1)) + // special case for one() + this === ONE && (fn = once(remove, element, events, fn, originalFn)) + for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args) + } + return element + } - fixEvent.preventDefault = function (e) { - return function () { - if (e.preventDefault) { - e.preventDefault(); + , one = function () { + return add.apply(ONE, arguments) } - else { - e.returnValue = false; + + , fireListener = W3C_MODEL ? function (isNative, type, element) { + var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents') + evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1) + element.dispatchEvent(evt) + } : function (isNative, type, element) { + element = targetElement(element, isNative) + // if not-native then we're using onpropertychange so we just increment a custom property + isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++ } - }; - }; - - fixEvent.stopPropagation = function (e) { - return function () { - if (e.stopPropagation) { - e.stopPropagation(); - } else { - e.cancelBubble = true; + + , fire = function (element, type, args) { + var i, j, l, names, handlers + , types = type.split(' ') + + for (i = types.length; i--;) { + type = types[i].replace(nameRegex, '') + if (names = types[i].replace(namespaceRegex, '')) + names = names.split('.') + if (!names && !args && element[eventSupport]) { + fireListener(nativeEvents[type], type, element) + } else { + // non-native event, either because of a namespace, arguments or a non DOM element + // iterate over all listeners and manually 'fire' + handlers = registry.get(element, type) + args = [false].concat(args) + for (j = 0, l = handlers.length; j < l; j++) { + if (handlers[j].inNamespaces(names)) + handlers[j].handler.apply(element, args) + } + } + } + return element } - }; - }; - - var nativeEvents = { click: 1, dblclick: 1, mouseup: 1, mousedown: 1, contextmenu: 1, //mouse buttons - mousewheel: 1, DOMMouseScroll: 1, //mouse wheel - mouseover: 1, mouseout: 1, mousemove: 1, selectstart: 1, selectend: 1, //mouse movement - keydown: 1, keypress: 1, keyup: 1, //keyboard - orientationchange: 1, // mobile - touchstart: 1, touchmove: 1, touchend: 1, touchcancel: 1, // touch - gesturestart: 1, gesturechange: 1, gestureend: 1, // gesture - focus: 1, blur: 1, change: 1, reset: 1, select: 1, submit: 1, //form elements - load: 1, unload: 1, beforeunload: 1, resize: 1, move: 1, DOMContentLoaded: 1, readystatechange: 1, //window - error: 1, abort: 1, scroll: 1 }; //misc - - function check(event) { - var related = event.relatedTarget; - if (!related) { - return related === null; - } - return (related != this && related.prefix != 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related)); - } - var customEvents = { - mouseenter: { base: 'mouseover', condition: check }, - mouseleave: { base: 'mouseout', condition: check }, - mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' } - }; + , clone = function (element, from, type) { + var i = 0 + , handlers = registry.get(from, type) + , l = handlers.length - var bean = { add: add, one: one, remove: remove, clone: clone, fire: fire }; + for (;i < l; i++) + handlers[i].original && add(element, handlers[i].type, handlers[i].original) + return element + } - var clean = function (el) { - var uid = remove(el).__uid; - if (uid) { - delete collected[uid]; - delete registry[uid]; - } - }; + , bean = { + add: add + , one: one + , remove: remove + , clone: clone + , fire: fire + , noConflict: function () { + context[name] = old + return this + } + } if (win[attachEvent]) { - add(win, 'unload', function () { - for (var k in collected) { - collected.hasOwnProperty(k) && clean(collected[k]); + // for IE, clean up on unload to avoid leaks + var cleanup = function () { + var i, entries = registry.entries() + for (i in entries) { + if (entries[i].type && entries[i].type !== 'unload') + remove(entries[i].element, entries[i].type) } - win.CollectGarbage && CollectGarbage(); - }); + win[detachEvent]('onunload', cleanup) + win.CollectGarbage && win.CollectGarbage() + } + win[attachEvent]('onunload', cleanup) } - bean.noConflict = function () { - context[name] = old; - return this; - }; - - return bean; - -}); + return bean +}) diff --git a/src/ender.js b/src/ender.js index df025a0..4e08f89 100644 --- a/src/ender.js +++ b/src/ender.js @@ -1,62 +1,60 @@ !function ($) { - var b = require('bean'), - integrate = function (method, type, method2) { - var _args = type ? [type] : []; + var b = require('bean') + , integrate = function (method, type, method2) { + var _args = type ? [type] : [] return function () { for (var args, i = 0, l = this.length; i < l; i++) { - args = [this[i]].concat(_args, Array.prototype.slice.call(arguments, 0)); - args.length == 4 && args.push($); - !arguments.length && method == 'add' && type && (method = 'fire'); - b[method].apply(this, args); + args = [this[i]].concat(_args, Array.prototype.slice.call(arguments, 0)) + args.length == 4 && args.push($) + !arguments.length && method == 'add' && type && (method = 'fire') + b[method].apply(this, args) } - return this; - }; - }; - - var add = integrate('add'), - remove = integrate('remove'), - fire = integrate('fire'); - - var methods = { - - on: add, - addListener: add, - bind: add, - listen: add, - delegate: add, - - one: integrate('one'), - - off: remove, - unbind: remove, - unlisten: remove, - removeListener: remove, - undelegate: remove, - - emit: fire, - trigger: fire, + return this + } + } + , add = integrate('add') + , remove = integrate('remove') + , fire = integrate('fire') + + , methods = { + on: add + , addListener: add + , bind: add + , listen: add + , delegate: add + + , one: integrate('one') + + , off: remove + , unbind: remove + , unlisten: remove + , removeListener: remove + , undelegate: remove + + , emit: fire + , trigger: fire + + , cloneEvents: integrate('clone') + + , hover: function (enter, leave, i) { // i for internal + for (i = this.length; i--;) { + b.add.call(this, this[i], 'mouseenter', enter) + b.add.call(this, this[i], 'mouseleave', leave) + } + return this + } + } - cloneEvents: integrate('clone'), + , shortcuts = [ + 'blur', 'change', 'click', 'dblclick', 'error', 'focus', 'focusin' + , 'focusout', 'keydown', 'keypress', 'keyup', 'load', 'mousedown' + , 'mouseenter', 'mouseleave', 'mouseout', 'mouseover', 'mouseup', 'mousemove' + , 'resize', 'scroll', 'select', 'submit', 'unload' + ] - hover: function (enter, leave, i) { // i for internal - for (i = this.length; i--;) { - b.add.call(this, this[i], 'mouseenter', enter); - b.add.call(this, this[i], 'mouseleave', leave); - } - return this; - } - }; - - var i, shortcuts = [ - 'blur', 'change', 'click', 'dblclick', 'error', 'focus', 'focusin', - 'focusout', 'keydown', 'keypress', 'keyup', 'load', 'mousedown', - 'mouseenter', 'mouseleave', 'mouseout', 'mouseover', 'mouseup', 'mousemove', - 'resize', 'scroll', 'select', 'submit', 'unload' - ]; - - for (i = shortcuts.length; i--;) { - methods[shortcuts[i]] = integrate('add', shortcuts[i]); + for (var i = shortcuts.length; i--;) { + methods[shortcuts[i]] = integrate('add', shortcuts[i]) } - $.ender(methods, true); -}(ender); + $.ender(methods, true) +}(ender) diff --git a/support/nwevents b/support/nwevents new file mode 160000 index 0000000..558e899 --- /dev/null +++ b/support/nwevents @@ -0,0 +1 @@ +Subproject commit 558e8993f30d4f6f82c11ac11ceefa164ef0b70b diff --git a/support/qwery b/support/qwery index 33fe83a..b394aa8 160000 --- a/support/qwery +++ b/support/qwery @@ -1 +1 @@ -Subproject commit 33fe83a73dcd7a084ada3abc97470961fef21ddf +Subproject commit b394aa84e8edd44ab4d1f6d830e0ab92621b7217 diff --git a/tests/benchmark.html b/tests/benchmark.html index 9fb8132..778b51b 100644 --- a/tests/benchmark.html +++ b/tests/benchmark.html @@ -2,164 +2,671 @@ Bean Benchmarks - - -

Running Benchmark Suite

+ + + + + + + + + + +
../bean.js../src/bean.jsNWEventsjQuery
+ + + + + + + + + + + + - \ No newline at end of file + diff --git a/tests/index.html b/tests/index.html index 8a4667a..2147ce0 100644 --- a/tests/index.html +++ b/tests/index.html @@ -2,7 +2,7 @@ - event.js tests + Bean tests @@ -25,7 +25,7 @@ -

Event Tests

+

Bean Tests

    @@ -37,6 +37,9 @@

    Event Tests

    +
    + +
    diff --git a/tests/tests.js b/tests/tests.js index 6525cd0..a8c6ece 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -124,17 +124,17 @@ sink('add', function (test, ok) { bean.add(el, 'foo', function () {ok(true, 'additional custom event listeners trigger event 2')}); }); - test('one: should only trigger handler once', 1, function() { + test('one: should only trigger handler once', 1, function () { var el = document.getElementById('input') - bean.one(el, 'click', function() { ok(true, 'handler called exactly one time') }) + bean.one(el, 'click', function () { ok(true, 'handler called exactly one time') }) Syn.click(el) Syn.click(el) Syn.click(el) }); - test('one: should be removable', 0, function() { + test('one: should be removable', 0, function () { var el = document.getElementById('input') - , handler = function() { ok(false, 'handler shouldn\'t have been called') } + , handler = function () { ok(false, 'handler shouldn\'t have been called') } bean.one(el, 'click', handler) bean.remove(el, 'click', handler) Syn.click(el) @@ -257,6 +257,47 @@ sink('event object', function (test, ok) { bean.fire(el, 'customEvent'); }); + test('event: stop should preventDefault and stopPropagation', 1, function () { + // first feature-test, if we don't support native createEvent then there's + // no point running this test unfortunately + var supported = false + try { + document.createEvent('KeyEvents') + supported = true + } catch (e) { + try { + document.createEvent('TextEvent') + supported = true + } catch (e) { } + } + if (!supported) { + ok(true, 'not running test in this browser, native createEvent() not supported') + return + } + + // we should be able to prevent a keypress and event propagation with stop() + // on the keypress event, checking the parent doesn't receive the keypress + // and then checking the input contents on a keyup, it should be empty. + var txt = document.getElementById('txt') + , parent = document.getElementById('stopper') + , txtHandler = function (event) { + event.stop() + } + , txtCheckHandler = function (event) { + ok(!txt.value.length, 'input is has no text after keypress') + } + , parentHandler = function (event) { + ok(true, 'parent should not receive event') + bean.remove(parent) + } + + txt.value = '' + bean.add(txt, 'keypress', txtHandler) + bean.add(txt, 'keyup', txtCheckHandler) + bean.add(parent, 'keypress', parentHandler) + Syn.key(txt, 'f') + }) + test('event: should have keyCode', 1, function () { var el = document.getElementById('input'); bean.add(el, 'keypress', function (e) { @@ -266,6 +307,83 @@ sink('event object', function (test, ok) { Syn.key(el, 'f'); }); + // the idea here is that we have a whitelist in bean.js for properties to copy over from the original + // event object (if they exist) to the new synthetic one. But, there are a bunch of browser specific + // properties we don't care about. We list those properties here and then we check to see if there are + // any properties in source event objects that aren't being copied to the new event objects that we + // haven't specifically listed as 'ignorable'. This way we should be able to pick up new event properties + // browsers as they're implemented and then make a decision as to whether they should be copied or not + + var commonIgnorables = 'cancelBubble clipboardData defaultPrevented explicitOriginalTarget getPreventDefault initEvent initUIEvent isChar originalTarget preventCapture preventBubble rangeOffset rangeParent returnValue stopImmediatePropagation synthetic'.split(' ') + // stuff from IE8 and below + , oldIEIgnorables = 'recordset altLeft repeat reason data behaviorCookie source contentOverflow behaviorPart url shiftLeft dataFld qualifier wheelDelta bookmarks srcFilter nextPage srcUrn origin boundElements propertyName ctrlLeft'.split(' ') + , clickIgnorables = commonIgnorables.concat(oldIEIgnorables).concat('charCode defaultPrevented initMouseEvent keyCode layerX layerY initNSMouseEvent x y'.split(' ')) + , oldIEKeyIgnorables = 'fromElement toElement dataTransfer button x y screenX screenY clientX clientY offsetX offsetY'.split(' ') + , keyIgnorables = commonIgnorables.concat(oldIEIgnorables).concat(oldIEKeyIgnorables).concat('initKeyEvent layerX layerY pageX pageY'.split(' ')) + + , getEventObject = function (evType, elType, trigger, callback) { + var el = document.getElementById('input') + , handler = function (e) { + bean.remove(el) + callback(e) + } + bean.add(el, evType, handler) + trigger(el) + } + + , contains = function (arr, e) { + for (var i = arr.length; i--;) { + if (arr[i] === e) return true + } + return false + } + + , verifyEventObject = function (event, ignorables) { + var p, orig = event.originalEvent + for (var p in orig) { + if ( + !event.hasOwnProperty(p) + && !contains(ignorables, p) + && !/^[A-Z_\d]+$/.test(p) // STUFF_LIKE_THIS + && !/^moz[A-Z]/.test(p) // Mozilla prefixed properties + ) + ok(true, 'additional, uncopied property: "' + p + '"') + } + } + + var testMouseEvent = function (type, syn) { + getEventObject( + type + , 'button' + , function (el) { Syn[syn || type](el) } + , function (event) { + ok(!!event && !!event.originalEvent && event.type === type, 'got event object') + verifyEventObject(event, clickIgnorables) + } + ) + } + + test('click: has correct properties', 1, function () { testMouseEvent('click') }) + test('dblclick: has correct properties', 1, function () { testMouseEvent('dblclick') }) + test('mousedown: has correct properties', 1, function () { testMouseEvent('mousedown', 'click') }) + test('mouseup: has correct properties', 1, function () { testMouseEvent('mouseup', 'click') }) + + var testKeyEvent = function (type) { + getEventObject( + type + , 'input' + , function (el) { Syn.key(el, 'f') } + , function (event) { + ok(!!event && !!event.originalEvent && event.type === type, 'got event object') + verifyEventObject(event, keyIgnorables) + } + ) + } + + test('keyup: has correct properties', 1, function () { testKeyEvent('keyup') }) + test('keydown: has correct properties', 1, function () { testKeyEvent('keydown') }) + test('keypress: has correct properties', 1, function () { testKeyEvent('keypress') }) + }) sink('remove', function (test, ok) { @@ -291,22 +409,27 @@ sink('remove', function (test, ok) { Syn.click(el) }); - test('remove: should be able to remove mulitple events with an object literal', 1, function () { - var el = document.getElementById('input'), - handler1 = function () { - ok(true, 'remove mulitple events with an object literal1'); + test('remove: should be able to remove mulitple events with an object literal', 2, function () { + var el = document.getElementById('input') + , handler1 = function () { + ok(true, 'remove mulitple events with an object literal (1)') bean.remove(el, { - click: handler1, - keydown: handler2 - }); - Syn.click(el).key('j'); - }, - handler2 = function () { - ok(true, 'remove mulitple events with an object literal2'); - }; - bean.add(el, 'click', handler1); - bean.add(el, 'keydown', handler2); - Syn.click(el); + click: handler1 + , keydown: handler2 + }) + Syn.click(el).key('j') + } + , handler2 = function () { + ok(true, 'remove mulitple events with an object literal (1)') + } + , handler3 = function () { // should be called once + bean.remove(el, 'keydown', handler3) + ok(true, 'remove mulitple events with an object literal (3)') + } + bean.add(el, 'click', handler1) + bean.add(el, 'keydown', handler2) + bean.add(el, 'keydown', handler3) + Syn.click(el) }); test('remove: should be able to remove all events of a specific type', 2, function () { @@ -356,24 +479,72 @@ sink('remove', function (test, ok) { Syn.click(el); }); - test('remove: should be able to remove all events of a certain namespace', 1, function () { - var el = document.getElementById('input'), - handler1 = function () { - ok(true, 'remove all events 1'); - bean.remove(el, '.foo'); - Syn.click(el).key('j'); - }, - handler2 = function () { - ok(true, 'remove all events 2'); - }; - bean.add(el, 'click.foo', handler1); - bean.add(el, 'keydown.foo', handler2); - Syn.click(el); - }); + test('remove: should only remove events of specified type', 4, function () { + // testing that bean.remove(el, type) removes *only* of that type and no others + var el = document.getElementById('input') + , handler1 = function (e) { + ok(true, 'handled ' + e.type + ' event (1)') + } + , handler2 = function (e) { + ok(true, 'handled ' + e.type + ' event (2)') + bean.remove(el, e.type) + } + + bean.add(el, 'click', handler1) + bean.add(el, 'keyup', handler1) + bean.add(el, 'click', handler2) + bean.add(el, 'keyup', handler2) + Syn.click(el) + Syn.key(el, 'f') + Syn.click(el) + Syn.key(el, 'f') + }) + + test('remove: should only remove events for specified handler', 2, function () { + // testing that bean.remove(el, fn) removes *only* that handler and no others + var el = document.getElementById('input') + , c = 0 + , handler1 = function (e) { + ok(true, 'handled ' + e.type + ' event (1)') + } + , handler2 = function (e) { + ok(true, 'handled ' + e.type + ' event (2)') + ++c == 2 && bean.remove(el) + } + + bean.add(el, 'click', handler1) + bean.add(el, 'keyup', handler1) + bean.add(el, 'click', handler2) + bean.add(el, 'keyup', handler2) + bean.remove(el, handler1) + Syn.click(el) + Syn.key(el, 'f') + }) + + test('remove: should be able to remove all events of a certain namespace', 3, function () { + var el = document.getElementById('input') + , handler1 = function () { + ok(true, 'remove all events (1)') + bean.remove(el, '.foo') + Syn.click(el).key('j') + } + , handler2 = function () { + ok(true, 'remove all events (2)') + } + , handler3 = function () { // should be called twice + bean.remove(el, '.foo') + ok(true, 'remove all events (3)') + } + bean.remove(el) + bean.add(el, 'click.foo', handler1) + bean.add(el, 'keydown.foo', handler2) + bean.add(el, 'click.bar', handler3) + Syn.click(el) + }) }) -sink('clone', function (test, ok, before) { +sink('clone', function (test, ok, before, after) { var el1 = document.getElementById('input'); var el2 = document.getElementById('input2'); @@ -383,6 +554,11 @@ sink('clone', function (test, ok, before) { bean.remove(el2); }) + after(function () { + bean.remove(el1); + bean.remove(el2); + }) + test('clone: should be able to clone events of a specific type from one element to another', 2, function () { bean.add(el2, 'click', function () {ok(true, 'clones events of a specific type from one element to another 1')}); bean.add(el2, 'click', function () { @@ -450,6 +626,20 @@ sink('delegation', function (test, ok) { Syn.click(el3); Syn.click(el4); }); + + test('delegate: should be able to remove delegated handler', 1, function () { + var el1 = document.getElementById('foo'); + var el2 = document.getElementById('bar'); + bean.remove(el1); + bean.remove(el2); + var fn = function () { + ok(true, 'degegated event triggered once'); + bean.remove(el1, 'click', fn); + } + bean.add(el1, '.bar', 'click', fn, qwery); + Syn.click(el2); + Syn.click(el2); + }); }) sink('namespaces', function (test, ok) { @@ -484,8 +674,8 @@ sink('namespaces', function (test, ok) { test('namespace: should be able to target namespaced event handlers with fire', 1, function () { var el1 = document.getElementById('foo'); bean.remove(el1); - bean.add(el1, 'click.fat', function () {ok(true, 'targets namespaced event handlers with fire')}); - bean.add(el1, 'click', function () {ok(true, 'targets namespaced event handlers with fire')}); + bean.add(el1, 'click.fat', function () {ok(true, 'targets namespaced event handlers with fire (namespaced)')}); + bean.add(el1, 'click', function () {ok(true, 'targets namespaced event handlers with fire (plain)')}); bean.fire(el1, 'click.fat'); }); @@ -519,4 +709,64 @@ sink('namespaces', function (test, ok) { }); -window.onload = start; +sink('custom types', function (test, ok) { + + test('custom types: mouseenter/mouseleave should wrap simple mouseover/mouseout', 2, function () { + var html = document.documentElement + , foo = document.getElementById('foo') + , bar = document.getElementById('bar') + , bang = document.getElementById('bang') + , me = function (e) { ok(true, 'triggers single mouseenter event') } + , ml = function (e) { ok(true, 'triggers single mouseleave event') } + bean.remove(foo) + bean.add(foo, 'mouseenter', me) + bean.add(foo, 'mouseleave', ml) + // relatedTarget is where the mouse came from for mouseover and where it's going to in mouseout + Syn.trigger('mouseover', { relatedTarget: html }, foo) + Syn.trigger('mouseover', { relatedTarget: foo }, bar) + Syn.trigger('mouseover', { relatedTarget: bar }, bang) + Syn.trigger('mouseout', { relatedTarget: bar }, bang) + Syn.trigger('mouseout', { relatedTarget: foo }, bar) + Syn.trigger('mouseout', { relatedTarget: html }, foo) + bean.remove(foo) + }) + + function testRemove(removeFn) { + var html = document.documentElement + , foo = document.getElementById('foo') + , me = function (e) { ok(true, 'triggers single mouseenter event') } + , ml = function (e) { ok(true, 'triggers single mouseleave event') } + + bean.remove(foo) + bean.add(foo, 'mouseenter', me) + bean.add(foo, 'mouseleave', ml) + Syn.trigger('mouseover', { relatedTarget: html }, foo) + Syn.trigger('mouseout', { relatedTarget: html }, foo) + removeFn(foo, me, ml) + Syn.trigger('mouseover', { relatedTarget: html }, foo) + Syn.trigger('mouseout', { relatedTarget: html }, foo) + } + + test('custom types: custom events should be removable', 2, function () { + testRemove(function (foo, me, ml) { + bean.remove(foo) + }) + }) + + test('custom types: custom events should be removable by type', 2, function () { + testRemove(function (foo, me, ml) { + bean.remove(foo, 'mouseenter') + bean.remove(foo, 'mouseleave') + }) + }) + + test('custom types: custom events should be removable by type+handler', 2, function () { + testRemove(function (foo, me, ml) { + bean.remove(foo, 'mouseenter', me) + bean.remove(foo, 'mouseleave', ml) + }) + }) + +}) + +window.onload = start