Move CoffeeScripts to asset pipeline
This commit is contained in:
1
static/coffee/.gitignore
vendored
Normal file
1
static/coffee/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.js
|
||||
15
static/coffee/fixtures/items.json
Normal file
15
static/coffee/fixtures/items.json
Normal 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"
|
||||
}
|
||||
]
|
||||
1
static/coffee/fixtures/problem.html
Normal file
1
static/coffee/fixtures/problem.html
Normal file
@@ -0,0 +1 @@
|
||||
<section id="problem_1" class="problems-wrapper" data-url="/problem/url/"></section>
|
||||
16
static/coffee/fixtures/problem_content.html
Normal file
16
static/coffee/fixtures/problem_content.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<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 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>
|
||||
20
static/coffee/fixtures/sequence.html
Normal file
20
static/coffee/fixtures/sequence.html
Normal 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>
|
||||
3
static/coffee/fixtures/tab.html
Normal file
3
static/coffee/fixtures/tab.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div id="tab_1" class="tab">
|
||||
<ul class="navigation"></ul>
|
||||
</div>
|
||||
12
static/coffee/fixtures/video.html
Normal file
12
static/coffee/fixtures/video.html
Normal 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>
|
||||
@@ -1,80 +0,0 @@
|
||||
(function() {
|
||||
|
||||
describe('Calculator', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('calculator.html');
|
||||
return this.calculator = new Calculator;
|
||||
});
|
||||
describe('bind', function() {
|
||||
beforeEach(function() {
|
||||
return Calculator.bind();
|
||||
});
|
||||
it('bind the calculator button', function() {
|
||||
return expect($('.calc')).toHandleWith('click', this.calculator.toggle);
|
||||
});
|
||||
it('bind the help button', function() {
|
||||
expect($('div.help-wrapper a')).toHandleWith('mouseenter', this.calculator.helpToggle);
|
||||
return expect($('div.help-wrapper a')).toHandleWith('mouseleave', this.calculator.helpToggle);
|
||||
});
|
||||
it('prevent default behavior on help button', function() {
|
||||
$('div.help-wrapper a').click(function(e) {
|
||||
return expect(e.isDefaultPrevented()).toBeTruthy();
|
||||
});
|
||||
return $('div.help-wrapper a').click();
|
||||
});
|
||||
it('bind the calculator submit', function() {
|
||||
return expect($('form#calculator')).toHandleWith('submit', this.calculator.calculate);
|
||||
});
|
||||
return it('prevent default behavior on form submit', function() {
|
||||
jasmine.stubRequests();
|
||||
$('form#calculator').submit(function(e) {
|
||||
expect(e.isDefaultPrevented()).toBeTruthy();
|
||||
return e.preventDefault();
|
||||
});
|
||||
return $('form#calculator').submit();
|
||||
});
|
||||
});
|
||||
describe('toggle', function() {
|
||||
it('toggle the calculator and focus the input', function() {
|
||||
spyOn($.fn, 'focus');
|
||||
this.calculator.toggle();
|
||||
expect($('li.calc-main')).toHaveClass('open');
|
||||
return expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled();
|
||||
});
|
||||
return it('toggle the close button on the calculator button', function() {
|
||||
this.calculator.toggle();
|
||||
expect($('.calc')).toHaveClass('closed');
|
||||
this.calculator.toggle();
|
||||
return expect($('.calc')).not.toHaveClass('closed');
|
||||
});
|
||||
});
|
||||
describe('helpToggle', function() {
|
||||
return it('toggle the help overlay', function() {
|
||||
this.calculator.helpToggle();
|
||||
expect($('.help')).toHaveClass('shown');
|
||||
this.calculator.helpToggle();
|
||||
return expect($('.help')).not.toHaveClass('shown');
|
||||
});
|
||||
});
|
||||
return describe('calculate', function() {
|
||||
beforeEach(function() {
|
||||
$('#calculator_input').val('1+2');
|
||||
spyOn($, 'getWithPrefix').andCallFake(function(url, data, callback) {
|
||||
return callback({
|
||||
result: 3
|
||||
});
|
||||
});
|
||||
return this.calculator.calculate();
|
||||
});
|
||||
it('send data to /calculate', function() {
|
||||
return expect($.getWithPrefix).toHaveBeenCalledWith('/calculate', {
|
||||
equation: '1+2'
|
||||
}, jasmine.any(Function));
|
||||
});
|
||||
return it('update the calculator output', function() {
|
||||
return expect($('#calculator_output').val()).toEqual('3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -1,59 +0,0 @@
|
||||
(function() {
|
||||
|
||||
describe('Courseware', function() {
|
||||
describe('start', function() {
|
||||
it('create the navigation', function() {
|
||||
spyOn(window, 'Navigation');
|
||||
Courseware.start();
|
||||
return expect(window.Navigation).toHaveBeenCalled();
|
||||
});
|
||||
it('create the calculator', function() {
|
||||
spyOn(window, 'Calculator');
|
||||
Courseware.start();
|
||||
return expect(window.Calculator).toHaveBeenCalled();
|
||||
});
|
||||
it('creates the FeedbackForm', function() {
|
||||
spyOn(window, 'FeedbackForm');
|
||||
Courseware.start();
|
||||
return expect(window.FeedbackForm).toHaveBeenCalled();
|
||||
});
|
||||
return it('binds the Logger', function() {
|
||||
spyOn(Logger, 'bind');
|
||||
Courseware.start();
|
||||
return expect(Logger.bind).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('bind', function() {
|
||||
beforeEach(function() {
|
||||
this.courseware = new Courseware;
|
||||
return setFixtures("<div class=\"course-content\">\n <div class=\"sequence\"></div>\n</div>");
|
||||
});
|
||||
return it('binds the content change event', function() {
|
||||
this.courseware.bind();
|
||||
return expect($('.course-content .sequence')).toHandleWith('contentChanged', this.courseware.render);
|
||||
});
|
||||
});
|
||||
return describe('render', function() {
|
||||
beforeEach(function() {
|
||||
jasmine.stubRequests();
|
||||
this.courseware = new Courseware;
|
||||
spyOn(window, 'Histogram');
|
||||
spyOn(window, 'Problem');
|
||||
spyOn(window, 'Video');
|
||||
setFixtures("<div class=\"course-content\">\n <div id=\"video_1\" class=\"video\" data-streams=\"1.0:abc1234\"></div>\n <div id=\"video_2\" class=\"video\" data-streams=\"1.0:def5678\"></div>\n <div id=\"problem_3\" class=\"problems-wrapper\" data-url=\"/example/url/\">\n <div id=\"histogram_3\" class=\"histogram\" data-histogram=\"[[0, 1]]\" style=\"height: 20px; display: block;\">\n </div>\n</div>");
|
||||
return this.courseware.render();
|
||||
});
|
||||
it('detect the video elements and convert them', function() {
|
||||
expect(window.Video).toHaveBeenCalledWith('1', '1.0:abc1234');
|
||||
return expect(window.Video).toHaveBeenCalledWith('2', '1.0:def5678');
|
||||
});
|
||||
it('detect the problem element and convert it', function() {
|
||||
return expect(window.Problem).toHaveBeenCalledWith('3', '/example/url/');
|
||||
});
|
||||
return it('detect the histrogram element and convert it', function() {
|
||||
return expect(window.Histogram).toHaveBeenCalledWith('3', [[0, 1]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -1,34 +0,0 @@
|
||||
(function() {
|
||||
|
||||
describe('FeedbackForm', function() {
|
||||
beforeEach(function() {
|
||||
return loadFixtures('feedback_form.html');
|
||||
});
|
||||
return describe('constructor', function() {
|
||||
beforeEach(function() {
|
||||
new FeedbackForm;
|
||||
return spyOn($, 'postWithPrefix').andCallFake(function(url, data, callback, format) {
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
it('binds to the #feedback_button', function() {
|
||||
return expect($('#feedback_button')).toHandle('click');
|
||||
});
|
||||
it('post data to /send_feedback on click', function() {
|
||||
$('#feedback_subject').val('Awesome!');
|
||||
$('#feedback_message').val('This site is really good.');
|
||||
$('#feedback_button').click();
|
||||
return expect($.postWithPrefix).toHaveBeenCalledWith('/send_feedback', {
|
||||
subject: 'Awesome!',
|
||||
message: 'This site is really good.',
|
||||
url: window.location.href
|
||||
}, jasmine.any(Function), 'json');
|
||||
});
|
||||
return it('replace the form with a thank you message', function() {
|
||||
$('#feedback_button').click();
|
||||
return expect($('#feedback_div').html()).toEqual('Feedback submitted. Thank you');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -1,91 +0,0 @@
|
||||
(function() {
|
||||
|
||||
jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/";
|
||||
|
||||
jasmine.stubbedMetadata = {
|
||||
abc123: {
|
||||
id: 'abc123',
|
||||
duration: 100
|
||||
},
|
||||
def456: {
|
||||
id: 'def456',
|
||||
duration: 200
|
||||
},
|
||||
bogus: {
|
||||
duration: 300
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.stubbedCaption = {
|
||||
start: [0, 10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000, 100000, 110000, 120000],
|
||||
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000', 'Caption at 40000', 'Caption at 50000', 'Caption at 60000', 'Caption at 70000', 'Caption at 80000', 'Caption at 90000', 'Caption at 100000', 'Caption at 110000', 'Caption at 120000']
|
||||
};
|
||||
|
||||
jasmine.stubRequests = function() {
|
||||
return spyOn($, 'ajax').andCallFake(function(settings) {
|
||||
var match;
|
||||
if (match = settings.url.match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/)) {
|
||||
return settings.success({
|
||||
data: jasmine.stubbedMetadata[match[1]]
|
||||
});
|
||||
} else if (match = settings.url.match(/static\/subs\/(.+)\.srt\.sjson/)) {
|
||||
return settings.success(jasmine.stubbedCaption);
|
||||
} else if (settings.url === '/calculate' || settings.url === '/6002x/modx/sequential/1/goto_position' || settings.url.match(/event$/) || settings.url.match(/6002x\/modx\/problem\/.+\/problem_(check|reset|show|save)$/)) {
|
||||
|
||||
} else {
|
||||
throw "External request attempted for " + settings.url + ", which is not defined.";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
jasmine.stubYoutubePlayer = function() {
|
||||
return YT.Player = function() {
|
||||
return jasmine.createSpyObj('YT.Player', ['cueVideoById', 'getVideoEmbedCode', 'getCurrentTime', 'getPlayerState', 'loadVideoById', 'playVideo', 'pauseVideo', 'seekTo']);
|
||||
};
|
||||
};
|
||||
|
||||
jasmine.stubVideoPlayer = function(context, enableParts) {
|
||||
var currentPartName, part, suite, _i, _len, _ref;
|
||||
if (!$.isArray(enableParts)) {
|
||||
enableParts = [enableParts];
|
||||
}
|
||||
suite = context.suite;
|
||||
while (suite = suite.parentSuite) {
|
||||
currentPartName = suite.description;
|
||||
}
|
||||
enableParts.push(currentPartName);
|
||||
_ref = ['VideoCaption', 'VideoSpeedControl', 'VideoProgressSlider'];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
part = _ref[_i];
|
||||
if (!($.inArray(part, enableParts) >= 0)) {
|
||||
spyOn(window, part);
|
||||
}
|
||||
}
|
||||
loadFixtures('video.html');
|
||||
jasmine.stubRequests();
|
||||
YT.Player = void 0;
|
||||
context.video = new Video('example', '.75:abc123,1.0:def456');
|
||||
jasmine.stubYoutubePlayer();
|
||||
return new VideoPlayer(context.video);
|
||||
};
|
||||
|
||||
spyOn(window, 'onunload');
|
||||
|
||||
window.YT = {
|
||||
PlayerState: {
|
||||
UNSTARTED: -1,
|
||||
ENDED: 0,
|
||||
PLAYING: 1,
|
||||
PAUSED: 2,
|
||||
BUFFERING: 3,
|
||||
CUED: 5
|
||||
}
|
||||
};
|
||||
|
||||
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn('1.0');
|
||||
|
||||
$.fn.qtip = jasmine.createSpy('jQuery.qtip');
|
||||
|
||||
$.fn.scrollTo = jasmine.createSpy('jQuery.scrollTo');
|
||||
|
||||
}).call(this);
|
||||
46
static/coffee/spec/histogram_spec.coffee
Normal file
46
static/coffee/spec/histogram_spec.coffee
Normal file
@@ -0,0 +1,46 @@
|
||||
describe 'Histogram', ->
|
||||
beforeEach ->
|
||||
spyOn $, 'plot'
|
||||
|
||||
describe 'constructor', ->
|
||||
it 'instantiate the data arrays', ->
|
||||
histogram = new Histogram 1, []
|
||||
expect(histogram.xTicks).toEqual []
|
||||
expect(histogram.yTicks).toEqual []
|
||||
expect(histogram.data).toEqual []
|
||||
|
||||
describe 'calculate', ->
|
||||
beforeEach ->
|
||||
@histogram = new Histogram(1, [[1, 1], [2, 2], [3, 3]])
|
||||
|
||||
it 'store the correct value for data', ->
|
||||
expect(@histogram.data).toEqual [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]
|
||||
|
||||
it 'store the correct value for x ticks', ->
|
||||
expect(@histogram.xTicks).toEqual [[1, '1'], [2, '2'], [3, '3']]
|
||||
|
||||
it 'store the correct value for y ticks', ->
|
||||
expect(@histogram.yTicks).toEqual
|
||||
|
||||
describe 'render', ->
|
||||
it 'call flot with correct option', ->
|
||||
new Histogram(1, [[1, 1], [2, 2], [3, 3]])
|
||||
expect($.plot).toHaveBeenCalledWith $("#histogram_1"), [
|
||||
data: [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]
|
||||
bars:
|
||||
show: true
|
||||
align: 'center'
|
||||
lineWidth: 0
|
||||
fill: 1.0
|
||||
color: "#b72121"
|
||||
],
|
||||
xaxis:
|
||||
min: -1
|
||||
max: 4
|
||||
ticks: [[1, '1'], [2, '2'], [3, '3']]
|
||||
tickLength: 0
|
||||
yaxis:
|
||||
min: 0.0
|
||||
max: Math.log(4) * 1.1
|
||||
ticks: [[Math.log(2), '1'], [Math.log(3), '2'], [Math.log(4), '3']]
|
||||
labelWidth: 50
|
||||
35
static/coffee/spec/logger_spec.coffee
Normal file
35
static/coffee/spec/logger_spec.coffee
Normal file
@@ -0,0 +1,35 @@
|
||||
describe 'Logger', ->
|
||||
it 'expose window.log_event', ->
|
||||
jasmine.stubRequests()
|
||||
expect(window.log_event).toBe Logger.log
|
||||
|
||||
describe 'log', ->
|
||||
it 'send a request to log event', ->
|
||||
spyOn $, 'getWithPrefix'
|
||||
Logger.log 'example', 'data'
|
||||
expect($.getWithPrefix).toHaveBeenCalledWith '/event',
|
||||
event_type: 'example'
|
||||
event: '"data"'
|
||||
page: window.location.href
|
||||
|
||||
describe 'bind', ->
|
||||
beforeEach ->
|
||||
Logger.bind()
|
||||
Courseware.prefix = '/6002x'
|
||||
|
||||
afterEach ->
|
||||
window.onunload = null
|
||||
|
||||
it 'bind the onunload event', ->
|
||||
expect(window.onunload).toEqual jasmine.any(Function)
|
||||
|
||||
it 'send a request to log event', ->
|
||||
spyOn($, 'ajax')
|
||||
$(window).trigger('onunload')
|
||||
expect($.ajax).toHaveBeenCalledWith
|
||||
url: "#{Courseware.prefix}/event",
|
||||
data:
|
||||
event_type: 'page_close'
|
||||
event: ''
|
||||
page: window.location.href
|
||||
async: false
|
||||
250
static/coffee/spec/modules/problem_spec.coffee
Normal file
250
static/coffee/spec/modules/problem_spec.coffee
Normal file
@@ -0,0 +1,250 @@
|
||||
describe 'Problem', ->
|
||||
beforeEach ->
|
||||
# Stub MathJax
|
||||
window.MathJax = { Hub: { Queue: -> } }
|
||||
window.update_schematics = ->
|
||||
|
||||
loadFixtures 'problem.html'
|
||||
spyOn Logger, 'log'
|
||||
spyOn($.fn, 'load').andCallFake (url, callback) ->
|
||||
$(@).html readFixtures('problem_content.html')
|
||||
callback()
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@problem.element).toBe '#problem_1'
|
||||
|
||||
it 'set the content url', ->
|
||||
expect(@problem.content_url).toEqual '/problem/url/problem_get?id=1'
|
||||
|
||||
it 'render the content', ->
|
||||
expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @problem.bind
|
||||
|
||||
describe 'bind', ->
|
||||
beforeEach ->
|
||||
spyOn MathJax.Hub, 'Queue'
|
||||
spyOn window, 'update_schematics'
|
||||
@problem = new 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
|
||||
|
||||
describe 'render', ->
|
||||
beforeEach ->
|
||||
@problem = new 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.element.html()).toEqual 'Hello World'
|
||||
|
||||
it 're-bind the content', ->
|
||||
expect(@problem.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'with no content given', ->
|
||||
it 'load the content via ajax', ->
|
||||
expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @bind
|
||||
|
||||
describe 'check', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@problem = new 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/problem/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.element.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.element.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 ->
|
||||
jasmine.stubRequests()
|
||||
@problem = new 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/problem/1/problem_reset', { id: 1 }, jasmine.any(Function)
|
||||
|
||||
it 'render the returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback("Reset!")
|
||||
@problem.reset()
|
||||
expect(@problem.element.html()).toEqual 'Reset!'
|
||||
|
||||
describe 'show', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
@problem.element.prepend '<div id="answer_1_1" /><div id="answer_1_2" />'
|
||||
|
||||
describe 'when the answer has not yet shown', ->
|
||||
beforeEach ->
|
||||
@problem.element.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/problem/1/problem_show', jasmine.any(Function)
|
||||
|
||||
it 'show the answers', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('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({})
|
||||
@problem.show()
|
||||
expect($('.show')).toHaveValue 'Hide Answer'
|
||||
|
||||
it 'add the showed class to element', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({})
|
||||
@problem.show()
|
||||
expect(@problem.element).toHaveClass 'showed'
|
||||
|
||||
describe 'multiple choice question', ->
|
||||
beforeEach ->
|
||||
@problem.element.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('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.element.addClass 'showed'
|
||||
@problem.element.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.element).not.toHaveClass 'showed'
|
||||
|
||||
describe 'save', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@problem = new 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/problem/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 'refreshAnswers', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
@problem.element.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"
|
||||
156
static/coffee/spec/modules/sequence_spec.coffee
Normal file
156
static/coffee/spec/modules/sequence_spec.coffee
Normal file
@@ -0,0 +1,156 @@
|
||||
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', @items, 1
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@sequence.element).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.element).toHandleWith 'contentChanged', @sequence.toggleArrows
|
||||
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', @items, 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', @items
|
||||
spyOnEvent @sequence.element, 'contentChanged'
|
||||
|
||||
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/sequential/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 'trigger contentChanged event', ->
|
||||
expect('contentChanged').toHaveBeenTriggeredOn @sequence.element
|
||||
|
||||
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.element
|
||||
|
||||
describe 'goto', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@sequence = new Sequence '1', @items, 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', @items, 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', @items, 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', @items, 2
|
||||
expect(sequence.link_for(2)).toBe '[data-element="2"]'
|
||||
39
static/coffee/spec/modules/tab_spec.coffee
Normal file
39
static/coffee/spec/modules/tab_spec.coffee
Normal file
@@ -0,0 +1,39 @@
|
||||
describe 'Tab', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'tab.html'
|
||||
@items = $.parseJSON readFixtures('items.json')
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn($.fn, 'tabs')
|
||||
@tab = new Tab 1, @items
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@tab.element).toEqual $('#tab_1')
|
||||
|
||||
it 'build the tabs', ->
|
||||
links = $('.navigation li>a').map(-> $(this).attr('href')).get()
|
||||
expect(links).toEqual ['#tab-1-0', '#tab-1-1', '#tab-1-2']
|
||||
|
||||
it 'build the container', ->
|
||||
containers = $('section').map(-> $(this).attr('id')).get()
|
||||
expect(containers).toEqual ['tab-1-0', 'tab-1-1', 'tab-1-2']
|
||||
|
||||
it 'bind the tabs', ->
|
||||
expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow
|
||||
|
||||
describe 'onShow', ->
|
||||
beforeEach ->
|
||||
@tab = new Tab 1, @items
|
||||
$('[href="#tab-1-0"]').click()
|
||||
|
||||
it 'replace content in the container', ->
|
||||
$('[href="#tab-1-1"]').click()
|
||||
expect($('#tab-1-0').html()).toEqual ''
|
||||
expect($('#tab-1-1').html()).toEqual 'Video 2'
|
||||
expect($('#tab-1-2').html()).toEqual ''
|
||||
|
||||
it 'trigger contentChanged event on the element', ->
|
||||
spyOnEvent @tab.element, 'contentChanged'
|
||||
$('[href="#tab-1-1"]').click()
|
||||
expect('contentChanged').toHaveBeenTriggeredOn @tab.element
|
||||
294
static/coffee/spec/modules/video/video_caption_spec.coffee
Normal file
294
static/coffee/spec/modules/video/video_caption_spec.coffee
Normal file
@@ -0,0 +1,294 @@
|
||||
describe 'VideoCaption', ->
|
||||
beforeEach ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
|
||||
afterEach ->
|
||||
YT.Player = undefined
|
||||
$.fn.scrollTo.reset()
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn($, 'getWithPrefix').andCallThrough()
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
it 'set the player', ->
|
||||
expect(@caption.player).toEqual @player
|
||||
|
||||
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 '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>
|
||||
<li data-index="4" data-start="40000">Caption at 40000</li>
|
||||
<li data-index="5" data-start="50000">Caption at 50000</li>
|
||||
<li data-index="6" data-start="60000">Caption at 60000</li>
|
||||
<li data-index="7" data-start="70000">Caption at 70000</li>
|
||||
<li data-index="8" data-start="80000">Caption at 80000</li>
|
||||
<li data-index="9" data-start="90000">Caption at 90000</li>
|
||||
<li data-index="10" data-start="100000">Caption at 100000</li>
|
||||
<li data-index="11" data-start="110000">Caption at 110000</li>
|
||||
<li data-index="12" data-start="120000">Caption at 120000</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 'bind window resize event', ->
|
||||
expect($(window)).toHandleWith 'resize', @caption.onWindowResize
|
||||
|
||||
it 'bind player resize event', ->
|
||||
expect($(@player)).toHandleWith 'resize', @caption.onWindowResize
|
||||
|
||||
it 'bind player updatePlayTime event', ->
|
||||
expect($(@player)).toHandleWith 'updatePlayTime', @caption.onUpdatePlayTime
|
||||
|
||||
it 'bind the hide caption button', ->
|
||||
expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle
|
||||
|
||||
it 'bind the mouse movement', ->
|
||||
expect($('.subtitles')).toHandleWith 'mouseenter', @caption.onMouseEnter
|
||||
expect($('.subtitles')).toHandleWith 'mouseleave', @caption.onMouseLeave
|
||||
expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement
|
||||
expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement
|
||||
expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement
|
||||
|
||||
describe 'mouse movement', ->
|
||||
beforeEach ->
|
||||
spyOn(window, 'setTimeout').andReturn 100
|
||||
spyOn window, 'clearTimeout'
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
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 ->
|
||||
spyOn(@player, 'isPlaying').andReturn 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 ->
|
||||
spyOn(@player, 'isPlaying').andReturn false
|
||||
$('.subtitles').trigger jQuery.Event 'mouseout'
|
||||
|
||||
it 'does not scroll the caption', ->
|
||||
expect($.fn.scrollTo).not.toHaveBeenCalled()
|
||||
|
||||
describe 'search', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
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(120000)).toEqual 12
|
||||
expect(@caption.search(120001)).toEqual 12
|
||||
|
||||
describe 'onUpdatePlayTime', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
describe 'when the video speed is 1.0x', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '1.0'
|
||||
@caption.onUpdatePlayTime {}, 25.000
|
||||
|
||||
it 'search the caption based on time', ->
|
||||
expect(@caption.currentIndex).toEqual 2
|
||||
|
||||
describe 'when the video speed is not 1.0x', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '0.75'
|
||||
@caption.onUpdatePlayTime {}, 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.onUpdatePlayTime {}, 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.onUpdatePlayTime {}, 15.000
|
||||
|
||||
it 'does not change current subtitle', ->
|
||||
expect($('.subtitles li[data-index=1]')).toHaveClass 'current'
|
||||
|
||||
describe 'onWindowResize', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
$('.subtitles li[data-index=1]').addClass 'current'
|
||||
@caption.onWindowResize()
|
||||
|
||||
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 @player, 'def456'
|
||||
|
||||
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', @player.element),
|
||||
offset: - ($('.video-wrapper').height() / 2 - $('.subtitles .current:first').height() / 2)
|
||||
|
||||
describe 'seekPlayer', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
@time = null
|
||||
$(@player).bind 'seek', (event, time) => @time = time
|
||||
|
||||
describe 'when the video speed is 1.0x', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '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 ->
|
||||
@video.setSpeed '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 @player, 'def456'
|
||||
$('.subtitles li[data-index=1]').addClass 'current'
|
||||
|
||||
describe 'when the caption is visible', ->
|
||||
beforeEach ->
|
||||
@player.element.removeClass 'closed'
|
||||
@caption.toggle jQuery.Event('click')
|
||||
|
||||
it 'hide the caption', ->
|
||||
expect(@player.element).toHaveClass 'closed'
|
||||
|
||||
|
||||
describe 'when the caption is hidden', ->
|
||||
beforeEach ->
|
||||
@player.element.addClass 'closed'
|
||||
@caption.toggle jQuery.Event('click')
|
||||
|
||||
it 'show the caption', ->
|
||||
expect(@player.element).not.toHaveClass 'closed'
|
||||
|
||||
it 'scroll the caption', ->
|
||||
expect($.fn.scrollTo).toHaveBeenCalled()
|
||||
73
static/coffee/spec/modules/video/video_control_spec.coffee
Normal file
73
static/coffee/spec/modules/video/video_control_spec.coffee
Normal file
@@ -0,0 +1,73 @@
|
||||
describe 'VideoControl', ->
|
||||
beforeEach ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@control = new VideoControl @player
|
||||
|
||||
it 'render the video controls', ->
|
||||
expect($('.video-controls').html()).toContain '''
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control play">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 player events', ->
|
||||
expect($(@player)).toHandleWith 'play', @control.onPlay
|
||||
expect($(@player)).toHandleWith 'pause', @control.onPause
|
||||
expect($(@player)).toHandleWith 'ended', @control.onPause
|
||||
|
||||
it 'bind the playback button', ->
|
||||
expect($('.video_control')).toHandleWith 'click', @control.togglePlayback
|
||||
|
||||
describe 'onPlay', ->
|
||||
beforeEach ->
|
||||
@control = new VideoControl @player
|
||||
@control.onPlay()
|
||||
|
||||
it 'switch playback button to play state', ->
|
||||
expect($('.video_control')).not.toHaveClass 'play'
|
||||
expect($('.video_control')).toHaveClass 'pause'
|
||||
expect($('.video_control')).toHaveHtml 'Pause'
|
||||
|
||||
describe 'onPause', ->
|
||||
beforeEach ->
|
||||
@control = new VideoControl @player
|
||||
@control.onPause()
|
||||
|
||||
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 @player
|
||||
|
||||
describe 'when the video is playing', ->
|
||||
beforeEach ->
|
||||
spyOn(@player, 'isPlaying').andReturn true
|
||||
spyOnEvent @player, 'pause'
|
||||
@control.togglePlayback jQuery.Event('click')
|
||||
|
||||
it 'trigger the pause event', ->
|
||||
expect('pause').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is paused', ->
|
||||
beforeEach ->
|
||||
spyOn(@player, 'isPlaying').andReturn false
|
||||
spyOnEvent @player, 'play'
|
||||
@control.togglePlayback jQuery.Event('click')
|
||||
|
||||
it 'trigger the play event', ->
|
||||
expect('play').toHaveBeenTriggeredOn @player
|
||||
381
static/coffee/spec/modules/video/video_player_spec.coffee
Normal file
381
static/coffee/spec/modules/video/video_player_spec.coffee
Normal file
@@ -0,0 +1,381 @@
|
||||
describe 'VideoPlayer', ->
|
||||
beforeEach ->
|
||||
jasmine.stubVideoPlayer @
|
||||
|
||||
afterEach ->
|
||||
YT.Player = undefined
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn window, 'VideoControl'
|
||||
spyOn YT, 'Player'
|
||||
$.fn.qtip.andCallFake ->
|
||||
$(this).data('qtip', true)
|
||||
$('.video').append $('<div class="hide-subtitles" />')
|
||||
@player = new VideoPlayer @video
|
||||
|
||||
it 'instanticate current time to zero', ->
|
||||
expect(@player.currentTime).toEqual 0
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@player.element).toBe '#video_example'
|
||||
|
||||
it 'create video control', ->
|
||||
expect(window.VideoControl).toHaveBeenCalledWith @player
|
||||
|
||||
it 'create video caption', ->
|
||||
expect(window.VideoCaption).toHaveBeenCalledWith @player, 'def456'
|
||||
|
||||
it 'create video speed control', ->
|
||||
expect(window.VideoSpeedControl).toHaveBeenCalledWith @player, ['0.75', '1.0']
|
||||
|
||||
it 'create video progress slider', ->
|
||||
expect(window.VideoProgressSlider).toHaveBeenCalledWith @player
|
||||
|
||||
it 'create Youtube player', ->
|
||||
expect(YT.Player).toHaveBeenCalledWith 'example'
|
||||
playerVars:
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
videoId: 'def456'
|
||||
events:
|
||||
onReady: @player.onReady
|
||||
onStateChange: @player.onStateChange
|
||||
|
||||
it 'bind to seek event', ->
|
||||
expect($(@player)).toHandleWith 'seek', @player.onSeek
|
||||
|
||||
it 'bind to updatePlayTime event', ->
|
||||
expect($(@player)).toHandleWith 'updatePlayTime', @player.onUpdatePlayTime
|
||||
|
||||
it 'bidn to speedChange event', ->
|
||||
expect($(@player)).toHandleWith 'speedChange', @player.onSpeedChange
|
||||
|
||||
it 'bind to play event', ->
|
||||
expect($(@player)).toHandleWith 'play', @player.onPlay
|
||||
|
||||
it 'bind to paused event', ->
|
||||
expect($(@player)).toHandleWith 'pause', @player.onPause
|
||||
|
||||
it 'bind to ended event', ->
|
||||
expect($(@player)).toHandleWith 'ended', @player.onPause
|
||||
|
||||
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', ->
|
||||
it 'add the tooltip to fullscreen and subtitle button', ->
|
||||
expect($('.add-fullscreen')).toHaveData 'qtip'
|
||||
expect($('.hide-subtitles')).toHaveData 'qtip'
|
||||
|
||||
describe 'onReady', ->
|
||||
beforeEach ->
|
||||
@video.embed()
|
||||
@player = @video.player
|
||||
spyOnEvent @player, 'ready'
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
@player.onReady()
|
||||
|
||||
it 'reset the progress to zero', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
it 'trigger ready event on the player', ->
|
||||
expect('ready').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when not on a touch based device', ->
|
||||
beforeEach ->
|
||||
window.onTouchBasedDevice = -> false
|
||||
spyOn @player, 'play'
|
||||
@player.onReady()
|
||||
|
||||
it 'autoplay the first video', ->
|
||||
expect(@player.play).toHaveBeenCalled()
|
||||
|
||||
describe 'when on a touch based device', ->
|
||||
beforeEach ->
|
||||
window.onTouchBasedDevice = -> 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
|
||||
|
||||
describe 'when the video is playing', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'play'
|
||||
@player.onStateChange data: YT.PlayerState.PLAYING
|
||||
|
||||
it 'trigger play event', ->
|
||||
expect('play').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is paused', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'pause'
|
||||
@player.onStateChange data: YT.PlayerState.PAUSED
|
||||
|
||||
it 'trigger pause event', ->
|
||||
expect('pause').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is ended', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'ended'
|
||||
@player.onStateChange data: YT.PlayerState.ENDED
|
||||
|
||||
it 'trigger ended event', ->
|
||||
expect('ended').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'onPlay', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
@anotherPlayer = jasmine.createSpyObj 'AnotherPlayer', ['pauseVideo']
|
||||
window.player = @anotherPlayer
|
||||
spyOn Logger, 'log'
|
||||
spyOn(window, 'setInterval').andReturn 100
|
||||
@player.player.getVideoEmbedCode.andReturn 'embedCode'
|
||||
@player.onPlay()
|
||||
|
||||
it 'log the play_video event', ->
|
||||
expect(Logger.log).toHaveBeenCalledWith 'play_video', id: @player.currentTime, code: 'embedCode'
|
||||
|
||||
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
|
||||
|
||||
describe 'onPause', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
window.player = @player.player
|
||||
spyOn Logger, 'log'
|
||||
spyOn window, 'clearInterval'
|
||||
@player.player.interval = 100
|
||||
@player.player.getVideoEmbedCode.andReturn 'embedCode'
|
||||
@player.onPause()
|
||||
|
||||
it 'log the pause_video event', ->
|
||||
expect(Logger.log).toHaveBeenCalledWith 'pause_video', id: @player.currentTime, code: 'embedCode'
|
||||
|
||||
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()
|
||||
|
||||
describe 'onSeek', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
spyOn window, 'clearInterval'
|
||||
@player.player.interval = 100
|
||||
@player.onSeek {}, 60
|
||||
|
||||
it 'seek the player', ->
|
||||
expect(@player.player.seekTo).toHaveBeenCalledWith 60, true
|
||||
|
||||
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
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
@player.onSeek {}, 60
|
||||
|
||||
it 'set the current time', ->
|
||||
expect(@player.currentTime).toEqual 60
|
||||
|
||||
it 'trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'onSpeedChange', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
@player.currentTime = 60
|
||||
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'
|
||||
|
||||
describe 'when the video is playing', ->
|
||||
beforeEach ->
|
||||
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
@player.onSpeedChange {}, '0.75'
|
||||
|
||||
it 'load the video', ->
|
||||
expect(@player.player.loadVideoById).toHaveBeenCalledWith 'abc123', '80.000'
|
||||
|
||||
it 'trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is not playing', ->
|
||||
beforeEach ->
|
||||
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
@player.onSpeedChange {}, '0.75'
|
||||
|
||||
it 'cue the video', ->
|
||||
expect(@player.player.cueVideoById).toHaveBeenCalledWith 'abc123', '80.000'
|
||||
|
||||
it 'trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'update', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
spyOnEvent @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('updatePlayTime').not.toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the current time is available from the player', ->
|
||||
beforeEach ->
|
||||
@player.player.getCurrentTime.andReturn 60
|
||||
@player.update()
|
||||
|
||||
it 'trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'onUpdatePlaytime', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
spyOn(@video, 'getDuration').andReturn 1800
|
||||
@player.onUpdatePlayTime {}, 60
|
||||
|
||||
it 'update the video playback time', ->
|
||||
expect($('.vidtime')).toHaveHtml '1:00 / 30:00'
|
||||
|
||||
describe 'toggleFullScreen', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
|
||||
describe 'when the video player is not full screen', ->
|
||||
beforeEach ->
|
||||
@player.element.removeClass 'fullscreen'
|
||||
spyOnEvent @player, 'resize'
|
||||
@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.element).toContain 'a.exit'
|
||||
|
||||
it 'add the fullscreen class', ->
|
||||
expect(@player.element).toHaveClass 'fullscreen'
|
||||
|
||||
it 'trigger resize event', ->
|
||||
expect('resize').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video player already full screen', ->
|
||||
beforeEach ->
|
||||
@player.element.addClass 'fullscreen'
|
||||
spyOnEvent @player, 'resize'
|
||||
@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.element).not.toContain 'a.exit'
|
||||
|
||||
it 'remove the fullscreen class', ->
|
||||
expect(@player.element).not.toHaveClass 'fullscreen'
|
||||
|
||||
it 'trigger resize event', ->
|
||||
expect('resize').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'play', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @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
|
||||
|
||||
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
|
||||
@player.pause()
|
||||
|
||||
it 'delegate to the Youtube player', ->
|
||||
expect(@player.player.pauseVideo).toHaveBeenCalled()
|
||||
|
||||
describe 'duration', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
spyOn @video, 'getDuration'
|
||||
@player.duration()
|
||||
|
||||
it 'delegate to the video', ->
|
||||
expect(@video.getDuration).toHaveBeenCalled()
|
||||
|
||||
describe 'currentSpeed', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
@video.speed = '3.0'
|
||||
|
||||
it 'delegate to the video', ->
|
||||
expect(@player.currentSpeed()).toEqual '3.0'
|
||||
@@ -0,0 +1,122 @@
|
||||
describe 'VideoProgressSlider', ->
|
||||
beforeEach ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn($.fn, 'slider').andCallThrough()
|
||||
@slider = new VideoProgressSlider @player
|
||||
|
||||
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
|
||||
|
||||
it 'bind player events', ->
|
||||
expect($(@player)).toHandleWith 'updatePlayTime', @slider.onUpdatePlayTime
|
||||
|
||||
describe 'onReady', ->
|
||||
beforeEach ->
|
||||
spyOn(@player, 'duration').andReturn 120
|
||||
@slider = new VideoProgressSlider @player
|
||||
@slider.onReady()
|
||||
|
||||
it 'set the max value to the length of video', ->
|
||||
expect(@slider.slider.slider('option', 'max')).toEqual 120
|
||||
|
||||
describe 'onUpdatePlayTime', ->
|
||||
beforeEach ->
|
||||
@slider = new VideoProgressSlider @player
|
||||
spyOn(@player, 'duration').andReturn 120
|
||||
spyOn($.fn, 'slider').andCallThrough()
|
||||
|
||||
describe 'when frozen', ->
|
||||
beforeEach ->
|
||||
@slider.frozen = true
|
||||
@slider.onUpdatePlayTime {}, 20
|
||||
|
||||
it 'does not update the slider', ->
|
||||
expect($.fn.slider).not.toHaveBeenCalled()
|
||||
|
||||
describe 'when not frozen', ->
|
||||
beforeEach ->
|
||||
@slider.frozen = false
|
||||
@slider.onUpdatePlayTime {}, 20
|
||||
|
||||
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 @player
|
||||
@time = null
|
||||
$(@player).bind 'seek', (event, time) => @time = time
|
||||
spyOnEvent @player, '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 @player
|
||||
expect(@time).toEqual 20
|
||||
|
||||
describe 'onChange', ->
|
||||
beforeEach ->
|
||||
@slider = new VideoProgressSlider @player
|
||||
@slider.onChange {}, value: 20
|
||||
|
||||
it 'update the tooltip', ->
|
||||
expect($.fn.qtip).toHaveBeenCalled()
|
||||
|
||||
describe 'onStop', ->
|
||||
beforeEach ->
|
||||
@slider = new VideoProgressSlider @player
|
||||
@time = null
|
||||
$(@player).bind 'seek', (event, time) => @time = time
|
||||
spyOnEvent @player, 'seek'
|
||||
spyOn(window, 'setTimeout')
|
||||
@slider.onStop {}, value: 20
|
||||
|
||||
it 'freeze the slider', ->
|
||||
expect(@slider.frozen).toBeTruthy()
|
||||
|
||||
it 'trigger seek event', ->
|
||||
expect('seek').toHaveBeenTriggeredOn @player
|
||||
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 @player
|
||||
@slider.updateTooltip 90
|
||||
|
||||
it 'set the tooltip value', ->
|
||||
expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'
|
||||
@@ -0,0 +1,95 @@
|
||||
describe 'VideoSpeedControl', ->
|
||||
beforeEach ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
$('.speeds').remove()
|
||||
|
||||
afterEach ->
|
||||
|
||||
describe 'constructor', ->
|
||||
describe 'always', ->
|
||||
beforeEach ->
|
||||
@speedControl = new VideoSpeedControl @player, @video.speeds
|
||||
|
||||
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 player speedChange event', ->
|
||||
expect($(@player)).toHandleWith 'speedChange', @speedControl.onSpeedChange
|
||||
|
||||
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 @player, @video.speeds
|
||||
|
||||
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 @player, @video.speeds
|
||||
|
||||
it 'open the speed toggle on hover', ->
|
||||
$('.speeds').mouseover()
|
||||
expect($('.speeds')).toHaveClass 'open'
|
||||
$('.speeds').mouseout()
|
||||
expect($('.speeds')).not.toHaveClass 'open'
|
||||
|
||||
it 'close the speed toggle on mouse out', ->
|
||||
$('.speeds').mouseover().mouseout()
|
||||
expect($('.speeds')).not.toHaveClass 'open'
|
||||
|
||||
it 'close the speed toggle on click', ->
|
||||
$('.speeds').mouseover().click()
|
||||
expect($('.speeds')).not.toHaveClass 'open'
|
||||
|
||||
describe 'changeVideoSpeed', ->
|
||||
beforeEach ->
|
||||
@speedControl = new VideoSpeedControl @player, @video.speeds
|
||||
@video.setSpeed '1.0'
|
||||
|
||||
describe 'when new speed is the same', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'speedChange'
|
||||
$('li[data-speed="1.0"] a').click()
|
||||
|
||||
it 'does not trigger speedChange event', ->
|
||||
expect('speedChange').not.toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when new speed is not the same', ->
|
||||
beforeEach ->
|
||||
@newSpeed = null
|
||||
$(@player).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed
|
||||
spyOnEvent @player, 'speedChange'
|
||||
$('li[data-speed="0.75"] a').click()
|
||||
|
||||
it 'trigger player speedChange event', ->
|
||||
expect('speedChange').toHaveBeenTriggeredOn @player
|
||||
expect(@newSpeed).toEqual 0.75
|
||||
|
||||
describe 'onSpeedChange', ->
|
||||
beforeEach ->
|
||||
@speedControl = new VideoSpeedControl @player, @video.speeds
|
||||
$('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'
|
||||
130
static/coffee/spec/modules/video_spec.coffee
Normal file
130
static/coffee/spec/modules/video_spec.coffee
Normal file
@@ -0,0 +1,130 @@
|
||||
describe 'Video', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'video.html'
|
||||
jasmine.stubRequests()
|
||||
|
||||
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', '.75:abc123,1.0:def456'
|
||||
|
||||
it 'reset the current video player', ->
|
||||
expect(window.player).toBeNull()
|
||||
|
||||
it 'set the elements', ->
|
||||
expect(@video.element).toBe '#video_example'
|
||||
|
||||
it 'parse the videos', ->
|
||||
expect(@video.videos).toEqual
|
||||
'0.75': 'abc123'
|
||||
'1.0': 'def456'
|
||||
|
||||
it 'fetch the video metadata', ->
|
||||
expect(@video.metadata).toEqual
|
||||
abc123:
|
||||
id: 'abc123'
|
||||
duration: 100
|
||||
def456:
|
||||
id: 'def456'
|
||||
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', '.75:abc123,1.0:def456'
|
||||
|
||||
afterEach ->
|
||||
window.YT = @originalYT
|
||||
|
||||
it 'create the Video Player', ->
|
||||
expect(window.VideoPlayer).toHaveBeenCalledWith @video
|
||||
expect(@video.player).toEqual @stubVideoPlayer
|
||||
|
||||
describe 'when the Youtube API is not ready', ->
|
||||
beforeEach ->
|
||||
@originalYT = window.YT
|
||||
window.YT = {}
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
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', '.75:abc123,1.0:def456'
|
||||
window.onYouTubePlayerAPIReady()
|
||||
|
||||
afterEach ->
|
||||
window.YT = @originalYT
|
||||
|
||||
it 'create the Video Player for all video elements', ->
|
||||
expect(window.VideoPlayer).toHaveBeenCalledWith @video
|
||||
expect(@video.player).toEqual @stubVideoPlayer
|
||||
|
||||
describe 'youtubeId', ->
|
||||
beforeEach ->
|
||||
$.cookie.andReturn '1.0'
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
describe 'with speed', ->
|
||||
it 'return the video id for given speed', ->
|
||||
expect(@video.youtubeId('0.75')).toEqual 'abc123'
|
||||
expect(@video.youtubeId('1.0')).toEqual 'def456'
|
||||
|
||||
describe 'without speed', ->
|
||||
it 'return the video id for current speed', ->
|
||||
expect(@video.youtubeId()).toEqual 'def456'
|
||||
|
||||
describe 'setSpeed', ->
|
||||
beforeEach ->
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
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', '.75:abc123,1.0:def456'
|
||||
|
||||
it 'return duration for current video', ->
|
||||
expect(@video.getDuration()).toEqual 200
|
||||
74
static/coffee/spec/navigation_spec.coffee
Normal file
74
static/coffee/spec/navigation_spec.coffee
Normal file
@@ -0,0 +1,74 @@
|
||||
describe 'Navigation', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'accordion.html'
|
||||
@navigation = new Navigation
|
||||
|
||||
describe 'constructor', ->
|
||||
describe 'when the #accordion exists', ->
|
||||
describe 'when there is an active section', ->
|
||||
beforeEach ->
|
||||
spyOn $.fn, 'accordion'
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>')
|
||||
new Navigation
|
||||
|
||||
it 'activate the accordion with correct active section', ->
|
||||
expect($('#accordion').accordion).toHaveBeenCalledWith
|
||||
active: 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
|
||||
describe 'when there is no active section', ->
|
||||
beforeEach ->
|
||||
spyOn $.fn, 'accordion'
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>')
|
||||
new Navigation
|
||||
|
||||
it 'activate the accordian with section 1 as active', ->
|
||||
expect($('#accordion').accordion).toHaveBeenCalledWith
|
||||
active: 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
|
||||
it 'binds the accordionchange event', ->
|
||||
Navigation.bind()
|
||||
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
|
||||
|
||||
it 'bind the navigation toggle', ->
|
||||
Navigation.bind()
|
||||
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
|
||||
|
||||
describe 'when the #accordion does not exists', ->
|
||||
beforeEach ->
|
||||
$('#accordion').remove()
|
||||
|
||||
it 'does not activate the accordion', ->
|
||||
spyOn $.fn, 'accordion'
|
||||
Navigation.bind()
|
||||
expect($('#accordion').accordion).wasNotCalled()
|
||||
|
||||
describe 'toggle', ->
|
||||
it 'toggle closed class on the wrapper', ->
|
||||
$('.course-wrapper').removeClass('closed')
|
||||
|
||||
@navigation.toggle()
|
||||
expect($('.course-wrapper')).toHaveClass('closed')
|
||||
|
||||
@navigation.toggle()
|
||||
expect($('.course-wrapper')).not.toHaveClass('closed')
|
||||
|
||||
describe 'log', ->
|
||||
beforeEach ->
|
||||
window.log_event = ->
|
||||
spyOn window, 'log_event'
|
||||
|
||||
it 'submit event log', ->
|
||||
@navigation.log {}, {
|
||||
newHeader:
|
||||
text: -> "new"
|
||||
oldHeader:
|
||||
text: -> "old"
|
||||
}
|
||||
|
||||
expect(window.log_event).toHaveBeenCalledWith 'accordion',
|
||||
newheader: 'new'
|
||||
oldheader: 'old'
|
||||
18
static/coffee/spec/time_spec.coffee
Normal file
18
static/coffee/spec/time_spec.coffee
Normal file
@@ -0,0 +1,18 @@
|
||||
describe 'Time', ->
|
||||
describe 'format', ->
|
||||
describe 'with duration more than or equal to 1 hour', ->
|
||||
it 'return a correct time format', ->
|
||||
expect(Time.format(3600)).toEqual '1:00:00'
|
||||
expect(Time.format(7272)).toEqual '2:01:12'
|
||||
|
||||
describe 'with duration less than 1 hour', ->
|
||||
it 'return a correct time format', ->
|
||||
expect(Time.format(1)).toEqual '0:01'
|
||||
expect(Time.format(61)).toEqual '1:01'
|
||||
expect(Time.format(3599)).toEqual '59:59'
|
||||
|
||||
describe 'convert', ->
|
||||
it 'return a correct time based on speed modifier', ->
|
||||
expect(Time.convert(0, 1, 1.5)).toEqual '0.000'
|
||||
expect(Time.convert(100, 1, 1.5)).toEqual '66.667'
|
||||
expect(Time.convert(100, 1.5, 1)).toEqual '150.000'
|
||||
35
static/coffee/src/histogram.coffee
Normal file
35
static/coffee/src/histogram.coffee
Normal file
@@ -0,0 +1,35 @@
|
||||
class @Histogram
|
||||
constructor: (@id, @rawData) ->
|
||||
@xTicks = []
|
||||
@yTicks = []
|
||||
@data = []
|
||||
@calculate()
|
||||
@render()
|
||||
|
||||
calculate: ->
|
||||
for [score, count] in @rawData
|
||||
log_count = Math.log(count + 1)
|
||||
@data.push [score, log_count]
|
||||
@xTicks.push [score, score.toString()]
|
||||
@yTicks.push [log_count, count.toString()]
|
||||
|
||||
render: ->
|
||||
$.plot $("#histogram_#{@id}"), [
|
||||
data: @data
|
||||
bars:
|
||||
show: true
|
||||
align: 'center'
|
||||
lineWidth: 0
|
||||
fill: 1.0
|
||||
color: "#b72121"
|
||||
],
|
||||
xaxis:
|
||||
min: -1
|
||||
max: Math.max.apply Math, $.map(@xTicks, (data) -> data[0] + 1)
|
||||
ticks: @xTicks
|
||||
tickLength: 0
|
||||
yaxis:
|
||||
min: 0.0
|
||||
max: Math.max.apply Math, $.map(@yTicks, (data) -> data[0] * 1.1)
|
||||
ticks: @yTicks
|
||||
labelWidth: 50
|
||||
19
static/coffee/src/logger.coffee
Normal file
19
static/coffee/src/logger.coffee
Normal file
@@ -0,0 +1,19 @@
|
||||
class @Logger
|
||||
@log: (event_type, data) ->
|
||||
$.getWithPrefix '/event',
|
||||
event_type: event_type
|
||||
event: JSON.stringify(data)
|
||||
page: window.location.href
|
||||
|
||||
@bind: ->
|
||||
window.onunload = ->
|
||||
$.ajax
|
||||
url: "#{Courseware.prefix}/event"
|
||||
data:
|
||||
event_type: 'page_close'
|
||||
event: ''
|
||||
page: window.location.href
|
||||
async: false
|
||||
|
||||
# Keeping this for conpatibility issue only.
|
||||
@log_event = Logger.log
|
||||
70
static/coffee/src/modules/problem.coffee
Normal file
70
static/coffee/src/modules/problem.coffee
Normal file
@@ -0,0 +1,70 @@
|
||||
class @Problem
|
||||
constructor: (@id, url) ->
|
||||
@element = $("#problem_#{id}")
|
||||
@content_url = "#{url}problem_get?id=#{@id}"
|
||||
@render()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @element)
|
||||
|
||||
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
|
||||
|
||||
render: (content) ->
|
||||
if content
|
||||
@element.html(content)
|
||||
@bind()
|
||||
else
|
||||
@element.load @content_url, @bind
|
||||
|
||||
check: =>
|
||||
Logger.log 'problem_check', @answers
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_check", @answers, (response) =>
|
||||
switch response.success
|
||||
when 'incorrect', 'correct'
|
||||
@render(response.contents)
|
||||
else
|
||||
alert(response.success)
|
||||
|
||||
reset: =>
|
||||
Logger.log 'problem_reset', @answers
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (content) =>
|
||||
@render(content)
|
||||
|
||||
show: =>
|
||||
if !@element.hasClass 'showed'
|
||||
Logger.log 'problem_show', problem: @id
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) =>
|
||||
$.each response, (key, value) =>
|
||||
if $.isArray(value)
|
||||
for choice in value
|
||||
@$("label[for='input_#{key}_#{choice}']").attr
|
||||
correct_answer: 'true'
|
||||
else
|
||||
@$("#answer_#{key}").text(value)
|
||||
@$('.show').val 'Hide Answer'
|
||||
@element.addClass 'showed'
|
||||
else
|
||||
@$('[id^=answer_]').text ''
|
||||
@$('[correct_answer]').attr correct_answer: null
|
||||
@element.removeClass 'showed'
|
||||
@$('.show').val 'Show Answer'
|
||||
|
||||
save: =>
|
||||
Logger.log 'problem_save', @answers
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) =>
|
||||
if response.success
|
||||
alert 'Saved'
|
||||
|
||||
refreshAnswers: =>
|
||||
@$('input.schematic').each (index, element) ->
|
||||
element.schematic.update_value()
|
||||
@$(".CodeMirror").each (index, element) ->
|
||||
element.CodeMirror.save() if element.CodeMirror.save
|
||||
@answers = @$("[id^=input_#{@id}_]").serialize()
|
||||
73
static/coffee/src/modules/sequence.coffee
Normal file
73
static/coffee/src/modules/sequence.coffee
Normal file
@@ -0,0 +1,73 @@
|
||||
class @Sequence
|
||||
constructor: (@id, @elements, @tag, position) ->
|
||||
@element = $("#sequence_#{@id}")
|
||||
@buildNavigation()
|
||||
@bind()
|
||||
@render position
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @element)
|
||||
|
||||
bind: ->
|
||||
@element.bind 'contentChanged', @toggleArrows
|
||||
@$('#sequence-list a').click @goto
|
||||
|
||||
buildNavigation: ->
|
||||
$.each @elements, (index, item) =>
|
||||
link = $('<a>').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1
|
||||
title = $('<p>').html(item.title)
|
||||
list_item = $('<li>').append(link.append(title))
|
||||
@$('#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/#{@tag}/#{@id}/goto_position", position: new_position
|
||||
|
||||
@mark_active new_position
|
||||
@$('#seq_content').html eval(@elements[new_position - 1].content)
|
||||
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub])
|
||||
@position = new_position
|
||||
@element.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) ->
|
||||
@link_for(position).attr class: "seq_#{@elements[position - 1].type}_visited"
|
||||
|
||||
mark_active: (position) ->
|
||||
@link_for(position).attr class: "seq_#{@elements[position - 1].type}_active"
|
||||
23
static/coffee/src/modules/tab.coffee
Normal file
23
static/coffee/src/modules/tab.coffee
Normal file
@@ -0,0 +1,23 @@
|
||||
class @Tab
|
||||
constructor: (@id, @items) ->
|
||||
@element = $("#tab_#{id}")
|
||||
@render()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @element)
|
||||
|
||||
render: ->
|
||||
$.each @items, (index, item) =>
|
||||
tab = $('<a>').attr(href: "##{@tabId(index)}").html(item.title)
|
||||
@$('.navigation').append($('<li>').append(tab))
|
||||
@element.append($('<section>').attr(id: @tabId(index)))
|
||||
@element.tabs
|
||||
show: @onShow
|
||||
|
||||
onShow: (element, ui) =>
|
||||
@$('section.ui-tabs-hide').html('')
|
||||
@$("##{@tabId(ui.index)}").html(eval(@items[ui.index]['content']))
|
||||
@element.trigger 'contentChanged'
|
||||
|
||||
tabId: (index) ->
|
||||
"tab-#{@id}-#{index}"
|
||||
47
static/coffee/src/modules/video.coffee
Normal file
47
static/coffee/src/modules/video.coffee
Normal file
@@ -0,0 +1,47 @@
|
||||
class @Video
|
||||
constructor: (@id, videos) ->
|
||||
window.player = null
|
||||
@element = $("#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(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
|
||||
129
static/coffee/src/modules/video/video_caption.coffee
Normal file
129
static/coffee/src/modules/video/video_caption.coffee
Normal file
@@ -0,0 +1,129 @@
|
||||
class @VideoCaption
|
||||
constructor: (@player, @youtubeId) ->
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
@player.$(selector)
|
||||
|
||||
bind: ->
|
||||
$(window).bind('resize', @onWindowResize)
|
||||
$(@player).bind('resize', @onWindowResize)
|
||||
$(@player).bind('updatePlayTime', @onUpdatePlayTime)
|
||||
@$('.hide-subtitles').click @toggle
|
||||
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
|
||||
.mousemove(@onMovement).bind('mousewheel', @onMovement)
|
||||
.bind('DOMMouseScroll', @onMovement)
|
||||
|
||||
captionURL: ->
|
||||
"/static/subs/#{@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
|
||||
@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()))
|
||||
|
||||
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
|
||||
|
||||
onUpdatePlayTime: (event, time) =>
|
||||
# This 250ms offset is required to match the video speed
|
||||
time = Math.round(Time.convert(time, @player.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()
|
||||
|
||||
onWindowResize: =>
|
||||
@$('.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 @player.isPlaying()
|
||||
|
||||
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', @player.currentSpeed()) / 1000)
|
||||
$(@player).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 @player.element.hasClass('closed')
|
||||
@$('.hide-subtitles').attr('title', 'Turn off captions')
|
||||
@player.element.removeClass('closed')
|
||||
@scrollCaption()
|
||||
else
|
||||
@$('.hide-subtitles').attr('title', 'Turn on captions')
|
||||
@player.element.addClass('closed')
|
||||
|
||||
captionHeight: ->
|
||||
if @player.element.hasClass('fullscreen')
|
||||
$(window).height() - @$('.video-controls').height()
|
||||
else
|
||||
@$('.video-wrapper').height()
|
||||
42
static/coffee/src/modules/video/video_control.coffee
Normal file
42
static/coffee/src/modules/video/video_control.coffee
Normal file
@@ -0,0 +1,42 @@
|
||||
class @VideoControl
|
||||
constructor: (@player) ->
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
@player.$(selector)
|
||||
|
||||
bind: ->
|
||||
$(@player).bind('play', @onPlay)
|
||||
.bind('pause', @onPause)
|
||||
.bind('ended', @onPause)
|
||||
@$('.video_control').click @togglePlayback
|
||||
|
||||
render: ->
|
||||
@$('.video-controls').append """
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control play">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>
|
||||
"""
|
||||
|
||||
onPlay: =>
|
||||
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
|
||||
|
||||
onPause: =>
|
||||
@$('.video_control').removeClass('pause').addClass('play').html('Play')
|
||||
|
||||
togglePlayback: (event) =>
|
||||
event.preventDefault()
|
||||
if @player.isPlaying()
|
||||
$(@player).trigger('pause')
|
||||
else
|
||||
$(@player).trigger('play')
|
||||
131
static/coffee/src/modules/video/video_player.coffee
Normal file
131
static/coffee/src/modules/video/video_player.coffee
Normal file
@@ -0,0 +1,131 @@
|
||||
class @VideoPlayer
|
||||
constructor: (@video) ->
|
||||
@currentTime = 0
|
||||
@element = $("#video_#{@video.id}")
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @element)
|
||||
|
||||
bind: ->
|
||||
$(@).bind('seek', @onSeek)
|
||||
.bind('updatePlayTime', @onUpdatePlayTime)
|
||||
.bind('speedChange', @onSpeedChange)
|
||||
.bind('play', @onPlay)
|
||||
.bind('pause', @onPause)
|
||||
.bind('ended', @onPause)
|
||||
$(document).keyup @bindExitFullScreen
|
||||
|
||||
@$('.add-fullscreen').click @toggleFullScreen
|
||||
@addToolTip() unless onTouchBasedDevice()
|
||||
|
||||
bindExitFullScreen: (event) =>
|
||||
if @element.hasClass('fullscreen') && event.keyCode == 27
|
||||
@toggleFullScreen(event)
|
||||
|
||||
render: ->
|
||||
new VideoControl @
|
||||
new VideoCaption @, @video.youtubeId('1.0')
|
||||
new VideoSpeedControl @, @video.speeds
|
||||
new VideoProgressSlider @
|
||||
@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: =>
|
||||
$(@).trigger('ready')
|
||||
$(@).trigger('updatePlayTime', 0)
|
||||
unless onTouchBasedDevice()
|
||||
$('.course-content .video:first').data('video').player.play()
|
||||
|
||||
onStateChange: (event) =>
|
||||
switch event.data
|
||||
when YT.PlayerState.PLAYING
|
||||
$(@).trigger('play')
|
||||
when YT.PlayerState.PAUSED
|
||||
$(@).trigger('pause')
|
||||
when YT.PlayerState.ENDED
|
||||
$(@).trigger('ended')
|
||||
|
||||
onPlay: =>
|
||||
Logger.log 'play_video', id: @currentTime, code: @player.getVideoEmbedCode()
|
||||
window.player.pauseVideo() if window.player && window.player != @player
|
||||
window.player = @player
|
||||
unless @player.interval
|
||||
@player.interval = setInterval(@update, 200)
|
||||
|
||||
onPause: =>
|
||||
Logger.log 'pause_video', id: @currentTime, code: @player.getVideoEmbedCode()
|
||||
window.player = null if window.player == @player
|
||||
clearInterval(@player.interval)
|
||||
@player.interval = null
|
||||
|
||||
onSeek: (event, time) ->
|
||||
@player.seekTo(time, true)
|
||||
if @isPlaying()
|
||||
clearInterval(@player.interval)
|
||||
@player.interval = setInterval(@update, 200)
|
||||
else
|
||||
@currentTime = time
|
||||
$(@).trigger('updatePlayTime', time)
|
||||
|
||||
onSpeedChange: (event, newSpeed) =>
|
||||
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
|
||||
@video.setSpeed(parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0')
|
||||
|
||||
if @isPlaying()
|
||||
@player.loadVideoById(@video.youtubeId(), @currentTime)
|
||||
else
|
||||
@player.cueVideoById(@video.youtubeId(), @currentTime)
|
||||
$(@).trigger('updatePlayTime', @currentTime)
|
||||
|
||||
update: =>
|
||||
if @currentTime = @player.getCurrentTime()
|
||||
$(@).trigger('updatePlayTime', @currentTime)
|
||||
|
||||
onUpdatePlayTime: (event, time) =>
|
||||
progress = Time.format(time) + ' / ' + Time.format(@duration())
|
||||
@$(".vidtime").html(progress)
|
||||
|
||||
toggleFullScreen: (event) =>
|
||||
event.preventDefault()
|
||||
if @element.hasClass('fullscreen')
|
||||
@$('.exit').remove()
|
||||
@$('.add-fullscreen').attr('title', 'Fill browser')
|
||||
@element.removeClass('fullscreen')
|
||||
else
|
||||
@element.append('<a href="#" class="exit">Exit</a>').addClass('fullscreen')
|
||||
@$('.add-fullscreen').attr('title', 'Exit fill browser')
|
||||
@$('.exit').click @toggleFullScreen
|
||||
$(@).trigger('resize')
|
||||
|
||||
# Delegates
|
||||
play: ->
|
||||
@player.playVideo() if @player.playVideo
|
||||
|
||||
isPlaying: ->
|
||||
@player.getPlayerState() == YT.PlayerState.PLAYING
|
||||
|
||||
pause: ->
|
||||
@player.pauseVideo()
|
||||
|
||||
duration: ->
|
||||
@video.getDuration()
|
||||
|
||||
currentSpeed: ->
|
||||
@video.speed
|
||||
54
static/coffee/src/modules/video/video_progress_slider.coffee
Normal file
54
static/coffee/src/modules/video/video_progress_slider.coffee
Normal file
@@ -0,0 +1,54 @@
|
||||
class @VideoProgressSlider
|
||||
constructor: (@player) ->
|
||||
@buildSlider()
|
||||
@buildHandle()
|
||||
$(@player).bind('updatePlayTime', @onUpdatePlayTime)
|
||||
$(@player).bind('ready', @onReady)
|
||||
|
||||
$: (selector) ->
|
||||
@player.$(selector)
|
||||
|
||||
buildSlider: ->
|
||||
@slider = @$('.slider').slider
|
||||
range: 'min'
|
||||
change: @onChange
|
||||
slide: @onSlide
|
||||
stop: @onStop
|
||||
|
||||
buildHandle: ->
|
||||
@handle = @$('.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
|
||||
|
||||
onReady: =>
|
||||
@slider.slider('option', 'max', @player.duration())
|
||||
|
||||
onUpdatePlayTime: (event, currentTime) =>
|
||||
if !@frozen
|
||||
@slider.slider('option', 'max', @player.duration())
|
||||
@slider.slider('value', currentTime)
|
||||
|
||||
onSlide: (event, ui) =>
|
||||
@frozen = true
|
||||
@updateTooltip(ui.value)
|
||||
$(@player).trigger('seek', ui.value)
|
||||
|
||||
onChange: (event, ui) =>
|
||||
@updateTooltip(ui.value)
|
||||
|
||||
onStop: (event, ui) =>
|
||||
@frozen = true
|
||||
$(@player).trigger('seek', ui.value)
|
||||
setTimeout (=> @frozen = false), 200
|
||||
|
||||
updateTooltip: (value)->
|
||||
@handle.qtip('option', 'content.text', "#{Time.format(value)}")
|
||||
48
static/coffee/src/modules/video/video_speed_control.coffee
Normal file
48
static/coffee/src/modules/video/video_speed_control.coffee
Normal file
@@ -0,0 +1,48 @@
|
||||
class @VideoSpeedControl
|
||||
constructor: (@player, @speeds) ->
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
@player.$(selector)
|
||||
|
||||
bind: ->
|
||||
$(@player).bind('speedChange', @onSpeedChange)
|
||||
@$('.video_speeds a').click @changeVideoSpeed
|
||||
if onTouchBasedDevice()
|
||||
@$('.speeds').click -> $(this).toggleClass('open')
|
||||
else
|
||||
@$('.speeds').mouseover -> $(this).addClass('open')
|
||||
@$('.speeds').mouseout -> $(this).removeClass('open')
|
||||
@$('.speeds').click (event) ->
|
||||
event.preventDefault()
|
||||
$(this).removeClass('open')
|
||||
|
||||
render: ->
|
||||
@$('.secondary-controls').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(@player.currentSpeed())
|
||||
|
||||
changeVideoSpeed: (event) =>
|
||||
event.preventDefault()
|
||||
unless $(event.target).parent().hasClass('active')
|
||||
$(@player).trigger 'speedChange', $(event.target).parent().data('speed')
|
||||
|
||||
onSpeedChange: (event, speed) =>
|
||||
@setSpeed(parseFloat(speed).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")
|
||||
19
static/coffee/src/navigation.coffee
Normal file
19
static/coffee/src/navigation.coffee
Normal file
@@ -0,0 +1,19 @@
|
||||
class @Navigation
|
||||
constructor: ->
|
||||
if $('#accordion').length
|
||||
active = $('#accordion ul:has(li.active)').index('#accordion ul')
|
||||
$('#accordion').bind('accordionchange', @log).accordion
|
||||
active: if active >= 0 then active else 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
$('#open_close_accordion a').click @toggle
|
||||
|
||||
$('#accordion').show()
|
||||
|
||||
log: (event, ui) ->
|
||||
log_event 'accordion',
|
||||
newheader: ui.newHeader.text()
|
||||
oldheader: ui.oldHeader.text()
|
||||
|
||||
toggle: ->
|
||||
$('.course-wrapper').toggleClass('closed')
|
||||
17
static/coffee/src/time.coffee
Normal file
17
static/coffee/src/time.coffee
Normal file
@@ -0,0 +1,17 @@
|
||||
class @Time
|
||||
@format: (time) ->
|
||||
pad = (number) -> if number < 10 then "0#{number}" else number
|
||||
|
||||
seconds = Math.floor time
|
||||
minutes = Math.floor seconds / 60
|
||||
hours = Math.floor minutes / 60
|
||||
seconds = seconds % 60
|
||||
minutes = minutes % 60
|
||||
|
||||
if hours
|
||||
"#{hours}:#{pad(minutes)}:#{pad(seconds % 60)}"
|
||||
else
|
||||
"#{minutes}:#{pad(seconds % 60)}"
|
||||
|
||||
@convert: (time, oldSpeed, newSpeed) ->
|
||||
(time * oldSpeed / newSpeed).toFixed(3)
|
||||
@@ -184,7 +184,18 @@ section.course-content {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
&.open {
|
||||
&>a {
|
||||
background: url('../images/open-arrow.png') 10px center no-repeat;
|
||||
}
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&>a {
|
||||
background: url('../images/closed-arrow.png') 10px center no-repeat;
|
||||
border-left: 1px solid #000;
|
||||
border-right: 1px solid #000;
|
||||
@@ -201,15 +212,6 @@ section.course-content {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 110px;
|
||||
|
||||
&.open {
|
||||
background: url('../images/open-arrow.png') 10px center no-repeat;
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #999;
|
||||
float: left;
|
||||
|
||||
Reference in New Issue
Block a user