'use strict'; exports.__esModule = true; var _window = require('global/window'); var _window2 = _interopRequireDefault(_window); var _dom = require('./utils/dom.js'); var Dom = _interopRequireWildcard(_dom); var _fn = require('./utils/fn.js'); var Fn = _interopRequireWildcard(_fn); var _guid = require('./utils/guid.js'); var Guid = _interopRequireWildcard(_guid); var _events = require('./utils/events.js'); var Events = _interopRequireWildcard(_events); var _log = require('./utils/log.js'); var _log2 = _interopRequireDefault(_log); var _toTitleCase = require('./utils/to-title-case.js'); var _toTitleCase2 = _interopRequireDefault(_toTitleCase); var _mergeOptions = require('./utils/merge-options.js'); var _mergeOptions2 = _interopRequireDefault(_mergeOptions); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Player Component - Base class for all UI objects * * @file component.js */ /** * 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} */ var Component = function () { /** * A callback that is called when a component is ready. Does not have any * paramters and any callback value will be ignored. * * @callback Component~ReadyCallback * @this 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 player options. # * @param {Object[]} [options.children] * An array of children objects to intialize 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 {Component~ReadyCallback} [ready] * Function that gets called when the `Component` is ready. */ function Component(player, options, ready) { _classCallCheck(this, Component); // 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; } // Make a copy of prototype.options_ to protect against overriding defaults this.options_ = (0, _mergeOptions2['default'])({}, this.options_); // Updated options with supplied options options = this.options_ = (0, _mergeOptions2['default'])(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 var 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(); } this.children_ = []; this.childIndex_ = {}; this.childNameIndex_ = {}; // Add any child components in options if (options.initChildren !== false) { this.initChildren(); } this.ready(ready); // Don't want to trigger ready here or it will before init is actually // finished for all children that run this constructor if (options.reportTouchActivity !== false) { this.enableTouchActivity(); } } /** * Dispose of the `Component` and all child components. * * @fires Component#dispose */ Component.prototype.dispose = function dispose() { /** * Triggered when a `Component` is disposed. * * @event Component#dispose * @type {EventTarget~Event} * * @property {boolean} [bubbles=false] * set to false so that the close event does not * bubble up */ this.trigger({ type: 'dispose', bubbles: false }); // Dispose all children. if (this.children_) { for (var 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; // Remove all event listeners. this.off(); // Remove element from DOM if (this.el_.parentNode) { this.el_.parentNode.removeChild(this.el_); } Dom.removeElData(this.el_); this.el_ = null; }; /** * Return the {@link Player} that the `Component` has attached to. * * @return {Player} * The player that this `Component` has attached to. */ Component.prototype.player = function 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:mergeOptions} * * @param {Object} obj * The object that contains new options. * * @return {Object} * A new object of `this.options_` and `obj` merged together. * * @deprecated since version 5 */ Component.prototype.options = function options(obj) { _log2['default'].warn('this.options() has been deprecated and will be moved to the constructor in 6.0'); if (!obj) { return this.options_; } this.options_ = (0, _mergeOptions2['default'])(this.options_, obj); return this.options_; }; /** * Get the `Component`s DOM element * * @return {Element} * The DOM element for this `Component`. */ Component.prototype.el = function 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. */ Component.prototype.createEl = function createEl(tagName, properties, attributes) { return Dom.createEl(tagName, properties, attributes); }; /** * Localize a string given the string in english. * * @param {string} string * The string to localize. * * @return {string} * The localized string or if no localization exists the english string. */ Component.prototype.localize = function localize(string) { var code = this.player_.language && this.player_.language(); var languages = this.player_.languages && this.player_.languages(); if (!code || !languages) { return string; } var language = languages[code]; if (language && language[string]) { return language[string]; } var primaryCode = code.split('-')[0]; var primaryLang = languages[primaryCode]; if (primaryLang && primaryLang[string]) { return primaryLang[string]; } return string; }; /** * 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`. */ Component.prototype.contentEl = function contentEl() { return this.contentEl_ || this.el_; }; /** * Get this `Component`s ID * * @return {string} * The id of this `Component` */ Component.prototype.id = function 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`. */ Component.prototype.name = function name() { return this.name_; }; /** * Get an array of all child components * * @return {Array} * The children */ Component.prototype.children = function 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. */ Component.prototype.getChildById = function 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. */ Component.prototype.getChild = function getChild(name) { if (!name) { return; } name = (0, _toTitleCase2['default'])(name); return this.childNameIndex_[name]; }; /** * 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. */ Component.prototype.addChild = function addChild(child) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.children_.length; var component = void 0; var componentName = void 0; // If child is a string, create component with options if (typeof child === 'string') { componentName = (0, _toTitleCase2['default'])(child); // Options can also be specified as a boolean, // so convert to an empty object if false. if (!options) { options = {}; } // Same as above, but true is deprecated so show a warning. if (options === true) { _log2['default'].warn('Initializing a child component with `true` is deprecated.' + 'Children should be defined in an array when possible, ' + 'but if necessary use an object instead of `true`.'); options = {}; } var 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 var 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; } this.children_.splice(index, 0, component); 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 && (0, _toTitleCase2['default'])(component.name()); if (componentName) { this.childNameIndex_[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()) { var childNodes = this.contentEl().children; var refNode = childNodes[index] || null; 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 {Component} component * The child `Component` to remove. */ Component.prototype.removeChild = function removeChild(component) { if (typeof component === 'string') { component = this.getChild(component); } if (!component || !this.children_) { return; } var childFound = false; for (var i = this.children_.length - 1; i >= 0; i--) { if (this.children_[i] === component) { childFound = true; this.children_.splice(i, 1); break; } } if (!childFound) { return; } this.childIndex_[component.id()] = null; this.childNameIndex_[component.name()] = null; var compEl = component.el(); if (compEl && compEl.parentNode === this.contentEl()) { this.contentEl().removeChild(component.el()); } }; /** * Add and initialize default child `Component`s based upon options. */ Component.prototype.initChildren = function initChildren() { var _this = this; var children = this.options_.children; if (children) { // `this` is `parent` var parentOptions = this.options_; var handleAdd = function handleAdd(child) { var name = child.name; var 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 var newChild = _this.addChild(name, opts); if (newChild) { _this[name] = newChild; } }; // Allow for an array of children details to passed in the options var workingChildren = void 0; var 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(function (child) { var name = void 0; var opts = void 0; if (typeof child === 'string') { name = child; opts = children[name] || _this.options_[name] || {}; } else { name = child.name; opts = child; } return { name: name, opts: opts }; }).filter(function (child) { // we have to make sure that child.name isn't in the techOrder since // techs are registerd as Components but can't aren't compatible // See https://github.com/videojs/video.js/issues/2772 var c = Component.getComponent(child.opts.componentClass || (0, _toTitleCase2['default'])(child.name)); return c && !Tech.isTech(c); }).forEach(handleAdd); } }; /** * Builds the default DOM class name. Should be overriden by sub-components. * * @return {string} * The DOM class name for this object. * * @abstract */ Component.prototype.buildCSSClass = function buildCSSClass() { // Child classes can include a function that does: // return 'CLASS NAME' + this._super(); return ''; }; /** * Add an `event listener` to this `Component`s element. * * The benefit of using this over the following: * - `VjsEvents.on(otherElement, 'eventName', myFunc)` * - `otherComponent.on('eventName', myFunc)` * * 1. Is that the listeners will get cleaned up when either component gets disposed. * 1. It will also bind `myComponent` as the context of `myFunc`. * > NOTE: If you remove the element from the DOM that has used `on` you need to * clean up references using: `myComponent.trigger(el, 'dispose')` * This will also allow the browser to garbage collect it. In special * cases such as with `window` and `document`, which are both permanent, * this is not necessary. * * @param {string|Component|string[]} [first] * The event name, and array of event names, or another `Component`. * * @param {EventTarget~EventListener|string|string[]} [second] * The listener function, an event name, or an Array of events names. * * @param {EventTarget~EventListener} [third] * The event handler if `first` is a `Component` and `second` is an event name * or an Array of event names. * * @return {Component} * Returns itself; method can be chained. * * @listens Component#dispose */ Component.prototype.on = function on(first, second, third) { var _this2 = this; if (typeof first === 'string' || Array.isArray(first)) { Events.on(this.el_, first, Fn.bind(this, second)); // Targeting another component or element } else { var target = first; var type = second; var fn = Fn.bind(this, third); // When this component is disposed, remove the listener from the other component var removeOnDispose = function removeOnDispose() { return _this2.off(target, type, fn); }; // Use the same function ID so we can remove it later it using the ID // of the original listener removeOnDispose.guid = fn.guid; this.on('dispose', removeOnDispose); // If the other component is disposed first we need to clean the reference // to the other component in this component's removeOnDispose listener // Otherwise we create a memory leak. var cleanRemover = function cleanRemover() { return _this2.off('dispose', removeOnDispose); }; // Add the same function ID so we can easily remove it later cleanRemover.guid = fn.guid; // Check if this is a DOM node if (first.nodeName) { // Add the listener to the other element Events.on(target, type, fn); Events.on(target, 'dispose', cleanRemover); // Should be a component // Not using `instanceof Component` because it makes mock players difficult } else if (typeof first.on === 'function') { // Add the listener to the other component target.on(type, fn); target.on('dispose', cleanRemover); } } return this; }; /** * Remove an event listener from this `Component`s element. If the second argument is * exluded all listeners for the type passed in as the first argument will be removed. * * @param {string|Component|string[]} [first] * The event name, and array of event names, or another `Component`. * * @param {EventTarget~EventListener|string|string[]} [second] * The listener function, an event name, or an Array of events names. * * @param {EventTarget~EventListener} [third] * The event handler if `first` is a `Component` and `second` is an event name * or an Array of event names. * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.off = function off(first, second, third) { if (!first || typeof first === 'string' || Array.isArray(first)) { Events.off(this.el_, first, second); } else { var target = first; var type = second; // Ensure there's at least a guid, even if the function hasn't been used var fn = Fn.bind(this, third); // Remove the dispose listener on this component, // which was given the same guid as the event listener this.off('dispose', fn); if (first.nodeName) { // Remove the listener Events.off(target, type, fn); // Remove the listener for cleaning the dispose listener Events.off(target, 'dispose', fn); } else { target.off(type, fn); target.off('dispose', fn); } } return this; }; /** * Add an event listener that gets triggered only once and then gets removed. * * @param {string|Component|string[]} [first] * The event name, and array of event names, or another `Component`. * * @param {EventTarget~EventListener|string|string[]} [second] * The listener function, an event name, or an Array of events names. * * @param {EventTarget~EventListener} [third] * The event handler if `first` is a `Component` and `second` is an event name * or an Array of event names. * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.one = function one(first, second, third) { var _this3 = this, _arguments = arguments; if (typeof first === 'string' || Array.isArray(first)) { Events.one(this.el_, first, Fn.bind(this, second)); } else { var target = first; var type = second; var fn = Fn.bind(this, third); var newFunc = function newFunc() { _this3.off(target, type, newFunc); fn.apply(null, _arguments); }; // Keep the same function ID so we can remove it later newFunc.guid = fn.guid; this.on(target, type, newFunc); } return this; }; /** * Trigger an event on an element. * * @param {EventTarget~Event|Object|string} event * The event name, and Event, or an event-like object with a type attribute * set to the event name. * * @param {Object} [hash] * Data hash to pass along with the event * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.trigger = function trigger(event, hash) { Events.trigger(this.el_, event, hash); return this; }; /** * Bind a listener to the component's ready state. If the ready event has already * happened it will trigger the function immediately. * * @param {Component~ReadyCallback} fn * A function to call when ready is triggered. * * @param {boolean} [sync=false] * Execute the listener synchronously if `Component` is ready. * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.ready = function ready(fn) { var sync = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; if (fn) { if (this.isReady_) { if (sync) { fn.call(this); } else { // Call the function asynchronously by default for consistency this.setTimeout(fn, 1); } } else { this.readyQueue_ = this.readyQueue_ || []; this.readyQueue_.push(fn); } } return this; }; /** * Trigger all the ready listeners for this `Component`. * * @fires Component#ready */ Component.prototype.triggerReady = function triggerReady() { this.isReady_ = true; // Ensure ready is triggerd asynchronously this.setTimeout(function () { var 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 {EventTarget~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) */ Component.prototype.$ = function $(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) */ Component.prototype.$$ = function $$(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` */ Component.prototype.hasClass = function hasClass(classToCheck) { return Dom.hasElClass(this.el_, classToCheck); }; /** * Add a CSS class name to the `Component`s element. * * @param {string} classToAdd * CSS class name to add * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.addClass = function addClass(classToAdd) { Dom.addElClass(this.el_, classToAdd); return this; }; /** * Remove a CSS class name from the `Component`s element. * * @param {string} classToRemove * CSS class name to remove * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.removeClass = function removeClass(classToRemove) { Dom.removeElClass(this.el_, classToRemove); return this; }; /** * 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 based on (@link Component#hasClass} * * @param {boolean|Dom~predicate} [predicate] * An {@link Dom~predicate} function or a boolean * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.toggleClass = function toggleClass(classToToggle, predicate) { Dom.toggleElClass(this.el_, classToToggle, predicate); return this; }; /** * Show the `Component`s element if it is hidden by removing the * 'vjs-hidden' class name from it. * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.show = function show() { this.removeClass('vjs-hidden'); return this; }; /** * Hide the `Component`s element if it is currently showing by adding the * 'vjs-hidden` class name to it. * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.hide = function hide() { this.addClass('vjs-hidden'); return this; }; /** * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing' * class name to it. Used during fadeIn/fadeOut. * * @return {Component} * Returns itself; method can be chained. * * @private */ Component.prototype.lockShowing = function lockShowing() { this.addClass('vjs-lock-showing'); return this; }; /** * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing' * class name from it. Used during fadeIn/fadeOut. * * @return {Component} * Returns itself; method can be chained. * * @private */ Component.prototype.unlockShowing = function unlockShowing() { this.removeClass('vjs-lock-showing'); return this; }; /** * 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 attibute does not exist or has * no value. * * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute} */ Component.prototype.getAttribute = function 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. * * @return {Component} * Returns itself; method can be chained. * * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute} */ Component.prototype.setAttribute = function setAttribute(attribute, value) { Dom.setAttribute(this.el_, attribute, value); return this; }; /** * Remove an attribute from the `Component`s element. * * @param {string} attribute * Name of the attribute to remove. * * @return {Component} * Returns itself; method can be chained. * * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute} */ Component.prototype.removeAttribute = function removeAttribute(attribute) { Dom.removeAttribute(this.el_, attribute); return this; }; /** * 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 resize event trigger * * @return {Component|number|string} * - The width when getting, zero if there is no width. Can be a string * postpixed with '%' or 'px'. * - Returns itself when setting; method can be chained. */ Component.prototype.width = function 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 resize event trigger * * @return {Component|number|string} * - The width when getting, zero if there is no width. Can be a string * postpixed with '%' or 'px'. * - Returns itself when setting; method can be chained. */ Component.prototype.height = function 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. * * @return {Component} * Returns itself; method can be chained. */ Component.prototype.dimensions = function dimensions(width, height) { // Skip resize listeners on width for optimization return this.width(width, true).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#resize * * @param {string} widthOrHeight 8 'width' or 'height' * * @param {number|string} [num] 8 New dimension * * @param {boolean} [skipListeners] * Skip resize event trigger * * @return {Component} * - the dimension when getting or 0 if unset * - Returns itself when setting; method can be chained. */ Component.prototype.dimension = function 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#resize * @type {EventTarget~Event} */ this.trigger('resize'); } // Return component return this; } // Not setting a value, so getting it // Make sure element exists if (!this.el_) { return 0; } // Get dimension value from style var val = this.el_.style[widthOrHeight]; var 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' + (0, _toTitleCase2['default'])(widthOrHeight)], 10); }; /** * Get the width or the height of the `Component` elements computed style. 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. */ Component.prototype.currentDimension = function currentDimension(widthOrHeight) { var computedWidthOrHeight = 0; if (widthOrHeight !== 'width' && widthOrHeight !== 'height') { throw new Error('currentDimension only accepts width or height value'); } if (typeof _window2['default'].getComputedStyle === 'function') { var computedStyle = _window2['default'].getComputedStyle(this.el_); computedWidthOrHeight = computedStyle.getPropertyValue(widthOrHeight) || computedStyle[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 on IE8 and wherever getComputedStyle doesn't exist. if (computedWidthOrHeight === 0) { var rule = 'offset' + (0, _toTitleCase2['default'])(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 width and height values of the `Component`s * computed style. * * @return {Component~DimensionObject} * The dimensions of the components element */ Component.prototype.currentDimensions = function currentDimensions() { return { width: this.currentDimension('width'), height: this.currentDimension('height') }; }; /** * Get the width of the `Component`s computed style. Uses `window.getComputedStyle`. * * @return {number} width * The width of the `Component`s computed style. */ Component.prototype.currentWidth = function currentWidth() { return this.currentDimension('width'); }; /** * Get the height of the `Component`s computed style. Uses `window.getComputedStyle`. * * @return {number} height * The height of the `Component`s computed style. */ Component.prototype.currentHeight = function currentHeight() { return this.currentDimension('height'); }; /** * Set the focus to this component */ Component.prototype.focus = function focus() { this.el_.focus(); }; /** * Remove the focus from this component */ Component.prototype.blur = function blur() { this.el_.blur(); }; /** * 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. * * @private * @fires Component#tap * @listens Component#touchstart * @listens Component#touchmove * @listens Component#touchleave * @listens Component#touchcancel * @listens Component#touchend */ Component.prototype.emitTapEvents = function emitTapEvents() { // Track the start time so we can determine how long the touch lasted var touchStart = 0; var 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. var tapMovementThreshold = 10; // The maximum length a touch can be while still being considered a tap var touchTimeThreshold = 200; var couldBeTap = void 0; 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 = new Date().getTime(); // 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 var xdiff = event.touches[0].pageX - firstTouch.pageX; var ydiff = event.touches[0].pageY - firstTouch.pageY; var touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); if (touchDistance > tapMovementThreshold) { couldBeTap = false; } } }); var noTap = function noTap() { 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 var touchTime = new Date().getTime() - 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 {EventTarget~Event} */ 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 */ Component.prototype.enableTouchActivity = function 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 var report = Fn.bind(this.player(), this.player().reportUserActivity); var touchHolding = void 0; 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); }); var touchEnd = function touchEnd(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 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}. * * @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} */ Component.prototype.setTimeout = function setTimeout(fn, timeout) { fn = Fn.bind(this, fn); var timeoutId = _window2['default'].setTimeout(fn, timeout); var disposeFn = function disposeFn() { this.clearTimeout(timeoutId); }; disposeFn.guid = 'vjs-timeout-' + timeoutId; this.on('dispose', disposeFn); 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} */ Component.prototype.clearTimeout = function clearTimeout(timeoutId) { _window2['default'].clearTimeout(timeoutId); var disposeFn = function disposeFn() {}; disposeFn.guid = 'vjs-timeout-' + timeoutId; this.off('dispose', disposeFn); 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} */ Component.prototype.setInterval = function setInterval(fn, interval) { fn = Fn.bind(this, fn); var intervalId = _window2['default'].setInterval(fn, interval); var disposeFn = function disposeFn() { this.clearInterval(intervalId); }; disposeFn.guid = 'vjs-interval-' + intervalId; this.on('dispose', disposeFn); return intervalId; }; /** * Clears an interval that gets created via `window.setInterval` or * {@link Component#setInterval}. If you set an inteval 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} */ Component.prototype.clearInterval = function clearInterval(intervalId) { _window2['default'].clearInterval(intervalId); var disposeFn = function disposeFn() {}; disposeFn.guid = 'vjs-interval-' + intervalId; this.off('dispose', disposeFn); return intervalId; }; /** * 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} comp * The `Component` class to register. * * @return {Component} * The `Component` that was registered. */ Component.registerComponent = function registerComponent(name, comp) { if (!name) { return; } name = (0, _toTitleCase2['default'])(name); if (!Component.components_) { Component.components_ = {}; } if (name === 'Player' && Component.components_[name]) { var Player = Component.components_[name]; // 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 not null. This allows registration of the Player component // after all players have been disposed or before any were created. if (Player.players && Object.keys(Player.players).length > 0 && Object.keys(Player.players).map(function (playerName) { return Player.players[playerName]; }).every(Boolean)) { throw new Error('Can not register Player component after player has been created'); } } Component.components_[name] = comp; return comp; }; /** * Get a `Component` based on the name it was registered with. * * @param {string} name * The Name of the component to get. * * @return {Component} * The `Component` that got registered under the given name. * * @deprecated In `videojs` 6 this will not return `Component`s that were not * registered using {@link Component.registerComponent}. Currently we * check the global `videojs` object for a `Component` name and * return that if it exists. */ Component.getComponent = function getComponent(name) { if (!name) { return; } name = (0, _toTitleCase2['default'])(name); if (Component.components_ && Component.components_[name]) { return Component.components_[name]; } if (_window2['default'] && _window2['default'].videojs && _window2['default'].videojs[name]) { _log2['default'].warn('The ' + name + ' component was added to the videojs object when it should be registered using videojs.registerComponent(name, component)'); return _window2['default'].videojs[name]; } }; /** * Sets up the constructor using the supplied init method or uses the init of the * parent object. * * @param {Object} [props={}] * An object of properties. * * @return {Object} * the extended object. * * @deprecated since version 5 */ Component.extend = function extend(props) { props = props || {}; _log2['default'].warn('Component.extend({}) has been deprecated, ' + ' use videojs.extend(Component, {}) instead'); // Set up the constructor using the supplied init method // or using the init of the parent object // Make sure to check the unobfuscated version for external libs var init = props.init || props.init || this.prototype.init || this.prototype.init || function () {}; // In Resig's simple class inheritance (previously used) the constructor // is a function that calls `this.init.apply(arguments)` // However that would prevent us from using `ParentObject.call(this);` // in a Child constructor because the `this` in `this.init` // would still refer to the Child and cause an infinite loop. // We would instead have to do // `ParentObject.prototype.init.apply(this, arguments);` // Bleh. We're not creating a _super() function, so it's good to keep // the parent constructor reference simple. var subObj = function subObj() { init.apply(this, arguments); }; // Inherit from this object's prototype subObj.prototype = Object.create(this.prototype); // Reset the constructor property for subObj otherwise // instances of subObj would have the constructor of the parent Object subObj.prototype.constructor = subObj; // Make the class extendable subObj.extend = Component.extend; // Extend subObj's prototype with functions and other properties from props for (var name in props) { if (props.hasOwnProperty(name)) { subObj.prototype[name] = props[name]; } } return subObj; }; return Component; }(); Component.registerComponent('Component', Component); exports['default'] = Component;