diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dfa79e7e..f98243d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ CHANGELOG * @heff added support for fluid widths, aspect ratios, and metadata defaults ([view](https://github.com/videojs/video.js/pull/1952)) * @heff reorganized all utility functions in the codebase ([view](https://github.com/videojs/video.js/pull/2139)) * @eXon made additional tech 2.0 improvements listed in #2126 ([view](https://github.com/videojs/video.js/pull/2166)) +* @heff Cleaned up and documented src/js/video.js and DOM functions ([view](https://github.com/videojs/video.js/pull/2182)) -------------------- diff --git a/package.json b/package.json index c91f7b1e7..c65b8a096 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "style": "./dist/video-js.css", "dependencies": { "global": "^4.3.0", - "lodash.clonedeep": "^3.0.0", "lodash.isplainobject": "^3.0.2", "lodash.merge": "^3.2.1", "object.assign": "^2.0.1", diff --git a/src/js/component.js b/src/js/component.js index 52b8d149c..5b63b3d7f 100644 --- a/src/js/component.js +++ b/src/js/component.js @@ -13,6 +13,7 @@ import toTitleCase from './utils/to-title-case.js'; import assign from 'object.assign'; import mergeOptions from './utils/merge-options.js'; + /** * Base UI Component class * @@ -52,8 +53,8 @@ class Component { this.player_ = player; } - // Make a copy of prototype.options_ to protect against overriding global defaults - this.options_ = assign({}, this.options_); + // Make a copy of prototype.options_ to protect against overriding defaults + this.options_ = mergeOptions({}, this.options_); // Updated options with supplied options options = this.options(options); @@ -130,7 +131,7 @@ class Component { this.el_.parentNode.removeChild(this.el_); } - Dom.removeData(this.el_); + Dom.removeElData(this.el_); this.el_ = null; } @@ -741,7 +742,7 @@ class Component { * @return {Component} */ hasClass(classToCheck) { - return Dom.hasClass(this.el_, classToCheck); + return Dom.hasElClass(this.el_, classToCheck); } /** @@ -751,7 +752,7 @@ class Component { * @return {Component} */ addClass(classToAdd) { - Dom.addClass(this.el_, classToAdd); + Dom.addElClass(this.el_, classToAdd); return this; } @@ -762,7 +763,7 @@ class Component { * @return {Component} */ removeClass(classToRemove) { - Dom.removeClass(this.el_, classToRemove); + Dom.removeElClass(this.el_, classToRemove); return this; } @@ -918,19 +919,6 @@ class Component { // If component has display:none, offset will return 0 // TODO: handle display:none and no dimension style using px return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10); - - // 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 = Dom.getComputedStyleValue(this.el_, widthOrHeight); - // var pxIndex = val.indexOf('px'); - - // if (pxIndex !== -1) { - // return val.slice(0, pxIndex); - // } else { - // return val; - // } } /** diff --git a/src/js/control-bar/mute-toggle.js b/src/js/control-bar/mute-toggle.js index a3cf4872b..7f2d0f469 100644 --- a/src/js/control-bar/mute-toggle.js +++ b/src/js/control-bar/mute-toggle.js @@ -68,9 +68,9 @@ class MuteToggle extends Button { /* TODO improve muted icon classes */ for (var i = 0; i < 4; i++) { - Dom.removeClass(this.el_, `vjs-vol-${i}`); + Dom.removeElClass(this.el_, `vjs-vol-${i}`); } - Dom.addClass(this.el_, `vjs-vol-${level}`); + Dom.addElClass(this.el_, `vjs-vol-${level}`); } } diff --git a/src/js/options.js b/src/js/global-options.js similarity index 100% rename from src/js/options.js rename to src/js/global-options.js diff --git a/src/js/player.js b/src/js/player.js index 4f870a0c0..73fdd1be1 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -1,4 +1,8 @@ +// Subclasses Component import Component from './component.js'; + +import document from 'global/document'; +import window from 'global/window'; import * as Events from './utils/events.js'; import * as Dom from './utils/dom.js'; import * as Fn from './utils/fn.js'; @@ -10,23 +14,22 @@ import { createTimeRange } from './utils/time-ranges.js'; import { bufferedPercent } from './utils/buffer.js'; import FullscreenApi from './fullscreen-api.js'; import MediaError from './media-error.js'; -import Options from './options.js'; +import globalOptions from './global-options.js'; import safeParseTuple from 'safe-json-parse/tuple'; -import window from 'global/window'; -import document from 'global/document'; import assign from 'object.assign'; import mergeOptions from './utils/merge-options.js'; -// Include required child components +// Include required child components (importing also registers them) import MediaLoader from './tech/loader.js'; -import Poster from './poster-image.js'; +import PosterImage from './poster-image.js'; import TextTrackDisplay from './tracks/text-track-display.js'; import LoadingSpinner from './loading-spinner.js'; import BigPlayButton from './big-play-button.js'; -import controlBar from './control-bar/control-bar.js'; +import ControlBar from './control-bar/control-bar.js'; import ErrorDisplay from './error-display.js'; import TextTrackSettings from './tracks/text-track-settings.js'; -// Require html5 for disposing the original video tag + +// Require html5 tech, at least for disposing the original video tag import Html5 from './tech/html5.js'; /** @@ -99,13 +102,13 @@ class Player extends Component { this.tag = tag; // Store the original tag used to set options // Store the tag attributes used to restore html5 element - this.tagAttributes = tag && Dom.getElementAttributes(tag); + this.tagAttributes = tag && Dom.getElAttributes(tag); // Update Current Language - this.language_ = options['language'] || Options['language']; + this.language_ = options['language'] || globalOptions['language']; // Update Supported Languages - this.languages_ = options['languages'] || Options['languages']; + this.languages_ = options['languages'] || globalOptions['languages']; // Cache for video property values. this.cache_ = {}; @@ -211,7 +214,7 @@ class Player extends Component { // Copy over all the attributes from the tag, including ID and class // ID will now reference player box, not the video tag - const attrs = Dom.getElementAttributes(tag); + const attrs = Dom.getElAttributes(tag); Object.getOwnPropertyNames(attrs).forEach(function(attr){ // workaround so we don't totally break IE7 @@ -246,7 +249,7 @@ class Player extends Component { this.fluid(this.options_['fluid']); this.aspectRatio(this.options_['aspectRatio']); - // insertFirst seems to cause the networkState to flicker from 3 to 2, so + // insertElFirst seems to cause the networkState to flicker from 3 to 2, so // keep track of the original for later so we can know if the source originally failed tag.initNetworkState_ = tag.networkState; @@ -254,7 +257,7 @@ class Player extends Component { if (tag.parentNode) { tag.parentNode.insertBefore(el, tag); } - Dom.insertFirst(tag, el); // Breaks iPhone, fixed in HTML5 setup. + Dom.insertElFirst(tag, el); // Breaks iPhone, fixed in HTML5 setup. this.el_ = el; @@ -479,7 +482,7 @@ class Player extends Component { // Add the tech element in the DOM if it was not already there // Make sure to not insert the original video element if using Html5 if (this.tech.el().parentNode !== this.el() && (techName !== 'Html5' || !this.tag)) { - Dom.insertFirst(this.tech.el(), this.el()); + Dom.insertElFirst(this.tech.el(), this.el()); } // Get rid of the original video tag reference after the first tech is loaded @@ -1376,7 +1379,7 @@ class Player extends Component { document.documentElement.style.overflow = 'hidden'; // Apply fullscreen styles - Dom.addClass(document.body, 'vjs-full-window'); + Dom.addElClass(document.body, 'vjs-full-window'); this.trigger('enterFullWindow'); } @@ -1399,7 +1402,7 @@ class Player extends Component { document.documentElement.style.overflow = this.docOrigOverflow; // Remove fullscreen styles - Dom.removeClass(document.body, 'vjs-full-window'); + Dom.removeElClass(document.body, 'vjs-full-window'); // Resize the box, controller, and poster to original sizes // this.positionAll(); @@ -2116,7 +2119,7 @@ class Player extends Component { 'tracks': [] }; - const tagOptions = Dom.getElementAttributes(tag); + const tagOptions = Dom.getElAttributes(tag); const dataSetup = tagOptions['data-setup']; // Check if data-setup attr exists. @@ -2141,9 +2144,9 @@ class Player extends Component { // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ const childName = child.nodeName.toLowerCase(); if (childName === 'source') { - baseOptions['sources'].push(Dom.getElementAttributes(child)); + baseOptions['sources'].push(Dom.getElAttributes(child)); } else if (childName === 'track') { - baseOptions['tracks'].push(Dom.getElementAttributes(child)); + baseOptions['tracks'].push(Dom.getElAttributes(child)); } } } @@ -2168,7 +2171,7 @@ Player.players = {}; * @type {Object} * @private */ -Player.prototype.options_ = Options; +Player.prototype.options_ = globalOptions; /** * Fired when the player has initial duration and dimension information diff --git a/src/js/plugins.js b/src/js/plugins.js index bd667e105..7dca6ecaf 100644 --- a/src/js/plugins.js +++ b/src/js/plugins.js @@ -1,7 +1,7 @@ -import Player from './player'; +import Player from './player.js'; /** - * the method for registering a video.js plugin + * The method for registering a video.js plugin * * @param {String} name The name of the plugin * @param {Function} init The function that is run when the player inits diff --git a/src/js/slider/slider.js b/src/js/slider/slider.js index 70612ed0d..19348f7a7 100644 --- a/src/js/slider/slider.js +++ b/src/js/slider/slider.js @@ -160,7 +160,7 @@ class Slider extends Component { calculateDistance(event){ let el = this.el_; - let box = Dom.findPosition(el); + let box = Dom.findElPosition(el); let boxW = el.offsetWidth; let boxH = el.offsetHeight; let handle = this.handle; diff --git a/src/js/tech/flash.js b/src/js/tech/flash.js index 2020e3de0..17c3fa665 100644 --- a/src/js/tech/flash.js +++ b/src/js/tech/flash.js @@ -267,7 +267,7 @@ Flash.formats = { }; Flash.onReady = function(currSwf){ - let el = Dom.el(currSwf); + let el = Dom.getEl(currSwf); let tech = el && el.tech; // if there is no el then the tech has been disposed @@ -300,13 +300,13 @@ Flash.checkReady = function(tech){ // Trigger events from the swf on the player Flash.onEvent = function(swfID, eventName){ - let tech = Dom.el(swfID).tech; + let tech = Dom.getEl(swfID).tech; tech.trigger(eventName); }; // Log errors from the swf Flash.onError = function(swfID, err){ - const tech = Dom.el(swfID).tech; + const tech = Dom.getEl(swfID).tech; const msg = 'FLASH: '+err; if (err === 'srcnotfound') { diff --git a/src/js/tech/html5.js b/src/js/tech/html5.js index 111b6aa76..f4a2d556a 100644 --- a/src/js/tech/html5.js +++ b/src/js/tech/html5.js @@ -101,13 +101,13 @@ class Html5 extends Tech { el = document.createElement('video'); // determine if native controls should be used - let tagAttributes = this.options_.tag && Dom.getElementAttributes(this.options_.tag); + let tagAttributes = this.options_.tag && Dom.getElAttributes(this.options_.tag); let attributes = mergeOptions({}, tagAttributes); if (!browser.TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) { delete attributes.controls; } - Dom.setElementAttributes(el, + Dom.setElAttributes(el, assign(attributes, { id: this.options_.techId, class: 'vjs-tech' @@ -139,7 +139,7 @@ class Html5 extends Tech { if (typeof this.options_[attr] !== 'undefined') { overwriteAttrs[attr] = this.options_[attr]; } - Dom.setElementAttributes(el, overwriteAttrs); + Dom.setElAttributes(el, overwriteAttrs); } return el; diff --git a/src/js/utils/dom.js b/src/js/utils/dom.js index 0ac2b3754..8a506d717 100644 --- a/src/js/utils/dom.js +++ b/src/js/utils/dom.js @@ -8,24 +8,22 @@ import roundFloat from './round-float.js'; * Also allows for CSS (jQuery) ID syntax. But nothing other than IDs. * @param {String} id Element ID * @return {Element} Element with supplied ID - * @private */ -export const el = function(id){ +export function getEl(id){ if (id.indexOf('#') === 0) { id = id.slice(1); } return document.getElementById(id); -}; +} /** * Creates an element and applies properties. * @param {String=} tagName Name of tag to be created. * @param {Object=} properties Element properties to be applied. * @return {Element} - * @private */ -export const createEl = function(tagName='div', properties={}){ +export function createEl(tagName='div', properties={}){ let el = document.createElement(tagName); Object.getOwnPropertyNames(properties).forEach(function(propName){ @@ -47,7 +45,7 @@ export const createEl = function(tagName='div', properties={}){ }); return el; -}; +} /** * Insert an element as the first child node of another @@ -55,13 +53,13 @@ export const createEl = function(tagName='div', properties={}){ * @param {[type]} parent Element to insert child into * @private */ -export const insertFirst = function(child, parent){ +export function insertElFirst(child, parent){ if (parent.firstChild) { parent.insertBefore(child, parent.firstChild); } else { parent.appendChild(child); } -}; +} /** * Element Data Store. Allows for binding data to an element without putting it directly on the element. @@ -70,7 +68,7 @@ export const insertFirst = function(child, parent){ * @type {Object} * @private */ -export const cache = {}; +const elData = {}; /** * Unique attribute name to store an element's guid in @@ -78,24 +76,26 @@ export const cache = {}; * @constant * @private */ -export const expando = 'vdata' + (new Date()).getTime(); +const elIdAttr = 'vdata' + (new Date()).getTime(); /** * Returns the cache object where data for an element is stored * @param {Element} el Element to store data for. * @return {Object} - * @private */ -export const getData = function(el){ - var id = el[expando]; +export function getElData(el) { + let id = el[elIdAttr]; + if (!id) { - id = el[expando] = Guid.newGUID(); + id = el[elIdAttr] = Guid.newGUID(); } - if (!cache[id]) { - cache[id] = {}; + + if (!elData[id]) { + elData[id] = {}; } - return cache[id]; -}; + + return elData[id]; +} /** * Returns whether or not an element has cached data @@ -103,73 +103,71 @@ export const getData = function(el){ * @return {Boolean} * @private */ -export const hasData = function(el){ - const id = el[expando]; +export function hasElData(el) { + const id = el[elIdAttr]; if (!id) { return false; } - return !!Object.getOwnPropertyNames(cache[id]).length; -}; + return !!Object.getOwnPropertyNames(elData[id]).length; +} /** * Delete data for the element from the cache and the guid attr from getElementById * @param {Element} el Remove data for an element * @private */ -export const removeData = function(el){ - var id = el[expando]; - if (!id) { return; } - // Remove all stored data - // Changed to = null - // http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/ - // cache[id] = null; - delete cache[id]; +export function removeElData(el) { + let id = el[elIdAttr]; - // Remove the expando property from the DOM node + if (!id) { + return; + } + + // Remove all stored data + delete elData[id]; + + // Remove the elIdAttr property from the DOM node try { - delete el[expando]; + delete el[elIdAttr]; } catch(e) { if (el.removeAttribute) { - el.removeAttribute(expando); + el.removeAttribute(elIdAttr); } else { // IE doesn't appear to support removeAttribute on the document element - el[expando] = null; + el[elIdAttr] = null; } } -}; +} /** * Check if an element has a CSS class * @param {Element} element Element to check * @param {String} classToCheck Classname to check - * @private */ -export const hasClass = function(element, classToCheck){ +export function hasElClass(element, classToCheck) { return ((' ' + element.className + ' ').indexOf(' ' + classToCheck + ' ') !== -1); -}; +} /** * Add a CSS class name to an element * @param {Element} element Element to add class name to * @param {String} classToAdd Classname to add - * @private */ -export const addClass = function(element, classToAdd){ - if (!hasClass(element, classToAdd)) { +export function addElClass(element, classToAdd) { + if (!hasElClass(element, classToAdd)) { element.className = element.className === '' ? classToAdd : element.className + ' ' + classToAdd; } -}; +} /** * Remove a CSS class name from an element * @param {Element} element Element to remove from class name * @param {String} classToAdd Classname to remove - * @private */ -export const removeClass = function(element, classToRemove){ - if (!hasClass(element, classToRemove)) {return;} +export function removeElClass(element, classToRemove) { + if (!hasElClass(element, classToRemove)) {return;} let classNames = element.className.split(' '); @@ -181,7 +179,7 @@ export const removeClass = function(element, classToRemove){ } element.className = classNames.join(' '); -}; +} /** * Apply attributes to an HTML element. @@ -189,7 +187,7 @@ export const removeClass = function(element, classToRemove){ * @param {Object=} attributes Element attributes to be applied. * @private */ -export const setElementAttributes = function(el, attributes){ +export function setElAttributes(el, attributes) { Object.getOwnPropertyNames(attributes).forEach(function(attrName){ let attrValue = attributes[attrName]; @@ -199,7 +197,7 @@ export const setElementAttributes = function(el, attributes){ el.setAttribute(attrName, (attrValue === true ? '' : attrValue)); } }); -}; +} /** * Get an element's attribute values, as defined on the HTML tag @@ -210,7 +208,7 @@ export const setElementAttributes = function(el, attributes){ * @return {Object} * @private */ -export const getElementAttributes = function(tag){ +export function getElAttributes(tag) { var obj, knownBooleans, attrs, attrName, attrVal; obj = {}; @@ -241,40 +239,26 @@ export const getElementAttributes = function(tag){ } return obj; -}; - -/** - * Get the computed style value for an element - * From http://robertnyman.com/2006/04/24/get-the-rendered-style-of-an-element/ - * @param {Element} el Element to get style value for - * @param {String} strCssRule Style name - * @return {String} Style value - * @private - */ -export const getComputedDimension = function(el, strCssRule){ - var strValue = ''; - if(document.defaultView && document.defaultView.getComputedStyle){ - strValue = document.defaultView.getComputedStyle(el, '').getPropertyValue(strCssRule); - - } else if(el.currentStyle){ - // IE8 Width/Height support - let upperCasedRule = strCssRule.substr(0,1).toUpperCase() + strCssRule.substr(1); - strValue = el[`client${upperCasedRule}`] + 'px'; - } - return strValue; -}; +} // Attempt to block the ability to select text while dragging controls -export const blockTextSelection = function(){ +export function blockTextSelection() { document.body.focus(); - document.onselectstart = function () { return false; }; -}; + document.onselectstart = function() { + return false; + }; +} + // Turn off text selection blocking -export const unblockTextSelection = function(){ document.onselectstart = function () { return true; }; }; +export function unblockTextSelection() { + document.onselectstart = function() { + return true; + }; +} // Offset Left // getBoundingClientRect technique from John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/ -export const findPosition = function(el) { +export function findElPosition(el) { let box; if (el.getBoundingClientRect && el.parentNode) { @@ -304,4 +288,4 @@ export const findPosition = function(el) { left: roundFloat(left), top: roundFloat(top) }; -}; +} diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 9bd6d6883..92526636f 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -24,7 +24,7 @@ export function on(elem, type, fn){ return _handleMultipleEvents(on, elem, type, fn); } - let data = Dom.getData(elem); + let data = Dom.getElData(elem); // We need a place to store all our handler data if (!data.handlers) data.handlers = {}; @@ -76,10 +76,10 @@ export function on(elem, type, fn){ * @param {Function} fn Specific listener to remove. Don't include to remove listeners for an event type. */ export function off(elem, type, fn) { - // Don't want to add a cache object through getData if not needed - if (!Dom.hasData(elem)) return; + // Don't want to add a cache object through getElData if not needed + if (!Dom.hasElData(elem)) return; - let data = Dom.getData(elem); + let data = Dom.getElData(elem); // If no events exist, nothing to unbind if (!data.handlers) { return; } @@ -131,8 +131,8 @@ export function off(elem, type, fn) { export function trigger(elem, event) { // Fetches element data and a reference to the parent (for bubbling). // Don't want to add a data object to cache for every parent, - // so checking hasData first. - var elemData = (Dom.hasData(elem)) ? Dom.getData(elem) : {}; + // so checking hasElData first. + var elemData = (Dom.hasElData(elem)) ? Dom.getElData(elem) : {}; var parent = elem.parentNode || elem.ownerDocument; // type = event.type || event, // handler; @@ -156,7 +156,7 @@ export function trigger(elem, event) { // If at the top of the DOM, triggers the default action unless disabled. } else if (!parent && !event.defaultPrevented) { - var targetData = Dom.getData(event.target); + var targetData = Dom.getElData(event.target); // Checks if the target has a default action for this event. if (event.target[event.type]) { @@ -309,7 +309,7 @@ export function fixEvent(event) { * @private */ function _cleanUpEvents(elem, type) { - var data = Dom.getData(elem); + var data = Dom.getElData(elem); // Remove the events of a particular type if there are none left if (data.handlers[type].length === 0) { @@ -330,15 +330,11 @@ function _cleanUpEvents(elem, type) { delete data.handlers; delete data.dispatcher; delete data.disabled; - - // data.handlers = null; - // data.dispatcher = null; - // data.disabled = null; } - // Finally remove the expando if there is no data left - if (Object.getOwnPropertyNames(data).length <= 0) { - Dom.removeData(elem); + // Finally remove the element data if there is no data left + if (Object.getOwnPropertyNames(data).length === 0) { + Dom.removeElData(elem); } } diff --git a/src/js/utils/merge-options.js b/src/js/utils/merge-options.js index 934839522..7a1289661 100644 --- a/src/js/utils/merge-options.js +++ b/src/js/utils/merge-options.js @@ -1,33 +1,39 @@ import merge from 'lodash.merge'; import isPlainObject from 'lodash.isplainobject'; -import cloneDeep from 'lodash.clonedeep'; /** - * Merge two options objects, recursively merging any plain object properties as - * well. Previously `deepMerge` + * Merge two options objects, recursively merging **only** plain object + * properties. Previously `deepMerge`. * - * @param {Object} obj1 Object to override values in - * @param {Object} obj2 Overriding object - * @return {Object} New object -- obj1 and obj2 will be untouched + * @param {Object} object The destination object + * @param {...Object} source One or more objects to merge into the first + * + * @returns {Object} The updated first object */ -export default function mergeOptions(obj1){ - // Copy to ensure we're not modifying the defaults somewhere - obj1 = cloneDeep(obj1, function(value) { - if (!isPlainObject(value)) { - return value; - } - }); +export default function mergeOptions(object={}) { // Allow for infinite additional object args to merge - Array.prototype.slice.call(arguments, 1).forEach(function(argObj){ + Array.prototype.slice.call(arguments, 1).forEach(function(source){ + // Recursively merge only plain objects // All other values will be directly copied - merge(obj1, argObj, function(a, b) { - if (!isPlainObject(a) || !isPlainObject(b)) { + merge(object, source, function(a, b) { + + // If we're not working with a plain object, copy the value as is + if (!isPlainObject(b)) { return b; } + + // If the new value is a plain object but the first object value is not + // we need to create a new object for the first object to merge with. + // This makes it consistent with how merge() works by default + // and also protects from later changes the to first object affecting + // the second object's values. + if (!isPlainObject(a)) { + return mergeOptions({}, b); + } }); }); - return obj1; + return object; } diff --git a/src/js/video.js b/src/js/video.js index 563eb858c..ee36db09c 100644 --- a/src/js/video.js +++ b/src/js/video.js @@ -1,26 +1,22 @@ import document from 'global/document'; -import assign from 'object.assign'; -import MediaLoader from './tech/loader.js'; -import Html5 from './tech/html5.js'; -import Flash from './tech/flash.js'; -import PosterImage from './poster-image.js'; -import TextTrackDisplay from './tracks/text-track-display.js'; -import LoadingSpinner from './loading-spinner.js'; -import BigPlayButton from './big-play-button.js'; -import ControlBar from './control-bar/control-bar.js'; -import ErrorDisplay from './error-display.js'; import * as setup from './setup'; import Component from './component'; -import Options from './options'; -import * as Dom from './utils/dom.js'; -import log from './utils/log.js'; -import * as browser from './utils/browser.js'; +import globalOptions from './global-options.js'; import Player from './player'; -import extendsFn from './extends.js'; import plugin from './plugins.js'; -import options from './options.js'; import mergeOptions from '../../src/js/utils/merge-options.js'; +import assign from 'object.assign'; +import log from './utils/log.js'; +import * as Dom from './utils/dom.js'; +import * as browser from './utils/browser.js'; +import extendsFn from './extends.js'; +import merge from 'lodash.merge'; + +// Include the built-in techs +import Html5 from './tech/html5.js'; +import Flash from './tech/flash.js'; + // HTML5 Element Shim for IE8 if (typeof HTMLVideoElement === 'undefined') { document.createElement('video'); @@ -32,11 +28,9 @@ if (typeof HTMLVideoElement === 'undefined') { * Doubles as the main function for users to create a player instance and also * the main library object. * - * **ALIASES** videojs, _V_ (deprecated) + * The `videojs` function can be used to initialize or retrieve a player. * - * The `vjs` function can be used to initialize or retrieve a player. - * - * var myPlayer = vjs('my_video_id'); + * var myPlayer = videojs('my_video_id'); * * @param {String|Element} id Video element or video element ID * @param {Object=} options Optional options object for config/settings @@ -72,7 +66,7 @@ var videojs = function(id, options, ready){ // Otherwise get element for ID } else { - tag = Dom.el(id); + tag = Dom.getEl(id); } // ID is a media element @@ -90,64 +84,235 @@ var videojs = function(id, options, ready){ return tag['player'] || new Player(tag, options, ready); }; -// CDN Version. Used to target right flash swf. -videojs.CDN_VERSION = '__VERSION_NO_PATCH__'; -videojs.ACCESS_PROTOCOL = ('https:' === document.location.protocol ? 'https://' : 'http://'); - -/** -* Full player version -* @type {string} -*/ -videojs['VERSION'] = '__VERSION__'; - -// Set CDN Version of swf -// The added (+) blocks the replace from changing this _VERSION_NO_PATCH_ string -if (videojs.CDN_VERSION !== '__VERSION_'+'NO_PATCH__') { - Options['flash']['swf'] = `${videojs.ACCESS_PROTOCOL}vjs.zencdn.net/${videojs.CDN_VERSION}/video-js.swf`; -} - // Run Auto-load players // You have to wait at least once in case this script is loaded after your video in the DOM (weird behavior only with minified version) setup.autoSetupTimeout(1, videojs); -videojs.getComponent = Component.getComponent; -videojs.registerComponent = Component.registerComponent; +/** + * Current software version (semver) + * @type {String} + */ +videojs['VERSION'] = '__VERSION__'; -// APIs that will be removed with 5.0, but need them to get tests passing -// in ES6 transition -videojs.TOUCH_ENABLED = browser.TOUCH_ENABLED; +/** + * Get the global options object + * + * @returns {Object} The global options object + */ +videojs.getGlobalOptions = () => globalOptions; -// Probably want to keep this one for 5.0? -videojs.players = Player.players; - -videojs.extends = extendsFn; - -videojs.mergeOptions = mergeOptions; - -videojs.getGlobalOptions = () => options; +/** + * Set options that will apply to every player + * + * videojs.setGlobalOptions({ + * autoplay: true + * }); + * // -> all players will autoplay by default + * + * NOTE: This will do a deep merge with the new options, + * not overwrite the entire global options object. + * + * @returns {Object} The updated global options object + */ videojs.setGlobalOptions = function(newOptions) { - mergeOptions(options, newOptions); + return mergeOptions(globalOptions, newOptions); }; +// Set CDN Version of swf +const MINOR_VERSION = '__VERSION_NO_PATCH__'; +const ACCESS_PROTOCOL = ('https:' === document.location.protocol ? 'https://' : 'http://'); + +// The added (+) blocks the replace from changing this _VERSION_NO_PATCH_ string +if (MINOR_VERSION !== '__VERSION_'+'NO_PATCH__') { + globalOptions['flash']['swf'] = `${ACCESS_PROTOCOL}vjs.zencdn.net/${MINOR_VERSION}/video-js.swf`; +} + +/** + * Get an object with the currently created players, keyed by player ID + * + * @returns {Object} The created players + */ +videojs.getPlayers = function() { + return Player.players; +}; + +/** + * Get a component class object by name + * + * var VjsButton = videojs.getComponent('Button'); + * + * // Create a new instance of the component + * var myButton = new VjsButton(myPlayer); + * + */ +videojs.getComponent = Component.getComponent; + +/** + * Register a component so it can referred to by name + * + * Used when adding to other + * components, either through addChild + * `component.addChild('myComponent')` + * or through default children options + * `{ children: ['myComponent'] }`. + * + * // Get a component to subclass + * var VjsButton = videojs.getComponent('Button'); + * + * // Subclass the component (see 'extends' doc for more info) + * var MySpecialButton = videojs.extends(VjsButton, {}); + * + * // Register the new component + * VjsButton.registerComponent('MySepcialButton', MySepcialButton); + * + * // (optionally) add the new component as a default player child + * myPlayer.addChild('MySepcialButton'); + * + * NOTE: You could also just initialize the component before adding. + * `component.addChild(new MyComponent());` + * + * @param {String} The class name of the component + * @param {Component} The component class + * @returns {Component} The newly registered component + */ +videojs.registerComponent = Component.registerComponent; + +/** + * A suite of browser and device tests + * @type {Object} + */ +videojs.browser = browser; + +/** + * Subclass an existing class + * Mimics ES6 subclassing with the `extends` keyword + * + * // Create a basic javascript 'class' + * function MyClass(name){ + * // Set a property at initialization + * this.myName = name; + * } + * + * // Create an instance method + * MyClass.prototype.sayMyName = function(){ + * alert(this.myName); + * }; + * + * // Subclass the exisitng class and change the name + * // when initializing + * var MySubClass = videojs.extends(MyClass, { + * constructor: function(name) { + * // Call the super class constructor for the subclass + * MyClass.call(this, name) + * } + * }); + * + * // Create an instance of the new sub class + * var myInstance = new MySubClass('John'); + * myInstance.sayMyName(); // -> should alert "John" + * + * @param {Function} The Class to subclass + * @param {Object} An object including instace methods for the new class + * Optionally including a `constructor` function + * + * @returns {Function} The newly created subclass + */ +videojs.extends = extendsFn; + +/** + * Merge two options objects recursively + * Performs a deep merge like lodash.merge but **only merges plain objects** + * (not arrays, elements, anything else) + * Other values will be copied directly from the second object. + * + * var defaultOptions = { + * foo: true, + * bar: { + * a: true, + * b: [1,2,3] + * } + * }; + * var newOptions = { + * foo: false, + * bar: { + * b: [4,5,6] + * } + * }; + * + * var result = videojs.mergeOptions(defaultOptions, newOptions); + * // result.foo = false; + * // result.bar.a = true; + * // result.bar.b = [4,5,6]; + * + * @param {Object} The options object whose values will be overriden + * @param {Object} The options object with values to override the first + * @param {Object} Any number of additional options objects + * + * @returns {Object} a new object with the merged values + */ +videojs.mergeOptions = mergeOptions; + +/** + * Create a Video.js player plugin + * + * Plugins are only initialized when options for the plugin are included + * in the player options, or the plugin function on the player instance is + * called. + * + * **See the plugin guide in the docs for a more detailed example** + * + * // Make a plugin that alerts when the player plays + * videojs.plugin('myPlugin', function(myPluginOptions) { + * myPluginOptions = myPluginOptions || {}; + * + * var player = this; + * var alertText = myPluginOptions.text || 'Player is playing!' + * + * player.on('play', function(){ + * alert(alertText); + * }); + * }); + * + * // USAGE EXAMPLES + * + * // EXAMPLE 1: New player with plugin options, call plugin immediately + * var player1 = videojs('idOne', { + * myPlugin: { + * text: 'Custom text!' + * } + * }); + * // Click play + * // --> Should alert 'Custom text!' + * + * // EXAMPLE 3: New player, initialize plugin later + * var player3 = videojs('idThree'); + * // Click play + * // --> NO ALERT + * // Click pause + * // Initialize plugin using the plugin function on the player instance + * player3.myPlugin({ + * text: 'Plugin added later!' + * }); + * // Click play + * // --> Should alert 'Plugin added later!' + * + * @param {String} The plugin name + * @param {Function} The plugin function that will be called with options + */ videojs.plugin = plugin; /** - * Utility function for adding languages to the default options. Useful for - * amending multiple language support at runtime. + * Adding languages so that they're available to all players. * - * Example: videojs.addLanguage('es', {'Hello':'Hola'}); + * videojs.addLanguage('es', { 'Hello': 'Hola' }); * * @param {String} code The language code or dictionary property * @param {Object} data The data values to be translated - * @return {Object} The resulting global languages dictionary object + * + * @return {Object} The resulting language dictionary object */ videojs.addLanguage = function(code, data){ - if(Options['languages'][code] !== undefined) { - Options['languages'][code] = mergeOptions(Options['languages'][code], data); - } else { - Options['languages'][code] = data; - } - return Options['languages']; + return merge(globalOptions.languages, { [code]: data })[code]; }; // REMOVING: We probably should add this to the migration plugin diff --git a/test/api/api.js b/test/api/api.js index 714eb8428..e9f759c4e 100644 --- a/test/api/api.js +++ b/test/api/api.js @@ -153,7 +153,7 @@ test('should export ready api call to public', function() { }); test('should export useful components to the public', function () { - ok(videojs.TOUCH_ENABLED !== undefined, 'Touch detection should be public'); + ok(videojs.browser.TOUCH_ENABLED !== undefined, 'Touch detection should be public'); ok(videojs.getComponent('ControlBar'), 'ControlBar should be public'); ok(videojs.getComponent('Button'), 'Button should be public'); ok(videojs.getComponent('PlayToggle'), 'PlayToggle should be public'); @@ -213,7 +213,7 @@ test('should be able to initialize player twice on the same tag using string ref player.dispose(); }); -test('videojs.players should be available after minification', function() { +test('videojs.getPlayers() should be available after minification', function() { var videoTag = testHelperMakeTag(); var id = videoTag.id; @@ -221,7 +221,7 @@ test('videojs.players should be available after minification', function() { fixture.appendChild(videoTag); var player = videojs(id); - ok(videojs.players[id] === player, 'videojs.players is available'); + ok(videojs.getPlayers()[id] === player, 'videojs.getPlayers() is available'); player.dispose(); }); diff --git a/test/unit/component.test.js b/test/unit/component.test.js index 142f92eea..ed75026fe 100644 --- a/test/unit/component.test.js +++ b/test/unit/component.test.js @@ -3,6 +3,7 @@ import * as Dom from '../../src/js/utils/dom.js'; import * as Events from '../../src/js/utils/events.js'; import * as browser from '../../src/js/utils/browser.js'; import document from 'global/document'; +import TestHelpers from './test-helpers.js'; q.module('Component', { 'setup': function() { @@ -97,14 +98,14 @@ test('should do a deep merge of child options', function(){ var mergedOptions = comp.options(); var children = mergedOptions['example']; - ok(children['childOne']['foo'] === 'baz', 'value three levels deep overridden'); - ok(children['childOne']['asdf'] === 'fdsa', 'value three levels deep maintained'); - ok(children['childOne']['abc'] === '123', 'value three levels deep added'); + strictEqual(children['childOne']['foo'], 'baz', 'value three levels deep overridden'); + strictEqual(children['childOne']['asdf'], 'fdsa', 'value three levels deep maintained'); + strictEqual(children['childOne']['abc'], '123', 'value three levels deep added'); ok(children['childTwo'], 'object two levels deep maintained'); - ok(children['childThree'] === false, 'object two levels deep removed'); + strictEqual(children['childThree'], false, 'object two levels deep removed'); ok(children['childFour'], 'object two levels deep added'); - ok(Component.prototype.options_['example']['childOne']['foo'] === 'bar', 'prototype options were not overridden'); + strictEqual(Component.prototype.options_['example']['childOne']['foo'], 'bar', 'prototype options were not overridden'); // Reset default component options to none Component.prototype.options_ = null; @@ -165,8 +166,8 @@ test('should dispose of component and children', function(){ // Add a listener comp.on('click', function(){ return true; }); - var data = Dom.getData(comp.el()); - var id = comp.el()[Dom.expando]; + var el = comp.el(); + var data = Dom.getElData(el); var hasDisposed = false; var bubbles = null; @@ -183,8 +184,8 @@ test('should dispose of component and children', function(){ ok(!comp.el(), 'component element was deleted'); ok(!child.children(), 'child children were deleted'); ok(!child.el(), 'child element was deleted'); - ok(!Dom.cache[id], 'listener cache nulled'); - ok(!Object.getOwnPropertyNames(data).length, 'original listener cache object was emptied'); + ok(!Dom.hasElData(el), 'listener data nulled'); + ok(!Object.getOwnPropertyNames(data).length, 'original listener data object was emptied'); }); test('should add and remove event listeners to element', function(){ @@ -428,7 +429,7 @@ test('should change the width and height of a component', function(){ comp.height('123px'); ok(comp.width() === 500, 'percent values working'); - var compStyle = Dom.getComputedDimension(el, 'width'); + var compStyle = TestHelpers.getComputedStyle(el, 'width'); ok(compStyle === comp.width() + 'px', 'matches computed style'); ok(comp.height() === 123, 'px values working'); diff --git a/test/unit/player.test.js b/test/unit/player.test.js index a7e00c1e5..5d95e26d3 100644 --- a/test/unit/player.test.js +++ b/test/unit/player.test.js @@ -1,6 +1,6 @@ import Player from '../../src/js/player.js'; import videojs from '../../src/js/video.js'; -import Options from '../../src/js/options.js'; +import globalOptions from '../../src/js/global-options.js'; import * as Dom from '../../src/js/utils/dom.js'; import * as browser from '../../src/js/utils/browser.js'; import log from '../../src/js/utils/log.js'; @@ -66,7 +66,7 @@ test('should accept options from multiple sources and override in correct order' // version of the key for all version. // Set a global option - Options['attr'] = 1; + globalOptions['attr'] = 1; var tag0 = TestHelpers.makeTag(); var player0 = new Player(tag0); @@ -92,7 +92,7 @@ test('should accept options from multiple sources and override in correct order' }); test('should get tag, source, and track settings', function(){ - // Partially tested in lib->getElementAttributes + // Partially tested in lib->getElAttributes var fixture = document.getElementById('qunit-fixture'); @@ -752,14 +752,14 @@ test('should be scrubbing while seeking', function(){ }); test('should throw on startup no techs are specified', function() { - const techOrder = Options.techOrder; + const techOrder = globalOptions.techOrder; - Options.techOrder = null; + globalOptions.techOrder = null; q.throws(function() { videojs(TestHelpers.makeTag()); }, 'a falsey techOrder should throw'); - Options.techOrder = techOrder; + globalOptions.techOrder = techOrder; }); test('should have a sensible toJSON that is equivalent to player.options', function() { diff --git a/test/unit/test-helpers.js b/test/unit/test-helpers.js index 96e784eda..deb97c857 100644 --- a/test/unit/test-helpers.js +++ b/test/unit/test-helpers.js @@ -26,16 +26,20 @@ var TestHelpers = { }, getComputedStyle: function(el, rule){ - var val; - - if(window.getComputedStyle){ - val = window.getComputedStyle(el, null).getPropertyValue(rule); - // IE8 - } else if(el.currentStyle){ - val = el.currentStyle[rule]; + if (document.defaultView && document.defaultView.getComputedStyle) { + return document.defaultView.getComputedStyle(el, null).getPropertyValue(rule); } - return val; + // IE8 + if (el.currentStyle) { + if (rule === 'width' || rule === 'height') { + // return clientWidth or clientHeight instead for better accuracy + rule = 'client' + rule.substr(0, 1).toUpperCase() + rule.substr(1); + return el[rule] + 'px'; + } else { + return el.currentStyle[rule]; + } + } } }; diff --git a/test/unit/utils/dom.test.js b/test/unit/utils/dom.test.js index dc132907c..cebc912dc 100644 --- a/test/unit/utils/dom.test.js +++ b/test/unit/utils/dom.test.js @@ -1,5 +1,6 @@ import document from 'global/document'; import * as Dom from '../../../src/js/utils/dom.js'; +import TestHelpers from '../test-helpers.js'; test('should return the element with the ID', function(){ var el1 = document.createElement('div'); @@ -12,8 +13,8 @@ test('should return the element with the ID', function(){ el1.id = 'test_id1'; el2.id = 'test_id2'; - ok(Dom.el('test_id1') === el1, 'found element for ID'); - ok(Dom.el('#test_id2') === el2, 'found element for CSS ID'); + ok(Dom.getEl('test_id1') === el1, 'found element for ID'); + ok(Dom.getEl('#test_id2') === el2, 'found element for CSS ID'); }); test('should create an element', function(){ @@ -30,49 +31,47 @@ test('should insert an element first in another', function(){ var el2 = document.createElement('div'); var parent = document.createElement('div'); - Dom.insertFirst(el1, parent); + Dom.insertElFirst(el1, parent); ok(parent.firstChild === el1, 'inserts first into empty parent'); - Dom.insertFirst(el2, parent); + Dom.insertElFirst(el2, parent); ok(parent.firstChild === el2, 'inserts first into parent with child'); }); test('should get and remove data from an element', function(){ var el = document.createElement('div'); - var data = Dom.getData(el); - var id = el[Dom.expando]; + var data = Dom.getElData(el); ok(typeof data === 'object', 'data object created'); // Add data var testData = { asdf: 'fdsa' }; data.test = testData; - ok(Dom.getData(el).test === testData, 'data added'); + ok(Dom.getElData(el).test === testData, 'data added'); // Remove all data - Dom.removeData(el); + Dom.removeElData(el); - ok(!Dom.cache[id], 'cached item nulled'); - ok(el[Dom.expando] === null || el[Dom.expando] === undefined, 'element data id removed'); + ok(!Dom.hasElData(el), 'cached item emptied'); }); test('should add and remove a class name on an element', function(){ var el = document.createElement('div'); - Dom.addClass(el, 'test-class'); + Dom.addElClass(el, 'test-class'); ok(el.className === 'test-class', 'class added'); - Dom.addClass(el, 'test-class'); + Dom.addElClass(el, 'test-class'); ok(el.className === 'test-class', 'same class not duplicated'); - Dom.addClass(el, 'test-class2'); + Dom.addElClass(el, 'test-class2'); ok(el.className === 'test-class test-class2', 'added second class'); - Dom.removeClass(el, 'test-class'); + Dom.removeElClass(el, 'test-class'); ok(el.className === 'test-class2', 'removed first class'); }); test('should read class names on an element', function(){ var el = document.createElement('div'); - Dom.addClass(el, 'test-class1'); - ok(Dom.hasClass(el, 'test-class1') === true, 'class detected'); - ok(Dom.hasClass(el, 'test-class') === false, 'substring correctly not detected'); + Dom.addElClass(el, 'test-class1'); + ok(Dom.hasElClass(el, 'test-class1') === true, 'class detected'); + ok(Dom.hasElClass(el, 'test-class') === false, 'substring correctly not detected'); }); test('should set element attributes from object', function(){ @@ -81,7 +80,7 @@ test('should set element attributes from object', function(){ el = document.createElement('div'); el.id = 'el1'; - Dom.setElementAttributes(el, { controls: true, 'data-test': 'asdf' }); + Dom.setElAttributes(el, { controls: true, 'data-test': 'asdf' }); equal(el.getAttribute('id'), 'el1'); equal(el.getAttribute('controls'), ''); @@ -100,10 +99,10 @@ test('should read tag attributes from elements, including HTML5 in all browsers' document.getElementById('qunit-fixture').innerHTML += tags; - var vid1Vals = Dom.getElementAttributes(document.getElementById('vid1')); - var vid2Vals = Dom.getElementAttributes(document.getElementById('vid2')); - var sourceVals = Dom.getElementAttributes(document.getElementById('source')); - var trackVals = Dom.getElementAttributes(document.getElementById('track')); + var vid1Vals = Dom.getElAttributes(document.getElementById('vid1')); + var vid2Vals = Dom.getElAttributes(document.getElementById('vid2')); + var sourceVals = Dom.getElAttributes(document.getElementById('source')); + var trackVals = Dom.getElAttributes(document.getElementById('track')); // was using deepEqual, but ie8 would send all properties as attributes @@ -139,29 +138,9 @@ test('should read tag attributes from elements, including HTML5 in all browsers' equal(trackVals['title'], 'test'); }); -test('should get the right style values for an element', function(){ - var el = document.createElement('div'); - var container = document.createElement('div'); - var fixture = document.getElementById('qunit-fixture'); - - container.appendChild(el); - fixture.appendChild(container); - - container.style.width = '1000px'; - container.style.height = '1000px'; - - el.style.height = '100%'; - el.style.width = '123px'; - - // integer px values may get translated int very-close floats in Chrome/OS X - // so round the dimensions to ignore this - equal(Math.round(parseFloat(Dom.getComputedDimension(el, 'height'))), 1000, 'the computed height is equal'); - equal(Math.round(parseFloat(Dom.getComputedDimension(el, 'width'))), 123, 'the computed width is equal'); -}); - -test('Dom.findPosition should find top and left position', function() { +test('Dom.findElPosition should find top and left position', function() { const d = document.createElement('div'); - let position = Dom.findPosition(d); + let position = Dom.findElPosition(d); d.style.top = '10px'; d.style.left = '20px'; d.style.position = 'absolute'; @@ -169,10 +148,10 @@ test('Dom.findPosition should find top and left position', function() { deepEqual(position, {left: 0, top: 0}, 'If element isn\'t in the DOM, we should get zeros'); document.body.appendChild(d); - position = Dom.findPosition(d); + position = Dom.findElPosition(d); deepEqual(position, {left: 20, top: 10}, 'The position was not correct'); d.getBoundingClientRect = null; - position = Dom.findPosition(d); + position = Dom.findElPosition(d); deepEqual(position, {left: 0, top: 0}, 'If there is no gBCR, we should get zeros'); }); diff --git a/test/unit/video.test.js b/test/unit/video.test.js index 59e105c84..afadb8343 100644 --- a/test/unit/video.test.js +++ b/test/unit/video.test.js @@ -1,7 +1,7 @@ import videojs from '../../src/js/video.js'; import TestHelpers from './test-helpers.js'; import Player from '../../src/js/player.js'; -import Options from '../../src/js/options.js'; +import globalOptions from '../../src/js/global-options.js'; import document from 'global/document'; q.module('video.js'); @@ -41,9 +41,9 @@ test('should add the value to the languages object', function() { data = {'Hello': 'Hola'}; result = videojs.addLanguage(code, data); - ok(Options['languages'][code], 'should exist'); - equal(Options['languages'][code], data, 'should match'); - deepEqual(result[code], Options['languages'][code], 'should also match'); + ok(globalOptions.languages[code], 'should exist'); + equal(globalOptions.languages['es']['Hello'], 'Hola', 'should match'); + deepEqual(result['Hello'], globalOptions.languages['es']['Hello'], 'should also match'); });