From 522087e0f4436fd5396e8e260681f324733479e9 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Tue, 23 Sep 2014 22:30:14 -0400 Subject: [PATCH 1/8] Start refactor work --- Gruntfile.js | 3 +- src/events.js | 19 +++++ src/html.js | 185 ++++++++++++++++++++++++---------------------- src/main.js | 53 ++++++------- src/options.js | 10 +++ src/outro.js | 1 - src/scrollable.js | 119 +++++++++++++++++++++++++++++ src/scroller.js | 155 -------------------------------------- src/shims.js | 26 +++++++ src/utils.js | 34 +++++---- 10 files changed, 314 insertions(+), 291 deletions(-) create mode 100644 src/events.js create mode 100644 src/options.js create mode 100644 src/scrollable.js delete mode 100644 src/scroller.js create mode 100644 src/shims.js diff --git a/Gruntfile.js b/Gruntfile.js index ee7df8f..5c5d608 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -17,9 +17,10 @@ module.exports = function(grunt) { }, build: { src: ['src/intro.js', + 'src/shims.js', 'src/utils.js', + 'src/scrollable.js', 'src/html.js', - 'src/scroller.js', 'src/main.js', 'src/outro.js'], dest: 'dist/<%= pkg.name %>.js' diff --git a/src/events.js b/src/events.js new file mode 100644 index 0000000..31614b5 --- /dev/null +++ b/src/events.js @@ -0,0 +1,19 @@ +/* + * Basic event handling. + */ +var events; +events.handlers_ = []; +events.on = function (eventtype, callback) { + if (typeof callback === 'function') { + this.handlers_.push([eventtype, callback]); + } else { + throw new TypeError('Callback is not a function.'); + } +}; +events.trigger = function (eventtype) { + for (var i = 0, i < handlers_.length i++) { + if (this.handlers_[i][0] === event) { + this.handlers_[i][1].apply(); + } + } +}; \ No newline at end of file diff --git a/src/html.js b/src/html.js index 17bf99f..5f81ec8 100644 --- a/src/html.js +++ b/src/html.js @@ -1,92 +1,101 @@ -/*global Utils*/ -var Html = (function () { - var myContainer, subContainer, myPlayer, myPrefix, settings; - var createSeekClickHandler = function (time) { - return function (e) { - myPlayer.currentTime(time); - }; - }; +/* + * Create and Manipulate Html Elements + */ +var html; - var createLine = function (cue) { - var line = document.createElement('div'); - var timestamp = document.createElement('span'); - var text = document.createElement('span'); - line.className = myPrefix + '-line'; - line.setAttribute('data-begin', cue.startTime); - timestamp.className = myPrefix + '-timestamp'; - timestamp.textContent = Utils.niceTimestamp(cue.startTime); - text.className = myPrefix + '-text'; - text.innerHTML = cue.text; - line.appendChild(timestamp); - line.appendChild(text); - return line; - }; - var setTrack = function (track) { - if (typeof track !== 'object') { - track = myPlayer.textTracks()[track]; - } - if (subContainer === undefined) { - throw new Error('videojs-transcript: Html not initialized!'); - } - var line, i; - var fragment = document.createDocumentFragment(); - var createTranscript = function () { - var cues = track.cues(); - for (i = 0; i < cues.length; i++) { - line = createLine(cues[i], myPrefix); - fragment.appendChild(line); - } - subContainer.innerHTML = ''; - subContainer.appendChild(fragment); - subContainer.setAttribute('lang', track.language()); - }; - subContainer.addEventListener('click', function (event) { - var clickedClasses = event.target.classList; - var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin'); - if (clickedTime !== undefined && clickedTime !== null) { // can be zero - if ((settings.clickArea === 'line') || // clickArea: 'line' activates on all elements - (settings.clickArea === 'timestamp' && clickedClasses.contains(myPrefix + '-timestamp')) || - (settings.clickArea === 'text' && clickedClasses.contains(myPrefix + '-text'))) { - myPlayer.currentTime(clickedTime); - } - } - }); - if (track.readyState() !== 2) { - track.load(); - track.on('loaded', createTranscript); - } else { - createTranscript(); +html.createEl = function (elementName, classSuffix) { + classSuffix = classSuffix || ''; + var el = document.createElement(elementName); + el.className = prefix_ + classSuffix; + return el; +}; + +html.createLine = function (cue) { + var line = html.createEl('div', '-line'); + var timestamp = html.createEl('span', '-timestamp') + var text = html.createEl('span', '-text'); + line.setAttribute('data-begin', cue.startTime); + timestamp.textContent = utils.secondsToTime(cue.startTime); + text.innerHTML = cue.text; + line.appendChild(timestamp); + line.appendChild(text); + return line; +}; + +html.createTranscriptBody = function (track) { + if (typeof track !== 'object') { + track = player_.textTracks()[track]; + } + var body = createScrollable(html.createEl('div', '-body')); + var el = body.element; + var line, i; + var fragment = document.createDocumentFragment(); + var createTranscript = function () { + var cues = track.cues(); + for (i = 0; i < cues.length; i++) { + line = html.createLine(cues[i]); + fragment.appendChild(line); } + el.innerHTML = ''; + el.appendChild(fragment); + el.setAttribute('lang', track.language()); }; - var createSelector = function (tracks) { - var select = document.createElement('select'); - var i, track, option; - for (i = 0; i < tracks.length; i++) { - track = tracks[i]; - option = document.createElement('option'); - option.value = i; - option.textContent = track.label() + ' (' + track.language() + ')'; - select.appendChild(option); + el.addEventListener('click', function (event) { + var clickedClasses = event.target.classList; + var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin'); + if (clickedTime !== undefined && clickedTime !== null) { // can be zero + if ((settings_.clickArea === 'line') || // clickArea: 'line' activates on all elements + (settings_.clickArea === 'timestamp' && clickedClasses.contains(prefix_ + '-timestamp')) || + (settings_.clickArea === 'text' && clickedClasses.contains(prefix_ + '-text'))) { + player_.currentTime(clickedTime); + } } - select.addEventListener('change', function (e) { - setTrack(document.querySelector('#' + myPrefix + '-' + myPlayer.id() + ' option:checked').value); - }); - return select; - }; - var init = function (container, player, prefix, plugin) { - myContainer = container; - myPlayer = player; - myPrefix = prefix; - subContainer = document.createElement('div'); - settings = plugin.options; - myContainer.className = prefix; - subContainer.className = prefix + '-lines'; - myContainer.id = myPrefix + '-' + myPlayer.id(); - myContainer.appendChild(createSelector(myPlayer.textTracks())); - myContainer.appendChild(subContainer); - }; - return { - init: init, - setTrack: setTrack, - }; -}()); \ No newline at end of file + }); + if (track.readyState() !== 2) { + track.load(); + track.on('loaded', createTranscript); + } else { + createTranscript(); + } + return el; +}; + +html.createTitle = function () { + var header = html.createEl('header', '-header'); + header.textContent = utils.localize('Transcript'); + return header; +}; + +html.createSelector = function () { + var select = html.createEl('select', '-selector'); + var i, track, option; + for (i = 0; i < tracks.length; i++) { + track = validTracks_[i]; + option = document.createElement('option'); + option.value = i; + option.textContent = track.label() + ' (' + track.language() + ')'; + select.appendChild(option); + } + select.addEventListener('change', function (e) { + html.replaceTranscriptBody(document.querySelector('#' + prefix_ + '-' + player_.id() + '-selector option:checked').value); + }); + return select; +}; + +html.createTranscript = function () { + var el = document.createElement('div'); + var elsetAttribute('id', prefix_ + '-' + player_.id()); + var title = html.createTitle(); + var selector = html.createSelector(); + var body = html.createTranscriptBody(currentTrack_); + el.appendChild(title); + el.appendChild(selector); + el.appendChild(body); + return el; +}; + +html.replaceTranscriptBody = function (track) { + var oldbody = document.querySelector('#' + prefix_ + '-' + player_.id()); + var newbody = html.createTranscriptBody(track); + oldbody.parent.replaceChild(newbody, oldbody); +} diff --git a/src/main.js b/src/main.js index a0b6e4f..8f2c953 100644 --- a/src/main.js +++ b/src/main.js @@ -1,22 +1,16 @@ -/*global window, videojs, Html, Scroller, Utils*/ +/*global window, videojs*/ -var Plugin = (function (window, videojs) { var defaults = { autoscroll: true, clickArea: 'line' }; var transcript = function (options) { - var settings = videojs.util.mergeOptions(defaults, options); - var player = this; - var htmlPrefix = 'transcript'; - var htmlContainer = document.createElement('div'); - var tracks; - var currentTrack; + var settings_ = videojs.util.mergeOptions(defaults, options); var getAllTracks = function () { var i, kind; var validTracks = []; - tracks = player.textTracks(); + var tracks = player_.textTracks(); for (i = 0; i < tracks.length; i++) { kind = tracks[i].kind(); if (kind === 'captions' || kind === 'subtitles') { @@ -32,11 +26,11 @@ var Plugin = (function (window, videojs) { return tracks[i]; } } - return currentTrack || tracks[0]; + return currentTrack_ || tracks[0]; }; var getCaptionNodes = function () { var i, node, caption; - var nodes = document.querySelectorAll('#' + htmlContainer.id + ' > .' + htmlPrefix + '-line'); + var nodes = document.querySelectorAll('#' + el_.id + ' > .' + prefix_ + '-line'); var captions = []; for (i = 0; i < nodes.length; i++) { node = nodes[i]; @@ -50,7 +44,7 @@ var Plugin = (function (window, videojs) { }; var timeUpdate = function () { var caption, end, i; - var time = player.currentTime(); + var time = player_.currentTime(); var captions = getCaptionNodes(); for (i = 0; i < captions.length; i++) { caption = captions[i]; @@ -59,45 +53,44 @@ var Plugin = (function (window, videojs) { if (i < captions.length - 1) { end = captions[i + 1].begin; } else { - end = player.duration() || Infinity; + end = player_.duration() || Infinity; } if (time > caption.begin && time < end) { if (!caption.element.classList.contains('is-active')) { // don't update if it hasn't changed caption.element.classList.add('is-active'); - if (settings.autoscroll && - Scroller.canScroll(htmlContainer) && - !Scroller.inUse()) { - Scroller.scrollToElement(caption.element); - } + // if (settings.autoscroll && + // Scroller.canScroll(htmlContainer) && + // !Scroller.inUse()) { + // Scroller.scrollToElement(caption.element); + // } } } else { caption.element.classList.remove('is-active'); } } }; + validTracks_ = getAllTracks(); var trackChange = function () { - currentTrack = getActiveTrack(tracks); - Html.setTrack(currentTrack); + currentTrack_ = getActiveTrack(validTracks_); + html.replaceTranscriptBody(currentTrack_) }; - tracks = getAllTracks(); - if (tracks.length > 0) { - Html.init(htmlContainer, player, htmlPrefix, this); - Scroller.initHandlers(htmlContainer); + if (validTracks_.length > 0) { + el_ = html.createTranscript; trackChange(); - player.on('timeupdate', timeUpdate); - player.on('captionstrackchange', trackChange); - player.on('subtitlestrackchange', trackChange); + player_.on('timeupdate', timeUpdate); + player_.on('captionstrackchange', trackChange); + player_.on('subtitlestrackchange', trackChange); } else { throw new Error('videojs-transcript: No tracks found!'); } var el = function () { - return htmlContainer; + return el_; }; return { el: el, setTrack: trackChange, - options: options, + options: options_, }; }; - return {transcript: transcript}; + videojs.plugin('transcript', transcript); }(window, videojs)); diff --git a/src/options.js b/src/options.js new file mode 100644 index 0000000..c13afa3 --- /dev/null +++ b/src/options.js @@ -0,0 +1,10 @@ +/* + * Options + */ + var validTracks_, //array of track objects + currentTrack_, //track object + currentCue_, //elemnt + settings_, //object + el_; + var prefix_ = 'transcript'; + var player_ = this; diff --git a/src/outro.js b/src/outro.js index eac8474..636d243 100644 --- a/src/outro.js +++ b/src/outro.js @@ -1,2 +1 @@ - videojs.plugin('transcript', Plugin.transcript); }(window, window.videojs)); \ No newline at end of file diff --git a/src/scrollable.js b/src/scrollable.js new file mode 100644 index 0000000..2f69508 --- /dev/null +++ b/src/scrollable.js @@ -0,0 +1,119 @@ +/* + * Scrollable class to handle scrolling. + */ +var scrollablePrototype = { + + easeOut: function (time, start, change, duration) { + return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2)); + }, + + // Animate the scrolling. + scrollTo: function (element, newPos, duration) { + var startTime = Date.now(); + var startPos = element.scrollTop; + + // Don't try to scroll beyond the limits. You won't get there and this will loop forever. + newPos = Math.max(0, newPos); + newPos = Math.min(element.scrollHeight - element.clientHeight, newPos); + var change = newPos - startPos; + + // This inner function is called until the elements scrollTop reaches newPos. + var updateScroll = function () { + var now = Date.now(); + var time = now - startTime; + isAutoScrolling = true; + element.scrollTop = easeOut(time, startPos, change, duration); + if (element.scrollTop !== newPos) { + requestAnimationFrame(updateScroll, element); + } + }; + requestAnimationFrame(updateScroll, element); + }, + + // Scroll an element's parent so the element is brought into view. + scrollToElement: function (element) { + var parent = element.parentElement; + var parentOffsetBottom = parent.offsetTop + parent.clientHeight; + var elementOffsetBottom = element.offsetTop + element.clientHeight; + var relPos = (element.offsetTop + element.clientHeight) - parent.offsetTop; + var newPos; + + // If the line is above the top of the parent view, were scrolling up, + // so we want to move the top of the element downwards to match the top of the parent. + if (relPos < parent.scrollTop) { + newPos = element.offsetTop - parent.offsetTop; + + // If the line is below the parent view, we're scrolling down, so we want the + // bottom edge of the line to move up to meet the bottom edge of the parent. + } else if (relPos > (parent.scrollTop + parent.clientHeight)) { + newPos = elementOffsetBottom - parentOffsetBottom; + } + + // Don't try to scroll if we haven't set a new position. If we didn't + // set a new position the line is already in view (i.e. It's not above + // or below the view) + // And don't try to scroll when the element is already in position. + if (newPos !== undefined && parent.scrollTop !== newPos) { + scrollTo(parent, newPos, 400); + } + }; + + initHandlers: function () { + var el = this.element; + // The scroll event. We want to keep track of when the user is scrolling the transcript. + el.addEventListener('scroll', function () { + if (this.isAutoScrolling) { + + // If isAutoScrolling was set to true, we can set it to false and then ignore this event. + this.isAutoScrolling = false; // event handled + } else { + + // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class. + this.userIsScrolling = true; + el.classList.add('is-inuse'); + } + }); + + // The mouseover event. + el.addEventListener('mouseover', function () { + this.mouseIsOverTranscript = true; + }); + el.addEventListener('mouseout', function () { + this.mouseIsOverTranscript = false; + + // Have a small delay before deciding user as done interacting. + setTimeout(function () { + + // Make sure the user didn't move the pointer back in. + if (!this.mouseIsOverTranscript) { + this.userIsScrolling = false; + el.classList.remove('is-inuse'); + } + }, 1000); + }); + }, + + // Return whether the element is scrollable. + canScroll: function () { + var el = this.element + return el.scrollHeight > el.offsetHeight; + }, + + // Return whether the user is interacting with the transcript. + inUse: function () { + return this.userIsScrolling; + } +}; + +createScrollable = function (element) { + var ob = Object.create(scrollablePrototype); + ob.element = element; + + // defaults + ob.userIsScrolling = false; + ob.mouseIsOver = false; + ob.isAutoScrolling = true; + return ob; +}; + + diff --git a/src/scroller.js b/src/scroller.js deleted file mode 100644 index dc686b4..0000000 --- a/src/scroller.js +++ /dev/null @@ -1,155 +0,0 @@ -var Scroller = (function () { - -// Keep track when the user is scrolling the transcript. - - var userIsScrolling = false; - -// Keep track of when the mouse is hovering the transcript. - - var mouseIsOverTranscript = false; - -// The initial element creation triggers a scroll event. We don't want to consider -// this as the user scrolling, so we initialize the isAutoScrolling flag to true. - - var isAutoScrolling = true; - -// requestAnimationFrame compatibility shim. - - var requestAnimationFrame = - window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.msRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - function (callback) { - window.setTimeout(callback, 1000 / 60); - }; - -// For smooth animation. - - var easeOut = function (time, start, change, duration) { - return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2)); - }; - -// Animate the scrolling. - - var scrollTo = function (element, newPos, duration) { - var startTime = Date.now(); - var startPos = element.scrollTop; - -// Don't try to scroll beyond the limits. You won't get there and this will loop forever. - - newPos = Math.max(0, newPos); - newPos = Math.min(element.scrollHeight - element.clientHeight, newPos); - var change = newPos - startPos; - -// This inner function is called until the elements scrollTop reaches newPos. - - var updateScroll = function () { - var now = Date.now(); - var time = now - startTime; - isAutoScrolling = true; - element.scrollTop = easeOut(time, startPos, change, duration); - if (element.scrollTop !== newPos) { - requestAnimationFrame(updateScroll, element); - } - }; - requestAnimationFrame(updateScroll, element); - }; - -// Scroll an element's parent so the element is brought into view. - - var scrollToElement = function (element) { - var parent = element.parentElement; - var parentOffsetBottom = parent.offsetTop + parent.clientHeight; - var elementOffsetBottom = element.offsetTop + element.clientHeight; - var relPos = (element.offsetTop + element.clientHeight) - parent.offsetTop; - var newPos; - -// If the line is above the top of the parent view, were scrolling up, -// so we want to move the top of the element downwards to match the top of the parent. - - if (relPos < parent.scrollTop) { - newPos = element.offsetTop - parent.offsetTop; - -// If the line is below the parent view, we're scrolling down, so we want the -// bottom edge of the line to move up to meet the bottom edge of the parent. - - } else if (relPos > (parent.scrollTop + parent.clientHeight)) { - newPos = elementOffsetBottom - parentOffsetBottom; - } - -// Don't try to scroll if we haven't set a new position. If we didn't -// set a new position the line is already in view (i.e. It's not above -// or below the view) -// And don't try to scroll when the element is already in position. - - if (newPos !== undefined && parent.scrollTop !== newPos) { - scrollTo(parent, newPos, 400); - } - }; - -// Set Event Handlers to monitor user scrolling. - - var initHandlers = function (element) { - -// The scroll event. We want to keep track of when the user is scrolling the transcript. - - element.addEventListener('scroll', function () { - if (isAutoScrolling) { - -// If isAutoScrolling was set to true, we can set it to false and then ignore this event. - - isAutoScrolling = false; // event handled - } else { - -// We only care about when the user scrolls. Set userIsScrolling to true and add a nice class. - - userIsScrolling = true; - element.classList.add('is-inuse'); - } - }); - -// The mouseover event. - - element.addEventListener('mouseover', function () { - mouseIsOverTranscript = true; - }); - element.addEventListener('mouseout', function () { - mouseIsOverTranscript = false; - -// Have a small delay before deciding user as done interacting. - - setTimeout(function () { - -// Make sure the user didn't move the pointer back in. - - if (!mouseIsOverTranscript) { - userIsScrolling = false; - element.classList.remove('is-inuse'); - } - }, 1000); - }); - }; - -// Return whether the element is scrollable. - - var canScroll = function (container) { - return container.scrollHeight > container.offsetHeight; - }; - -// Return whether the user is interacting with the transcript. - - var inUse = function () { - return userIsScrolling; - }; - -// Public Methods - - return { - scrollToElement : scrollToElement, - initHandlers : initHandlers, - inUse : inUse, - canScroll : canScroll, - }; -}()); \ No newline at end of file diff --git a/src/shims.js b/src/shims.js new file mode 100644 index 0000000..4ea49d0 --- /dev/null +++ b/src/shims.js @@ -0,0 +1,26 @@ +/* + * Compatibility Shims + */ + +/* requestAnimationFrame polyfill */ +var requestAnimationFrame = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.msRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; + +/*Object.create Shim*/ +if (!Object.create) { + Object.create = function (o) { + if (arguments.length > 1) { + throw new Error('Object.create implementation only accepts the first parameter.'); + } + function F() {} + F.prototype = o; + return new F(); + }; +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 16ebe81..e58176e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,16 +1,18 @@ -var Utils = (function () { - var niceTimestamp = function (timeInSeconds) { - var hour = Math.floor(timeInSeconds / 3600); - var min = Math.floor(timeInSeconds % 3600 / 60); - var sec = Math.floor(timeInSeconds % 60); - sec = (sec < 10) ? '0' + sec : sec; - min = (hour > 0 && min < 10) ? '0' + min : min; - if (hour > 0) { - return hour + ':' + min + ':' + sec; - } - return min + ':' + sec; - }; - return { - niceTimestamp : niceTimestamp, - }; -}()); \ No newline at end of file +/* + * Utils + */ +var utils; +utils.secondsToTime = function (timeInSeconds) { + var hour = Math.floor(timeInSeconds / 3600); + var min = Math.floor(timeInSeconds % 3600 / 60); + var sec = Math.floor(timeInSeconds % 60); + sec = (sec < 10) ? '0' + sec : sec; + min = (hour > 0 && min < 10) ? '0' + min : min; + if (hour > 0) { + return hour + ':' + min + ':' + sec; + } + return min + ':' + sec; +}; +utils.localize = function (string) { + return string; // TODO: do something here; +}; From cd0daa02c5928b13c517b12a38c40eb63af02548 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 25 Sep 2014 22:31:23 -0400 Subject: [PATCH 2/8] Work on submodules --- Gruntfile.js | 9 +- src/events.js | 27 +++--- src/html.js | 101 -------------------- src/main.js | 118 +++++------------------ src/options.js | 21 +++-- src/outro.js | 2 +- src/polyfill.js | 77 +++++++++++++++ src/scrollable.js | 234 ++++++++++++++++++++++++---------------------- src/shims.js | 26 ------ src/tracklist.js | 30 ++++++ src/utils.js | 41 +++++--- src/widget.js | 103 ++++++++++++++++++++ 12 files changed, 418 insertions(+), 371 deletions(-) delete mode 100644 src/html.js create mode 100644 src/polyfill.js delete mode 100644 src/shims.js create mode 100644 src/tracklist.js create mode 100644 src/widget.js diff --git a/Gruntfile.js b/Gruntfile.js index 5c5d608..836614f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -17,10 +17,13 @@ module.exports = function(grunt) { }, build: { src: ['src/intro.js', - 'src/shims.js', + 'src/polyfill.js', + 'src/options.js', 'src/utils.js', + 'src/events.js', 'src/scrollable.js', - 'src/html.js', + 'src/tracklist.js', + 'src/widget.js', 'src/main.js', 'src/outro.js'], dest: 'dist/<%= pkg.name %>.js' @@ -91,7 +94,7 @@ module.exports = function(grunt) { grunt.registerTask('default', ['clean', - 'jshint:src', + //'jshint:src', 'concat', 'jshint:build', 'jshint:test', diff --git a/src/events.js b/src/events.js index 31614b5..7deeff0 100644 --- a/src/events.js +++ b/src/events.js @@ -1,19 +1,22 @@ /* * Basic event handling. */ -var events; -events.handlers_ = []; -events.on = function (eventtype, callback) { + +var eventEmitter = { + handlers_: [], + on: function on (object, eventtype, callback) { if (typeof callback === 'function') { - this.handlers_.push([eventtype, callback]); + this.handlers_.push([object, eventtype, callback]); } else { - throw new TypeError('Callback is not a function.'); + throw new TypeError('Callback is not a function.'); } + }, + trigger: function trigger (object, eventtype) { + this.handlers_.forEach( function(h) { + if (h[0] === object && + h[1] === eventtype) { + h[2].apply(); + } + }); + } }; -events.trigger = function (eventtype) { - for (var i = 0, i < handlers_.length i++) { - if (this.handlers_[i][0] === event) { - this.handlers_[i][1].apply(); - } - } -}; \ No newline at end of file diff --git a/src/html.js b/src/html.js deleted file mode 100644 index 5f81ec8..0000000 --- a/src/html.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Create and Manipulate Html Elements - */ -var html; - -html.createEl = function (elementName, classSuffix) { - classSuffix = classSuffix || ''; - var el = document.createElement(elementName); - el.className = prefix_ + classSuffix; - return el; -}; - -html.createLine = function (cue) { - var line = html.createEl('div', '-line'); - var timestamp = html.createEl('span', '-timestamp') - var text = html.createEl('span', '-text'); - line.setAttribute('data-begin', cue.startTime); - timestamp.textContent = utils.secondsToTime(cue.startTime); - text.innerHTML = cue.text; - line.appendChild(timestamp); - line.appendChild(text); - return line; -}; - -html.createTranscriptBody = function (track) { - if (typeof track !== 'object') { - track = player_.textTracks()[track]; - } - var body = createScrollable(html.createEl('div', '-body')); - var el = body.element; - var line, i; - var fragment = document.createDocumentFragment(); - var createTranscript = function () { - var cues = track.cues(); - for (i = 0; i < cues.length; i++) { - line = html.createLine(cues[i]); - fragment.appendChild(line); - } - el.innerHTML = ''; - el.appendChild(fragment); - el.setAttribute('lang', track.language()); - }; - el.addEventListener('click', function (event) { - var clickedClasses = event.target.classList; - var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin'); - if (clickedTime !== undefined && clickedTime !== null) { // can be zero - if ((settings_.clickArea === 'line') || // clickArea: 'line' activates on all elements - (settings_.clickArea === 'timestamp' && clickedClasses.contains(prefix_ + '-timestamp')) || - (settings_.clickArea === 'text' && clickedClasses.contains(prefix_ + '-text'))) { - player_.currentTime(clickedTime); - } - } - }); - if (track.readyState() !== 2) { - track.load(); - track.on('loaded', createTranscript); - } else { - createTranscript(); - } - return el; -}; - -html.createTitle = function () { - var header = html.createEl('header', '-header'); - header.textContent = utils.localize('Transcript'); - return header; -}; - -html.createSelector = function () { - var select = html.createEl('select', '-selector'); - var i, track, option; - for (i = 0; i < tracks.length; i++) { - track = validTracks_[i]; - option = document.createElement('option'); - option.value = i; - option.textContent = track.label() + ' (' + track.language() + ')'; - select.appendChild(option); - } - select.addEventListener('change', function (e) { - html.replaceTranscriptBody(document.querySelector('#' + prefix_ + '-' + player_.id() + '-selector option:checked').value); - }); - return select; -}; - -html.createTranscript = function () { - var el = document.createElement('div'); - var elsetAttribute('id', prefix_ + '-' + player_.id()); - var title = html.createTitle(); - var selector = html.createSelector(); - var body = html.createTranscriptBody(currentTrack_); - el.appendChild(title); - el.appendChild(selector); - el.appendChild(body); - return el; -}; - -html.replaceTranscriptBody = function (track) { - var oldbody = document.querySelector('#' + prefix_ + '-' + player_.id()); - var newbody = html.createTranscriptBody(track); - oldbody.parent.replaceChild(newbody, oldbody); -} diff --git a/src/main.js b/src/main.js index 8f2c953..cf6ae05 100644 --- a/src/main.js +++ b/src/main.js @@ -1,96 +1,26 @@ -/*global window, videojs*/ - - var defaults = { - autoscroll: true, - clickArea: 'line' +/*global window, videojs, my, defaults, trackList, transcriptWidget*/ +var transcript = function (options) { + my.validTracks = trackList.get(); + my.settings = videojs.util.mergeOptions(defaults, options); + my.widget = transcriptWidget.create(); + var timeUpdate = function () { + my.widget.setCue(my.player.currentTime()); }; - - var transcript = function (options) { - var settings_ = videojs.util.mergeOptions(defaults, options); - var getAllTracks = function () { - var i, kind; - var validTracks = []; - var tracks = player_.textTracks(); - for (i = 0; i < tracks.length; i++) { - kind = tracks[i].kind(); - if (kind === 'captions' || kind === 'subtitles') { - validTracks.push(tracks[i]); - } - } - return validTracks; - }; - var getActiveTrack = function (tracks) { - var i; - for (i = 0; i < tracks.length; i++) { - if (tracks[i].mode() === 2) { - return tracks[i]; - } - } - return currentTrack_ || tracks[0]; - }; - var getCaptionNodes = function () { - var i, node, caption; - var nodes = document.querySelectorAll('#' + el_.id + ' > .' + prefix_ + '-line'); - var captions = []; - for (i = 0; i < nodes.length; i++) { - node = nodes[i]; - caption = { - 'element': node, - 'begin': node.getAttribute('data-begin'), - }; - captions.push(caption); - } - return captions; - }; - var timeUpdate = function () { - var caption, end, i; - var time = player_.currentTime(); - var captions = getCaptionNodes(); - for (i = 0; i < captions.length; i++) { - caption = captions[i]; - // Remain active until next caption. - // On final caption, remain active until video duration if known, or forever; - if (i < captions.length - 1) { - end = captions[i + 1].begin; - } else { - end = player_.duration() || Infinity; - } - if (time > caption.begin && time < end) { - if (!caption.element.classList.contains('is-active')) { // don't update if it hasn't changed - caption.element.classList.add('is-active'); - // if (settings.autoscroll && - // Scroller.canScroll(htmlContainer) && - // !Scroller.inUse()) { - // Scroller.scrollToElement(caption.element); - // } - } - } else { - caption.element.classList.remove('is-active'); - } - } - }; - validTracks_ = getAllTracks(); - var trackChange = function () { - currentTrack_ = getActiveTrack(validTracks_); - html.replaceTranscriptBody(currentTrack_) - }; - if (validTracks_.length > 0) { - el_ = html.createTranscript; - trackChange(); - player_.on('timeupdate', timeUpdate); - player_.on('captionstrackchange', trackChange); - player_.on('subtitlestrackchange', trackChange); - } else { - throw new Error('videojs-transcript: No tracks found!'); - } - var el = function () { - return el_; - }; - return { - el: el, - setTrack: trackChange, - options: options_, - }; + var updateTrack = function () { + my.currentTrack = trackList.active(); + my.widget.setTrack(my.currentTrack); }; - videojs.plugin('transcript', transcript); -}(window, videojs)); + if (my.validTracks.length > 0) { + updateTrack(); + my.player.on('timeupdate', timeUpdate); + my.player.on('captionstrackchange', updateTrack); + my.player.on('subtitlestrackchange', updateTrack); + } else { + throw new Error('videojs-transcript: No tracks found!'); + } + return { + el: my.widget.el, + setTrack: my.widget.setTrack, + }; +}; +videojs.plugin('transcript', transcript); diff --git a/src/options.js b/src/options.js index c13afa3..750235b 100644 --- a/src/options.js +++ b/src/options.js @@ -1,10 +1,15 @@ /* - * Options + * Shared Setup */ - var validTracks_, //array of track objects - currentTrack_, //track object - currentCue_, //elemnt - settings_, //object - el_; - var prefix_ = 'transcript'; - var player_ = this; + +// Global settings +var my = {}; +my.settings = {}; +my.prefix = 'transcript'; +my.player = this; + +// Defaults +var defaults = { + autoscroll: true, + clickArea: 'line' +}; diff --git a/src/outro.js b/src/outro.js index 636d243..89b22a3 100644 --- a/src/outro.js +++ b/src/outro.js @@ -1 +1 @@ -}(window, window.videojs)); \ No newline at end of file +}(window, videojs)); diff --git a/src/polyfill.js b/src/polyfill.js new file mode 100644 index 0000000..9ae0cff --- /dev/null +++ b/src/polyfill.js @@ -0,0 +1,77 @@ +/* + * Polyfills + */ + +// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel +// MIT license +// https://gist.github.com/paulirish/1579671 +(function() { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] + || window[vendors[x]+'CancelRequestAnimationFrame']; + } + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +}()); + +// Object.create() polyfill +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Polyfill +if (typeof Object.create != 'function') { + Object.create = (function() { + var Object = function() {}; + return function (prototype) { + if (arguments.length > 1) { + throw Error('Second argument not supported'); + } + if (typeof prototype != 'object') { + throw TypeError('Argument must be an object'); + } + Object.prototype = prototype; + var result = new Object(); + Object.prototype = null; + return result; + }; + })(); +} + +// forEach polyfill +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill +if (!Array.prototype.forEach) { + Array.prototype.forEach = function(callback, thisArg) { + var T, k; + if (this == null) { + throw new TypeError(' this is null or not defined'); + } + var O = Object(this); + var len = O.length >>> 0; + if (typeof callback != "function") { + throw new TypeError(callback + ' is not a function'); + } + if (arguments.length > 1) { + T = thisArg; + } + k = 0; + while (k < len) { + var kValue; + if (k in O) { + kValue = O[k]; + callback.call(T, kValue, k, O); + } + k++; + } + }; +} diff --git a/src/scrollable.js b/src/scrollable.js index 2f69508..435e96c 100644 --- a/src/scrollable.js +++ b/src/scrollable.js @@ -1,119 +1,131 @@ /* * Scrollable class to handle scrolling. + * Need to clean up some unneccesarily exposed methods later... */ -var scrollablePrototype = { - - easeOut: function (time, start, change, duration) { - return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2)); - }, - - // Animate the scrolling. - scrollTo: function (element, newPos, duration) { - var startTime = Date.now(); - var startPos = element.scrollTop; - - // Don't try to scroll beyond the limits. You won't get there and this will loop forever. - newPos = Math.max(0, newPos); - newPos = Math.min(element.scrollHeight - element.clientHeight, newPos); - var change = newPos - startPos; - - // This inner function is called until the elements scrollTop reaches newPos. - var updateScroll = function () { - var now = Date.now(); - var time = now - startTime; - isAutoScrolling = true; - element.scrollTop = easeOut(time, startPos, change, duration); - if (element.scrollTop !== newPos) { - requestAnimationFrame(updateScroll, element); - } + +/*global my*/ +var scrollable = function (plugin) { +'use strict'; + var scrollablePrototype = function() { + var easeOut = function (time, start, change, duration) { + return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2)); + }; + + // Animate the scrolling. + var scrollTo = function (element, newPos, duration) { + var startTime = Date.now(); + var startPos = element.scrollTop; + + // Don't try to scroll beyond the limits. You won't get there and this will loop forever. + newPos = Math.max(0, newPos); + newPos = Math.min(element.scrollHeight - element.clientHeight, newPos); + var change = newPos - startPos; + + // This inner function is called until the elements scrollTop reaches newPos. + var updateScroll = function () { + var now = Date.now(); + var time = now - startTime; + this.isAutoScrolling = true; + element.scrollTop = this.easeOut(time, startPos, change, duration); + if (element.scrollTop !== newPos) { + requestAnimationFrame(updateScroll, element); + } + }; + requestAnimationFrame(updateScroll, element); }; - requestAnimationFrame(updateScroll, element); - }, - - // Scroll an element's parent so the element is brought into view. - scrollToElement: function (element) { - var parent = element.parentElement; - var parentOffsetBottom = parent.offsetTop + parent.clientHeight; - var elementOffsetBottom = element.offsetTop + element.clientHeight; - var relPos = (element.offsetTop + element.clientHeight) - parent.offsetTop; - var newPos; - - // If the line is above the top of the parent view, were scrolling up, - // so we want to move the top of the element downwards to match the top of the parent. - if (relPos < parent.scrollTop) { - newPos = element.offsetTop - parent.offsetTop; - - // If the line is below the parent view, we're scrolling down, so we want the - // bottom edge of the line to move up to meet the bottom edge of the parent. - } else if (relPos > (parent.scrollTop + parent.clientHeight)) { - newPos = elementOffsetBottom - parentOffsetBottom; - } - - // Don't try to scroll if we haven't set a new position. If we didn't - // set a new position the line is already in view (i.e. It's not above - // or below the view) - // And don't try to scroll when the element is already in position. - if (newPos !== undefined && parent.scrollTop !== newPos) { - scrollTo(parent, newPos, 400); - } - }; - initHandlers: function () { - var el = this.element; - // The scroll event. We want to keep track of when the user is scrolling the transcript. - el.addEventListener('scroll', function () { - if (this.isAutoScrolling) { - - // If isAutoScrolling was set to true, we can set it to false and then ignore this event. - this.isAutoScrolling = false; // event handled - } else { - - // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class. - this.userIsScrolling = true; - el.classList.add('is-inuse'); - } - }); - - // The mouseover event. - el.addEventListener('mouseover', function () { - this.mouseIsOverTranscript = true; - }); - el.addEventListener('mouseout', function () { - this.mouseIsOverTranscript = false; - - // Have a small delay before deciding user as done interacting. - setTimeout(function () { - - // Make sure the user didn't move the pointer back in. - if (!this.mouseIsOverTranscript) { - this.userIsScrolling = false; - el.classList.remove('is-inuse'); + return { + // Scroll an element's parent so the element is brought into view. + scrollToElement: function (element) { + var parent = element.parentElement; + var parentOffsetBottom = parent.offsetTop + parent.clientHeight; + var elementOffsetBottom = element.offsetTop + element.clientHeight; + var relPos = (element.offsetTop + element.clientHeight) - parent.offsetTop; + var newPos; + + // If the line is above the top of the parent view, were scrolling up, + // so we want to move the top of the element downwards to match the top of the parent. + if (relPos < parent.scrollTop) { + newPos = element.offsetTop - parent.offsetTop; + + // If the line is below the parent view, we're scrolling down, so we want the + // bottom edge of the line to move up to meet the bottom edge of the parent. + } else if (relPos > (parent.scrollTop + parent.clientHeight)) { + newPos = elementOffsetBottom - parentOffsetBottom; } - }, 1000); - }); - }, - - // Return whether the element is scrollable. - canScroll: function () { - var el = this.element - return el.scrollHeight > el.offsetHeight; - }, - - // Return whether the user is interacting with the transcript. - inUse: function () { - return this.userIsScrolling; - } -}; - -createScrollable = function (element) { - var ob = Object.create(scrollablePrototype); - ob.element = element; - - // defaults - ob.userIsScrolling = false; - ob.mouseIsOver = false; - ob.isAutoScrolling = true; - return ob; -}; + // Don't try to scroll if we haven't set a new position. If we didn't + // set a new position the line is already in view (i.e. It's not above + // or below the view) + // And don't try to scroll when the element is already in position. + if (newPos !== undefined && parent.scrollTop !== newPos) { + scrollTo(parent, newPos, 400); + } + }, + + initHandlers: function () { + var el = this.element; + // The scroll event. We want to keep track of when the user is scrolling the transcript. + el.addEventListener('scroll', function () { + if (this.isAutoScrolling) { + + // If isAutoScrolling was set to true, we can set it to false and then ignore this event. + this.isAutoScrolling = false; // event handled + } else { + + // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class. + this.userIsScrolling = true; + el.classList.add('is-inuse'); + } + }); + + // The mouseover event. + el.addEventListener('mouseover', function () { + this.mouseIsOverTranscript = true; + }); + el.addEventListener('mouseout', function () { + this.mouseIsOverTranscript = false; + + // Have a small delay before deciding user as done interacting. + setTimeout(function () { + + // Make sure the user didn't move the pointer back in. + if (!this.mouseIsOverTranscript) { + this.userIsScrolling = false; + el.classList.remove('is-inuse'); + } + }, 1000); + }); + }, + + // Return whether the element is scrollable. + canScroll: function () { + var el = this.element; + return el.scrollHeight > el.offsetHeight; + }, + + // Return whether the user is interacting with the transcript. + inUse: function () { + return this.userIsScrolling; + }, + el: function () { + return this.element; + }, + }; + }; + //Factory function + var createScrollable = function (element) { + var ob = Object.create(scrollablePrototype); + ob.element = element; + + // defaults + ob.userIsScrolling = false; + ob.mouseIsOver = false; + ob.isAutoScrolling = true; + return ob; + }; + return { + create: createScrollable + }; +}(my); diff --git a/src/shims.js b/src/shims.js deleted file mode 100644 index 4ea49d0..0000000 --- a/src/shims.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Compatibility Shims - */ - -/* requestAnimationFrame polyfill */ -var requestAnimationFrame = - window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.msRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - function (callback) { - window.setTimeout(callback, 1000 / 60); - }; - -/*Object.create Shim*/ -if (!Object.create) { - Object.create = function (o) { - if (arguments.length > 1) { - throw new Error('Object.create implementation only accepts the first parameter.'); - } - function F() {} - F.prototype = o; - return new F(); - }; -} \ No newline at end of file diff --git a/src/tracklist.js b/src/tracklist.js new file mode 100644 index 0000000..f3bcfa1 --- /dev/null +++ b/src/tracklist.js @@ -0,0 +1,30 @@ +/* + * Tracklist Helper + */ + +/*global my*/ +var trackList = function (plugin) { + var activeTrack; + return { + get: function () { + var validTracks = []; + my.tracks = my.player.textTracks(); + my.tracks.forEach(function (track) { + if (track.kind() === 'captions' || track.kind() === 'subtitles') { + validTracks.push(track); + } + }); + return validTracks; + }, + active: function (tracks) { + tracks.forEach(function (track) { + if (track.mode() === 2) { + activeTrack = track; + return track; + } + }); + // fallback to first track + return activeTrack || tracks[0]; + }, + }; +}(my); diff --git a/src/utils.js b/src/utils.js index e58176e..ec0cc31 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,18 +1,29 @@ /* * Utils */ -var utils; -utils.secondsToTime = function (timeInSeconds) { - var hour = Math.floor(timeInSeconds / 3600); - var min = Math.floor(timeInSeconds % 3600 / 60); - var sec = Math.floor(timeInSeconds % 60); - sec = (sec < 10) ? '0' + sec : sec; - min = (hour > 0 && min < 10) ? '0' + min : min; - if (hour > 0) { - return hour + ':' + min + ':' + sec; - } - return min + ':' + sec; -}; -utils.localize = function (string) { - return string; // TODO: do something here; -}; + +/*global my*/ +var utils = (function (plugin) { + return { + secondsToTime: function (timeInSeconds) { + var hour = Math.floor(timeInSeconds / 3600); + var min = Math.floor(timeInSeconds % 3600 / 60); + var sec = Math.floor(timeInSeconds % 60); + sec = (sec < 10) ? '0' + sec : sec; + min = (hour > 0 && min < 10) ? '0' + min : min; + if (hour > 0) { + return hour + ':' + min + ':' + sec; + } + return min + ':' + sec; + }, + localize: function (string) { + return string; // TODO: do something here; + }, + createEl: function (elementName, classSuffix) { + classSuffix = classSuffix || ''; + var el = document.createElement(elementName); + el.className = plugin.prefix + classSuffix; + return el; + } + }; +}(my)); diff --git a/src/widget.js b/src/widget.js new file mode 100644 index 0000000..06cab7c --- /dev/null +++ b/src/widget.js @@ -0,0 +1,103 @@ +/* + * Create and Manipulate DOM Widgets + */ + +/*globals utils, eventEmitter, my, scrollable*/ +var selectorWidget = function (plugin) { + var on = function (event, callback) { + eventEmitter.on(this, event, callback); + }; + var trigger = function(event) { + eventEmitter.trigger(this, event); + }; + var create = function () { + var select = utils.createEl('select', '-selector'); + plugin.validTracks.forEach(function (track, i) { + var option = document.createElement('option'); + option.value = i; + option.textContent = track.label() + ' (' + track.language() + ')'; + select.appendChild(option); + }); + select.addEventListener('change', function (e) { + trigger('change'); + }); + return select; + }; + return { + create: create, + on: on + }; +}(my); + +var transcriptWidget = function (plugin) { + 'use strict'; + + var createLine = function (cue) { + var line = utils.createEl('div', '-line'); + var timestamp = utils.createEl('span', '-timestamp'); + var text = utils.createEl('span', '-text'); + line.setAttribute('data-begin', cue.startTime); + timestamp.textContent = utils.secondsToTime(cue.startTime); + text.innerHTML = cue.text; + line.appendChild(timestamp); + line.appendChild(text); + return line; + }; + + var createTranscriptBody = function (track) { + if (typeof track !== 'object') { + track = plugin.player.textTracks()[track]; + } + var body = scrollable.create(utils.createEl('div', '-body')); + var el = body.element; + var line, i; + var fragment = document.createDocumentFragment(); + var createTranscript = function () { + var cues = track.cues(); + for (i = 0; i < cues.length; i++) { + line = createLine(cues[i]); + fragment.appendChild(line); + } + el.innerHTML = ''; + el.appendChild(fragment); + el.setAttribute('lang', track.language()); + }; + }; + + var createTitle = function () { + var header = utils.createEl('header', '-header'); + header.textContent = utils.localize('Transcript'); + return header; + }; + + var createTranscript = function () { + var el = document.createElement('div'); + plugin.el.setAttribute('id', plugin.prefix + '-' + plugin.player.id()); + var title = createTitle(); + var selector = selectorWidget.create(); + var body = createTranscriptBody(plugin.currentTrack); + el.appendChild(title); + el.appendChild(selector); + el.appendChild(body.el); + return el; + }; + + //need to implement these methods + var setTrack = function () { + + }; + var setCue = function () { + + }; + var el = function () { + + }; + + return { + create: createTranscript, + setTrack: setTrack, + setCue: setCue, + el : el + }; + +}(my); From 2e229fd1a2e5519ce545fbb59c84ac2a6215db34 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 26 Sep 2014 23:31:31 -0400 Subject: [PATCH 3/8] More tweaks --- dist/videojs-transcript.js | 695 ++++++++++++++++++++----------------- src/main.js | 1 + src/scrollable.js | 3 +- src/widget.js | 9 +- 4 files changed, 378 insertions(+), 330 deletions(-) diff --git a/dist/videojs-transcript.js b/dist/videojs-transcript.js index d309bee..732d509 100644 --- a/dist/videojs-transcript.js +++ b/dist/videojs-transcript.js @@ -1,372 +1,419 @@ -/*! videojs-transcript - v0.0.0 - 2014-09-20 +/*! videojs-transcript - v0.0.0 - 2014-09-26 * Copyright (c) 2014 Matthew Walsh; Licensed MIT */ (function (window, videojs) { 'use strict'; -var Utils = (function () { - var niceTimestamp = function (timeInSeconds) { - var hour = Math.floor(timeInSeconds / 3600); - var min = Math.floor(timeInSeconds % 3600 / 60); - var sec = Math.floor(timeInSeconds % 60); - sec = (sec < 10) ? '0' + sec : sec; - min = (hour > 0 && min < 10) ? '0' + min : min; - if (hour > 0) { - return hour + ':' + min + ':' + sec; - } - return min + ':' + sec; - }; - return { - niceTimestamp : niceTimestamp, - }; -}()); -var Html = (function () { - var myContainer, subContainer, myPlayer, myPrefix, settings; - var createSeekClickHandler = function (time) { - return function (e) { - myPlayer.currentTime(time); +// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel +// MIT license +// https://gist.github.com/paulirish/1579671 +(function() { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] + || window[vendors[x]+'CancelRequestAnimationFrame']; + } + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; }; - }; + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +}()); - var createLine = function (cue) { - var line = document.createElement('div'); - var timestamp = document.createElement('span'); - var text = document.createElement('span'); - line.className = myPrefix + '-line'; - line.setAttribute('data-begin', cue.startTime); - timestamp.className = myPrefix + '-timestamp'; - timestamp.textContent = Utils.niceTimestamp(cue.startTime); - text.className = myPrefix + '-text'; - text.innerHTML = cue.text; - line.appendChild(timestamp); - line.appendChild(text); - return line; - }; - var setTrack = function (track) { - if (typeof track !== 'object') { - track = myPlayer.textTracks()[track]; - } - if (subContainer === undefined) { - throw new Error('videojs-transcript: Html not initialized!'); - } - var line, i; - var fragment = document.createDocumentFragment(); - var createTranscript = function () { - var cues = track.cues(); - for (i = 0; i < cues.length; i++) { - line = createLine(cues[i], myPrefix); - fragment.appendChild(line); +// Object.create() polyfill +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Polyfill +if (typeof Object.create != 'function') { + Object.create = (function() { + var Object = function() {}; + return function (prototype) { + if (arguments.length > 1) { + throw Error('Second argument not supported'); } - subContainer.innerHTML = ''; - subContainer.appendChild(fragment); - subContainer.setAttribute('lang', track.language()); - }; - subContainer.addEventListener('click', function (event) { - var clickedClasses = event.target.classList; - var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin'); - if (clickedTime !== undefined && clickedTime !== null) { // can be zero - if ((settings.clickArea === 'line') || // clickArea: 'line' activates on all elements - (settings.clickArea === 'timestamp' && clickedClasses.contains(myPrefix + '-timestamp')) || - (settings.clickArea === 'text' && clickedClasses.contains(myPrefix + '-text'))) { - myPlayer.currentTime(clickedTime); - } + if (typeof prototype != 'object') { + throw TypeError('Argument must be an object'); } - }); - if (track.readyState() !== 2) { - track.load(); - track.on('loaded', createTranscript); - } else { - createTranscript(); + Object.prototype = prototype; + var result = new Object(); + Object.prototype = null; + return result; + }; + })(); +} + +// forEach polyfill +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill +if (!Array.prototype.forEach) { + Array.prototype.forEach = function(callback, thisArg) { + var T, k; + if (this == null) { + throw new TypeError(' this is null or not defined'); } - }; - var createSelector = function (tracks) { - var select = document.createElement('select'); - var i, track, option; - for (i = 0; i < tracks.length; i++) { - track = tracks[i]; - option = document.createElement('option'); - option.value = i; - option.textContent = track.label() + ' (' + track.language() + ')'; - select.appendChild(option); + var O = Object(this); + var len = O.length >>> 0; + if (typeof callback != "function") { + throw new TypeError(callback + ' is not a function'); + } + if (arguments.length > 1) { + T = thisArg; + } + k = 0; + while (k < len) { + var kValue; + if (k in O) { + kValue = O[k]; + callback.call(T, kValue, k, O); + } + k++; } - select.addEventListener('change', function (e) { - setTrack(document.querySelector('#' + myPrefix + '-' + myPlayer.id() + ' option:checked').value); - }); - return select; - }; - var init = function (container, player, prefix, plugin) { - myContainer = container; - myPlayer = player; - myPrefix = prefix; - subContainer = document.createElement('div'); - settings = plugin.options; - myContainer.className = prefix; - subContainer.className = prefix + '-lines'; - myContainer.id = myPrefix + '-' + myPlayer.id(); - myContainer.appendChild(createSelector(myPlayer.textTracks())); - myContainer.appendChild(subContainer); }; +} + +// Global settings +var my = {}; +my.settings = {}; +my.prefix = 'transcript'; +my.player = this; + +// Defaults +var defaults = { + autoscroll: true, + clickArea: 'line' +}; + +/*global my*/ +var utils = (function (plugin) { return { - init: init, - setTrack: setTrack, + secondsToTime: function (timeInSeconds) { + var hour = Math.floor(timeInSeconds / 3600); + var min = Math.floor(timeInSeconds % 3600 / 60); + var sec = Math.floor(timeInSeconds % 60); + sec = (sec < 10) ? '0' + sec : sec; + min = (hour > 0 && min < 10) ? '0' + min : min; + if (hour > 0) { + return hour + ':' + min + ':' + sec; + } + return min + ':' + sec; + }, + localize: function (string) { + return string; // TODO: do something here; + }, + createEl: function (elementName, classSuffix) { + classSuffix = classSuffix || ''; + var el = document.createElement(elementName); + el.className = plugin.prefix + classSuffix; + return el; + } }; -}()); -var Scroller = (function () { +}(my)); -// Keep track when the user is scrolling the transcript. - - var userIsScrolling = false; +var eventEmitter = { + handlers_: [], + on: function on (object, eventtype, callback) { + if (typeof callback === 'function') { + this.handlers_.push([object, eventtype, callback]); + } else { + throw new TypeError('Callback is not a function.'); + } + }, + trigger: function trigger (object, eventtype) { + this.handlers_.forEach( function(h) { + if (h[0] === object && + h[1] === eventtype) { + h[2].apply(); + } + }); + } +}; + +/*global my*/ +var scrollable = function (plugin) { +'use strict'; + var scrollablePrototype = function() { + var easeOut = function (time, start, change, duration) { + return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2)); + }; -// Keep track of when the mouse is hovering the transcript. + // Animate the scrolling. + var scrollTo = function (element, newPos, duration) { + var startTime = Date.now(); + var startPos = element.scrollTop; + + // Don't try to scroll beyond the limits. You won't get there and this will loop forever. + newPos = Math.max(0, newPos); + newPos = Math.min(element.scrollHeight - element.clientHeight, newPos); + var change = newPos - startPos; + + // This inner function is called until the elements scrollTop reaches newPos. + var updateScroll = function () { + var now = Date.now(); + var time = now - startTime; + this.isAutoScrolling = true; + element.scrollTop = this.easeOut(time, startPos, change, duration); + if (element.scrollTop !== newPos) { + requestAnimationFrame(updateScroll, element); + } + }; + requestAnimationFrame(updateScroll, element); + }; - var mouseIsOverTranscript = false; + return { + // Scroll an element's parent so the element is brought into view. + scrollToElement: function (element) { + var parent = element.parentElement; + var parentOffsetBottom = parent.offsetTop + parent.clientHeight; + var elementOffsetBottom = element.offsetTop + element.clientHeight; + var relPos = (element.offsetTop + element.clientHeight) - parent.offsetTop; + var newPos; + + // If the line is above the top of the parent view, were scrolling up, + // so we want to move the top of the element downwards to match the top of the parent. + if (relPos < parent.scrollTop) { + newPos = element.offsetTop - parent.offsetTop; + + // If the line is below the parent view, we're scrolling down, so we want the + // bottom edge of the line to move up to meet the bottom edge of the parent. + } else if (relPos > (parent.scrollTop + parent.clientHeight)) { + newPos = elementOffsetBottom - parentOffsetBottom; + } -// The initial element creation triggers a scroll event. We don't want to consider -// this as the user scrolling, so we initialize the isAutoScrolling flag to true. + // Don't try to scroll if we haven't set a new position. If we didn't + // set a new position the line is already in view (i.e. It's not above + // or below the view) + // And don't try to scroll when the element is already in position. + if (newPos !== undefined && parent.scrollTop !== newPos) { + scrollTo(parent, newPos, 400); + } + }, - var isAutoScrolling = true; + initHandlers: function () { + var el = this.element; + // The scroll event. We want to keep track of when the user is scrolling the transcript. + el.addEventListener('scroll', function () { + if (this.isAutoScrolling) { -// requestAnimationFrame compatibility shim. + // If isAutoScrolling was set to true, we can set it to false and then ignore this event. + this.isAutoScrolling = false; // event handled + } else { - var requestAnimationFrame = - window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.msRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - function (callback) { - window.setTimeout(callback, 1000 / 60); + // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class. + this.userIsScrolling = true; + el.classList.add('is-inuse'); + } + }); + + // The mouseover event. + el.addEventListener('mouseover', function () { + this.mouseIsOverTranscript = true; + }); + el.addEventListener('mouseout', function () { + this.mouseIsOverTranscript = false; + + // Have a small delay before deciding user as done interacting. + setTimeout(function () { + + // Make sure the user didn't move the pointer back in. + if (!this.mouseIsOverTranscript) { + this.userIsScrolling = false; + el.classList.remove('is-inuse'); + } + }, 1000); + }); + }, + + // Return whether the element is scrollable. + canScroll: function () { + var el = this.element; + return el.scrollHeight > el.offsetHeight; + }, + + // Return whether the user is interacting with the transcript. + inUse: function () { + return this.userIsScrolling; + }, + el: function () { + return this.element; + }, }; - -// For smooth animation. - - var easeOut = function (time, start, change, duration) { - return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2)); }; + //Factory function + var createScrollable = function (element) { + var ob = Object.create(scrollablePrototype()); + ob.element = element; + // defaults + ob.userIsScrolling = false; + ob.mouseIsOver = false; + ob.isAutoScrolling = true; + return ob; + }; + return { + create: createScrollable + }; +}(my); -// Animate the scrolling. - var scrollTo = function (element, newPos, duration) { - var startTime = Date.now(); - var startPos = element.scrollTop; +/*global my*/ +var trackList = function (plugin) { + var activeTrack; + return { + get: function () { + var validTracks = []; + my.tracks = my.player.textTracks(); + my.tracks.forEach(function (track) { + if (track.kind() === 'captions' || track.kind() === 'subtitles') { + validTracks.push(track); + } + }); + return validTracks; + }, + active: function (tracks) { + tracks.forEach(function (track) { + if (track.mode() === 2) { + activeTrack = track; + return track; + } + }); + // fallback to first track + return activeTrack || tracks[0]; + }, + }; +}(my); -// Don't try to scroll beyond the limits. You won't get there and this will loop forever. +/*globals utils, eventEmitter, my, scrollable*/ +var selectorWidget = function (plugin) { + var on = function (event, callback) { + eventEmitter.on(this, event, callback); + }; + var trigger = function(event) { + eventEmitter.trigger(this, event); + }; + var create = function () { + var select = utils.createEl('select', '-selector'); + plugin.validTracks.forEach(function (track, i) { + var option = document.createElement('option'); + option.value = i; + option.textContent = track.label() + ' (' + track.language() + ')'; + select.appendChild(option); + }); + select.addEventListener('change', function (e) { + trigger('change'); + }); + return select; + }; + return { + create: create, + on: on + }; +}(my); - newPos = Math.max(0, newPos); - newPos = Math.min(element.scrollHeight - element.clientHeight, newPos); - var change = newPos - startPos; +var transcriptWidget = function (plugin) { + 'use strict'; -// This inner function is called until the elements scrollTop reaches newPos. + var createLine = function (cue) { + var line = utils.createEl('div', '-line'); + var timestamp = utils.createEl('span', '-timestamp'); + var text = utils.createEl('span', '-text'); + line.setAttribute('data-begin', cue.startTime); + timestamp.textContent = utils.secondsToTime(cue.startTime); + text.innerHTML = cue.text; + line.appendChild(timestamp); + line.appendChild(text); + return line; + }; - var updateScroll = function () { - var now = Date.now(); - var time = now - startTime; - isAutoScrolling = true; - element.scrollTop = easeOut(time, startPos, change, duration); - if (element.scrollTop !== newPos) { - requestAnimationFrame(updateScroll, element); + var createTranscriptBody = function (track) { + if (typeof track !== 'object') { + track = plugin.player.textTracks()[track]; + } + var body = scrollable.create(utils.createEl('div', '-body')); + var el = body.element; + var line, i; + var fragment = document.createDocumentFragment(); + var createTranscript = function () { + var cues = track.cues(); + for (i = 0; i < cues.length; i++) { + line = createLine(cues[i]); + fragment.appendChild(line); } + el.innerHTML = ''; + el.appendChild(fragment); + el.setAttribute('lang', track.language()); }; - requestAnimationFrame(updateScroll, element); }; -// Scroll an element's parent so the element is brought into view. - - var scrollToElement = function (element) { - var parent = element.parentElement; - var parentOffsetBottom = parent.offsetTop + parent.clientHeight; - var elementOffsetBottom = element.offsetTop + element.clientHeight; - var relPos = (element.offsetTop + element.clientHeight) - parent.offsetTop; - var newPos; - -// If the line is above the top of the parent view, were scrolling up, -// so we want to move the top of the element downwards to match the top of the parent. - - if (relPos < parent.scrollTop) { - newPos = element.offsetTop - parent.offsetTop; - -// If the line is below the parent view, we're scrolling down, so we want the -// bottom edge of the line to move up to meet the bottom edge of the parent. - - } else if (relPos > (parent.scrollTop + parent.clientHeight)) { - newPos = elementOffsetBottom - parentOffsetBottom; - } - -// Don't try to scroll if we haven't set a new position. If we didn't -// set a new position the line is already in view (i.e. It's not above -// or below the view) -// And don't try to scroll when the element is already in position. - - if (newPos !== undefined && parent.scrollTop !== newPos) { - scrollTo(parent, newPos, 400); - } + var createTitle = function () { + var header = utils.createEl('header', '-header'); + header.textContent = utils.localize('Transcript'); + return header; }; -// Set Event Handlers to monitor user scrolling. - - var initHandlers = function (element) { - -// The scroll event. We want to keep track of when the user is scrolling the transcript. - - element.addEventListener('scroll', function () { - if (isAutoScrolling) { - -// If isAutoScrolling was set to true, we can set it to false and then ignore this event. - - isAutoScrolling = false; // event handled - } else { - -// We only care about when the user scrolls. Set userIsScrolling to true and add a nice class. - - userIsScrolling = true; - element.classList.add('is-inuse'); - } - }); - -// The mouseover event. - - element.addEventListener('mouseover', function () { - mouseIsOverTranscript = true; - }); - element.addEventListener('mouseout', function () { - mouseIsOverTranscript = false; - -// Have a small delay before deciding user as done interacting. - - setTimeout(function () { - -// Make sure the user didn't move the pointer back in. - - if (!mouseIsOverTranscript) { - userIsScrolling = false; - element.classList.remove('is-inuse'); - } - }, 1000); - }); + var createTranscript = function () { + var el = document.createElement('div'); + el.setAttribute('id', plugin.prefix + '-' + plugin.player.id()); + var title = createTitle(); + var selector = selectorWidget.create(); + var body = scrollable.create(createTranscriptBody(plugin.currentTrack)); + el.appendChild(title); + el.appendChild(selector); + el.appendChild(body.element); + this.element = el; + return el; }; -// Return whether the element is scrollable. + //need to implement these methods + var setTrack = function () { - var canScroll = function (container) { - return container.scrollHeight > container.offsetHeight; }; + var setCue = function () { -// Return whether the user is interacting with the transcript. + }; + var el = function () { + return this.element; + }; - var inUse = function () { - return userIsScrolling; + return { + create: createTranscript, + setTrack: setTrack, + setCue: setCue, + el : el }; -// Public Methods +}(my); - return { - scrollToElement : scrollToElement, - initHandlers : initHandlers, - inUse : inUse, - canScroll : canScroll, +var transcript = function (options) { + my.player = this; + my.validTracks = trackList.get(); + my.settings = videojs.util.mergeOptions(defaults, options); + my.widget = transcriptWidget.create(); + var timeUpdate = function () { + my.widget.setCue(my.player.currentTime()); }; -}()); -var Plugin = (function (window, videojs) { - var defaults = { - autoscroll: true, - clickArea: 'line' + var updateTrack = function () { + my.currentTrack = trackList.active(); + my.widget.setTrack(my.currentTrack); }; - - var transcript = function (options) { - var settings = videojs.util.mergeOptions(defaults, options); - var player = this; - var htmlPrefix = 'transcript'; - var htmlContainer = document.createElement('div'); - var tracks; - var currentTrack; - var getAllTracks = function () { - var i, kind; - var validTracks = []; - tracks = player.textTracks(); - for (i = 0; i < tracks.length; i++) { - kind = tracks[i].kind(); - if (kind === 'captions' || kind === 'subtitles') { - validTracks.push(tracks[i]); - } - } - return validTracks; - }; - var getActiveTrack = function (tracks) { - var i; - for (i = 0; i < tracks.length; i++) { - if (tracks[i].mode() === 2) { - return tracks[i]; - } - } - return currentTrack || tracks[0]; - }; - var getCaptionNodes = function () { - var i, node, caption; - var nodes = document.querySelectorAll('#' + htmlContainer.id + ' > .' + htmlPrefix + '-line'); - var captions = []; - for (i = 0; i < nodes.length; i++) { - node = nodes[i]; - caption = { - 'element': node, - 'begin': node.getAttribute('data-begin'), - }; - captions.push(caption); - } - return captions; - }; - var timeUpdate = function () { - var caption, end, i; - var time = player.currentTime(); - var captions = getCaptionNodes(); - for (i = 0; i < captions.length; i++) { - caption = captions[i]; - // Remain active until next caption. - // On final caption, remain active until video duration if known, or forever; - if (i < captions.length - 1) { - end = captions[i + 1].begin; - } else { - end = player.duration() || Infinity; - } - if (time > caption.begin && time < end) { - if (!caption.element.classList.contains('is-active')) { // don't update if it hasn't changed - caption.element.classList.add('is-active'); - if (settings.autoscroll && - Scroller.canScroll(htmlContainer) && - !Scroller.inUse()) { - Scroller.scrollToElement(caption.element); - } - } - } else { - caption.element.classList.remove('is-active'); - } - } - }; - var trackChange = function () { - currentTrack = getActiveTrack(tracks); - Html.setTrack(currentTrack); - }; - tracks = getAllTracks(); - if (tracks.length > 0) { - Html.init(htmlContainer, player, htmlPrefix, this); - Scroller.initHandlers(htmlContainer); - trackChange(); - player.on('timeupdate', timeUpdate); - player.on('captionstrackchange', trackChange); - player.on('subtitlestrackchange', trackChange); - } else { - throw new Error('videojs-transcript: No tracks found!'); - } - var el = function () { - return htmlContainer; - }; - return { - el: el, - setTrack: trackChange, - options: options, - }; + if (my.validTracks.length > 0) { + updateTrack(); + my.player.on('timeupdate', timeUpdate); + my.player.on('captionstrackchange', updateTrack); + my.player.on('subtitlestrackchange', updateTrack); + } else { + throw new Error('videojs-transcript: No tracks found!'); + } + return { + el: my.widget.el, + setTrack: my.widget.setTrack, }; - return {transcript: transcript}; -}(window, videojs)); +}; +videojs.plugin('transcript', transcript); - videojs.plugin('transcript', Plugin.transcript); -}(window, window.videojs)); \ No newline at end of file +}(window, videojs)); diff --git a/src/main.js b/src/main.js index cf6ae05..b486853 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ /*global window, videojs, my, defaults, trackList, transcriptWidget*/ var transcript = function (options) { + my.player = this; my.validTracks = trackList.get(); my.settings = videojs.util.mergeOptions(defaults, options); my.widget = transcriptWidget.create(); diff --git a/src/scrollable.js b/src/scrollable.js index 435e96c..4b7a8c3 100644 --- a/src/scrollable.js +++ b/src/scrollable.js @@ -115,9 +115,8 @@ var scrollable = function (plugin) { }; //Factory function var createScrollable = function (element) { - var ob = Object.create(scrollablePrototype); + var ob = Object.create(scrollablePrototype()); ob.element = element; - // defaults ob.userIsScrolling = false; ob.mouseIsOver = false; diff --git a/src/widget.js b/src/widget.js index 06cab7c..0c96462 100644 --- a/src/widget.js +++ b/src/widget.js @@ -72,13 +72,14 @@ var transcriptWidget = function (plugin) { var createTranscript = function () { var el = document.createElement('div'); - plugin.el.setAttribute('id', plugin.prefix + '-' + plugin.player.id()); + el.setAttribute('id', plugin.prefix + '-' + plugin.player.id()); var title = createTitle(); var selector = selectorWidget.create(); - var body = createTranscriptBody(plugin.currentTrack); + var body = scrollable.create(createTranscriptBody(plugin.currentTrack)); el.appendChild(title); el.appendChild(selector); - el.appendChild(body.el); + el.appendChild(body.element); + this.element = el; return el; }; @@ -90,7 +91,7 @@ var transcriptWidget = function (plugin) { }; var el = function () { - + return this.element; }; return { From 7980df12dfa2e64ae505ddd10c412efcb4302e87 Mon Sep 17 00:00:00 2001 From: walsh9 Date: Sun, 5 Oct 2014 16:22:03 -0400 Subject: [PATCH 4/8] More refactoring --- css/videojs-transcript.css | 4 +- css/videojs-transcript3.css | 2 +- dist/videojs-transcript.js | 152 +++++++++++++++++++++--------------- example.html | 2 +- src/events.js | 9 +++ src/main.js | 13 +-- src/scrollable.js | 30 +++---- src/utils.js | 14 ++++ src/widget.js | 86 ++++++++++---------- 9 files changed, 180 insertions(+), 132 deletions(-) diff --git a/css/videojs-transcript.css b/css/videojs-transcript.css index 539a067..f3307d1 100644 --- a/css/videojs-transcript.css +++ b/css/videojs-transcript.css @@ -4,7 +4,7 @@ font-family: Arial, sans-serif; } -.transcript { +.transcript-body { height: 200px; overflow-y: scroll; } @@ -34,4 +34,4 @@ .transcript-line.is-active { background-color: #e2fbfb; -} \ No newline at end of file +} diff --git a/css/videojs-transcript3.css b/css/videojs-transcript3.css index 0d84470..a8c606b 100644 --- a/css/videojs-transcript3.css +++ b/css/videojs-transcript3.css @@ -4,7 +4,7 @@ font-family: Arial, sans-serif; } -.transcript-lines { +.transcript-body { height: 200px; overflow-y: scroll; border: 1px solid #111; diff --git a/dist/videojs-transcript.js b/dist/videojs-transcript.js index 732d509..bdfd531 100644 --- a/dist/videojs-transcript.js +++ b/dist/videojs-transcript.js @@ -1,4 +1,4 @@ -/*! videojs-transcript - v0.0.0 - 2014-09-26 +/*! videojs-transcript - v0.0.0 - 2014-10-05 * Copyright (c) 2014 Matthew Walsh; Licensed MIT */ (function (window, videojs) { 'use strict'; @@ -112,6 +112,20 @@ var utils = (function (plugin) { var el = document.createElement(elementName); el.className = plugin.prefix + classSuffix; return el; + }, + extend: function(obj) { + var type = typeof obj; + if (!(type === 'function' || type === 'object' && !!obj)) { + return obj; + } + var source, prop; + for (var i = 1, length = arguments.length; i < length; i++) { + source = arguments[i]; + for (prop in source) { + obj[prop] = source[prop]; + } + } + return obj; } }; }(my)); @@ -132,19 +146,28 @@ var eventEmitter = { h[2].apply(); } }); + }, + delegate: function (obj) { + obj.on = function (event, callback) { + eventEmitter.on(obj, event, callback); + }; + obj.trigger = function (obj) { + eventEmitter.trigget(obj, event); + }; + return obj; } }; -/*global my*/ +/*global my, utils*/ var scrollable = function (plugin) { 'use strict'; - var scrollablePrototype = function() { - var easeOut = function (time, start, change, duration) { + var scrollablePrototype = { + easeOut: function (time, start, change, duration) { return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2)); - }; + }, // Animate the scrolling. - var scrollTo = function (element, newPos, duration) { + scrollTo: function (element, newPos, duration) { var startTime = Date.now(); var startPos = element.scrollTop; @@ -164,9 +187,7 @@ var scrollable = function (plugin) { } }; requestAnimationFrame(updateScroll, element); - }; - - return { + }, // Scroll an element's parent so the element is brought into view. scrollToElement: function (element) { var parent = element.parentElement; @@ -243,16 +264,18 @@ var scrollable = function (plugin) { el: function () { return this.element; }, - }; }; //Factory function var createScrollable = function (element) { - var ob = Object.create(scrollablePrototype()); - ob.element = element; - // defaults - ob.userIsScrolling = false; - ob.mouseIsOver = false; - ob.isAutoScrolling = true; + var ob = Object.create(scrollablePrototype) + console.log(ob); + utils.extend(ob, { + element: element, + userIsScrolling : false, + mouseIsOver: false, + isAutoScrolling: true, + }); + console.log(ob); return ob; }; return { @@ -289,35 +312,34 @@ var trackList = function (plugin) { }(my); /*globals utils, eventEmitter, my, scrollable*/ -var selectorWidget = function (plugin) { + +var widget = function (plugin) { + var element = {}; + var body = {}; var on = function (event, callback) { eventEmitter.on(this, event, callback); }; - var trigger = function(event) { + var trigger = function (event) { eventEmitter.trigger(this, event); }; - var create = function () { - var select = utils.createEl('select', '-selector'); - plugin.validTracks.forEach(function (track, i) { + var createTitle = function () { + var header = utils.createEl('header', '-header'); + header.textContent = utils.localize('Transcript'); + return header; + }; + var createSelector = function (){ + var selector = utils.createEl('select', '-selector'); + plugin.validTracks.forEach(function (track, i) { var option = document.createElement('option'); option.value = i; option.textContent = track.label() + ' (' + track.language() + ')'; - select.appendChild(option); + selector.appendChild(option); }); - select.addEventListener('change', function (e) { - trigger('change'); + selector.addEventListener('change', function (e) { + trigger('trackchanged'); }); - return select; - }; - return { - create: create, - on: on + return selector; }; -}(my); - -var transcriptWidget = function (plugin) { - 'use strict'; - var createLine = function (cue) { var line = utils.createEl('div', '-line'); var timestamp = utils.createEl('span', '-timestamp'); @@ -329,62 +351,61 @@ var transcriptWidget = function (plugin) { line.appendChild(text); return line; }; - var createTranscriptBody = function (track) { + //console.log(track.cues()); if (typeof track !== 'object') { track = plugin.player.textTracks()[track]; } - var body = scrollable.create(utils.createEl('div', '-body')); - var el = body.element; + var body = utils.createEl('div', '-body'); var line, i; var fragment = document.createDocumentFragment(); var createTranscript = function () { + console.log(track); var cues = track.cues(); for (i = 0; i < cues.length; i++) { line = createLine(cues[i]); fragment.appendChild(line); } - el.innerHTML = ''; - el.appendChild(fragment); - el.setAttribute('lang', track.language()); + body.innerHTML = ''; + body.appendChild(fragment); + body.setAttribute('lang', track.language()); }; + if (track.readyState() !==2) { + track.load(); + track.on('loaded', createTranscript); + } else { + createTranscript(); + } + return body; }; - - var createTitle = function () { - var header = utils.createEl('header', '-header'); - header.textContent = utils.localize('Transcript'); - return header; - }; - - var createTranscript = function () { + var create = function () { var el = document.createElement('div'); el.setAttribute('id', plugin.prefix + '-' + plugin.player.id()); var title = createTitle(); - var selector = selectorWidget.create(); - var body = scrollable.create(createTranscriptBody(plugin.currentTrack)); + var selector = createSelector(); + this.body = createTranscriptBody(plugin.currentTrack); el.appendChild(title); el.appendChild(selector); - el.appendChild(body.element); + el.appendChild(this.body); this.element = el; - return el; + return this; }; - - //need to implement these methods - var setTrack = function () { - + var setTrack = function (track) { + this.body = createTranscriptBody(track); }; var setCue = function () { - + //need to implement }; var el = function () { return this.element; }; - return { - create: createTranscript, + create: create, setTrack: setTrack, setCue: setCue, - el : el + el : el, + on: on, + trigger: trigger, }; }(my); @@ -392,13 +413,14 @@ var transcriptWidget = function (plugin) { var transcript = function (options) { my.player = this; my.validTracks = trackList.get(); + my.currentTrack = trackList.active(my.validTracks); my.settings = videojs.util.mergeOptions(defaults, options); - my.widget = transcriptWidget.create(); + my.widget = widget.create(); var timeUpdate = function () { my.widget.setCue(my.player.currentTime()); }; var updateTrack = function () { - my.currentTrack = trackList.active(); + my.currentTrack = trackList.active(my.validTracks); my.widget.setTrack(my.currentTrack); }; if (my.validTracks.length > 0) { @@ -410,8 +432,10 @@ var transcript = function (options) { throw new Error('videojs-transcript: No tracks found!'); } return { - el: my.widget.el, - setTrack: my.widget.setTrack, + el: function () { + return my.widget.el(); + }, + setTrack: my.widget.setTrack }; }; videojs.plugin('transcript', transcript); diff --git a/example.html b/example.html index f02e1f6..d66ffde 100644 --- a/example.html +++ b/example.html @@ -5,7 +5,7 @@ Video.js Transcript - +