mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-18 11:28:23 -07:00
737 lines
31 KiB
JavaScript
737 lines
31 KiB
JavaScript
(function (name, context, definition) {
|
|
if (typeof module != 'undefined' && module.exports) module.exports = definition()
|
|
else if (typeof define == 'function' && define.amd) define(definition)
|
|
else context[name] = definition()
|
|
})('bean', this, function (name, context) {
|
|
name = name || 'bean'
|
|
context = context || this
|
|
|
|
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 path')
|
|
, mouseProps = commonProps.concat(str2arr('button buttons clientX clientY dataTransfer ' +
|
|
'fromElement offsetX offsetY pageX pageY screenX screenY toElement movementX movementY region'))
|
|
, mouseWheelProps = mouseProps.concat(str2arr('wheelDelta wheelDeltaX wheelDeltaY wheelDeltaZ ' +
|
|
'axis')) // 'axis' is FF specific
|
|
, keyProps = commonProps.concat(str2arr('char charCode key keyCode keyIdentifier ' +
|
|
'keyLocation location isComposing code'))
|
|
, 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
|
|
, 'Event' : Event
|
|
, '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
|
|
});
|