/** * @fileoverview Player Component - Base class for all UI objects * */ /** * Base UI Component class * * Components are embeddable UI objects that are represented by both a * javascript object and an element in the DOM. They can be children of other * components, and can have many children themselves. * * // adding a button to the player * var button = player.addChild('button'); * button.el(); // -> button element * * <div class="video-js"> * <div class="vjs-button">Button</div> * </div> * * Components are also event emitters. * * button.on('click', function(){ * console.log('Button Clicked!'); * }); * * button.trigger('customevent'); * * @param {Object} player Main Player * @param {Object=} options * @class * @constructor * @extends vjs.CoreObject */ vjs.Component = vjs.CoreObject.extend({ /** * the constructor function for the class * * @constructor */ init: function(player, options, ready){ this.player_ = player; // Make a copy of prototype.options_ to protect against overriding global defaults this.options_ = vjs.obj.copy(this.options_); // Updated options with supplied options options = this.options(options); // Get ID from options, element, or create using player ID and unique ID this.id_ = options['id'] || ((options['el'] && options['el']['id']) ? options['el']['id'] : player.id() + '_component_' + vjs.guid++ ); this.name_ = options['name'] || null; // Create element if one wasn't provided in options this.el_ = options['el'] || this.createEl(); this.children_ = []; this.childIndex_ = {}; this.childNameIndex_ = {}; // Add any child components in options 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 var touchmove = false; this.on('touchstart', function() { touchmove = false; }); this.on('touchmove', vjs.bind(this, function() { if (this.listenToTouchMove) { this.player_.reportUserActivity(); } touchmove = true; })); this.on('touchend', vjs.bind(this, function(event) { if (!touchmove && !didSomething) { this.player_.reportUserActivity(); } })); } }); /** * Dispose of the component and all child components */ vjs.Component.prototype.dispose = function(){ this.trigger('dispose'); // 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_); } vjs.removeData(this.el_); this.el_ = null; }; /** * Reference to main player instance * * @type {vjs.Player} * @private */ vjs.Component.prototype.player_ = true; /** * Return the component's player * * @return {vjs.Player} */ vjs.Component.prototype.player = function(){ return this.player_; }; /** * The component's options object * * @type {Object} * @private */ vjs.Component.prototype.options_; /** * Deep merge of options objects * * Whenever a property is an object on both options objects * the two properties will be merged using vjs.obj.deepMerge. * * This is used for merging options for child components. We * want it to be easy to override individual options on a child * component without having to rewrite all the other default options. * * Parent.prototype.options_ = { * children: { * 'childOne': { 'foo': 'bar', 'asdf': 'fdsa' }, * 'childTwo': {}, * 'childThree': {} * } * } * newOptions = { * children: { * 'childOne': { 'foo': 'baz', 'abc': '123' } * 'childTwo': null, * 'childFour': {} * } * } * * this.options(newOptions); * * RESULT * * { * children: { * 'childOne': { 'foo': 'baz', 'asdf': 'fdsa', 'abc': '123' }, * 'childTwo': null, // Disabled. Won't be initialized. * 'childThree': {}, * 'childFour': {} * } * } * * @param {Object} obj Object of new option values * @return {Object} A NEW object of this.options_ and obj merged */ vjs.Component.prototype.options = function(obj){ if (obj === undefined) return this.options_; return this.options_ = vjs.util.mergeOptions(this.options_, obj); }; /** * The DOM element for the component * * @type {Element} * @private */ vjs.Component.prototype.el_; /** * Create the component's DOM element * * @param {String=} tagName Element's node type. e.g. 'div' * @param {Object=} attributes An object of element attributes that should be set on the element * @return {Element} */ vjs.Component.prototype.createEl = function(tagName, attributes){ return vjs.createEl(tagName, attributes); }; /** * Get the component's DOM element * * var domEl = myComponent.el(); * * @return {Element} */ vjs.Component.prototype.el = function(){ return this.el_; }; /** * An optional element where, if defined, children will be inserted instead of * directly in `el_` * * @type {Element} * @private */ vjs.Component.prototype.contentEl_; /** * Return the component's DOM element for embedding content. * Will either be el_ or a new element defined in createEl. * * @return {Element} */ vjs.Component.prototype.contentEl = function(){ return this.contentEl_ || this.el_; }; /** * The ID for the component * * @type {String} * @private */ vjs.Component.prototype.id_; /** * Get the component's ID * * var id = myComponent.id(); * * @return {String} */ vjs.Component.prototype.id = function(){ return this.id_; }; /** * The name for the component. Often used to reference the component. * * @type {String} * @private */ vjs.Component.prototype.name_; /** * Get the component's name. The name is often used to reference the component. * * var name = myComponent.name(); * * @return {String} */ vjs.Component.prototype.name = function(){ return this.name_; }; /** * Array of child components * * @type {Array} * @private */ vjs.Component.prototype.children_; /** * Get an array of all child components * * var kids = myComponent.children(); * * @return {Array} The children */ vjs.Component.prototype.children = function(){ return this.children_; }; /** * Object of child components by ID * * @type {Object} * @private */ vjs.Component.prototype.childIndex_; /** * Returns a child component with the provided ID * * @return {vjs.Component} */ vjs.Component.prototype.getChildById = function(id){ return this.childIndex_[id]; }; /** * Object of child components by name * * @type {Object} * @private */ vjs.Component.prototype.childNameIndex_; /** * Returns a child component with the provided name * * @return {vjs.Component} */ vjs.Component.prototype.getChild = function(name){ return this.childNameIndex_[name]; }; /** * Adds a child component inside this component * * myComponent.el(); * // -> <div class='my-component'></div> * myComonent.children(); * // [empty array] * * var myButton = myComponent.addChild('MyButton'); * // -> <div class='my-component'><div class="my-button">myButton<div></div> * // -> myButton === myComonent.children()[0]; * * Pass in options for child constructors and options for children of the child * * var myButton = myComponent.addChild('MyButton', { * text: 'Press Me', * children: { * buttonChildExample: { * buttonChildOption: true * } * } * }); * * @param {String|vjs.Component} child The class name or instance of a child to add * @param {Object=} options Options, including options to be passed to children of the child. * @return {vjs.Component} The child component (created by this process if a string was used) * @suppress {accessControls|checkRegExp|checkTypes|checkVars|const|constantProperty|deprecated|duplicate|es5Strict|fileoverviewTags|globalThis|invalidCasts|missingProperties|nonStandardJsDocs|strictModuleDepCheck|undefinedNames|undefinedVars|unknownDefines|uselessCode|visibility} */ vjs.Component.prototype.addChild = function(child, options){ var component, componentClass, componentName, componentId; // If string, create new component with options if (typeof child === 'string') { componentName = child; // Make sure options is at least an empty object to protect against errors options = options || {}; // Assume name of set is a lowercased name of the UI Class (PlayButton, etc.) componentClass = options['componentClass'] || vjs.capitalize(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 // Closure Compiler throws an 'incomplete alias' warning if we use the vjs variable directly. // Every class should be exported, so this should never be a problem here. component = new window['videojs'][componentClass](this.player_ || this, options); // child is a component instance } else { component = child; } this.children_.push(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 && 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']()) { this.contentEl().appendChild(component['el']()); } // Return so it can stored on parent object if desired. return component; }; /** * Remove a child component from this component's list of children, and the * child component's element from this component's element * * @param {vjs.Component} component Component to remove */ vjs.Component.prototype.removeChild = function(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 components from options * * // when an instance of MyComponent is created, all children in options * // will be added to the instance by their name strings and options * MyComponent.prototype.options_.children = { * myChildComponent: { * myChildOption: true * } * } */ vjs.Component.prototype.initChildren = function(){ var options = this.options_; if (options && options['children']) { var self = this; // Loop through components and add them to the player vjs.obj.each(options['children'], function(name, opts){ // Allow for disabling default components // e.g. vjs.options['children']['posterImage'] = false if (opts === false) return; // Allow waiting to add components until a specific event is called var tempAdd = function(){ // Set property name on player. Could cause conflicts with other prop names, but it's worth making refs easy. self[name] = self.addChild(name, opts); }; if (opts['loadEvent']) { // this.one(opts.loadEvent, tempAdd) } else { tempAdd(); } }); } }; /** * Allows sub components to stack CSS class names * * @return {String} The constructed class name */ vjs.Component.prototype.buildCSSClass = function(){ // Child classes can include a function that does: // return 'CLASS NAME' + this._super(); return ''; }; /* Events ============================================================================= */ /** * Add an event listener to this component's element * * var myFunc = function(){ * var myPlayer = this; * // Do something when the event is fired * }; * * myPlayer.on("eventName", myFunc); * * The context will be the component. * * @param {String} type The event type e.g. 'click' * @param {Function} fn The event listener * @return {vjs.Component} self */ vjs.Component.prototype.on = function(type, fn){ vjs.on(this.el_, type, vjs.bind(this, fn)); return this; }; /** * Remove an event listener from the component's element * * myComponent.off("eventName", myFunc); * * @param {String=} type Event type. Without type it will remove all listeners. * @param {Function=} fn Event listener. Without fn it will remove all listeners for a type. * @return {vjs.Component} */ vjs.Component.prototype.off = function(type, fn){ vjs.off(this.el_, type, fn); return this; }; /** * Add an event listener to be triggered only once and then removed * * @param {String} type Event type * @param {Function} fn Event listener * @return {vjs.Component} */ vjs.Component.prototype.one = function(type, fn) { vjs.one(this.el_, type, vjs.bind(this, fn)); return this; }; /** * Trigger an event on an element * * myComponent.trigger('eventName'); * * @param {String} type The event type to trigger, e.g. 'click' * @param {Event|Object} event The event object to be passed to the listener * @return {vjs.Component} self */ vjs.Component.prototype.trigger = function(type, event){ vjs.trigger(this.el_, type, event); return this; }; /* Ready ================================================================================ */ /** * Is the component loaded * This can mean different things depending on the component. * * @private * @type {Boolean} */ vjs.Component.prototype.isReady_; /** * Trigger ready as soon as initialization is finished * * Allows for delaying ready. Override on a sub class prototype. * If you set this.isReadyOnInitFinish_ it will affect all components. * Specially used when waiting for the Flash player to asynchrnously load. * * @type {Boolean} * @private */ vjs.Component.prototype.isReadyOnInitFinish_ = true; /** * List of ready listeners * * @type {Array} * @private */ vjs.Component.prototype.readyQueue_; /** * Bind a listener to the component's ready state * * Different from event listeners in that if the ready event has already happend * it will trigger the function immediately. * * @param {Function} fn Ready listener * @return {vjs.Component} */ vjs.Component.prototype.ready = function(fn){ if (fn) { if (this.isReady_) { fn.call(this); } else { if (this.readyQueue_ === undefined) { this.readyQueue_ = []; } this.readyQueue_.push(fn); } } return this; }; /** * Trigger the ready listeners * * @return {vjs.Component} */ vjs.Component.prototype.triggerReady = function(){ this.isReady_ = true; var readyQueue = this.readyQueue_; if (readyQueue && readyQueue.length > 0) { for (var i = 0, j = readyQueue.length; i < j; i++) { readyQueue[i].call(this); } // Reset Ready Queue this.readyQueue_ = []; // Allow for using event listeners also, in case you want to do something everytime a source is ready. this.trigger('ready'); } }; /* Display ============================================================================= */ /** * Add a CSS class name to the component's element * * @param {String} classToAdd Classname to add * @return {vjs.Component} */ vjs.Component.prototype.addClass = function(classToAdd){ vjs.addClass(this.el_, classToAdd); return this; }; /** * Remove a CSS class name from the component's element * * @param {String} classToRemove Classname to remove * @return {vjs.Component} */ vjs.Component.prototype.removeClass = function(classToRemove){ vjs.removeClass(this.el_, classToRemove); return this; }; /** * Show the component element if hidden * * @return {vjs.Component} */ vjs.Component.prototype.show = function(){ this.el_.style.display = 'block'; return this; }; /** * Hide the component element if currently showing * * @return {vjs.Component} */ vjs.Component.prototype.hide = function(){ this.el_.style.display = 'none'; return this; }; /** * Lock an item in its visible state * To be used with fadeIn/fadeOut. * * @return {vjs.Component} * @private */ vjs.Component.prototype.lockShowing = function(){ this.addClass('vjs-lock-showing'); return this; }; /** * Unlock an item to be hidden * To be used with fadeIn/fadeOut. * * @return {vjs.Component} * @private */ vjs.Component.prototype.unlockShowing = function(){ this.removeClass('vjs-lock-showing'); return this; }; /** * Disable component by making it unshowable * * Currently private because we're movign towards more css-based states. * @private */ vjs.Component.prototype.disable = function(){ this.hide(); this.show = function(){}; }; /** * Set or get the width of the component (CSS values) * * Setting the video tag dimension values only works with values in pixels. * Percent values will not work. * Some percents can be used, but width()/height() will return the number + %, * not the actual computed width/height. * * @param {Number|String=} num Optional width number * @param {Boolean} skipListeners Skip the 'resize' event trigger * @return {vjs.Component} This component, when setting the width * @return {Number|String} The width, when getting */ vjs.Component.prototype.width = function(num, skipListeners){ return this.dimension('width', num, skipListeners); }; /** * Get or set the height of the component (CSS values) * * Setting the video tag dimension values only works with values in pixels. * Percent values will not work. * Some percents can be used, but width()/height() will return the number + %, * not the actual computed width/height. * * @param {Number|String=} num New component height * @param {Boolean=} skipListeners Skip the resize event trigger * @return {vjs.Component} This component, when setting the height * @return {Number|String} The height, when getting */ vjs.Component.prototype.height = function(num, skipListeners){ return this.dimension('height', num, skipListeners); }; /** * Set both width and height at the same time * * @param {Number|String} width * @param {Number|String} height * @return {vjs.Component} The component */ vjs.Component.prototype.dimensions = function(width, height){ // Skip resize listeners on width for optimization return this.width(width, true).height(height); }; /** * Get or set width or height * * This is the shared code for the width() and height() methods. * All for an integer, integer + 'px' or integer + '%'; * * Known issue: Hidden elements officially have a width of 0. We're defaulting * to the style.width value and falling back to computedStyle which has the * hidden element issue. Info, but probably not an efficient fix: * http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/ * * @param {String} widthOrHeight 'width' or 'height' * @param {Number|String=} num New dimension * @param {Boolean=} skipListeners Skip resize event trigger * @return {vjs.Component} The component if a dimension was set * @return {Number|String} The dimension if nothing was set * @private */ vjs.Component.prototype.dimension = function(widthOrHeight, num, skipListeners){ if (num !== undefined) { // 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) { 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 } else { return parseInt(this.el_['offset'+vjs.capitalize(widthOrHeight)], 10); // ComputedStyle version. // Only difference is if the element is hidden it will return // the percent value (e.g. '100%'') // instead of zero like offsetWidth returns. // var val = vjs.getComputedStyleValue(this.el_, widthOrHeight); // var pxIndex = val.indexOf('px'); // if (pxIndex !== -1) { // return val.slice(0, pxIndex); // } else { // return val; // } } }; /** * Fired when the width and/or height of the component changes * @event resize */ vjs.Component.prototype.onResize; /** * Emit 'tap' events when touch events are supported * * This is used to support toggling the controls through a tap on the video. * * We're requireing them to be enabled because otherwise every component would * have this extra overhead unnecessarily, on mobile devices where extra * overhead is especially bad. * @private */ vjs.Component.prototype.emitTapEvents = function(){ var touchStart, touchTime, couldBeTap, noTap; // Track the start time so we can determine how long the touch lasted touchStart = 0; this.on('touchstart', function(event) { // Record start time so we can detect a tap vs. "touch and hold" touchStart = new Date().getTime(); // Reset couldBeTap tracking couldBeTap = true; }); noTap = function(){ couldBeTap = false; }; // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s this.on('touchmove', noTap); 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() { // Proceed only if the touchmove/leave/cancel event didn't happen if (couldBeTap === true) { // Measure how long the touch lasted touchTime = new Date().getTime() - touchStart; // The touch needs to be quick in order to consider it a tap if (touchTime < 250) { 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 // vjs.fixEvent runs (e.g. event.target) } } }); };