summaryrefslogtreecommitdiff
path: root/javascript/videojs/src/js/component.js
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/videojs/src/js/component.js')
-rw-r--r--javascript/videojs/src/js/component.js2063
1 files changed, 2063 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/component.js b/javascript/videojs/src/js/component.js
new file mode 100644
index 0000000..8ba1079
--- /dev/null
+++ b/javascript/videojs/src/js/component.js
@@ -0,0 +1,2063 @@
+/**
+ * Player Component - Base class for all UI objects
+ *
+ * @file component.js
+ */
+import document from 'global/document';
+import window from 'global/window';
+import evented from './mixins/evented';
+import stateful from './mixins/stateful';
+import * as Dom from './utils/dom.js';
+import * as Fn from './utils/fn.js';
+import * as Guid from './utils/guid.js';
+import {toTitleCase, toLowerCase} from './utils/str.js';
+import {merge} from './utils/obj.js';
+
+/** @import Player from './player' */
+
+/**
+ * A callback to be called if and when the component is ready.
+ * `this` will be the Component instance.
+ *
+ * @callback ReadyCallback
+ * @returns {void}
+ */
+
+/**
+ * Base class for all UI Components.
+ * Components are UI objects which represent both a javascript object and an element
+ * in the DOM. They can be children of other components, and can have
+ * children themselves.
+ *
+ * Components can also use methods from {@link EventTarget}
+ */
+class Component {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of component options.
+ *
+ * @param {Object[]} [options.children]
+ * An array of children objects to initialize this component with. Children objects have
+ * a name property that will be used if more than one component of the same type needs to be
+ * added.
+ *
+ * @param {string} [options.className]
+ * A class or space separated list of classes to add the component
+ *
+ * @param {ReadyCallback} [ready]
+ * Function that gets called when the `Component` is ready.
+ */
+ constructor(player, options, ready) {
+
+ // The component might be the player itself and we can't pass `this` to super
+ if (!player && this.play) {
+ this.player_ = player = this; // eslint-disable-line
+ } else {
+ this.player_ = player;
+ }
+
+ this.isDisposed_ = false;
+
+ // Hold the reference to the parent component via `addChild` method
+ this.parentComponent_ = null;
+
+ // Make a copy of prototype.options_ to protect against overriding defaults
+ this.options_ = merge({}, this.options_);
+
+ // Updated options with supplied options
+ options = this.options_ = merge(this.options_, options);
+
+ // Get ID from options or options element if one is supplied
+ this.id_ = options.id || (options.el && options.el.id);
+
+ // If there was no ID from the options, generate one
+ if (!this.id_) {
+ // Don't require the player ID function in the case of mock players
+ const id = player && player.id && player.id() || 'no_player';
+
+ this.id_ = `${id}_component_${Guid.newGUID()}`;
+ }
+
+ this.name_ = options.name || null;
+
+ // Create element if one wasn't provided in options
+ if (options.el) {
+ this.el_ = options.el;
+ } else if (options.createEl !== false) {
+ this.el_ = this.createEl();
+ }
+
+ if (options.className && this.el_) {
+ options.className.split(' ').forEach(c => this.addClass(c));
+ }
+
+ // Remove the placeholder event methods. If the component is evented, the
+ // real methods are added next
+ ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
+ this[fn] = undefined;
+ });
+
+ // if evented is anything except false, we want to mixin in evented
+ if (options.evented !== false) {
+ // Make this an evented object and use `el_`, if available, as its event bus
+ evented(this, {eventBusKey: this.el_ ? 'el_' : null});
+
+ this.handleLanguagechange = this.handleLanguagechange.bind(this);
+ this.on(this.player_, 'languagechange', this.handleLanguagechange);
+ }
+ stateful(this, this.constructor.defaultState);
+
+ this.children_ = [];
+ this.childIndex_ = {};
+ this.childNameIndex_ = {};
+
+ this.setTimeoutIds_ = new Set();
+ this.setIntervalIds_ = new Set();
+ this.rafIds_ = new Set();
+ this.namedRafs_ = new Map();
+ this.clearingTimersOnDispose_ = false;
+
+ // Add any child components in options
+ if (options.initChildren !== false) {
+ this.initChildren();
+ }
+
+ // Don't want to trigger ready here or it will go before init is actually
+ // finished for all children that run this constructor
+ this.ready(ready);
+
+ if (options.reportTouchActivity !== false) {
+ this.enableTouchActivity();
+ }
+
+ }
+
+ // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
+ // They are replaced or removed in the constructor
+
+ /**
+ * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
+ * function that will get called when an event with a certain name gets triggered.
+ *
+ * @param {string|string[]} type
+ * An event name or an array of event names.
+ *
+ * @param {Function} fn
+ * The function to call with `EventTarget`s
+ */
+ /* start-delete-from-build */
+ on(type, fn) {}
+ /* end-delete-from-build */
+
+ /**
+ * Removes an `event listener` for a specific event from an instance of `EventTarget`.
+ * This makes it so that the `event listener` will no longer get called when the
+ * named event happens.
+ *
+ * @param {string|string[]} type
+ * An event name or an array of event names.
+ *
+ * @param {Function} [fn]
+ * The function to remove. If not specified, all listeners managed by Video.js will be removed.
+ */
+ /* start-delete-from-build */
+ off(type, fn) {}
+ /* end-delete-from-build */
+
+ /**
+ * This function will add an `event listener` that gets triggered only once. After the
+ * first trigger it will get removed. This is like adding an `event listener`
+ * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
+ *
+ * @param {string|string[]} type
+ * An event name or an array of event names.
+ *
+ * @param {Function} fn
+ * The function to be called once for each event name.
+ */
+ /* start-delete-from-build */
+ one(type, fn) {}
+ /* end-delete-from-build */
+
+ /**
+ * This function will add an `event listener` that gets triggered only once and is
+ * removed from all events. This is like adding an array of `event listener`s
+ * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
+ * first time it is triggered.
+ *
+ * @param {string|string[]} type
+ * An event name or an array of event names.
+ *
+ * @param {Function} fn
+ * The function to be called once for each event name.
+ */
+ /* start-delete-from-build */
+ any(type, fn) {}
+ /* end-delete-from-build */
+
+ /**
+ * This function causes an event to happen. This will then cause any `event listeners`
+ * that are waiting for that event, to get called. If there are no `event listeners`
+ * for an event then nothing will happen.
+ *
+ * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
+ * Trigger will also call the `on` + `uppercaseEventName` function.
+ *
+ * Example:
+ * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
+ * `onClick` if it exists.
+ *
+ * @param {string|Event|Object} event
+ * The name of the event, an `Event`, or an object with a key of type set to
+ * an event name.
+ *
+ * @param {Object} [hash]
+ * Optionally extra argument to pass through to an event listener
+ */
+ /* start-delete-from-build */
+ trigger(event, hash) {}
+ /* end-delete-from-build */
+
+ /**
+ * Dispose of the `Component` and all child components.
+ *
+ * @fires Component#dispose
+ *
+ * @param {Object} options
+ * @param {Element} options.originalEl element with which to replace player element
+ */
+ dispose(options = {}) {
+
+ // Bail out if the component has already been disposed.
+ if (this.isDisposed_) {
+ return;
+ }
+
+ if (this.readyQueue_) {
+ this.readyQueue_.length = 0;
+ }
+
+ /**
+ * Triggered when a `Component` is disposed.
+ *
+ * @event Component#dispose
+ * @type {Event}
+ *
+ * @property {boolean} [bubbles=false]
+ * set to false so that the dispose event does not
+ * bubble up
+ */
+ this.trigger({type: 'dispose', bubbles: false});
+
+ this.isDisposed_ = true;
+
+ // Dispose all children.
+ if (this.children_) {
+ for (let i = this.children_.length - 1; i >= 0; i--) {
+ if (this.children_[i].dispose) {
+ this.children_[i].dispose();
+ }
+ }
+ }
+
+ // Delete child references
+ this.children_ = null;
+ this.childIndex_ = null;
+ this.childNameIndex_ = null;
+
+ this.parentComponent_ = null;
+
+ if (this.el_) {
+ // Remove element from DOM
+ if (this.el_.parentNode) {
+ if (options.restoreEl) {
+ this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
+ } else {
+ this.el_.parentNode.removeChild(this.el_);
+ }
+ }
+
+ this.el_ = null;
+ }
+
+ // remove reference to the player after disposing of the element
+ this.player_ = null;
+ }
+
+ /**
+ * Determine whether or not this component has been disposed.
+ *
+ * @return {boolean}
+ * If the component has been disposed, will be `true`. Otherwise, `false`.
+ */
+ isDisposed() {
+ return Boolean(this.isDisposed_);
+ }
+
+ /**
+ * Return the {@link Player} that the `Component` has attached to.
+ *
+ * @return {Player}
+ * The player that this `Component` has attached to.
+ */
+ player() {
+ return this.player_;
+ }
+
+ /**
+ * Deep merge of options objects with new options.
+ * > Note: When both `obj` and `options` contain properties whose values are objects.
+ * The two properties get merged using {@link module:obj.merge}
+ *
+ * @param {Object} obj
+ * The object that contains new options.
+ *
+ * @return {Object}
+ * A new object of `this.options_` and `obj` merged together.
+ */
+ options(obj) {
+ if (!obj) {
+ return this.options_;
+ }
+
+ this.options_ = merge(this.options_, obj);
+ return this.options_;
+ }
+
+ /**
+ * Get the `Component`s DOM element
+ *
+ * @return {Element}
+ * The DOM element for this `Component`.
+ */
+ el() {
+ return this.el_;
+ }
+
+ /**
+ * Create the `Component`s DOM element.
+ *
+ * @param {string} [tagName]
+ * Element's DOM node type. e.g. 'div'
+ *
+ * @param {Object} [properties]
+ * An object of properties that should be set.
+ *
+ * @param {Object} [attributes]
+ * An object of attributes that should be set.
+ *
+ * @return {Element}
+ * The element that gets created.
+ */
+ createEl(tagName, properties, attributes) {
+ return Dom.createEl(tagName, properties, attributes);
+ }
+
+ /**
+ * Localize a string given the string in english.
+ *
+ * If tokens are provided, it'll try and run a simple token replacement on the provided string.
+ * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
+ *
+ * If a `defaultValue` is provided, it'll use that over `string`,
+ * if a value isn't found in provided language files.
+ * This is useful if you want to have a descriptive key for token replacement
+ * but have a succinct localized string and not require `en.json` to be included.
+ *
+ * Currently, it is used for the progress bar timing.
+ * ```js
+ * {
+ * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
+ * }
+ * ```
+ * It is then used like so:
+ * ```js
+ * this.localize('progress bar timing: currentTime={1} duration{2}',
+ * [this.player_.currentTime(), this.player_.duration()],
+ * '{1} of {2}');
+ * ```
+ *
+ * Which outputs something like: `01:23 of 24:56`.
+ *
+ *
+ * @param {string} string
+ * The string to localize and the key to lookup in the language files.
+ * @param {string[]} [tokens]
+ * If the current item has token replacements, provide the tokens here.
+ * @param {string} [defaultValue]
+ * Defaults to `string`. Can be a default value to use for token replacement
+ * if the lookup key is needed to be separate.
+ *
+ * @return {string}
+ * The localized string or if no localization exists the english string.
+ */
+ localize(string, tokens, defaultValue = string) {
+
+ const code = this.player_.language && this.player_.language();
+ const languages = this.player_.languages && this.player_.languages();
+ const language = languages && languages[code];
+ const primaryCode = code && code.split('-')[0];
+ const primaryLang = languages && languages[primaryCode];
+
+ let localizedString = defaultValue;
+
+ if (language && language[string]) {
+ localizedString = language[string];
+ } else if (primaryLang && primaryLang[string]) {
+ localizedString = primaryLang[string];
+ }
+
+ if (tokens) {
+ localizedString = localizedString.replace(/\{(\d+)\}/g, function(match, index) {
+ const value = tokens[index - 1];
+ let ret = value;
+
+ if (typeof value === 'undefined') {
+ ret = match;
+ }
+
+ return ret;
+ });
+ }
+
+ return localizedString;
+ }
+
+ /**
+ * Handles language change for the player in components. Should be overridden by sub-components.
+ *
+ * @abstract
+ */
+ handleLanguagechange() {}
+
+ /**
+ * Return the `Component`s DOM element. This is where children get inserted.
+ * This will usually be the the same as the element returned in {@link Component#el}.
+ *
+ * @return {Element}
+ * The content element for this `Component`.
+ */
+ contentEl() {
+ return this.contentEl_ || this.el_;
+ }
+
+ /**
+ * Get this `Component`s ID
+ *
+ * @return {string}
+ * The id of this `Component`
+ */
+ id() {
+ return this.id_;
+ }
+
+ /**
+ * Get the `Component`s name. The name gets used to reference the `Component`
+ * and is set during registration.
+ *
+ * @return {string}
+ * The name of this `Component`.
+ */
+ name() {
+ return this.name_;
+ }
+
+ /**
+ * Get an array of all child components
+ *
+ * @return {Array}
+ * The children
+ */
+ children() {
+ return this.children_;
+ }
+
+ /**
+ * Returns the child `Component` with the given `id`.
+ *
+ * @param {string} id
+ * The id of the child `Component` to get.
+ *
+ * @return {Component|undefined}
+ * The child `Component` with the given `id` or undefined.
+ */
+ getChildById(id) {
+ return this.childIndex_[id];
+ }
+
+ /**
+ * Returns the child `Component` with the given `name`.
+ *
+ * @param {string} name
+ * The name of the child `Component` to get.
+ *
+ * @return {Component|undefined}
+ * The child `Component` with the given `name` or undefined.
+ */
+ getChild(name) {
+ if (!name) {
+ return;
+ }
+
+ return this.childNameIndex_[name];
+ }
+
+ /**
+ * Returns the descendant `Component` following the givent
+ * descendant `names`. For instance ['foo', 'bar', 'baz'] would
+ * try to get 'foo' on the current component, 'bar' on the 'foo'
+ * component and 'baz' on the 'bar' component and return undefined
+ * if any of those don't exist.
+ *
+ * @param {...string[]|...string} names
+ * The name of the child `Component` to get.
+ *
+ * @return {Component|undefined}
+ * The descendant `Component` following the given descendant
+ * `names` or undefined.
+ */
+ getDescendant(...names) {
+ // flatten array argument into the main array
+ names = names.reduce((acc, n) => acc.concat(n), []);
+
+ let currentChild = this;
+
+ for (let i = 0; i < names.length; i++) {
+ currentChild = currentChild.getChild(names[i]);
+
+ if (!currentChild || !currentChild.getChild) {
+ return;
+ }
+ }
+
+ return currentChild;
+ }
+
+ /**
+ * Adds an SVG icon element to another element or component.
+ *
+ * @param {string} iconName
+ * The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html'
+ *
+ * @param {Element} [el=this.el()]
+ * Element to set the title on. Defaults to the current Component's element.
+ *
+ * @return {Element}
+ * The newly created icon element.
+ */
+ setIcon(iconName, el = this.el()) {
+ // TODO: In v9 of video.js, we will want to remove font icons entirely.
+ // This means this check, as well as the others throughout the code, and
+ // the unecessary CSS for font icons, will need to be removed.
+ // See https://github.com/videojs/video.js/pull/8260 as to which components
+ // need updating.
+ if (!this.player_.options_.experimentalSvgIcons) {
+ return;
+ }
+
+ const xmlnsURL = 'http://www.w3.org/2000/svg';
+
+ // The below creates an element in the format of:
+ // <span><svg><use>....</use></svg></span>
+ const iconContainer = Dom.createEl('span', {
+ className: 'vjs-icon-placeholder vjs-svg-icon'
+ }, {'aria-hidden': 'true'});
+
+ const svgEl = document.createElementNS(xmlnsURL, 'svg');
+
+ svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512');
+ const useEl = document.createElementNS(xmlnsURL, 'use');
+
+ svgEl.appendChild(useEl);
+ useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`);
+ iconContainer.appendChild(svgEl);
+
+ // Replace a pre-existing icon if one exists.
+ if (this.iconIsSet_) {
+ el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder'));
+ } else {
+ el.appendChild(iconContainer);
+ }
+
+ this.iconIsSet_ = true;
+
+ return iconContainer;
+ }
+
+ /**
+ * Add a child `Component` inside the current `Component`.
+ *
+ * @param {string|Component} child
+ * The name or instance of a child to add.
+ *
+ * @param {Object} [options={}]
+ * The key/value store of options that will get passed to children of
+ * the child.
+ *
+ * @param {number} [index=this.children_.length]
+ * The index to attempt to add a child into.
+ *
+ *
+ * @return {Component}
+ * The `Component` that gets added as a child. When using a string the
+ * `Component` will get created by this process.
+ */
+ addChild(child, options = {}, index = this.children_.length) {
+ let component;
+ let componentName;
+
+ // If child is a string, create component with options
+ if (typeof child === 'string') {
+ componentName = toTitleCase(child);
+
+ const componentClassName = options.componentClass || componentName;
+
+ // Set name through options
+ options.name = componentName;
+
+ // Create a new object & element for this controls set
+ // If there's no .player_, this is a player
+ const ComponentClass = Component.getComponent(componentClassName);
+
+ if (!ComponentClass) {
+ throw new Error(`Component ${componentClassName} does not exist`);
+ }
+
+ // data stored directly on the videojs object may be
+ // misidentified as a component to retain
+ // backwards-compatibility with 4.x. check to make sure the
+ // component class can be instantiated.
+ if (typeof ComponentClass !== 'function') {
+ return null;
+ }
+
+ component = new ComponentClass(this.player_ || this, options);
+
+ // child is a component instance
+ } else {
+ component = child;
+ }
+
+ if (component.parentComponent_) {
+ component.parentComponent_.removeChild(component);
+ }
+ this.children_.splice(index, 0, component);
+ component.parentComponent_ = this;
+
+ if (typeof component.id === 'function') {
+ this.childIndex_[component.id()] = component;
+ }
+
+ // If a name wasn't used to create the component, check if we can use the
+ // name function of the component
+ componentName = componentName || (component.name && toTitleCase(component.name()));
+
+ if (componentName) {
+ this.childNameIndex_[componentName] = component;
+ this.childNameIndex_[toLowerCase(componentName)] = component;
+ }
+
+ // Add the UI object's element to the container div (box)
+ // Having an element is not required
+ if (typeof component.el === 'function' && component.el()) {
+ // If inserting before a component, insert before that component's element
+ let refNode = null;
+
+ if (this.children_[index + 1]) {
+ // Most children are components, but the video tech is an HTML element
+ if (this.children_[index + 1].el_) {
+ refNode = this.children_[index + 1].el_;
+ } else if (Dom.isEl(this.children_[index + 1])) {
+ refNode = this.children_[index + 1];
+ }
+ }
+
+ this.contentEl().insertBefore(component.el(), refNode);
+ }
+
+ // Return so it can stored on parent object if desired.
+ return component;
+ }
+
+ /**
+ * Remove a child `Component` from this `Component`s list of children. Also removes
+ * the child `Component`s element from this `Component`s element.
+ *
+ * @param {string|Component} component
+ * The name or instance of a child to remove.
+ */
+ removeChild(component) {
+ if (typeof component === 'string') {
+ component = this.getChild(component);
+ }
+
+ if (!component || !this.children_) {
+ return;
+ }
+
+ let childFound = false;
+
+ for (let i = this.children_.length - 1; i >= 0; i--) {
+ if (this.children_[i] === component) {
+ childFound = true;
+ this.children_.splice(i, 1);
+ break;
+ }
+ }
+
+ if (!childFound) {
+ return;
+ }
+
+ component.parentComponent_ = null;
+
+ this.childIndex_[component.id()] = null;
+ this.childNameIndex_[toTitleCase(component.name())] = null;
+ this.childNameIndex_[toLowerCase(component.name())] = null;
+
+ const compEl = component.el();
+
+ if (compEl && compEl.parentNode === this.contentEl()) {
+ this.contentEl().removeChild(component.el());
+ }
+ }
+
+ /**
+ * Add and initialize default child `Component`s based upon options.
+ */
+ initChildren() {
+ const children = this.options_.children;
+
+ if (children) {
+ // `this` is `parent`
+ const parentOptions = this.options_;
+
+ const handleAdd = (child) => {
+ const name = child.name;
+ let opts = child.opts;
+
+ // Allow options for children to be set at the parent options
+ // e.g. videojs(id, { controlBar: false });
+ // instead of videojs(id, { children: { controlBar: false });
+ if (parentOptions[name] !== undefined) {
+ opts = parentOptions[name];
+ }
+
+ // Allow for disabling default components
+ // e.g. options['children']['posterImage'] = false
+ if (opts === false) {
+ return;
+ }
+
+ // Allow options to be passed as a simple boolean if no configuration
+ // is necessary.
+ if (opts === true) {
+ opts = {};
+ }
+
+ // We also want to pass the original player options
+ // to each component as well so they don't need to
+ // reach back into the player for options later.
+ opts.playerOptions = this.options_.playerOptions;
+
+ // Create and add the child component.
+ // Add a direct reference to the child by name on the parent instance.
+ // If two of the same component are used, different names should be supplied
+ // for each
+ const newChild = this.addChild(name, opts);
+
+ if (newChild) {
+ this[name] = newChild;
+ }
+ };
+
+ // Allow for an array of children details to passed in the options
+ let workingChildren;
+ const Tech = Component.getComponent('Tech');
+
+ if (Array.isArray(children)) {
+ workingChildren = children;
+ } else {
+ workingChildren = Object.keys(children);
+ }
+
+ workingChildren
+ // children that are in this.options_ but also in workingChildren would
+ // give us extra children we do not want. So, we want to filter them out.
+ .concat(Object.keys(this.options_)
+ .filter(function(child) {
+ return !workingChildren.some(function(wchild) {
+ if (typeof wchild === 'string') {
+ return child === wchild;
+ }
+ return child === wchild.name;
+ });
+ }))
+ .map((child) => {
+ let name;
+ let opts;
+
+ if (typeof child === 'string') {
+ name = child;
+ opts = children[name] || this.options_[name] || {};
+ } else {
+ name = child.name;
+ opts = child;
+ }
+
+ return {name, opts};
+ })
+ .filter((child) => {
+ // we have to make sure that child.name isn't in the techOrder since
+ // techs are registered as Components but can't aren't compatible
+ // See https://github.com/videojs/video.js/issues/2772
+ const c = Component.getComponent(child.opts.componentClass ||
+ toTitleCase(child.name));
+
+ return c && !Tech.isTech(c);
+ })
+ .forEach(handleAdd);
+ }
+ }
+
+ /**
+ * Builds the default DOM class name. Should be overridden by sub-components.
+ *
+ * @return {string}
+ * The DOM class name for this object.
+ *
+ * @abstract
+ */
+ buildCSSClass() {
+ // Child classes can include a function that does:
+ // return 'CLASS NAME' + this._super();
+ return '';
+ }
+
+ /**
+ * Bind a listener to the component's ready state.
+ * Different from event listeners in that if the ready event has already happened
+ * it will trigger the function immediately.
+ *
+ * @param {ReadyCallback} fn
+ * Function that gets called when the `Component` is ready.
+ */
+ ready(fn, sync = false) {
+ if (!fn) {
+ return;
+ }
+
+ if (!this.isReady_) {
+ this.readyQueue_ = this.readyQueue_ || [];
+ this.readyQueue_.push(fn);
+ return;
+ }
+
+ if (sync) {
+ fn.call(this);
+ } else {
+ // Call the function asynchronously by default for consistency
+ this.setTimeout(fn, 1);
+ }
+ }
+
+ /**
+ * Trigger all the ready listeners for this `Component`.
+ *
+ * @fires Component#ready
+ */
+ triggerReady() {
+ this.isReady_ = true;
+
+ // Ensure ready is triggered asynchronously
+ this.setTimeout(function() {
+ const readyQueue = this.readyQueue_;
+
+ // Reset Ready Queue
+ this.readyQueue_ = [];
+
+ if (readyQueue && readyQueue.length > 0) {
+ readyQueue.forEach(function(fn) {
+ fn.call(this);
+ }, this);
+ }
+
+ // Allow for using event listeners also
+ /**
+ * Triggered when a `Component` is ready.
+ *
+ * @event Component#ready
+ * @type {Event}
+ */
+ this.trigger('ready');
+ }, 1);
+ }
+
+ /**
+ * Find a single DOM element matching a `selector`. This can be within the `Component`s
+ * `contentEl()` or another custom context.
+ *
+ * @param {string} selector
+ * A valid CSS selector, which will be passed to `querySelector`.
+ *
+ * @param {Element|string} [context=this.contentEl()]
+ * A DOM element within which to query. Can also be a selector string in
+ * which case the first matching element will get used as context. If
+ * missing `this.contentEl()` gets used. If `this.contentEl()` returns
+ * nothing it falls back to `document`.
+ *
+ * @return {Element|null}
+ * the dom element that was found, or null
+ *
+ * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
+ */
+ $(selector, context) {
+ return Dom.$(selector, context || this.contentEl());
+ }
+
+ /**
+ * Finds all DOM element matching a `selector`. This can be within the `Component`s
+ * `contentEl()` or another custom context.
+ *
+ * @param {string} selector
+ * A valid CSS selector, which will be passed to `querySelectorAll`.
+ *
+ * @param {Element|string} [context=this.contentEl()]
+ * A DOM element within which to query. Can also be a selector string in
+ * which case the first matching element will get used as context. If
+ * missing `this.contentEl()` gets used. If `this.contentEl()` returns
+ * nothing it falls back to `document`.
+ *
+ * @return {NodeList}
+ * a list of dom elements that were found
+ *
+ * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
+ */
+ $$(selector, context) {
+ return Dom.$$(selector, context || this.contentEl());
+ }
+
+ /**
+ * Check if a component's element has a CSS class name.
+ *
+ * @param {string} classToCheck
+ * CSS class name to check.
+ *
+ * @return {boolean}
+ * - True if the `Component` has the class.
+ * - False if the `Component` does not have the class`
+ */
+ hasClass(classToCheck) {
+ return Dom.hasClass(this.el_, classToCheck);
+ }
+
+ /**
+ * Add a CSS class name to the `Component`s element.
+ *
+ * @param {...string} classesToAdd
+ * One or more CSS class name to add.
+ */
+ addClass(...classesToAdd) {
+ Dom.addClass(this.el_, ...classesToAdd);
+ }
+
+ /**
+ * Remove a CSS class name from the `Component`s element.
+ *
+ * @param {...string} classesToRemove
+ * One or more CSS class name to remove.
+ */
+ removeClass(...classesToRemove) {
+ Dom.removeClass(this.el_, ...classesToRemove);
+ }
+
+ /**
+ * Add or remove a CSS class name from the component's element.
+ * - `classToToggle` gets added when {@link Component#hasClass} would return false.
+ * - `classToToggle` gets removed when {@link Component#hasClass} would return true.
+ *
+ * @param {string} classToToggle
+ * The class to add or remove. Passed to DOMTokenList's toggle()
+ *
+ * @param {boolean|Dom.PredicateCallback} [predicate]
+ * A boolean or function that returns a boolean. Passed to DOMTokenList's toggle().
+ */
+ toggleClass(classToToggle, predicate) {
+ Dom.toggleClass(this.el_, classToToggle, predicate);
+ }
+
+ /**
+ * Show the `Component`s element if it is hidden by removing the
+ * 'vjs-hidden' class name from it.
+ */
+ show() {
+ this.removeClass('vjs-hidden');
+ }
+
+ /**
+ * Hide the `Component`s element if it is currently showing by adding the
+ * 'vjs-hidden` class name to it.
+ */
+ hide() {
+ this.addClass('vjs-hidden');
+ }
+
+ /**
+ * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
+ * class name to it. Used during fadeIn/fadeOut.
+ *
+ * @private
+ */
+ lockShowing() {
+ this.addClass('vjs-lock-showing');
+ }
+
+ /**
+ * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
+ * class name from it. Used during fadeIn/fadeOut.
+ *
+ * @private
+ */
+ unlockShowing() {
+ this.removeClass('vjs-lock-showing');
+ }
+
+ /**
+ * Get the value of an attribute on the `Component`s element.
+ *
+ * @param {string} attribute
+ * Name of the attribute to get the value from.
+ *
+ * @return {string|null}
+ * - The value of the attribute that was asked for.
+ * - Can be an empty string on some browsers if the attribute does not exist
+ * or has no value
+ * - Most browsers will return null if the attribute does not exist or has
+ * no value.
+ *
+ * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
+ */
+ getAttribute(attribute) {
+ return Dom.getAttribute(this.el_, attribute);
+ }
+
+ /**
+ * Set the value of an attribute on the `Component`'s element
+ *
+ * @param {string} attribute
+ * Name of the attribute to set.
+ *
+ * @param {string} value
+ * Value to set the attribute to.
+ *
+ * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
+ */
+ setAttribute(attribute, value) {
+ Dom.setAttribute(this.el_, attribute, value);
+ }
+
+ /**
+ * Remove an attribute from the `Component`s element.
+ *
+ * @param {string} attribute
+ * Name of the attribute to remove.
+ *
+ * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
+ */
+ removeAttribute(attribute) {
+ Dom.removeAttribute(this.el_, attribute);
+ }
+
+ /**
+ * Get or set the width of the component based upon the CSS styles.
+ * See {@link Component#dimension} for more detailed information.
+ *
+ * @param {number|string} [num]
+ * The width that you want to set postfixed with '%', 'px' or nothing.
+ *
+ * @param {boolean} [skipListeners]
+ * Skip the componentresize event trigger
+ *
+ * @return {number|undefined}
+ * The width when getting, zero if there is no width
+ */
+ width(num, skipListeners) {
+ return this.dimension('width', num, skipListeners);
+ }
+
+ /**
+ * Get or set the height of the component based upon the CSS styles.
+ * See {@link Component#dimension} for more detailed information.
+ *
+ * @param {number|string} [num]
+ * The height that you want to set postfixed with '%', 'px' or nothing.
+ *
+ * @param {boolean} [skipListeners]
+ * Skip the componentresize event trigger
+ *
+ * @return {number|undefined}
+ * The height when getting, zero if there is no height
+ */
+ height(num, skipListeners) {
+ return this.dimension('height', num, skipListeners);
+ }
+
+ /**
+ * Set both the width and height of the `Component` element at the same time.
+ *
+ * @param {number|string} width
+ * Width to set the `Component`s element to.
+ *
+ * @param {number|string} height
+ * Height to set the `Component`s element to.
+ */
+ dimensions(width, height) {
+ // Skip componentresize listeners on width for optimization
+ this.width(width, true);
+ this.height(height);
+ }
+
+ /**
+ * Get or set width or height of the `Component` element. This is the shared code
+ * for the {@link Component#width} and {@link Component#height}.
+ *
+ * Things to know:
+ * - If the width or height in an number this will return the number postfixed with 'px'.
+ * - If the width/height is a percent this will return the percent postfixed with '%'
+ * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
+ * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
+ * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
+ * for more information
+ * - If you want the computed style of the component, use {@link Component#currentWidth}
+ * and {@link {Component#currentHeight}
+ *
+ * @fires Component#componentresize
+ *
+ * @param {string} widthOrHeight
+ 8 'width' or 'height'
+ *
+ * @param {number|string} [num]
+ 8 New dimension
+ *
+ * @param {boolean} [skipListeners]
+ * Skip componentresize event trigger
+ *
+ * @return {number|undefined}
+ * The dimension when getting or 0 if unset
+ */
+ dimension(widthOrHeight, num, skipListeners) {
+ if (num !== undefined) {
+ // Set to zero if null or literally NaN (NaN !== NaN)
+ if (num === null || num !== num) {
+ num = 0;
+ }
+
+ // Check if using css width/height (% or px) and adjust
+ if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
+ this.el_.style[widthOrHeight] = num;
+ } else if (num === 'auto') {
+ this.el_.style[widthOrHeight] = '';
+ } else {
+ this.el_.style[widthOrHeight] = num + 'px';
+ }
+
+ // skipListeners allows us to avoid triggering the resize event when setting both width and height
+ if (!skipListeners) {
+ /**
+ * Triggered when a component is resized.
+ *
+ * @event Component#componentresize
+ * @type {Event}
+ */
+ this.trigger('componentresize');
+ }
+
+ return;
+ }
+
+ // Not setting a value, so getting it
+ // Make sure element exists
+ if (!this.el_) {
+ return 0;
+ }
+
+ // Get dimension value from style
+ const val = this.el_.style[widthOrHeight];
+ const pxIndex = val.indexOf('px');
+
+ if (pxIndex !== -1) {
+ // Return the pixel value with no 'px'
+ return parseInt(val.slice(0, pxIndex), 10);
+ }
+
+ // No px so using % or no style was set, so falling back to offsetWidth/height
+ // If component has display:none, offset will return 0
+ // TODO: handle display:none and no dimension style using px
+ return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
+ }
+
+ /**
+ * Get the computed width or the height of the component's element.
+ *
+ * Uses `window.getComputedStyle`.
+ *
+ * @param {string} widthOrHeight
+ * A string containing 'width' or 'height'. Whichever one you want to get.
+ *
+ * @return {number}
+ * The dimension that gets asked for or 0 if nothing was set
+ * for that dimension.
+ */
+ currentDimension(widthOrHeight) {
+ let computedWidthOrHeight = 0;
+
+ if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
+ throw new Error('currentDimension only accepts width or height value');
+ }
+
+ computedWidthOrHeight = Dom.computedStyle(this.el_, widthOrHeight);
+
+ // remove 'px' from variable and parse as integer
+ computedWidthOrHeight = parseFloat(computedWidthOrHeight);
+
+ // if the computed value is still 0, it's possible that the browser is lying
+ // and we want to check the offset values.
+ // This code also runs wherever getComputedStyle doesn't exist.
+ if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
+ const rule = `offset${toTitleCase(widthOrHeight)}`;
+
+ computedWidthOrHeight = this.el_[rule];
+ }
+
+ return computedWidthOrHeight;
+ }
+
+ /**
+ * An object that contains width and height values of the `Component`s
+ * computed style. Uses `window.getComputedStyle`.
+ *
+ * @typedef {Object} Component~DimensionObject
+ *
+ * @property {number} width
+ * The width of the `Component`s computed style.
+ *
+ * @property {number} height
+ * The height of the `Component`s computed style.
+ */
+
+ /**
+ * Get an object that contains computed width and height values of the
+ * component's element.
+ *
+ * Uses `window.getComputedStyle`.
+ *
+ * @return {Component~DimensionObject}
+ * The computed dimensions of the component's element.
+ */
+ currentDimensions() {
+ return {
+ width: this.currentDimension('width'),
+ height: this.currentDimension('height')
+ };
+ }
+
+ /**
+ * Get the computed width of the component's element.
+ *
+ * Uses `window.getComputedStyle`.
+ *
+ * @return {number}
+ * The computed width of the component's element.
+ */
+ currentWidth() {
+ return this.currentDimension('width');
+ }
+
+ /**
+ * Get the computed height of the component's element.
+ *
+ * Uses `window.getComputedStyle`.
+ *
+ * @return {number}
+ * The computed height of the component's element.
+ */
+ currentHeight() {
+ return this.currentDimension('height');
+ }
+
+ /**
+ * Retrieves the position and size information of the component's element.
+ *
+ * @return {Object} An object with `boundingClientRect` and `center` properties.
+ * - `boundingClientRect`: An object with properties `x`, `y`, `width`,
+ * `height`, `top`, `right`, `bottom`, and `left`, representing
+ * the bounding rectangle of the element.
+ * - `center`: An object with properties `x` and `y`, representing
+ * the center point of the element. `width` and `height` are set to 0.
+ */
+ getPositions() {
+ const rect = this.el_.getBoundingClientRect();
+
+ // Creating objects that mirror DOMRectReadOnly for boundingClientRect and center
+ const boundingClientRect = {
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height,
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left
+ };
+
+ // Calculating the center position
+ const center = {
+ x: rect.left + rect.width / 2,
+ y: rect.top + rect.height / 2,
+ width: 0,
+ height: 0,
+ top: rect.top + rect.height / 2,
+ right: rect.left + rect.width / 2,
+ bottom: rect.top + rect.height / 2,
+ left: rect.left + rect.width / 2
+ };
+
+ return {
+ boundingClientRect,
+ center
+ };
+ }
+
+ /**
+ * Set the focus to this component
+ */
+ focus() {
+ this.el_.focus();
+ }
+
+ /**
+ * Remove the focus from this component
+ */
+ blur() {
+ this.el_.blur();
+ }
+
+ /**
+ * When this Component receives a `keydown` event which it does not process,
+ * it passes the event to the Player for handling.
+ *
+ * @param {KeyboardEvent} event
+ * The `keydown` event that caused this function to be called.
+ */
+ handleKeyDown(event) {
+ if (this.player_) {
+
+ // We only stop propagation here because we want unhandled events to fall
+ // back to the browser. Exclude Tab for focus trapping, exclude also when spatialNavigation is enabled.
+ if (event.key !== 'Tab' && !(this.player_.options_.playerOptions.spatialNavigation && this.player_.options_.playerOptions.spatialNavigation.enabled)) {
+ event.stopPropagation();
+ }
+ this.player_.handleKeyDown(event);
+ }
+ }
+
+ /**
+ * Many components used to have a `handleKeyPress` method, which was poorly
+ * named because it listened to a `keydown` event. This method name now
+ * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
+ * will not see their method calls stop working.
+ *
+ * @param {KeyboardEvent} event
+ * The event that caused this function to be called.
+ */
+ handleKeyPress(event) {
+ this.handleKeyDown(event);
+ }
+
+ /**
+ * Emit a 'tap' events when touch event support gets detected. This gets used to
+ * support toggling the controls through a tap on the video. They get enabled
+ * because every sub-component would have extra overhead otherwise.
+ *
+ * @protected
+ * @fires Component#tap
+ * @listens Component#touchstart
+ * @listens Component#touchmove
+ * @listens Component#touchleave
+ * @listens Component#touchcancel
+ * @listens Component#touchend
+
+ */
+ emitTapEvents() {
+ // Track the start time so we can determine how long the touch lasted
+ let touchStart = 0;
+ let firstTouch = null;
+
+ // Maximum movement allowed during a touch event to still be considered a tap
+ // Other popular libs use anywhere from 2 (hammer.js) to 15,
+ // so 10 seems like a nice, round number.
+ const tapMovementThreshold = 10;
+
+ // The maximum length a touch can be while still being considered a tap
+ const touchTimeThreshold = 200;
+
+ let couldBeTap;
+
+ this.on('touchstart', function(event) {
+ // If more than one finger, don't consider treating this as a click
+ if (event.touches.length === 1) {
+ // Copy pageX/pageY from the object
+ firstTouch = {
+ pageX: event.touches[0].pageX,
+ pageY: event.touches[0].pageY
+ };
+ // Record start time so we can detect a tap vs. "touch and hold"
+ touchStart = window.performance.now();
+ // Reset couldBeTap tracking
+ couldBeTap = true;
+ }
+ });
+
+ this.on('touchmove', function(event) {
+ // If more than one finger, don't consider treating this as a click
+ if (event.touches.length > 1) {
+ couldBeTap = false;
+ } else if (firstTouch) {
+ // Some devices will throw touchmoves for all but the slightest of taps.
+ // So, if we moved only a small distance, this could still be a tap
+ const xdiff = event.touches[0].pageX - firstTouch.pageX;
+ const ydiff = event.touches[0].pageY - firstTouch.pageY;
+ const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
+
+ if (touchDistance > tapMovementThreshold) {
+ couldBeTap = false;
+ }
+ }
+ });
+
+ const noTap = function() {
+ couldBeTap = false;
+ };
+
+ // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
+ this.on('touchleave', noTap);
+ this.on('touchcancel', noTap);
+
+ // When the touch ends, measure how long it took and trigger the appropriate
+ // event
+ this.on('touchend', function(event) {
+ firstTouch = null;
+ // Proceed only if the touchmove/leave/cancel event didn't happen
+ if (couldBeTap === true) {
+ // Measure how long the touch lasted
+ const touchTime = window.performance.now() - touchStart;
+
+ // Make sure the touch was less than the threshold to be considered a tap
+ if (touchTime < touchTimeThreshold) {
+ // Don't let browser turn this into a click
+ event.preventDefault();
+ /**
+ * Triggered when a `Component` is tapped.
+ *
+ * @event Component#tap
+ * @type {MouseEvent}
+ */
+ this.trigger('tap');
+ // It may be good to copy the touchend event object and change the
+ // type to tap, if the other event properties aren't exact after
+ // Events.fixEvent runs (e.g. event.target)
+ }
+ }
+ });
+ }
+
+ /**
+ * This function reports user activity whenever touch events happen. This can get
+ * turned off by any sub-components that wants touch events to act another way.
+ *
+ * Report user touch activity when touch events occur. User activity gets used to
+ * determine when controls should show/hide. It is simple when it comes to mouse
+ * events, because any mouse event should show the controls. So we capture mouse
+ * events that bubble up to the player and report activity when that happens.
+ * With touch events it isn't as easy as `touchstart` and `touchend` toggle player
+ * controls. So touch events can't help us at the player level either.
+ *
+ * User activity gets checked asynchronously. So what could happen is a tap event
+ * on the video turns the controls off. Then the `touchend` event bubbles up to
+ * the player. Which, if it reported user activity, would turn the controls right
+ * back on. We also don't want to completely block touch events from bubbling up.
+ * Furthermore a `touchmove` event and anything other than a tap, should not turn
+ * controls back on.
+ *
+ * @listens Component#touchstart
+ * @listens Component#touchmove
+ * @listens Component#touchend
+ * @listens Component#touchcancel
+ */
+ enableTouchActivity() {
+ // Don't continue if the root player doesn't support reporting user activity
+ if (!this.player() || !this.player().reportUserActivity) {
+ return;
+ }
+
+ // listener for reporting that the user is active
+ const report = Fn.bind_(this.player(), this.player().reportUserActivity);
+
+ let touchHolding;
+
+ this.on('touchstart', function() {
+ report();
+ // For as long as the they are touching the device or have their mouse down,
+ // we consider them active even if they're not moving their finger or mouse.
+ // So we want to continue to update that they are active
+ this.clearInterval(touchHolding);
+ // report at the same interval as activityCheck
+ touchHolding = this.setInterval(report, 250);
+ });
+
+ const touchEnd = function(event) {
+ report();
+ // stop the interval that maintains activity if the touch is holding
+ this.clearInterval(touchHolding);
+ };
+
+ this.on('touchmove', report);
+ this.on('touchend', touchEnd);
+ this.on('touchcancel', touchEnd);
+ }
+
+ /**
+ * A callback that has no parameters and is bound into `Component`s context.
+ *
+ * @callback Component~GenericCallback
+ * @this Component
+ */
+
+ /**
+ * Creates a function that runs after an `x` millisecond timeout. This function is a
+ * wrapper around `window.setTimeout`. There are a few reasons to use this one
+ * instead though:
+ * 1. It gets cleared via {@link Component#clearTimeout} when
+ * {@link Component#dispose} gets called.
+ * 2. The function callback will gets turned into a {@link Component~GenericCallback}
+ *
+ * > Note: You can't use `window.clearTimeout` on the id returned by this function. This
+ * will cause its dispose listener not to get cleaned up! Please use
+ * {@link Component#clearTimeout} or {@link Component#dispose} instead.
+ *
+ * @param {Component~GenericCallback} fn
+ * The function that will be run after `timeout`.
+ *
+ * @param {number} timeout
+ * Timeout in milliseconds to delay before executing the specified function.
+ *
+ * @return {number}
+ * Returns a timeout ID that gets used to identify the timeout. It can also
+ * get used in {@link Component#clearTimeout} to clear the timeout that
+ * was set.
+ *
+ * @listens Component#dispose
+ * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
+ */
+ setTimeout(fn, timeout) {
+ // declare as variables so they are properly available in timeout function
+ // eslint-disable-next-line
+ var timeoutId, disposeFn;
+
+ fn = Fn.bind_(this, fn);
+
+ this.clearTimersOnDispose_();
+
+ timeoutId = window.setTimeout(() => {
+ if (this.setTimeoutIds_.has(timeoutId)) {
+ this.setTimeoutIds_.delete(timeoutId);
+ }
+ fn();
+ }, timeout);
+
+ this.setTimeoutIds_.add(timeoutId);
+
+ return timeoutId;
+ }
+
+ /**
+ * Clears a timeout that gets created via `window.setTimeout` or
+ * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
+ * use this function instead of `window.clearTimout`. If you don't your dispose
+ * listener will not get cleaned up until {@link Component#dispose}!
+ *
+ * @param {number} timeoutId
+ * The id of the timeout to clear. The return value of
+ * {@link Component#setTimeout} or `window.setTimeout`.
+ *
+ * @return {number}
+ * Returns the timeout id that was cleared.
+ *
+ * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
+ */
+ clearTimeout(timeoutId) {
+ if (this.setTimeoutIds_.has(timeoutId)) {
+ this.setTimeoutIds_.delete(timeoutId);
+ window.clearTimeout(timeoutId);
+ }
+
+ return timeoutId;
+ }
+
+ /**
+ * Creates a function that gets run every `x` milliseconds. This function is a wrapper
+ * around `window.setInterval`. There are a few reasons to use this one instead though.
+ * 1. It gets cleared via {@link Component#clearInterval} when
+ * {@link Component#dispose} gets called.
+ * 2. The function callback will be a {@link Component~GenericCallback}
+ *
+ * @param {Component~GenericCallback} fn
+ * The function to run every `x` seconds.
+ *
+ * @param {number} interval
+ * Execute the specified function every `x` milliseconds.
+ *
+ * @return {number}
+ * Returns an id that can be used to identify the interval. It can also be be used in
+ * {@link Component#clearInterval} to clear the interval.
+ *
+ * @listens Component#dispose
+ * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
+ */
+ setInterval(fn, interval) {
+ fn = Fn.bind_(this, fn);
+
+ this.clearTimersOnDispose_();
+
+ const intervalId = window.setInterval(fn, interval);
+
+ this.setIntervalIds_.add(intervalId);
+
+ return intervalId;
+ }
+
+ /**
+ * Clears an interval that gets created via `window.setInterval` or
+ * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
+ * use this function instead of `window.clearInterval`. If you don't your dispose
+ * listener will not get cleaned up until {@link Component#dispose}!
+ *
+ * @param {number} intervalId
+ * The id of the interval to clear. The return value of
+ * {@link Component#setInterval} or `window.setInterval`.
+ *
+ * @return {number}
+ * Returns the interval id that was cleared.
+ *
+ * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
+ */
+ clearInterval(intervalId) {
+ if (this.setIntervalIds_.has(intervalId)) {
+ this.setIntervalIds_.delete(intervalId);
+ window.clearInterval(intervalId);
+ }
+
+ return intervalId;
+ }
+
+ /**
+ * Queues up a callback to be passed to requestAnimationFrame (rAF), but
+ * with a few extra bonuses:
+ *
+ * - Supports browsers that do not support rAF by falling back to
+ * {@link Component#setTimeout}.
+ *
+ * - The callback is turned into a {@link Component~GenericCallback} (i.e.
+ * bound to the component).
+ *
+ * - Automatic cancellation of the rAF callback is handled if the component
+ * is disposed before it is called.
+ *
+ * @param {Component~GenericCallback} fn
+ * A function that will be bound to this component and executed just
+ * before the browser's next repaint.
+ *
+ * @return {number}
+ * Returns an rAF ID that gets used to identify the timeout. It can
+ * also be used in {@link Component#cancelAnimationFrame} to cancel
+ * the animation frame callback.
+ *
+ * @listens Component#dispose
+ * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
+ */
+ requestAnimationFrame(fn) {
+ this.clearTimersOnDispose_();
+
+ // declare as variables so they are properly available in rAF function
+ // eslint-disable-next-line
+ var id;
+ fn = Fn.bind_(this, fn);
+
+ id = window.requestAnimationFrame(() => {
+ if (this.rafIds_.has(id)) {
+ this.rafIds_.delete(id);
+ }
+ fn();
+ });
+ this.rafIds_.add(id);
+
+ return id;
+ }
+
+ /**
+ * Request an animation frame, but only one named animation
+ * frame will be queued. Another will never be added until
+ * the previous one finishes.
+ *
+ * @param {string} name
+ * The name to give this requestAnimationFrame
+ *
+ * @param {Component~GenericCallback} fn
+ * A function that will be bound to this component and executed just
+ * before the browser's next repaint.
+ */
+ requestNamedAnimationFrame(name, fn) {
+ if (this.namedRafs_.has(name)) {
+ this.cancelNamedAnimationFrame(name);
+ }
+ this.clearTimersOnDispose_();
+
+ fn = Fn.bind_(this, fn);
+
+ const id = this.requestAnimationFrame(() => {
+ fn();
+ if (this.namedRafs_.has(name)) {
+ this.namedRafs_.delete(name);
+ }
+ });
+
+ this.namedRafs_.set(name, id);
+
+ return name;
+ }
+
+ /**
+ * Cancels a current named animation frame if it exists.
+ *
+ * @param {string} name
+ * The name of the requestAnimationFrame to cancel.
+ */
+ cancelNamedAnimationFrame(name) {
+ if (!this.namedRafs_.has(name)) {
+ return;
+ }
+
+ this.cancelAnimationFrame(this.namedRafs_.get(name));
+ this.namedRafs_.delete(name);
+ }
+
+ /**
+ * Cancels a queued callback passed to {@link Component#requestAnimationFrame}
+ * (rAF).
+ *
+ * If you queue an rAF callback via {@link Component#requestAnimationFrame},
+ * use this function instead of `window.cancelAnimationFrame`. If you don't,
+ * your dispose listener will not get cleaned up until {@link Component#dispose}!
+ *
+ * @param {number} id
+ * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
+ *
+ * @return {number}
+ * Returns the rAF ID that was cleared.
+ *
+ * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
+ */
+ cancelAnimationFrame(id) {
+ if (this.rafIds_.has(id)) {
+ this.rafIds_.delete(id);
+ window.cancelAnimationFrame(id);
+ }
+
+ return id;
+
+ }
+
+ /**
+ * A function to setup `requestAnimationFrame`, `setTimeout`,
+ * and `setInterval`, clearing on dispose.
+ *
+ * > Previously each timer added and removed dispose listeners on it's own.
+ * For better performance it was decided to batch them all, and use `Set`s
+ * to track outstanding timer ids.
+ *
+ * @private
+ */
+ clearTimersOnDispose_() {
+ if (this.clearingTimersOnDispose_) {
+ return;
+ }
+
+ this.clearingTimersOnDispose_ = true;
+ this.one('dispose', () => {
+ [
+ ['namedRafs_', 'cancelNamedAnimationFrame'],
+ ['rafIds_', 'cancelAnimationFrame'],
+ ['setTimeoutIds_', 'clearTimeout'],
+ ['setIntervalIds_', 'clearInterval']
+ ].forEach(([idName, cancelName]) => {
+ // for a `Set` key will actually be the value again
+ // so forEach((val, val) =>` but for maps we want to use
+ // the key.
+ this[idName].forEach((val, key) => this[cancelName](key));
+ });
+
+ this.clearingTimersOnDispose_ = false;
+ });
+ }
+
+ /**
+ * Decide whether an element is actually disabled or not.
+ *
+ * @function isActuallyDisabled
+ * @param element {Node}
+ * @return {boolean}
+ *
+ * @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled}
+ */
+ getIsDisabled() {
+ return Boolean(this.el_.disabled);
+ }
+
+ /**
+ * Decide whether the element is expressly inert or not.
+ *
+ * @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert}
+ * @function isExpresslyInert
+ * @param element {Node}
+ * @return {boolean}
+ */
+ getIsExpresslyInert() {
+ return this.el_.inert && !this.el_.ownerDocument.documentElement.inert;
+ }
+
+ /**
+ * Determine whether or not this component can be considered as focusable component.
+ *
+ * @param {HTMLElement} el - The HTML element representing the component.
+ * @return {boolean}
+ * If the component can be focused, will be `true`. Otherwise, `false`.
+ */
+ getIsFocusable(el) {
+ const element = el || this.el_;
+
+ return element.tabIndex >= 0 && !(this.getIsDisabled() || this.getIsExpresslyInert());
+ }
+
+ /**
+ * Determine whether or not this component is currently visible/enabled/etc...
+ *
+ * @param {HTMLElement} el - The HTML element representing the component.
+ * @return {boolean}
+ * If the component can is currently visible & enabled, will be `true`. Otherwise, `false`.
+ */
+ getIsAvailableToBeFocused(el) {
+ /**
+ * Decide the style property of this element is specified whether it's visible or not.
+ *
+ * @function isVisibleStyleProperty
+ * @param element {CSSStyleDeclaration}
+ * @return {boolean}
+ */
+ function isVisibleStyleProperty(element) {
+ const elementStyle = window.getComputedStyle(element, null);
+ const thisVisibility = elementStyle.getPropertyValue('visibility');
+ const thisDisplay = elementStyle.getPropertyValue('display');
+ const invisibleStyle = ['hidden', 'collapse'];
+
+ return (thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility));
+ }
+
+ /**
+ * Decide whether the element is being rendered or not.
+ * 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered.
+ * 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible).
+ * 3. If width and height of an element are explicitly set to 0, it is not being rendered.
+ * 4. If a parent element is hidden, an element itself is not being rendered.
+ * (CSS visibility property and display property are inherited.)
+ *
+ * @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered}
+ * @function isBeingRendered
+ * @param element {Node}
+ * @return {boolean}
+ */
+ function isBeingRendered(element) {
+ if (!isVisibleStyleProperty(element.parentElement)) {
+ return false;
+ }
+ if (!isVisibleStyleProperty(element) || (element.style.opacity === '0') || (window.getComputedStyle(element).height === '0px' || window.getComputedStyle(element).width === '0px')) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Determine if the element is visible for the user or not.
+ * 1. If an element sum of its offsetWidth, offsetHeight, height and width is less than 1 is not visible.
+ * 2. If elementCenter.x is less than is not visible.
+ * 3. If elementCenter.x is more than the document's width is not visible.
+ * 4. If elementCenter.y is less than 0 is not visible.
+ * 5. If elementCenter.y is the document's height is not visible.
+ *
+ * @function isVisible
+ * @param element {Node}
+ * @return {boolean}
+ */
+ function isVisible(element) {
+ if ((element.offsetWidth + element.offsetHeight + element.getBoundingClientRect().height + element.getBoundingClientRect().width) === 0) {
+ return false;
+ }
+
+ // Define elementCenter object with props of x and y
+ // x: Left position relative to the viewport plus element's width (no margin) divided between 2.
+ // y: Top position relative to the viewport plus element's height (no margin) divided between 2.
+ const elementCenter = {
+ x: element.getBoundingClientRect().left + element.offsetWidth / 2,
+ y: element.getBoundingClientRect().top + element.offsetHeight / 2
+ };
+
+ if (elementCenter.x < 0) {
+ return false;
+ }
+ if (elementCenter.x > (document.documentElement.clientWidth || window.innerWidth)) {
+ return false;
+ }
+ if (elementCenter.y < 0) {
+ return false;
+ }
+ if (elementCenter.y > (document.documentElement.clientHeight || window.innerHeight)) {
+ return false;
+ }
+
+ let pointContainer = document.elementFromPoint(elementCenter.x, elementCenter.y);
+
+ while (pointContainer) {
+ if (pointContainer === element) {
+ return true;
+ }
+ if (pointContainer.parentNode) {
+ pointContainer = pointContainer.parentNode;
+ } else {
+ return false;
+ }
+
+ }
+ }
+
+ // If no DOM element was passed as argument use this component's element.
+ if (!el) {
+ el = this.el();
+ }
+
+ // If element is visible, is being rendered & either does not have a parent element or its tabIndex is not negative.
+ if (isVisible(el) && isBeingRendered(el) && ((!el.parentElement) || (el.tabIndex >= 0))) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Register a `Component` with `videojs` given the name and the component.
+ *
+ * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
+ * should be registered using {@link Tech.registerTech} or
+ * {@link videojs:videojs.registerTech}.
+ *
+ * > NOTE: This function can also be seen on videojs as
+ * {@link videojs:videojs.registerComponent}.
+ *
+ * @param {string} name
+ * The name of the `Component` to register.
+ *
+ * @param {Component} ComponentToRegister
+ * The `Component` class to register.
+ *
+ * @return {Component}
+ * The `Component` that was registered.
+ */
+ static registerComponent(name, ComponentToRegister) {
+ if (typeof name !== 'string' || !name) {
+ throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
+ }
+
+ const Tech = Component.getComponent('Tech');
+
+ // We need to make sure this check is only done if Tech has been registered.
+ const isTech = Tech && Tech.isTech(ComponentToRegister);
+ const isComp = Component === ComponentToRegister ||
+ Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
+
+ if (isTech || !isComp) {
+ let reason;
+
+ if (isTech) {
+ reason = 'techs must be registered using Tech.registerTech()';
+ } else {
+ reason = 'must be a Component subclass';
+ }
+
+ throw new Error(`Illegal component, "${name}"; ${reason}.`);
+ }
+
+ name = toTitleCase(name);
+
+ if (!Component.components_) {
+ Component.components_ = {};
+ }
+
+ const Player = Component.getComponent('Player');
+
+ if (name === 'Player' && Player && Player.players) {
+ const players = Player.players;
+ const playerNames = Object.keys(players);
+
+ // If we have players that were disposed, then their name will still be
+ // in Players.players. So, we must loop through and verify that the value
+ // for each item is null. This allows registration of the Player component
+ // after all players have been disposed or before any were created.
+ if (players && playerNames.length > 0) {
+ for (let i = 0; i < playerNames.length; i++) {
+ if (players[playerNames[i]] !== null) {
+ throw new Error('Can not register Player component after player has been created.');
+ }
+ }
+ }
+ }
+
+ Component.components_[name] = ComponentToRegister;
+ Component.components_[toLowerCase(name)] = ComponentToRegister;
+
+ return ComponentToRegister;
+ }
+
+ /**
+ * Get a `Component` based on the name it was registered with.
+ *
+ * @param {string} name
+ * The Name of the component to get.
+ *
+ * @return {typeof Component}
+ * The `Component` that got registered under the given name.
+ */
+ static getComponent(name) {
+ if (!name || !Component.components_) {
+ return;
+ }
+
+ return Component.components_[name];
+ }
+}
+
+Component.registerComponent('Component', Component);
+
+export default Component;