diff options
Diffstat (limited to 'javascript/videojs/src/js/component.js')
| -rw-r--r-- | javascript/videojs/src/js/component.js | 2063 |
1 files changed, 0 insertions, 2063 deletions
diff --git a/javascript/videojs/src/js/component.js b/javascript/videojs/src/js/component.js deleted file mode 100644 index 8ba1079..0000000 --- a/javascript/videojs/src/js/component.js +++ /dev/null @@ -1,2063 +0,0 @@ -/** - * 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; |
