Commit 6eb29140 by Calen Pennington

Merge pull request #40 from MITx/courseware_module

Courseware module rewrite
parents f467134f 92971127
......@@ -33,7 +33,7 @@ class Command(BaseCommand):
ajax_url='',
state=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:
print "==============> Error in ", etree.tostring(module)
check = False
......
import json
import logging
from lxml import etree
......@@ -130,8 +131,6 @@ def render_x_module(user, request, xml_module, module_object_preload):
# Grab content
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
if user.is_staff:
......@@ -139,14 +138,10 @@ def render_x_module(user, request, xml_module, module_object_preload):
render_histogram = len(histogram) > 0
content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module),
'module_id' : module_id,
'histogram': json.dumps(histogram),
'render_histogram' : render_histogram})
if render_histogram:
init_js = init_js+render_to_string("staff_problem_histogram.js", {'histogram' : histogram,
'module_id' : module_id})
content = {'content':content,
"destroy_js":destory_js,
'init_js':init_js,
content = {'content':content,
'type':module_type}
return content
......
......@@ -61,12 +61,6 @@ class Module(XModule):
'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):
html = self.lcp.get_html()
content={'name':self.name,
......@@ -130,8 +124,8 @@ class Module(XModule):
html=render_to_string('problem.html', context)
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
def __init__(self, system, xml, item_id, state=None):
......
......@@ -28,14 +28,6 @@ class Module(XModule):
self.render()
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):
if dispatch=='goto_position':
self.position = int(get['position'])
......@@ -45,45 +37,39 @@ class Module(XModule):
def render(self):
if self.rendered:
return
def j(m):
def j(m):
''' jsonify contents so it can be embedded in a js array
We also need to split </script> tags so they don't break
mid-string'''
if 'init_js' not in m: m['init_js']=""
if 'type' not in m: m['init_js']=""
content=json.dumps(m['content'])
content=content.replace('</script>', '<"+"/script>')
return {'content':content,
"destroy_js":m['destroy_js'],
'init_js':m['init_js'],
content=json.dumps(m['content'])
content=content.replace('</script>', '<"+"/script>')
return {'content':content,
'type': m['type']}
## 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]
self.titles = json.dumps(["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \
for e in self.xmltree])
titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \
for e in self.xmltree]
self.contents = [j(self.render_function(e)) \
for e in self.xmltree]
self.contents = [j(self.render_function(e)) for e in self.xmltree]
print self.titles
for contents, title in zip(self.contents, titles):
contents['title'] = title
for (content, element_class) in zip(self.contents, child_classes):
new_class = 'other'
for c in class_priority:
if c in element_class:
if c in element_class:
new_class = c
content['type'] = new_class
js=""
params={'items':self.contents,
'id':self.item_id,
'position': self.position,
'titles':self.titles,
'titles':titles,
'tag':self.xmltree.tag}
# TODO/BUG: Destroy JavaScript should only be called for the active view
......@@ -94,16 +80,10 @@ class Module(XModule):
destroy_js="".join([e['destroy_js'] for e in self.contents if 'destroy_js' in e])
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)
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.rendered = True
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
......
......@@ -18,17 +18,8 @@ class Module(XModule):
def get_html(self):
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):
XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml)
self.contents=[(e.get("name"),self.render_function(e)) \
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]])
......@@ -30,32 +30,17 @@ class Module(XModule):
def get_xml_tags(c):
'''Tags in the courseware file guaranteed to correspond to the module'''
return ["video"]
def video_list(self):
l = self.youtube.split(',')
l = [i.split(":") for i in l]
return json.dumps(dict(l))
return self.youtube
def get_html(self):
return render_to_string('video.html',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position,
'name':self.name,
'position':self.position,
'name':self.name,
'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):
XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml)
......@@ -69,5 +54,3 @@ class Module(XModule):
self.annotations=[(e.get("name"),self.render_function(e)) \
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]])
......@@ -78,25 +78,6 @@ class XModule(object):
'''
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):
''' dispatch is last part of the URL.
get is a dictionary-like object '''
......
......@@ -20,6 +20,7 @@ Longer TODO:
"""
import sys
import tempfile
import glob2
import djcelery
from path import path
......@@ -285,13 +286,12 @@ PIPELINE_CSS = {
PIPELINE_JS = {
'application': {
'source_filenames': [
'coffee/src/calculator.coffee',
'coffee/src/courseware.coffee',
'coffee/src/feedback_form.coffee',
'coffee/src/main.coffee'
],
'source_filenames': [path.replace('static/', '') for path in glob2.glob('static/coffee/src/**/*.coffee')],
'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'
}
}
......
......@@ -2,7 +2,8 @@
"js_files": [
"/static/js/jquery-1.6.2.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": [
"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', ->
expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate
it 'prevent default behavior on form submit', ->
jasmine.stubRequests()
$('form#calculator').submit (e) ->
expect(e.isDefaultPrevented()).toBeTruthy()
e.preventDefault()
......@@ -55,12 +56,12 @@ describe 'Calculator', ->
describe 'calculate', ->
beforeEach ->
$('#calculator_input').val '1+2'
spyOn($, 'getJSON').andCallFake (url, data, callback) ->
spyOn($, 'getWithPrefix').andCallFake (url, data, callback) ->
callback({ result: 3 })
@calculator.calculate()
it 'send data to /calculate', ->
expect($.getJSON).toHaveBeenCalledWith '/calculate',
expect($.getWithPrefix).toHaveBeenCalledWith '/calculate',
equation: '1+2'
, 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 'bind', ->
it 'bind the navigation', ->
spyOn Courseware.Navigation, 'bind'
Courseware.bind()
expect(Courseware.Navigation.bind).toHaveBeenCalled()
describe 'start', ->
it 'create the navigation', ->
spyOn(window, 'Navigation')
Courseware.start()
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 ->
loadFixtures 'accordion.html'
@navigation = new Courseware.Navigation
describe 'bind', ->
describe 'when the #accordion exists', ->
describe 'when there is an active section', ->
it 'activate the accordion with correct active section', ->
spyOn $.fn, 'accordion'
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>')
Courseware.Navigation.bind()
expect($('#accordion').accordion).toHaveBeenCalledWith
active: 1
header: 'h3'
autoHeight: false
describe 'when there is no active section', ->
it 'activate the accordian with section 1 as active', ->
spyOn $.fn, 'accordion'
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>')
Courseware.Navigation.bind()
expect($('#accordion').accordion).toHaveBeenCalledWith
active: 1
header: 'h3'
autoHeight: false
it 'binds the accordionchange event', ->
Courseware.Navigation.bind()
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
it 'bind the navigation toggle', ->
Courseware.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'
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'
@courseware = new Courseware
setFixtures """
<div class="course-content">
<div class="sequence"></div>
</div>
"""
it 'binds the content change event', ->
@courseware.bind()
expect($('.course-content .sequence')).toHandleWith 'contentChanged', @courseware.render
describe 'render', ->
beforeEach ->
jasmine.stubRequests()
@courseware = new Courseware
spyOn(window, 'Histogram')
spyOn(window, 'Problem')
spyOn(window, 'Video')
setFixtures """
<div class="course-content">
<div id="video_1" class="video" data-streams="1.0:abc1234"></div>
<div id="video_2" class="video" data-streams="1.0:def5678"></div>
<div id="problem_3" class="problems-wrapper" data-url="/example/url/">
<div id="histogram_3" class="histogram" data-histogram="[[0, 1]]" style="height: 20px; display: block;">
</div>
</div>
"""
@courseware.render()
it 'detect the video elements and convert them', ->
expect(window.Video).toHaveBeenCalledWith('1', '1.0:abc1234')
expect(window.Video).toHaveBeenCalledWith('2', '1.0:def5678')
it 'detect the problem element and convert it', ->
expect(window.Problem).toHaveBeenCalledWith('3', '/example/url/')
it 'detect the histrogram element and convert it', ->
expect(window.Histogram).toHaveBeenCalledWith('3', [[0, 1]])
(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', ->
beforeEach ->
loadFixtures 'feedback_form.html'
describe 'bind', ->
describe 'constructor', ->
beforeEach ->
FeedbackForm.bind()
spyOn($, 'post').andCallFake (url, data, callback, format) ->
new FeedbackForm
spyOn($, 'postWithPrefix').andCallFake (url, data, callback, format) ->
callback()
it 'binds to the #feedback_button', ->
......@@ -16,7 +16,7 @@ describe 'FeedbackForm', ->
$('#feedback_message').val 'This site is really good.'
$('#feedback_button').click()
expect($.post).toHaveBeenCalledWith '/send_feedback', {
expect($.postWithPrefix).toHaveBeenCalledWith '/send_feedback', {
subject: 'Awesome!'
message: 'This site is really good.'
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.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 '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 '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
@bind: ->
calculator = new Calculator
$('.calc').click calculator.toggle
$('form#calculator').submit(calculator.calculate).submit (e) ->
class @Calculator
constructor: ->
$('.calc').click @toggle
$('form#calculator').submit(@calculate).submit (e) ->
e.preventDefault()
$('div.help-wrapper a').hover(calculator.helpToggle).click (e) ->
$('div.help-wrapper a').hover(@helpToggle).click (e) ->
e.preventDefault()
toggle: ->
......@@ -21,5 +20,5 @@ class window.Calculator
$('.help').toggleClass 'shown'
calculate: ->
$.getJSON '/calculate', { equation: $('#calculator_input').val() }, (data) ->
$.getWithPrefix '/calculate', { equation: $('#calculator_input').val() }, (data) ->
$('#calculator_output').val(data.result)
class window.Courseware
@bind: ->
@Navigation.bind()
class @Courseware
@prefix: ''
class @Navigation
@bind: ->
if $('#accordion').length
navigation = new Navigation
active = $('#accordion ul:has(li.active)').index('#accordion ul')
$('#accordion').bind('accordionchange', navigation.log).accordion
active: if active >= 0 then active else 1
header: 'h3'
autoHeight: false
$('#open_close_accordion a').click navigation.toggle
constructor: ->
Courseware.prefix = $("meta[name='path_prefix']").attr('content')
new Navigation
new Calculator
new FeedbackForm
Logger.bind()
@bind()
@render()
log: (event, ui) ->
log_event 'accordion',
newheader: ui.newHeader.text()
oldheader: ui.oldHeader.text()
@start: ->
new Courseware
toggle: ->
$('.course-wrapper').toggleClass('closed')
bind: ->
$('.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
@bind: ->
class @FeedbackForm
constructor: ->
$('#feedback_button').click ->
data =
subject: $('#feedback_subject').val()
message: $('#feedback_message').val()
url: window.location.href
$.post '/send_feedback', data, ->
$.postWithPrefix '/send_feedback', data, ->
$('#feedback_div').html 'Feedback submitted. Thank you'
,'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
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()
$('#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)
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
......@@ -54,6 +54,13 @@ div.course-wrapper {
display: table-cell;
vertical-align: top;
&.problem-header {
section.staff {
margin-top: 30px;
font-size: 80%;
}
}
@media screen and (max-width:1120px) {
display: block;
width: auto;
......@@ -195,6 +202,10 @@ div.course-wrapper {
padding-bottom: 0;
}
.histogram {
width: 200px;
height: 150px;
}
ul {
list-style: disc outside none;
......
......@@ -14,38 +14,29 @@ section.course-content {
}
}
div.video-subtitles {
div.video {
@include clearfix();
background: #f3f3f3;
border-bottom: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1;
@include clearfix();
display: block;
margin: 0 (-(lh()));
padding: 6px lh();
div.video-wrapper {
article.video-wrapper {
float: left;
margin-right: flex-gutter(9);
width: flex-grid(6, 9);
div.video-player {
section.video-player {
height: 0;
overflow: hidden;
padding-bottom: 56.25%;
padding-top: 30px;
position: relative;
object {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
iframe#html5_player {
object, iframe {
border: none;
display: none;
height: 100%;
left: 0;
position: absolute;
......@@ -68,7 +59,7 @@ section.course-content {
}
}
div#slider {
div.slider {
@extend .clearfix;
background: #c2c2c2;
border: none;
......@@ -175,7 +166,8 @@ section.course-content {
}
}
div#vidtime {
div.vidtime {
padding-left: lh(.75);
font-weight: bold;
line-height: 46px; //height of play pause buttons
padding-left: lh(.75);
......@@ -190,8 +182,20 @@ section.course-content {
div.speeds {
float: left;
position: relative;
a {
&.open {
&>a {
background: url('../images/open-arrow.png') 10px center no-repeat;
}
ol.video_speeds {
display: block;
opacity: 1;
}
}
&>a {
background: url('../images/closed-arrow.png') 10px center no-repeat;
border-left: 1px solid #000;
border-right: 1px solid #000;
......@@ -208,15 +212,6 @@ section.course-content {
-webkit-font-smoothing: antialiased;
width: 110px;
&.open {
background: url('../images/open-arrow.png') 10px center no-repeat;
ol#video_speeds {
display: block;
opacity: 1;
}
}
h3 {
color: #999;
float: left;
......@@ -234,47 +229,52 @@ section.course-content {
padding: 0 lh(.5) 0 0;
}
// fix for now
ol#video_speeds {
&:hover, &:active, &:focus {
opacity: 1;
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 {
font-weight: bold;
}
// fix for now
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 {
border-bottom: 0;
@include box-shadow(none);
margin-top: 0;
}
li {
@include box-shadow( 0 1px 0 #555);
border-bottom: 1px solid #000;
color: #fff;
cursor: pointer;
a {
border: 0;
color: #fff;
display: block;
padding: lh(.5);
&:hover {
background-color: #666;
color: #aaa;
}
}
}
&:hover {
background-color: #444;
opacity: 1;
&.active {
font-weight: bold;
}
&:last-child {
@include box-shadow(none);
border-bottom: 0;
margin-top: 0;
}
}
}
}
......@@ -334,7 +334,7 @@ section.course-content {
opacity: 1;
}
div#slider {
div.slider {
height: 14px;
margin-top: -7px;
......@@ -352,15 +352,14 @@ section.course-content {
ol.subtitles {
float: left;
max-height: 460px;
overflow: hidden;
padding-top: 10px;
overflow: auto;
width: flex-grid(3, 9);
li {
border: 0;
color: #666;
cursor: pointer;
margin-bottom: 0px;
margin-bottom: 8px;
padding: 0;
@include transition(all, .5s, ease-in);
......@@ -373,11 +372,7 @@ section.course-content {
color: $mit-red;
}
div {
margin-bottom: 8px;
}
div:empty {
&:empty {
margin-bottom: 0px;
}
}
......@@ -386,7 +381,7 @@ section.course-content {
&.closed {
@extend .trans;
div.video-wrapper {
article.video-wrapper {
width: flex-grid(9,9);
}
......@@ -441,11 +436,11 @@ section.course-content {
}
div.tc-wrapper {
div.video-wrapper {
article.video-wrapper {
width: 100%;
}
object#myytplayer, iframe {
object, iframe {
bottom: 0;
height: 100%;
left: 0;
......@@ -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 {
max-height: 577px;
}
......
......@@ -7,12 +7,10 @@
</%block>
<%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 -->
<script type="text/javascript">
$(function() {
${init}
});
document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
</script>
</%block>
......@@ -26,7 +24,7 @@
<a href="#">close</a>
</header>
<div id="accordion">
<div id="accordion" style="display: none">
<nav>
${accordion}
</nav>
......
<!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">
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
% if dojs == 'math':
onkeyup="DoUpdateMath('${id}')"
% endif
/>
<section id="jstextline_${id}" class="jstextline">
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
% if dojs == 'math':
onkeyup="DoUpdateMath('${id}')"
% endif
/>
% if dojs == 'math':
<span id="display_${id}">`{::}`</span>
......@@ -20,7 +20,7 @@
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
......
......@@ -28,6 +28,7 @@
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
MathJax extension libraries -->
<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>
<body class="<%block name='bodyclass'/>">
......@@ -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.qtip.min.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/cktsim.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.scrollTo-1.4.2-min.js')}"></script>
<%block name="js_extra"/>
</body>
</html>
<%namespace name='static' file='static_content.html'/>
<h2 class="problem-header">${ problem['name'] }
% if problem['weight']:
: ${ problem['weight'] } points
% endif
% if settings.QUICKEDIT:
<span class="staff">
<br/>
<br/>
<br/>
<br/>
<font size=-2><a href=${MITX_ROOT_URL}/quickedit/${id}>Quick
Edit Problem</a></font></span>
% endif
<h2 class="problem-header">
${ problem['name'] }
% if problem['weight']:
: ${ problem['weight'] } points
% endif
% if settings.QUICKEDIT:
<section class="staff">
<a href=${MITX_ROOT_URL}/quickedit/${id}>Quick Edit Problem</a>
</section>
% endif
</h2>
<section class="problem">
......@@ -21,16 +19,16 @@ Edit Problem</a></font></span>
<input type="hidden" name="problem_id" value="${ problem['name'] }">
% if check_button:
<input id="check_${ id }" type="button" value="${ check_button }" >
<input class="check" type="button" value="${ check_button }">
% endif
% if reset_button:
<input id="reset_${ id }" type="button" value="Reset" >
<input class="reset" type="button" value="Reset">
% endif
% if save_button:
<input id="save_${ id }" type="button" value="Save" >
<input class="save" type="button" value="Save">
% endif
% if answer_available:
<input id="show_${ id }" type="button" value="Show Answer" >
<input class="show" type="button" value="Show Answer">
% endif
% if explain :
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
......@@ -42,3 +40,4 @@ Edit Problem</a></font></span>
% endif
</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() {
});
$('#pwd_reset_button').click(function() {
$.post('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
$.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
"email" : $('#id_email').val()}, function(data){
$("#password_reset_complete_link").click();
log_event("profile", {"type":"password_send"});
......
<nav aria-label="Section Navigation" class="sequence-nav">
<ol>
% for t in range(1,1+len(items)):
<li><a href="#" class="seq_inactive" id="tt_${ t }"></a></li>
% endfor
</ol>
<div id="sequence_${id}" class="sequence">
<nav aria-label="Section Navigation" class="sequence-nav">
<ol id="sequence-list">
</ol>
<ul>
<li class="${ id }prev prev"><a href="#">Previous</a></li>
<li class="${ id }next next"><a href="#">Next</a></li>
</ul>
</nav>
<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>
<div id="seq_content"></div>
<nav class="sequence-bottom">
<ul aria-label="Section Navigation">
<li class="${ id }prev prev"><a href="#">Previous</a></li>
<li class="${ id }next next"><a href="#">Next</a></li>
</ul>
</nav>
<nav class="sequence-bottom">
<ul aria-label="Section Navigation" class="sequence-nav-buttons">
<li class="prev"><a href="#">Previous</a></li>
<li class="next"><a href="#">Next</a></li>
</ul>
</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 @@
${xml | h}
</div>
%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
<div id="tabs">
<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 id="tab_${id}" class="tab">
<ul class="navigation"></ul>
</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>
<span id="answer_${id}"></span>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
......@@ -18,17 +18,21 @@
<span class="debug">${msg|n}</span>
<br/>
<br/>
<script>
// Note: We need to make the area follow the CodeMirror for this to
// work.
$(function(){
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"),
{'mode':"python"});
});
</script>
<style type="text/css">
.CodeMirror {border-style: solid;
border-width: 1px;}
</style>
<br/>
<script type="text/javascript" src="${ settings.LIB_URL }CodeMirror/codemirror.js"></script>
<script type="text/javascript" src="${ settings.LIB_URL }CodeMirror/python.js"></script>
<link rel="stylesheet" href="${ settings.LIB_URL }CodeMirror/codemirror.css" />
<script>
// Note: We need to make the area follow the CodeMirror for this to work.
$(function(){
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"), {
mode: "python"
});
});
</script>
<style type="text/css">
.CodeMirror {border-style: solid;
border-width: 1px;}
</style>
</section>
<section class="text-input">
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
/>
<section id="textinput_${id}" class="textinput">
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
/>
<span id="answer_${id}"></span>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
......
......@@ -2,127 +2,18 @@
<h1> ${name} </h1>
% endif
<div class="video-subtitles">
<div class="tc-wrapper">
<div class="video-wrapper">
<div class="video-player">
<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>
<div id="video_${id}" class="video" data-streams="${streams}">
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
<div id="${id}"></div>
</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>
<%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">
% for t in annotations:
<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