minow
Version:
A drop in replacement for jquery based off enders core libs
744 lines (678 loc) • 34 kB
JavaScript
/*!
* Bean - copyright (c) Jacob Thornton 2011-2012
* https://github.com/fat/bean
* MIT license
*/
!(function(name, context, definition) {
if (typeof module != 'undefined' && module.exports) 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],
namespaceRegex = /[^\.]*(?=\..*)\.|.*/,
nameRegex = /\..*/,
addEvent = 'addEventListener',
removeEvent = 'removeEventListener',
doc = document || {}, root = doc.documentElement || {}, W3C_MODEL = root[addEvent],
eventSupport = W3C_MODEL ? addEvent : 'attachEvent',
ONE = {} // singleton for quick matching making add() do one()
, slice = Array.prototype.slice,
str2arr = function(s, d) {
return s.split(d || ' ')
}, isString = function(o) {
return typeof o == 'string'
}, isFunction = function(o) {
return typeof o == 'function'
}
// events that we consider to be 'native', anything not in this list will
// be treated as a custom event
, standardNativeEvents =
'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
'mousewheel mousemultiwheel 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 ' + // window
'readystatechange message ' + // window
'error abort scroll ' // misc
// 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
,
w3cNativeEvents =
'show ' + // mouse buttons
'input invalid ' + // form elements
'touchstart touchmove touchend touchcancel ' + // touch
'gesturestart gesturechange gestureend ' + // gesture
'textinput' + // TextEvent
'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
// convert to a hash for quick lookups
,
nativeEvents = (function(hash, events, i) {
for (i = 0; i < events.length; i++) events[i] && (hash[events[i]] = 1)
return hash
}({}, str2arr(standardNativeEvents + (W3C_MODEL ? w3cNativeEvents : ''))))
// custom events are events that we *fake*, they are not provided natively but
// we can use native events to generate them
,
customEvents = (function() {
var isAncestor = 'compareDocumentPosition' in root ? function(element, container) {
return container.compareDocumentPosition && (container.compareDocumentPosition(element) & 16) === 16
} : 'contains' in root ? function(element, container) {
container = container.nodeType === 9 || container === window ? root : container
return container !== element && container.contains(element)
} : function(element, container) {
while (element = element.parentNode)
if (element === container) return 1
return 0
}, check = function(event) {
var related = event.relatedTarget
return !related ? related == null : (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isAncestor(related, this))
}
return {
mouseenter: {
base: 'mouseover',
condition: check
},
mouseleave: {
base: 'mouseout',
condition: check
},
mousewheel: {
base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel'
}
}
}())
// we provide a consistent Event object across browsers by taking the actual DOM
// event object and generating a new one from its properties.
,
Event = (function() {
// a whitelist of properties (for different event types) tells us what to check for and copy
var commonProps = str2arr('altKey attrChange attrName bubbles cancelable ctrlKey currentTarget ' +
'detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey ' +
'srcElement target timeStamp type view which propertyName'),
mouseProps = commonProps.concat(str2arr('button buttons clientX clientY dataTransfer ' +
'fromElement offsetX offsetY pageX pageY screenX screenY toElement')),
mouseWheelProps = mouseProps.concat(str2arr('wheelDelta wheelDeltaX wheelDeltaY wheelDeltaZ ' +
'axis')) // 'axis' is FF specific
,
keyProps = commonProps.concat(str2arr('char charCode key keyCode keyIdentifier ' +
'keyLocation location')),
textProps = commonProps.concat(str2arr('data')),
touchProps = commonProps.concat(str2arr('touches targetTouches changedTouches scale rotation')),
messageProps = commonProps.concat(str2arr('data origin source')),
stateProps = commonProps.concat(str2arr('state')),
overOutRegex = /over|out/
// some event types need special handling and some need special properties, do that all here
,
typeFixers = [{ // key events
reg: /key/i,
fix: function(event, newEvent) {
newEvent.keyCode = event.keyCode || event.which
return keyProps
}
}, { // mouse events
reg: /click|mouse(?!(.*wheel|scroll))|menu|drag|drop/i,
fix: function(event, newEvent, type) {
newEvent.rightClick = event.which === 3 || event.button === 2
newEvent.pos = {
x: 0,
y: 0
}
if (event.pageX || event.pageY) {
newEvent.clientX = event.pageX
newEvent.clientY = event.pageY
} else if (event.clientX || event.clientY) {
newEvent.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft
newEvent.clientY = event.clientY + doc.body.scrollTop + root.scrollTop
}
if (overOutRegex.test(type)) {
newEvent.relatedTarget = event.relatedTarget || event[(type == 'mouseover' ? 'from' : 'to') + 'Element']
}
return mouseProps
}
}, { // mouse wheel events
reg: /mouse.*(wheel|scroll)/i,
fix: function() {
return mouseWheelProps
}
}, { // TextEvent
reg: /^text/i,
fix: function() {
return textProps
}
}, { // touch and gesture events
reg: /^touch|^gesture/i,
fix: function() {
return touchProps
}
}, { // message events
reg: /^message$/i,
fix: function() {
return messageProps
}
}, { // popstate events
reg: /^popstate$/i,
fix: function() {
return stateProps
}
}, { // everything else
reg: /.*/,
fix: function() {
return commonProps
}
}],
typeFixerMap = {} // used to map event types to fixer functions (above), a basic cache mechanism
, Event = function(event, element, isNative) {
if (!arguments.length) return
event = event || ((element.ownerDocument || element.document || element).parentWindow || win).event
this.originalEvent = event
this.isNative = isNative
this.isBean = true
if (!event) return
var type = event.type,
target = event.target || event.srcElement,
i, l, p, props, fixer
this.target = target && target.nodeType === 3 ? target.parentNode : target
if (isNative) { // we only need basic augmentation on custom events, the rest expensive & pointless
fixer = typeFixerMap[type]
if (!fixer) { // haven't encountered this event type before, map a fixer function for it
for (i = 0, l = typeFixers.length; i < l; i++) {
if (typeFixers[i].reg.test(type)) { // guaranteed to match at least one, last is .*
typeFixerMap[type] = fixer = typeFixers[i].fix
break
}
}
}
props = fixer(event, this, type)
for (i = props.length; i--;) {
if (!((p = props[i]) in this) && p in event) this[p] = event[p]
}
}
}
// preventDefault() and stopPropagation() are a consistent interface to those functions
// on the DOM, stop() is an alias for both of them together
Event.prototype.preventDefault = function() {
if (this.originalEvent.preventDefault) this.originalEvent.preventDefault()
else this.originalEvent.returnValue = false
}
Event.prototype.stopPropagation = function() {
if (this.originalEvent.stopPropagation) this.originalEvent.stopPropagation()
else this.originalEvent.cancelBubble = true
}
Event.prototype.stop = function() {
this.preventDefault()
this.stopPropagation()
this.stopped = true
}
// stopImmediatePropagation() has to be handled internally because we manage the event list for
// each element
// note that originalElement may be a Bean#Event object in some situations
Event.prototype.stopImmediatePropagation = function() {
if (this.originalEvent.stopImmediatePropagation) this.originalEvent.stopImmediatePropagation()
this.isImmediatePropagationStopped = function() {
return true
}
}
Event.prototype.isImmediatePropagationStopped = function() {
return this.originalEvent.isImmediatePropagationStopped && this.originalEvent.isImmediatePropagationStopped()
}
Event.prototype.clone = function(currentTarget) {
//TODO: this is ripe for optimisation, new events are *expensive*
// improving this will speed up delegated events
var ne = new Event(this, this.element, this.isNative)
ne.currentTarget = currentTarget
return ne
}
return Event
}())
// 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
}
/**
* Bean maintains an internal registry for event listeners. We don't touch elements, objects
* or functions to identify them, instead we store everything in the registry.
* Each event listener has a RegEntry object, we have one 'registry' for the whole instance.
*/
, RegEntry = (function() {
// each handler is wrapped so we can handle delegation and custom events
var wrappedHandler = function(element, fn, condition, args) {
var call = function(event, eargs) {
return fn.apply(element, args ? slice.call(eargs, event ? 0 : 1).concat(args) : eargs)
}, findTarget = function(event, eventElement) {
return fn.__beanDel ? fn.__beanDel.ft(event.target, element) : eventElement
}, handler = condition ? function(event) {
var target = findTarget(event, this) // deleated event
if (condition.apply(target, arguments)) {
if (event) event.currentTarget = target
return call(event, arguments)
}
} : function(event) {
if (fn.__beanDel) event = event.clone(findTarget(event)) // delegated event, fix the fix
return call(event, arguments)
}
handler.__beanDel = fn.__beanDel
return handler
}
, RegEntry = function(element, type, handler, original, namespaces, args, root) {
var customType = customEvents[type],
isNative
if (type == 'unload') {
// self clean-up
handler = once(removeListener, element, type, handler, original)
}
if (customType) {
if (customType.condition) {
handler = wrappedHandler(element, handler, customType.condition, args)
}
type = customType.base || type
}
this.isNative = isNative = nativeEvents[type] && !! element[eventSupport]
this.customType = !W3C_MODEL && !isNative && type
this.element = element
this.type = type
this.original = original
this.namespaces = namespaces
this.eventType = W3C_MODEL || isNative ? type : 'propertychange'
this.target = targetElement(element, isNative)
this[eventSupport] = !! this.target[eventSupport]
this.root = root
this.handler = wrappedHandler(element, handler, null, args)
}
// given a list of namespaces, is our entry in any of them?
RegEntry.prototype.inNamespaces = function(checkNamespaces) {
var i, j, c = 0
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]) c++
}
}
return checkNamespaces.length === c
}
// match by element, original fn (opt), handler fn (opt)
RegEntry.prototype.matches = function(checkElement, checkOriginal, checkHandler) {
return this.element === checkElement &&
(!checkOriginal || this.original === checkOriginal) &&
(!checkHandler || this.handler === checkHandler)
}
return RegEntry
}())
,
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 and 'r' as a special prefix for
// rootListeners so we can look them up fast
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, root, fn) {
var pfx = root ? 'r' : '$'
if (!type || type == '*') {
// search the whole registry
for (var t in map) {
if (t.charAt(0) == pfx) {
forAll(element, t.substr(1), original, handler, root, fn)
}
}
} else {
var i = 0,
l, list = map[pfx + type],
all = element == '*'
if (!list) return
for (l = list.length; i < l; i++) {
if ((all || list[i].matches(element, original, handler)) && !fn(list[i], list, i, type)) return
}
}
}
, has = function(element, type, original, root) {
// we're not using forAll here simply because it's a bit slower and this
// needs to be fast
var i, list = map[(root ? 'r' : '$') + type]
if (list) {
for (i = list.length; i--;) {
if (!list[i].root && list[i].matches(element, original, null)) return true
}
}
return false
}
, get = function(element, type, original, root) {
var entries = []
forAll(element, type, original, null, root, function(entry) {
return entries.push(entry)
})
return entries
}
, put = function(entry) {
var has = !entry.root && !this.has(entry.element, entry.type, null, false),
key = (entry.root ? 'r' : '$') + entry.type;
(map[key] || (map[key] = [])).push(entry)
return has
}
, del = function(entry) {
forAll(entry.element, entry.type, null, entry.handler, entry.root, function(entry, list, i) {
list.splice(i, 1)
entry.removed = true
if (list.length === 0) delete map[(entry.root ? 'r' : '$') + 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
}
}())
// we need a selector engine for delegated events, use querySelectorAll if it exists
// but for older browsers we need Qwery, Sizzle or similar
,
selectorEngine, setSelectorEngine = function(e) {
if (!arguments.length) {
selectorEngine = doc.querySelectorAll ? function(s, r) {
return r.querySelectorAll(s)
} : function() {
throw new Error('Bean: No selector engine installed') // eeek
}
} else {
selectorEngine = e
}
}
// we attach this listener to each DOM event that we need to listen to, only once
// per event type per DOM element
, rootListener = function(event, type) {
if (!W3C_MODEL && type && event && event.propertyName != '_on' + type) return
var listeners = registry.get(this, type || event.type, null, false),
l = listeners.length,
i = 0
event = new Event(event, this, true)
if (type) event.type = type
// iterate through all handlers registered for this type, calling them unless they have
// been removed by a previous handler or stopImmediatePropagation() has been called
for (; i < l && !event.isImmediatePropagationStopped(); i++) {
if (!listeners[i].removed) listeners[i].handler.call(this, event)
}
}
// add and remove listeners to DOM elements
, listener = W3C_MODEL ? function(element, type, add) {
// new browsers
element[add ? addEvent : removeEvent](type, rootListener, false)
} : function(element, type, add, custom) {
// IE8 and below, use attachEvent/detachEvent and we have to piggy-back propertychange events
// to simulate event bubbling etc.
var entry
if (add) {
registry.put(entry = new RegEntry(
element, custom || type, function(event) { // handler
rootListener.call(element, event, custom)
}, rootListener, null, null, true // is root
))
if (custom && element['_on' + custom] == null) element['_on' + custom] = 0
entry.target.attachEvent('on' + entry.eventType, entry.handler)
} else {
entry = registry.get(element, custom || type, rootListener, true)[0]
if (entry) {
entry.target.detachEvent('on' + entry.eventType, entry.handler)
registry.del(entry)
}
}
}
, once = function(rm, element, type, fn, originalFn) {
// wrap the handler in a handler that does a remove as well
return function() {
fn.apply(this, arguments)
rm(element, type, originalFn)
}
}
, removeListener = function(element, orgType, handler, namespaces) {
var type = orgType && orgType.replace(nameRegex, ''),
handlers = registry.get(element, type, null, false),
removed = {}, i, l
for (i = 0, l = handlers.length; i < l; i++) {
if ((!handler || handlers[i].original === handler) && handlers[i].inNamespaces(namespaces)) {
// 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(handlers[i])
if (!removed[handlers[i].eventType] && handlers[i][eventSupport])
removed[handlers[i].eventType] = {
t: handlers[i].eventType,
c: handlers[i].type
}
}
}
// check each type/element for removed listeners and remove the rootListener where it's no longer needed
for (i in removed) {
if (!registry.has(element, removed[i].t, null, false)) {
// last listener of this type, remove the rootListener
listener(element, removed[i].t, false, removed[i].c)
}
}
}
// set up a delegate helper using the given selector, wrap the handler function
, delegate = function(selector, fn) {
//TODO: findTarget (therefore $) is called twice, once for match and once for
// setting e.currentTarget, fix this so it's only needed once
var findTarget = function(target, root) {
var i, array = isString(selector) ? selectorEngine(selector, root) : selector
for (; target && target !== root; target = target.parentNode) {
for (i = array.length; i--;) {
if (array[i] === target) return target
}
}
}, handler = function(e) {
var match = findTarget(e.target, this)
if (match) fn.apply(match, arguments)
}
// __beanDel isn't pleasant but it's a private function, not exposed outside of Bean
handler.__beanDel = {
ft: findTarget // attach it here for customEvents to use too
,
selector: selector
}
return handler
}
, fireListener = W3C_MODEL ? function(isNative, type, element) {
// modern browsers, do a proper dispatchEvent()
var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents')
evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1)
element.dispatchEvent(evt)
} : function(isNative, type, element) {
// old browser use onpropertychange, just increment a custom property to trigger the event
element = targetElement(element, isNative)
isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++
}
/**
* Public API: off(), on(), add(), (remove()), one(), fire(), clone()
*/
/**
* off(element[, eventType(s)[, handler ]])
*/
, off = function(element, typeSpec, fn) {
var isTypeStr = isString(typeSpec),
k, type, namespaces, i
if (isTypeStr && typeSpec.indexOf(' ') > 0) {
// off(el, 't1 t2 t3', fn) or off(el, 't1 t2 t3')
typeSpec = str2arr(typeSpec)
for (i = typeSpec.length; i--;)
off(element, typeSpec[i], fn)
return element
}
type = isTypeStr && typeSpec.replace(nameRegex, '')
if (type && customEvents[type]) type = customEvents[type].base
if (!typeSpec || isTypeStr) {
// off(el) or off(el, t1.ns) or off(el, .ns) or off(el, .ns1.ns2.ns3)
if (namespaces = isTypeStr && typeSpec.replace(namespaceRegex, '')) namespaces = str2arr(namespaces, '.')
removeListener(element, type, fn, namespaces)
} else if (isFunction(typeSpec)) {
// off(el, fn)
removeListener(element, null, typeSpec)
} else {
// off(el, { t1: fn1, t2, fn2 })
for (k in typeSpec) {
if (typeSpec.hasOwnProperty(k)) off(element, k, typeSpec[k])
}
}
return element
}
/**
* on(element, eventType(s)[, selector], handler[, args ])
*/
, on = function(element, events, selector, fn) {
var originalFn, type, types, i, args, entry, first
//TODO: the undefined check means you can't pass an 'args' argument, fix this perhaps?
if (selector === undefined && typeof events == 'object') {
//TODO: this can't handle delegated events
for (type in events) {
if (events.hasOwnProperty(type)) {
on.call(this, element, type, events[type])
}
}
return
}
if (!isFunction(selector)) {
// delegated event
originalFn = fn
args = slice.call(arguments, 4)
fn = delegate(selector, originalFn, selectorEngine)
} else {
args = slice.call(arguments, 3)
fn = originalFn = selector
}
types = str2arr(events)
// special case for one(), wrap in a self-removing handler
if (this === ONE) {
fn = once(off, element, events, fn, originalFn)
}
for (i = types.length; i--;) {
// add new handler to the registry and check if it's the first for this element/type
first = registry.put(entry = new RegEntry(
element, types[i].replace(nameRegex, '') // event type
, fn, originalFn, str2arr(types[i].replace(namespaceRegex, ''), '.') // namespaces
, args, false // not root
))
if (entry[eventSupport] && first) {
// first event of this type on this element, add root listener
listener(element, entry.eventType, true, entry.customType)
}
}
return element
}
/**
* add(element[, selector], eventType(s), handler[, args ])
*
* Deprecated: kept (for now) for backward-compatibility
*/
, add = function(element, events, fn, delfn) {
return on.apply(
null, !isString(fn) ? slice.call(arguments) : [element, fn, events, delfn].concat(arguments.length > 3 ? slice.call(arguments, 5) : [])
)
}
/**
* one(element, eventType(s)[, selector], handler[, args ])
*/
, one = function() {
return on.apply(ONE, arguments)
}
/**
* fire(element, eventType(s)[, args ])
*
* The optional 'args' argument must be an array, if no 'args' argument is provided
* then we can use the browser's DOM event system, otherwise we trigger handlers manually
*/
, fire = function(element, type, args) {
var types = str2arr(type),
i, j, l, names, handlers
for (i = types.length; i--;) {
type = types[i].replace(nameRegex, '')
if (names = types[i].replace(namespaceRegex, '')) names = str2arr(names, '.')
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, null, false)
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(dstElement, srcElement[, eventType ])
*
* TODO: perhaps for consistency we should allow the same flexibility in type specifiers?
*/
, clone = function(element, from, type) {
var handlers = registry.get(from, type, null, false),
l = handlers.length,
i = 0,
args, beanDel
for (; i < l; i++) {
if (handlers[i].original) {
args = [element, handlers[i].type]
if (beanDel = handlers[i].handler.__beanDel) args.push(beanDel.selector)
args.push(handlers[i].original)
on.apply(null, args)
}
}
return element
}
, bean = {
on: on,
add: add,
one: one,
off: off,
remove: off,
clone: clone,
fire: fire,
setSelectorEngine: setSelectorEngine,
noConflict: function() {
context[name] = old
return this
}
}
// for IE, clean up on unload to avoid leaks
if (win.attachEvent) {
var cleanup = function() {
var i, entries = registry.entries()
for (i in entries) {
if (entries[i].type && entries[i].type !== 'unload') off(entries[i].element, entries[i].type)
}
win.detachEvent('onunload', cleanup)
win.CollectGarbage && win.CollectGarbage()
}
win.attachEvent('onunload', cleanup)
}
// initialize selector engine to internal default (qSA or throw Error)
setSelectorEngine()
return bean
}));