Commit 97c6a26c by Piotr Mitros

Merge master

parents 34dc32e5 585e4c8d
...@@ -33,7 +33,7 @@ class Command(BaseCommand): ...@@ -33,7 +33,7 @@ class Command(BaseCommand):
ajax_url='', ajax_url='',
state=None, state=None,
track_function = lambda x,y,z:None, track_function = lambda x,y,z:None,
render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'}) render_function = lambda x: {'content':'','type':'video'})
except: except:
print "==============> Error in ", etree.tostring(module) print "==============> Error in ", etree.tostring(module)
check = False check = False
......
import json
import logging import logging
from lxml import etree from lxml import etree
...@@ -138,8 +139,6 @@ def render_x_module(user, request, xml_module, module_object_preload): ...@@ -138,8 +139,6 @@ def render_x_module(user, request, xml_module, module_object_preload):
# Grab content # Grab content
content = instance.get_html() content = instance.get_html()
init_js = instance.get_init_js()
destory_js = instance.get_destroy_js()
# special extra information about each problem, only for users who are staff # special extra information about each problem, only for users who are staff
if user.is_staff: if user.is_staff:
...@@ -147,14 +146,10 @@ def render_x_module(user, request, xml_module, module_object_preload): ...@@ -147,14 +146,10 @@ def render_x_module(user, request, xml_module, module_object_preload):
render_histogram = len(histogram) > 0 render_histogram = len(histogram) > 0
content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module), content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module),
'module_id' : module_id, 'module_id' : module_id,
'histogram': json.dumps(histogram),
'render_histogram' : render_histogram}) 'render_histogram' : render_histogram})
if render_histogram:
init_js = init_js+render_to_string("staff_problem_histogram.js", {'histogram' : histogram, content = {'content':content,
'module_id' : module_id})
content = {'content':content,
"destroy_js":destory_js,
'init_js':init_js,
'type':module_type} 'type':module_type}
return content return content
......
...@@ -64,12 +64,6 @@ class Module(XModule): ...@@ -64,12 +64,6 @@ class Module(XModule):
'ajax_url':self.ajax_url, 'ajax_url':self.ajax_url,
}) })
def get_init_js(self):
return render_to_string('problem.js',
{'id':self.item_id,
'ajax_url':self.ajax_url,
})
def get_problem_html(self, encapsulate=True): def get_problem_html(self, encapsulate=True):
html = self.lcp.get_html() html = self.lcp.get_html()
content={'name':self.name, content={'name':self.name,
...@@ -133,8 +127,8 @@ class Module(XModule): ...@@ -133,8 +127,8 @@ class Module(XModule):
html=render_to_string('problem.html', context) html=render_to_string('problem.html', context)
if encapsulate: if encapsulate:
html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>" html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(id=self.item_id)+html+"</div>"
return html return html
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, xml, item_id, state=None):
......
...@@ -31,14 +31,6 @@ class Module(XModule): ...@@ -31,14 +31,6 @@ class Module(XModule):
self.render() self.render()
return self.content return self.content
def get_init_js(self):
self.render()
return self.init_js
def get_destroy_js(self):
self.render()
return self.destroy_js
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
if dispatch=='goto_position': if dispatch=='goto_position':
self.position = int(get['position']) self.position = int(get['position'])
...@@ -48,44 +40,39 @@ class Module(XModule): ...@@ -48,44 +40,39 @@ class Module(XModule):
def render(self): def render(self):
if self.rendered: if self.rendered:
return return
def j(m): def j(m):
''' jsonify contents so it can be embedded in a js array ''' jsonify contents so it can be embedded in a js array
We also need to split </script> tags so they don't break We also need to split </script> tags so they don't break
mid-string''' mid-string'''
if 'init_js' not in m: m['init_js']="" content=json.dumps(m['content'])
if 'type' not in m: m['init_js']="" content=content.replace('</script>', '<"+"/script>')
content=json.dumps(m['content'])
content=content.replace('</script>', '<"+"/script>') return {'content':content,
return {'content':content,
"destroy_js":m['destroy_js'],
'init_js':m['init_js'],
'type': m['type']} 'type': m['type']}
## Returns a set of all types of all sub-children ## Returns a set of all types of all sub-children
child_classes = [set([i.tag for i in e.iter()]) for e in self.xmltree] child_classes = [set([i.tag for i in e.iter()]) for e in self.xmltree]
self.titles = json.dumps(["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \ titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \
for e in self.xmltree]) for e in self.xmltree]
self.contents = self.rendered_children(self) self.contents = self.rendered_children(self)
print self.titles for contents, title in zip(self.contents, titles):
contents['title'] = title
for (content, element_class) in zip(self.contents, child_classes): for (content, element_class) in zip(self.contents, child_classes):
new_class = 'other' new_class = 'other'
for c in class_priority: for c in class_priority:
if c in element_class: if c in element_class:
new_class = c new_class = c
content['type'] = new_class content['type'] = new_class
js=""
params={'items':self.contents, params={'items':self.contents,
'id':self.item_id, 'id':self.item_id,
'position': self.position, 'position': self.position,
'titles':self.titles, 'titles':titles,
'tag':self.xmltree.tag} 'tag':self.xmltree.tag}
# TODO/BUG: Destroy JavaScript should only be called for the active view # TODO/BUG: Destroy JavaScript should only be called for the active view
...@@ -96,16 +83,10 @@ class Module(XModule): ...@@ -96,16 +83,10 @@ class Module(XModule):
destroy_js="".join([e['destroy_js'] for e in self.contents if 'destroy_js' in e]) destroy_js="".join([e['destroy_js'] for e in self.contents if 'destroy_js' in e])
if self.xmltree.tag in ['sequential', 'videosequence']: if self.xmltree.tag in ['sequential', 'videosequence']:
self.init_js=js+render_to_string('seq_module.js',params)
self.destroy_js=destroy_js
self.content=render_to_string('seq_module.html',params) self.content=render_to_string('seq_module.html',params)
if self.xmltree.tag == 'tab': if self.xmltree.tag == 'tab':
params['id'] = 'tab'
self.init_js=js+render_to_string('tab_module.js',params)
self.destroy_js=destroy_js
self.content=render_to_string('tab_module.html',params) self.content=render_to_string('tab_module.html',params)
self.rendered = True self.rendered = True
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, xml, item_id, state)
......
...@@ -21,17 +21,8 @@ class Module(XModule): ...@@ -21,17 +21,8 @@ class Module(XModule):
def get_html(self): def get_html(self):
return render_to_string('vert_module.html',{'items':self.contents}) return render_to_string('vert_module.html',{'items':self.contents})
def get_init_js(self):
return self.init_js_text
def get_destroy_js(self):
return self.destroy_js_text
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml) xmltree=etree.fromstring(xml)
self.contents=[(e.get("name"),self.render_function(e)) \ self.contents=[(e.get("name"),self.render_function(e)) \
for e in xmltree] for e in xmltree]
self.init_js_text="".join([e[1]['init_js'] for e in self.contents if 'init_js' in e[1]])
self.destroy_js_text="".join([e[1]['destroy_js'] for e in self.contents if 'destroy_js' in e[1]])
...@@ -33,32 +33,17 @@ class Module(XModule): ...@@ -33,32 +33,17 @@ class Module(XModule):
def get_xml_tags(c): def get_xml_tags(c):
'''Tags in the courseware file guaranteed to correspond to the module''' '''Tags in the courseware file guaranteed to correspond to the module'''
return ["video"] return ["video"]
def video_list(self): def video_list(self):
l = self.youtube.split(',') return self.youtube
l = [i.split(":") for i in l]
return json.dumps(dict(l))
def get_html(self): def get_html(self):
return render_to_string('video.html',{'streams':self.video_list(), return render_to_string('video.html',{'streams':self.video_list(),
'id':self.item_id, 'id':self.item_id,
'position':self.position, 'position':self.position,
'name':self.name, 'name':self.name,
'annotations':self.annotations}) 'annotations':self.annotations})
def get_init_js(self):
'''JavaScript code to be run when problem is shown. Be aware
that this may happen several times on the same page
(e.g. student switching tabs). Common functions should be put
in the main course .js files for now. '''
log.debug(u"INIT POSITION {0}".format(self.position))
return render_to_string('video_init.js',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position})+self.annotations_init
def get_destroy_js(self):
return "videoDestroy(\"{0}\");".format(self.item_id)+self.annotations_destroy
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml) xmltree=etree.fromstring(xml)
...@@ -72,5 +57,3 @@ class Module(XModule): ...@@ -72,5 +57,3 @@ class Module(XModule):
self.annotations=[(e.get("name"),self.render_function(e)) \ self.annotations=[(e.get("name"),self.render_function(e)) \
for e in xmltree] for e in xmltree]
self.annotations_init="".join([e[1]['init_js'] for e in self.annotations if 'init_js' in e[1]])
self.annotations_destroy="".join([e[1]['destroy_js'] for e in self.annotations if 'destroy_js' in e[1]])
...@@ -95,25 +95,6 @@ class XModule(object): ...@@ -95,25 +95,6 @@ class XModule(object):
''' '''
return "Unimplemented" return "Unimplemented"
# TODO:
# def get_header_js(self):
# ''' Filename of common js that needs to be included in the header
# '''
# raise NotImplementedError
def get_init_js(self):
''' JavaScript code to be run when problem is shown. Be aware
that this may happen several times on the same page
(e.g. student switching tabs). Common functions should be put
in the main course .js files for now. '''
return ""
def get_destroy_js(self):
''' JavaScript called to destroy the problem (e.g. when a user switches to a different tab).
We make an attempt, but not a promise, to call this when the user closes the web page.
'''
return ""
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' dispatch is last part of the URL. ''' dispatch is last part of the URL.
get is a dictionary-like object ''' get is a dictionary-like object '''
......
...@@ -20,6 +20,7 @@ Longer TODO: ...@@ -20,6 +20,7 @@ Longer TODO:
""" """
import sys import sys
import tempfile import tempfile
import glob2
import djcelery import djcelery
from path import path from path import path
...@@ -285,13 +286,12 @@ PIPELINE_CSS = { ...@@ -285,13 +286,12 @@ PIPELINE_CSS = {
PIPELINE_JS = { PIPELINE_JS = {
'application': { 'application': {
'source_filenames': [ 'source_filenames': [path.replace('static/', '') for path in glob2.glob('static/coffee/src/**/*.coffee')],
'coffee/src/calculator.coffee',
'coffee/src/courseware.coffee',
'coffee/src/feedback_form.coffee',
'coffee/src/main.coffee'
],
'output_filename': 'js/application.js' 'output_filename': 'js/application.js'
},
'spec': {
'source_filenames': [path.replace('static/', '') for path in glob2.glob('static/coffee/spec/**/*.coffee')],
'output_filename': 'js/spec.js'
} }
} }
......
...@@ -80,7 +80,7 @@ task :package do ...@@ -80,7 +80,7 @@ task :package do
chown -R makeitso:makeitso #{INSTALL_DIR_PATH} chown -R makeitso:makeitso #{INSTALL_DIR_PATH}
chmod +x #{INSTALL_DIR_PATH}/collect_static_resources chmod +x #{INSTALL_DIR_PATH}/collect_static_resources
service gunicorn stop service gunicorn stop || echo "Unable to stop gunicorn. Continuing"
rm -f #{LINK_PATH} rm -f #{LINK_PATH}
ln -s #{INSTALL_DIR_PATH} #{LINK_PATH} ln -s #{INSTALL_DIR_PATH} #{LINK_PATH}
chown makeitso:makeitso #{LINK_PATH} chown makeitso:makeitso #{LINK_PATH}
......
...@@ -21,3 +21,4 @@ django-jasmine ...@@ -21,3 +21,4 @@ django-jasmine
beautifulsoup beautifulsoup
requests requests
newrelic newrelic
glob2
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
"js_files": [ "js_files": [
"/static/js/jquery-1.6.2.min.js", "/static/js/jquery-1.6.2.min.js",
"/static/js/jquery-ui-1.8.16.custom.min.js", "/static/js/jquery-ui-1.8.16.custom.min.js",
"/static/js/jquery.leanModal.js" "/static/js/jquery.leanModal.js",
"/static/js/flot/jquery.flot.js"
], ],
"static_files": [ "static_files": [
"js/application.js" "js/application.js"
......
[
{
"content": "\"Video 1\"",
"type": "video",
"title": "Video 1"
}, {
"content": "\"Video 2\"",
"type": "video",
"title": "Video 2"
}, {
"content": "\"Sample Problem\"",
"type": "problem",
"title": "Sample Problem"
}
]
<section id="problem_1" class="problems-wrapper" data-url="/problem/url/"></section>
<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>
<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>
<div id="tab_1" class="tab">
<ul class="navigation"></ul>
</div>
<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>
...@@ -24,6 +24,7 @@ describe 'Calculator', -> ...@@ -24,6 +24,7 @@ describe 'Calculator', ->
expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate
it 'prevent default behavior on form submit', -> it 'prevent default behavior on form submit', ->
jasmine.stubRequests()
$('form#calculator').submit (e) -> $('form#calculator').submit (e) ->
expect(e.isDefaultPrevented()).toBeTruthy() expect(e.isDefaultPrevented()).toBeTruthy()
e.preventDefault() e.preventDefault()
...@@ -55,12 +56,12 @@ describe 'Calculator', -> ...@@ -55,12 +56,12 @@ describe 'Calculator', ->
describe 'calculate', -> describe 'calculate', ->
beforeEach -> beforeEach ->
$('#calculator_input').val '1+2' $('#calculator_input').val '1+2'
spyOn($, 'getJSON').andCallFake (url, data, callback) -> spyOn($, 'getWithPrefix').andCallFake (url, data, callback) ->
callback({ result: 3 }) callback({ result: 3 })
@calculator.calculate() @calculator.calculate()
it 'send data to /calculate', -> it 'send data to /calculate', ->
expect($.getJSON).toHaveBeenCalledWith '/calculate', expect($.getWithPrefix).toHaveBeenCalledWith '/calculate',
equation: '1+2' equation: '1+2'
, jasmine.any(Function) , jasmine.any(Function)
......
(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() {
$('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($, 'getJSON').andCallFake(function(url, data, callback) {
return callback({
result: 3
});
});
return this.calculator.calculate();
});
it('send data to /calculate', function() {
return expect($.getJSON).toHaveBeenCalledWith('/calculate', {
equation: '1+2'
}, jasmine.any(Function));
});
return it('update the calculator output', function() {
return expect($('#calculator_output').val()).toEqual('3');
});
});
});
}).call(this);
describe 'Courseware', -> describe 'Courseware', ->
describe 'bind', -> describe 'start', ->
it 'bind the navigation', -> it 'create the navigation', ->
spyOn Courseware.Navigation, 'bind' spyOn(window, 'Navigation')
Courseware.bind() Courseware.start()
expect(Courseware.Navigation.bind).toHaveBeenCalled() expect(window.Navigation).toHaveBeenCalled()
it 'create the calculator', ->
spyOn(window, 'Calculator')
Courseware.start()
expect(window.Calculator).toHaveBeenCalled()
it 'creates the FeedbackForm', ->
spyOn(window, 'FeedbackForm')
Courseware.start()
expect(window.FeedbackForm).toHaveBeenCalled()
it 'binds the Logger', ->
spyOn(Logger, 'bind')
Courseware.start()
expect(Logger.bind).toHaveBeenCalled()
describe 'Navigation', -> describe 'bind', ->
beforeEach -> beforeEach ->
loadFixtures 'accordion.html' @courseware = new Courseware
@navigation = new Courseware.Navigation setFixtures """
<div class="course-content">
describe 'bind', -> <div class="sequence"></div>
describe 'when the #accordion exists', -> </div>
describe 'when there is an active section', -> """
it 'activate the accordion with correct active section', ->
spyOn $.fn, 'accordion' it 'binds the content change event', ->
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>') @courseware.bind()
Courseware.Navigation.bind() expect($('.course-content .sequence')).toHandleWith 'contentChanged', @courseware.render
expect($('#accordion').accordion).toHaveBeenCalledWith
active: 1 describe 'render', ->
header: 'h3' beforeEach ->
autoHeight: false jasmine.stubRequests()
@courseware = new Courseware
describe 'when there is no active section', -> spyOn(window, 'Histogram')
it 'activate the accordian with section 1 as active', -> spyOn(window, 'Problem')
spyOn $.fn, 'accordion' spyOn(window, 'Video')
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>') setFixtures """
Courseware.Navigation.bind() <div class="course-content">
expect($('#accordion').accordion).toHaveBeenCalledWith <div id="video_1" class="video" data-streams="1.0:abc1234"></div>
active: 1 <div id="video_2" class="video" data-streams="1.0:def5678"></div>
header: 'h3' <div id="problem_3" class="problems-wrapper" data-url="/example/url/">
autoHeight: false <div id="histogram_3" class="histogram" data-histogram="[[0, 1]]" style="height: 20px; display: block;">
</div>
it 'binds the accordionchange event', -> </div>
Courseware.Navigation.bind() """
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log @courseware.render()
it 'bind the navigation toggle', -> it 'detect the video elements and convert them', ->
Courseware.Navigation.bind() expect(window.Video).toHaveBeenCalledWith('1', '1.0:abc1234')
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle expect(window.Video).toHaveBeenCalledWith('2', '1.0:def5678')
describe 'when the #accordion does not exists', -> it 'detect the problem element and convert it', ->
beforeEach -> expect(window.Problem).toHaveBeenCalledWith('3', '/example/url/')
$('#accordion').remove()
it 'detect the histrogram element and convert it', ->
it 'does not activate the accordion', -> expect(window.Histogram).toHaveBeenCalledWith('3', [[0, 1]])
spyOn $.fn, 'accordion'
Courseware.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'
(function() {
describe('Courseware', function() {
describe('bind', function() {
return it('bind the navigation', function() {
spyOn(Courseware.Navigation, 'bind');
Courseware.bind();
return expect(Courseware.Navigation.bind).toHaveBeenCalled();
});
});
return describe('Navigation', function() {
beforeEach(function() {
loadFixtures('accordion.html');
return this.navigation = new Courseware.Navigation;
});
describe('bind', function() {
describe('when the #accordion exists', function() {
describe('when there is an active section', function() {
return it('activate the accordion with correct active section', function() {
spyOn($.fn, 'accordion');
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>');
Courseware.Navigation.bind();
return expect($('#accordion').accordion).toHaveBeenCalledWith({
active: 1,
header: 'h3',
autoHeight: false
});
});
});
describe('when there is no active section', function() {
return it('activate the accordian with section 1 as active', function() {
spyOn($.fn, 'accordion');
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>');
Courseware.Navigation.bind();
return expect($('#accordion').accordion).toHaveBeenCalledWith({
active: 1,
header: 'h3',
autoHeight: false
});
});
});
it('binds the accordionchange event', function() {
Courseware.Navigation.bind();
return expect($('#accordion')).toHandleWith('accordionchange', this.navigation.log);
});
return it('bind the navigation toggle', function() {
Courseware.Navigation.bind();
return expect($('#open_close_accordion a')).toHandleWith('click', this.navigation.toggle);
});
});
return describe('when the #accordion does not exists', function() {
beforeEach(function() {
return $('#accordion').remove();
});
return it('does not activate the accordion', function() {
spyOn($.fn, 'accordion');
Courseware.Navigation.bind();
return expect($('#accordion').accordion).wasNotCalled();
});
});
});
describe('toggle', function() {
return it('toggle closed class on the wrapper', function() {
$('.course-wrapper').removeClass('closed');
this.navigation.toggle();
expect($('.course-wrapper')).toHaveClass('closed');
this.navigation.toggle();
return expect($('.course-wrapper')).not.toHaveClass('closed');
});
});
return describe('log', function() {
beforeEach(function() {
window.log_event = function() {};
return spyOn(window, 'log_event');
});
return it('submit event log', function() {
this.navigation.log({}, {
newHeader: {
text: function() {
return "new";
}
},
oldHeader: {
text: function() {
return "old";
}
}
});
return expect(window.log_event).toHaveBeenCalledWith('accordion', {
newheader: 'new',
oldheader: 'old'
});
});
});
});
});
}).call(this);
...@@ -2,10 +2,10 @@ describe 'FeedbackForm', -> ...@@ -2,10 +2,10 @@ describe 'FeedbackForm', ->
beforeEach -> beforeEach ->
loadFixtures 'feedback_form.html' loadFixtures 'feedback_form.html'
describe 'bind', -> describe 'constructor', ->
beforeEach -> beforeEach ->
FeedbackForm.bind() new FeedbackForm
spyOn($, 'post').andCallFake (url, data, callback, format) -> spyOn($, 'postWithPrefix').andCallFake (url, data, callback, format) ->
callback() callback()
it 'binds to the #feedback_button', -> it 'binds to the #feedback_button', ->
...@@ -16,7 +16,7 @@ describe 'FeedbackForm', -> ...@@ -16,7 +16,7 @@ describe 'FeedbackForm', ->
$('#feedback_message').val 'This site is really good.' $('#feedback_message').val 'This site is really good.'
$('#feedback_button').click() $('#feedback_button').click()
expect($.post).toHaveBeenCalledWith '/send_feedback', { expect($.postWithPrefix).toHaveBeenCalledWith '/send_feedback', {
subject: 'Awesome!' subject: 'Awesome!'
message: 'This site is really good.' message: 'This site is really good.'
url: window.location.href url: window.location.href
......
(function() {
describe('FeedbackForm', function() {
beforeEach(function() {
return loadFixtures('feedback_form.html');
});
return describe('bind', function() {
beforeEach(function() {
FeedbackForm.bind();
return spyOn($, 'post').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($.post).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);
jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/" 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 = ->
spyOn($, 'ajax').andCallFake (settings) ->
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
settings.success data: jasmine.stubbedMetadata[match[1]]
else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/
settings.success jasmine.stubbedCaption
else if settings.url == '/calculate' ||
settings.url == '/6002x/modx/sequence/1/goto_position' ||
settings.url.match(/event$/) ||
settings.url.match(/6002x\/modx\/problem\/.+\/problem_(check|reset|show|save)$/)
# do nothing
else
throw "External request attempted for #{settings.url}, which is not defined."
jasmine.stubYoutubePlayer = ->
YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
'getCurrentTime', 'getPlayerState', 'loadVideoById', 'playVideo', 'pauseVideo', 'seekTo']
jasmine.stubVideoPlayer = (context, enableParts) ->
enableParts = [enableParts] unless $.isArray(enableParts)
suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite
enableParts.push currentPartName
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoProgressSlider']
unless $.inArray(part, enableParts) >= 0
spyOn window, part
loadFixtures 'video.html'
jasmine.stubRequests()
YT.Player = undefined
context.video = new Video 'example', '.75:abc123,1.0:def456'
jasmine.stubYoutubePlayer()
return new VideoPlayer context.video
spyOn(window, 'onunload')
# Stub Youtube API
window.YT =
PlayerState:
UNSTARTED: -1
ENDED: 0
PLAYING: 1
PAUSED: 2
BUFFERING: 3
CUED: 5
# Stub jQuery.cookie
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0'
# Stub jQuery.qtip
$.fn.qtip = jasmine.createSpy 'jQuery.qtip'
# Stub jQuery.scrollTo
$.fn.scrollTo = jasmine.createSpy 'jQuery.scrollTo'
(function() {
jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/";
}).call(this);
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
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
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"
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, 'sequence', 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, 'sequence', 1
describe 'when the first tab is active', ->
beforeEach ->
@sequence.position = 1
@sequence.toggleArrows()
it 'disable the previous button', ->
expect($('.sequence-nav-buttons .prev a')).toHaveClass 'disabled'
it 'enable the next button', ->
expect($('.sequence-nav-buttons .next a')).not.toHaveClass 'disabled'
expect($('.sequence-nav-buttons .next a')).toHandleWith 'click', @sequence.next
describe 'when the middle tab is active', ->
beforeEach ->
@sequence.position = 2
@sequence.toggleArrows()
it 'enable the previous button', ->
expect($('.sequence-nav-buttons .prev a')).not.toHaveClass 'disabled'
expect($('.sequence-nav-buttons .prev a')).toHandleWith 'click', @sequence.previous
it 'enable the next button', ->
expect($('.sequence-nav-buttons .next a')).not.toHaveClass 'disabled'
expect($('.sequence-nav-buttons .next a')).toHandleWith 'click', @sequence.next
describe 'when the last tab is active', ->
beforeEach ->
@sequence.position = 3
@sequence.toggleArrows()
it 'enable the previous button', ->
expect($('.sequence-nav-buttons .prev a')).not.toHaveClass 'disabled'
expect($('.sequence-nav-buttons .prev a')).toHandleWith 'click', @sequence.previous
it 'disable the next button', ->
expect($('.sequence-nav-buttons .next a')).toHaveClass 'disabled'
describe 'render', ->
beforeEach ->
spyOn $, 'postWithPrefix'
@sequence = new Sequence '1', @items, 'sequence'
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/sequence/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, 'sequence', 2
$('[data-element="3"]').click()
it 'log the sequence goto event', ->
expect(Logger.log).toHaveBeenCalledWith 'seq_goto', old: 2, new: 3, id: '1'
it 'call render on the right sequence', ->
expect($('#seq_content').html()).toEqual 'Sample Problem'
describe 'next', ->
beforeEach ->
jasmine.stubRequests()
@sequence = new Sequence '1', @items, 'sequence', 2
$('.sequence-nav-buttons .next a').click()
it 'log the next sequence event', ->
expect(Logger.log).toHaveBeenCalledWith 'seq_next', old: 2, new: 3, id: '1'
it 'call render on the next sequence', ->
expect($('#seq_content').html()).toEqual 'Sample Problem'
describe 'previous', ->
beforeEach ->
jasmine.stubRequests()
@sequence = new Sequence '1', @items, 'sequence', 2
$('.sequence-nav-buttons .prev a').click()
it 'log the previous sequence event', ->
expect(Logger.log).toHaveBeenCalledWith 'seq_prev', old: 2, new: 1, id: '1'
it 'call render on the previous sequence', ->
expect($('#seq_content').html()).toEqual 'Video 1'
describe 'link_for', ->
it 'return a link for specific position', ->
sequence = new Sequence '1', @items, 2
expect(sequence.link_for(2)).toBe '[data-element="2"]'
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
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()
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
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'
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'
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').mouseenter()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on mouse out', ->
$('.speeds').mouseenter().mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on click', ->
$('.speeds').mouseenter().click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'changeVideoSpeed', ->
beforeEach ->
@speedControl = new VideoSpeedControl @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'
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
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'
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'
class window.Calculator class @Calculator
@bind: -> constructor: ->
calculator = new Calculator $('.calc').click @toggle
$('.calc').click calculator.toggle $('form#calculator').submit(@calculate).submit (e) ->
$('form#calculator').submit(calculator.calculate).submit (e) ->
e.preventDefault() e.preventDefault()
$('div.help-wrapper a').hover(calculator.helpToggle).click (e) -> $('div.help-wrapper a').hover(@helpToggle).click (e) ->
e.preventDefault() e.preventDefault()
toggle: -> toggle: ->
...@@ -21,5 +20,5 @@ class window.Calculator ...@@ -21,5 +20,5 @@ class window.Calculator
$('.help').toggleClass 'shown' $('.help').toggleClass 'shown'
calculate: -> calculate: ->
$.getJSON '/calculate', { equation: $('#calculator_input').val() }, (data) -> $.getWithPrefix '/calculate', { equation: $('#calculator_input').val() }, (data) ->
$('#calculator_output').val(data.result) $('#calculator_output').val(data.result)
class window.Courseware class @Courseware
@bind: -> @prefix: ''
@Navigation.bind()
class @Navigation constructor: ->
@bind: -> Courseware.prefix = $("meta[name='path_prefix']").attr('content')
if $('#accordion').length new Navigation
navigation = new Navigation new Calculator
active = $('#accordion ul:has(li.active)').index('#accordion ul') new FeedbackForm
$('#accordion').bind('accordionchange', navigation.log).accordion Logger.bind()
active: if active >= 0 then active else 1 @bind()
header: 'h3' @render()
autoHeight: false
$('#open_close_accordion a').click navigation.toggle
log: (event, ui) -> @start: ->
log_event 'accordion', new Courseware
newheader: ui.newHeader.text()
oldheader: ui.oldHeader.text()
toggle: -> bind: ->
$('.course-wrapper').toggleClass('closed') $('.course-content .sequence, .course-content .tab')
.bind 'contentChanged', @render
render: ->
$('.course-content .video').each ->
id = $(this).attr('id').replace(/video_/, '')
new Video id, $(this).data('streams')
$('.course-content .problems-wrapper').each ->
id = $(this).attr('id').replace(/problem_/, '')
new Problem id, $(this).data('url')
$('.course-content .histogram').each ->
id = $(this).attr('id').replace(/histogram_/, '')
new Histogram id, $(this).data('histogram')
class window.FeedbackForm class @FeedbackForm
@bind: -> constructor: ->
$('#feedback_button').click -> $('#feedback_button').click ->
data = data =
subject: $('#feedback_subject').val() subject: $('#feedback_subject').val()
message: $('#feedback_message').val() message: $('#feedback_message').val()
url: window.location.href url: window.location.href
$.post '/send_feedback', data, -> $.postWithPrefix '/send_feedback', data, ->
$('#feedback_div').html 'Feedback submitted. Thank you' $('#feedback_div').html 'Feedback submitted. Thank you'
,'json' ,'json'
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
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
jQuery.postWithPrefix = (url, data, callback, type) ->
$.post("#{Courseware.prefix}#{url}", data, callback, type)
jQuery.getWithPrefix = (url, data, callback, type) ->
$.get("#{Courseware.prefix}#{url}", data, callback, type)
$ -> $ ->
$.ajaxSetup $.ajaxSetup
headers : { 'X-CSRFToken': $.cookie 'csrftoken' } headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
dataType: 'json'
window.onTouchBasedDevice = ->
navigator.userAgent.match /iPhone|iPod|iPad/i
Calculator.bind()
Courseware.bind()
FeedbackForm.bind()
$("a[rel*=leanModal]").leanModal() $("a[rel*=leanModal]").leanModal()
$('#csrfmiddlewaretoken').attr 'value', $.cookie('csrftoken')
if $('body').hasClass('courseware')
Courseware.start()
# Preserved for backward compatibility
window.submit_circuit = (circuit_id) ->
$("input.schematic").each (index, element) ->
element.schematic.update_value()
schematic_value $("#schematic_#{circuit_id}").attr("value")
$.postWithPrefix "/save_circuit/#{circuit_id}", schematic: schematic_value, (data) ->
alert('Saved') if data.results == 'success'
window.postJSON = (url, data, callback) ->
$.postWithPrefix url, data, callback
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()
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"
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}"
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
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()
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')
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
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)}")
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').mouseenter ->
$(this).addClass('open')
@$('.speeds').mouseleave ->
$(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")
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')
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)
CodeMirror.defineMode("python", function(conf, parserConf) {
var ERRORCLASS = 'error';
function wordRegexp(words) {
return new RegExp("^((" + words.join(")|(") + "))\\b");
}
var singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!]");
var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]');
var doubleOperators = new RegExp("^((==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))");
var doubleDelimiters = new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))");
var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))");
var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
var wordOperators = wordRegexp(['and', 'or', 'not', 'is', 'in']);
var commonkeywords = ['as', 'assert', 'break', 'class', 'continue',
'def', 'del', 'elif', 'else', 'except', 'finally',
'for', 'from', 'global', 'if', 'import',
'lambda', 'pass', 'raise', 'return',
'try', 'while', 'with', 'yield'];
var commonBuiltins = ['abs', 'all', 'any', 'bin', 'bool', 'bytearray', 'callable', 'chr',
'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
'enumerate', 'eval', 'filter', 'float', 'format', 'frozenset',
'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id',
'input', 'int', 'isinstance', 'issubclass', 'iter', 'len',
'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next',
'object', 'oct', 'open', 'ord', 'pow', 'property', 'range',
'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple',
'type', 'vars', 'zip', '__import__', 'NotImplemented',
'Ellipsis', '__debug__'];
var py2 = {'builtins': ['apply', 'basestring', 'buffer', 'cmp', 'coerce', 'execfile',
'file', 'intern', 'long', 'raw_input', 'reduce', 'reload',
'unichr', 'unicode', 'xrange', 'False', 'True', 'None'],
'keywords': ['exec', 'print']};
var py3 = {'builtins': ['ascii', 'bytes', 'exec', 'print'],
'keywords': ['nonlocal', 'False', 'True', 'None']};
if (!!parserConf.version && parseInt(parserConf.version, 10) === 3) {
commonkeywords = commonkeywords.concat(py3.keywords);
commonBuiltins = commonBuiltins.concat(py3.builtins);
var stringPrefixes = new RegExp("^(([rb]|(br))?('{3}|\"{3}|['\"]))", "i");
} else {
commonkeywords = commonkeywords.concat(py2.keywords);
commonBuiltins = commonBuiltins.concat(py2.builtins);
var stringPrefixes = new RegExp("^(([rub]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i");
}
var keywords = wordRegexp(commonkeywords);
var builtins = wordRegexp(commonBuiltins);
var indentInfo = null;
// tokenizers
function tokenBase(stream, state) {
// Handle scope changes
if (stream.sol()) {
var scopeOffset = state.scopes[0].offset;
if (stream.eatSpace()) {
var lineOffset = stream.indentation();
if (lineOffset > scopeOffset) {
indentInfo = 'indent';
} else if (lineOffset < scopeOffset) {
indentInfo = 'dedent';
}
return null;
} else {
if (scopeOffset > 0) {
dedent(stream, state);
}
}
}
if (stream.eatSpace()) {
return null;
}
var ch = stream.peek();
// Handle Comments
if (ch === '#') {
stream.skipToEnd();
return 'comment';
}
// Handle Number Literals
if (stream.match(/^[0-9\.]/, false)) {
var floatLiteral = false;
// Floats
if (stream.match(/^\d*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; }
if (stream.match(/^\d+\.\d*/)) { floatLiteral = true; }
if (stream.match(/^\.\d+/)) { floatLiteral = true; }
if (floatLiteral) {
// Float literals may be "imaginary"
stream.eat(/J/i);
return 'number';
}
// Integers
var intLiteral = false;
// Hex
if (stream.match(/^0x[0-9a-f]+/i)) { intLiteral = true; }
// Binary
if (stream.match(/^0b[01]+/i)) { intLiteral = true; }
// Octal
if (stream.match(/^0o[0-7]+/i)) { intLiteral = true; }
// Decimal
if (stream.match(/^[1-9]\d*(e[\+\-]?\d+)?/)) {
// Decimal literals may be "imaginary"
stream.eat(/J/i);
// TODO - Can you have imaginary longs?
intLiteral = true;
}
// Zero by itself with no other piece of number.
if (stream.match(/^0(?![\dx])/i)) { intLiteral = true; }
if (intLiteral) {
// Integer literals may be "long"
stream.eat(/L/i);
return 'number';
}
}
// Handle Strings
if (stream.match(stringPrefixes)) {
state.tokenize = tokenStringFactory(stream.current());
return state.tokenize(stream, state);
}
// Handle operators and Delimiters
if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) {
return null;
}
if (stream.match(doubleOperators)
|| stream.match(singleOperators)
|| stream.match(wordOperators)) {
return 'operator';
}
if (stream.match(singleDelimiters)) {
return null;
}
if (stream.match(keywords)) {
return 'keyword';
}
if (stream.match(builtins)) {
return 'builtin';
}
if (stream.match(identifiers)) {
return 'variable';
}
// Handle non-detected items
stream.next();
return ERRORCLASS;
}
function tokenStringFactory(delimiter) {
while ('rub'.indexOf(delimiter.charAt(0).toLowerCase()) >= 0) {
delimiter = delimiter.substr(1);
}
var singleline = delimiter.length == 1;
var OUTCLASS = 'string';
return function tokenString(stream, state) {
while (!stream.eol()) {
stream.eatWhile(/[^'"\\]/);
if (stream.eat('\\')) {
stream.next();
if (singleline && stream.eol()) {
return OUTCLASS;
}
} else if (stream.match(delimiter)) {
state.tokenize = tokenBase;
return OUTCLASS;
} else {
stream.eat(/['"]/);
}
}
if (singleline) {
if (parserConf.singleLineStringErrors) {
return ERRORCLASS;
} else {
state.tokenize = tokenBase;
}
}
return OUTCLASS;
};
}
function indent(stream, state, type) {
type = type || 'py';
var indentUnit = 0;
if (type === 'py') {
if (state.scopes[0].type !== 'py') {
state.scopes[0].offset = stream.indentation();
return;
}
for (var i = 0; i < state.scopes.length; ++i) {
if (state.scopes[i].type === 'py') {
indentUnit = state.scopes[i].offset + conf.indentUnit;
break;
}
}
} else {
indentUnit = stream.column() + stream.current().length;
}
state.scopes.unshift({
offset: indentUnit,
type: type
});
}
function dedent(stream, state, type) {
type = type || 'py';
if (state.scopes.length == 1) return;
if (state.scopes[0].type === 'py') {
var _indent = stream.indentation();
var _indent_index = -1;
for (var i = 0; i < state.scopes.length; ++i) {
if (_indent === state.scopes[i].offset) {
_indent_index = i;
break;
}
}
if (_indent_index === -1) {
return true;
}
while (state.scopes[0].offset !== _indent) {
state.scopes.shift();
}
return false
} else {
if (type === 'py') {
state.scopes[0].offset = stream.indentation();
return false;
} else {
if (state.scopes[0].type != type) {
return true;
}
state.scopes.shift();
return false;
}
}
}
function tokenLexer(stream, state) {
indentInfo = null;
var style = state.tokenize(stream, state);
var current = stream.current();
// Handle '.' connected identifiers
if (current === '.') {
style = state.tokenize(stream, state);
current = stream.current();
if (style === 'variable' || style === 'builtin') {
return 'variable';
} else {
return ERRORCLASS;
}
}
// Handle decorators
if (current === '@') {
style = state.tokenize(stream, state);
current = stream.current();
if (style === 'variable'
|| current === '@staticmethod'
|| current === '@classmethod') {
return 'meta';
} else {
return ERRORCLASS;
}
}
// Handle scope changes.
if (current === 'pass' || current === 'return') {
state.dedent += 1;
}
if (current === 'lambda') state.lambda = true;
if ((current === ':' && !state.lambda && state.scopes[0].type == 'py')
|| indentInfo === 'indent') {
indent(stream, state);
}
var delimiter_index = '[({'.indexOf(current);
if (delimiter_index !== -1) {
indent(stream, state, '])}'.slice(delimiter_index, delimiter_index+1));
}
if (indentInfo === 'dedent') {
if (dedent(stream, state)) {
return ERRORCLASS;
}
}
delimiter_index = '])}'.indexOf(current);
if (delimiter_index !== -1) {
if (dedent(stream, state, current)) {
return ERRORCLASS;
}
}
if (state.dedent > 0 && stream.eol() && state.scopes[0].type == 'py') {
if (state.scopes.length > 1) state.scopes.shift();
state.dedent -= 1;
}
return style;
}
var external = {
startState: function(basecolumn) {
return {
tokenize: tokenBase,
scopes: [{offset:basecolumn || 0, type:'py'}],
lastToken: null,
lambda: false,
dedent: 0
};
},
token: function(stream, state) {
var style = tokenLexer(stream, state);
state.lastToken = {style:style, content: stream.current()};
if (stream.eol() && stream.lambda) {
state.lambda = false;
}
return style;
},
indent: function(state, textAfter) {
if (state.tokenize != tokenBase) {
return 0;
}
return state.scopes[0].offset;
}
};
return external;
});
CodeMirror.defineMIME("text/x-python", "python");
var readFixtures = function() {
return jasmine.getFixtures().proxyCallTo_('read', arguments);
};
var preloadFixtures = function() {
jasmine.getFixtures().proxyCallTo_('preload', arguments);
};
var loadFixtures = function() {
jasmine.getFixtures().proxyCallTo_('load', arguments);
};
var setFixtures = function(html) {
jasmine.getFixtures().set(html);
};
var sandbox = function(attributes) {
return jasmine.getFixtures().sandbox(attributes);
};
var spyOnEvent = function(selector, eventName) {
jasmine.JQuery.events.spyOn(selector, eventName);
};
jasmine.getFixtures = function() {
return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures();
};
jasmine.Fixtures = function() {
this.containerId = 'jasmine-fixtures';
this.fixturesCache_ = {};
this.fixturesPath = 'spec/javascripts/fixtures';
};
jasmine.Fixtures.prototype.set = function(html) {
this.cleanUp();
this.createContainer_(html);
};
jasmine.Fixtures.prototype.preload = function() {
this.read.apply(this, arguments);
};
jasmine.Fixtures.prototype.load = function() {
this.cleanUp();
this.createContainer_(this.read.apply(this, arguments));
};
jasmine.Fixtures.prototype.read = function() {
var htmlChunks = [];
var fixtureUrls = arguments;
for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]));
}
return htmlChunks.join('');
};
jasmine.Fixtures.prototype.clearCache = function() {
this.fixturesCache_ = {};
};
jasmine.Fixtures.prototype.cleanUp = function() {
jQuery('#' + this.containerId).remove();
};
jasmine.Fixtures.prototype.sandbox = function(attributes) {
var attributesToSet = attributes || {};
return jQuery('<div id="sandbox" />').attr(attributesToSet);
};
jasmine.Fixtures.prototype.createContainer_ = function(html) {
var container;
if(html instanceof jQuery) {
container = jQuery('<div id="' + this.containerId + '" />');
container.html(html);
} else {
container = '<div id="' + this.containerId + '">' + html + '</div>'
}
jQuery('body').append(container);
};
jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) {
if (typeof this.fixturesCache_[url] == 'undefined') {
this.loadFixtureIntoCache_(url);
}
return this.fixturesCache_[url];
};
jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
var url = this.makeFixtureUrl_(relativeUrl);
var request = new XMLHttpRequest();
request.open("GET", url + "?" + new Date().getTime(), false);
request.send(null);
this.fixturesCache_[relativeUrl] = request.responseText;
};
jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){
return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl;
};
jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
return this[methodName].apply(this, passedArguments);
};
jasmine.JQuery = function() {};
jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
return jQuery('<div/>').append(html).html();
};
jasmine.JQuery.elementToString = function(element) {
var sample = $(element).get()[0]
if (sample == undefined || sample.cloneNode)
return jQuery('<div />').append($(element).clone()).html();
else
return element.toString();
};
jasmine.JQuery.matchersClass = {};
(function(namespace) {
var data = {
spiedEvents: {},
handlers: []
};
namespace.events = {
spyOn: function(selector, eventName) {
var handler = function(e) {
data.spiedEvents[[selector, eventName]] = e;
};
jQuery(selector).bind(eventName, handler);
data.handlers.push(handler);
},
wasTriggered: function(selector, eventName) {
return !!(data.spiedEvents[[selector, eventName]]);
},
wasPrevented: function(selector, eventName) {
return data.spiedEvents[[selector, eventName]].isDefaultPrevented();
},
cleanUp: function() {
data.spiedEvents = {};
data.handlers = [];
}
}
})(jasmine.JQuery);
(function(){
var jQueryMatchers = {
toHaveClass: function(className) {
return this.actual.hasClass(className);
},
toBeVisible: function() {
return this.actual.is(':visible');
},
toBeHidden: function() {
return this.actual.is(':hidden');
},
toBeSelected: function() {
return this.actual.is(':selected');
},
toBeChecked: function() {
return this.actual.is(':checked');
},
toBeEmpty: function() {
return this.actual.is(':empty');
},
toExist: function() {
return $(document).find(this.actual).length;
},
toHaveAttr: function(attributeName, expectedAttributeValue) {
return hasProperty(this.actual.attr(attributeName), expectedAttributeValue);
},
toHaveProp: function(propertyName, expectedPropertyValue) {
return hasProperty(this.actual.prop(propertyName), expectedPropertyValue);
},
toHaveId: function(id) {
return this.actual.attr('id') == id;
},
toHaveHtml: function(html) {
return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html);
},
toHaveText: function(text) {
var trimmedText = $.trim(this.actual.text());
if (text && jQuery.isFunction(text.test)) {
return text.test(trimmedText);
} else {
return trimmedText == text;
}
},
toHaveValue: function(value) {
return this.actual.val() == value;
},
toHaveData: function(key, expectedValue) {
return hasProperty(this.actual.data(key), expectedValue);
},
toBe: function(selector) {
return this.actual.is(selector);
},
toContain: function(selector) {
return this.actual.find(selector).length;
},
toBeDisabled: function(selector){
return this.actual.is(':disabled');
},
toBeFocused: function(selector) {
return this.actual.is(':focus');
},
// tests the existence of a specific event binding
toHandle: function(eventName) {
var events = this.actual.data("events");
return events && events[eventName].length > 0;
},
// tests the existence of a specific event binding + handler
toHandleWith: function(eventName, eventHandler) {
var stack = this.actual.data("events")[eventName];
var i;
for (i = 0; i < stack.length; i++) {
if (stack[i].handler == eventHandler) {
return true;
}
}
return false;
}
};
var hasProperty = function(actualValue, expectedValue) {
if (expectedValue === undefined) {
return actualValue !== undefined;
}
return actualValue == expectedValue;
};
var bindMatcher = function(methodName) {
var builtInMatcher = jasmine.Matchers.prototype[methodName];
jasmine.JQuery.matchersClass[methodName] = function() {
if (this.actual
&& (this.actual instanceof jQuery
|| jasmine.isDomNode(this.actual))) {
this.actual = $(this.actual);
var result = jQueryMatchers[methodName].apply(this, arguments)
if (this.actual.get && !$.isWindow(this.actual.get()[0]))
this.actual = jasmine.JQuery.elementToString(this.actual)
return result;
}
if (builtInMatcher) {
return builtInMatcher.apply(this, arguments);
}
return false;
};
};
for(var methodName in jQueryMatchers) {
bindMatcher(methodName);
}
})();
beforeEach(function() {
this.addMatchers(jasmine.JQuery.matchersClass);
this.addMatchers({
toHaveBeenTriggeredOn: function(selector) {
this.message = function() {
return [
"Expected event " + this.actual + " to have been triggered on " + selector,
"Expected event " + this.actual + " not to have been triggered on " + selector
];
};
return jasmine.JQuery.events.wasTriggered($(selector), this.actual);
}
});
this.addMatchers({
toHaveBeenPreventedOn: function(selector) {
this.message = function() {
return [
"Expected event " + this.actual + " to have been prevented on " + selector,
"Expected event " + this.actual + " not to have been prevented on " + selector
];
};
return jasmine.JQuery.events.wasPrevented(selector, this.actual);
}
});
});
afterEach(function() {
jasmine.getFixtures().cleanUp();
jasmine.JQuery.events.cleanUp();
});
/**
* jQuery.ScrollTo - Easy element scrolling using jQuery.
* Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
* Dual licensed under MIT and GPL.
* Date: 5/25/2009
* @author Ariel Flesler
* @version 1.4.2
*
* http://flesler.blogspot.com/2007/10/jqueryscrollto.html
*/
;(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery);
\ No newline at end of file
// Things to abstract out to another file
// We do sync AJAX for just the page close event.
// TODO: This should _really_ not be a global.
var log_close_event = false;
function log_close() {
var d=new Date();
var t=d.getTime();
//close_event_logged = "waiting";
log_close_event = true;
log_event('page_close', {});
log_close_event = false;
// Google Chrome will close without letting the event go through.
// This causes the page close to be delayed until we've hit the
// server. The code below fixes it, but breaks Firefox.
// TODO: Check what happens with no network.
/*while((close_event_logged != "done") && (d.getTime() < t+500)) {
console.log(close_event_logged);
}*/
}
window.onbeforeunload = log_close;
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function postJSON(url, data, callback) {
$.ajax({type:'POST',
url: url,
dataType: 'json',
data: data,
success: callback,
headers : {'X-CSRFToken':getCookie('csrftoken')}
});
}
function postJSONAsync(url, data, callback) {
$.ajax({type:'POST',
url: url,
dataType: 'json',
data: data,
success: callback,
headers : {'X-CSRFToken':getCookie('csrftoken')},
async:true
});
}
// For easy embedding of CSRF in forms
$(function() {
$('#csrfmiddlewaretoken').attr("value", getCookie('csrftoken'))
});
// For working with circuits in wiki:
function submit_circuit(circuit_id) {
$("input.schematic").each(function(index,element){ element.schematic.update_value(); });
postJSON('/save_circuit/'+circuit_id,
{'schematic': $('#schematic_'+circuit_id).attr("value")},
function(data){ if (data.results=='success') alert("Saved");});
return false;
}
// Video player
var load_id = 0;
var caption_id;
var video_speed = "1.0";
var updateytPlayerInterval;
var ajax_videoInterval;
function change_video_speed(speed, youtube_id) {
new_position = ytplayer.getCurrentTime() * video_speed / speed;
video_speed = speed;
ytplayer.loadVideoById(youtube_id, new_position);
syncPlayButton();
log_event("speed", {"new_speed":speed, "clip":youtube_id});
$.cookie("video_speed", speed, {'expires':3650, 'path':'/'});
}
function caption_at(index) {
if (captions==0)
return "";
text_array=captions.text
if ((index>=text_array.length) || (index < 0))
return "";
return text_array[index];
}
function caption_time_at(index) {
if (captions==0)
return 0;
time_array=captions.start;
if (index < 0)
return 0;
if (index>=time_array.length)
return ytplayer.getDuration();
return time_array[index] / 1000.0 / video_speed;
}
function caption_index(now) {
// Returns the index of the current caption, given a time
now = now * video_speed;
if (captions==0)
return 0;
time_array=captions.start
// TODO: Bisection would be better, or something incremental
var i;
for(i=0;i<captions.start.length; i++) {
if(time_array[i]>(now*1000)) {
return i-1;
}
}
return i-1;
}
function format_time(t)
{
seconds = Math.floor(t);
minutes = Math.floor(seconds / 60);
hours = Math.floor(minutes / 60);
seconds = seconds % 60;
minutes = minutes % 60;
if (hours) {
return hours+":"+((minutes < 10)?"0":"")+minutes+":"+((seconds < 10)?"0":"")+(seconds%60);
} else {
return minutes+":"+((seconds < 10)?"0":"")+(seconds%60);
}
}
function update_captions(t) {
var i=caption_index(t);
$("#vidtime").html(format_time(ytplayer.getCurrentTime())+' / '+format_time(ytplayer.getDuration()));
var j;
for(j=1; j<9; j++) {
$("#std_n"+j).html(caption_at(i-j));
$("#std_p"+j).html(caption_at(i+j));
}
$("#std_0").html(caption_at(i));
}
function title_seek(i) {
// Seek video forwards or backwards by i subtitles
current=caption_index(getCurrentTime());
new_time=caption_time_at(current+i);
ytplayer.seekTo(new_time, true);
}
function updateHTML(elmId, value) {
document.getElementById(elmId).innerHTML = value;
}
function setytplayerState(newState) {
// updateHTML("playerstate", newState);
}
// Updates server with location in video so we can resume from the same place
// IMPORTANT TODO: Load test
// POSSIBLE FIX: Move to unload() event and similar
var ajax_video=function(){};
var ytplayer;
function onYouTubePlayerReady(playerId) {
ytplayer = document.getElementById("myytplayer");
updateytplayerInfoInterval = setInterval(updateytplayerInfo, 500);
ajax_videoInterval = setInterval(ajax_video,5000);
ytplayer.addEventListener("onStateChange", "onytplayerStateChange");
ytplayer.addEventListener("onError", "onPlayerError");
if((typeof load_id != "undefined") && (load_id != 0)) {
var id=load_id;
loadNewVideo(caption_id, id, 0);
}
}
/* HTML5 YouTube iFrame API Specific */
function onYouTubePlayerAPIReady() {
ytplayer = new YT.Player('html5_player', {
events: {
'onReady': onPlayerReady,
'onStateChange': onPlayerStateChange
}
});
updateytplayerInfoInterval = setInterval(updateHTML5ytplayerInfo, 200);
//ajax_videoInterval = setInterval(ajax_video, 5000);
}
// Need this function to call the API ready callback when we switch to a tab with AJAX that has a video
// That callback is not being fired when we switch tabs.
function loadHTML5Video() {
if (!ytplayer && switched_tab){
onYouTubePlayerAPIReady();
}
}
function isiOSDevice(){
var iphone = "iphone";
var ipod = "ipod";
var ipad = "ipad";
var uagent = navigator.userAgent.toLowerCase();
//alert(uagent);
if (uagent.search(ipad) > -1 || uagent.search(iphone) > -1
|| uagent.search(ipod) > -1) {
return true;
}
return false;
}
function onPlayerReady(event) {
//do not want to autoplay on iOS devices since its not enabled
//and leads to confusing behavior for the user
if (!isiOSDevice()) {
event.target.playVideo();
}
}
function onPlayerStateChange(event) {
if (event.data == YT.PlayerState.PLAYING) {
}
}
/* End HTML5 Specific */
var switched_tab = false; // switch to true when we destroy so we know to call onYouTubePlayerAPIReady()
// clear pings to video status when we switch to a different sequence tab with ajax
function videoDestroy(id) {
// postJSON('/modx/video/'+id+'/goto_position',
// {'position' : ytplayer.getCurrentTime()});
load_id = 0;
clearInterval(updateytplayerInfoInterval);
clearInterval(ajax_videoInterval);
ytplayer = false;
switched_tab = true;
}
function log_event(e, d) {
data = {
"event_type" : e,
"event" : JSON.stringify(d),
"page" : document.URL
}
$.ajax({type:'GET',
url: '/event',
dataType: 'json',
data: data,
async: !log_close_event, // HACK: See comment on log_close_event
success: function(){},
headers : {'X-CSRFToken':getCookie('csrftoken')}
});
/*, // Commenting out Chrome bug fix, since it breaks FF
function(data) {
console.log("closing");
if (close_event_logged == "waiting") {
close_event_logged = "done";
console.log("closed");
}
});*/
}
function seek_slide(type,oe,value) {
//log_event('video', [type, value]);
if(type=='slide') {
// HACK/TODO: Youtube recommends this be false for slide and true for stop.
// Works better on my system with true/true.
// We should test both configurations on low/high bandwidth
// connections, and different browsers
// One issue is that we query the Youtube window every 250ms for position/state
// With false, it returns the old one (ignoring the new seek), and moves the
// scroll bar to the wrong spot.
ytplayer.seekTo(value, true);
} else if (type=='stop') {
ytplayer.seekTo(value, true);
log_event('video', [type, value]);
}
update_captions(value);
}
function get_state() {
if (ytplayer)
return [ytplayer.getPlayerState(),
ytplayer.getVideoUrl(),
ytplayer.getDuration(), ytplayer.getCurrentTime(),
ytplayer.getVideoBytesLoaded(), ytplayer.getVideoBytesTotal(),
ytplayer.getVideoStartBytes(),
ytplayer.getVolume(),ytplayer.isMuted(),
ytplayer.getPlaybackQuality(),
ytplayer.getAvailableQualityLevels()];
return [];
}
function onytplayerStateChange(newState) {
setytplayerState(newState);
log_event('video', ['State Change',newState, get_state()]);
}
function onPlayerError(errorCode) {
// alert("An error occured: " + errorCode);
log_event("player_error", {"error":errorCode});
}
// Currently duplicated to check for if video control changed by clicking the video for HTML5
// Hacky b/c of lack of control over YT player
function updateHTML5ytplayerInfo() {
var player_state = getPlayerState();
if(player_state != 3) {
$("#slider").slider("option","max",ytplayer.getDuration());
$("#slider").slider("option","value",ytplayer.getCurrentTime());
}
if (player_state == 1){
update_captions(getCurrentTime());
}
if (player_state == 1 && $("#video_control").hasClass("play"))
$("#video_control").removeClass().addClass("pause");
else if (player_state == 2 && $("#video_control").hasClass("pause"))
$("#video_control").removeClass().addClass("play");
}
function updateytplayerInfo() {
var player_state = getPlayerState();
if(player_state != 3) {
$("#slider").slider("option","max",ytplayer.getDuration());
$("#slider").slider("option","value",ytplayer.getCurrentTime());
}
if (player_state == 1){
update_captions(getCurrentTime());
handle = $('.ui-slider-handle', $('#slider'));
handle.qtip('option', 'content.text', '' + format_time(getCurrentTime()));
}
// updateHTML("videoduration", getDuration());
// updateHTML("videotime", getCurrentTime());
// updateHTML("startbytes", getStartBytes());
// updateHTML("volume", getVolume());
}
// functions for the api calls
function loadNewVideo(cap_id, id, startSeconds) {
captions={"start":[0],"end":[0],"text":["Attempting to load captions..."]};
$.getJSON("/static/subs/"+cap_id+".srt.sjson", function(data) {
captions=data;
});
caption_id = cap_id;
load_id = id;
//if ((typeof ytplayer != "undefined") && (ytplayer.type=="application/x-shockwave-flash")) {
// Try it every time. If we fail, we want the error message for now.
// TODO: Add try/catch
try {
ytplayer.loadVideoById(id, parseInt(startSeconds));
load_id=0;
}
catch(e) {
window['console'].log(JSON.stringify(e));
}
log_event("load_video", {"id":id,"start":startSeconds});
//$("#slider").slider("option","value",startSeconds);
//seekTo(startSeconds);
}
function syncPlayButton(){
var state = getPlayerState();
if (state == 1 || state == 3) {
$("#video_control").removeClass("play").addClass("pause");
} else if (state == 2 || state == -1 || state == 0){
$("#video_control").removeClass("pause").addClass("play");
}
}
function cueNewVideo(id, startSeconds) {
if (ytplayer) {
ytplayer.cueVideoById(id, startSeconds);
}
}
function play() {
if (ytplayer) {
ytplayer.playVideo();
}
log_event("play_video", {"id":getCurrentTime(), "code":getEmbedCode()});
}
function pause() {
if (ytplayer) {
ytplayer.pauseVideo();
}
log_event("pause_video", {"id":getCurrentTime(), "code":getEmbedCode()});
}
function stop() {
if (ytplayer) {
ytplayer.stopVideo();
}
log_event("stop_video", {"id":getCurrentTime(), "code":getEmbedCode()});
}
function getPlayerState() {
if (ytplayer) {
return ytplayer.getPlayerState();
}
}
function seekTo(seconds) {
if (ytplayer) {
ytplayer.seekTo(seconds, true);
}
}
function getBytesTotal() {
if (ytplayer) {
return ytplayer.getVideoBytesTotal();
}
}
function getCurrentTime() {
if (ytplayer) {
return ytplayer.getCurrentTime();
}
}
function getDuration() {
if (ytplayer) {
return ytplayer.getDuration();
}
}
function getStartBytes() {
if (ytplayer) {
return ytplayer.getVideoStartBytes();
}
}
function mute() {
if (ytplayer) {
ytplayer.mute();
}
}
function unMute() {
if (ytplayer) {
ytplayer.unMute();
}
}
function getEmbedCode() {
if(ytplayer) {
ytplayer.getVideoEmbedCode();
}
}
function getVideoUrl() {
if(ytplayer) {
ytplayer.getVideoUrl();
}
}
function setVolume(newVolume) {
if (ytplayer) {
ytplayer.setVolume(newVolume);
}
}
function getVolume() {
if (ytplayer) {
return ytplayer.getVolume();
}
}
function clearVideo() {
if (ytplayer) {
ytplayer.clearVideo();
}
}
...@@ -54,6 +54,13 @@ div.course-wrapper { ...@@ -54,6 +54,13 @@ div.course-wrapper {
display: table-cell; display: table-cell;
vertical-align: top; vertical-align: top;
&.problem-header {
section.staff {
margin-top: 30px;
font-size: 80%;
}
}
@media screen and (max-width:1120px) { @media screen and (max-width:1120px) {
display: block; display: block;
width: auto; width: auto;
...@@ -195,6 +202,10 @@ div.course-wrapper { ...@@ -195,6 +202,10 @@ div.course-wrapper {
padding-bottom: 0; padding-bottom: 0;
} }
.histogram {
width: 200px;
height: 150px;
}
ul { ul {
list-style: disc outside none; list-style: disc outside none;
......
...@@ -14,38 +14,29 @@ section.course-content { ...@@ -14,38 +14,29 @@ section.course-content {
} }
} }
div.video-subtitles { div.video {
@include clearfix();
background: #f3f3f3; background: #f3f3f3;
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1; border-top: 1px solid #e1e1e1;
@include clearfix();
display: block; display: block;
margin: 0 (-(lh())); margin: 0 (-(lh()));
padding: 6px lh(); padding: 6px lh();
div.video-wrapper { article.video-wrapper {
float: left; float: left;
margin-right: flex-gutter(9); margin-right: flex-gutter(9);
width: flex-grid(6, 9); width: flex-grid(6, 9);
div.video-player { section.video-player {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
padding-bottom: 56.25%; padding-bottom: 56.25%;
padding-top: 30px; padding-top: 30px;
position: relative; position: relative;
object { object, iframe {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
iframe#html5_player {
border: none; border: none;
display: none;
height: 100%; height: 100%;
left: 0; left: 0;
position: absolute; position: absolute;
...@@ -68,7 +59,7 @@ section.course-content { ...@@ -68,7 +59,7 @@ section.course-content {
} }
} }
div#slider { div.slider {
@extend .clearfix; @extend .clearfix;
background: #c2c2c2; background: #c2c2c2;
border: none; border: none;
...@@ -175,7 +166,8 @@ section.course-content { ...@@ -175,7 +166,8 @@ section.course-content {
} }
} }
div#vidtime { div.vidtime {
padding-left: lh(.75);
font-weight: bold; font-weight: bold;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
padding-left: lh(.75); padding-left: lh(.75);
...@@ -190,8 +182,20 @@ section.course-content { ...@@ -190,8 +182,20 @@ section.course-content {
div.speeds { div.speeds {
float: left; 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; background: url('../images/closed-arrow.png') 10px center no-repeat;
border-left: 1px solid #000; border-left: 1px solid #000;
border-right: 1px solid #000; border-right: 1px solid #000;
...@@ -208,15 +212,6 @@ section.course-content { ...@@ -208,15 +212,6 @@ section.course-content {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
width: 110px; width: 110px;
&.open {
background: url('../images/open-arrow.png') 10px center no-repeat;
ol#video_speeds {
display: block;
opacity: 1;
}
}
h3 { h3 {
color: #999; color: #999;
float: left; float: left;
...@@ -234,47 +229,52 @@ section.course-content { ...@@ -234,47 +229,52 @@ section.course-content {
padding: 0 lh(.5) 0 0; padding: 0 lh(.5) 0 0;
} }
// fix for now &:hover, &:active, &:focus {
ol#video_speeds { opacity: 1;
background-color: #444; background-color: #444;
border: 1px solid #000; }
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); }
display: none;
left: -1px;
opacity: 0;
position: absolute;
top:0;
@include transition();
width: 100%;
z-index: 10;
li {
border-bottom: 1px solid #000;
@include box-shadow( 0 1px 0 #555);
color: #fff;
cursor: pointer;
padding: 0 lh(.5);
&.active { // fix for now
font-weight: bold; ol.video_speeds {
} @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
@include transition();
background-color: #444;
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0;
position: absolute;
width: 125px;
z-index: 10;
&:last-child { li {
border-bottom: 0; @include box-shadow( 0 1px 0 #555);
@include box-shadow(none); border-bottom: 1px solid #000;
margin-top: 0; color: #fff;
} cursor: pointer;
a {
border: 0;
color: #fff;
display: block;
padding: lh(.5);
&:hover { &:hover {
background-color: #666; background-color: #666;
color: #aaa; color: #aaa;
} }
} }
}
&:hover { &.active {
background-color: #444; font-weight: bold;
opacity: 1; }
&:last-child {
@include box-shadow(none);
border-bottom: 0;
margin-top: 0;
}
} }
} }
} }
...@@ -334,7 +334,7 @@ section.course-content { ...@@ -334,7 +334,7 @@ section.course-content {
opacity: 1; opacity: 1;
} }
div#slider { div.slider {
height: 14px; height: 14px;
margin-top: -7px; margin-top: -7px;
...@@ -352,15 +352,14 @@ section.course-content { ...@@ -352,15 +352,14 @@ section.course-content {
ol.subtitles { ol.subtitles {
float: left; float: left;
max-height: 460px; max-height: 460px;
overflow: hidden; overflow: auto;
padding-top: 10px;
width: flex-grid(3, 9); width: flex-grid(3, 9);
li { li {
border: 0; border: 0;
color: #666; color: #666;
cursor: pointer; cursor: pointer;
margin-bottom: 0px; margin-bottom: 8px;
padding: 0; padding: 0;
@include transition(all, .5s, ease-in); @include transition(all, .5s, ease-in);
...@@ -373,11 +372,7 @@ section.course-content { ...@@ -373,11 +372,7 @@ section.course-content {
color: $mit-red; color: $mit-red;
} }
div { &:empty {
margin-bottom: 8px;
}
div:empty {
margin-bottom: 0px; margin-bottom: 0px;
} }
} }
...@@ -386,7 +381,7 @@ section.course-content { ...@@ -386,7 +381,7 @@ section.course-content {
&.closed { &.closed {
@extend .trans; @extend .trans;
div.video-wrapper { article.video-wrapper {
width: flex-grid(9,9); width: flex-grid(9,9);
} }
...@@ -441,11 +436,11 @@ section.course-content { ...@@ -441,11 +436,11 @@ section.course-content {
} }
div.tc-wrapper { div.tc-wrapper {
div.video-wrapper { article.video-wrapper {
width: 100%; width: 100%;
} }
object#myytplayer, iframe { object, iframe {
bottom: 0; bottom: 0;
height: 100%; height: 100%;
left: 0; left: 0;
...@@ -487,7 +482,7 @@ section.course-content { ...@@ -487,7 +482,7 @@ section.course-content {
} }
} }
div.course-wrapper.closed section.course-content div.video-subtitles { div.course-wrapper.closed section.course-content div.video {
ol.subtitles { ol.subtitles {
max-height: 577px; max-height: 577px;
} }
......
...@@ -7,12 +7,10 @@ ...@@ -7,12 +7,10 @@
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
##Is there a reason this isn't in header_extra? Is it important that the javascript is at the bottom of the generated document?
<!-- TODO: http://docs.jquery.com/Plugins/Validation --> <!-- TODO: http://docs.jquery.com/Plugins/Validation -->
<script type="text/javascript"> <script type="text/javascript">
$(function() { document.write('\x3Cscript type="text/javascript" src="' +
${init} document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
});
</script> </script>
</%block> </%block>
...@@ -26,7 +24,7 @@ ...@@ -26,7 +24,7 @@
<a href="#">close</a> <a href="#">close</a>
</header> </header>
<div id="accordion"> <div id="accordion" style="display: none">
<nav> <nav>
${accordion} ${accordion}
</nav> </nav>
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
<h1>Circuits &amp; Electronics</h1> <h1>Circuits &amp; Electronics</h1>
<h2>6.002x</h2> <h2>6.002x</h2>
<a class="enroll" rel="leanModal" href="/info">View 6.002x Circuits <span>&amp;</span> Electronics as a guest</a> <a class="enroll" rel="leanModal" href="/info">View 6.002x Circuits <span>&amp;</span> Electronics as a guest</a>
<a class="enroll" rel="leanModal" href="#enroll"><noscript>In order to</noscript> Enroll in 6.002x Circuits <span>&amp;</span> Electronics <noscript>you need to have javascript enabled</noscript></a>
</section> </section>
<p>6.002x (Circuits and Electronics) is an experimental on-line adaptation of MIT&rsquo;s first undergraduate analog design course: 6.002. This course is running, free of charge, for students worldwide from March 5, 2012 through June 8, 2012.</p> <p>6.002x (Circuits and Electronics) is an experimental on-line adaptation of MIT&rsquo;s first undergraduate analog design course: 6.002. This course is running, free of charge, for students worldwide from March 5, 2012 through June 8, 2012.</p>
</section> </section>
...@@ -53,6 +54,7 @@ ...@@ -53,6 +54,7 @@
<section class="cta"> <section class="cta">
<a class="enroll" rel="leanModal" href="/info">View 6.002x Circuits &amp; Electronics as a guest</a> <a class="enroll" rel="leanModal" href="/info">View 6.002x Circuits &amp; Electronics as a guest</a>
<a class="enroll" rel="leanModal" href="#enroll"><noscript>In order to</noscript> Enroll in 6.002x Circuits &amp; Electronics <noscript>you need to have javascript enabled</noscript></a>
</section> </section>
</section> </section>
......
<!doctype html>
<html>
<head>
<title>Jasmine Spec Runner</title>
{% load staticfiles %}
<link rel="stylesheet" href="{% static 'jasmine-latest/jasmine.css' %}" media="screen">
{# core files #}
<script src="{% static 'jasmine-latest/jasmine.js' %}"></script>
<script src="{% static 'jasmine-latest/jasmine-html.js' %}"></script>
<script src="{% static 'js/jasmine-jquery.js' %}"></script>
{# source files #}
{% for url in suite.js_files %}
<script src="{{ url }}"></script>
{% endfor %}
{% load compressed %}
{# static files #}
{% compressed_js 'application' %}
{# spec files #}
{% compressed_js 'spec' %}
</head>
<body>
<h1>Jasmine Spec Runner</h1>
<script>
{% block jasmine %}
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
var trivialReporter = new jasmine.TrivialReporter();
jasmineEnv.addReporter(trivialReporter);
jasmineEnv.specFilter = function(spec) {
return trivialReporter.specFilter(spec);
};
// Additional configuration can be done in this block
{% block jasmine_extra %}{% endblock %}
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
})();
{% endblock %}
</script>
</body>
</html>
<section class="text-input"> <section id="jstextline_${id}" class="jstextline">
<input type="text" name="input_${id}" id="input_${id}" value="${value}" <input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size: % if size:
size="${size}" size="${size}"
% endif % endif
% if dojs == 'math': % if dojs == 'math':
onkeyup="DoUpdateMath('${id}')" onkeyup="DoUpdateMath('${id}')"
% endif % endif
/> />
% if dojs == 'math': % if dojs == 'math':
<span id="display_${id}">`{::}`</span> <span id="display_${id}">`{::}`</span>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
% if state == 'unsubmitted': % if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif state == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
MathJax extension libraries --> MathJax extension libraries -->
<script type="text/javascript" src="/static/js/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-AMS_HTML-full"></script> <script type="text/javascript" src="/static/js/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-AMS_HTML-full"></script>
<meta name="path_prefix" content="${MITX_ROOT_URL}">
</head> </head>
<body class="<%block name='bodyclass'/>"> <body class="<%block name='bodyclass'/>">
...@@ -113,10 +114,9 @@ ...@@ -113,10 +114,9 @@
<script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.qtip.min.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.qtip.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.cookie.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.cookie.js')}"></script>
<script type="text/javascript" src="${static.url('js/video_player.js')}"></script>
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script> <script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
<script type="text/javascript" src="${static.url('js/cktsim.js')}"></script> <script type="text/javascript" src="${static.url('js/cktsim.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.scrollTo-1.4.2-min.js')}"></script>
<%block name="js_extra"/> <%block name="js_extra"/>
</body> </body>
</html> </html>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<h2 class="problem-header">${ problem['name'] } <h2 class="problem-header">
% if problem['weight']: ${ problem['name'] }
: ${ problem['weight'] } points % if problem['weight']:
% endif : ${ problem['weight'] } points
% if settings.QUICKEDIT: % endif
<span class="staff">
<br/> % if settings.QUICKEDIT:
<br/> <section class="staff">
<br/> <a href=${MITX_ROOT_URL}/quickedit/${id}>Quick Edit Problem</a>
<br/> </section>
<font size=-2><a href=${MITX_ROOT_URL}/quickedit/${id}>Quick % endif
Edit Problem</a></font></span>
% endif
</h2> </h2>
<section class="problem"> <section class="problem">
...@@ -21,16 +19,16 @@ Edit Problem</a></font></span> ...@@ -21,16 +19,16 @@ Edit Problem</a></font></span>
<input type="hidden" name="problem_id" value="${ problem['name'] }"> <input type="hidden" name="problem_id" value="${ problem['name'] }">
% if check_button: % if check_button:
<input id="check_${ id }" type="button" value="${ check_button }" > <input class="check" type="button" value="${ check_button }">
% endif % endif
% if reset_button: % if reset_button:
<input id="reset_${ id }" type="button" value="Reset" > <input class="reset" type="button" value="Reset">
% endif % endif
% if save_button: % if save_button:
<input id="save_${ id }" type="button" value="Save" > <input class="save" type="button" value="Save">
% endif % endif
% if answer_available: % if answer_available:
<input id="show_${ id }" type="button" value="Show Answer" > <input class="show" type="button" value="Show Answer">
% endif % endif
% if explain : % if explain :
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a> <a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
...@@ -42,3 +40,4 @@ Edit Problem</a></font></span> ...@@ -42,3 +40,4 @@ Edit Problem</a></font></span>
% endif % endif
</section> </section>
</section> </section>
function ${ id }_content_updated() {
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
update_schematics();
$('#check_${ id }').unbind('click').click(function() {
$("input.schematic").each(function(index,element){ element.schematic.update_value(); });
$(".CodeMirror").each(function(index,element){ if (element.CodeMirror.save) element.CodeMirror.save(); });
var submit_data={};
$.each($("[id^=input_${ id }_]"), function(index,value){
if (value.type==="checkbox"){
if (value.checked) {
if (typeof submit_data[value.name] == 'undefined'){
submit_data[value.name]=[];
}
submit_data[value.name].push(value.value);
}
}
if (value.type==="radio"){
if (value.checked) {
submit_data[value.name]= value.value;
}
}
else{
submit_data[value.id]=value.value;
}
});
postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_check',
submit_data,
function(json) {
switch(json.success) {
case 'incorrect': // Worked, but answer not
case 'correct':
$('#main_${ id }').html(json.contents);
${ id }_content_updated();
break;
default:
alert(json.success);
}}
);
log_event('problem_check', submit_data);
});
$('#reset_${ id }').unbind('click').click(function() {
var submit_data={};
$.each($("[id^=input_${ id }_]"), function(index,value){
submit_data[value.id]=value.value;
});
postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_reset', {'id':'${ id }'}, function(html_as_json) {
$('#main_${ id }').html(html_as_json);
${ id }_content_updated();
});
log_event('problem_reset', submit_data);
});
// show answer button
// TODO: the button should turn into "hide answer" afterwards
$('#show_${ id }').unbind('click').click(function() {
postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_show', {}, function(data) {
for (var key in data) {
if ($.isArray(data[key])){
for (var ans_index in data[key]){
var choice_id = 'input_'+key+'_'+data[key][ans_index];
$("label[for="+choice_id+"]").attr("correct_answer", "true");
}
}
$("#answer_"+key).text(data[key]);
}
});
log_event('problem_show', {'problem':'${ id }'});
});
$('#save_${ id }').unbind('click').click(function() {
$("input.schematic").each(function(index,element){ element.schematic.update_value(); });
var submit_data={};
$.each($("[id^=input_${ id }_]"), function(index,value) {
submit_data[value.id]=value.value;
});
postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_save',
submit_data,
function(data) {
if(data.success) {
alert('Saved');
}});
log_event('problem_save', submit_data);
});
}
function ${ id }_load() {
$('#main_${ id }').load('${ ajax_url }problem_get?id=${ id }', ${ id }_content_updated);
}
$(function() {
${ id }_load();
});
<section id="main_${id}" class="problems-wrapper"></section> <section id="problem_${id}" class="problems-wrapper" data-url="${ajax_url}"></section>
...@@ -75,7 +75,7 @@ $(function() { ...@@ -75,7 +75,7 @@ $(function() {
}); });
$('#pwd_reset_button').click(function() { $('#pwd_reset_button').click(function() {
$.post('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }", $.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
"email" : $('#id_email').val()}, function(data){ "email" : $('#id_email').val()}, function(data){
$("#password_reset_complete_link").click(); $("#password_reset_complete_link").click();
log_event("profile", {"type":"password_send"}); log_event("profile", {"type":"password_send"});
......
<nav aria-label="Section Navigation" class="sequence-nav"> <div id="sequence_${id}" class="sequence">
<ol> <nav aria-label="Section Navigation" class="sequence-nav">
% for t in range(1,1+len(items)): <ol id="sequence-list">
<li><a href="#" class="seq_inactive" id="tt_${ t }"></a></li> </ol>
% endfor
</ol>
<ul> <ul class="sequence-nav-buttons">
<li class="${ id }prev prev"><a href="#">Previous</a></li> <li class="prev"><a href="#">Previous</a></li>
<li class="${ id }next next"><a href="#">Next</a></li> <li class="next"><a href="#">Next</a></li>
</ul> </ul>
</nav> </nav>
<div id="seq_content"></div> <div id="seq_content"></div>
<nav class="sequence-bottom"> <nav class="sequence-bottom">
<ul aria-label="Section Navigation"> <ul aria-label="Section Navigation" class="sequence-nav-buttons">
<li class="${ id }prev prev"><a href="#">Previous</a></li> <li class="prev"><a href="#">Previous</a></li>
<li class="${ id }next next"><a href="#">Next</a></li> <li class="next"><a href="#">Next</a></li>
</ul> </ul>
</nav> </nav>
</div>
<%block name="js_extra">
<script type="text/javascript">
$(function(){
new Sequence('${id}', ${items}, '${tag}', ${position});
});
</script>
</%block>
// IMPORTANT TODO: Namespace
var ${ id }contents=["",
%for t in items:
${t['content']} ,
%endfor
""
];
var ${ id }types=["",
%for t in items:
"${t['type']}" ,
%endfor
""
];
var ${ id }init_functions=["",
%for t in items:
function(){ ${t['init_js']} },
%endfor
""];
var ${ id }titles=${titles};
var ${ id }destroy_functions=["",
%for t in items:
function(){ ${t['destroy_js']} },
%endfor
""];
var ${ id }loc = -1;
function disablePrev() {
var i=${ id }loc-1;
log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'});
if (i < 1 ) {
$('.${ id }prev a').addClass('disabled');
} else {
$('.${ id }prev a').removeClass('disabled');
};
}
function disableNext() {
var i=${ id }loc+1;
log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'});
if(i > ${ len(items) } ) {
$('.${ id }next a').addClass('disabled');
} else {
$('.${ id }next a').removeClass('disabled');
};
}
function ${ id }goto(i) {
log_event("seq_goto", {'old':${id}loc, 'new':i,'id':'${id}'});
postJSON('${ MITX_ROOT_URL }/modx/${tag}/${ id }/goto_position',
{'position' : i });
if (${ id }loc!=-1)
${ id }destroy_functions[ ${ id }loc ]();
$('#seq_content').html(${ id }contents[i]);
${ id }init_functions[i]()
//$('#tt_'+${ id }loc).attr("style", "background-color:gray");
$('#tt_'+${ id }loc).removeClass();
$('#tt_'+${ id }loc).addClass("seq_"+${ id }types[${ id }loc]+"_visited");
${ id }loc=i;
//$('#tt_'+i).attr("style", "background-color:red");
$('#tt_'+i).removeClass();
$('#tt_'+i).addClass("seq_"+${ id }types[${ id }loc]+"_active");
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
disableNext();
disablePrev();
}
function ${ id }setup_click(i) {
$('#tt_'+i).click(function(eo) { ${ id }goto(i);});
$('#tt_'+i).addClass("seq_"+${ id }types[i]+"_inactive");
$('#tt_'+i).append("<p>" + ${ id }titles[i-1] + "</p>");
}
function ${ id }next() {
var i=${ id }loc+1;
log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'});
if(i > ${ len(items) } ) {
i = ${ len(items) };
} else {
${ id }goto(i);
};
}
function ${ id }prev() {
var i=${ id }loc-1;
log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'});
if (i < 1 ) {
i = 1;
} else {
${ id }goto(i);
};
}
$(function() {
var i;
for(i=1; i<${ len(items)+1 }; i++) {
${ id }setup_click(i);
}
$('.${ id }next a').click(function(eo) { ${ id }next(); return false;});
$('.${ id }prev a').click(function(eo) { ${ id }prev(); return false;});
${ id }goto( ${ position } );
});
<%!
import json
import math
%>
var rawData = ${json.dumps(histogram)};
var maxx = 1;
var maxy = 1.5;
var xticks = Array();
var yticks = Array();
var data = Array();
for (var i = 0; i < rawData.length; i++) {
var score = rawData[i][0];
var count = rawData[i][1];
var log_count = Math.log(count + 1);
data.push( [score, log_count] );
xticks.push( [score, score.toString()] );
yticks.push( [log_count, count.toString()] );
maxx = Math.max( score + 1, maxx );
maxy = Math.max(log_count*1.1, maxy );
}
$.plot($("#histogram_${module_id}"), [{
data: data,
bars: { show: true,
align: 'center',
lineWidth: 0,
fill: 1.0 },
color: "#b72121",
}],
{
xaxis: {min: -1, max: maxx, ticks: xticks, tickLength: 0},
yaxis: {min: 0.0, max: maxy, ticks: yticks, labelWidth: 50},
}
);
...@@ -2,5 +2,5 @@ ...@@ -2,5 +2,5 @@
${xml | h} ${xml | h}
</div> </div>
%if render_histogram: %if render_histogram:
<div id="histogram_${module_id}" style="width:200px;height:150px"></div> <div id="histogram_${module_id}" class="histogram" data-histogram="${histogram}"></div>
%endif %endif
<div id="tabs"> <div id="tab_${id}" class="tab">
<ul> <ul class="navigation"></ul>
% for t in items:
<li> <a href="#tabs-${items.index(t)}">${t[0]}</a>
% endfor
</ul>
% for t in items:
<div id="tabs-${items.index(t)}">
</div>
% endfor
</div> </div>
<%block name="js_extra">
<script type="text/javascript">
$(function(){
new Tab('${id}', ${items});
});
</script>
</%block>
// IMPORTANT TODO: Namespace
var ${ id }contents=["",
%for t in items:
${t[1]['content']} ,
%endfor
""
];
var ${ id }init_functions=["",
%for t in items:
function(){ ${t[1]['init_js']} },
%endfor
""];
var ${ id }destroy_functions=["",
%for t in items:
function(){ ${t[1]['destroy_js']} },
%endfor
""];
var ${ id }loc = -1;
function ${ id }goto(i) {
if (${ id }loc!=-1)
${ id }destroy_functions[ ${ id }loc ]();
$('#tabs-'+(i-1)).html(${ id }contents[i]);
${ id }init_functions[i]()
$('#tt_'+${ id }loc).attr("style", "background-color:grey");
${ id }loc=i;
$('#tt_'+i).attr("style", "background-color:red");
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
}
$("#tabs").tabs({select:function(event, ui){
//global=ui;
return true;
},
show:function(event,ui){
//global=ui;
${ id }goto(ui.index+1);
return true;
},
});
<section class="text-input"> <section id="textbox_${id}" class="textbox">
<textarea rows="30" cols="80" name="input_${id}" id="input_${id}">${value|h}</textarea> <textarea rows="30" cols="80" name="input_${id}" id="input_${id}">${value|h}</textarea>
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if state == 'unsubmitted': % if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif state == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
...@@ -18,17 +18,21 @@ ...@@ -18,17 +18,21 @@
<span class="debug">${msg|n}</span> <span class="debug">${msg|n}</span>
<br/> <br/>
<br/> <br/>
<script>
// Note: We need to make the area follow the CodeMirror for this to <script type="text/javascript" src="${ settings.LIB_URL }CodeMirror/codemirror.js"></script>
// work. <script type="text/javascript" src="${ settings.LIB_URL }CodeMirror/python.js"></script>
$(function(){ <link rel="stylesheet" href="${ settings.LIB_URL }CodeMirror/codemirror.css" />
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"), <script>
{'mode':"python"}); // Note: We need to make the area follow the CodeMirror for this to work.
}); $(function(){
</script> var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"), {
<style type="text/css"> mode: "python"
.CodeMirror {border-style: solid; });
border-width: 1px;} });
</style> </script>
<style type="text/css">
.CodeMirror {border-style: solid;
border-width: 1px;}
</style>
</section> </section>
<section class="text-input"> <section id="textinput_${id}" class="textinput">
<input type="text" name="input_${id}" id="input_${id}" value="${value}" <input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size: % if size:
size="${size}" size="${size}"
% endif % endif
/> />
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if state == 'unsubmitted': % if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif state == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
......
...@@ -2,127 +2,18 @@ ...@@ -2,127 +2,18 @@
<h1> ${name} </h1> <h1> ${name} </h1>
% endif % endif
<div class="video-subtitles"> <div id="video_${id}" class="video" data-streams="${streams}">
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-wrapper"> <section class="video-player">
<div class="video-player"> <div id="${id}"></div>
<div id="ytapiplayer">
</div>
<iframe id="html5_player" type="text/html" frameborder="0">
</iframe>
</div>
<section class="video-controls">
<div id="slider"></div>
<section>
<ul class="vcr">
<li><a id="video_control" class="pause">Pause</a></li>
<li>
<div id="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
<ol id="video_speeds"></ol>
</a>
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</section> </section>
</section>
</div>
<ol class="subtitles">
<!-- <li id="stt_n5"><div id="std_n7" onclick="title_seek(-7);"></div></li> -->
<li id="stt_n4"><div id="std_n6" onclick="title_seek(-6);"></div></li>
<li id="stt_n4"><div id="std_n5" onclick="title_seek(-5);"></div></li>
<li id="stt_n4"><div id="std_n4" onclick="title_seek(-4);"></div></li>
<li id="stt_n3"><div id="std_n3" onclick="title_seek(-3);"></div></li>
<li id="stt_n2"><div id="std_n2" onclick="title_seek(-2);"></div></li>
<li id="stt_n1"><div id="std_n1" onclick="title_seek(-1);"></div></li>
<li id="stt_0 "class="current"><div id="std_0" onclick="title_seek(0);"></div></li>
<li id="stt_p1"><div id="std_p1" onclick="title_seek( 1);"></div></li>
<li id="stt_p2"><div id="std_p2" onclick="title_seek( 2);"></div></li>
<li id="stt_p3"><div id="std_p3" onclick="title_seek( 3);"></div></li>
<li id="stt_p4"><div id="std_p4" onclick="title_seek( 4);"></div></li>
<li id="stt_p5"><div id="std_p5" onclick="title_seek( 5);"></div></li>
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 6);"></div></li>
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 7);"></div></li>
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 8);"></div></li>
</ol>
</div>
<section class="video-controls"></section>
</article>
</div>
</div> </div>
<%block name="js_extra">
<script src="/static/js/jquery.ui.touch-punch.min.js"></script>
<script type="text/javascript" charset="utf-8">
$(function() {
// tooltips for full browser and closed caption
$('.add-fullscreen, .hide-subtitles ').qtip({
position: {
my: 'top right',
at: 'top center'
}
});
//full browser
$('.add-fullscreen').click(function() {
$('div.video-subtitles').toggleClass('fullscreen');
if ($('div.video-subtitles').hasClass('fullscreen')) {
$('div.video-subtitles').append('<a href="#" class="exit">Exit</a>');
} else {
$('a.exit').remove();
}
$('.exit').click(function() {
$('div.video-subtitles').removeClass('fullscreen');
$(this).remove();
return false;
});
var link_title = $(this).attr('title');
$(this).attr('title', (link_title == 'Exit fill browser') ? 'Fill browser' : 'Exit fill browser');
return false;
});
//hide subtitles
$('.hide-subtitles').click(function() {
$('div.video-subtitles').toggleClass('closed');
var link_title = $(this).attr('title');
$(this).attr('title', (link_title == 'Turn on captions') ? 'Turn off captions' : 'Turn on captions');
return false;
});
$("div.speeds a").hover(function() {
$(this).toggleClass("open");
});
$("div.speeds a").click(function() {
return false;
});
});
</script>
</%block>
<ol class="video-mod"> <ol class="video-mod">
% for t in annotations: % for t in annotations:
<li id="video-${annotations.index(t)}"> <li id="video-${annotations.index(t)}">
......
var streams=${ streams }
var params = { allowScriptAccess: "always", bgcolor: "#cccccc", wmode: "transparent", allowFullScreen: "true" };
var atts = { id: "myytplayer" };
// If the user doesn't have flash, use the HTML5 Video instead. YouTube's
// iFrame API which supports HTML5 is still developmental so it is not default
if (swfobject.hasFlashPlayerVersion("10.1")){
swfobject.embedSWF(document.location.protocol + "//www.youtube.com/apiplayer?enablejsapi=1&playerapiid=ytplayer?wmode=transparent",
"ytapiplayer", "640", "385", "8", null, null, params, atts);
} else {
//end of this URL may need &origin=http://..... once pushed to production to prevent XSS
$("#html5_player").attr("src", document.location.protocol + "//www.youtube.com/embed/" + streams["1.0"] + "?enablejsapi=1&controls=0");
$("#html5_player").show();
var tag = document.createElement('script');
tag.src = document.location.protocol + "//www.youtube.com/player_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// Make sure the callback is called once API ready, YT seems to be buggy
loadHTML5Video();
}
var captions=0;
/* Cache a reference to our slider element */
var slider = $('#slider')
.slider({
range: "min",
slide: function(event,ui) {
var slider_time = format_time(ui.value)
seek_slide('slide',event.originalEvent,ui.value);
handle.qtip('option', 'content.text', '' + slider_time);
},
stop:function(event,ui){seek_slide('stop',event.originalEvent,ui.value);}
}),
/* Grab and cache the newly created slider handle */
handle = $('.ui-slider-handle', slider);
/*
* Selector needs changing here to match your elements.
*
* Notice the second argument to the $() constructor, which tells
* jQuery to use that as the top-level element to seareh down from.
*/
handle.qtip({
content: '' + slider.slider('option', 'value'), // Use the current value of the slider
position: {
my: 'bottom center',
at: 'top center',
container: handle // Stick it inside the handle element so it keeps the position synched up
},
hide: {
delay: 700 // Give it a longer delay so it doesn't hide frequently as we move the handle
},
style: {
classes: 'ui-tooltip-slider',
widget: true // Make it Themeroller compatible
}
});
function good() {
window['console'].log(ytplayer.getCurrentTime());
}
ajax_video=good;
// load the same video speed your last video was at in a sequence
// if the last speed played on video doesn't exist on another video just use 1.0 as default
function add_speed(key, stream) {
var id = 'speed_' + stream;
if (key == video_speed) {
$("#video_speeds").append(' <li class="active" id="'+id+'">'+key+'x</li>');
$("p.active").text(key + 'x');
} else {
$("#video_speeds").append(' <li id="'+id+'">'+key+'x</li>');
}
$("#"+id).click(function(){
change_video_speed(key, stream);
$(this).siblings().removeClass("active");
$(this).addClass("active");
var active = $(this).text();
$("p.active").text(active);
});
}
var l=[]
for (var key in streams) {
l.push(key);
}
function sort_by_value(a,b) {
var x=parseFloat(a);
var y=parseFloat(b);
var r=((x < y) ? -1 : ((x > y) ? 1 : 0));
return r;
}
l.sort(sort_by_value);
$(document).ready(function() {
video_speed = $.cookie("video_speed");
//ugly hack to account for different formats in vid speed in the XML (.75 vs 0.75, 1.5 vs 1.50);
if (( !video_speed ) || ( !streams[video_speed] && !streams[video_speed + "0"]) && !streams[video_speed.slice(0,-1)] && !streams[video_speed.slice(1)] && !streams["0" + video_speed]) {
video_speed = "1.0";
}
if (streams[video_speed + "0"]){
video_speed = video_speed + "0";
} else if (streams[video_speed.slice(0, -1)]){
video_speed = video_speed.slice(0, -1);
} else if (streams[video_speed.slice(1)]) {
video_speed = video_speed.slice(1);
} else if (streams["0" + video_speed]) {
video_speed = "0" + video_speed;
}
loadNewVideo(streams["1.0"], streams[video_speed], ${ position });
for(var i=0; i<l.length; i++) {
add_speed(l[i], streams[l[i]])
}
var dropUpHeight = $('ol#video_speeds').height();
console.log(dropUpHeight);
$('ol#video_speeds').css('top', -(dropUpHeight + 2));
});
function toggleVideo(){
if ($("#video_control").hasClass("play")){
play();
$("#video_control").removeClass().addClass("pause");
} else {
pause();
$("#video_control").removeClass().addClass("play");
}
}
$("#video_control").click(toggleVideo);
// space bar to pause video
$(".video-wrapper").keyup(function(e){
active = document.activeElement;
if (e.which == 32) {
e.preventDefault();
$("#video_control").click();
}
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment