summaryrefslogtreecommitdiff
path: root/javascript/videojs/src/js/utils/events.js
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/videojs/src/js/utils/events.js')
-rw-r--r--javascript/videojs/src/js/utils/events.js529
1 files changed, 529 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/utils/events.js b/javascript/videojs/src/js/utils/events.js
new file mode 100644
index 0000000..2a76b07
--- /dev/null
+++ b/javascript/videojs/src/js/utils/events.js
@@ -0,0 +1,529 @@
+/**
+ * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
+ * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
+ * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
+ * robust as jquery's, so there's probably some differences.
+ *
+ * @file events.js
+ * @module events
+ */
+import DomData from './dom-data';
+import * as Guid from './guid.js';
+import log from './log.js';
+import window from 'global/window';
+import document from 'global/document';
+
+/**
+ * Clean up the listener cache and dispatchers
+ *
+ * @param {Element|Object} elem
+ * Element to clean up
+ *
+ * @param {string} type
+ * Type of event to clean up
+ */
+function _cleanUpEvents(elem, type) {
+ if (!DomData.has(elem)) {
+ return;
+ }
+ const data = DomData.get(elem);
+
+ // Remove the events of a particular type if there are none left
+ if (data.handlers[type].length === 0) {
+ delete data.handlers[type];
+ // data.handlers[type] = null;
+ // Setting to null was causing an error with data.handlers
+
+ // Remove the meta-handler from the element
+ if (elem.removeEventListener) {
+ elem.removeEventListener(type, data.dispatcher, false);
+ } else if (elem.detachEvent) {
+ elem.detachEvent('on' + type, data.dispatcher);
+ }
+ }
+
+ // Remove the events object if there are no types left
+ if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
+ delete data.handlers;
+ delete data.dispatcher;
+ delete data.disabled;
+ }
+
+ // Finally remove the element data if there is no data left
+ if (Object.getOwnPropertyNames(data).length === 0) {
+ DomData.delete(elem);
+ }
+}
+
+/**
+ * Loops through an array of event types and calls the requested method for each type.
+ *
+ * @param {Function} fn
+ * The event method we want to use.
+ *
+ * @param {Element|Object} elem
+ * Element or object to bind listeners to
+ *
+ * @param {string[]} types
+ * Type of event to bind to.
+ *
+ * @param {Function} callback
+ * Event listener.
+ */
+function _handleMultipleEvents(fn, elem, types, callback) {
+ types.forEach(function(type) {
+ // Call the event method for each one of the types
+ fn(elem, type, callback);
+ });
+}
+
+/**
+ * Fix a native event to have standard property values
+ *
+ * @param {Object} event
+ * Event object to fix.
+ *
+ * @return {Object}
+ * Fixed event object.
+ */
+export function fixEvent(event) {
+ if (event.fixed_) {
+ return event;
+ }
+
+ function returnTrue() {
+ return true;
+ }
+
+ function returnFalse() {
+ return false;
+ }
+
+ // Test if fixing up is needed
+ // Used to check if !event.stopPropagation instead of isPropagationStopped
+ // But native events return true for stopPropagation, but don't have
+ // other expected methods like isPropagationStopped. Seems to be a problem
+ // with the Javascript Ninja code. So we're just overriding all events now.
+ if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
+ const old = event || window.event;
+
+ event = {};
+ // Clone the old object so that we can modify the values event = {};
+ // IE8 Doesn't like when you mess with native event properties
+ // Firefox returns false for event.hasOwnProperty('type') and other props
+ // which makes copying more difficult.
+
+ // TODO: Probably best to create an allowlist of event props
+ const deprecatedProps = [
+ 'layerX', 'layerY', 'keyLocation', 'path',
+ 'webkitMovementX', 'webkitMovementY', 'mozPressure', 'mozInputSource'
+ ];
+
+ for (const key in old) {
+ // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
+ // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
+ // and webkitMovementX/Y
+ // Lighthouse complains if Event.path is copied
+ if (!deprecatedProps.includes(key)) {
+ // Chrome 32+ warns if you try to copy deprecated returnValue, but
+ // we still want to if preventDefault isn't supported (IE8).
+ if (!(key === 'returnValue' && old.preventDefault)) {
+ event[key] = old[key];
+ }
+ }
+ }
+
+ // The event occurred on this element
+ if (!event.target) {
+ event.target = event.srcElement || document;
+ }
+
+ // Handle which other element the event is related to
+ if (!event.relatedTarget) {
+ event.relatedTarget = event.fromElement === event.target ?
+ event.toElement :
+ event.fromElement;
+ }
+
+ // Stop the default browser action
+ event.preventDefault = function() {
+ if (old.preventDefault) {
+ old.preventDefault();
+ }
+ event.returnValue = false;
+ old.returnValue = false;
+ event.defaultPrevented = true;
+ };
+
+ event.defaultPrevented = false;
+
+ // Stop the event from bubbling
+ event.stopPropagation = function() {
+ if (old.stopPropagation) {
+ old.stopPropagation();
+ }
+ event.cancelBubble = true;
+ old.cancelBubble = true;
+ event.isPropagationStopped = returnTrue;
+ };
+
+ event.isPropagationStopped = returnFalse;
+
+ // Stop the event from bubbling and executing other handlers
+ event.stopImmediatePropagation = function() {
+ if (old.stopImmediatePropagation) {
+ old.stopImmediatePropagation();
+ }
+ event.isImmediatePropagationStopped = returnTrue;
+ event.stopPropagation();
+ };
+
+ event.isImmediatePropagationStopped = returnFalse;
+
+ // Handle mouse position
+ if (event.clientX !== null && event.clientX !== undefined) {
+ const doc = document.documentElement;
+ const body = document.body;
+
+ event.pageX = event.clientX +
+ (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
+ (doc && doc.clientLeft || body && body.clientLeft || 0);
+ event.pageY = event.clientY +
+ (doc && doc.scrollTop || body && body.scrollTop || 0) -
+ (doc && doc.clientTop || body && body.clientTop || 0);
+ }
+
+ // Handle key presses
+ event.which = event.charCode || event.keyCode;
+
+ // Fix button for mouse clicks:
+ // 0 == left; 1 == middle; 2 == right
+ if (event.button !== null && event.button !== undefined) {
+
+ // The following is disabled because it does not pass videojs-standard
+ // and... yikes.
+ /* eslint-disable */
+ event.button = (event.button & 1 ? 0 :
+ (event.button & 4 ? 1 :
+ (event.button & 2 ? 2 : 0)));
+ /* eslint-enable */
+ }
+ }
+
+ event.fixed_ = true;
+ // Returns fixed-up instance
+ return event;
+}
+
+/**
+ * Whether passive event listeners are supported
+ */
+let _supportsPassive;
+
+const supportsPassive = function() {
+ if (typeof _supportsPassive !== 'boolean') {
+ _supportsPassive = false;
+ try {
+ const opts = Object.defineProperty({}, 'passive', {
+ get() {
+ _supportsPassive = true;
+ }
+ });
+
+ window.addEventListener('test', null, opts);
+ window.removeEventListener('test', null, opts);
+ } catch (e) {
+ // disregard
+ }
+ }
+
+ return _supportsPassive;
+};
+
+/**
+ * Touch events Chrome expects to be passive
+ */
+const passiveEvents = [
+ 'touchstart',
+ 'touchmove'
+];
+
+/**
+ * Add an event listener to element
+ * It stores the handler function in a separate cache object
+ * and adds a generic handler to the element's event,
+ * along with a unique id (guid) to the element.
+ *
+ * @param {Element|Object} elem
+ * Element or object to bind listeners to
+ *
+ * @param {string|string[]} type
+ * Type of event to bind to.
+ *
+ * @param {Function} fn
+ * Event listener.
+ */
+export function on(elem, type, fn) {
+ if (Array.isArray(type)) {
+ return _handleMultipleEvents(on, elem, type, fn);
+ }
+
+ if (!DomData.has(elem)) {
+ DomData.set(elem, {});
+ }
+
+ const data = DomData.get(elem);
+
+ // We need a place to store all our handler data
+ if (!data.handlers) {
+ data.handlers = {};
+ }
+
+ if (!data.handlers[type]) {
+ data.handlers[type] = [];
+ }
+
+ if (!fn.guid) {
+ fn.guid = Guid.newGUID();
+ }
+
+ data.handlers[type].push(fn);
+
+ if (!data.dispatcher) {
+ data.disabled = false;
+
+ data.dispatcher = function(event, hash) {
+
+ if (data.disabled) {
+ return;
+ }
+
+ event = fixEvent(event);
+
+ const handlers = data.handlers[event.type];
+
+ if (handlers) {
+ // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
+ const handlersCopy = handlers.slice(0);
+
+ for (let m = 0, n = handlersCopy.length; m < n; m++) {
+ if (event.isImmediatePropagationStopped()) {
+ break;
+ } else {
+ try {
+ handlersCopy[m].call(elem, event, hash);
+ } catch (e) {
+ log.error(e);
+ }
+ }
+ }
+ }
+ };
+ }
+
+ if (data.handlers[type].length === 1) {
+ if (elem.addEventListener) {
+ let options = false;
+
+ if (supportsPassive() &&
+ passiveEvents.indexOf(type) > -1) {
+ options = {passive: true};
+ }
+ elem.addEventListener(type, data.dispatcher, options);
+ } else if (elem.attachEvent) {
+ elem.attachEvent('on' + type, data.dispatcher);
+ }
+ }
+}
+
+/**
+ * Removes event listeners from an element
+ *
+ * @param {Element|Object} elem
+ * Object to remove listeners from.
+ *
+ * @param {string|string[]} [type]
+ * Type of listener to remove. Don't include to remove all events from element.
+ *
+ * @param {Function} [fn]
+ * Specific listener to remove. Don't include to remove listeners for an event
+ * type.
+ */
+export function off(elem, type, fn) {
+ // Don't want to add a cache object through getElData if not needed
+ if (!DomData.has(elem)) {
+ return;
+ }
+
+ const data = DomData.get(elem);
+
+ // If no events exist, nothing to unbind
+ if (!data.handlers) {
+ return;
+ }
+
+ if (Array.isArray(type)) {
+ return _handleMultipleEvents(off, elem, type, fn);
+ }
+
+ // Utility function
+ const removeType = function(el, t) {
+ data.handlers[t] = [];
+ _cleanUpEvents(el, t);
+ };
+
+ // Are we removing all bound events?
+ if (type === undefined) {
+ for (const t in data.handlers) {
+ if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
+ removeType(elem, t);
+ }
+ }
+ return;
+ }
+
+ const handlers = data.handlers[type];
+
+ // If no handlers exist, nothing to unbind
+ if (!handlers) {
+ return;
+ }
+
+ // If no listener was provided, remove all listeners for type
+ if (!fn) {
+ removeType(elem, type);
+ return;
+ }
+
+ // We're only removing a single handler
+ if (fn.guid) {
+ for (let n = 0; n < handlers.length; n++) {
+ if (handlers[n].guid === fn.guid) {
+ handlers.splice(n--, 1);
+ }
+ }
+ }
+
+ _cleanUpEvents(elem, type);
+}
+
+/**
+ * Trigger an event for an element
+ *
+ * @param {Element|Object} elem
+ * Element to trigger an event on
+ *
+ * @param {EventTarget~Event|string} event
+ * A string (the type) or an event object with a type attribute
+ *
+ * @param {Object} [hash]
+ * data hash to pass along with the event
+ *
+ * @return {boolean|undefined}
+ * Returns the opposite of `defaultPrevented` if default was
+ * prevented. Otherwise, returns `undefined`
+ */
+export function trigger(elem, event, hash) {
+ // Fetches element data and a reference to the parent (for bubbling).
+ // Don't want to add a data object to cache for every parent,
+ // so checking hasElData first.
+ const elemData = DomData.has(elem) ? DomData.get(elem) : {};
+ const parent = elem.parentNode || elem.ownerDocument;
+ // type = event.type || event,
+ // handler;
+
+ // If an event name was passed as a string, creates an event out of it
+ if (typeof event === 'string') {
+ event = {type: event, target: elem};
+ } else if (!event.target) {
+ event.target = elem;
+ }
+
+ // Normalizes the event properties.
+ event = fixEvent(event);
+
+ // If the passed element has a dispatcher, executes the established handlers.
+ if (elemData.dispatcher) {
+ elemData.dispatcher.call(elem, event, hash);
+ }
+
+ // Unless explicitly stopped or the event does not bubble (e.g. media events)
+ // recursively calls this function to bubble the event up the DOM.
+ if (parent && !event.isPropagationStopped() && event.bubbles === true) {
+ trigger.call(null, parent, event, hash);
+
+ // If at the top of the DOM, triggers the default action unless disabled.
+ } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
+ if (!DomData.has(event.target)) {
+ DomData.set(event.target, {});
+ }
+ const targetData = DomData.get(event.target);
+
+ // Checks if the target has a default action for this event.
+ if (event.target[event.type]) {
+ // Temporarily disables event dispatching on the target as we have already executed the handler.
+ targetData.disabled = true;
+ // Executes the default action.
+ if (typeof event.target[event.type] === 'function') {
+ event.target[event.type]();
+ }
+ // Re-enables event dispatching.
+ targetData.disabled = false;
+ }
+ }
+
+ // Inform the triggerer if the default was prevented by returning false
+ return !event.defaultPrevented;
+}
+
+/**
+ * Trigger a listener only once for an event.
+ *
+ * @param {Element|Object} elem
+ * Element or object to bind to.
+ *
+ * @param {string|string[]} type
+ * Name/type of event
+ *
+ * @param {Event~EventListener} fn
+ * Event listener function
+ */
+export function one(elem, type, fn) {
+ if (Array.isArray(type)) {
+ return _handleMultipleEvents(one, elem, type, fn);
+ }
+ const func = function() {
+ off(elem, type, func);
+ fn.apply(this, arguments);
+ };
+
+ // copy the guid to the new function so it can removed using the original function's ID
+ func.guid = fn.guid = fn.guid || Guid.newGUID();
+ on(elem, type, func);
+}
+
+/**
+ * Trigger a listener only once and then turn if off for all
+ * configured events
+ *
+ * @param {Element|Object} elem
+ * Element or object to bind to.
+ *
+ * @param {string|string[]} type
+ * Name/type of event
+ *
+ * @param {Event~EventListener} fn
+ * Event listener function
+ */
+export function any(elem, type, fn) {
+ const func = function() {
+ off(elem, type, func);
+ fn.apply(this, arguments);
+ };
+
+ // copy the guid to the new function so it can removed using the original function's ID
+ func.guid = fn.guid = fn.guid || Guid.newGUID();
+
+ // multiple ons, but one off for everything
+ on(elem, type, func);
+}