504 lines
21 KiB
JavaScript
504 lines
21 KiB
JavaScript
/*!
|
|
* bean.js - copyright Jacob Thornton 2011
|
|
* https://github.com/fat/bean
|
|
* MIT License
|
|
* special thanks to:
|
|
* dean edwards: http://dean.edwards.name/
|
|
* dperini: https://github.com/dperini/nwevents
|
|
* the entire mootools team: github.com/mootools/mootools-core
|
|
*/
|
|
/*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);
|
|
else context[name] = definition(name, context);
|
|
}('bean', this, function (name, context) {
|
|
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
|
|
, mouseTypeRegex = /click|mouse|menu|drag|drop/i
|
|
, touchTypeRegex = /^touch|^gesture/i
|
|
, ONE = { one: 1 } // singleton for quick matching making add() do one()
|
|
|
|
, nativeEvents = (function (hash, events, i) {
|
|
for (i = 0; i < events.length; i++)
|
|
hash[events[i]] = 1
|
|
return hash
|
|
})({}, (
|
|
'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
|
|
'mousewheel DOMMouseScroll ' + // mouse wheel
|
|
'mouseover mouseout mousemove selectstart selectend ' + // mouse movement
|
|
'keydown keypress keyup ' + // keyboard
|
|
'orientationchange ' + // mobile
|
|
'focus blur change reset select submit ' + // form elements
|
|
'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window
|
|
'error abort scroll ' + // misc
|
|
(W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event
|
|
// that doesn't actually exist, so make sure we only do these on newer browsers
|
|
'show ' + // mouse buttons
|
|
'input invalid ' + // form elements
|
|
'touchstart touchmove touchend touchcancel ' + // touch
|
|
'gesturestart gesturechange gestureend ' + // gesture
|
|
'message readystatechange pageshow pagehide popstate ' + // window
|
|
'hashchange offline online ' + // window
|
|
'afterprint beforeprint ' + // printing
|
|
'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd
|
|
'loadstart progress suspend emptied stalled loadmetadata ' + // media
|
|
'loadeddata canplay canplaythrough playing waiting seeking ' + // media
|
|
'seeked ended durationchange timeupdate play pause ratechange ' + // media
|
|
'volumechange cuechange ' + // media
|
|
'checking noupdate downloading cached updateready obsolete ' + // appcache
|
|
'' : '')
|
|
).split(' ')
|
|
)
|
|
|
|
, customEvents = (function () {
|
|
function isDescendant(parent, node) {
|
|
while ((node = node.parentNode) !== null) {
|
|
if (node === parent) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
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(' '))
|
|
, touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' '))
|
|
, preventDefault = 'preventDefault'
|
|
, createPreventDefault = function (event) {
|
|
return function () {
|
|
if (event[preventDefault])
|
|
event[preventDefault]()
|
|
else
|
|
event.returnValue = false
|
|
}
|
|
}
|
|
, stopPropagation = 'stopPropagation'
|
|
, createStopPropagation = function (event) {
|
|
return function () {
|
|
if (event[stopPropagation])
|
|
event[stopPropagation]()
|
|
else
|
|
event.cancelBubble = true
|
|
}
|
|
}
|
|
, createStop = function (synEvent) {
|
|
return function () {
|
|
synEvent[preventDefault]()
|
|
synEvent[stopPropagation]()
|
|
synEvent.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(result)
|
|
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 (mouseTypeRegex.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']
|
|
} else if (touchTypeRegex.test(type)) {
|
|
props = touchProps
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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]
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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. uses '$' as a prefix for the keys for safety
|
|
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 (t.charAt(0) === '$')
|
|
forAll(element, t.substr(1), 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)
|
|
if (list.length === 0)
|
|
delete map['$' + entry.type]
|
|
return false
|
|
})
|
|
}
|
|
|
|
// dump all entries, used for onunload
|
|
, entries = function () {
|
|
var t, entries = []
|
|
for (t in map) {
|
|
if (t.charAt(0) === '$')
|
|
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)
|
|
}
|
|
|
|
, 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))
|
|
}
|
|
}
|
|
|
|
, 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))
|
|
}
|
|
}
|
|
}
|
|
|
|
, 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)
|
|
}
|
|
}
|
|
|
|
, 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
, 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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
, 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
|
|
}
|
|
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])
|
|
}
|
|
}
|
|
return element
|
|
}
|
|
|
|
, add = function (element, events, fn, delfn, $) {
|
|
var type, types, i, args
|
|
, originalFn = fn
|
|
, isDel = fn && typeof fn === 'string'
|
|
|
|
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
|
|
}
|
|
|
|
, one = function () {
|
|
return add.apply(ONE, arguments)
|
|
}
|
|
|
|
, 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]++
|
|
}
|
|
|
|
, 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
|
|
}
|
|
|
|
, clone = function (element, from, type) {
|
|
var i = 0
|
|
, handlers = registry.get(from, type)
|
|
, l = handlers.length
|
|
|
|
for (;i < l; i++)
|
|
handlers[i].original && add(element, handlers[i].type, handlers[i].original)
|
|
return element
|
|
}
|
|
|
|
, bean = {
|
|
add: add
|
|
, one: one
|
|
, remove: remove
|
|
, clone: clone
|
|
, fire: fire
|
|
, noConflict: function () {
|
|
context[name] = old
|
|
return this
|
|
}
|
|
}
|
|
|
|
if (win[attachEvent]) {
|
|
// 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[detachEvent]('onunload', cleanup)
|
|
win.CollectGarbage && win.CollectGarbage()
|
|
}
|
|
win[attachEvent]('onunload', cleanup)
|
|
}
|
|
|
|
return bean
|
|
});
|