Move module javascript into common/lib/xmodule. Still TODO: run jasmine tests in that directory

This commit is contained in:
Calen Pennington
2012-07-17 16:46:36 -04:00
parent 1395e5d55c
commit 3fa636e060
40 changed files with 110 additions and 28 deletions

View File

@@ -8,6 +8,7 @@ import re
from datetime import timedelta
from lxml import etree
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
@@ -70,6 +71,8 @@ class CapaModule(XModule):
'''
icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee')]}
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)

View File

@@ -0,0 +1,15 @@
[
{
"content": "Video 1",
"type": "video",
"title": "Video 1"
}, {
"content": "Video 2",
"type": "video",
"title": "Video 2"
}, {
"content": "Sample Problem",
"type": "problem",
"title": "Sample Problem"
}
]

View File

@@ -0,0 +1 @@
<section id="problem_1" class="problems-wrapper" data-url="/problem/url/"></section>

View File

@@ -0,0 +1,20 @@
<h2 class="problem-header">Problem Header</h2>
<section class="problem">
<p>Problem Content</p>
<section class="action">
<input type="hidden" name="problem_id" value="1">
<input type="text" name="input_example_1" id="input_example_1" value="" class="math" />
<span id="display_example_1"></span>
<span id="input_example_1_dynamath"></span>
<input class="check" type="button" value="Check">
<input class="reset" type="button" value="Reset">
<input class="save" type="button" value="Save">
<input class="show" type="button" value="Show Answer">
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
<section class="submission_feedback"></section>
</section>
</section>

View File

@@ -0,0 +1,20 @@
<div id="sequence_1" class="sequence">
<nav class="sequence-nav">
<ol id="sequence-list">
</ol>
<ul class="sequence-nav-buttons">
<li class="prev"><a href="#">Previous</a></li>
<li class="next"><a href="#">Next</a></li>
</ul>
</nav>
<div id="seq_content"></div>
<nav class="sequence-bottom">
<ul class="sequence-nav-buttons">
<li class="prev"><a href="#">Previous</a></li>
<li class="next"><a href="#">Next</a></li>
</ul>
</nav>
</div>

View File

@@ -0,0 +1,12 @@
<div class="course-content">
<div id="video_example" class="video">
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
<div id="example"></div>
</section>
<section class="video-controls"></section>
</article>
</div>
</div>
</div>

View File

@@ -1,5 +0,0 @@
class @Raw
constructor: (@element) ->
@edit_box = $(".edit-box", @element)
save: -> @edit_box.val()

View File

@@ -0,0 +1,295 @@
describe 'Problem', ->
beforeEach ->
# Stub MathJax
window.MathJax =
Hub: jasmine.createSpyObj('MathJax.Hub', ['getAllJax', 'Queue'])
Callback: jasmine.createSpyObj('MathJax.Callback', ['After'])
@stubbedJax = root: jasmine.createSpyObj('jax.root', ['toMathML'])
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
window.update_schematics = ->
loadFixtures 'problem.html'
spyOn Logger, 'log'
spyOn($.fn, 'load').andCallFake (url, callback) ->
$(@).html readFixtures('problem_content.html')
callback()
jasmine.stubRequests()
describe 'constructor', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
it 'set the element', ->
expect(@problem.el).toBe '#problem_1'
describe 'bind', ->
beforeEach ->
spyOn window, 'update_schematics'
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
@problem = new Problem 1, "problem_1", "/problem/url/"
it 'set mathjax typeset', ->
expect(MathJax.Hub.Queue).toHaveBeenCalled()
it 'update schematics', ->
expect(window.update_schematics).toHaveBeenCalled()
it 'bind answer refresh on button click', ->
expect($('section.action input:button')).toHandleWith 'click', @problem.refreshAnswers
it 'bind the check button', ->
expect($('section.action input.check')).toHandleWith 'click', @problem.check
it 'bind the reset button', ->
expect($('section.action input.reset')).toHandleWith 'click', @problem.reset
it 'bind the show button', ->
expect($('section.action input.show')).toHandleWith 'click', @problem.show
it 'bind the save button', ->
expect($('section.action input.save')).toHandleWith 'click', @problem.save
it 'bind the math input', ->
expect($('input.math')).toHandleWith 'keyup', @problem.refreshMath
it 'replace math content on the page', ->
expect(MathJax.Hub.Queue.mostRecentCall.args).toEqual [
['Text', @stubbedJax, ''],
[@problem.updateMathML, @stubbedJax, $('#input_example_1').get(0)]
]
describe 'render', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@bind = @problem.bind
spyOn @problem, 'bind'
describe 'with content given', ->
beforeEach ->
@problem.render 'Hello World'
it 'render the content', ->
expect(@problem.el.html()).toEqual 'Hello World'
it 're-bind the content', ->
expect(@problem.bind).toHaveBeenCalled()
describe 'with no content given', ->
beforeEach ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
callback html: "Hello World"
@problem.render()
it 'load the content via ajax', ->
expect(@problem.el.html()).toEqual 'Hello World'
it 're-bind the content', ->
expect(@problem.bind).toHaveBeenCalled()
describe 'check', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem.answers = 'foo=1&bar=2'
it 'log the problem_check event', ->
@problem.check()
expect(Logger.log).toHaveBeenCalledWith 'problem_check', 'foo=1&bar=2'
it 'submit the answer for check', ->
spyOn $, 'postWithPrefix'
@problem.check()
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_check', 'foo=1&bar=2', jasmine.any(Function)
describe 'when the response is correct', ->
it 'call render with returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'correct', contents: 'Correct!')
@problem.check()
expect(@problem.el.html()).toEqual 'Correct!'
describe 'when the response is incorrect', ->
it 'call render with returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'incorrect', contents: 'Correct!')
@problem.check()
expect(@problem.el.html()).toEqual 'Correct!'
describe 'when the response is undetermined', ->
it 'alert the response', ->
spyOn window, 'alert'
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'Number Only!')
@problem.check()
expect(window.alert).toHaveBeenCalledWith 'Number Only!'
describe 'reset', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
it 'log the problem_reset event', ->
@problem.answers = 'foo=1&bar=2'
@problem.reset()
expect(Logger.log).toHaveBeenCalledWith 'problem_reset', 'foo=1&bar=2'
it 'POST to the problem reset page', ->
spyOn $, 'postWithPrefix'
@problem.reset()
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_reset', { id: 1 }, jasmine.any(Function)
it 'render the returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback html: "Reset!"
@problem.reset()
expect(@problem.el.html()).toEqual 'Reset!'
describe 'show', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem.el.prepend '<div id="answer_1_1" /><div id="answer_1_2" />'
describe 'when the answer has not yet shown', ->
beforeEach ->
@problem.el.removeClass 'showed'
it 'log the problem_show event', ->
@problem.show()
expect(Logger.log).toHaveBeenCalledWith 'problem_show', problem: 1
it 'fetch the answers', ->
spyOn $, 'postWithPrefix'
@problem.show()
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_show', jasmine.any(Function)
it 'show the answers', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
callback answers: '1_1': 'One', '1_2': 'Two'
@problem.show()
expect($('#answer_1_1')).toHaveHtml 'One'
expect($('#answer_1_2')).toHaveHtml 'Two'
it 'toggle the show answer button', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {})
@problem.show()
expect($('.show')).toHaveValue 'Hide Answer'
it 'add the showed class to element', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {})
@problem.show()
expect(@problem.el).toHaveClass 'showed'
describe 'multiple choice question', ->
beforeEach ->
@problem.el.prepend '''
<label for="input_1_1_1"><input type="checkbox" name="input_1_1" id="input_1_1_1" value="1"> One</label>
<label for="input_1_1_2"><input type="checkbox" name="input_1_1" id="input_1_1_2" value="2"> Two</label>
<label for="input_1_1_3"><input type="checkbox" name="input_1_1" id="input_1_1_3" value="3"> Three</label>
<label for="input_1_2_1"><input type="radio" name="input_1_2" id="input_1_2_1" value="1"> Other</label>
'''
it 'set the correct_answer attribute on the choice', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
callback answers: '1_1': [2, 3]
@problem.show()
expect($('label[for="input_1_1_1"]')).not.toHaveAttr 'correct_answer', 'true'
expect($('label[for="input_1_1_2"]')).toHaveAttr 'correct_answer', 'true'
expect($('label[for="input_1_1_3"]')).toHaveAttr 'correct_answer', 'true'
expect($('label[for="input_1_2_1"]')).not.toHaveAttr 'correct_answer', 'true'
describe 'when the answers are alreay shown', ->
beforeEach ->
@problem.el.addClass 'showed'
@problem.el.prepend '''
<label for="input_1_1_1" correct_answer="true">
<input type="checkbox" name="input_1_1" id="input_1_1_1" value="1" />
One
</label>
'''
$('#answer_1_1').html('One')
$('#answer_1_2').html('Two')
it 'hide the answers', ->
@problem.show()
expect($('#answer_1_1')).toHaveHtml ''
expect($('#answer_1_2')).toHaveHtml ''
expect($('label[for="input_1_1_1"]')).not.toHaveAttr 'correct_answer'
it 'toggle the show answer button', ->
@problem.show()
expect($('.show')).toHaveValue 'Show Answer'
it 'remove the showed class from element', ->
@problem.show()
expect(@problem.el).not.toHaveClass 'showed'
describe 'save', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem.answers = 'foo=1&bar=2'
it 'log the problem_save event', ->
@problem.save()
expect(Logger.log).toHaveBeenCalledWith 'problem_save', 'foo=1&bar=2'
it 'POST to save problem', ->
spyOn $, 'postWithPrefix'
@problem.save()
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_save', 'foo=1&bar=2', jasmine.any(Function)
it 'alert to the user', ->
spyOn window, 'alert'
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'OK')
@problem.save()
expect(window.alert).toHaveBeenCalledWith 'Saved'
describe 'refreshMath', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
$('#input_example_1').val 'E=mc^2'
@problem.refreshMath target: $('#input_example_1').get(0)
it 'should queue the conversion and MathML element update', ->
expect(MathJax.Hub.Queue).toHaveBeenCalledWith ['Text', @stubbedJax, 'E=mc^2'],
[@problem.updateMathML, @stubbedJax, $('#input_example_1').get(0)]
describe 'updateMathML', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@stubbedJax.root.toMathML.andReturn '<MathML>'
describe 'when there is no exception', ->
beforeEach ->
@problem.updateMathML @stubbedJax, $('#input_example_1').get(0)
it 'convert jax to MathML', ->
expect($('#input_example_1_dynamath')).toHaveValue '<MathML>'
describe 'when there is an exception', ->
beforeEach ->
@stubbedJax.root.toMathML.andThrow {restart: true}
@problem.updateMathML @stubbedJax, $('#input_example_1').get(0)
it 'should queue up the exception', ->
expect(MathJax.Callback.After).toHaveBeenCalledWith [@problem.refreshMath, @stubbedJax], true
describe 'refreshAnswers', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem.el.html '''
<textarea class="CodeMirror" />
<input id="input_1_1" name="input_1_1" class="schematic" value="one" />
<input id="input_1_2" name="input_1_2" value="two" />
<input id="input_bogus_3" name="input_bogus_3" value="three" />
'''
@stubSchematic = { update_value: jasmine.createSpy('schematic') }
@stubCodeMirror = { save: jasmine.createSpy('CodeMirror') }
$('input.schematic').get(0).schematic = @stubSchematic
$('textarea.CodeMirror').get(0).CodeMirror = @stubCodeMirror
it 'update each schematic', ->
@problem.refreshAnswers()
expect(@stubSchematic.update_value).toHaveBeenCalled()
it 'update each code block', ->
@problem.refreshAnswers()
expect(@stubCodeMirror.save).toHaveBeenCalled()
it 'serialize all answers', ->
@problem.refreshAnswers()
expect(@problem.answers).toEqual "input_1_1=one&input_1_2=two"

View File

@@ -0,0 +1,159 @@
describe 'Sequence', ->
beforeEach ->
# Stub MathJax
window.MathJax = { Hub: { Queue: -> } }
spyOn Logger, 'log'
loadFixtures 'sequence.html'
@items = $.parseJSON readFixtures('items.json')
describe 'constructor', ->
beforeEach ->
@sequence = new Sequence '1', 'sequence_1', @items, 'sequence', 1
it 'set the element', ->
expect(@sequence.el).toEqual $('#sequence_1')
it 'build the navigation', ->
classes = $('#sequence-list li>a').map(-> $(this).attr('class')).get()
elements = $('#sequence-list li>a').map(-> $(this).attr('data-element')).get()
titles = $('#sequence-list li>a>p').map(-> $(this).html()).get()
expect(classes).toEqual ['seq_video_active', 'seq_video_inactive', 'seq_problem_inactive']
expect(elements).toEqual ['1', '2', '3']
expect(titles).toEqual ['Video 1', 'Video 2', 'Sample Problem']
it 'bind the page events', ->
expect($('#sequence-list a')).toHandleWith 'click', @sequence.goto
it 'render the active sequence content', ->
expect($('#seq_content').html()).toEqual 'Video 1'
describe 'toggleArrows', ->
beforeEach ->
@sequence = new Sequence '1', 'sequence_1', @items, 'sequence', 1
describe 'when the first tab is active', ->
beforeEach ->
@sequence.position = 1
@sequence.toggleArrows()
it 'disable the previous button', ->
expect($('.sequence-nav-buttons .prev a')).toHaveClass 'disabled'
it 'enable the next button', ->
expect($('.sequence-nav-buttons .next a')).not.toHaveClass 'disabled'
expect($('.sequence-nav-buttons .next a')).toHandleWith 'click', @sequence.next
describe 'when the middle tab is active', ->
beforeEach ->
@sequence.position = 2
@sequence.toggleArrows()
it 'enable the previous button', ->
expect($('.sequence-nav-buttons .prev a')).not.toHaveClass 'disabled'
expect($('.sequence-nav-buttons .prev a')).toHandleWith 'click', @sequence.previous
it 'enable the next button', ->
expect($('.sequence-nav-buttons .next a')).not.toHaveClass 'disabled'
expect($('.sequence-nav-buttons .next a')).toHandleWith 'click', @sequence.next
describe 'when the last tab is active', ->
beforeEach ->
@sequence.position = 3
@sequence.toggleArrows()
it 'enable the previous button', ->
expect($('.sequence-nav-buttons .prev a')).not.toHaveClass 'disabled'
expect($('.sequence-nav-buttons .prev a')).toHandleWith 'click', @sequence.previous
it 'disable the next button', ->
expect($('.sequence-nav-buttons .next a')).toHaveClass 'disabled'
describe 'render', ->
beforeEach ->
spyOn $, 'postWithPrefix'
@sequence = new Sequence '1', 'sequence_1', @items, 'sequence'
spyOnEvent @sequence.el, 'contentChanged'
spyOn(@sequence, 'toggleArrows').andCallThrough()
describe 'with a different position than the current one', ->
beforeEach ->
@sequence.render 1
describe 'with no previous position', ->
it 'does not save the new position', ->
expect($.postWithPrefix).not.toHaveBeenCalled()
describe 'with previous position', ->
beforeEach ->
@sequence.position = 2
@sequence.render 1
it 'mark the previous tab as visited', ->
expect($('[data-element="2"]')).toHaveClass 'seq_video_visited'
it 'save the new position', ->
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/goto_position', position: 1
it 'mark new tab as active', ->
expect($('[data-element="1"]')).toHaveClass 'seq_video_active'
it 'render the new content', ->
expect($('#seq_content').html()).toEqual 'Video 1'
it 'update the position', ->
expect(@sequence.position).toEqual 1
it 're-update the arrows', ->
expect(@sequence.toggleArrows).toHaveBeenCalled()
it 'trigger contentChanged event', ->
expect('contentChanged').toHaveBeenTriggeredOn @sequence.el
describe 'with the same position as the current one', ->
it 'should not trigger contentChanged event', ->
@sequence.position = 2
@sequence.render 2
expect('contentChanged').not.toHaveBeenTriggeredOn @sequence.el
describe 'goto', ->
beforeEach ->
jasmine.stubRequests()
@sequence = new Sequence '1', 'sequence_1', @items, 'sequence', 2
$('[data-element="3"]').click()
it 'log the sequence goto event', ->
expect(Logger.log).toHaveBeenCalledWith 'seq_goto', old: 2, new: 3, id: '1'
it 'call render on the right sequence', ->
expect($('#seq_content').html()).toEqual 'Sample Problem'
describe 'next', ->
beforeEach ->
jasmine.stubRequests()
@sequence = new Sequence '1', 'sequence_1', @items, 'sequence', 2
$('.sequence-nav-buttons .next a').click()
it 'log the next sequence event', ->
expect(Logger.log).toHaveBeenCalledWith 'seq_next', old: 2, new: 3, id: '1'
it 'call render on the next sequence', ->
expect($('#seq_content').html()).toEqual 'Sample Problem'
describe 'previous', ->
beforeEach ->
jasmine.stubRequests()
@sequence = new Sequence '1', 'sequence_1', @items, 'sequence', 2
$('.sequence-nav-buttons .prev a').click()
it 'log the previous sequence event', ->
expect(Logger.log).toHaveBeenCalledWith 'seq_prev', old: 2, new: 1, id: '1'
it 'call render on the previous sequence', ->
expect($('#seq_content').html()).toEqual 'Video 1'
describe 'link_for', ->
it 'return a link for specific position', ->
sequence = new Sequence '1', 'sequence_1', @items, 2
expect(sequence.link_for(2)).toBe '[data-element="2"]'

View File

@@ -0,0 +1,337 @@
describe 'VideoCaption', ->
beforeEach ->
jasmine.stubVideoPlayer @
$('.subtitles').remove()
afterEach ->
YT.Player = undefined
$.fn.scrollTo.reset()
describe 'constructor', ->
beforeEach ->
spyOn($, 'getWithPrefix').andCallThrough()
describe 'always', ->
beforeEach ->
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
it 'set the youtube id', ->
expect(@caption.youtubeId).toEqual 'def456'
it 'create the caption element', ->
expect($('.video')).toContain 'ol.subtitles'
it 'add caption control to video player', ->
expect($('.video')).toContain 'a.hide-subtitles'
it 'fetch the caption', ->
expect($.getWithPrefix).toHaveBeenCalledWith @caption.captionURL(), jasmine.any(Function)
it 'bind window resize event', ->
expect($(window)).toHandleWith 'resize', @caption.resize
it 'bind the hide caption button', ->
expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle
it 'bind the mouse movement', ->
expect($('.subtitles')).toHandleWith 'mouseover', @caption.onMouseEnter
expect($('.subtitles')).toHandleWith 'mouseout', @caption.onMouseLeave
expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement
expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement
expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement
describe 'when on a non touch-based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
it 'render the caption', ->
expect($('.subtitles').html()).toMatch new RegExp('''
<li data-index="0" data-start="0">Caption at 0</li>
<li data-index="1" data-start="10000">Caption at 10000</li>
<li data-index="2" data-start="20000">Caption at 20000</li>
<li data-index="3" data-start="30000">Caption at 30000</li>
'''.replace(/\n/g, ''))
it 'add a padding element to caption', ->
expect($('.subtitles li:first')).toBe '.spacing'
expect($('.subtitles li:last')).toBe '.spacing'
it 'bind all the caption link', ->
$('.subtitles li[data-index]').each (index, link) =>
expect($(link)).toHandleWith 'click', @caption.seekPlayer
it 'set rendered to true', ->
expect(@caption.rendered).toBeTruthy()
describe 'when on a touch-based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
it 'show explaination message', ->
expect($('.subtitles li')).toHaveHtml "Caption will be displayed when you start playing the video."
it 'does not set rendered to true', ->
expect(@caption.rendered).toBeFalsy()
describe 'mouse movement', ->
beforeEach ->
spyOn(window, 'setTimeout').andReturn 100
spyOn window, 'clearTimeout'
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
describe 'when cursor is outside of the caption box', ->
beforeEach ->
$(window).trigger jQuery.Event 'mousemove'
it 'does not set freezing timeout', ->
expect(@caption.frozen).toBeFalsy()
describe 'when cursor is in the caption box', ->
beforeEach ->
$('.subtitles').trigger jQuery.Event 'mouseenter'
it 'set the freezing timeout', ->
expect(@caption.frozen).toEqual 100
describe 'when the cursor is moving', ->
beforeEach ->
$('.subtitles').trigger jQuery.Event 'mousemove'
it 'reset the freezing timeout', ->
expect(window.clearTimeout).toHaveBeenCalledWith 100
describe 'when the mouse is scrolling', ->
beforeEach ->
$('.subtitles').trigger jQuery.Event 'mousewheel'
it 'reset the freezing timeout', ->
expect(window.clearTimeout).toHaveBeenCalledWith 100
describe 'when cursor is moving out of the caption box', ->
beforeEach ->
@caption.frozen = 100
$.fn.scrollTo.reset()
describe 'always', ->
beforeEach ->
$('.subtitles').trigger jQuery.Event 'mouseout'
it 'reset the freezing timeout', ->
expect(window.clearTimeout).toHaveBeenCalledWith 100
it 'unfreeze the caption', ->
expect(@caption.frozen).toBeNull()
describe 'when the player is playing', ->
beforeEach ->
@caption.playing = true
$('.subtitles li[data-index]:first').addClass 'current'
$('.subtitles').trigger jQuery.Event 'mouseout'
it 'scroll the caption', ->
expect($.fn.scrollTo).toHaveBeenCalled()
describe 'when the player is not playing', ->
beforeEach ->
@caption.playing = false
$('.subtitles').trigger jQuery.Event 'mouseout'
it 'does not scroll the caption', ->
expect($.fn.scrollTo).not.toHaveBeenCalled()
describe 'search', ->
beforeEach ->
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
it 'return a correct caption index', ->
expect(@caption.search(0)).toEqual 0
expect(@caption.search(9999)).toEqual 0
expect(@caption.search(10000)).toEqual 1
expect(@caption.search(15000)).toEqual 1
expect(@caption.search(30000)).toEqual 3
expect(@caption.search(30001)).toEqual 3
describe 'play', ->
describe 'when the caption was not rendered', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
@caption.play()
it 'render the caption', ->
expect($('.subtitles').html()).toMatch new RegExp(
'''<li data-index="0" data-start="0">Caption at 0</li>''' +
'''<li data-index="1" data-start="10000">Caption at 10000</li>''' +
'''<li data-index="2" data-start="20000">Caption at 20000</li>''' +
'''<li data-index="3" data-start="30000">Caption at 30000</li>'''
)
it 'add a padding element to caption', ->
expect($('.subtitles li:first')).toBe '.spacing'
expect($('.subtitles li:last')).toBe '.spacing'
it 'bind all the caption link', ->
$('.subtitles li[data-index]').each (index, link) =>
expect($(link)).toHandleWith 'click', @caption.seekPlayer
it 'set rendered to true', ->
expect(@caption.rendered).toBeTruthy()
it 'set playing to true', ->
expect(@caption.playing).toBeTruthy()
describe 'pause', ->
beforeEach ->
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
@caption.playing = true
@caption.pause()
it 'set playing to false', ->
expect(@caption.playing).toBeFalsy()
describe 'updatePlayTime', ->
beforeEach ->
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
describe 'when the video speed is 1.0x', ->
beforeEach ->
@caption.currentSpeed = '1.0'
@caption.updatePlayTime 25.000
it 'search the caption based on time', ->
expect(@caption.currentIndex).toEqual 2
describe 'when the video speed is not 1.0x', ->
beforeEach ->
@caption.currentSpeed = '0.75'
@caption.updatePlayTime 25.000
it 'search the caption based on 1.0x speed', ->
expect(@caption.currentIndex).toEqual 1
describe 'when the index is not the same', ->
beforeEach ->
@caption.currentIndex = 1
$('.subtitles li[data-index=1]').addClass 'current'
@caption.updatePlayTime 25.000
it 'deactivate the previous caption', ->
expect($('.subtitles li[data-index=1]')).not.toHaveClass 'current'
it 'activate new caption', ->
expect($('.subtitles li[data-index=2]')).toHaveClass 'current'
it 'save new index', ->
expect(@caption.currentIndex).toEqual 2
it 'scroll caption to new position', ->
expect($.fn.scrollTo).toHaveBeenCalled()
describe 'when the index is the same', ->
beforeEach ->
@caption.currentIndex = 1
$('.subtitles li[data-index=1]').addClass 'current'
@caption.updatePlayTime 15.000
it 'does not change current subtitle', ->
expect($('.subtitles li[data-index=1]')).toHaveClass 'current'
describe 'resize', ->
beforeEach ->
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
$('.subtitles li[data-index=1]').addClass 'current'
@caption.resize()
it 'set the height of caption container', ->
expect(parseInt($('.subtitles').css('maxHeight'))).toEqual $('.video-wrapper').height()
it 'set the height of caption spacing', ->
expect(parseInt($('.subtitles .spacing:first').css('height'))).toEqual(
$('.video-wrapper').height() / 2 - $('.subtitles li:not(.spacing):first').height() / 2)
expect(parseInt($('.subtitles .spacing:last').css('height'))).toEqual(
$('.video-wrapper').height() / 2 - $('.subtitles li:not(.spacing):last').height() / 2)
it 'scroll caption to new position', ->
expect($.fn.scrollTo).toHaveBeenCalled()
describe 'scrollCaption', ->
beforeEach ->
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
describe 'when frozen', ->
beforeEach ->
@caption.frozen = true
$('.subtitles li[data-index=1]').addClass 'current'
@caption.scrollCaption()
it 'does not scroll the caption', ->
expect($.fn.scrollTo).not.toHaveBeenCalled()
describe 'when not frozen', ->
beforeEach ->
@caption.frozen = false
describe 'when there is no current caption', ->
beforeEach ->
@caption.scrollCaption()
it 'does not scroll the caption', ->
expect($.fn.scrollTo).not.toHaveBeenCalled()
describe 'when there is a current caption', ->
beforeEach ->
$('.subtitles li[data-index=1]').addClass 'current'
@caption.scrollCaption()
it 'scroll to current caption', ->
expect($.fn.scrollTo).toHaveBeenCalledWith $('.subtitles .current:first', @caption.el),
offset: - ($('.video-wrapper').height() / 2 - $('.subtitles .current:first').height() / 2)
describe 'seekPlayer', ->
beforeEach ->
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
@time = null
$(@caption).bind 'seek', (event, time) => @time = time
describe 'when the video speed is 1.0x', ->
beforeEach ->
@caption.currentSpeed = '1.0'
$('.subtitles li[data-start="30000"]').click()
it 'trigger seek event with the correct time', ->
expect(@time).toEqual 30.000
describe 'when the video speed is not 1.0x', ->
beforeEach ->
@caption.currentSpeed = '0.75'
$('.subtitles li[data-start="30000"]').click()
it 'trigger seek event with the correct time', ->
expect(@time).toEqual 40.000
describe 'toggle', ->
beforeEach ->
@caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0'
$('.subtitles li[data-index=1]').addClass 'current'
describe 'when the caption is visible', ->
beforeEach ->
@caption.el.removeClass 'closed'
@caption.toggle jQuery.Event('click')
it 'hide the caption', ->
expect(@caption.el).toHaveClass 'closed'
describe 'when the caption is hidden', ->
beforeEach ->
@caption.el.addClass 'closed'
@caption.toggle jQuery.Event('click')
it 'show the caption', ->
expect(@caption.el).not.toHaveClass 'closed'
it 'scroll the caption', ->
expect($.fn.scrollTo).toHaveBeenCalled()

View File

@@ -0,0 +1,109 @@
describe 'VideoControl', ->
beforeEach ->
jasmine.stubVideoPlayer @
$('.video-controls').html ''
describe 'constructor', ->
it 'render the video controls', ->
new VideoControl(el: $('.video-controls'))
expect($('.video-controls').html()).toContain '''
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control play" href="#">Play</a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
'''
it 'bind the playback button', ->
control = new VideoControl(el: $('.video-controls'))
expect($('.video_control')).toHandleWith 'click', control.togglePlayback
describe 'when on a touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
it 'does not add the play class to video control', ->
new VideoControl(el: $('.video-controls'))
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).not.toHaveHtml 'Play'
describe 'when on a non-touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
it 'add the play class to video control', ->
new VideoControl(el: $('.video-controls'))
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'play', ->
beforeEach ->
@control = new VideoControl(el: $('.video-controls'))
@control.play()
it 'switch playback button to play state', ->
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).toHaveClass 'pause'
expect($('.video_control')).toHaveHtml 'Pause'
describe 'pause', ->
beforeEach ->
@control = new VideoControl(el: $('.video-controls'))
@control.pause()
it 'switch playback button to pause state', ->
expect($('.video_control')).not.toHaveClass 'pause'
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'togglePlayback', ->
beforeEach ->
@control = new VideoControl(el: $('.video-controls'))
describe 'when the control does not have play or pause class', ->
beforeEach ->
$('.video_control').removeClass('play').removeClass('pause')
describe 'when the video is playing', ->
beforeEach ->
$('.video_control').addClass('play')
spyOnEvent @control, 'pause'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the pause event', ->
expect('pause').not.toHaveBeenTriggeredOn @control
describe 'when the video is paused', ->
beforeEach ->
$('.video_control').addClass('pause')
spyOnEvent @control, 'play'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the play event', ->
expect('play').not.toHaveBeenTriggeredOn @control
describe 'when the video is playing', ->
beforeEach ->
spyOnEvent @control, 'pause'
$('.video_control').addClass 'pause'
@control.togglePlayback jQuery.Event('click')
it 'trigger the pause event', ->
expect('pause').toHaveBeenTriggeredOn @control
describe 'when the video is paused', ->
beforeEach ->
spyOnEvent @control, 'play'
$('.video_control').addClass 'play'
@control.togglePlayback jQuery.Event('click')
it 'trigger the play event', ->
expect('play').toHaveBeenTriggeredOn @control

View File

@@ -0,0 +1,449 @@
describe 'VideoPlayer', ->
beforeEach ->
jasmine.stubVideoPlayer @, [], false
afterEach ->
YT.Player = undefined
describe 'constructor', ->
beforeEach ->
spyOn window, 'VideoControl'
spyOn YT, 'Player'
$.fn.qtip.andCallFake ->
$(this).data('qtip', true)
$('.video').append $('<div class="add-fullscreen" /><div class="hide-subtitles" />')
describe 'always', ->
beforeEach ->
@player = new VideoPlayer video: @video
it 'instanticate current time to zero', ->
expect(@player.currentTime).toEqual 0
it 'set the element', ->
expect(@player.el).toBe '#video_example'
it 'create video control', ->
expect(window.VideoControl).toHaveBeenCalledWith el: $('.video-controls', @player.el)
it 'create video caption', ->
expect(window.VideoCaption).toHaveBeenCalledWith el: @player.el, youtubeId: 'normalSpeedYoutubeId', currentSpeed: '1.0'
it 'create video speed control', ->
expect(window.VideoSpeedControl).toHaveBeenCalledWith el: $('.secondary-controls', @player.el), speeds: ['0.75', '1.0'], currentSpeed: '1.0'
it 'create video progress slider', ->
expect(window.VideoProgressSlider).toHaveBeenCalledWith el: $('.slider', @player.el)
it 'create Youtube player', ->
expect(YT.Player).toHaveBeenCalledWith 'example'
playerVars:
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
videoId: 'normalSpeedYoutubeId'
events:
onReady: @player.onReady
onStateChange: @player.onStateChange
it 'bind to video control play event', ->
expect($(@player.control)).toHandleWith 'play', @player.play
it 'bind to video control pause event', ->
expect($(@player.control)).toHandleWith 'pause', @player.pause
it 'bind to video caption seek event', ->
expect($(@player.caption)).toHandleWith 'seek', @player.onSeek
it 'bind to video speed control speedChange event', ->
expect($(@player.speedControl)).toHandleWith 'speedChange', @player.onSpeedChange
it 'bind to video progress slider seek event', ->
expect($(@player.progressSlider)).toHandleWith 'seek', @player.onSeek
it 'bind to video volume control volumeChange event', ->
expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange
it 'bind to key press', ->
expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen
it 'bind to fullscreen switching button', ->
expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen
describe 'when not on a touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
$('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer video: @video
it 'add the tooltip to fullscreen and subtitle button', ->
expect($('.add-fullscreen')).toHaveData 'qtip'
expect($('.hide-subtitles')).toHaveData 'qtip'
it 'create video volume control', ->
expect(window.VideoVolumeControl).toHaveBeenCalledWith el: $('.secondary-controls', @player.el)
describe 'when on a touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
$('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer video: @video
it 'does not add the tooltip to fullscreen and subtitle button', ->
expect($('.add-fullscreen')).not.toHaveData 'qtip'
expect($('.hide-subtitles')).not.toHaveData 'qtip'
it 'does not create video volume control', ->
expect(window.VideoVolumeControl).not.toHaveBeenCalled()
describe 'onReady', ->
beforeEach ->
@video.embed()
@player = @video.player
spyOnEvent @player, 'ready'
spyOnEvent @player, 'updatePlayTime'
@player.onReady()
describe 'when not on a touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
spyOn @player, 'play'
@player.onReady()
it 'autoplay the first video', ->
expect(@player.play).toHaveBeenCalled()
describe 'when on a touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
spyOn @player, 'play'
@player.onReady()
it 'does not autoplay the first video', ->
expect(@player.play).not.toHaveBeenCalled()
describe 'onStateChange', ->
beforeEach ->
@player = new VideoPlayer video: @video
describe 'when the video is unstarted', ->
beforeEach ->
spyOn @player.control, 'pause'
@player.caption.pause = jasmine.createSpy('VideoCaption.pause')
@player.onStateChange data: YT.PlayerState.UNSTARTED
it 'pause the video control', ->
expect(@player.control.pause).toHaveBeenCalled()
it 'pause the video caption', ->
expect(@player.caption.pause).toHaveBeenCalled()
describe 'when the video is playing', ->
beforeEach ->
@anotherPlayer = jasmine.createSpyObj 'AnotherPlayer', ['pauseVideo']
window.player = @anotherPlayer
spyOn @video, 'log'
spyOn(window, 'setInterval').andReturn 100
spyOn @player.control, 'play'
@player.caption.play = jasmine.createSpy('VideoCaption.play')
@player.progressSlider.play = jasmine.createSpy('VideoProgressSlider.play')
@player.player.getVideoEmbedCode.andReturn 'embedCode'
@player.onStateChange data: YT.PlayerState.PLAYING
it 'log the play_video event', ->
expect(@video.log).toHaveBeenCalledWith 'play_video'
it 'pause other video player', ->
expect(@anotherPlayer.pauseVideo).toHaveBeenCalled()
it 'set current video player as active player', ->
expect(window.player).toEqual @player.player
it 'set update interval', ->
expect(window.setInterval).toHaveBeenCalledWith @player.update, 200
expect(@player.player.interval).toEqual 100
it 'play the video control', ->
expect(@player.control.play).toHaveBeenCalled()
it 'play the video caption', ->
expect(@player.caption.play).toHaveBeenCalled()
it 'play the video progress slider', ->
expect(@player.progressSlider.play).toHaveBeenCalled()
describe 'when the video is paused', ->
beforeEach ->
@player = new VideoPlayer video: @video
window.player = @player.player
spyOn @video, 'log'
spyOn window, 'clearInterval'
spyOn @player.control, 'pause'
@player.caption.pause = jasmine.createSpy('VideoCaption.pause')
@player.player.interval = 100
@player.player.getVideoEmbedCode.andReturn 'embedCode'
@player.onStateChange data: YT.PlayerState.PAUSED
it 'log the pause_video event', ->
expect(@video.log).toHaveBeenCalledWith 'pause_video'
it 'set current video player as inactive', ->
expect(window.player).toBeNull()
it 'clear update interval', ->
expect(window.clearInterval).toHaveBeenCalledWith 100
expect(@player.player.interval).toBeNull()
it 'pause the video control', ->
expect(@player.control.pause).toHaveBeenCalled()
it 'pause the video caption', ->
expect(@player.caption.pause).toHaveBeenCalled()
describe 'when the video is ended', ->
beforeEach ->
spyOn @player.control, 'pause'
@player.caption.pause = jasmine.createSpy('VideoCaption.pause')
@player.onStateChange data: YT.PlayerState.ENDED
it 'pause the video control', ->
expect(@player.control.pause).toHaveBeenCalled()
it 'pause the video caption', ->
expect(@player.caption.pause).toHaveBeenCalled()
describe 'onSeek', ->
beforeEach ->
@player = new VideoPlayer video: @video
spyOn window, 'clearInterval'
@player.player.interval = 100
spyOn @player, 'updatePlayTime'
@player.onSeek {}, 60
it 'seek the player', ->
expect(@player.player.seekTo).toHaveBeenCalledWith 60, true
it 'call updatePlayTime on player', ->
expect(@player.updatePlayTime).toHaveBeenCalledWith 60
describe 'when the player is playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
@player.onSeek {}, 60
it 'reset the update interval', ->
expect(window.clearInterval).toHaveBeenCalledWith 100
describe 'when the player is not playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
@player.onSeek {}, 60
it 'set the current time', ->
expect(@player.currentTime).toEqual 60
describe 'onSpeedChange', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.currentTime = 60
spyOn @player, 'updatePlayTime'
spyOn(@video, 'setSpeed').andCallThrough()
describe 'always', ->
beforeEach ->
@player.onSpeedChange {}, '0.75'
it 'convert the current time to the new speed', ->
expect(@player.currentTime).toEqual '80.000'
it 'set video speed to the new speed', ->
expect(@video.setSpeed).toHaveBeenCalledWith '0.75'
it 'tell video caption that the speed has changed', ->
expect(@player.caption.currentSpeed).toEqual '0.75'
describe 'when the video is playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
@player.onSpeedChange {}, '0.75'
it 'load the video', ->
expect(@player.player.loadVideoById).toHaveBeenCalledWith 'slowerSpeedYoutubeId', '80.000'
it 'trigger updatePlayTime event', ->
expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000'
describe 'when the video is not playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
@player.onSpeedChange {}, '0.75'
it 'cue the video', ->
expect(@player.player.cueVideoById).toHaveBeenCalledWith 'slowerSpeedYoutubeId', '80.000'
it 'trigger updatePlayTime event', ->
expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000'
describe 'onVolumeChange', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.onVolumeChange undefined, 60
it 'set the volume on player', ->
expect(@player.player.setVolume).toHaveBeenCalledWith 60
describe 'update', ->
beforeEach ->
@player = new VideoPlayer video: @video
spyOn @player, 'updatePlayTime'
describe 'when the current time is unavailable from the player', ->
beforeEach ->
@player.player.getCurrentTime.andReturn undefined
@player.update()
it 'does not trigger updatePlayTime event', ->
expect(@player.updatePlayTime).not.toHaveBeenCalled()
describe 'when the current time is available from the player', ->
beforeEach ->
@player.player.getCurrentTime.andReturn 60
@player.update()
it 'trigger updatePlayTime event', ->
expect(@player.updatePlayTime).toHaveBeenCalledWith(60)
describe 'updatePlayTime', ->
beforeEach ->
@player = new VideoPlayer video: @video
spyOn(@video, 'getDuration').andReturn 1800
@player.caption.updatePlayTime = jasmine.createSpy('VideoCaption.updatePlayTime')
@player.progressSlider.updatePlayTime = jasmine.createSpy('VideoProgressSlider.updatePlayTime')
@player.updatePlayTime 60
it 'update the video playback time', ->
expect($('.vidtime')).toHaveHtml '1:00 / 30:00'
it 'update the playback time on caption', ->
expect(@player.caption.updatePlayTime).toHaveBeenCalledWith 60
it 'update the playback time on progress slider', ->
expect(@player.progressSlider.updatePlayTime).toHaveBeenCalledWith 60, 1800
describe 'toggleFullScreen', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.caption.resize = jasmine.createSpy('VideoCaption.resize')
describe 'when the video player is not full screen', ->
beforeEach ->
@player.el.removeClass 'fullscreen'
@player.toggleFullScreen(jQuery.Event("click"))
it 'replace the full screen button tooltip', ->
expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser'
it 'add a new exit from fullscreen button', ->
expect(@player.el).toContain 'a.exit'
it 'add the fullscreen class', ->
expect(@player.el).toHaveClass 'fullscreen'
it 'tell VideoCaption to resize', ->
expect(@player.caption.resize).toHaveBeenCalled()
describe 'when the video player already full screen', ->
beforeEach ->
@player.el.addClass 'fullscreen'
@player.toggleFullScreen(jQuery.Event("click"))
it 'replace the full screen button tooltip', ->
expect($('.add-fullscreen')).toHaveAttr 'title', 'Fill browser'
it 'remove exit full screen button', ->
expect(@player.el).not.toContain 'a.exit'
it 'remove the fullscreen class', ->
expect(@player.el).not.toHaveClass 'fullscreen'
it 'tell VideoCaption to resize', ->
expect(@player.caption.resize).toHaveBeenCalled()
describe 'play', ->
beforeEach ->
@player = new VideoPlayer video: @video
describe 'when the player is not ready', ->
beforeEach ->
@player.player.playVideo = undefined
@player.play()
it 'does nothing', ->
expect(@player.player.playVideo).toBeUndefined()
describe 'when the player is ready', ->
beforeEach ->
@player.player.playVideo.andReturn true
@player.play()
it 'delegate to the Youtube player', ->
expect(@player.player.playVideo).toHaveBeenCalled()
describe 'isPlaying', ->
beforeEach ->
@player = new VideoPlayer video: @video
describe 'when the video is playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
it 'return true', ->
expect(@player.isPlaying()).toBeTruthy()
describe 'when the video is not playing', ->
beforeEach ->
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
it 'return false', ->
expect(@player.isPlaying()).toBeFalsy()
describe 'pause', ->
beforeEach ->
@player = new VideoPlayer video: @video
@player.pause()
it 'delegate to the Youtube player', ->
expect(@player.player.pauseVideo).toHaveBeenCalled()
describe 'duration', ->
beforeEach ->
@player = new VideoPlayer video: @video
spyOn @video, 'getDuration'
@player.duration()
it 'delegate to the video', ->
expect(@video.getDuration).toHaveBeenCalled()
describe 'currentSpeed', ->
beforeEach ->
@player = new VideoPlayer video: @video
@video.speed = '3.0'
it 'delegate to the video', ->
expect(@player.currentSpeed()).toEqual '3.0'
describe 'volume', ->
beforeEach ->
@player = new VideoPlayer @video
@player.player.getVolume.andReturn 42
describe 'without value', ->
it 'return current volume', ->
expect(@player.volume()).toEqual 42
describe 'with value', ->
it 'set player volume', ->
@player.volume(60)
expect(@player.player.setVolume).toHaveBeenCalledWith(60)

View File

@@ -0,0 +1,160 @@
describe 'VideoProgressSlider', ->
beforeEach ->
jasmine.stubVideoPlayer @
describe 'constructor', ->
describe 'on a non-touch based device', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
spyOn(window, 'onTouchBasedDevice').andReturn false
@slider = new VideoProgressSlider el: $('.slider')
it 'build the slider', ->
expect(@slider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @slider.onChange
slide: @slider.onSlide
stop: @slider.onStop
it 'build the seek handle', ->
expect(@slider.handle).toBe '.slider .ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @slider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'on a touch-based device', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
spyOn(window, 'onTouchBasedDevice').andReturn true
@slider = new VideoProgressSlider el: $('.slider')
it 'does not build the slider', ->
expect(@slider.slider).toBeUndefined
expect($.fn.slider).not.toHaveBeenCalled()
describe 'play', ->
beforeEach ->
@slider = new VideoProgressSlider el: $('.slider')
spyOn($.fn, 'slider').andCallThrough()
describe 'when the slider was already built', ->
beforeEach ->
@slider.play()
it 'does not build the slider', ->
expect($.fn.slider).not.toHaveBeenCalled
describe 'when the slider was not already built', ->
beforeEach ->
@slider.slider = null
@slider.play()
it 'build the slider', ->
expect(@slider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @slider.onChange
slide: @slider.onSlide
stop: @slider.onStop
it 'build the seek handle', ->
expect(@slider.handle).toBe '.ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @slider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'updatePlayTime', ->
beforeEach ->
@slider = new VideoProgressSlider el: $('.slider')
spyOn($.fn, 'slider').andCallThrough()
describe 'when frozen', ->
beforeEach ->
@slider.frozen = true
@slider.updatePlayTime 20, 120
it 'does not update the slider', ->
expect($.fn.slider).not.toHaveBeenCalled()
describe 'when not frozen', ->
beforeEach ->
@slider.frozen = false
@slider.updatePlayTime 20, 120
it 'update the max value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120
it 'update current value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'value', 20
describe 'onSlide', ->
beforeEach ->
@slider = new VideoProgressSlider el: $('.slider')
@time = null
$(@slider).bind 'seek', (event, time) => @time = time
spyOnEvent @slider, 'seek'
@slider.onSlide {}, value: 20
it 'freeze the slider', ->
expect(@slider.frozen).toBeTruthy()
it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled()
it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @slider
expect(@time).toEqual 20
describe 'onChange', ->
beforeEach ->
@slider = new VideoProgressSlider el: $('.slider')
@slider.onChange {}, value: 20
it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled()
describe 'onStop', ->
beforeEach ->
@slider = new VideoProgressSlider el: $('.slider')
@time = null
$(@slider).bind 'seek', (event, time) => @time = time
spyOnEvent @slider, 'seek'
spyOn(window, 'setTimeout')
@slider.onStop {}, value: 20
it 'freeze the slider', ->
expect(@slider.frozen).toBeTruthy()
it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @slider
expect(@time).toEqual 20
it 'set timeout to unfreeze the slider', ->
expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200
window.setTimeout.mostRecentCall.args[0]()
expect(@slider.frozen).toBeFalsy()
describe 'updateTooltip', ->
beforeEach ->
@slider = new VideoProgressSlider el: $('.slider')
@slider.updateTooltip 90
it 'set the tooltip value', ->
expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'

View File

@@ -0,0 +1,90 @@
describe 'VideoSpeedControl', ->
beforeEach ->
jasmine.stubVideoPlayer @
$('.speeds').remove()
describe 'constructor', ->
describe 'always', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'add the video speed control to player', ->
expect($('.secondary-controls').html()).toContain '''
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active">1.0x</p>
</a>
<ol class="video_speeds"><li data-speed="1.0" class="active"><a href="#">1.0x</a></li><li data-speed="0.75"><a href="#">0.75x</a></li></ol>
</div>
'''
it 'bind to change video speed link', ->
expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed
describe 'when running on touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
$('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'open the speed toggle on click', ->
$('.speeds').click()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'when running on non-touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
$('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'open the speed toggle on hover', ->
$('.speeds').mouseenter()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on mouse out', ->
$('.speeds').mouseenter().mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on click', ->
$('.speeds').mouseenter().click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'changeVideoSpeed', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
@video.setSpeed '1.0'
describe 'when new speed is the same', ->
beforeEach ->
spyOnEvent @speedControl, 'speedChange'
$('li[data-speed="1.0"] a').click()
it 'does not trigger speedChange event', ->
expect('speedChange').not.toHaveBeenTriggeredOn @speedControl
describe 'when new speed is not the same', ->
beforeEach ->
@newSpeed = null
$(@speedControl).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed
spyOnEvent @speedControl, 'speedChange'
$('li[data-speed="0.75"] a').click()
it 'trigger speedChange event', ->
expect('speedChange').toHaveBeenTriggeredOn @speedControl
expect(@newSpeed).toEqual 0.75
describe 'onSpeedChange', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
$('li[data-speed="1.0"] a').addClass 'active'
@speedControl.setSpeed '0.75'
it 'set the new speed as active', ->
expect($('.video_speeds li[data-speed="1.0"]')).not.toHaveClass 'active'
expect($('.video_speeds li[data-speed="0.75"]')).toHaveClass 'active'
expect($('.speeds p.active')).toHaveHtml '0.75x'

View File

@@ -0,0 +1,94 @@
describe 'VideoVolumeControl', ->
beforeEach ->
jasmine.stubVideoPlayer @
$('.volume').remove()
describe 'constructor', ->
beforeEach ->
spyOn($.fn, 'slider')
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
it 'initialize currentVolume to 100', ->
expect(@volumeControl.currentVolume).toEqual 100
it 'render the volume control', ->
expect($('.secondary-controls').html()).toContain """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""
it 'create the slider', ->
expect($.fn.slider).toHaveBeenCalledWith
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @volumeControl.onChange
slide: @volumeControl.onChange
it 'bind the volume control', ->
expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute
expect($('.volume')).not.toHaveClass 'open'
$('.volume').mouseenter()
expect($('.volume')).toHaveClass 'open'
$('.volume').mouseleave()
expect($('.volume')).not.toHaveClass 'open'
describe 'onChange', ->
beforeEach ->
spyOnEvent @volumeControl, 'volumeChange'
@newVolume = undefined
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
$(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
describe 'when the new volume is more than 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 60
it 'set the player volume', ->
expect(@newVolume).toEqual 60
it 'remote muted class', ->
expect($('.volume')).not.toHaveClass 'muted'
describe 'when the new volume is 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 0
it 'set the player volume', ->
expect(@newVolume).toEqual 0
it 'add muted class', ->
expect($('.volume')).toHaveClass 'muted'
describe 'toggleMute', ->
beforeEach ->
@newVolume = undefined
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
$(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
describe 'when the current volume is more than 0', ->
beforeEach ->
@volumeControl.currentVolume = 60
@volumeControl.toggleMute()
it 'save the previous volume', ->
expect(@volumeControl.previousVolume).toEqual 60
it 'set the player volume', ->
expect(@newVolume).toEqual 0
describe 'when the current volume is 0', ->
beforeEach ->
@volumeControl.currentVolume = 0
@volumeControl.previousVolume = 60
@volumeControl.toggleMute()
it 'set the player volume to previous volume', ->
expect(@newVolume).toEqual 60

View File

@@ -0,0 +1,149 @@
describe 'Video', ->
beforeEach ->
loadFixtures 'video.html'
jasmine.stubRequests()
@videosDefinition = '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
@slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
@normalSpeedYoutubeId = 'normalSpeedYoutubeId'
afterEach ->
window.player = undefined
window.onYouTubePlayerAPIReady = undefined
describe 'constructor', ->
beforeEach ->
@stubVideoPlayer = jasmine.createSpy('VideoPlayer')
$.cookie.andReturn '0.75'
window.player = 100
describe 'by default', ->
beforeEach ->
@video = new Video 'example', @videosDefinition
it 'reset the current video player', ->
expect(window.player).toBeNull()
it 'set the elements', ->
expect(@video.el).toBe '#video_example'
it 'parse the videos', ->
expect(@video.videos).toEqual
'0.75': @slowerSpeedYoutubeId
'1.0': @normalSpeedYoutubeId
it 'fetch the video metadata', ->
expect(@video.metadata).toEqual
slowerSpeedYoutubeId:
id: @slowerSpeedYoutubeId
duration: 300
normalSpeedYoutubeId:
id: @normalSpeedYoutubeId
duration: 200
it 'parse available video speeds', ->
expect(@video.speeds).toEqual ['0.75', '1.0']
it 'set current video speed via cookie', ->
expect(@video.speed).toEqual '0.75'
it 'store a reference for this video player in the element', ->
expect($('.video').data('video')).toEqual @video
describe 'when the Youtube API is already available', ->
beforeEach ->
@originalYT = window.YT
window.YT = { Player: true }
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video 'example', @videosDefinition
afterEach ->
window.YT = @originalYT
it 'create the Video Player', ->
expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayer
describe 'when the Youtube API is not ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
@video = new Video 'example', @videosDefinition
afterEach ->
window.YT = @originalYT
it 'set the callback on the window object', ->
expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function)
describe 'when the Youtube API becoming ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video 'example', @videosDefinition
window.onYouTubePlayerAPIReady()
afterEach ->
window.YT = @originalYT
it 'create the Video Player for all video elements', ->
expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayer
describe 'youtubeId', ->
beforeEach ->
$.cookie.andReturn '1.0'
@video = new Video 'example', @videosDefinition
describe 'with speed', ->
it 'return the video id for given speed', ->
expect(@video.youtubeId('0.75')).toEqual @slowerSpeedYoutubeId
expect(@video.youtubeId('1.0')).toEqual @normalSpeedYoutubeId
describe 'without speed', ->
it 'return the video id for current speed', ->
expect(@video.youtubeId()).toEqual @normalSpeedYoutubeId
describe 'setSpeed', ->
beforeEach ->
@video = new Video 'example', @videosDefinition
describe 'when new speed is available', ->
beforeEach ->
@video.setSpeed '0.75'
it 'set new speed', ->
expect(@video.speed).toEqual '0.75'
it 'save setting for new speed', ->
expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
describe 'when new speed is not available', ->
beforeEach ->
@video.setSpeed '1.75'
it 'set speed to 1.0x', ->
expect(@video.speed).toEqual '1.0'
describe 'getDuration', ->
beforeEach ->
@video = new Video 'example', @videosDefinition
it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200
describe 'log', ->
beforeEach ->
@video = new Video 'example', @videosDefinition
@video.setSpeed '1.0'
spyOn Logger, 'log'
@video.player = { currentTime: 25 }
@video.log 'someEvent'
it 'call the logger with valid parameters', ->
expect(Logger.log).toHaveBeenCalledWith 'someEvent',
id: 'example'
code: @normalSpeedYoutubeId
currentTime: 25
speed: '1.0'

View File

@@ -0,0 +1,97 @@
class @Problem
constructor: (@id, @element_id, url) ->
@el = $("##{@element_id}")
@render()
$: (selector) ->
$(selector, @el)
bind: =>
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
window.update_schematics()
@$('section.action input:button').click @refreshAnswers
@$('section.action input.check').click @check
@$('section.action input.reset').click @reset
@$('section.action input.show').click @show
@$('section.action input.save').click @save
@$('input.math').keyup(@refreshMath).each(@refreshMath)
updateProgress: (response) =>
if response.progress_changed
@el.attr progress: response.progress_status
@el.trigger('progressChanged')
render: (content) ->
if content
@el.html(content)
@bind()
else
$.postWithPrefix "/modx/#{@id}/problem_get", (response) =>
@el.html(response.html)
@bind()
check: =>
Logger.log 'problem_check', @answers
$.postWithPrefix "/modx/#{@id}/problem_check", @answers, (response) =>
switch response.success
when 'incorrect', 'correct'
@render(response.contents)
@updateProgress response
else
alert(response.success)
reset: =>
Logger.log 'problem_reset', @answers
$.postWithPrefix "/modx/#{@id}/problem_reset", id: @id, (response) =>
@render(response.html)
@updateProgress response
show: =>
if !@el.hasClass 'showed'
Logger.log 'problem_show', problem: @id
$.postWithPrefix "/modx/#{@id}/problem_show", (response) =>
answers = response.answers
$.each answers, (key, value) =>
if $.isArray(value)
for choice in value
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true'
else
@$("#answer_#{key}, #solution_#{key}").html(value)
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
@$('.show').val 'Hide Answer'
@el.addClass 'showed'
@updateProgress response
else
@$('[id^=answer_], [id^=solution_]').text ''
@$('[correct_answer]').attr correct_answer: null
@el.removeClass 'showed'
@$('.show').val 'Show Answer'
save: =>
Logger.log 'problem_save', @answers
$.postWithPrefix "/modx/#{@id}/problem_save", @answers, (response) =>
if response.success
alert 'Saved'
@updateProgress response
refreshMath: (event, element) =>
element = event.target unless element
target = "display_#{element.id.replace(/^input_/, '')}"
if jax = MathJax.Hub.getAllJax(target)[0]
MathJax.Hub.Queue ['Text', jax, $(element).val()],
[@updateMathML, jax, element]
updateMathML: (jax, element) =>
try
$("##{element.id}_dynamath").val(jax.root.toMathML '')
catch exception
throw exception unless exception.restart
MathJax.Callback.After [@refreshMath, jax], exception.restart
refreshAnswers: =>
@$('input.schematic').each (index, element) ->
element.schematic.update_value()
@$(".CodeMirror").each (index, element) ->
element.CodeMirror.save() if element.CodeMirror.save
@answers = @$("[id^=input_#{@element_id.replace(/problem_/, '')}_]").serialize()

View File

@@ -1,4 +1,4 @@
class @HTML
class @RawDescriptor
constructor: (@element) ->
@edit_box = $(".edit-box", @element)

View File

@@ -0,0 +1,137 @@
class @Sequence
constructor: (@id, @element_id, @elements, @tag, position) ->
@el = $("##{@element_id}")
@buildNavigation()
@initProgress()
@bind()
@render position
$: (selector) ->
$(selector, @el)
bind: ->
@$('#sequence-list a').click @goto
initProgress: ->
@progressTable = {} # "#problem_#{id}" -> progress
hookUpProgressEvent: ->
$('.problems-wrapper').bind 'progressChanged', @updateProgress
mergeProgress: (p1, p2) ->
# if either is "NA", return the other one
if p1 == "NA"
return p2
if p2 == "NA"
return p1
# Both real progresses
if p1 == "done" and p2 == "done"
return "done"
# not done, so if any progress on either, in_progress
w1 = p1 == "done" or p1 == "in_progress"
w2 = p2 == "done" or p2 == "in_progress"
if w1 or w2
return "in_progress"
return "none"
updateProgress: =>
new_progress = "NA"
_this = this
$('.problems-wrapper').each (index) ->
progress = $(this).attr 'progress'
new_progress = _this.mergeProgress progress, new_progress
@progressTable[@position] = new_progress
@setProgress(new_progress, @link_for(@position))
setProgress: (progress, element) ->
# If progress is "NA", don't add any css class
element.removeClass('progress-none')
.removeClass('progress-some')
.removeClass('progress-done')
switch progress
when 'none' then element.addClass('progress-none')
when 'in_progress' then element.addClass('progress-some')
when 'done' then element.addClass('progress-done')
buildNavigation: ->
$.each @elements, (index, item) =>
link = $('<a>').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1
title = $('<p>').html(item.title)
# TODO (vshnayder): add item.progress_detail either to the title or somewhere else.
# Make sure it gets updated after ajax calls.
# implementation note: will need to figure out how to handle combining detail
# statuses of multiple modules in js.
list_item = $('<li>').append(link.append(title))
@setProgress item.progress_status, link
@$('#sequence-list').append list_item
toggleArrows: =>
@$('.sequence-nav-buttons a').unbind('click')
if @position == 1
@$('.sequence-nav-buttons .prev a').addClass('disabled')
else
@$('.sequence-nav-buttons .prev a').removeClass('disabled').click(@previous)
if @position == @elements.length
@$('.sequence-nav-buttons .next a').addClass('disabled')
else
@$('.sequence-nav-buttons .next a').removeClass('disabled').click(@next)
render: (new_position) ->
if @position != new_position
if @position != undefined
@mark_visited @position
$.postWithPrefix "/modx/#{@id}/goto_position", position: new_position
@mark_active new_position
@$('#seq_content').html @elements[new_position - 1].content
MathJax.Hub.Queue(["Typeset", MathJax.Hub])
@position = new_position
@toggleArrows()
@hookUpProgressEvent()
@el.trigger 'contentChanged'
goto: (event) =>
event.preventDefault()
new_position = $(event.target).data('element')
Logger.log "seq_goto", old: @position, new: new_position, id: @id
@render new_position
next: (event) =>
event.preventDefault()
new_position = @position + 1
Logger.log "seq_next", old: @position, new: new_position, id: @id
@render new_position
previous: (event) =>
event.preventDefault()
new_position = @position - 1
Logger.log "seq_prev", old: @position, new: new_position, id: @id
@render new_position
link_for: (position) ->
@$("#sequence-list a[data-element=#{position}]")
mark_visited: (position) ->
# Don't overwrite class attribute to avoid changing Progress class
type = @elements[position - 1].type
element = @link_for(position)
element.removeClass("seq_#{type}_inactive")
.removeClass("seq_#{type}_active")
.addClass("seq_#{type}_visited")
mark_active: (position) ->
# Don't overwrite class attribute to avoid changing Progress class
type = @elements[position - 1].type
element = @link_for(position)
element.removeClass("seq_#{type}_inactive")
.removeClass("seq_#{type}_visited")
.addClass("seq_#{type}_active")

View File

@@ -0,0 +1,54 @@
class @Video
constructor: (@id, videos, @caption_url_base) ->
window.player = null
@el = $("#video_#{@id}")
@parseVideos videos
@fetchMetadata()
@parseSpeed()
$("#video_#{@id}").data('video', this)
if YT.Player
@embed()
else
window.onYouTubePlayerAPIReady = =>
$('.course-content .video').each ->
$(this).data('video').embed()
youtubeId: (speed)->
@videos[speed || @speed]
parseVideos: (videos) ->
@videos = {}
$.each videos.split(/,/), (index, video) =>
video = video.split(/:/)
speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0'
@videos[speed] = video[1]
parseSpeed: ->
@setSpeed($.cookie('video_speed'))
@speeds = ($.map @videos, (url, speed) -> speed).sort()
setSpeed: (newSpeed) ->
if @videos[newSpeed] != undefined
@speed = newSpeed
$.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/')
else
@speed = '1.0'
embed: ->
@player = new VideoPlayer video: this
fetchMetadata: (url) ->
@metadata = {}
$.each @videos, (speed, url) =>
$.get "http://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
getDuration: ->
@metadata[@youtubeId()].duration
log: (eventName) ->
Logger.log eventName,
id: @id
code: @youtubeId()
currentTime: @player.currentTime
speed: @speed

View File

@@ -0,0 +1,133 @@
class @VideoCaption extends Subview
bind: ->
$(window).bind('resize', @resize)
@$('.hide-subtitles').click @toggle
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
.mousemove(@onMovement).bind('mousewheel', @onMovement)
.bind('DOMMouseScroll', @onMovement)
captionURL: ->
"#{@captionURLBase}/#{@youtubeId}.srt.sjson"
render: ->
@$('.video-wrapper').after """
<ol class="subtitles"><li>Attempting to load captions...</li></ol>
"""
@$('.video-controls .secondary-controls').append """
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
"""
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
@fetchCaption()
fetchCaption: ->
$.getWithPrefix @captionURL(), (captions) =>
@captions = captions.text
@start = captions.start
if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video."
else
@renderCaption()
renderCaption: ->
container = $('<ol>')
$.each @captions, (index, text) =>
container.append $('<li>').html(text).attr
'data-index': index
'data-start': @start[index]
@$('.subtitles').html(container.html())
@$('.subtitles li[data-index]').click @seekPlayer
# prepend and append an empty <li> for cosmatic reason
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
@rendered = true
search: (time) ->
min = 0
max = @start.length - 1
while min < max
index = Math.ceil((max + min) / 2)
if time < @start[index]
max = index - 1
if time >= @start[index]
min = index
return min
play: ->
@renderCaption() unless @rendered
@playing = true
pause: ->
@playing = false
updatePlayTime: (time) ->
# This 250ms offset is required to match the video speed
time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250)
newIndex = @search time
if newIndex != undefined && @currentIndex != newIndex
if @currentIndex
@$(".subtitles li.current").removeClass('current')
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
@currentIndex = newIndex
@scrollCaption()
resize: =>
@$('.subtitles').css maxHeight: @captionHeight()
@$('.subtitles .spacing:first').height(@topSpacingHeight())
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
@scrollCaption()
onMouseEnter: =>
clearTimeout @frozen if @frozen
@frozen = setTimeout @onMouseLeave, 10000
onMovement: =>
@onMouseEnter()
onMouseLeave: =>
clearTimeout @frozen if @frozen
@frozen = null
@scrollCaption() if @playing
scrollCaption: ->
if !@frozen && @$('.subtitles .current:first').length
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
offset: - @calculateOffset(@$('.subtitles .current:first'))
seekPlayer: (event) =>
event.preventDefault()
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000)
$(@).trigger('seek', time)
calculateOffset: (element) ->
@captionHeight() / 2 - element.height() / 2
topSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):first'))
bottomSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):last'))
toggle: (event) =>
event.preventDefault()
if @el.hasClass('closed')
@$('.hide-subtitles').attr('title', 'Turn off captions')
@el.removeClass('closed')
@scrollCaption()
else
@$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed')
captionHeight: ->
if @el.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height()
else
@$('.video-wrapper').height()

View File

@@ -0,0 +1,35 @@
class @VideoControl extends Subview
bind: ->
@$('.video_control').click @togglePlayback
render: ->
@el.append """
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#"></a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
"""
unless onTouchBasedDevice()
@$('.video_control').addClass('play').html('Play')
play: ->
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
pause: ->
@$('.video_control').removeClass('pause').addClass('play').html('Play')
togglePlayback: (event) =>
event.preventDefault()
if @$('.video_control').hasClass('play')
$(@).trigger('play')
else if @$('.video_control').hasClass('pause')
$(@).trigger('pause')

View File

@@ -0,0 +1,158 @@
class @VideoPlayer extends Subview
initialize: ->
# Define a missing constant of Youtube API
YT.PlayerState.UNSTARTED = -1
@currentTime = 0
@el = $("#video_#{@video.id}")
bind: ->
$(@control).bind('play', @play)
.bind('pause', @pause)
$(@caption).bind('seek', @onSeek)
$(@speedControl).bind('speedChange', @onSpeedChange)
$(@progressSlider).bind('seek', @onSeek)
if @volumeControl
$(@volumeControl).bind('volumeChange', @onVolumeChange)
$(document).keyup @bindExitFullScreen
@$('.add-fullscreen').click @toggleFullScreen
@addToolTip() unless onTouchBasedDevice()
bindExitFullScreen: (event) =>
if @el.hasClass('fullscreen') && event.keyCode == 27
@toggleFullScreen(event)
render: ->
@control = new VideoControl el: @$('.video-controls')
@caption = new VideoCaption el: @el, youtubeId: @video.youtubeId('1.0'), currentSpeed: @currentSpeed(), captionURLBase: @video.caption_url_base
unless onTouchBasedDevice()
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
@progressSlider = new VideoProgressSlider el: @$('.slider')
@player = new YT.Player @video.id,
playerVars:
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
videoId: @video.youtubeId()
events:
onReady: @onReady
onStateChange: @onStateChange
addToolTip: ->
@$('.add-fullscreen, .hide-subtitles').qtip
position:
my: 'top right'
at: 'top center'
onReady: =>
unless onTouchBasedDevice()
$('.course-content .video:first').data('video').player.play()
onStateChange: (event) =>
switch event.data
when YT.PlayerState.UNSTARTED
@onUnstarted()
when YT.PlayerState.PLAYING
@onPlay()
when YT.PlayerState.PAUSED
@onPause()
when YT.PlayerState.ENDED
@onEnded()
onUnstarted: =>
@control.pause()
@caption.pause()
onPlay: =>
@video.log 'play_video'
window.player.pauseVideo() if window.player && window.player != @player
window.player = @player
unless @player.interval
@player.interval = setInterval(@update, 200)
@caption.play()
@control.play()
@progressSlider.play()
onPause: =>
@video.log 'pause_video'
window.player = null if window.player == @player
clearInterval(@player.interval)
@player.interval = null
@caption.pause()
@control.pause()
onEnded: =>
@control.pause()
@caption.pause()
onSeek: (event, time) =>
@player.seekTo(time, true)
if @isPlaying()
clearInterval(@player.interval)
@player.interval = setInterval(@update, 200)
else
@currentTime = time
@updatePlayTime time
onSpeedChange: (event, newSpeed) =>
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0'
@video.setSpeed(newSpeed)
@caption.currentSpeed = newSpeed
if @isPlaying()
@player.loadVideoById(@video.youtubeId(), @currentTime)
else
@player.cueVideoById(@video.youtubeId(), @currentTime)
@updatePlayTime @currentTime
onVolumeChange: (event, volume) =>
@player.setVolume volume
update: =>
if @currentTime = @player.getCurrentTime()
@updatePlayTime @currentTime
updatePlayTime: (time) ->
progress = Time.format(time) + ' / ' + Time.format(@duration())
@$(".vidtime").html(progress)
@caption.updatePlayTime(time)
@progressSlider.updatePlayTime(time, @duration())
toggleFullScreen: (event) =>
event.preventDefault()
if @el.hasClass('fullscreen')
@$('.exit').remove()
@$('.add-fullscreen').attr('title', 'Fill browser')
@el.removeClass('fullscreen')
else
@el.append('<a href="#" class="exit">Exit</a>').addClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Exit fill browser')
@$('.exit').click @toggleFullScreen
@caption.resize()
# Delegates
play: =>
@player.playVideo() if @player.playVideo
isPlaying: ->
@player.getPlayerState() == YT.PlayerState.PLAYING
pause: =>
@player.pauseVideo() if @player.pauseVideo
duration: ->
@video.getDuration()
currentSpeed: ->
@video.speed
volume: (value) ->
if value?
@player.setVolume value
else
@player.getVolume()

View File

@@ -0,0 +1,49 @@
class @VideoProgressSlider extends Subview
initialize: ->
@buildSlider() unless onTouchBasedDevice()
buildSlider: ->
@slider = @el.slider
range: 'min'
change: @onChange
slide: @onSlide
stop: @onStop
@buildHandle()
buildHandle: ->
@handle = @$('.slider .ui-slider-handle')
@handle.qtip
content: "#{Time.format(@slider.slider('value'))}"
position:
my: 'bottom center'
at: 'top center'
container: @handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
play: =>
@buildSlider() unless @slider
updatePlayTime: (currentTime, duration) ->
if @slider && !@frozen
@slider.slider('option', 'max', duration)
@slider.slider('value', currentTime)
onSlide: (event, ui) =>
@frozen = true
@updateTooltip(ui.value)
$(@).trigger('seek', ui.value)
onChange: (event, ui) =>
@updateTooltip(ui.value)
onStop: (event, ui) =>
@frozen = true
$(@).trigger('seek', ui.value)
setTimeout (=> @frozen = false), 200
updateTooltip: (value)->
@handle.qtip('option', 'content.text', "#{Time.format(value)}")

View File

@@ -0,0 +1,43 @@
class @VideoSpeedControl extends Subview
bind: ->
@$('.video_speeds a').click @changeVideoSpeed
if onTouchBasedDevice()
@$('.speeds').click (event) ->
event.preventDefault()
$(this).toggleClass('open')
else
@$('.speeds').mouseenter ->
$(this).addClass('open')
@$('.speeds').mouseleave ->
$(this).removeClass('open')
@$('.speeds').click (event) ->
event.preventDefault()
$(this).removeClass('open')
render: ->
@el.prepend """
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
"""
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
@setSpeed(@currentSpeed)
changeVideoSpeed: (event) =>
event.preventDefault()
unless $(event.target).parent().hasClass('active')
@currentSpeed = $(event.target).parent().data('speed')
$(@).trigger 'speedChange', $(event.target).parent().data('speed')
@setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0')
setSpeed: (speed) ->
@$('.video_speeds li').removeClass('active')
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
@$('.speeds p.active').html("#{speed}x")

View File

@@ -0,0 +1,40 @@
class @VideoVolumeControl extends Subview
initialize: ->
@currentVolume = 100
bind: ->
@$('.volume').mouseenter ->
$(this).addClass('open')
@$('.volume').mouseleave ->
$(this).removeClass('open')
@$('.volume>a').click(@toggleMute)
render: ->
@el.prepend """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""
@slider = @$('.volume-slider').slider
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @onChange
slide: @onChange
onChange: (event, ui) =>
@currentVolume = ui.value
$(@).trigger 'volumeChange', @currentVolume
@$('.volume').toggleClass 'muted', @currentVolume == 0
toggleMute: =>
if @currentVolume > 0
@previousVolume = @currentVolume
@slider.slider 'option', 'value', 0
else
@slider.slider 'option', 'value', @previousVolume

View File

@@ -12,8 +12,7 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
"""
mako_template = "widgets/raw-edit.html"
js = {'coffee': [resource_string(__name__, 'js/module/raw.coffee')]}
js_module = 'Raw'
js = {'coffee': [resource_string(__name__, 'js/src/raw/edit.coffee')]}
def get_context(self):
return {

View File

@@ -8,6 +8,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError
from pkg_resources import resource_string
log = logging.getLogger("mitx.common.lib.seq_module")
@@ -19,6 +20,7 @@ class_priority = ['video', 'problem']
class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence
'''
js = {'coffee': [resource_string(__name__, 'js/src/sequence/display.coffee')]}
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)

View File

@@ -2,6 +2,7 @@ import json
import logging
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
@@ -13,6 +14,13 @@ class VideoModule(XModule):
video_time = 0
icon_class = 'video'
js = {'coffee':
[resource_string(__name__, 'js/src/video/display.coffee')] +
[resource_string(__name__, 'js/src/video/display/' + filename)
for filename
in resource_listdir(__name__, 'js/src/video/display')
]}
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data'])

View File

@@ -77,6 +77,8 @@ class XModule(object):
# if the icon class depends on the data in the module
icon_class = 'other'
js = {}
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
'''
Construct a new xmodule
@@ -179,11 +181,6 @@ class XModule(object):
'''
return None
def get_html(self):
''' HTML, as shown in the browser. This is the only method that must be implemented
'''
raise NotImplementedError("get_html must be defined for all XModules that appear on the screen. Not defined in %s" % self.__class__.__name__)
def get_progress(self):
''' Return a progress.Progress object that represents how far the student has gone
in this module. Must be implemented to get correct progress tracking behavior in
@@ -198,6 +195,25 @@ class XModule(object):
get is a dictionary-like object '''
return ""
# ================================== HTML INTERFACE DEFINITIONS ======================
@classmethod
def get_javascript(cls):
"""
Return a dictionary containing some of the following keys:
coffee: A list of coffeescript fragments that should be compiled and
placed on the page
js: A list of javascript fragments that should be included on the page
All of these will be loaded onto the page in the LMS
"""
return cls.js
def get_html(self):
''' HTML, as shown in the browser. This is the only method that must be implemented
'''
raise NotImplementedError("get_html must be defined for all XModules that appear on the screen. Not defined in %s" % self.__class__.__name__)
class XModuleDescriptor(Plugin):
"""
@@ -211,7 +227,6 @@ class XModuleDescriptor(Plugin):
"""
entry_point = "xmodule.v1"
js = {}
js_module = None
# A list of metadata that this module can inherit from its parent module
inheritable_metadata = (
@@ -398,13 +413,6 @@ class XModuleDescriptor(Plugin):
"""
return cls.js
def js_module_name(self):
"""
Return the name of the javascript class to instantiate when
this module descriptor is loaded for editing
"""
return self.js_module
def get_html(self):
"""
Return the html used to edit this module