diff --git a/Gruntfile.js b/Gruntfile.js index 58864fc58..352cace04 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -39,7 +39,7 @@ module.exports = function(grunt) { minified: ['test/minified.html'] }, watch: { - files: [ "src/**/*.js" ], + files: [ "src/**/*.js", "test/unit/*.js" ], tasks: "dev" } // Copy is broken. Waiting for an update to use. @@ -133,6 +133,7 @@ module.exports = function(grunt) { // + ' --formatting=pretty_print' + ' --js_output_file=' + dest + ' --create_source_map ' + dest + '.map --source_map_format=V3' + + ' --jscomp_warning=checkTypes --warning_level=VERBOSE'; + ' --output_wrapper "(function() {%output%})();//@ sourceMappingURL=video.js.map"'; files.forEach(function(file){ diff --git a/src/js/component.js b/src/js/component.js index 148dbf94a..90f0446d2 100644 --- a/src/js/component.js +++ b/src/js/component.js @@ -17,8 +17,11 @@ goog.require('vjs.dom'); vjs.Component = function(player, options, ready){ this.player_ = player; - // // Allow for overridding default component options - options = this.options_ = this.mergeOptions(this.options_, options); + // Make a copy of prototype.options_ to protect against overriding global defaults + this.options_ = vjs.obj.copy(this.options_); + + // Updated options with supplied options + options = this.options(options); // Get ID from options, element, or create using player ID and unique ID this.id_ = options['id'] || ((options['el'] && options['el']['id']) ? options['el']['id'] : player.id() + '_component_' + vjs.guid++ ); @@ -95,20 +98,20 @@ vjs.Component.prototype.options_; /** * Deep merge of options objects * Whenever a property is an object on both options objects - * the two properties will be merged using mergeOptions + * 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. * - * parentDefaultOptions = { + * Parent.prototype.options_ = { * children: { * 'childOne': { 'foo': 'bar', 'asdf': 'fdsa' }, * 'childTwo': {}, * 'childThree': {} * } * } - * parentOptionsFromInit = { + * newOptions = { * children: { * 'childOne': { 'foo': 'baz', 'abc': '123' } * 'childTwo': null, @@ -116,7 +119,7 @@ vjs.Component.prototype.options_; * } * } * - * this.mergeOptions(parentDefaultOptions, parentOptionsFromInit); + * this.options(newOptions); * * RESULT * @@ -129,41 +132,13 @@ vjs.Component.prototype.options_; * } * } * - * - * @param {Object} obj1 Object whose values will be overwritten - * @param {Object} obj2 Object whose values will overwrite + * @param {Object} obj Object whose values will be overwritten * @return {Object} NEW merged object. Does not return obj1. */ -vjs.Component.prototype.mergeOptions = function(obj1, obj2){ - var retObj, toString, hasOwnProp, propName, objDef, val1, val2; +vjs.Component.prototype.options = function(obj){ + if (obj === undefined) return this.options_; - hasOwnProp = Object.prototype.hasOwnProperty; - toString = Object.prototype.toString; - objDef = '[object Object]'; - obj1 = obj1 || {}; - retObj = {}; - - // Make a copy of obj1 so we don't affect the original options - vjs.eachProp(obj1, function(name, val){ - retObj[name] = val; - }); - - if (!obj2) { return retObj; } - - for (propName in obj2){ - if (hasOwnProp.call(obj2, propName)) { - val1 = retObj[propName]; - val2 = obj2[propName]; - - if (toString.call(val1) === objDef && toString.call(val2) === objDef) { - retObj[propName] = this.mergeOptions(val1, val2); - } else { - retObj[propName] = obj2[propName]; - } - } - } - - return retObj; + return this.options_ = vjs.obj.deepMerge(this.options_, obj); }; /** @@ -363,7 +338,7 @@ vjs.Component.prototype.initChildren = function(){ var self = this; // Loop through components and add them to the player - vjs.eachProp(options['children'], function(name, opts){ + vjs.obj.each(options['children'], function(name, opts){ // Allow for disabling default components // e.g. vjs.options['children']['posterImage'] = false diff --git a/src/js/controls.js b/src/js/controls.js index 7066ff17a..993728a42 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -96,7 +96,7 @@ goog.inherits(vjs.Button, vjs.Control); vjs.Button.prototype.createEl = function(type, props){ // Add standard Aria and Tabindex info - props = vjs.merge({ + props = vjs.obj.merge({ className: this.buildCSSClass(), innerHTML: '
' + (this.buttonText || 'Need Text') + '
', role: 'button', @@ -486,7 +486,7 @@ vjs.Slider = function(player, options){ goog.inherits(vjs.Slider, vjs.Component); vjs.Slider.prototype.createEl = function(type, props) { - props = vjs.merge({ + props = vjs.obj.merge({ role: 'slider', 'aria-valuenow': 0, 'aria-valuemin': 0, @@ -983,7 +983,7 @@ vjs.MenuItem = function(player, options){ goog.inherits(vjs.MenuItem, vjs.Button); vjs.MenuItem.prototype.createEl = function(type, props){ - return goog.base(this, 'createEl', 'li', vjs.merge({ + return goog.base(this, 'createEl', 'li', vjs.obj.merge({ className: 'vjs-menu-item', innerHTML: this.options_['label'] }, props)); diff --git a/src/js/json.js b/src/js/json.js index ae6b16cdf..3bbb63cdf 100644 --- a/src/js/json.js +++ b/src/js/json.js @@ -1,3 +1,9 @@ +/** + * @fileoverview Add JSON support + * @suppress {undefinedVars} + * (Compiler doesn't like JSON not being declared) + */ + goog.provide('vjs.JSON'); /** diff --git a/src/js/lib.js b/src/js/lib.js index 712ee74b4..9a3e98a78 100644 --- a/src/js/lib.js +++ b/src/js/lib.js @@ -1,4 +1,5 @@ goog.provide('vjs.dom'); +goog.provide('vjs.obj'); goog.require('vjs'); @@ -35,6 +36,14 @@ vjs.capitalize = function(string){ return string.charAt(0).toUpperCase() + string.slice(1); }; +/** + * Object functions container + * @type {Object} + */ +vjs.obj = {}; +vjs.obj.toString = Object.prototype.toString; +vjs.obj.hasOwnProperty = Object.prototype.hasOwnProperty; + /** * Loop through each property in an object and call a function * whose arguments are (key,value) @@ -42,33 +51,71 @@ vjs.capitalize = function(string){ * @param {Function} fn Function to be called on each property. * @this {*} */ -vjs.eachProp = function(obj, fn){ - if (!obj) { return; } - - for (var name in obj) { - if (obj.hasOwnProperty(name)) { - fn.call(this, name, obj[name]); +vjs.obj.each = function(obj, fn){ + for (var key in obj) { + if (vjs.obj.hasOwnProperty.call(obj, key)) { + fn.call(this, key, obj[key]); } } }; /** * Merge two objects together and return the original. - * @param {[type]} obj1 [description] - * @param {[type]} obj2 [description] - * @param {[type]} safe [description] - * @return {[type]} + * @param {Object} obj1 + * @param {Object} obj2 + * @return {Object} */ -vjs.merge = function(obj1, obj2){ - // Make sure second object exists +vjs.obj.merge = function(obj1, obj2){ if (!obj2) { return obj1; } - - for (var propName in obj2){ - if (obj2.hasOwnProperty(propName)) { obj1[propName] = obj2[propName]; } + for (var key in obj2){ + if (vjs.obj.hasOwnProperty.call(obj2, key)) { + obj1[key] = obj2[key]; + } } return obj1; }; +/** + * Merge two objects, and merge any properties that are objects + * instead of just overwriting one. Uses to merge options hashes + * where deeper default settings are important. + * @param {Object} obj1 Object to override + * @param {Object} obj2 Overriding object + * @return {Object} New object. Obj1 and Obj2 will be untouched. + */ +vjs.obj.deepMerge = function(obj1, obj2){ + var key, val1, val2, objDef; + objDef = '[object Object]'; + + // Make a copy of obj1 so we're not ovewriting original values. + // like prototype.options_ and all sub options objects + obj1 = vjs.obj.copy(obj1); + + for (key in obj2){ + if (vjs.obj.hasOwnProperty.call(obj2, key)) { + val1 = obj1[key]; + val2 = obj2[key]; + + // Check if both properties are pure objects and do a deep merge if so + if (vjs.obj.toString.call(val1) === objDef && vjs.obj.toString.call(val2) === objDef) { + obj1[key] = vjs.obj.deepMerge(val1, val2); + } else { + obj1[key] = obj2[key]; + } + } + } + return obj1; +}; + +/** + * Make a copy of the supplied object + * @param {Object} obj Object to copy + * @return {Object} Copy of object + */ +vjs.obj.copy = function(obj){ + return vjs.obj.merge({}, obj); +}; + /** * Bind (a.k.a proxy or Context). A simple method for changing the context of a function It also stores a unique id on the function so it can be easily removed from events diff --git a/src/js/media.flash.js b/src/js/media.flash.js index f109a0779..e89a24790 100644 --- a/src/js/media.flash.js +++ b/src/js/media.flash.js @@ -35,7 +35,7 @@ vjs.Flash = function(player, options, ready){ playerOptions = player.options_, // Merge default flashvars with ones passed in to init - flashVars = vjs.merge({ + flashVars = vjs.obj.merge({ // SWF Callback Functions 'readyFunction': 'videojs.Flash.onReady', @@ -51,13 +51,13 @@ vjs.Flash = function(player, options, ready){ }, options['flashVars']), // Merge default parames with ones passed in - params = vjs.merge({ + params = vjs.obj.merge({ 'wmode': 'opaque', // Opaque is needed to overlay controls, but can affect playback performance 'bgcolor': '#000000' // Using bgcolor prevents a white flash when the object is loading }, options['params']), // Merge default attributes with ones passed in - attributes = vjs.merge({ + attributes = vjs.obj.merge({ 'id': objId, 'name': objId, // Both ID and Name needed or swf to identifty itself 'class': 'vjs-tech' @@ -285,16 +285,19 @@ var createGetter = function(attr){ api[attr] = function(){ return this.el_.vjs_getProperty(attr); }; }; -// Create getter and setters for all read/write attributes -for (var i = 0; i < readWrite.length; i++) { - createGetter(readWrite[i]); - createSetter(readWrite[i]); -} +(function(){ + var i; + // Create getter and setters for all read/write attributes + for (i = 0; i < readWrite.length; i++) { + createGetter(readWrite[i]); + createSetter(readWrite[i]); + } -// Create getters for read-only attributes -for (var i = 0; i < readOnly.length; i++) { - createGetter(readOnly[i]); -} + // Create getters for read-only attributes + for (i = 0; i < readOnly.length; i++) { + createGetter(readOnly[i]); + } +})(); /* Flash Support Testing -------------------------------------------------------- */ @@ -434,13 +437,13 @@ vjs.Flash.getEmbedCode = function(swf, flashVars, params, attributes){ // Convert flash vars to string if (flashVars) { - vjs.eachProp(flashVars, function(key, val){ + vjs.obj.each(flashVars, function(key, val){ flashVarsString += (key + '=' + val + '&'); }); } // Add swf, flashVars, and other default params - params = vjs.merge({ + params = vjs.obj.merge({ 'movie': swf, 'flashvars': flashVarsString, 'allowScriptAccess': 'always', // Required to talk to swf @@ -448,11 +451,11 @@ vjs.Flash.getEmbedCode = function(swf, flashVars, params, attributes){ }, params); // Create param tags string - vjs.eachProp(params, function(key, val){ + vjs.obj.each(params, function(key, val){ paramsString += ''; }); - attributes = vjs.merge({ + attributes = vjs.obj.merge({ // Add swf to attributes (need both for IE and Others to work) 'data': swf, @@ -463,7 +466,7 @@ vjs.Flash.getEmbedCode = function(swf, flashVars, params, attributes){ }, attributes); // Create Attributes string - vjs.eachProp(attributes, function(key, val){ + vjs.obj.each(attributes, function(key, val){ attrsString += (key + '="' + val + '" '); }); diff --git a/src/js/player.js b/src/js/player.js index 579c79c95..79339a1ec 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -17,7 +17,7 @@ vjs.Player = function(tag, options, ready){ // which overrides globally set options. // This latter part coincides with the load order // (tag must exist before Player) - options = this.mergeOptions(this.getTagSettings(tag), options); + options = vjs.obj.merge(this.getTagSettings(tag), options); // Cache for video property values. this.cache_ = {}; @@ -93,7 +93,7 @@ vjs.Player.prototype.getTagSettings = function(tag){ 'tracks': [] }; - vjs.merge(options, vjs.getAttributeValues(tag)); + vjs.obj.merge(options, vjs.getAttributeValues(tag)); // Get tag children settings if (tag.hasChildNodes()) { @@ -216,7 +216,7 @@ vjs.Player.prototype.loadTech = function(techName, source){ }; // Grab tech-specific options from player options and add source and parent element to use. - var techOptions = vjs.merge({ source: source, parentEl: this.el_ }, this.options_[techName.toLowerCase()]); + var techOptions = vjs.obj.merge({ source: source, parentEl: this.el_ }, this.options_[techName.toLowerCase()]); if (source) { if (source.src == this.cache_.src && this.cache_.currentTime > 0) { @@ -832,7 +832,7 @@ vjs.Player.prototype.poster_; /** * Get or set the poster image source url. * @param {String} src Poster image source URL - * @return {String=} Poster image source URL or null + * @return {String} Poster image source URL or null */ vjs.Player.prototype.poster = function(src){ if (src !== undefined) { diff --git a/src/js/tracks.js b/src/js/tracks.js index 742399e87..2bd1af3a8 100644 --- a/src/js/tracks.js +++ b/src/js/tracks.js @@ -1012,7 +1012,7 @@ vjs.ChaptersTrackMenuItem.prototype.update = function(){ }; // Add Buttons to controlBar -vjs.merge(vjs.ControlBar.prototype.options_['children'], { +vjs.obj.merge(vjs.ControlBar.prototype.options_['children'], { 'subtitlesButton': {}, 'captionsButton': {}, 'chaptersButton': {} diff --git a/test/unit/component.js b/test/unit/component.js index f2d098137..6d34754d9 100644 --- a/test/unit/component.js +++ b/test/unit/component.js @@ -37,24 +37,25 @@ test('should init child coponents from options', function(){ }); test('should do a deep merge of child options', function(){ - var compDefaultOptions = { - 'children': { + // Create a default option for component + vjs.Component.prototype.options_ = { + 'example': { 'childOne': { 'foo': 'bar', 'asdf': 'fdsa' }, 'childTwo': {}, 'childThree': {} } } - var compInitOptions = { - 'children': { + var comp = new vjs.Component(getFakePlayer(), { + 'example': { 'childOne': { 'foo': 'baz', 'abc': '123' }, 'childThree': null, 'childFour': {} } - } + }); - var mergedOptions = vjs.Component.prototype.mergeOptions(compDefaultOptions, compInitOptions); - var children = mergedOptions['children']; + 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'); @@ -62,6 +63,11 @@ test('should do a deep merge of child options', function(){ ok(children['childTwo'], 'object two levels deep maintained'); ok(children['childThree'] === null, 'object two levels deep removed'); ok(children['childFour'], 'object two levels deep added'); + + ok(vjs.Component.prototype.options_['example']['childOne']['foo'] === 'bar', 'prototype options were not overridden'); + + // Reset default component options to none + vjs.Component.prototype.options_ = null; }); test('should dispose of component and children', function(){ diff --git a/test/unit/core.js b/test/unit/core.js index 4f0ae7246..5e6390220 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -5,7 +5,7 @@ test('should create a video tag and have access children in old IE', function(){ fixture.innerHTML += ""; - vid = document.getElementById('test_vid_id'); + var vid = document.getElementById('test_vid_id'); ok(vid.childNodes.length === 1); ok(vid.childNodes[0].getAttribute('type') === 'video/mp4'); diff --git a/test/unit/lib.js b/test/unit/lib.js index 910d130c1..526db0e84 100644 --- a/test/unit/lib.js +++ b/test/unit/lib.js @@ -22,13 +22,25 @@ test('should loop through each property on an object', function(){ } // Add 3 to each value - vjs.eachProp(asdf, function(key, value){ + vjs.obj.each(asdf, function(key, value){ asdf[key] = value + 3; }); deepEqual(asdf,{a:4,b:5,'c':6}) }); +test('should copy an object', function(){ + var asdf = { + a: 1, + b: 2, + 'c': 3 + } + + var fdsa = vjs.obj.copy(asdf); + + deepEqual(asdf,fdsa) +}); + test('should add context to a function', function(){ var newContext = { test: 'obj'}; var asdf = function(){ diff --git a/test/unit/mediafaker.js b/test/unit/mediafaker.js index 332623f17..6381eef52 100644 --- a/test/unit/mediafaker.js +++ b/test/unit/mediafaker.js @@ -1,6 +1,9 @@ -// Fake a media playback tech controller so that player tests +// Fake a media playback tech controller so that player tests // can run without HTML5 or Flash, of which PhantomJS supports neither. +/** + * @constructor + */ vjs.MediaFaker = function(player, options, onReady){ goog.base(this, player, options, onReady); @@ -30,4 +33,4 @@ vjs.MediaFaker.prototype.volume = function(){ return 0; }; goog.exportSymbol('videojs.MediaFaker', vjs.MediaFaker); goog.exportProperty(vjs.MediaFaker, 'isSupported', vjs.MediaFaker.isSupported); -goog.exportProperty(vjs.MediaFaker, 'canPlaySource', vjs.MediaFaker.canPlaySource); \ No newline at end of file +goog.exportProperty(vjs.MediaFaker, 'canPlaySource', vjs.MediaFaker.canPlaySource); diff --git a/test/unit/player.js b/test/unit/player.js index d4331b5d6..524a7bc40 100644 --- a/test/unit/player.js +++ b/test/unit/player.js @@ -8,12 +8,13 @@ var PlayerTest = { return videoTag; }, makePlayer: function(playerOptions){ + var player; var videoTag = PlayerTest.makeTag(); var fixture = document.getElementById('qunit-fixture'); fixture.appendChild(videoTag); - var opts = vjs.merge({ + var opts = vjs.obj.merge({ 'techOrder': ['mediaFaker'] }, playerOptions);