From 9bc7d3bcccacff22a8b7c669d90a5f5b05415761 Mon Sep 17 00:00:00 2001 From: heff Date: Sat, 4 Apr 2015 22:37:10 -0700 Subject: [PATCH 1/2] Converted all classes to use ES6 classes Had to modify some of the workflow because class syntax restrictions e.g. no `this` before super(). But all tests are passing. --- package.json | 2 +- src/js/big-play-button.js | 28 +- src/js/button.js | 108 +- src/js/component.js | 2033 ++++++------ src/js/control-bar/control-bar.js | 46 +- src/js/control-bar/current-time-display.js | 42 + src/js/control-bar/duration-display.js | 48 + src/js/control-bar/fullscreen-toggle.js | 41 +- src/js/control-bar/live-display.js | 33 +- src/js/control-bar/mute-toggle.js | 83 +- src/js/control-bar/play-toggle.js | 59 +- .../control-bar/playback-rate-menu-button.js | 139 - .../playback-rate-menu-button.js | 108 + .../playback-rate-menu-item.js | 39 + src/js/control-bar/progress-control.js | 242 -- .../progress-control/load-progress-bar.js | 64 + .../progress-control/play-progress-bar.js | 22 + .../progress-control/progress-control.js | 27 + .../control-bar/progress-control/seek-bar.js | 93 + .../progress-control/seek-handle.js | 43 + src/js/control-bar/remaining-time-display.js | 46 + .../caption-settings-menu-item.js | 25 + .../text-track-controls/captions-button.js | 49 + .../text-track-controls/chapters-button.js | 109 + .../chapters-track-menu-item.js | 41 + .../off-text-track-menu-item.js | 43 + .../text-track-controls/subtitles-button.js | 22 + .../text-track-controls/text-track-button.js | 65 + .../text-track-menu-item.js | 91 + src/js/control-bar/time-display.js | 154 - src/js/control-bar/time-divider.js | 24 + src/js/control-bar/volume-control.js | 155 - .../control-bar/volume-control/volume-bar.js | 74 + .../volume-control/volume-control.js | 47 + .../volume-control/volume-handle.js | 24 + .../volume-control/volume-level.js | 22 + src/js/control-bar/volume-menu-button.js | 73 +- src/js/error-display.js | 36 +- src/js/loading-spinner.js | 38 +- src/js/media/html5.js | 684 ---- src/js/media/media.js | 527 --- src/js/menu.js | 237 -- src/js/menu/menu-button.js | 141 + src/js/menu/menu-item.js | 51 + src/js/menu/menu.js | 52 + src/js/player.js | 2844 ++++++++--------- src/js/poster-image.js | 101 + src/js/poster.js | 102 - src/js/slider.js | 290 -- src/js/slider/slider-handle.js | 28 + src/js/slider/slider.js | 257 ++ src/js/{media => tech}/flash-rtmp.js | 0 src/js/{media => tech}/flash.js | 135 +- src/js/tech/html5.js | 682 ++++ src/js/{media => tech}/loader.js | 11 +- src/js/tech/tech.js | 536 ++++ src/js/tracks/text-track-controls.js | 580 ---- src/js/tracks/text-track-display.js | 185 ++ src/js/tracks/text-track-settings.js | 162 +- src/js/tracks/text-track.js | 3 +- src/js/video.js | 18 +- test/api/api.js | 5 +- test/unit/component.js | 3 +- test/unit/controls.js | 6 +- test/unit/flash.js | 2 +- test/unit/media.html5.js | 2 +- test/unit/media.js | 62 +- test/unit/mediafaker.js | 8 +- test/unit/menu.js | 2 +- test/unit/player.js | 2 +- test/unit/poster.js | 2 +- test/unit/tracks/text-track-controls.js | 2 +- test/unit/tracks/tracks.js | 25 +- 73 files changed, 6053 insertions(+), 6132 deletions(-) create mode 100644 src/js/control-bar/current-time-display.js create mode 100644 src/js/control-bar/duration-display.js delete mode 100644 src/js/control-bar/playback-rate-menu-button.js create mode 100644 src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js create mode 100644 src/js/control-bar/playback-rate-menu/playback-rate-menu-item.js delete mode 100644 src/js/control-bar/progress-control.js create mode 100644 src/js/control-bar/progress-control/load-progress-bar.js create mode 100644 src/js/control-bar/progress-control/play-progress-bar.js create mode 100644 src/js/control-bar/progress-control/progress-control.js create mode 100644 src/js/control-bar/progress-control/seek-bar.js create mode 100644 src/js/control-bar/progress-control/seek-handle.js create mode 100644 src/js/control-bar/remaining-time-display.js create mode 100644 src/js/control-bar/text-track-controls/caption-settings-menu-item.js create mode 100644 src/js/control-bar/text-track-controls/captions-button.js create mode 100644 src/js/control-bar/text-track-controls/chapters-button.js create mode 100644 src/js/control-bar/text-track-controls/chapters-track-menu-item.js create mode 100644 src/js/control-bar/text-track-controls/off-text-track-menu-item.js create mode 100644 src/js/control-bar/text-track-controls/subtitles-button.js create mode 100644 src/js/control-bar/text-track-controls/text-track-button.js create mode 100644 src/js/control-bar/text-track-controls/text-track-menu-item.js delete mode 100644 src/js/control-bar/time-display.js create mode 100644 src/js/control-bar/time-divider.js delete mode 100644 src/js/control-bar/volume-control.js create mode 100644 src/js/control-bar/volume-control/volume-bar.js create mode 100644 src/js/control-bar/volume-control/volume-control.js create mode 100644 src/js/control-bar/volume-control/volume-handle.js create mode 100644 src/js/control-bar/volume-control/volume-level.js delete mode 100644 src/js/media/html5.js delete mode 100644 src/js/media/media.js delete mode 100644 src/js/menu.js create mode 100644 src/js/menu/menu-button.js create mode 100644 src/js/menu/menu-item.js create mode 100644 src/js/menu/menu.js create mode 100644 src/js/poster-image.js delete mode 100644 src/js/poster.js delete mode 100644 src/js/slider.js create mode 100644 src/js/slider/slider-handle.js create mode 100644 src/js/slider/slider.js rename src/js/{media => tech}/flash-rtmp.js (100%) rename src/js/{media => tech}/flash.js (82%) create mode 100644 src/js/tech/html5.js rename src/js/{media => tech}/loader.js (88%) create mode 100644 src/js/tech/tech.js delete mode 100644 src/js/tracks/text-track-controls.js create mode 100644 src/js/tracks/text-track-display.js diff --git a/package.json b/package.json index 737fd865ce..d6aef59f46 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "global": "^4.3.0" }, "devDependencies": { - "babelify": "^5.0.4", + "babelify": "^6.0.1", "blanket": "^1.1.6", "browserify-istanbul": "^0.2.1", "browserify-versionify": "^1.0.4", diff --git a/src/js/big-play-button.js b/src/js/big-play-button.js index 5722fd81fb..821798f9c0 100644 --- a/src/js/big-play-button.js +++ b/src/js/big-play-button.js @@ -1,4 +1,3 @@ -import Component from './component'; import Button from './button'; /* Big Play Button @@ -11,20 +10,21 @@ import Button from './button'; * @class * @constructor */ -var BigPlayButton = Button.extend(); +class BigPlayButton extends Button { -Component.registerComponent('BigPlayButton', BigPlayButton); + createEl() { + return super.createEl('div', { + className: 'vjs-big-play-button', + innerHTML: '', + 'aria-label': 'play video' + }); + } -BigPlayButton.prototype.createEl = function(){ - return Button.prototype.createEl.call(this, 'div', { - className: 'vjs-big-play-button', - innerHTML: '', - 'aria-label': 'play video' - }); -}; + onClick() { + this.player_.play(); + } -BigPlayButton.prototype.onClick = function(){ - this.player_.play(); -}; +} -export default BigPlayButton; +Button.registerComponent('BigPlayButton', BigPlayButton); +export default BigPlayButton; \ No newline at end of file diff --git a/src/js/button.js b/src/js/button.js index bf52254449..aa80067dd9 100644 --- a/src/js/button.js +++ b/src/js/button.js @@ -12,13 +12,10 @@ import document from 'global/document'; * @class * @constructor */ -var Button = Component.extend({ - /** - * @constructor - * @inheritDoc - */ - init: function(player, options){ - Component.call(this, player, options); +class Button extends Component { + + constructor(player, options){ + super(player, options); this.emitTapEvents(); @@ -27,64 +24,65 @@ var Button = Component.extend({ this.on('focus', this.onFocus); this.on('blur', this.onBlur); } -}); - -Component.registerComponent('Button', Button); -Button.prototype.createEl = function(type, props){ - // Add standard Aria and Tabindex info - props = Lib.obj.merge({ - className: this.buildCSSClass(), - 'role': 'button', - 'aria-live': 'polite', // let the screen reader user know that the text of the button may change - tabIndex: 0 - }, props); - - let el = Component.prototype.createEl.call(this, type, props); - - // if innerHTML hasn't been overridden (bigPlayButton), add content elements - if (!props.innerHTML) { - this.contentEl_ = Lib.createEl('div', { - className: 'vjs-control-content' - }); - - this.controlText_ = Lib.createEl('span', { - className: 'vjs-control-text', - innerHTML: this.localize(this.buttonText) || 'Need Text' - }); - - this.contentEl_.appendChild(this.controlText_); - el.appendChild(this.contentEl_); + createEl(type, props) { + // Add standard Aria and Tabindex info + props = Lib.obj.merge({ + className: this.buildCSSClass(), + 'role': 'button', + 'aria-live': 'polite', // let the screen reader user know that the text of the button may change + tabIndex: 0 + }, props); + + let el = Component.prototype.createEl.call(this, type, props); + + // if innerHTML hasn't been overridden (bigPlayButton), add content elements + if (!props.innerHTML) { + this.contentEl_ = Lib.createEl('div', { + className: 'vjs-control-content' + }); + + this.controlText_ = Lib.createEl('span', { + className: 'vjs-control-text', + innerHTML: this.localize(this.buttonText) || 'Need Text' + }); + + this.contentEl_.appendChild(this.controlText_); + el.appendChild(this.contentEl_); + } + + return el; } - return el; -}; - -Button.prototype.buildCSSClass = function(){ - // TODO: Change vjs-control to vjs-button? - return 'vjs-control ' + Component.prototype.buildCSSClass.call(this); -}; + buildCSSClass() { + // TODO: Change vjs-control to vjs-button? + return 'vjs-control ' + Component.prototype.buildCSSClass.call(this); + } // Click - Override with specific functionality for button -Button.prototype.onClick = function(){}; + onClick(){} // Focus - Add keyboard functionality to element -Button.prototype.onFocus = function(){ - Events.on(document, 'keydown', Lib.bind(this, this.onKeyPress)); -}; + onFocus(){ + Events.on(document, 'keydown', Lib.bind(this, this.onKeyPress)); + } // KeyPress (document level) - Trigger click when keys are pressed -Button.prototype.onKeyPress = function(event){ - // Check for space bar (32) or enter (13) keys - if (event.which == 32 || event.which == 13) { - event.preventDefault(); - this.onClick(); + onKeyPress(event){ + // Check for space bar (32) or enter (13) keys + if (event.which == 32 || event.which == 13) { + event.preventDefault(); + this.onClick(); + } + } + + // Blur - Remove keyboard triggers + onBlur(){ + Events.off(document, 'keydown', Lib.bind(this, this.onKeyPress)); } -}; -// Blur - Remove keyboard triggers -Button.prototype.onBlur = function(){ - Events.off(document, 'keydown', Lib.bind(this, this.onKeyPress)); -}; +} + +Component.registerComponent('Button', Button); export default Button; diff --git a/src/js/component.js b/src/js/component.js index 0a9ee3d75b..6b002cad24 100644 --- a/src/js/component.js +++ b/src/js/component.js @@ -3,7 +3,7 @@ * */ -import CoreObject from './core-object.js'; +// import CoreObject from './core-object.js'; import * as Lib from './lib.js'; import * as VjsUtil from './util.js'; import * as Events from './events.js'; @@ -38,14 +38,16 @@ import window from 'global/window'; * @constructor * @extends vjs.CoreObject */ -var Component = CoreObject.extend({ - /** - * the constructor function for the class - * - * @constructor - */ - init: function(player, options, ready){ - this.player_ = player; +class Component { + + 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; + } else { + this.player_ = player; + } // Make a copy of prototype.options_ to protect against overriding global defaults this.options_ = Lib.obj.copy(this.options_); @@ -54,7 +56,7 @@ var Component = CoreObject.extend({ options = this.options(options); // Get ID from options or options element if one is supplied - this.id_ = options['id'] || (options['el'] && options['el']['id']); + this.id_ = options.id || (options.el && options.el.id); // If there was no ID from the options, generate one if (!this.id_) { @@ -65,14 +67,20 @@ var Component = CoreObject.extend({ this.name_ = options['name'] || null; // Create element if one wasn't provided in options - this.el_ = options['el'] || this.createEl(); + 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 - this.initChildren(); + if (options.initChildren !== false) { + this.initChildren(); + } this.ready(ready); // Don't want to trigger ready here or it will before init is actually @@ -82,1182 +90,1103 @@ var Component = CoreObject.extend({ this.enableTouchActivity(); } } -}); -/** - * Dispose of the component and all child components - */ -Component.prototype.dispose = function(){ - this.trigger({ type: 'dispose', 'bubbles': false }); + // Temp for ES6 class transition, remove before 5.0 + init() { + // console.log('init called on Component'); + Component.apply(this, arguments); + } - // 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(); + /** + * Dispose of the component and all child components + */ + dispose() { + 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; + // 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_); - } + // Remove all event listeners. + this.off(); - Lib.removeData(this.el_); - this.el_ = null; -}; + // Remove element from DOM + if (this.el_.parentNode) { + this.el_.parentNode.removeChild(this.el_); + } -/** - * Reference to main player instance - * - * @type {vjs.Player} - * @private - */ -Component.prototype.player_ = true; + Lib.removeData(this.el_); + this.el_ = null; + } -/** - * Return the component's player - * - * @return {vjs.Player} - */ -Component.prototype.player = function(){ - return this.player_; -}; + /** + * Return the component's player + * + * @return {vjs.Player} + */ + player() { + return this.player_; + } -/** - * The component's options object - * - * @type {Object} - * @private - */ -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 + */ + options(obj){ + if (obj === undefined) return this.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 - */ -Component.prototype.options = function(obj){ - if (obj === undefined) return this.options_; + return this.options_ = VjsUtil.mergeOptions(this.options_, obj); + } - return this.options_ = VjsUtil.mergeOptions(this.options_, obj); -}; + /** + * Get the component's DOM element + * + * var domEl = myComponent.el(); + * + * @return {Element} + */ + el(){ + return this.el_; + } -/** - * The DOM element for the component - * - * @type {Element} - * @private - */ -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} + */ + createEl(tagName, attributes){ + return Lib.createEl(tagName, attributes); + } -/** - * 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} - */ -Component.prototype.createEl = function(tagName, attributes){ - return Lib.createEl(tagName, attributes); -}; - -Component.prototype.localize = function(string){ - var lang = this.player_.language(), - languages = this.player_.languages(); - if (languages && languages[lang] && languages[lang][string]) { - return languages[lang][string]; + localize(string){ + var lang = this.player_.language(), + languages = this.player_.languages(); + if (languages && languages[lang] && languages[lang][string]) { + return languages[lang][string]; + } + return string; } - return string; -}; -/** - * Get the component's DOM element - * - * var domEl = myComponent.el(); - * - * @return {Element} - */ -Component.prototype.el = function(){ - return this.el_; -}; + /** + * Return the component's DOM element where children are inserted. + * Will either be the same as el() or a new element defined in createEl(). + * + * @return {Element} + */ + contentEl(){ + return this.contentEl_ || this.el_; + } -/** - * An optional element where, if defined, children will be inserted instead of - * directly in `el_` - * - * @type {Element} - * @private - */ -Component.prototype.contentEl_; + /** + * Get the component's ID + * + * var id = myComponent.id(); + * + * @return {String} + */ + id(){ + return this.id_; + } -/** - * Return the component's DOM element for embedding content. - * Will either be el_ or a new element defined in createEl. - * - * @return {Element} - */ -Component.prototype.contentEl = function(){ - return this.contentEl_ || this.el_; -}; + /** + * Get the component's name. The name is often used to reference the component. + * + * var name = myComponent.name(); + * + * @return {String} + */ + name(){ + return this.name_; + } -/** - * The ID for the component - * - * @type {String} - * @private - */ -Component.prototype.id_; + /** + * Get an array of all child components + * + * var kids = myComponent.children(); + * + * @return {Array} The children + */ + children(){ + return this.children_; + } -/** - * Get the component's ID - * - * var id = myComponent.id(); - * - * @return {String} - */ -Component.prototype.id = function(){ - return this.id_; -}; + /** + * Returns a child component with the provided ID + * + * @return {vjs.Component} + */ + getChildById(id){ + return this.childIndex_[id]; + } -/** - * The name for the component. Often used to reference the component. - * - * @type {String} - * @private - */ -Component.prototype.name_; + /** + * Returns a child component with the provided name + * + * @return {vjs.Component} + */ + getChild(name){ + return this.childNameIndex_[name]; + } -/** - * Get the component's name. The name is often used to reference the component. - * - * var name = myComponent.name(); - * - * @return {String} - */ -Component.prototype.name = function(){ - return this.name_; -}; + /** + * Adds a child component inside this component + * + * myComponent.el(); + * // ->
+ * myComponent.children(); + * // [empty array] + * + * var myButton = myComponent.addChild('MyButton'); + * // ->
myButton
+ * // -> 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} + */ + addChild(child, options){ + let component, componentName; + // If child is a string, create nt with options + if (typeof child === 'string') { + let componentName = child; + + // Make sure options is at least an empty object to protect against errors + if (!options || options === true) { + options = {}; + } -/** - * Array of child components - * - * @type {Array} - * @private - */ -Component.prototype.children_; + // If no componentClass in options, assume componentClass is the name lowercased + // (e.g. playButton) + let componentClassName = options['componentClass'] || Lib.capitalize(componentName); -/** - * Get an array of all child components - * - * var kids = myComponent.children(); - * - * @return {Array} The children - */ -Component.prototype.children = function(){ - return this.children_; -}; + // Set name through options + options['name'] = componentName; -/** - * Object of child components by ID - * - * @type {Object} - * @private - */ -Component.prototype.childIndex_; + // 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. + let componentClass = Component.getComponent(componentClassName); -/** - * Returns a child component with the provided ID - * - * @return {vjs.Component} - */ -Component.prototype.getChildById = function(id){ - return this.childIndex_[id]; -}; + component = new componentClass(this.player_ || this, options); -/** - * Object of child components by name - * - * @type {Object} - * @private - */ -Component.prototype.childNameIndex_; + // child is a component instance + } else { + component = child; + } -/** - * Returns a child component with the provided name - * - * @return {vjs.Component} - */ -Component.prototype.getChild = function(name){ - return this.childNameIndex_[name]; -}; + this.children_.push(component); -/** - * Adds a child component inside this component - * - * myComponent.el(); - * // ->
- * myComponent.children(); - * // [empty array] - * - * var myButton = myComponent.addChild('MyButton'); - * // ->
myButton
- * // -> 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} - */ -Component.prototype.addChild = function(child, options){ - let component, componentName; - // If child is a string, create nt with options - if (typeof child === 'string') { - let componentName = child; - - // Make sure options is at least an empty object to protect against errors - if (!options || options === true) { - options = {}; + if (typeof component.id === 'function') { + this.childIndex_[component.id()] = component; } - // If no componentClass in options, assume componentClass is the name lowercased - // (e.g. playButton) - let componentClassName = options['componentClass'] || Lib.capitalize(componentName); - - // Set name through options - options['name'] = componentName; + // 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()); - // 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. - let componentClass = Component.getComponent(componentClassName); + if (componentName) { + this.childNameIndex_[componentName] = component; + } - component = new componentClass(this.player_ || this, options); + // 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']()); + } - // child is a component instance - } else { - component = child; + // Return so it can stored on parent object if desired. + return component; } - this.children_.push(component); - - if (typeof component.id === 'function') { - this.childIndex_[component.id()] = 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 + */ + removeChild(component){ + if (typeof component === 'string') { + component = this.getChild(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 (!component || !this.children_) return; - if (componentName) { - this.childNameIndex_[componentName] = component; - } + 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; + } + } - // 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']()); - } + if (!childFound) return; - // Return so it can stored on parent object if desired. - return component; -}; + this.childIndex_[component.id()] = null; + this.childNameIndex_[component.name()] = null; -/** - * 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 - */ -Component.prototype.removeChild = function(component){ - if (typeof component === 'string') { - component = this.getChild(component); + var compEl = component.el(); + if (compEl && compEl.parentNode === this.contentEl()) { + this.contentEl().removeChild(component.el()); + } } - if (!component || !this.children_) return; + /** + * 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 + * } + * } + * + * // Or when creating the component + * var myComp = new MyComponent(player, { + * children: { + * myChildComponent: { + * myChildOption: true + * } + * } + * }); + * + * The children option can also be an Array of child names or + * child options objects (that also include a 'name' key). + * + * var myComp = new MyComponent(player, { + * children: [ + * 'button', + * { + * name: 'button', + * someOtherOption: true + * } + * ] + * }); + * + */ + initChildren(){ + let parent = this; + let parentOptions = parent.options(); + let children = parentOptions['children']; + + let handleAdd; + if (children) { + handleAdd = function(name, 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]; + } - 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; + // Allow for disabling default components + // e.g. vjs.options['children']['posterImage'] = false + if (opts === false) return; + + // 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 + parent[name] = parent.addChild(name, opts); + }; + + // Allow for an array of children details to passed in the options + if (Lib.obj.isArray(children)) { + for (var i = 0; i < children.length; i++) { + let child = children[i]; + + let name, opts; + if (typeof child == 'string') { + // ['myComponent'] + name = child; + opts = {}; + } else { + // [{ name: 'myComponent', otherOption: true }] + name = child.name; + opts = child; + } + + handleAdd(name, opts); + } + } else { + Lib.obj.each(children, handleAdd); + } } } - 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()); + /** + * Allows sub components to stack CSS class names + * + * @return {String} The constructed class name + */ + buildCSSClass(){ + // Child classes can include a function that does: + // return 'CLASS NAME' + this._super(); + return ''; } -}; - -/** - * 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 - * } - * } - * - * // Or when creating the component - * var myComp = new MyComponent(player, { - * children: { - * myChildComponent: { - * myChildOption: true - * } - * } - * }); - * - * The children option can also be an Array of child names or - * child options objects (that also include a 'name' key). - * - * var myComp = new MyComponent(player, { - * children: [ - * 'button', - * { - * name: 'button', - * someOtherOption: true - * } - * ] - * }); - * - */ -Component.prototype.initChildren = function(){ - let parent = this; - let parentOptions = parent.options(); - let children = parentOptions['children']; - - let handleAdd; - if (children) { - handleAdd = function(name, 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. vjs.options['children']['posterImage'] = false - if (opts === false) return; - - // 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 - parent[name] = parent.addChild(name, opts); - }; + /** + * Add an event listener to this component's element + * + * var myFunc = function(){ + * var myComponent = this; + * // Do something when the event is fired + * }; + * + * myComponent.on('eventType', myFunc); + * + * The context of myFunc will be myComponent unless previously bound. + * + * Alternatively, you can add a listener to another element or component. + * + * myComponent.on(otherElement, 'eventName', myFunc); + * myComponent.on(otherComponent, 'eventName', myFunc); + * + * The benefit of using this over `vjs.on(otherElement, 'eventName', myFunc)` + * and `otherComponent.on('eventName', myFunc)` is that this way the listeners + * will be automatically cleaned up when either component is disposed. + * It will also bind myComponent as the context of myFunc. + * + * **NOTE**: When using this on elements in the page other than window + * and document (both permanent), if you remove the element from the DOM + * you need to call `vjs.trigger(el, 'dispose')` on it to clean up + * references to it and allow the browser to garbage collect it. + * + * @param {String|vjs.Component} first The event type or other component + * @param {Function|String} second The event handler or event type + * @param {Function} third The event handler + * @return {vjs.Component} self + */ + on(first, second, third){ + var target, type, fn, removeOnDispose, cleanRemover, thisComponent; - // Allow for an array of children details to passed in the options - if (Lib.obj.isArray(children)) { - for (var i = 0; i < children.length; i++) { - let child = children[i]; - - let name, opts; - if (typeof child == 'string') { - // ['myComponent'] - name = child; - opts = {}; - } else { - // [{ name: 'myComponent', otherOption: true }] - name = child.name; - opts = child; - } + if (typeof first === 'string' || Lib.obj.isArray(first)) { + Events.on(this.el_, first, Lib.bind(this, second)); - handleAdd(name, opts); - } + // Targeting another component or element } else { - Lib.obj.each(children, handleAdd); + target = first; + type = second; + fn = Lib.bind(this, third); + thisComponent = this; + + // When this component is disposed, remove the listener from the other component + removeOnDispose = function(){ + thisComponent.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. + cleanRemover = function(){ + thisComponent.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 vjs.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); + } } - } -}; -/** - * Allows sub components to stack CSS class names - * - * @return {String} The constructed class name - */ -Component.prototype.buildCSSClass = function(){ - // Child classes can include a function that does: - // return 'CLASS NAME' + this._super(); - return ''; -}; + return this; + } -/* Events -============================================================================= */ + /** + * Remove an event listener from this component's element + * + * myComponent.off('eventType', myFunc); + * + * If myFunc is excluded, ALL listeners for the event type will be removed. + * If eventType is excluded, ALL listeners will be removed from the component. + * + * Alternatively you can use `off` to remove listeners that were added to other + * elements or components using `myComponent.on(otherComponent...`. + * In this case both the event type and listener function are REQUIRED. + * + * myComponent.off(otherElement, 'eventType', myFunc); + * myComponent.off(otherComponent, 'eventType', myFunc); + * + * @param {String=|vjs.Component} first The event type or other component + * @param {Function=|String} second The listener function or event type + * @param {Function=} third The listener for other component + * @return {vjs.Component} + */ + off(first, second, third){ + var target, otherComponent, type, fn, otherEl; -/** - * Add an event listener to this component's element - * - * var myFunc = function(){ - * var myComponent = this; - * // Do something when the event is fired - * }; - * - * myComponent.on('eventType', myFunc); - * - * The context of myFunc will be myComponent unless previously bound. - * - * Alternatively, you can add a listener to another element or component. - * - * myComponent.on(otherElement, 'eventName', myFunc); - * myComponent.on(otherComponent, 'eventName', myFunc); - * - * The benefit of using this over `vjs.on(otherElement, 'eventName', myFunc)` - * and `otherComponent.on('eventName', myFunc)` is that this way the listeners - * will be automatically cleaned up when either component is disposed. - * It will also bind myComponent as the context of myFunc. - * - * **NOTE**: When using this on elements in the page other than window - * and document (both permanent), if you remove the element from the DOM - * you need to call `vjs.trigger(el, 'dispose')` on it to clean up - * references to it and allow the browser to garbage collect it. - * - * @param {String|vjs.Component} first The event type or other component - * @param {Function|String} second The event handler or event type - * @param {Function} third The event handler - * @return {vjs.Component} self - */ -Component.prototype.on = function(first, second, third){ - var target, type, fn, removeOnDispose, cleanRemover, thisComponent; - - if (typeof first === 'string' || Lib.obj.isArray(first)) { - Events.on(this.el_, first, Lib.bind(this, second)); - - // Targeting another component or element - } else { - target = first; - type = second; - fn = Lib.bind(this, third); - thisComponent = this; - - // When this component is disposed, remove the listener from the other component - removeOnDispose = function(){ - thisComponent.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. - cleanRemover = function(){ - thisComponent.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 vjs.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); + if (!first || typeof first === 'string' || Lib.obj.isArray(first)) { + Events.off(this.el_, first, second); + } else { + target = first; + type = second; + // Ensure there's at least a guid, even if the function hasn't been used + fn = Lib.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; } - return this; -}; + /** + * Add an event listener to be triggered only once and then removed + * + * myComponent.one('eventName', myFunc); + * + * Alternatively you can add a listener to another element or component + * that will be triggered only once. + * + * myComponent.one(otherElement, 'eventName', myFunc); + * myComponent.one(otherComponent, 'eventName', myFunc); + * + * @param {String|vjs.Component} first The event type or other component + * @param {Function|String} second The listener function or event type + * @param {Function=} third The listener function for other component + * @return {vjs.Component} + */ + one(first, second, third) { + var target, type, fn, thisComponent, newFunc; -/** - * Remove an event listener from this component's element - * - * myComponent.off('eventType', myFunc); - * - * If myFunc is excluded, ALL listeners for the event type will be removed. - * If eventType is excluded, ALL listeners will be removed from the component. - * - * Alternatively you can use `off` to remove listeners that were added to other - * elements or components using `myComponent.on(otherComponent...`. - * In this case both the event type and listener function are REQUIRED. - * - * myComponent.off(otherElement, 'eventType', myFunc); - * myComponent.off(otherComponent, 'eventType', myFunc); - * - * @param {String=|vjs.Component} first The event type or other component - * @param {Function=|String} second The listener function or event type - * @param {Function=} third The listener for other component - * @return {vjs.Component} - */ -Component.prototype.off = function(first, second, third){ - var target, otherComponent, type, fn, otherEl; - - if (!first || typeof first === 'string' || Lib.obj.isArray(first)) { - Events.off(this.el_, first, second); - } else { - target = first; - type = second; - // Ensure there's at least a guid, even if the function hasn't been used - fn = Lib.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); + if (typeof first === 'string' || Lib.obj.isArray(first)) { + Events.one(this.el_, first, Lib.bind(this, second)); } else { - target.off(type, fn); - target.off('dispose', fn); + target = first; + type = second; + fn = Lib.bind(this, third); + thisComponent = this; + + newFunc = function(){ + thisComponent.off(target, type, newFunc); + fn.apply(this, arguments); + }; + // Keep the same function ID so we can remove it later + newFunc.guid = fn.guid; + + this.on(target, type, newFunc); } - } - return this; -}; + return this; + } -/** - * Add an event listener to be triggered only once and then removed - * - * myComponent.one('eventName', myFunc); - * - * Alternatively you can add a listener to another element or component - * that will be triggered only once. - * - * myComponent.one(otherElement, 'eventName', myFunc); - * myComponent.one(otherComponent, 'eventName', myFunc); - * - * @param {String|vjs.Component} first The event type or other component - * @param {Function|String} second The listener function or event type - * @param {Function=} third The listener function for other component - * @return {vjs.Component} - */ -Component.prototype.one = function(first, second, third) { - var target, type, fn, thisComponent, newFunc; - - if (typeof first === 'string' || Lib.obj.isArray(first)) { - Events.one(this.el_, first, Lib.bind(this, second)); - } else { - target = first; - type = second; - fn = Lib.bind(this, third); - thisComponent = this; - - newFunc = function(){ - thisComponent.off(target, type, newFunc); - fn.apply(this, arguments); - }; - // Keep the same function ID so we can remove it later - newFunc.guid = fn.guid; + /** + * Trigger an event on an element + * + * myComponent.trigger('eventName'); + * myComponent.trigger({'type':'eventName'}); + * + * @param {Event|Object|String} event A string (the type) or an event object with a type attribute + * @return {vjs.Component} self + */ + trigger(event){ + Events.trigger(this.el_, event); + return this; + } - this.on(target, type, newFunc); + /** + * 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 {Function} fn Ready listener + * @return {vjs.Component} + */ + ready(fn){ + if (fn) { + if (this.isReady_) { + fn.call(this); + } else { + if (this.readyQueue_ === undefined) { + this.readyQueue_ = []; + } + this.readyQueue_.push(fn); + } + } + return this; } - return this; -}; + /** + * Trigger the ready listeners + * + * @return {vjs.Component} + */ + triggerReady(){ + this.isReady_ = true; -/** - * Trigger an event on an element - * - * myComponent.trigger('eventName'); - * myComponent.trigger({'type':'eventName'}); - * - * @param {Event|Object|String} event A string (the type) or an event object with a type attribute - * @return {vjs.Component} self - */ -Component.prototype.trigger = function(event){ - Events.trigger(this.el_, event); - return this; -}; + var readyQueue = this.readyQueue_; -/* Ready -================================================================================ */ -/** - * Is the component loaded - * This can mean different things depending on the component. - * - * @private - * @type {Boolean} - */ -Component.prototype.isReady_; + if (readyQueue && readyQueue.length > 0) { -/** - * 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 asynchronously load. - * - * @type {Boolean} - * @private - */ -Component.prototype.isReadyOnInitFinish_ = true; + for (var i = 0, j = readyQueue.length; i < j; i++) { + readyQueue[i].call(this); + } -/** - * List of ready listeners - * - * @type {Array} - * @private - */ -Component.prototype.readyQueue_; + // Reset Ready Queue + this.readyQueue_ = []; -/** - * 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 {Function} fn Ready listener - * @return {vjs.Component} - */ -Component.prototype.ready = function(fn){ - if (fn) { - if (this.isReady_) { - fn.call(this); - } else { - if (this.readyQueue_ === undefined) { - this.readyQueue_ = []; - } - this.readyQueue_.push(fn); + // Allow for using event listeners also, in case you want to do something everytime a source is ready. + this.trigger('ready'); } } - return this; -}; -/** - * Trigger the ready listeners - * - * @return {vjs.Component} - */ -Component.prototype.triggerReady = function(){ - this.isReady_ = true; + /** + * Check if a component's element has a CSS class name + * + * @param {String} classToCheck Classname to check + * @return {vjs.Component} + */ + hasClass(classToCheck){ + return Lib.hasClass(this.el_, classToCheck); + } - var readyQueue = this.readyQueue_; + /** + * Add a CSS class name to the component's element + * + * @param {String} classToAdd Classname to add + * @return {vjs.Component} + */ + addClass(classToAdd){ + Lib.addClass(this.el_, classToAdd); + return this; + } - if (readyQueue && readyQueue.length > 0) { + /** + * Remove a CSS class name from the component's element + * + * @param {String} classToRemove Classname to remove + * @return {vjs.Component} + */ + removeClass(classToRemove){ + Lib.removeClass(this.el_, classToRemove); + return this; + } - for (var i = 0, j = readyQueue.length; i < j; i++) { - readyQueue[i].call(this); - } + /** + * Show the component element if hidden + * + * @return {vjs.Component} + */ + show(){ + this.removeClass('vjs-hidden'); + return this; + } - // Reset Ready Queue - this.readyQueue_ = []; + /** + * Hide the component element if currently showing + * + * @return {vjs.Component} + */ + hide(){ + this.addClass('vjs-hidden'); + return this; + } - // Allow for using event listeners also, in case you want to do something everytime a source is ready. - this.trigger('ready'); + /** + * Lock an item in its visible state + * To be used with fadeIn/fadeOut. + * + * @return {vjs.Component} + * @private + */ + lockShowing(){ + this.addClass('vjs-lock-showing'); + return this; } -}; -/* Display -============================================================================= */ + /** + * Unlock an item to be hidden + * To be used with fadeIn/fadeOut. + * + * @return {vjs.Component} + * @private + */ + unlockShowing(){ + this.removeClass('vjs-lock-showing'); + return this; + } -/** - * Check if a component's element has a CSS class name - * - * @param {String} classToCheck Classname to check - * @return {vjs.Component} - */ -Component.prototype.hasClass = function(classToCheck){ - return Lib.hasClass(this.el_, classToCheck); -}; + /** + * 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 + */ + width(num, skipListeners){ + return this.dimension('width', num, skipListeners); + } -/** - * Add a CSS class name to the component's element - * - * @param {String} classToAdd Classname to add - * @return {vjs.Component} - */ -Component.prototype.addClass = function(classToAdd){ - Lib.addClass(this.el_, classToAdd); - return this; -}; + /** + * 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 + */ + height(num, skipListeners){ + return this.dimension('height', num, skipListeners); + } -/** - * Remove a CSS class name from the component's element - * - * @param {String} classToRemove Classname to remove - * @return {vjs.Component} - */ -Component.prototype.removeClass = function(classToRemove){ - Lib.removeClass(this.el_, classToRemove); - return this; -}; + /** + * Set both width and height at the same time + * + * @param {Number|String} width + * @param {Number|String} height + * @return {vjs.Component} The component + */ + dimensions(width, height){ + // Skip resize listeners on width for optimization + return this.width(width, true).height(height); + } -/** - * Show the component element if hidden - * - * @return {vjs.Component} - */ -Component.prototype.show = function(){ - this.removeClass('vjs-hidden'); - return this; -}; + /** + * 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 + */ + dimension(widthOrHeight, num, skipListeners){ + if (num !== undefined) { + // Set to zero if null or literally NaN (NaN !== NaN) + if (num === null || num !== num) { + num = 0; + } -/** - * Hide the component element if currently showing - * - * @return {vjs.Component} - */ -Component.prototype.hide = function(){ - this.addClass('vjs-hidden'); - return this; -}; + // 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'; + } -/** - * Lock an item in its visible state - * To be used with fadeIn/fadeOut. - * - * @return {vjs.Component} - * @private - */ -Component.prototype.lockShowing = function(){ - this.addClass('vjs-lock-showing'); - return this; -}; + // skipListeners allows us to avoid triggering the resize event when setting both width and height + if (!skipListeners) { this.trigger('resize'); } -/** - * Unlock an item to be hidden - * To be used with fadeIn/fadeOut. - * - * @return {vjs.Component} - * @private - */ -Component.prototype.unlockShowing = function(){ - this.removeClass('vjs-lock-showing'); - return this; -}; + // Return component + return this; + } -/** - * Disable component by making it unshowable - * - * Currently private because we're moving towards more css-based states. - * @private - */ -Component.prototype.disable = function(){ - this.hide(); - this.show = function(){}; -}; + // Not setting a value, so getting it + // Make sure element exists + if (!this.el_) return 0; -/** - * 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 - */ -Component.prototype.width = function(num, skipListeners){ - return this.dimension('width', num, skipListeners); -}; + // 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); -/** - * 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 - */ -Component.prototype.height = function(num, skipListeners){ - return this.dimension('height', num, skipListeners); -}; + // 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 { -/** - * Set both width and height at the same time - * - * @param {Number|String} width - * @param {Number|String} height - * @return {vjs.Component} The component - */ -Component.prototype.dimensions = function(width, height){ - // Skip resize listeners on width for optimization - return this.width(width, true).height(height); -}; + return parseInt(this.el_['offset'+Lib.capitalize(widthOrHeight)], 10); -/** - * 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 - */ -Component.prototype.dimension = function(widthOrHeight, num, skipListeners){ - if (num !== undefined) { - // Set to zero if null or literally NaN (NaN !== NaN) - if (num === null || num !== num) { - num = 0; - } + // 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'); - // 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'; + // if (pxIndex !== -1) { + // return val.slice(0, pxIndex); + // } else { + // return val; + // } } - - // 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'+Lib.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; - // } - } -}; + /** + * Emit 'tap' events when touch events are supported + * + * This is used to support toggling the controls through a tap on the video. + * + * We're requiring them to be enabled because otherwise every component would + * have this extra overhead unnecessarily, on mobile devices where extra + * overhead is especially bad. + * @private + */ + emitTapEvents(){ + var touchStart, firstTouch, touchTime, couldBeTap, noTap, + xdiff, ydiff, touchDistance, tapMovementThreshold, touchTimeThreshold; -/** - * Fired when the width and/or height of the component changes - * @event resize - */ -Component.prototype.onResize; + // Track the start time so we can determine how long the touch lasted + touchStart = 0; + firstTouch = null; -/** - * Emit 'tap' events when touch events are supported - * - * This is used to support toggling the controls through a tap on the video. - * - * We're requiring them to be enabled because otherwise every component would - * have this extra overhead unnecessarily, on mobile devices where extra - * overhead is especially bad. - * @private - */ -Component.prototype.emitTapEvents = function(){ - var touchStart, firstTouch, touchTime, couldBeTap, noTap, - xdiff, ydiff, touchDistance, tapMovementThreshold, touchTimeThreshold; - - // Track the start time so we can determine how long the touch lasted - touchStart = 0; - 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. - tapMovementThreshold = 10; - - // The maximum length a touch can be while still being considered a tap - touchTimeThreshold = 200; - - this.on('touchstart', function(event) { - // If more than one finger, don't consider treating this as a click - if (event.touches.length === 1) { - firstTouch = Lib.obj.copy(event.touches[0]); - // Record start time so we can detect a tap vs. "touch and hold" - touchStart = new Date().getTime(); - // Reset couldBeTap tracking - couldBeTap = true; - } - }); + // 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. + tapMovementThreshold = 10; + + // The maximum length a touch can be while still being considered a tap + touchTimeThreshold = 200; + + this.on('touchstart', function(event) { + // If more than one finger, don't consider treating this as a click + if (event.touches.length === 1) { + firstTouch = Lib.obj.copy(event.touches[0]); + // 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 - xdiff = event.touches[0].pageX - firstTouch.pageX; - ydiff = event.touches[0].pageY - firstTouch.pageY; - touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); - if (touchDistance > tapMovementThreshold) { + 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 + xdiff = event.touches[0].pageX - firstTouch.pageX; + ydiff = event.touches[0].pageY - firstTouch.pageY; + touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); + if (touchDistance > tapMovementThreshold) { + couldBeTap = false; + } } - } - }); - - 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 - touchTime = new Date().getTime() - touchStart; - // Make sure the touch was less than the threshold to be considered a tap - if (touchTime < touchTimeThreshold) { - event.preventDefault(); // Don't let browser turn this into a click - 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) + }); + + 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 + touchTime = new Date().getTime() - touchStart; + // Make sure the touch was less than the threshold to be considered a tap + if (touchTime < touchTimeThreshold) { + event.preventDefault(); // Don't let browser turn this into a click + 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) + } } + }); + } + + /** + * Report user touch activity when touch events occur + * + * User activity is used to determine when controls should show/hide. It's + * relatively 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. We can't rely on touch events at the + * player level, because a tap (touchstart + touchend) on the video itself on + * mobile devices is meant to turn controls off (and on). User activity is + * 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) + * + * Also a touchmove, touch+hold, and anything other than a tap is not supposed + * to turn the controls back on on a mobile device. + * + * Here we're setting the default component behavior to report user activity + * whenever touch events happen, and this can be turned off by components that + * want touch events to act differently. + */ + enableTouchActivity() { + var report, touchHolding, touchEnd; + + // Don't continue if the root player doesn't support reporting user activity + if (!this.player().reportUserActivity) { + return; } - }); -}; -/** - * Report user touch activity when touch events occur - * - * User activity is used to determine when controls should show/hide. It's - * relatively 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. We can't rely on touch events at the - * player level, because a tap (touchstart + touchend) on the video itself on - * mobile devices is meant to turn controls off (and on). User activity is - * 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) - * - * Also a touchmove, touch+hold, and anything other than a tap is not supposed - * to turn the controls back on on a mobile device. - * - * Here we're setting the default component behavior to report user activity - * whenever touch events happen, and this can be turned off by components that - * want touch events to act differently. - */ -Component.prototype.enableTouchActivity = function() { - var report, touchHolding, touchEnd; + // listener for reporting that the user is active + report = Lib.bind(this.player(), this.player().reportUserActivity); + + 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); + }); + + touchEnd = function(event) { + report(); + // stop the interval that maintains activity if the touch is holding + this.clearInterval(touchHolding); + }; - // Don't continue if the root player doesn't support reporting user activity - if (!this.player().reportUserActivity) { - return; + this.on('touchmove', report); + this.on('touchend', touchEnd); + this.on('touchcancel', touchEnd); } - // listener for reporting that the user is active - report = Lib.bind(this.player(), this.player().reportUserActivity); - - 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); - }); - - 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); -}; + /** + * Creates timeout and sets up disposal automatically. + * @param {Function} fn The function to run after the timeout. + * @param {Number} timeout Number of ms to delay before executing specified function. + * @return {Number} Returns the timeout ID + */ + setTimeout(fn, timeout) { + fn = Lib.bind(this, fn); -/** - * Creates timeout and sets up disposal automatically. - * @param {Function} fn The function to run after the timeout. - * @param {Number} timeout Number of ms to delay before executing specified function. - * @return {Number} Returns the timeout ID - */ -Component.prototype.setTimeout = function(fn, timeout) { - fn = Lib.bind(this, fn); + // window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/or Phantomjs, we can't. + var timeoutId = setTimeout(fn, timeout); - // window.setTimeout would be preferable here, but due to some bizarre issue with Sinon and/or Phantomjs, we can't. - var timeoutId = setTimeout(fn, timeout); + var disposeFn = function() { + this.clearTimeout(timeoutId); + }; - var disposeFn = function() { - this.clearTimeout(timeoutId); - }; + disposeFn.guid = 'vjs-timeout-'+ timeoutId; - disposeFn.guid = 'vjs-timeout-'+ timeoutId; + this.on('dispose', disposeFn); - this.on('dispose', disposeFn); + return timeoutId; + } - return timeoutId; -}; + /** + * Clears a timeout and removes the associated dispose listener + * @param {Number} timeoutId The id of the timeout to clear + * @return {Number} Returns the timeout ID + */ + clearTimeout(timeoutId) { + clearTimeout(timeoutId); -/** - * Clears a timeout and removes the associated dispose listener - * @param {Number} timeoutId The id of the timeout to clear - * @return {Number} Returns the timeout ID - */ -Component.prototype.clearTimeout = function(timeoutId) { - clearTimeout(timeoutId); + var disposeFn = function(){}; + disposeFn.guid = 'vjs-timeout-'+ timeoutId; - var disposeFn = function(){}; - disposeFn.guid = 'vjs-timeout-'+ timeoutId; + this.off('dispose', disposeFn); - this.off('dispose', disposeFn); + return timeoutId; + } - return timeoutId; -}; + /** + * Creates an interval and sets up disposal automatically. + * @param {Function} fn The function to run every N seconds. + * @param {Number} interval Number of ms to delay before executing specified function. + * @return {Number} Returns the interval ID + */ + setInterval(fn, interval) { + fn = Lib.bind(this, fn); -/** - * Creates an interval and sets up disposal automatically. - * @param {Function} fn The function to run every N seconds. - * @param {Number} interval Number of ms to delay before executing specified function. - * @return {Number} Returns the interval ID - */ -Component.prototype.setInterval = function(fn, interval) { - fn = Lib.bind(this, fn); + var intervalId = setInterval(fn, interval); - var intervalId = setInterval(fn, interval); + var disposeFn = function() { + this.clearInterval(intervalId); + }; - var disposeFn = function() { - this.clearInterval(intervalId); - }; + disposeFn.guid = 'vjs-interval-'+ intervalId; - disposeFn.guid = 'vjs-interval-'+ intervalId; + this.on('dispose', disposeFn); - this.on('dispose', disposeFn); + return intervalId; + } - return intervalId; -}; + /** + * Clears an interval and removes the associated dispose listener + * @param {Number} intervalId The id of the interval to clear + * @return {Number} Returns the interval ID + */ + clearInterval(intervalId) { + clearInterval(intervalId); -/** - * Clears an interval and removes the associated dispose listener - * @param {Number} intervalId The id of the interval to clear - * @return {Number} Returns the interval ID - */ -Component.prototype.clearInterval = function(intervalId) { - clearInterval(intervalId); + var disposeFn = function(){}; + disposeFn.guid = 'vjs-interval-'+ intervalId; - var disposeFn = function(){}; - disposeFn.guid = 'vjs-interval-'+ intervalId; + this.off('dispose', disposeFn); - this.off('dispose', disposeFn); + return intervalId; + } - return intervalId; -}; + static registerComponent(name, comp){ + if (!Component.components_) { + Component.components_ = {}; + } -Component.components = {}; + Component.components_[name] = comp; + return comp; + } -Component.registerComponent = function(name, comp){ - Component.components[name] = comp; - return comp; -}; + static getComponent(name){ + if (Component.components_ && Component.components_[name]) { + return Component.components_[name]; + } -Component.getComponent = function(name){ - if (Component.components[name]) { - return Component.components[name]; + if (window && window.videojs && window.videojs[name]) { + Lib.log.warn('The '+name+' component was added to the videojs object when it should be registered using videojs.registerComponent(name, component)'); + return window.videojs[name]; + } } - if (window && window.videojs && window.videojs[name]) { - Lib.log.warn('The '+name+' component was added to the videojs object when it should be registered using videojs.registerComponent'); - return window.videojs[name]; + static extend(props){ + props = props || {}; + // 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 + let 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. + let subObj = function(){ + init.apply(this, arguments); + }; + + // Inherit from this object's prototype + subObj.prototype = Lib.obj.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; + // Make a function for creating instances + // subObj.create = CoreObject.create; + + // 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; } -}; +} Component.registerComponent('Component', Component); export default Component; diff --git a/src/js/control-bar/control-bar.js b/src/js/control-bar/control-bar.js index 0ac3ba830b..80f41f1ff0 100644 --- a/src/js/control-bar/control-bar.js +++ b/src/js/control-bar/control-bar.js @@ -1,15 +1,22 @@ -import Component from '../component'; -import * as Lib from '../lib'; +import Component from '../component.js'; +import * as Lib from '../lib.js'; -import PlayToggle from './play-toggle'; -import CurrentTimeDisplay from './time-display'; -import LiveDisplay from './live-display'; -import ProgressControl from './progress-control'; -import FullscreenToggle from './fullscreen-toggle'; -import VolumeControl from './volume-control'; -import VolumeMenuButton from './volume-menu-button'; -import MuteToggle from './mute-toggle'; -import PlaybackRateMenuButton from './playback-rate-menu-button'; +// Required children +import PlayToggle from './play-toggle.js'; +import CurrentTimeDisplay from './current-time-display.js'; +import DurationDisplay from './duration-display.js'; +import TimeDivider from './time-divider.js'; +import RemainingTimeDisplay from './remaining-time-display.js'; +import LiveDisplay from './live-display.js'; +import ProgressControl from './progress-control/progress-control.js'; +import FullscreenToggle from './fullscreen-toggle.js'; +import VolumeControl from './volume-control/volume-control.js'; +import VolumeMenuButton from './volume-menu-button.js'; +import MuteToggle from './mute-toggle.js'; +import ChaptersButton from './text-track-controls/chapters-button.js'; +import SubtitlesButton from './text-track-controls/subtitles-button.js'; +import CaptionsButton from './text-track-controls/captions-button.js'; +import PlaybackRateMenuButton from './playback-rate-menu/playback-rate-menu-button.js'; /** * Container of main controls @@ -19,9 +26,13 @@ import PlaybackRateMenuButton from './playback-rate-menu-button'; * @constructor * @extends vjs.Component */ -var ControlBar = Component.extend(); - -Component.registerComponent('ControlBar', ControlBar); +class ControlBar extends Component { + createEl() { + return Lib.createEl('div', { + className: 'vjs-control-bar' + }); + } +} ControlBar.prototype.options_ = { loadEvent: 'play', @@ -44,8 +55,5 @@ ControlBar.prototype.options_ = { } }; -ControlBar.prototype.createEl = function(){ - return Lib.createEl('div', { - className: 'vjs-control-bar' - }); -}; +Component.registerComponent('ControlBar', ControlBar); +export default ControlBar; \ No newline at end of file diff --git a/src/js/control-bar/current-time-display.js b/src/js/control-bar/current-time-display.js new file mode 100644 index 0000000000..72786f31ac --- /dev/null +++ b/src/js/control-bar/current-time-display.js @@ -0,0 +1,42 @@ +import Component from '../component.js'; +import * as Lib from '../lib.js'; + +/** + * Displays the current time + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class CurrentTimeDisplay extends Component { + + constructor(player, options){ + super(player, options); + + this.on(player, 'timeupdate', this.updateContent); + } + + createEl() { + let el = super.createEl('div', { + className: 'vjs-current-time vjs-time-controls vjs-control' + }); + + this.contentEl_ = Lib.createEl('div', { + className: 'vjs-current-time-display', + innerHTML: 'Current Time ' + '0:00', // label the current time for screen reader users + 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes + }); + + el.appendChild(this.contentEl_); + return el; + } + + updateContent() { + // Allows for smooth scrubbing, when player can't keep up. + let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); + this.contentEl_.innerHTML = '' + this.localize('Current Time') + ' ' + Lib.formatTime(time, this.player_.duration()); + } + +} + +Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); +export default CurrentTimeDisplay; diff --git a/src/js/control-bar/duration-display.js b/src/js/control-bar/duration-display.js new file mode 100644 index 0000000000..cec7843178 --- /dev/null +++ b/src/js/control-bar/duration-display.js @@ -0,0 +1,48 @@ +import Component from '../component.js'; +import * as Lib from '../lib.js'; + +/** + * Displays the duration + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class DurationDisplay extends Component { + + constructor(player, options){ + super(player, options); + + // this might need to be changed to 'durationchange' instead of 'timeupdate' eventually, + // however the durationchange event fires before this.player_.duration() is set, + // so the value cannot be written out using this method. + // Once the order of durationchange and this.player_.duration() being set is figured out, + // this can be updated. + this.on(player, 'timeupdate', this.updateContent); + } + + createEl() { + let el = Component.prototype.createEl.call(this, 'div', { + className: 'vjs-duration vjs-time-controls vjs-control' + }); + + this.contentEl_ = Lib.createEl('div', { + className: 'vjs-duration-display', + innerHTML: '' + this.localize('Duration Time') + ' ' + '0:00', // label the duration time for screen reader users + 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes + }); + + el.appendChild(this.contentEl_); + return el; + } + + updateContent() { + let duration = this.player_.duration(); + if (duration) { + this.contentEl_.innerHTML = '' + this.localize('Duration Time') + ' ' + Lib.formatTime(duration); // label the duration time for screen reader users + } + } + +} + +Component.registerComponent('DurationDisplay', DurationDisplay); +export default DurationDisplay; \ No newline at end of file diff --git a/src/js/control-bar/fullscreen-toggle.js b/src/js/control-bar/fullscreen-toggle.js index 30c0b39435..e3c989dfba 100644 --- a/src/js/control-bar/fullscreen-toggle.js +++ b/src/js/control-bar/fullscreen-toggle.js @@ -1,4 +1,3 @@ -import Component from '../component'; import Button from '../button'; /** @@ -8,33 +7,25 @@ import Button from '../button'; * @class * @extends vjs.Button */ -var FullscreenToggle = Button.extend({ - /** - * @constructor - * @memberof vjs.FullscreenToggle - * @instance - */ - init: function(player, options){ - Button.call(this, player, options); - } -}); +class FullscreenToggle extends Button { -Component.registerComponent('FullscreenToggle', FullscreenToggle); + buildCSSClass() { + return 'vjs-fullscreen-control ' + Button.prototype.buildCSSClass.call(this); + } -FullscreenToggle.prototype.buttonText = 'Fullscreen'; + onClick() { + if (!this.player_.isFullscreen()) { + this.player_.requestFullscreen(); + this.controlText_.innerHTML = this.localize('Non-Fullscreen'); + } else { + this.player_.exitFullscreen(); + this.controlText_.innerHTML = this.localize('Fullscreen'); + } + } -FullscreenToggle.prototype.buildCSSClass = function(){ - return 'vjs-fullscreen-control ' + Button.prototype.buildCSSClass.call(this); -}; +} -FullscreenToggle.prototype.onClick = function(){ - if (!this.player_.isFullscreen()) { - this.player_.requestFullscreen(); - this.controlText_.innerHTML = this.localize('Non-Fullscreen'); - } else { - this.player_.exitFullscreen(); - this.controlText_.innerHTML = this.localize('Fullscreen'); - } -}; +FullscreenToggle.prototype.buttonText = 'Fullscreen'; +Button.registerComponent('FullscreenToggle', FullscreenToggle); export default FullscreenToggle; diff --git a/src/js/control-bar/live-display.js b/src/js/control-bar/live-display.js index 723ab522a8..46a85ea624 100644 --- a/src/js/control-bar/live-display.js +++ b/src/js/control-bar/live-display.js @@ -8,28 +8,25 @@ import * as Lib from '../lib'; * @param {Object=} options * @constructor */ -var LiveDisplay = Component.extend({ - init: function(player, options){ - Component.call(this, player, options); - } -}); +class LiveDisplay extends Component { -Component.registerComponent('LiveDisplay', LiveDisplay); + createEl() { + var el = super.createEl('div', { + className: 'vjs-live-controls vjs-control' + }); -LiveDisplay.prototype.createEl = function(){ - var el = Component.prototype.createEl.call(this, 'div', { - className: 'vjs-live-controls vjs-control' - }); + this.contentEl_ = Lib.createEl('div', { + className: 'vjs-live-display', + innerHTML: '' + this.localize('Stream Type') + '' + this.localize('LIVE'), + 'aria-live': 'off' + }); - this.contentEl_ = Lib.createEl('div', { - className: 'vjs-live-display', - innerHTML: '' + this.localize('Stream Type') + '' + this.localize('LIVE'), - 'aria-live': 'off' - }); + el.appendChild(this.contentEl_); - el.appendChild(this.contentEl_); + return el; + } - return el; -}; +} +Component.registerComponent('LiveDisplay', LiveDisplay); export default LiveDisplay; diff --git a/src/js/control-bar/mute-toggle.js b/src/js/control-bar/mute-toggle.js index 0ba52fae37..5b00a35348 100644 --- a/src/js/control-bar/mute-toggle.js +++ b/src/js/control-bar/mute-toggle.js @@ -9,10 +9,10 @@ import * as Lib from '../lib'; * @param {Object=} options * @constructor */ -var MuteToggle = Button.extend({ - /** @constructor */ - init: function(player, options){ - Button.call(this, player, options); +class MuteToggle extends Button { + + constructor(player, options) { + super(player, options); this.on(player, 'volumechange', this.update); @@ -29,50 +29,51 @@ var MuteToggle = Button.extend({ } }); } -}); -MuteToggle.prototype.createEl = function(){ - return Button.prototype.createEl.call(this, 'div', { - className: 'vjs-mute-control vjs-control', - innerHTML: '
' + this.localize('Mute') + '
' - }); -}; + createEl() { + return Button.prototype.createEl.call(this, 'div', { + className: 'vjs-mute-control vjs-control', + innerHTML: '
' + this.localize('Mute') + '
' + }); + } -MuteToggle.prototype.onClick = function(){ - this.player_.muted( this.player_.muted() ? false : true ); -}; + onClick() { + this.player_.muted( this.player_.muted() ? false : true ); + } -MuteToggle.prototype.update = function(){ - var vol = this.player_.volume(), - level = 3; + update() { + var vol = this.player_.volume(), + level = 3; - if (vol === 0 || this.player_.muted()) { - level = 0; - } else if (vol < 0.33) { - level = 1; - } else if (vol < 0.67) { - level = 2; - } + if (vol === 0 || this.player_.muted()) { + level = 0; + } else if (vol < 0.33) { + level = 1; + } else if (vol < 0.67) { + level = 2; + } - // Don't rewrite the button text if the actual text doesn't change. - // This causes unnecessary and confusing information for screen reader users. - // This check is needed because this function gets called every time the volume level is changed. - if(this.player_.muted()){ - if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){ - this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute" - } - } else { - if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){ - this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute" - } - } + // Don't rewrite the button text if the actual text doesn't change. + // This causes unnecessary and confusing information for screen reader users. + // This check is needed because this function gets called every time the volume level is changed. + if(this.player_.muted()){ + if(this.el_.children[0].children[0].innerHTML!=this.localize('Unmute')){ + this.el_.children[0].children[0].innerHTML = this.localize('Unmute'); // change the button text to "Unmute" + } + } else { + if(this.el_.children[0].children[0].innerHTML!=this.localize('Mute')){ + this.el_.children[0].children[0].innerHTML = this.localize('Mute'); // change the button text to "Mute" + } + } - /* TODO improve muted icon classes */ - for (var i = 0; i < 4; i++) { - Lib.removeClass(this.el_, 'vjs-vol-'+i); + /* TODO improve muted icon classes */ + for (var i = 0; i < 4; i++) { + Lib.removeClass(this.el_, 'vjs-vol-'+i); + } + Lib.addClass(this.el_, 'vjs-vol-'+level); } - Lib.addClass(this.el_, 'vjs-vol-'+level); -}; + +} Component.registerComponent('MuteToggle', MuteToggle); export default MuteToggle; diff --git a/src/js/control-bar/play-toggle.js b/src/js/control-bar/play-toggle.js index b67036a98e..9078b553a8 100644 --- a/src/js/control-bar/play-toggle.js +++ b/src/js/control-bar/play-toggle.js @@ -1,5 +1,4 @@ import Button from '../button'; -import Component from '../component'; import * as Lib from '../lib'; /** @@ -9,45 +8,45 @@ import * as Lib from '../lib'; * @class * @constructor */ -var PlayToggle = Button.extend({ - /** @constructor */ - init: function(player, options){ - Button.call(this, player, options); +class PlayToggle extends Button { + + constructor(player, options){ + super(player, options); this.on(player, 'play', this.onPlay); this.on(player, 'pause', this.onPause); } -}); - -Component.registerComponent('PlayToggle', PlayToggle); -PlayToggle.prototype.buttonText = 'Play'; - -PlayToggle.prototype.buildCSSClass = function(){ - return 'vjs-play-control ' + Button.prototype.buildCSSClass.call(this); -}; + buildCSSClass() { + return 'vjs-play-control ' + Button.prototype.buildCSSClass.call(this); + } -// OnClick - Toggle between play and pause -PlayToggle.prototype.onClick = function(){ - if (this.player_.paused()) { - this.player_.play(); - } else { - this.player_.pause(); + // OnClick - Toggle between play and pause + onClick() { + if (this.player_.paused()) { + this.player_.play(); + } else { + this.player_.pause(); + } } -}; // OnPlay - Add the vjs-playing class to the element so it can change appearance -PlayToggle.prototype.onPlay = function(){ - this.removeClass('vjs-paused'); - this.addClass('vjs-playing'); - this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause" -}; + onPlay() { + this.removeClass('vjs-paused'); + this.addClass('vjs-playing'); + this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause" + } // OnPause - Add the vjs-paused class to the element so it can change appearance -PlayToggle.prototype.onPause = function(){ - this.removeClass('vjs-playing'); - this.addClass('vjs-paused'); - this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play" -}; + onPause() { + this.removeClass('vjs-playing'); + this.addClass('vjs-paused'); + this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play" + } + +} + +PlayToggle.prototype.buttonText = 'Play'; +Button.registerComponent('PlayToggle', PlayToggle); export default PlayToggle; diff --git a/src/js/control-bar/playback-rate-menu-button.js b/src/js/control-bar/playback-rate-menu-button.js deleted file mode 100644 index 2ca3dce12f..0000000000 --- a/src/js/control-bar/playback-rate-menu-button.js +++ /dev/null @@ -1,139 +0,0 @@ -import Component from '../component'; -import Menu, { MenuButton, MenuItem } from '../menu'; -import * as Lib from '../lib'; - -/** - * The component for controlling the playback rate - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -let PlaybackRateMenuButton = MenuButton.extend({ - /** @constructor */ - init: function(player, options){ - MenuButton.call(this, player, options); - - this.updateVisibility(); - this.updateLabel(); - - this.on(player, 'loadstart', this.updateVisibility); - this.on(player, 'ratechange', this.updateLabel); - } -}); - -PlaybackRateMenuButton.prototype.buttonText = 'Playback Rate'; -PlaybackRateMenuButton.prototype.className = 'vjs-playback-rate'; - -PlaybackRateMenuButton.prototype.createEl = function(){ - let el = MenuButton.prototype.createEl.call(this); - - this.labelEl_ = Lib.createEl('div', { - className: 'vjs-playback-rate-value', - innerHTML: 1.0 - }); - - el.appendChild(this.labelEl_); - - return el; -}; - -// Menu creation -PlaybackRateMenuButton.prototype.createMenu = function(){ - let menu = new Menu(this.player()); - let rates = this.player().options()['playbackRates']; - - if (rates) { - for (let i = rates.length - 1; i >= 0; i--) { - menu.addChild( - new PlaybackRateMenuItem(this.player(), { 'rate': rates[i] + 'x'}) - ); - } - } - - return menu; -}; - -PlaybackRateMenuButton.prototype.updateARIAAttributes = function(){ - // Current playback rate - this.el().setAttribute('aria-valuenow', this.player().playbackRate()); -}; - -PlaybackRateMenuButton.prototype.onClick = function(){ - // select next rate option - let currentRate = this.player().playbackRate(); - let rates = this.player().options()['playbackRates']; - // this will select first one if the last one currently selected - let newRate = rates[0]; - for (let i = 0; i currentRate) { - newRate = rates[i]; - break; - } - } - this.player().playbackRate(newRate); -}; - -PlaybackRateMenuButton.prototype.playbackRateSupported = function(){ - return this.player().tech - && this.player().tech['featuresPlaybackRate'] - && this.player().options()['playbackRates'] - && this.player().options()['playbackRates'].length > 0 - ; -}; - -/** - * Hide playback rate controls when they're no playback rate options to select - */ -PlaybackRateMenuButton.prototype.updateVisibility = function(){ - if (this.playbackRateSupported()) { - this.removeClass('vjs-hidden'); - } else { - this.addClass('vjs-hidden'); - } -}; - -/** - * Update button label when rate changed - */ -PlaybackRateMenuButton.prototype.updateLabel = function(){ - if (this.playbackRateSupported()) { - this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; - } -}; - -/** - * The specific menu item type for selecting a playback rate - * - * @constructor - */ -var PlaybackRateMenuItem = MenuItem.extend({ - contentElType: 'button', - /** @constructor */ - init: function(player, options){ - let label = this.label = options['rate']; - let rate = this.rate = parseFloat(label, 10); - - // Modify options for parent MenuItem class's init. - options['label'] = label; - options['selected'] = rate === 1; - MenuItem.call(this, player, options); - - this.on(player, 'ratechange', this.update); - } -}); - -Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); - -PlaybackRateMenuItem.prototype.onClick = function(){ - MenuItem.prototype.onClick.call(this); - this.player().playbackRate(this.rate); -}; - -PlaybackRateMenuItem.prototype.update = function(){ - this.selected(this.player().playbackRate() == this.rate); -}; - -Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton); -export default PlaybackRateMenuButton; -export { PlaybackRateMenuItem }; diff --git a/src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js b/src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js new file mode 100644 index 0000000000..1e7d927da7 --- /dev/null +++ b/src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js @@ -0,0 +1,108 @@ +import MenuButton from '../../menu/menu-button.js'; +import Menu from '../../menu/menu.js'; +import PlaybackRateMenuItem from './playback-rate-menu-item.js'; +import * as Lib from '../../lib.js'; + +/** + * The component for controlling the playback rate + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class PlaybackRateMenuButton extends MenuButton { + + constructor(player, options){ + super(player, options); + + this.updateVisibility(); + this.updateLabel(); + + this.on(player, 'loadstart', this.updateVisibility); + this.on(player, 'ratechange', this.updateLabel); + } + + createEl() { + let el = MenuButton.prototype.createEl.call(this); + + this.labelEl_ = Lib.createEl('div', { + className: 'vjs-playback-rate-value', + innerHTML: 1.0 + }); + + el.appendChild(this.labelEl_); + + return el; + } + + // Menu creation + createMenu() { + let menu = new Menu(this.player()); + let rates = this.player().options()['playbackRates']; + + if (rates) { + for (let i = rates.length - 1; i >= 0; i--) { + menu.addChild( + new PlaybackRateMenuItem(this.player(), { 'rate': rates[i] + 'x'}) + ); + } + } + + return menu; + } + + updateARIAAttributes() { + // Current playback rate + this.el().setAttribute('aria-valuenow', this.player().playbackRate()); + } + + onClick() { + // select next rate option + let currentRate = this.player().playbackRate(); + let rates = this.player().options()['playbackRates']; + // this will select first one if the last one currently selected + let newRate = rates[0]; + for (let i = 0; i currentRate) { + newRate = rates[i]; + break; + } + } + this.player().playbackRate(newRate); + } + + playbackRateSupported() { + return this.player().tech + && this.player().tech['featuresPlaybackRate'] + && this.player().options()['playbackRates'] + && this.player().options()['playbackRates'].length > 0 + ; + } + + /** + * Hide playback rate controls when they're no playback rate options to select + */ + updateVisibility() { + if (this.playbackRateSupported()) { + this.removeClass('vjs-hidden'); + } else { + this.addClass('vjs-hidden'); + } + } + + /** + * Update button label when rate changed + */ + updateLabel() { + if (this.playbackRateSupported()) { + this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; + } + } + +} + +PlaybackRateMenuButton.prototype.buttonText = 'Playback Rate'; +PlaybackRateMenuButton.prototype.className = 'vjs-playback-rate'; + +MenuButton.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton); +export default PlaybackRateMenuButton; diff --git a/src/js/control-bar/playback-rate-menu/playback-rate-menu-item.js b/src/js/control-bar/playback-rate-menu/playback-rate-menu-item.js new file mode 100644 index 0000000000..572203f02f --- /dev/null +++ b/src/js/control-bar/playback-rate-menu/playback-rate-menu-item.js @@ -0,0 +1,39 @@ +import MenuItem from '../../menu/menu-item.js'; + +/** + * The specific menu item type for selecting a playback rate + * + * @constructor + */ +class PlaybackRateMenuItem extends MenuItem { + + constructor(player, options){ + let label = options['rate']; + let rate = parseFloat(label, 10); + + // Modify options for parent MenuItem class's init. + options['label'] = label; + options['selected'] = rate === 1; + super(player, options); + + this.label = label; + this.rate = rate; + + this.on(player, 'ratechange', this.update); + } + + onClick() { + super.onClick(); + this.player().playbackRate(this.rate); + } + + update() { + this.selected(this.player().playbackRate() == this.rate); + } + +} + +PlaybackRateMenuItem.prototype.contentElType = 'button'; + +MenuItem.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); +export default PlaybackRateMenuItem; \ No newline at end of file diff --git a/src/js/control-bar/progress-control.js b/src/js/control-bar/progress-control.js deleted file mode 100644 index f2afa48145..0000000000 --- a/src/js/control-bar/progress-control.js +++ /dev/null @@ -1,242 +0,0 @@ -import Component from '../component'; -import Slider, { SliderHandle } from '../slider'; -import * as Lib from '../lib'; - -/** - * The Progress Control component contains the seek bar, load progress, - * and play progress - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -let ProgressControl = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - } -}); - -Component.registerComponent('ProgressControl', ProgressControl); - -ProgressControl.prototype.options_ = { - children: { - 'seekBar': {} - } -}; - -ProgressControl.prototype.createEl = function(){ - return Component.prototype.createEl.call(this, 'div', { - className: 'vjs-progress-control vjs-control' - }); -}; - -/** - * Seek Bar and holder for the progress bars - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var SeekBar = Slider.extend({ - /** @constructor */ - init: function(player, options){ - Slider.call(this, player, options); - this.on(player, 'timeupdate', this.updateARIAAttributes); - player.ready(Lib.bind(this, this.updateARIAAttributes)); - } -}); - -Component.registerComponent('SeekBar', SeekBar); - -SeekBar.prototype.options_ = { - children: { - 'loadProgressBar': {}, - 'playProgressBar': {}, - 'seekHandle': {} - }, - 'barName': 'playProgressBar', - 'handleName': 'seekHandle' -}; - -SeekBar.prototype.playerEvent = 'timeupdate'; - -SeekBar.prototype.createEl = function(){ - return Slider.prototype.createEl.call(this, 'div', { - className: 'vjs-progress-holder', - 'aria-label': 'video progress bar' - }); -}; - -SeekBar.prototype.updateARIAAttributes = function(){ - // Allows for smooth scrubbing, when player can't keep up. - let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); - this.el_.setAttribute('aria-valuenow', Lib.round(this.getPercent()*100, 2)); // machine readable value of progress bar (percentage complete) - this.el_.setAttribute('aria-valuetext', Lib.formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete) -}; - -SeekBar.prototype.getPercent = function(){ - return this.player_.currentTime() / this.player_.duration(); -}; - -SeekBar.prototype.onMouseDown = function(event){ - Slider.prototype.onMouseDown.call(this, event); - - this.player_.scrubbing = true; - this.player_.addClass('vjs-scrubbing'); - - this.videoWasPlaying = !this.player_.paused(); - this.player_.pause(); -}; - -SeekBar.prototype.onMouseMove = function(event){ - let newTime = this.calculateDistance(event) * this.player_.duration(); - - // Don't let video end while scrubbing. - if (newTime == this.player_.duration()) { newTime = newTime - 0.1; } - - // Set new time (tell player to seek to new time) - this.player_.currentTime(newTime); -}; - -SeekBar.prototype.onMouseUp = function(event){ - Slider.prototype.onMouseUp.call(this, event); - - this.player_.scrubbing = false; - this.player_.removeClass('vjs-scrubbing'); - if (this.videoWasPlaying) { - this.player_.play(); - } -}; - -SeekBar.prototype.stepForward = function(){ - this.player_.currentTime(this.player_.currentTime() + 5); // more quickly fast forward for keyboard-only users -}; - -SeekBar.prototype.stepBack = function(){ - this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users -}; - -/** - * Shows load progress - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var LoadProgressBar = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - this.on(player, 'progress', this.update); - } -}); - -Component.registerComponent('LoadProgressBar', LoadProgressBar); - -LoadProgressBar.prototype.createEl = function(){ - return Component.prototype.createEl.call(this, 'div', { - className: 'vjs-load-progress', - innerHTML: '' + this.localize('Loaded') + ': 0%' - }); -}; - -LoadProgressBar.prototype.update = function(){ - let buffered = this.player_.buffered(); - let duration = this.player_.duration(); - let bufferedEnd = this.player_.bufferedEnd(); - let children = this.el_.children; - - // get the percent width of a time compared to the total end - let percentify = function (time, end){ - let percent = (time / end) || 0; // no NaN - return (percent * 100) + '%'; - }; - - // update the width of the progress bar - this.el_.style.width = percentify(bufferedEnd, duration); - - // add child elements to represent the individual buffered time ranges - for (let i = 0; i < buffered.length; i++) { - let start = buffered.start(i); - let end = buffered.end(i); - let part = children[i]; - - if (!part) { - part = this.el_.appendChild(Lib.createEl()); - } - - // set the percent based on the width of the progress bar (bufferedEnd) - part.style.left = percentify(start, bufferedEnd); - part.style.width = percentify(end - start, bufferedEnd); - } - - // remove unused buffered range elements - for (let i = children.length; i > buffered.length; i--) { - this.el_.removeChild(children[i-1]); - } -}; - -/** - * Shows play progress - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var PlayProgressBar = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - } -}); - -Component.registerComponent('PlayProgressBar', PlayProgressBar); - -PlayProgressBar.prototype.createEl = function(){ - return Component.prototype.createEl.call(this, 'div', { - className: 'vjs-play-progress', - innerHTML: '' + this.localize('Progress') + ': 0%' - }); -}; - -/** - * The Seek Handle shows the current position of the playhead during playback, - * and can be dragged to adjust the playhead. - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var SeekHandle = SliderHandle.extend({ - init: function(player, options) { - SliderHandle.call(this, player, options); - this.on(player, 'timeupdate', this.updateContent); - } -}); - -Component.registerComponent('SeekHandle', SeekHandle); - -/** - * The default value for the handle content, which may be read by screen readers - * - * @type {String} - * @private - */ -SeekHandle.prototype.defaultValue = '00:00'; - -/** @inheritDoc */ -SeekHandle.prototype.createEl = function() { - return SliderHandle.prototype.createEl.call(this, 'div', { - className: 'vjs-seek-handle', - 'aria-live': 'off' - }); -}; - -SeekHandle.prototype.updateContent = function() { - let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); - this.el_.innerHTML = '' + Lib.formatTime(time, this.player_.duration()) + ''; -}; - -export default ProgressControl; -export { SeekBar, LoadProgressBar, PlayProgressBar, SeekHandle }; diff --git a/src/js/control-bar/progress-control/load-progress-bar.js b/src/js/control-bar/progress-control/load-progress-bar.js new file mode 100644 index 0000000000..589c2003e5 --- /dev/null +++ b/src/js/control-bar/progress-control/load-progress-bar.js @@ -0,0 +1,64 @@ +import Component from '../../component.js'; +import * as Lib from '../../lib.js'; + +/** + * Shows load progress + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class LoadProgressBar extends Component { + + constructor(player, options){ + super(player, options); + this.on(player, 'progress', this.update); + } + + createEl() { + return super.createEl('div', { + className: 'vjs-load-progress', + innerHTML: '' + this.localize('Loaded') + ': 0%' + }); + } + + update() { + let buffered = this.player_.buffered(); + let duration = this.player_.duration(); + let bufferedEnd = this.player_.bufferedEnd(); + let children = this.el_.children; + + // get the percent width of a time compared to the total end + let percentify = function (time, end){ + let percent = (time / end) || 0; // no NaN + return (percent * 100) + '%'; + }; + + // update the width of the progress bar + this.el_.style.width = percentify(bufferedEnd, duration); + + // add child elements to represent the individual buffered time ranges + for (let i = 0; i < buffered.length; i++) { + let start = buffered.start(i); + let end = buffered.end(i); + let part = children[i]; + + if (!part) { + part = this.el_.appendChild(Lib.createEl()); + } + + // set the percent based on the width of the progress bar (bufferedEnd) + part.style.left = percentify(start, bufferedEnd); + part.style.width = percentify(end - start, bufferedEnd); + } + + // remove unused buffered range elements + for (let i = children.length; i > buffered.length; i--) { + this.el_.removeChild(children[i-1]); + } + } + +} + +Component.registerComponent('LoadProgressBar', LoadProgressBar); +export default LoadProgressBar; \ No newline at end of file diff --git a/src/js/control-bar/progress-control/play-progress-bar.js b/src/js/control-bar/progress-control/play-progress-bar.js new file mode 100644 index 0000000000..f78c7a3859 --- /dev/null +++ b/src/js/control-bar/progress-control/play-progress-bar.js @@ -0,0 +1,22 @@ +import Component from '../../component.js'; + +/** + * Shows play progress + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class PlayProgressBar extends Component { + + createEl() { + return super.createEl('div', { + className: 'vjs-play-progress', + innerHTML: '' + this.localize('Progress') + ': 0%' + }); + } + +} + +Component.registerComponent('PlayProgressBar', PlayProgressBar); +export default PlayProgressBar; \ No newline at end of file diff --git a/src/js/control-bar/progress-control/progress-control.js b/src/js/control-bar/progress-control/progress-control.js new file mode 100644 index 0000000000..7afe41e234 --- /dev/null +++ b/src/js/control-bar/progress-control/progress-control.js @@ -0,0 +1,27 @@ +import Component from '../../component.js'; +import SeekBar from './seek-bar.js'; + +/** + * The Progress Control component contains the seek bar, load progress, + * and play progress + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class ProgressControl extends Component { + createEl() { + return super.createEl('div', { + className: 'vjs-progress-control vjs-control' + }); + } +} + +ProgressControl.prototype.options_ = { + children: { + 'seekBar': {} + } +}; + +Component.registerComponent('ProgressControl', ProgressControl); +export default ProgressControl; diff --git a/src/js/control-bar/progress-control/seek-bar.js b/src/js/control-bar/progress-control/seek-bar.js new file mode 100644 index 0000000000..0146f0a9ce --- /dev/null +++ b/src/js/control-bar/progress-control/seek-bar.js @@ -0,0 +1,93 @@ +import Slider from '../../slider/slider.js'; +import LoadProgressBar from './load-progress-bar.js'; +import PlayProgressBar from './play-progress-bar.js'; +import SeekHandle from './seek-handle.js'; +import * as Lib from '../../lib.js'; + +/** + * Seek Bar and holder for the progress bars + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class SeekBar extends Slider { + + constructor(player, options){ + super(player, options); + this.on(player, 'timeupdate', this.updateARIAAttributes); + player.ready(Lib.bind(this, this.updateARIAAttributes)); + } + + createEl() { + return Slider.prototype.createEl.call(this, 'div', { + className: 'vjs-progress-holder', + 'aria-label': 'video progress bar' + }); + } + + updateARIAAttributes() { + // Allows for smooth scrubbing, when player can't keep up. + let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); + this.el_.setAttribute('aria-valuenow', Lib.round(this.getPercent()*100, 2)); // machine readable value of progress bar (percentage complete) + this.el_.setAttribute('aria-valuetext', Lib.formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete) + } + + getPercent() { + return this.player_.currentTime() / this.player_.duration(); + } + + onMouseDown(event) { + super.onMouseDown(event); + + this.player_.scrubbing = true; + this.player_.addClass('vjs-scrubbing'); + + this.videoWasPlaying = !this.player_.paused(); + this.player_.pause(); + } + + onMouseMove(event) { + let newTime = this.calculateDistance(event) * this.player_.duration(); + + // Don't let video end while scrubbing. + if (newTime == this.player_.duration()) { newTime = newTime - 0.1; } + + // Set new time (tell player to seek to new time) + this.player_.currentTime(newTime); + } + + onMouseUp(event) { + super.onMouseUp(event); + + this.player_.scrubbing = false; + this.player_.removeClass('vjs-scrubbing'); + if (this.videoWasPlaying) { + this.player_.play(); + } + } + + stepForward() { + this.player_.currentTime(this.player_.currentTime() + 5); // more quickly fast forward for keyboard-only users + } + + stepBack() { + this.player_.currentTime(this.player_.currentTime() - 5); // more quickly rewind for keyboard-only users + } + +} + +SeekBar.prototype.options_ = { + children: { + 'loadProgressBar': {}, + 'playProgressBar': {}, + 'seekHandle': {} + }, + 'barName': 'playProgressBar', + 'handleName': 'seekHandle' +}; + +SeekBar.prototype.playerEvent = 'timeupdate'; + +Slider.registerComponent('SeekBar', SeekBar); +export default SeekBar; \ No newline at end of file diff --git a/src/js/control-bar/progress-control/seek-handle.js b/src/js/control-bar/progress-control/seek-handle.js new file mode 100644 index 0000000000..ff7c4ae3e6 --- /dev/null +++ b/src/js/control-bar/progress-control/seek-handle.js @@ -0,0 +1,43 @@ +import SliderHandle from '../../slider/slider-handle.js'; +import * as Lib from '../../lib.js'; + +/** + * The Seek Handle shows the current position of the playhead during playback, + * and can be dragged to adjust the playhead. + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class SeekHandle extends SliderHandle { + + constructor(player, options) { + super(player, options); + this.on(player, 'timeupdate', this.updateContent); + } + + /** @inheritDoc */ + createEl() { + return super.createEl.call('div', { + className: 'vjs-seek-handle', + 'aria-live': 'off' + }); + } + + updateContent() { + let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); + this.el_.innerHTML = '' + Lib.formatTime(time, this.player_.duration()) + ''; + } + +} + +/** + * The default value for the handle content, which may be read by screen readers + * + * @type {String} + * @private + */ +SeekHandle.prototype.defaultValue = '00:00'; + +SliderHandle.registerComponent('SeekHandle', SeekHandle); +export default SeekHandle; \ No newline at end of file diff --git a/src/js/control-bar/remaining-time-display.js b/src/js/control-bar/remaining-time-display.js new file mode 100644 index 0000000000..ce256fafd1 --- /dev/null +++ b/src/js/control-bar/remaining-time-display.js @@ -0,0 +1,46 @@ +import Component from '../component.js'; +import * as Lib from '../lib'; + +/** + * Displays the time left in the video + * @param {Player|Object} player + * @param {Object=} options + * @constructor + */ +class RemainingTimeDisplay extends Component { + + constructor(player, options){ + super(player, options); + + this.on(player, 'timeupdate', this.updateContent); + } + + createEl() { + let el = Component.prototype.createEl.call(this, 'div', { + className: 'vjs-remaining-time vjs-time-controls vjs-control' + }); + + this.contentEl_ = Lib.createEl('div', { + className: 'vjs-remaining-time-display', + innerHTML: '' + this.localize('Remaining Time') + ' ' + '-0:00', // label the remaining time for screen reader users + 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes + }); + + el.appendChild(this.contentEl_); + return el; + } + + updateContent() { + if (this.player_.duration()) { + this.contentEl_.innerHTML = '' + this.localize('Remaining Time') + ' ' + '-'+ Lib.formatTime(this.player_.remainingTime()); + } + + // Allows for smooth scrubbing, when player can't keep up. + // var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); + // this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration()); + } + +} + +Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); +export default RemainingTimeDisplay; \ No newline at end of file diff --git a/src/js/control-bar/text-track-controls/caption-settings-menu-item.js b/src/js/control-bar/text-track-controls/caption-settings-menu-item.js new file mode 100644 index 0000000000..037c6c4cbf --- /dev/null +++ b/src/js/control-bar/text-track-controls/caption-settings-menu-item.js @@ -0,0 +1,25 @@ +import TextTrackMenuItem from './text-track-menu-item.js'; + +class CaptionSettingsMenuItem extends TextTrackMenuItem { + + constructor(player, options) { + options['track'] = { + 'kind': options['kind'], + 'player': player, + 'label': options['kind'] + ' settings', + 'default': false, + mode: 'disabled' + }; + + super(player, options); + this.addClass('vjs-texttrack-settings'); + } + + onClick() { + this.player().getChild('textTrackSettings').show(); + } + +} + +TextTrackMenuItem.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); +export default CaptionSettingsMenuItem; \ No newline at end of file diff --git a/src/js/control-bar/text-track-controls/captions-button.js b/src/js/control-bar/text-track-controls/captions-button.js new file mode 100644 index 0000000000..1465da24a2 --- /dev/null +++ b/src/js/control-bar/text-track-controls/captions-button.js @@ -0,0 +1,49 @@ +import TextTrackButton from './text-track-button.js'; +import CaptionSettingsMenuItem from './caption-settings-menu-item.js'; + +/** + * The button component for toggling and selecting captions + * + * @constructor + */ +class CaptionsButton extends TextTrackButton { + + constructor(player, options, ready){ + super(player, options, ready); + this.el_.setAttribute('aria-label','Captions Menu'); + } + + update() { + let threshold = 2; + TextTrackButton.prototype.update.call(this); + + // if native, then threshold is 1 because no settings button + if (this.player().tech && this.player().tech['featuresNativeTextTracks']) { + threshold = 1; + } + + if (this.items && this.items.length > threshold) { + this.show(); + } else { + this.hide(); + } + } + + createItems() { + let items = []; + + if (!(this.player().tech && this.player().tech['featuresNativeTextTracks'])) { + items.push(new CaptionSettingsMenuItem(this.player_, { 'kind': this.kind_ })); + } + + return super.createItems(items); + } + +} + +CaptionsButton.prototype.kind_ = 'captions'; +CaptionsButton.prototype.buttonText = 'Captions'; +CaptionsButton.prototype.className = 'vjs-captions-button'; + +TextTrackButton.registerComponent('CaptionsButton', CaptionsButton); +export default CaptionsButton; \ No newline at end of file diff --git a/src/js/control-bar/text-track-controls/chapters-button.js b/src/js/control-bar/text-track-controls/chapters-button.js new file mode 100644 index 0000000000..b92995e21d --- /dev/null +++ b/src/js/control-bar/text-track-controls/chapters-button.js @@ -0,0 +1,109 @@ +import TextTrackButton from './text-track-button.js'; +import TextTrackMenuItem from './text-track-menu-item.js'; +import ChaptersTrackMenuItem from './chapters-track-menu-item.js'; +import Menu from '../../menu/menu.js'; +import * as Lib from '../../lib.js'; +import window from 'global/window'; + +// Chapters act much differently than other text tracks +// Cues are navigation vs. other tracks of alternative languages +/** + * The button component for toggling and selecting chapters + * + * @constructor + */ +class ChaptersButton extends TextTrackButton { + + constructor(player, options, ready){ + super(player, options, ready); + this.el_.setAttribute('aria-label','Chapters Menu'); + } + + // Create a menu item for each text track + createItems() { + let items = []; + + let tracks = this.player_.textTracks(); + + if (!tracks) { + return items; + } + + for (let i = 0; i < tracks.length; i++) { + let track = tracks[i]; + if (track['kind'] === this.kind_) { + items.push(new TextTrackMenuItem(this.player_, { + 'track': track + })); + } + } + + return items; + } + + createMenu() { + let tracks = this.player_.textTracks() || []; + let chaptersTrack; + let items = this.items = []; + + for (let i = 0, l = tracks.length; i < l; i++) { + let track = tracks[i]; + if (track['kind'] == this.kind_) { + if (!track.cues) { + track['mode'] = 'hidden'; + /* jshint loopfunc:true */ + // TODO see if we can figure out a better way of doing this https://github.com/videojs/video.js/issues/1864 + window.setTimeout(Lib.bind(this, function() { + this.createMenu(); + }), 100); + /* jshint loopfunc:false */ + } else { + chaptersTrack = track; + break; + } + } + } + + let menu = this.menu; + if (menu === undefined) { + menu = new Menu(this.player_); + menu.contentEl().appendChild(Lib.createEl('li', { + className: 'vjs-menu-title', + innerHTML: Lib.capitalize(this.kind_), + tabindex: -1 + })); + } + + if (chaptersTrack) { + let cues = chaptersTrack['cues'], cue; + + for (let i = 0, l = cues.length; i < l; i++) { + cue = cues[i]; + + let mi = new ChaptersTrackMenuItem(this.player_, { + 'track': chaptersTrack, + 'cue': cue + }); + + items.push(mi); + + menu.addChild(mi); + } + this.addChild(menu); + } + + if (this.items.length > 0) { + this.show(); + } + + return menu; + } + +} + +ChaptersButton.prototype.kind_ = 'chapters'; +ChaptersButton.prototype.buttonText = 'Chapters'; +ChaptersButton.prototype.className = 'vjs-chapters-button'; + +TextTrackButton.registerComponent('ChaptersButton', ChaptersButton); +export default ChaptersButton; diff --git a/src/js/control-bar/text-track-controls/chapters-track-menu-item.js b/src/js/control-bar/text-track-controls/chapters-track-menu-item.js new file mode 100644 index 0000000000..4165d7ba3a --- /dev/null +++ b/src/js/control-bar/text-track-controls/chapters-track-menu-item.js @@ -0,0 +1,41 @@ +import MenuItem from '../../menu/menu-item.js'; +import * as Lib from '../../lib.js'; + +/** + * @constructor + */ +class ChaptersTrackMenuItem extends MenuItem { + + constructor(player, options){ + let track = options['track']; + let cue = options['cue']; + let currentTime = player.currentTime(); + + // Modify options for parent MenuItem class's init. + options['label'] = cue.text; + options['selected'] = (cue['startTime'] <= currentTime && currentTime < cue['endTime']); + super(player, options); + + this.track = track; + this.cue = cue; + track.addEventListener('cuechange', Lib.bind(this, this.update)); + } + + onClick() { + MenuItem.prototype.onClick.call(this); + this.player_.currentTime(this.cue.startTime); + this.update(this.cue.startTime); + } + + update() { + let cue = this.cue; + let currentTime = this.player_.currentTime(); + + // vjs.log(currentTime, cue.startTime); + this.selected(cue['startTime'] <= currentTime && currentTime < cue['endTime']); + } + +} + +MenuItem.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem); +export default ChaptersTrackMenuItem; \ No newline at end of file diff --git a/src/js/control-bar/text-track-controls/off-text-track-menu-item.js b/src/js/control-bar/text-track-controls/off-text-track-menu-item.js new file mode 100644 index 0000000000..1b02bf77a2 --- /dev/null +++ b/src/js/control-bar/text-track-controls/off-text-track-menu-item.js @@ -0,0 +1,43 @@ +import TextTrackMenuItem from './text-track-menu-item.js'; + +/** + * A special menu item for turning of a specific type of text track + * + * @constructor + */ +class OffTextTrackMenuItem extends TextTrackMenuItem { + + constructor(player, options){ + // Create pseudo track info + // Requires options['kind'] + options['track'] = { + 'kind': options['kind'], + 'player': player, + 'label': options['kind'] + ' off', + 'default': false, + 'mode': 'disabled' + }; + + super(player, options); + this.selected(true); + } + + handleTracksChange(event){ + let tracks = this.player().textTracks(); + let selected = true; + + for (let i = 0, l = tracks.length; i < l; i++) { + let track = tracks[i]; + if (track['kind'] === this.track['kind'] && track['mode'] === 'showing') { + selected = false; + break; + } + } + + this.selected(selected); + } + +} + +TextTrackMenuItem.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem); +export default OffTextTrackMenuItem; \ No newline at end of file diff --git a/src/js/control-bar/text-track-controls/subtitles-button.js b/src/js/control-bar/text-track-controls/subtitles-button.js new file mode 100644 index 0000000000..59bcc701c6 --- /dev/null +++ b/src/js/control-bar/text-track-controls/subtitles-button.js @@ -0,0 +1,22 @@ +import TextTrackButton from './text-track-button.js'; + +/** + * The button component for toggling and selecting subtitles + * + * @constructor + */ +class SubtitlesButton extends TextTrackButton { + + constructor(player, options, ready){ + super(player, options, ready); + this.el_.setAttribute('aria-label','Subtitles Menu'); + } + +} + +SubtitlesButton.prototype.kind_ = 'subtitles'; +SubtitlesButton.prototype.buttonText = 'Subtitles'; +SubtitlesButton.prototype.className = 'vjs-subtitles-button'; + +TextTrackButton.registerComponent('SubtitlesButton', SubtitlesButton); +export default SubtitlesButton; \ No newline at end of file diff --git a/src/js/control-bar/text-track-controls/text-track-button.js b/src/js/control-bar/text-track-controls/text-track-button.js new file mode 100644 index 0000000000..d0d097ddef --- /dev/null +++ b/src/js/control-bar/text-track-controls/text-track-button.js @@ -0,0 +1,65 @@ +import MenuButton from '../../menu/menu-button.js'; +import * as Lib from '../../lib.js'; + +import TextTrackMenuItem from './text-track-menu-item.js'; +import OffTextTrackMenuItem from './off-text-track-menu-item.js'; + +/** + * The base class for buttons that toggle specific text track types (e.g. subtitles) + * + * @constructor + */ +class TextTrackButton extends MenuButton { + + constructor(player, options){ + super(player, options); + + let tracks = this.player_.textTracks(); + + if (this.items.length <= 1) { + this.hide(); + } + + if (!tracks) { + return; + } + + let updateHandler = Lib.bind(this, this.update); + tracks.addEventListener('removetrack', updateHandler); + tracks.addEventListener('addtrack', updateHandler); + + this.player_.on('dispose', function() { + tracks.removeEventListener('removetrack', updateHandler); + tracks.removeEventListener('addtrack', updateHandler); + }); + } + + // Create a menu item for each text track + createItems(items=[]) { + // Add an OFF menu item to turn all tracks off + items.push(new OffTextTrackMenuItem(this.player_, { 'kind': this.kind_ })); + + let tracks = this.player_.textTracks(); + + if (!tracks) { + return items; + } + + for (let i = 0; i < tracks.length; i++) { + let track = tracks[i]; + + // only add tracks that are of the appropriate kind and have a label + if (track['kind'] === this.kind_) { + items.push(new TextTrackMenuItem(this.player_, { + 'track': track + })); + } + } + + return items; + } + +} + +MenuButton.registerComponent('TextTrackButton', TextTrackButton); +export default TextTrackButton; \ No newline at end of file diff --git a/src/js/control-bar/text-track-controls/text-track-menu-item.js b/src/js/control-bar/text-track-controls/text-track-menu-item.js new file mode 100644 index 0000000000..41a74e262c --- /dev/null +++ b/src/js/control-bar/text-track-controls/text-track-menu-item.js @@ -0,0 +1,91 @@ +import MenuItem from '../../menu/menu-item.js'; +import * as Lib from '../../lib.js'; + +import window from 'global/window'; +import document from 'global/document'; + +/** + * The specific menu item type for selecting a language within a text track kind + * + * @constructor + */ +class TextTrackMenuItem extends MenuItem { + + constructor(player, options){ + let track = options['track']; + let tracks = player.textTracks(); + + // Modify options for parent MenuItem class's init. + options['label'] = track['label'] || track['language'] || 'Unknown'; + options['selected'] = track['default'] || track['mode'] === 'showing'; + super(player, options); + + this.track = track; + + if (tracks) { + let changeHandler = Lib.bind(this, this.handleTracksChange); + + tracks.addEventListener('change', changeHandler); + this.on('dispose', function() { + tracks.removeEventListener('change', changeHandler); + }); + } + + // iOS7 doesn't dispatch change events to TextTrackLists when an + // associated track's mode changes. Without something like + // Object.observe() (also not present on iOS7), it's not + // possible to detect changes to the mode attribute and polyfill + // the change event. As a poor substitute, we manually dispatch + // change events whenever the controls modify the mode. + if (tracks && tracks.onchange === undefined) { + let event; + + this.on(['tap', 'click'], function() { + if (typeof window.Event !== 'object') { + // Android 2.3 throws an Illegal Constructor error for window.Event + try { + event = new window.Event('change'); + } catch(err){} + } + + if (!event) { + event = document.createEvent('Event'); + event.initEvent('change', true, true); + } + + tracks.dispatchEvent(event); + }); + } + } + + onClick(event) { + let kind = this.track['kind']; + let tracks = this.player_.textTracks(); + + super.onClick(event); + + if (!tracks) return; + + for (let i = 0; i < tracks.length; i++) { + let track = tracks[i]; + + if (track['kind'] !== kind) { + continue; + } + + if (track === this.track) { + track['mode'] = 'showing'; + } else { + track['mode'] = 'disabled'; + } + } + } + + handleTracksChange(event){ + this.selected(this.track['mode'] === 'showing'); + } + +} + +MenuItem.registerComponent('TextTrackMenuItem', TextTrackMenuItem); +export default TextTrackMenuItem; \ No newline at end of file diff --git a/src/js/control-bar/time-display.js b/src/js/control-bar/time-display.js deleted file mode 100644 index 297df4202a..0000000000 --- a/src/js/control-bar/time-display.js +++ /dev/null @@ -1,154 +0,0 @@ -import Component from '../component'; -import * as Lib from '../lib'; - -/** - * Displays the current time - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -let CurrentTimeDisplay = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - - this.on(player, 'timeupdate', this.updateContent); - } -}); - -Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); - -CurrentTimeDisplay.prototype.createEl = function(){ - let el = Component.prototype.createEl.call(this, 'div', { - className: 'vjs-current-time vjs-time-controls vjs-control' - }); - - this.contentEl_ = Lib.createEl('div', { - className: 'vjs-current-time-display', - innerHTML: 'Current Time ' + '0:00', // label the current time for screen reader users - 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes - }); - - el.appendChild(this.contentEl_); - return el; -}; - -CurrentTimeDisplay.prototype.updateContent = function(){ - // Allows for smooth scrubbing, when player can't keep up. - let time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); - this.contentEl_.innerHTML = '' + this.localize('Current Time') + ' ' + Lib.formatTime(time, this.player_.duration()); -}; - -/** - * Displays the duration - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var DurationDisplay = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - - // this might need to be changed to 'durationchange' instead of 'timeupdate' eventually, - // however the durationchange event fires before this.player_.duration() is set, - // so the value cannot be written out using this method. - // Once the order of durationchange and this.player_.duration() being set is figured out, - // this can be updated. - this.on(player, 'timeupdate', this.updateContent); - } -}); - -Component.registerComponent('DurationDisplay', DurationDisplay); - -DurationDisplay.prototype.createEl = function(){ - let el = Component.prototype.createEl.call(this, 'div', { - className: 'vjs-duration vjs-time-controls vjs-control' - }); - - this.contentEl_ = Lib.createEl('div', { - className: 'vjs-duration-display', - innerHTML: '' + this.localize('Duration Time') + ' ' + '0:00', // label the duration time for screen reader users - 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes - }); - - el.appendChild(this.contentEl_); - return el; -}; - -DurationDisplay.prototype.updateContent = function(){ - let duration = this.player_.duration(); - if (duration) { - this.contentEl_.innerHTML = '' + this.localize('Duration Time') + ' ' + Lib.formatTime(duration); // label the duration time for screen reader users - } -}; - -/** - * The separator between the current time and duration - * - * Can be hidden if it's not needed in the design. - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var TimeDivider = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - } -}); - -Component.registerComponent('TimeDivider', TimeDivider); - -TimeDivider.prototype.createEl = function(){ - return Component.prototype.createEl.call(this, 'div', { - className: 'vjs-time-divider', - innerHTML: '
/
' - }); -}; - -/** - * Displays the time left in the video - * @param {Player|Object} player - * @param {Object=} options - * @constructor - */ -var RemainingTimeDisplay = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - - this.on(player, 'timeupdate', this.updateContent); - } -}); - -Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); - -RemainingTimeDisplay.prototype.createEl = function(){ - let el = Component.prototype.createEl.call(this, 'div', { - className: 'vjs-remaining-time vjs-time-controls vjs-control' - }); - - this.contentEl_ = Lib.createEl('div', { - className: 'vjs-remaining-time-display', - innerHTML: '' + this.localize('Remaining Time') + ' ' + '-0:00', // label the remaining time for screen reader users - 'aria-live': 'off' // tell screen readers not to automatically read the time as it changes - }); - - el.appendChild(this.contentEl_); - return el; -}; - -RemainingTimeDisplay.prototype.updateContent = function(){ - if (this.player_.duration()) { - this.contentEl_.innerHTML = '' + this.localize('Remaining Time') + ' ' + '-'+ Lib.formatTime(this.player_.remainingTime()); - } - - // Allows for smooth scrubbing, when player can't keep up. - // var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime(); - // this.contentEl_.innerHTML = vjs.formatTime(time, this.player_.duration()); -}; - -export default CurrentTimeDisplay; -export { DurationDisplay, TimeDivider, RemainingTimeDisplay }; diff --git a/src/js/control-bar/time-divider.js b/src/js/control-bar/time-divider.js new file mode 100644 index 0000000000..e3c4996f4a --- /dev/null +++ b/src/js/control-bar/time-divider.js @@ -0,0 +1,24 @@ +import Component from '../component.js'; + +/** + * The separator between the current time and duration + * + * Can be hidden if it's not needed in the design. + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class TimeDivider extends Component { + + createEl() { + return super.createEl('div', { + className: 'vjs-time-divider', + innerHTML: '
/
' + }); + } + +} + +Component.registerComponent('TimeDivider', TimeDivider); +export default TimeDivider; \ No newline at end of file diff --git a/src/js/control-bar/volume-control.js b/src/js/control-bar/volume-control.js deleted file mode 100644 index 7950948f31..0000000000 --- a/src/js/control-bar/volume-control.js +++ /dev/null @@ -1,155 +0,0 @@ -import Component from '../component'; -import * as Lib from '../lib'; -import Slider, { SliderHandle } from '../slider'; - -/** - * The component for controlling the volume level - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -let VolumeControl = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - - // hide volume controls when they're not supported by the current tech - if (player.tech && player.tech['featuresVolumeControl'] === false) { - this.addClass('vjs-hidden'); - } - this.on(player, 'loadstart', function(){ - if (player.tech['featuresVolumeControl'] === false) { - this.addClass('vjs-hidden'); - } else { - this.removeClass('vjs-hidden'); - } - }); - } -}); - -Component.registerComponent('VolumeControl', VolumeControl); - -VolumeControl.prototype.options_ = { - children: { - 'volumeBar': {} - } -}; - -VolumeControl.prototype.createEl = function(){ - return Component.prototype.createEl.call(this, 'div', { - className: 'vjs-volume-control vjs-control' - }); -}; - -/** - * The bar that contains the volume level and can be clicked on to adjust the level - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var VolumeBar = Slider.extend({ - /** @constructor */ - init: function(player, options){ - Slider.call(this, player, options); - this.on(player, 'volumechange', this.updateARIAAttributes); - player.ready(Lib.bind(this, this.updateARIAAttributes)); - } -}); - -Component.registerComponent('VolumeBar', VolumeBar); - -VolumeBar.prototype.updateARIAAttributes = function(){ - // Current value of volume bar as a percentage - this.el_.setAttribute('aria-valuenow', Lib.round(this.player_.volume()*100, 2)); - this.el_.setAttribute('aria-valuetext', Lib.round(this.player_.volume()*100, 2)+'%'); -}; - -VolumeBar.prototype.options_ = { - children: { - 'volumeLevel': {}, - 'volumeHandle': {} - }, - 'barName': 'volumeLevel', - 'handleName': 'volumeHandle' -}; - -VolumeBar.prototype.playerEvent = 'volumechange'; - -VolumeBar.prototype.createEl = function(){ - return Slider.prototype.createEl.call(this, 'div', { - className: 'vjs-volume-bar', - 'aria-label': 'volume level' - }); -}; - -VolumeBar.prototype.onMouseMove = function(event) { - if (this.player_.muted()) { - this.player_.muted(false); - } - - this.player_.volume(this.calculateDistance(event)); -}; - -VolumeBar.prototype.getPercent = function(){ - if (this.player_.muted()) { - return 0; - } else { - return this.player_.volume(); - } -}; - -VolumeBar.prototype.stepForward = function(){ - this.player_.volume(this.player_.volume() + 0.1); -}; - -VolumeBar.prototype.stepBack = function(){ - this.player_.volume(this.player_.volume() - 0.1); -}; - -/** - * Shows volume level - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var VolumeLevel = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - } -}); - -Component.registerComponent('VolumeLevel', VolumeLevel); - -VolumeLevel.prototype.createEl = function(){ - return Component.prototype.createEl.call(this, 'div', { - className: 'vjs-volume-level', - innerHTML: '' - }); -}; - -/** - * The volume handle can be dragged to adjust the volume level - * - * @param {vjs.Player|Object} player - * @param {Object=} options - * @constructor - */ -var VolumeHandle = SliderHandle.extend(); - -Component.registerComponent('VolumeHandle', VolumeHandle); - -VolumeHandle.prototype.defaultValue = '00:00'; - -/** @inheritDoc */ -VolumeHandle.prototype.createEl = function(){ - return SliderHandle.prototype.createEl.call(this, 'div', { - className: 'vjs-volume-handle' - }); -}; - -export default VolumeControl; -export { VolumeBar, VolumeLevel, VolumeHandle }; diff --git a/src/js/control-bar/volume-control/volume-bar.js b/src/js/control-bar/volume-control/volume-bar.js new file mode 100644 index 0000000000..8f6630f775 --- /dev/null +++ b/src/js/control-bar/volume-control/volume-bar.js @@ -0,0 +1,74 @@ +import Slider from '../../slider/slider.js'; +import * as Lib from '../../lib.js'; + +// Required children +import VolumeHandle from './volume-handle.js'; +import VolumeLevel from './volume-level.js'; + +/** + * The bar that contains the volume level and can be clicked on to adjust the level + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class VolumeBar extends Slider { + + constructor(player, options){ + super(player, options); + this.on(player, 'volumechange', this.updateARIAAttributes); + player.ready(Lib.bind(this, this.updateARIAAttributes)); + } + + createEl() { + return Slider.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-bar', + 'aria-label': 'volume level' + }); + } + + onMouseMove(event) { + if (this.player_.muted()) { + this.player_.muted(false); + } + + this.player_.volume(this.calculateDistance(event)); + } + + getPercent() { + if (this.player_.muted()) { + return 0; + } else { + return this.player_.volume(); + } + } + + stepForward() { + this.player_.volume(this.player_.volume() + 0.1); + } + + stepBack() { + this.player_.volume(this.player_.volume() - 0.1); + } + + updateARIAAttributes() { + // Current value of volume bar as a percentage + this.el_.setAttribute('aria-valuenow', Lib.round(this.player_.volume()*100, 2)); + this.el_.setAttribute('aria-valuetext', Lib.round(this.player_.volume()*100, 2)+'%'); + } + +} + +VolumeBar.prototype.options_ = { + children: { + 'volumeLevel': {}, + 'volumeHandle': {} + }, + 'barName': 'volumeLevel', + 'handleName': 'volumeHandle' +}; + +VolumeBar.prototype.playerEvent = 'volumechange'; + +Slider.registerComponent('VolumeBar', VolumeBar); +export default VolumeBar; \ No newline at end of file diff --git a/src/js/control-bar/volume-control/volume-control.js b/src/js/control-bar/volume-control/volume-control.js new file mode 100644 index 0000000000..b3400838cd --- /dev/null +++ b/src/js/control-bar/volume-control/volume-control.js @@ -0,0 +1,47 @@ +import Component from '../../component.js'; +import * as Lib from '../../lib.js'; + +// Required children +import VolumeBar from './volume-bar.js'; + +/** + * The component for controlling the volume level + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class VolumeControl extends Component { + + constructor(player, options){ + super(player, options); + + // hide volume controls when they're not supported by the current tech + if (player.tech && player.tech['featuresVolumeControl'] === false) { + this.addClass('vjs-hidden'); + } + this.on(player, 'loadstart', function(){ + if (player.tech['featuresVolumeControl'] === false) { + this.addClass('vjs-hidden'); + } else { + this.removeClass('vjs-hidden'); + } + }); + } + + createEl() { + return Component.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-control vjs-control' + }); + } + +} + +VolumeControl.prototype.options_ = { + children: { + 'volumeBar': {} + } +}; + +Component.registerComponent('VolumeControl', VolumeControl); +export default VolumeControl; diff --git a/src/js/control-bar/volume-control/volume-handle.js b/src/js/control-bar/volume-control/volume-handle.js new file mode 100644 index 0000000000..9382574fbd --- /dev/null +++ b/src/js/control-bar/volume-control/volume-handle.js @@ -0,0 +1,24 @@ +import SliderHandle from '../../slider/slider-handle.js'; + +/** + * The volume handle can be dragged to adjust the volume level + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class VolumeHandle extends SliderHandle { + + /** @inheritDoc */ + createEl() { + return super.createEl('div', { + className: 'vjs-volume-handle' + }); + } + +} + +VolumeHandle.prototype.defaultValue = '00:00'; + +SliderHandle.registerComponent('VolumeHandle', VolumeHandle); +export default VolumeHandle; \ No newline at end of file diff --git a/src/js/control-bar/volume-control/volume-level.js b/src/js/control-bar/volume-control/volume-level.js new file mode 100644 index 0000000000..de25e95d32 --- /dev/null +++ b/src/js/control-bar/volume-control/volume-level.js @@ -0,0 +1,22 @@ +import Component from '../../component.js'; + +/** + * Shows volume level + * + * @param {vjs.Player|Object} player + * @param {Object=} options + * @constructor + */ +class VolumeLevel extends Component { + + createEl() { + return super.createEl('div', { + className: 'vjs-volume-level', + innerHTML: '' + }); + } + +} + +Component.registerComponent('VolumeLevel', VolumeLevel); +export default VolumeLevel; \ No newline at end of file diff --git a/src/js/control-bar/volume-menu-button.js b/src/js/control-bar/volume-menu-button.js index 34f078d8cf..a779a519de 100644 --- a/src/js/control-bar/volume-menu-button.js +++ b/src/js/control-bar/volume-menu-button.js @@ -1,18 +1,18 @@ -import Button from '../button'; -import Component from '../component'; -import Menu, { MenuButton } from '../menu'; -import MuteToggle from './mute-toggle'; -import * as Lib from '../lib'; -import { VolumeBar } from './volume-control'; +import Button from '../button.js'; +import Menu from '../menu/menu.js'; +import MenuButton from '../menu/menu-button.js'; +import MuteToggle from './mute-toggle.js'; +import * as Lib from '../lib.js'; +import VolumeBar from './volume-control/volume-bar.js'; /** * Menu button with a popup for showing the volume slider. * @constructor */ -let VolumeMenuButton = MenuButton.extend({ - /** @constructor */ - init: function(player, options){ - MenuButton.call(this, player, options); +class VolumeMenuButton extends MenuButton { + + constructor(player, options){ + super(player, options); // Same listeners as MuteToggle this.on(player, 'volumechange', this.volumeUpdate); @@ -30,36 +30,37 @@ let VolumeMenuButton = MenuButton.extend({ }); this.addClass('vjs-menu-button'); } -}); -VolumeMenuButton.prototype.createMenu = function(){ - let menu = new Menu(this.player_, { - contentElType: 'div' - }); - let vc = new VolumeBar(this.player_, this.options_['volumeBar']); - vc.on('focus', function() { - menu.lockShowing(); - }); - vc.on('blur', function() { - menu.unlockShowing(); - }); - menu.addChild(vc); - return menu; -}; + createMenu() { + let menu = new Menu(this.player_, { + contentElType: 'div' + }); + let vc = new VolumeBar(this.player_, this.options_['volumeBar']); + vc.on('focus', function() { + menu.lockShowing(); + }); + vc.on('blur', function() { + menu.unlockShowing(); + }); + menu.addChild(vc); + return menu; + } -VolumeMenuButton.prototype.onClick = function(){ - MuteToggle.prototype.onClick.call(this); - MenuButton.prototype.onClick.call(this); -}; + onClick() { + MuteToggle.prototype.onClick.call(this); + MenuButton.prototype.onClick.call(this); + } + + createEl() { + return super.createEl('div', { + className: 'vjs-volume-menu-button vjs-menu-button vjs-control', + innerHTML: '
' + this.localize('Mute') + '
' + }); + } -VolumeMenuButton.prototype.createEl = function(){ - return Button.prototype.createEl.call(this, 'div', { - className: 'vjs-volume-menu-button vjs-menu-button vjs-control', - innerHTML: '
' + this.localize('Mute') + '
' - }); -}; +} VolumeMenuButton.prototype.volumeUpdate = MuteToggle.prototype.update; -Component.registerComponent('VolumeMenuButton', VolumeMenuButton); +Button.registerComponent('VolumeMenuButton', VolumeMenuButton); export default VolumeMenuButton; diff --git a/src/js/error-display.js b/src/js/error-display.js index dc954f8908..597fcfdbfa 100644 --- a/src/js/error-display.js +++ b/src/js/error-display.js @@ -7,32 +7,32 @@ import * as Lib from './lib'; * @param {Object=} options * @constructor */ -let ErrorDisplay = Component.extend({ - init: function(player, options){ - Component.call(this, player, options); +class ErrorDisplay extends Component { + + constructor(player, options) { + super(player, options); this.update(); this.on(player, 'error', this.update); } -}); - -Component.registerComponent('ErrorDisplay', ErrorDisplay); -ErrorDisplay.prototype.createEl = function(){ - var el = Component.prototype.createEl.call(this, 'div', { - className: 'vjs-error-display' - }); + createEl() { + var el = super.createEl('div', { + className: 'vjs-error-display' + }); - this.contentEl_ = Lib.createEl('div'); - el.appendChild(this.contentEl_); + this.contentEl_ = Lib.createEl('div'); + el.appendChild(this.contentEl_); - return el; -}; + return el; + } -ErrorDisplay.prototype.update = function(){ - if (this.player().error()) { - this.contentEl_.innerHTML = this.localize(this.player().error().message); + update() { + if (this.player().error()) { + this.contentEl_.innerHTML = this.localize(this.player().error().message); + } } -}; +} +Component.registerComponent('ErrorDisplay', ErrorDisplay); export default ErrorDisplay; diff --git a/src/js/loading-spinner.js b/src/js/loading-spinner.js index f3812f19a0..3f70b084f0 100644 --- a/src/js/loading-spinner.js +++ b/src/js/loading-spinner.js @@ -9,39 +9,13 @@ import Component from './component'; * @class * @constructor */ -let LoadingSpinner = Component.extend({ - /** @constructor */ - init: function(player, options){ - Component.call(this, player, options); - - // MOVING DISPLAY HANDLING TO CSS - - // player.on('canplay', vjs.bind(this, this.hide)); - // player.on('canplaythrough', vjs.bind(this, this.hide)); - // player.on('playing', vjs.bind(this, this.hide)); - // player.on('seeking', vjs.bind(this, this.show)); - - // in some browsers seeking does not trigger the 'playing' event, - // so we also need to trap 'seeked' if we are going to set a - // 'seeking' event - // player.on('seeked', vjs.bind(this, this.hide)); - - // player.on('ended', vjs.bind(this, this.hide)); - - // Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner. - // Checked in Chrome 16 and Safari 5.1.2. http://help.videojs.com/discussions/problems/883-why-is-the-download-progress-showing - // player.on('stalled', vjs.bind(this, this.show)); - - // player.on('waiting', vjs.bind(this, this.show)); +class LoadingSpinner extends Component { + createEl() { + return super.createEl('div', { + className: 'vjs-loading-spinner' + }); } -}); +} Component.registerComponent('LoadingSpinner', LoadingSpinner); - -LoadingSpinner.prototype.createEl = function(){ - return Component.prototype.createEl.call(this, 'div', { - className: 'vjs-loading-spinner' - }); -}; - export default LoadingSpinner; diff --git a/src/js/media/html5.js b/src/js/media/html5.js deleted file mode 100644 index 5664758f0e..0000000000 --- a/src/js/media/html5.js +++ /dev/null @@ -1,684 +0,0 @@ -/** - * @fileoverview HTML5 Media Controller - Wrapper for HTML5 Media API - */ - -import MediaTechController from './media'; -import Component from '../component'; -import * as Lib from '../lib'; -import * as VjsUtil from '../util'; -import document from 'global/document'; - -/** - * HTML5 Media Controller - Wrapper for HTML5 Media API - * @param {vjs.Player|Object} player - * @param {Object=} options - * @param {Function=} ready - * @constructor - */ -var Html5 = MediaTechController.extend({ - /** @constructor */ - init: function(player, options, ready){ - if (options['nativeCaptions'] === false || options['nativeTextTracks'] === false) { - this['featuresNativeTextTracks'] = false; - } - - MediaTechController.call(this, player, options, ready); - - this.setupTriggers(); - - const source = options['source']; - - // Set the source if one is provided - // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) - // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source - // anyway so the error gets fired. - if (source && (this.el_.currentSrc !== source.src || (player.tag && player.tag.initNetworkState_ === 3))) { - this.setSource(source); - } - - if (this.el_.hasChildNodes()) { - - let nodes = this.el_.childNodes; - let nodesLength = nodes.length; - let removeNodes = []; - - while (nodesLength--) { - let node = nodes[nodesLength]; - let nodeName = node.nodeName.toLowerCase(); - if (nodeName === 'track') { - if (!this['featuresNativeTextTracks']) { - // Empty video tag tracks so the built-in player doesn't use them also. - // This may not be fast enough to stop HTML5 browsers from reading the tags - // so we'll need to turn off any default tracks if we're manually doing - // captions and subtitles. videoElement.textTracks - removeNodes.push(node); - } else { - this.remoteTextTracks().addTrack_(node['track']); - } - } - } - - for (let i=0; i