- 1 :
/**
- 2 :
* @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
- 3 :
* (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
- 4 :
* This should work very similarly to jQuery's events, however it's based off the book version which isn't as
- 5 :
* robust as jquery's, so there's probably some differences.
- 6 :
*
- 7 :
* @file events.js
- 8 :
* @module events
- 9 :
*/
- 10 :
import DomData from './dom-data';
- 11 :
import * as Guid from './guid.js';
- 12 :
import log from './log.js';
- 13 :
import window from 'global/window';
- 14 :
import document from 'global/document';
- 15 :
- 16 :
/**
- 17 :
* Clean up the listener cache and dispatchers
- 18 :
*
- 19 :
* @param {Element|Object} elem
- 20 :
* Element to clean up
- 21 :
*
- 22 :
* @param {string} type
- 23 :
* Type of event to clean up
- 24 :
*/
- 25 :
function _cleanUpEvents(elem, type) {
- 26 :
if (!DomData.has(elem)) {
- 27 :
return;
- 28 :
}
- 29 :
const data = DomData.get(elem);
- 30 :
- 31 :
// Remove the events of a particular type if there are none left
- 32 :
if (data.handlers[type].length === 0) {
- 33 :
delete data.handlers[type];
- 34 :
// data.handlers[type] = null;
- 35 :
// Setting to null was causing an error with data.handlers
- 36 :
- 37 :
// Remove the meta-handler from the element
- 38 :
if (elem.removeEventListener) {
- 39 :
elem.removeEventListener(type, data.dispatcher, false);
- 40 :
} else if (elem.detachEvent) {
- 41 :
elem.detachEvent('on' + type, data.dispatcher);
- 42 :
}
- 43 :
}
- 44 :
- 45 :
// Remove the events object if there are no types left
- 46 :
if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
- 47 :
delete data.handlers;
- 48 :
delete data.dispatcher;
- 49 :
delete data.disabled;
- 50 :
}
- 51 :
- 52 :
// Finally remove the element data if there is no data left
- 53 :
if (Object.getOwnPropertyNames(data).length === 0) {
- 54 :
DomData.delete(elem);
- 55 :
}
- 56 :
}
- 57 :
- 58 :
/**
- 59 :
* Loops through an array of event types and calls the requested method for each type.
- 60 :
*
- 61 :
* @param {Function} fn
- 62 :
* The event method we want to use.
- 63 :
*
- 64 :
* @param {Element|Object} elem
- 65 :
* Element or object to bind listeners to
- 66 :
*
- 67 :
* @param {string} type
- 68 :
* Type of event to bind to.
- 69 :
*
- 70 :
* @param {Function} callback
- 71 :
* Event listener.
- 72 :
*/
- 73 :
function _handleMultipleEvents(fn, elem, types, callback) {
- 74 :
types.forEach(function(type) {
- 75 :
// Call the event method for each one of the types
- 76 :
fn(elem, type, callback);
- 77 :
});
- 78 :
}
- 79 :
- 80 :
/**
- 81 :
* Fix a native event to have standard property values
- 82 :
*
- 83 :
* @param {Object} event
- 84 :
* Event object to fix.
- 85 :
*
- 86 :
* @return {Object}
- 87 :
* Fixed event object.
- 88 :
*/
- 89 :
export function fixEvent(event) {
- 90 :
if (event.fixed_) {
- 91 :
return event;
- 92 :
}
- 93 :
- 94 :
function returnTrue() {
- 95 :
return true;
- 96 :
}
- 97 :
- 98 :
function returnFalse() {
- 99 :
return false;
- 100 :
}
- 101 :
- 102 :
// Test if fixing up is needed
- 103 :
// Used to check if !event.stopPropagation instead of isPropagationStopped
- 104 :
// But native events return true for stopPropagation, but don't have
- 105 :
// other expected methods like isPropagationStopped. Seems to be a problem
- 106 :
// with the Javascript Ninja code. So we're just overriding all events now.
- 107 :
if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
- 108 :
const old = event || window.event;
- 109 :
- 110 :
event = {};
- 111 :
// Clone the old object so that we can modify the values event = {};
- 112 :
// IE8 Doesn't like when you mess with native event properties
- 113 :
// Firefox returns false for event.hasOwnProperty('type') and other props
- 114 :
// which makes copying more difficult.
- 115 :
// TODO: Probably best to create a whitelist of event props
- 116 :
for (const key in old) {
- 117 :
// Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
- 118 :
// Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
- 119 :
// and webkitMovementX/Y
- 120 :
// Lighthouse complains if Event.path is copied
- 121 :
if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' &&
- 122 :
key !== 'webkitMovementX' && key !== 'webkitMovementY' &&
- 123 :
key !== 'path') {
- 124 :
// Chrome 32+ warns if you try to copy deprecated returnValue, but
- 125 :
// we still want to if preventDefault isn't supported (IE8).
- 126 :
if (!(key === 'returnValue' && old.preventDefault)) {
- 127 :
event[key] = old[key];
- 128 :
}
- 129 :
}
- 130 :
}
- 131 :
- 132 :
// The event occurred on this element
- 133 :
if (!event.target) {
- 134 :
event.target = event.srcElement || document;
- 135 :
}
- 136 :
- 137 :
// Handle which other element the event is related to
- 138 :
if (!event.relatedTarget) {
- 139 :
event.relatedTarget = event.fromElement === event.target ?
- 140 :
event.toElement :
- 141 :
event.fromElement;
- 142 :
}
- 143 :
- 144 :
// Stop the default browser action
- 145 :
event.preventDefault = function() {
- 146 :
if (old.preventDefault) {
- 147 :
old.preventDefault();
- 148 :
}
- 149 :
event.returnValue = false;
- 150 :
old.returnValue = false;
- 151 :
event.defaultPrevented = true;
- 152 :
};
- 153 :
- 154 :
event.defaultPrevented = false;
- 155 :
- 156 :
// Stop the event from bubbling
- 157 :
event.stopPropagation = function() {
- 158 :
if (old.stopPropagation) {
- 159 :
old.stopPropagation();
- 160 :
}
- 161 :
event.cancelBubble = true;
- 162 :
old.cancelBubble = true;
- 163 :
event.isPropagationStopped = returnTrue;
- 164 :
};
- 165 :
- 166 :
event.isPropagationStopped = returnFalse;
- 167 :
- 168 :
// Stop the event from bubbling and executing other handlers
- 169 :
event.stopImmediatePropagation = function() {
- 170 :
if (old.stopImmediatePropagation) {
- 171 :
old.stopImmediatePropagation();
- 172 :
}
- 173 :
event.isImmediatePropagationStopped = returnTrue;
- 174 :
event.stopPropagation();
- 175 :
};
- 176 :
- 177 :
event.isImmediatePropagationStopped = returnFalse;
- 178 :
- 179 :
// Handle mouse position
- 180 :
if (event.clientX !== null && event.clientX !== undefined) {
- 181 :
const doc = document.documentElement;
- 182 :
const body = document.body;
- 183 :
- 184 :
event.pageX = event.clientX +
- 185 :
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
- 186 :
(doc && doc.clientLeft || body && body.clientLeft || 0);
- 187 :
event.pageY = event.clientY +
- 188 :
(doc && doc.scrollTop || body && body.scrollTop || 0) -
- 189 :
(doc && doc.clientTop || body && body.clientTop || 0);
- 190 :
}
- 191 :
- 192 :
// Handle key presses
- 193 :
event.which = event.charCode || event.keyCode;
- 194 :
- 195 :
// Fix button for mouse clicks:
- 196 :
// 0 == left; 1 == middle; 2 == right
- 197 :
if (event.button !== null && event.button !== undefined) {
- 198 :
- 199 :
// The following is disabled because it does not pass videojs-standard
- 200 :
// and... yikes.
- 201 :
/* eslint-disable */
- 202 :
event.button = (event.button & 1 ? 0 :
- 203 :
(event.button & 4 ? 1 :
- 204 :
(event.button & 2 ? 2 : 0)));
- 205 :
/* eslint-enable */
- 206 :
}
- 207 :
}
- 208 :
- 209 :
event.fixed_ = true;
- 210 :
// Returns fixed-up instance
- 211 :
return event;
- 212 :
}
- 213 :
- 214 :
/**
- 215 :
* Whether passive event listeners are supported
- 216 :
*/
- 217 :
let _supportsPassive;
- 218 :
- 219 :
const supportsPassive = function() {
- 220 :
if (typeof _supportsPassive !== 'boolean') {
- 221 :
_supportsPassive = false;
- 222 :
try {
- 223 :
const opts = Object.defineProperty({}, 'passive', {
- 224 :
get() {
- 225 :
_supportsPassive = true;
- 226 :
}
- 227 :
});
- 228 :
- 229 :
window.addEventListener('test', null, opts);
- 230 :
window.removeEventListener('test', null, opts);
- 231 :
} catch (e) {
- 232 :
// disregard
- 233 :
}
- 234 :
}
- 235 :
- 236 :
return _supportsPassive;
- 237 :
};
- 238 :
- 239 :
/**
- 240 :
* Touch events Chrome expects to be passive
- 241 :
*/
- 242 :
const passiveEvents = [
- 243 :
'touchstart',
- 244 :
'touchmove'
- 245 :
];
- 246 :
- 247 :
/**
- 248 :
* Add an event listener to element
- 249 :
* It stores the handler function in a separate cache object
- 250 :
* and adds a generic handler to the element's event,
- 251 :
* along with a unique id (guid) to the element.
- 252 :
*
- 253 :
* @param {Element|Object} elem
- 254 :
* Element or object to bind listeners to
- 255 :
*
- 256 :
* @param {string|string[]} type
- 257 :
* Type of event to bind to.
- 258 :
*
- 259 :
* @param {Function} fn
- 260 :
* Event listener.
- 261 :
*/
- 262 :
export function on(elem, type, fn) {
- 263 :
if (Array.isArray(type)) {
- 264 :
return _handleMultipleEvents(on, elem, type, fn);
- 265 :
}
- 266 :
- 267 :
if (!DomData.has(elem)) {
- 268 :
DomData.set(elem, {});
- 269 :
}
- 270 :
- 271 :
const data = DomData.get(elem);
- 272 :
- 273 :
// We need a place to store all our handler data
- 274 :
if (!data.handlers) {
- 275 :
data.handlers = {};
- 276 :
}
- 277 :
- 278 :
if (!data.handlers[type]) {
- 279 :
data.handlers[type] = [];
- 280 :
}
- 281 :
- 282 :
if (!fn.guid) {
- 283 :
fn.guid = Guid.newGUID();
- 284 :
}
- 285 :
- 286 :
data.handlers[type].push(fn);
- 287 :
- 288 :
if (!data.dispatcher) {
- 289 :
data.disabled = false;
- 290 :
- 291 :
data.dispatcher = function(event, hash) {
- 292 :
- 293 :
if (data.disabled) {
- 294 :
return;
- 295 :
}
- 296 :
- 297 :
event = fixEvent(event);
- 298 :
- 299 :
const handlers = data.handlers[event.type];
- 300 :
- 301 :
if (handlers) {
- 302 :
// Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
- 303 :
const handlersCopy = handlers.slice(0);
- 304 :
- 305 :
for (let m = 0, n = handlersCopy.length; m < n; m++) {
- 306 :
if (event.isImmediatePropagationStopped()) {
- 307 :
break;
- 308 :
} else {
- 309 :
try {
- 310 :
handlersCopy[m].call(elem, event, hash);
- 311 :
} catch (e) {
- 312 :
log.error(e);
- 313 :
}
- 314 :
}
- 315 :
}
- 316 :
}
- 317 :
};
- 318 :
}
- 319 :
- 320 :
if (data.handlers[type].length === 1) {
- 321 :
if (elem.addEventListener) {
- 322 :
let options = false;
- 323 :
- 324 :
if (supportsPassive() &&
- 325 :
passiveEvents.indexOf(type) > -1) {
- 326 :
options = {passive: true};
- 327 :
}
- 328 :
elem.addEventListener(type, data.dispatcher, options);
- 329 :
} else if (elem.attachEvent) {
- 330 :
elem.attachEvent('on' + type, data.dispatcher);
- 331 :
}
- 332 :
}
- 333 :
}
- 334 :
- 335 :
/**
- 336 :
* Removes event listeners from an element
- 337 :
*
- 338 :
* @param {Element|Object} elem
- 339 :
* Object to remove listeners from.
- 340 :
*
- 341 :
* @param {string|string[]} [type]
- 342 :
* Type of listener to remove. Don't include to remove all events from element.
- 343 :
*
- 344 :
* @param {Function} [fn]
- 345 :
* Specific listener to remove. Don't include to remove listeners for an event
- 346 :
* type.
- 347 :
*/
- 348 :
export function off(elem, type, fn) {
- 349 :
// Don't want to add a cache object through getElData if not needed
- 350 :
if (!DomData.has(elem)) {
- 351 :
return;
- 352 :
}
- 353 :
- 354 :
const data = DomData.get(elem);
- 355 :
- 356 :
// If no events exist, nothing to unbind
- 357 :
if (!data.handlers) {
- 358 :
return;
- 359 :
}
- 360 :
- 361 :
if (Array.isArray(type)) {
- 362 :
return _handleMultipleEvents(off, elem, type, fn);
- 363 :
}
- 364 :
- 365 :
// Utility function
- 366 :
const removeType = function(el, t) {
- 367 :
data.handlers[t] = [];
- 368 :
_cleanUpEvents(el, t);
- 369 :
};
- 370 :
- 371 :
// Are we removing all bound events?
- 372 :
if (type === undefined) {
- 373 :
for (const t in data.handlers) {
- 374 :
if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
- 375 :
removeType(elem, t);
- 376 :
}
- 377 :
}
- 378 :
return;
- 379 :
}
- 380 :
- 381 :
const handlers = data.handlers[type];
- 382 :
- 383 :
// If no handlers exist, nothing to unbind
- 384 :
if (!handlers) {
- 385 :
return;
- 386 :
}
- 387 :
- 388 :
// If no listener was provided, remove all listeners for type
- 389 :
if (!fn) {
- 390 :
removeType(elem, type);
- 391 :
return;
- 392 :
}
- 393 :
- 394 :
// We're only removing a single handler
- 395 :
if (fn.guid) {
- 396 :
for (let n = 0; n < handlers.length; n++) {
- 397 :
if (handlers[n].guid === fn.guid) {
- 398 :
handlers.splice(n--, 1);
- 399 :
}
- 400 :
}
- 401 :
}
- 402 :
- 403 :
_cleanUpEvents(elem, type);
- 404 :
}
- 405 :
- 406 :
/**
- 407 :
* Trigger an event for an element
- 408 :
*
- 409 :
* @param {Element|Object} elem
- 410 :
* Element to trigger an event on
- 411 :
*
- 412 :
* @param {EventTarget~Event|string} event
- 413 :
* A string (the type) or an event object with a type attribute
- 414 :
*
- 415 :
* @param {Object} [hash]
- 416 :
* data hash to pass along with the event
- 417 :
*
- 418 :
* @return {boolean|undefined}
- 419 :
* Returns the opposite of `defaultPrevented` if default was
- 420 :
* prevented. Otherwise, returns `undefined`
- 421 :
*/
- 422 :
export function trigger(elem, event, hash) {
- 423 :
// Fetches element data and a reference to the parent (for bubbling).
- 424 :
// Don't want to add a data object to cache for every parent,
- 425 :
// so checking hasElData first.
- 426 :
const elemData = DomData.has(elem) ? DomData.get(elem) : {};
- 427 :
const parent = elem.parentNode || elem.ownerDocument;
- 428 :
// type = event.type || event,
- 429 :
// handler;
- 430 :
- 431 :
// If an event name was passed as a string, creates an event out of it
- 432 :
if (typeof event === 'string') {
- 433 :
event = {type: event, target: elem};
- 434 :
} else if (!event.target) {
- 435 :
event.target = elem;
- 436 :
}
- 437 :
- 438 :
// Normalizes the event properties.
- 439 :
event = fixEvent(event);
- 440 :
- 441 :
// If the passed element has a dispatcher, executes the established handlers.
- 442 :
if (elemData.dispatcher) {
- 443 :
elemData.dispatcher.call(elem, event, hash);
- 444 :
}
- 445 :
- 446 :
// Unless explicitly stopped or the event does not bubble (e.g. media events)
- 447 :
// recursively calls this function to bubble the event up the DOM.
- 448 :
if (parent && !event.isPropagationStopped() && event.bubbles === true) {
- 449 :
trigger.call(null, parent, event, hash);
- 450 :
- 451 :
// If at the top of the DOM, triggers the default action unless disabled.
- 452 :
} else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
- 453 :
if (!DomData.has(event.target)) {
- 454 :
DomData.set(event.target, {});
- 455 :
}
- 456 :
const targetData = DomData.get(event.target);
- 457 :
- 458 :
// Checks if the target has a default action for this event.
- 459 :
if (event.target[event.type]) {
- 460 :
// Temporarily disables event dispatching on the target as we have already executed the handler.
- 461 :
targetData.disabled = true;
- 462 :
// Executes the default action.
- 463 :
if (typeof event.target[event.type] === 'function') {
- 464 :
event.target[event.type]();
- 465 :
}
- 466 :
// Re-enables event dispatching.
- 467 :
targetData.disabled = false;
- 468 :
}
- 469 :
}
- 470 :
- 471 :
// Inform the triggerer if the default was prevented by returning false
- 472 :
return !event.defaultPrevented;
- 473 :
}
- 474 :
- 475 :
/**
- 476 :
* Trigger a listener only once for an event.
- 477 :
*
- 478 :
* @param {Element|Object} elem
- 479 :
* Element or object to bind to.
- 480 :
*
- 481 :
* @param {string|string[]} type
- 482 :
* Name/type of event
- 483 :
*
- 484 :
* @param {Event~EventListener} fn
- 485 :
* Event listener function
- 486 :
*/
- 487 :
export function one(elem, type, fn) {
- 488 :
if (Array.isArray(type)) {
- 489 :
return _handleMultipleEvents(one, elem, type, fn);
- 490 :
}
- 491 :
const func = function() {
- 492 :
off(elem, type, func);
- 493 :
fn.apply(this, arguments);
- 494 :
};
- 495 :
- 496 :
// copy the guid to the new function so it can removed using the original function's ID
- 497 :
func.guid = fn.guid = fn.guid || Guid.newGUID();
- 498 :
on(elem, type, func);
- 499 :
}
- 500 :
- 501 :
/**
- 502 :
* Trigger a listener only once and then turn if off for all
- 503 :
* configured events
- 504 :
*
- 505 :
* @param {Element|Object} elem
- 506 :
* Element or object to bind to.
- 507 :
*
- 508 :
* @param {string|string[]} type
- 509 :
* Name/type of event
- 510 :
*
- 511 :
* @param {Event~EventListener} fn
- 512 :
* Event listener function
- 513 :
*/
- 514 :
export function any(elem, type, fn) {
- 515 :
const func = function() {
- 516 :
off(elem, type, func);
- 517 :
fn.apply(this, arguments);
- 518 :
};
- 519 :
- 520 :
// copy the guid to the new function so it can removed using the original function's ID
- 521 :
func.guid = fn.guid = fn.guid || Guid.newGUID();
- 522 :
- 523 :
// multiple ons, but one off for everything
- 524 :
on(elem, type, func);
- 525 :
}