PageRenderTime 55ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/javascript/socrates-package/socrates.js

https://gitlab.com/gregtyka/KhanLatest
JavaScript | 1009 lines | 771 code | 163 blank | 75 comment | 97 complexity | 9ffeba0e19f06e9188d07c5cef00ef08 MD5 | raw file
  1. Socrates = {};
  2. // this should work with a QuestionView
  3. Socrates.ControlPanel = Backbone.View.extend({
  4. el: ".interactive-video-controls",
  5. controls: [],
  6. events: {
  7. "click button#label": "addLabel",
  8. "click button#inputtext": "addInputText"
  9. },
  10. addLabel: function() {
  11. this.addView(new Socrates.Label());
  12. },
  13. addInputText: function() {
  14. this.addView(new Socrates.InputText());
  15. },
  16. addView: function(view) {
  17. this.controls.push(view);
  18. // place in document before rendering, as jquery.ui checks if element is
  19. // positioned, and positioning is done in external CSS.
  20. this.$controlEl.append(view.el);
  21. view.render();
  22. },
  23. serializeHtml: function() {
  24. _.each(this.controls, function(c) {
  25. c.moveable(false);
  26. });
  27. return this.$controlEl.html();
  28. }
  29. }, {
  30. onReady: function() {
  31. window.ControlPanel = new Socrates.ControlPanel();
  32. }
  33. });
  34. // Editing actions needed:
  35. // 1. Lock / unlock moving (console)
  36. // 2. Delete (console)
  37. // 3. Edit text (dblclick)
  38. Socrates.Label = Backbone.View.extend({
  39. tagName: "div",
  40. className: "label",
  41. events: {
  42. "dblclick": "promptForContents"
  43. },
  44. render: function() {
  45. this.$el.text("Default label contents");
  46. this.moveable(true);
  47. return this;
  48. },
  49. isMoveable: false,
  50. moveable: function(val) {
  51. if (val === this.isMoveable) return this;
  52. if (val == null) {
  53. val = !this.isMoveable;
  54. }
  55. this.isMoveable = val;
  56. if (this.isMoveable) {
  57. this.$el
  58. .addClass("moveable")
  59. .resizable()
  60. .draggable();
  61. } else {
  62. this.$el
  63. .removeClass("moveable")
  64. .resizable("destroy")
  65. .draggable("destroy");
  66. }
  67. return this;
  68. },
  69. promptForContents: function(evt) {
  70. var contents = prompt("Enter label contents", this.$el.text());
  71. this.$el.text(contents);
  72. if (this.isMoveable) {
  73. // need to toggle as .text() destroys the corner thing
  74. this.moveable(false);
  75. this.moveable(true);
  76. }
  77. },
  78. serializedForm: function() {
  79. }
  80. });
  81. Socrates.InputText = Backbone.View.extend({
  82. className: "inputtext",
  83. template: Templates.get("socrates.inputtext"),
  84. events: {
  85. "dblclick": "promptForContents"
  86. },
  87. render: function() {
  88. var contents = this.template({
  89. placeholder: "?"
  90. });
  91. this.$el.html(contents);
  92. this.moveable(true);
  93. return this;
  94. },
  95. isMoveable: false,
  96. moveable: function(val) {
  97. if (val === this.isMoveable) return this;
  98. if (val == null) {
  99. val = !this.isMoveable;
  100. }
  101. this.isMoveable = val;
  102. if (this.isMoveable) {
  103. this.$el
  104. .addClass("moveable")
  105. .resizable()
  106. .draggable();
  107. } else {
  108. this.$el
  109. .removeClass("moveable")
  110. .resizable("destroy")
  111. .draggable("destroy");
  112. }
  113. return this;
  114. },
  115. promptForContents: function(evt) {
  116. var $input = this.$("input");
  117. var contents = prompt("Enter placeholder contents",
  118. $input.attr("placeholder"));
  119. $input.attr("placeholder", contents);
  120. },
  121. serializedForm: function() {
  122. this.$("input").prop("disabled", false);
  123. }
  124. });
  125. Socrates.Bookmark = Backbone.Model.extend({
  126. defaults: {
  127. complete: false
  128. },
  129. seconds: function() {
  130. return Socrates.Question.timeToSeconds(this.get("time"));
  131. },
  132. slug: function() {
  133. return _.str.slugify(this.get("title"));
  134. },
  135. toJSON: function() {
  136. var json = Backbone.Model.prototype.toJSON.call(this);
  137. json.slug = this.slug();
  138. return json;
  139. }
  140. }, {
  141. timeToSeconds: function(time) {
  142. if (time == null || time.length === 0) {
  143. throw "Invalid argument";
  144. }
  145. // convert a string like "4m21s" into just the number of seconds
  146. result = 0;
  147. var i = 0;
  148. while (time[i]) {
  149. var start = i;
  150. while (time[i] && /[\d\.,]/.test(time[i])) i++;
  151. var n = parseFloat(time.slice(start, i));
  152. var unit = time[i] || "s"; // assume seconds if reached end
  153. if (unit == "m") {
  154. result += n * 60;
  155. } else if (unit == "s") {
  156. result += n;
  157. } else {
  158. throw "Unimplemented unit, only ISO8601 durations with mins and secs";
  159. }
  160. i++;
  161. }
  162. return result;
  163. }
  164. });
  165. // todo(dmnd): need to make this less confusing
  166. Socrates.Question = Socrates.Bookmark.extend({
  167. baseSlug: Socrates.Bookmark.prototype.slug,
  168. slug: function() {
  169. return this.baseSlug() + "/q";
  170. },
  171. imageUrl: function() {
  172. return this.get("youtubeId") + "-" + this.get("time");
  173. },
  174. templateName: function() {
  175. return this.get("youtubeId") + "." + this.baseSlug();
  176. }
  177. });
  178. Socrates.QuestionCollection = Backbone.Collection.extend({
  179. model: Socrates.Question
  180. });
  181. Socrates.QuestionView = Backbone.View.extend({
  182. className: "question",
  183. events: {
  184. "submit form": "submit",
  185. "click .submit-area a.skip": "skip",
  186. "click .close": "skip",
  187. "click .submit-area a.see-answer": "seeAnswerClicked"
  188. },
  189. timeDisplayed: 0,
  190. startTime: null,
  191. initialize: function() {
  192. _.extend(this, this.options);
  193. this.version = 1;
  194. this.loaded = false;
  195. this.template = Templates.get(this.model.templateName());
  196. this.render();
  197. },
  198. render: function() {
  199. this.$el.html(this.template({
  200. title: this.model.get("title"),
  201. explainUrl: this.model.get("nested")
  202. }));
  203. // add in a backdrop if necessary
  204. var $screenshot = this.$(".layer.backdrop.videoframe");
  205. if ($screenshot.length > 0) {
  206. $screenshot.append($("<img>", {src: this.imageUrl()}));
  207. }
  208. // linkify the explain button
  209. var parent = this.model.get("nested");
  210. if (parent) {
  211. this.$(".simple-button.explain").attr("href", "#" + parent);
  212. }
  213. this.loaded = true;
  214. return this;
  215. },
  216. qtip: function() {
  217. var qtipq = this.$(".qtip-question");
  218. if (qtipq.length > 0) {
  219. var $controls = this.$('.controls');
  220. $controls.qtip({
  221. content: {
  222. text: qtipq,
  223. title: this.model.get('title')
  224. },
  225. position: $.extend({
  226. container: $controls,
  227. at: [0, 0]
  228. }, this.model.get('qtip-position')),
  229. style: {
  230. classes: "ui-tooltip ui-tooltip-rounded ui-tooltip-shadow"
  231. },
  232. show: {
  233. event: false,
  234. ready: true
  235. },
  236. hide: false
  237. });
  238. }
  239. },
  240. hide: function() {
  241. this.finishRecordingTime();
  242. this.$el.removeClass("visible");
  243. return this;
  244. },
  245. finishRecordingTime: function() {
  246. if (this.startTime) {
  247. this.timeDisplayed += (+new Date() - this.startTime);
  248. this.startTime = null;
  249. } else {
  250. this.timeDisplayed = 0;
  251. }
  252. return this.timeDisplayed;
  253. },
  254. show: function() {
  255. this.startTime = +new Date();
  256. this.$el.addClass("visible");
  257. this.qtip();
  258. return this;
  259. },
  260. imageUrl: function() {
  261. return "/images/videoframes/" + this.model.imageUrl() + ".jpeg";
  262. },
  263. isCorrect: function(data) {
  264. var correctAnswer = this.model.get("correctData");
  265. // if no answer is specified, any answer is correct
  266. if (correctAnswer == null) {
  267. return true;
  268. }
  269. // otherwise make sure they got it right.
  270. // todo: look at how khan-exercise does their fancy number handling
  271. return _.isEqual(data, correctAnswer);
  272. },
  273. getData: function() {
  274. data = {};
  275. // process all matrix-inputs
  276. var $matrixInputs = this.$("table.matrix-input");
  277. data = _.extend(data, this.matrixInputToAnswer($matrixInputs));
  278. // process all checkbox-grids
  279. var $checkboxGrids = this.$("table.checkbox-grid");
  280. data = _.extend(data, this.checkBoxGridToAnswer($checkboxGrids));
  281. // process the result of the inputs
  282. var $inputs = this.$("input").
  283. not($matrixInputs.find("input")).
  284. not($checkboxGrids.find("input"));
  285. data = _.extend(data, this.freeInputsToAnswer($inputs));
  286. return data;
  287. },
  288. matrixInputToAnswer: function($matrixInputs) {
  289. var data = {};
  290. _.each($matrixInputs, function(table) {
  291. var matrix = _.map($(table).find("tr"), function(tr) {
  292. return _.map($(tr).find("input"), function(input) {
  293. return parseFloat($(input).val());
  294. });
  295. });
  296. var name = $(table).attr("name") || "answer";
  297. data[name] = matrix;
  298. });
  299. return data;
  300. },
  301. checkBoxGridToAnswer: function($checkboxGrids) {
  302. var data = {};
  303. _.each($checkboxGrids, function(grid) {
  304. var headers = _.map($(grid).find("thead th"), function(td) {
  305. return $(td).attr("name");
  306. });
  307. headers = _.rest(headers, 1);
  308. var answer = {};
  309. _.each($(grid).find("tbody tr"), function(tr) {
  310. var row = {};
  311. _.each($(tr).find("input"), function(input, i) {
  312. row[headers[i]] = $(input).prop("checked");
  313. });
  314. answer[$(tr).attr("name")] = row;
  315. });
  316. var name = $(grid).attr("name") || "answer";
  317. data[name] = answer;
  318. });
  319. return data;
  320. },
  321. freeInputsToAnswer: function($inputs) {
  322. var data = {};
  323. $inputs.each(function(i, el) {
  324. var $el = $(el);
  325. var key = $el.attr("name");
  326. var val;
  327. if ($el.attr("type") === "checkbox") {
  328. val = $el.prop("checked");
  329. } else if ($el.attr("type") === "radio") {
  330. if ($el.prop("checked")) {
  331. val = $el.val();
  332. } else {
  333. // ignore if it's an unchecked radio button
  334. return true; // continue
  335. }
  336. } else {
  337. val = $el.val();
  338. }
  339. var isArray = false;
  340. if (data[key] != null) {
  341. if (!_.isArray(data[key])) {
  342. data[key] = [data[key]];
  343. }
  344. isArray = true;
  345. }
  346. if (isArray) {
  347. data[key].push(val);
  348. } else {
  349. data[key] = val;
  350. }
  351. });
  352. return data;
  353. },
  354. seeAnswerClicked: function() {
  355. this.$(".submit-area .submit").prop("disabled", true);
  356. this.showMem();
  357. this.loadAnswer();
  358. },
  359. loadAnswer: function() {
  360. var data = $.extend(true, {}, this.model.get("correctData"));
  361. // process all matrix-inputs
  362. var $matrixInputs = this.$("table.matrix-input");
  363. data = this.answerToMatrixInputs($matrixInputs, data);
  364. // process all checkbox-grids
  365. var $checkboxGrids = this.$("table.checkbox-grid");
  366. data = this.answerToCheckboxGrids($checkboxGrids, data);
  367. // process the result of the inputs
  368. var $inputs = this.$("input").
  369. not($matrixInputs.find("input")).
  370. not($checkboxGrids.find("input"));
  371. data = this.answerToFreeInputs($inputs, data);
  372. // by now data should be empty
  373. if (!_.isEmpty(data)) {
  374. console.log("failed to load answer correctly");
  375. }
  376. },
  377. answerToMatrixInputs: function($matrixInputs, data) {
  378. _.each($matrixInputs, function(table) {
  379. var name = $(table).attr("name") || "answer";
  380. var matrix = data[name];
  381. _.each($(table).find("tr"), function(tr, i) {
  382. return _.each($(tr).find("input"), function(input, j) {
  383. $(input).val(matrix[i][j]);
  384. });
  385. });
  386. delete data[name];
  387. });
  388. return data;
  389. },
  390. answerToCheckboxGrids: function($checkboxGrids, data) {
  391. _.each($checkboxGrids, function(grid) {
  392. var name = $(grid).attr("name") || "answer";
  393. var answer = data[name];
  394. var headers = _.map($(grid).find("thead th"), function(td) {
  395. return $(td).attr("name");
  396. });
  397. headers = _.rest(headers, 1);
  398. _.each($(grid).find("tbody tr"), function(tr) {
  399. var rowName = $(tr).attr("name");
  400. _.each($(tr).find("input"), function(input, i) {
  401. $(input).prop("checked", answer[rowName][headers[i]]);
  402. });
  403. });
  404. });
  405. return data;
  406. },
  407. answerToFreeInputs: function($inputs, data) {
  408. $inputs.each(function(i, el) {
  409. var $el = $(el);
  410. var key = $el.attr("name");
  411. var val = data[key];
  412. var isArray = _.isArray(data[key]);
  413. if (isArray) {
  414. val = data[key].pop();
  415. }
  416. // delete the item unless it's a nonempty array
  417. if (!(isArray && !_.isEmpty(data[key]))) {
  418. delete data[key];
  419. }
  420. if ($el.attr("type") === "checkbox") {
  421. $el.prop("checked", val);
  422. } if ($el.attr("type") === "radio") {
  423. if ($el.val() === val) {
  424. $el.prop("checked", true);
  425. }
  426. else {
  427. // put the item back since we can't use it
  428. data[key] = val;
  429. return true; // continue
  430. }
  431. } else {
  432. $el.val(val);
  433. }
  434. });
  435. return data;
  436. },
  437. getResponse: function() {
  438. // get response data
  439. var data = this.getData();
  440. // find how long it took to answer, then reset the countera
  441. var timeDisplayed = this.finishRecordingTime();
  442. this.timeDisplayed = 0;
  443. return {
  444. time: this.model.get("time"),
  445. youtubeId: this.model.get("youtubeId"),
  446. id: this.model.get("id"),
  447. version: this.version,
  448. correct: this.isCorrect(data),
  449. data: data,
  450. timeDisplayed: timeDisplayed
  451. };
  452. },
  453. validateResponse: function(response) {
  454. requiredProps = ["id", "version", "correct", "data", "youtubeId",
  455. "time"];
  456. var hasAllProps = _.all(requiredProps, function(prop) {
  457. return response[prop] != null;
  458. });
  459. if (!hasAllProps) {
  460. console.log(response);
  461. throw "Invalid response from question";
  462. }
  463. return true;
  464. },
  465. alreadyFiredAnswered: false,
  466. fireAnswered: function() {
  467. if (!this.alreadyFiredAnswered) {
  468. this.alreadyFiredAnswered = true;
  469. // notify router that the question was answered correctly
  470. this.trigger("answered");
  471. }
  472. },
  473. submit: function(evt) {
  474. evt.preventDefault();
  475. var $form = $(evt.currentTarget);
  476. var $button = $form.find(".submit");
  477. // when question has been answered correctly, the submit button
  478. // says continue.
  479. if ($button.text() === "Continue") {
  480. this.fireAnswered();
  481. return;
  482. }
  483. // otherwise, get the answer
  484. var response = this.getResponse();
  485. this.validateResponse(response);
  486. // log it on the server side
  487. this.log("submit", response);
  488. // tell the user if they got it right or wrong
  489. if (response.correct) {
  490. this.model.set({"complete": true});
  491. this.$(".submit-area .alert-error").hide();
  492. this.$(".submit-area .alert-success").show();
  493. if ($button) {
  494. $button.html("Continue");
  495. }
  496. if (this.hasMem()) {
  497. this.showMem();
  498. } else {
  499. // otherwise resume the video in 3s
  500. _.delay(_.bind(this.fireAnswered, this), 3000);
  501. }
  502. } else {
  503. this.$(".submit-area .alert-success").hide();
  504. this.$(".submit-area .alert-error").show();
  505. }
  506. },
  507. hasMem: function() {
  508. return this.$(".mem").length > 0;
  509. },
  510. showMem: function() {
  511. this.$(".mem").slideDown(350, 'easeInOutCubic');
  512. },
  513. skip: function() {
  514. var response = this.getResponse();
  515. this.validateResponse(response);
  516. this.log("skip", response);
  517. this.trigger("skipped");
  518. },
  519. log: function(kind, response) {
  520. console.log("POSTing response", kind, response);
  521. }
  522. });
  523. Socrates.MasterView = Backbone.View.extend({
  524. initialize: function(options) {
  525. this.views = options.views;
  526. },
  527. render: function() {
  528. this.$el.append(_.pluck(this.views, "el"));
  529. }
  530. });
  531. Socrates.Nav = Backbone.View.extend({
  532. template: Templates.get("socrates.socrates-nav"),
  533. initialize: function() {
  534. this.model.bind("change", this.render, this);
  535. // only show the event bar when the mouse is hovering over the video
  536. var that = this;
  537. this.options.$hoverContainerEl.hoverIntent(
  538. function() {
  539. that.$(".timebar").fadeIn(300, 'easeInOutCubic');
  540. },
  541. function() {
  542. that.$(".timebar").fadeOut(300, 'easeInOutCubic');
  543. }
  544. );
  545. },
  546. questionsJson: function() {
  547. return this.model.
  548. filter(function(i) {return i.constructor == Socrates.Question;}).
  549. map(function(question) {
  550. var pc = question.seconds() / this.options.videoDuration * 100;
  551. return {
  552. title: question.get("title"),
  553. time: question.get("time"),
  554. slug: question.slug(),
  555. percentage: pc,
  556. complete: question.get("complete") ? "complete" : ""
  557. };
  558. }, this);
  559. },
  560. render: function() {
  561. this.$el.html(this.template({
  562. questions: this.questionsJson()
  563. }));
  564. return this;
  565. }
  566. });
  567. var recursiveTrigger = function recursiveTrigger(triggerFn) {
  568. var t = window.VideoStats.getSecondsWatched();
  569. triggerFn(t);
  570. // schedule another call when the duration is probably ticking over to
  571. // the next tenth of a second
  572. t = window.VideoStats.getSecondsWatched();
  573. var delay = (Poppler.nextPeriod(t, 0.1) - t) * 1000;
  574. _.delay(recursiveTrigger, delay, triggerFn);
  575. };
  576. Socrates.QuestionRouter = Backbone.Router.extend({
  577. routes: {
  578. ":segment": "reactToNewFragment",
  579. ":segment/:qid": "reactToNewFragment"
  580. },
  581. initialize: function(options) {
  582. _.defaults(options, this.constructor.defaults);
  583. this.beep = new Audio("");
  584. var mimeTypes = {
  585. "ogg": "audio/ogg",
  586. "mp3": "audio/mpeg",
  587. "wav": "audio/x-wav"
  588. };
  589. var ext;
  590. var match = _.find(mimeTypes, function(i, k) {
  591. if (this.beep.canPlayType(mimeTypes[k]) !== "") {
  592. ext = k;
  593. return true;
  594. }
  595. return false;
  596. }, this);
  597. if (match) {
  598. this.beep.src = options.beepUrl + "." + ext;
  599. this.beep.volume = options.beepVolume;
  600. } else {
  601. this.beep = null;
  602. }
  603. this.videoControls = options.videoControls;
  604. // listen to player state changes
  605. $(this.videoControls).on("playerStateChange",
  606. _.bind(this.playerStateChange, this));
  607. this.bookmarks = options.bookmarks;
  608. this.questions = this.bookmarks.filter(function(b) {
  609. return b.constructor.prototype === Socrates.Question.prototype;
  610. });
  611. // wrap each question in a view
  612. this.questionViews = this.questions.map(function(question) {
  613. return new Socrates.QuestionView({model: question});
  614. });
  615. // subscribe to submit and skip
  616. _.each(this.questionViews, function(view) {
  617. view.bind("skipped", this.skipped, this);
  618. view.bind("answered", this.submitted, this);
  619. }, this);
  620. // hookup question display to video timelime
  621. this.poppler = new Poppler();
  622. _.each(this.questions, function(q) {
  623. this.poppler.add(q.seconds(), _.bind(this.videoTriggeredQuestion, this, q), q.slug());
  624. }, this);
  625. // trigger poppler every tenth of a second
  626. recursiveTrigger(_.bind(this.poppler.trigger, this.poppler));
  627. },
  628. playerStateChange: function(evt, state) {
  629. if (state === VideoPlayerState.PLAYING) {
  630. if (this.ignoreNextPlay) {
  631. this.ignoreNextPlay = false;
  632. } else {
  633. var t = VideoStats.getSecondsWatched();
  634. this.poppler.seek(t);
  635. }
  636. } else if (state === VideoPlayerState.PAUSED) {
  637. // sometimes the video buffers then pauses. When this happens, allow
  638. // the next play event to cause a seek
  639. this.ignoreNextPlay = false;
  640. } else if (state === VideoPlayerState.BUFFERING) {
  641. // buffering is usually followed by a play event. We only care about
  642. // play events caused by the user scrubbing, so ignore it
  643. this.ignoreNextPlay = true;
  644. }
  645. },
  646. // recieved a question or view, find the corresponding view
  647. questionToView: function(view) {
  648. if (view.constructor.prototype == Socrates.Question.prototype) {
  649. view = _.find(this.questionViews, function(v) { return v.model == view; });
  650. }
  651. return view;
  652. },
  653. reactToNewFragment: function(segment, qid) {
  654. if (qid) {
  655. segment = segment + "/" + qid;
  656. }
  657. // blank fragment for current state of video
  658. if (segment === "") {
  659. this.leaveCurrentState();
  660. }
  661. // top level question
  662. // slug for navigating to a particular question
  663. var question = this.bookmarks.find(function(b) {
  664. return b.slug() === segment;
  665. });
  666. if (question) {
  667. if (question.constructor.prototype === Socrates.Question.prototype) {
  668. this.linkTriggeredQuestion(question);
  669. return;
  670. } else {
  671. // was a bookmark
  672. var seconds = question.seconds();
  673. this.fragmentTriggeredSeek(seconds);
  674. return;
  675. }
  676. }
  677. // seek to time, e.g. 4m32s
  678. try {
  679. var seconds = Socrates.Question.timeToSeconds(slug);
  680. this.fragmentTriggeredSeek(seconds);
  681. return;
  682. } catch (e) {
  683. // ignore
  684. }
  685. // invalid fragment, replace it with nothing
  686. // todo(dmnd) replace playing with something that makes more sense
  687. this.navigate("playing", {replace: true, trigger: true});
  688. },
  689. // called when video was playing and caused a question to trigger
  690. videoTriggeredQuestion: function(question) {
  691. // if questions are disabled, ignore
  692. if (!$(".socrates-enable").prop("checked")) return;
  693. // pause the video
  694. this.videoControls.pause();
  695. if (this.beep != null) {
  696. this.beep.play();
  697. }
  698. // update the fragment in the URL
  699. this.navigate(question.slug());
  700. this.enterState(question);
  701. return true; // block poppler
  702. },
  703. // called when question has been triggered manually via clicking a link
  704. linkTriggeredQuestion: function(question) {
  705. this.videoControls.invokeWhenReady(_.bind(function() {
  706. // notify poppler
  707. this.poppler.blocked = true;
  708. this.poppler.seekToId(question.slug());
  709. this.poppler.eventIndex++; // make poppler only listen to events after the current one
  710. // put video in correct position
  711. this.videoControls.pause();
  712. var state = this.videoControls.player.getPlayerState();
  713. if (state === VideoPlayerState.PAUSED) {
  714. // only seek to the correct spot if we are actually paused
  715. this.videoControls.player.seekTo(question.seconds(), true);
  716. }
  717. this.enterState(question);
  718. }, this));
  719. },
  720. fragmentTriggeredSeek: function(seconds) {
  721. this.leaveCurrentState();
  722. this.videoControls.invokeWhenReady(_.bind(function() {
  723. this.poppler.blocked = true;
  724. this.poppler.seek(seconds);
  725. this.videoControls.player.seekTo(seconds, true);
  726. var state = this.videoControls.player.getPlayerState();
  727. if (state === VideoPlayerState.PAUSED) {
  728. this.videoControls.play();
  729. }
  730. this.poppler.blocked = false;
  731. }, this));
  732. },
  733. enterState: function(view) {
  734. this.leaveCurrentState();
  735. var nextView = this.questionToView(view);
  736. if (nextView) {
  737. this.currentView = nextView;
  738. this.currentView.show();
  739. } else {
  740. console.log("no view, wtf");
  741. }
  742. return this;
  743. },
  744. leaveCurrentState: function() {
  745. if (this.currentView) {
  746. if (this.currentView.hide)
  747. this.currentView.hide();
  748. this.currentView = null;
  749. }
  750. return this;
  751. },
  752. skipped: function() {
  753. var seconds = this.currentView.model.seconds();
  754. this.currentView.hide();
  755. this.navigate("playing");
  756. this.poppler.resumeEvents();
  757. if (this.poppler.blocked) {
  758. // another blocking event was present. Do nothing.
  759. } else {
  760. // no more events left, play video
  761. // prevent seek() from being called
  762. this.ignoreNextPlay = true;
  763. var state = this.videoControls.player.getPlayerState();
  764. if (state == VideoPlayerState.PAUSED) {
  765. this.videoControls.play();
  766. }
  767. else {
  768. this.videoControls.player.seekTo(seconds);
  769. }
  770. }
  771. },
  772. submitted: function() {
  773. this.skipped();
  774. }
  775. }, {
  776. defaults: {
  777. beepUrl: "/sounds/72126__kizilsungur__sweetalertsound2",
  778. beepVolume: 0.3
  779. }
  780. });
  781. Socrates.Skippable = (function() {
  782. var Skippable = function(options) {
  783. _.extend(this, options);
  784. };
  785. Skippable.prototype.seconds = function() {
  786. return _.map(this.span, Socrates.Question.timeToSeconds);
  787. };
  788. Skippable.prototype.trigger = function() {
  789. var pos = this.seconds()[1];
  790. this.videoControls.player.seekTo(pos, true);
  791. };
  792. return Skippable;
  793. })();
  794. Socrates.init = function(youtubeId) {
  795. // Create data
  796. window.Bookmarks = new Backbone.Collection(Socrates.Data[youtubeId].Events);
  797. // Create router which will manage transitions between questions
  798. window.Router = new Socrates.QuestionRouter({
  799. bookmarks: window.Bookmarks,
  800. videoControls: window.VideoControls
  801. });
  802. // For now, don't call Video.init() Just render the page then let our router
  803. // take over.
  804. // todo(dmnd) Integrate socrates & ajax video player routers. May need to
  805. // use hashChange from here: https://github.com/documentcloud/backbone/issues/803
  806. Video.videoLibrary = {};
  807. Video.pushStateDisabled = true;
  808. Video.rendered = true; // Stops video.js from assuming templates are pre-rendered
  809. Video.navigateToVideo(window.location.pathname);
  810. Backbone.history.start({
  811. pushState: false,
  812. root: window.location.pathname
  813. });
  814. // Render views
  815. VideoControls.invokeWhenReady(function() {
  816. var duration = VideoControls.player.getDuration();
  817. window.nav = new Socrates.Nav({
  818. el: ".socrates-nav",
  819. model: Bookmarks,
  820. videoDuration: duration,
  821. $hoverContainerEl: $(".youtube-video")
  822. });
  823. nav.render();
  824. $(".socrates-enable").
  825. prop("checked", true).
  826. parents('li').eq(0).show();
  827. });
  828. window.masterView = new Socrates.MasterView({
  829. el: ".video-overlay",
  830. views: Router.questionViews
  831. });
  832. masterView.render();
  833. };
  834. Socrates.initSkips = function(youtubeId) {
  835. window.skippable = _.map(Socrates.Data[youtubeId].Skips, function(item) {
  836. return new Socrates.Skippable(_.extend(item, {videoControls: window.VideoControls}));
  837. });
  838. _.each(skippable, function(item) {
  839. poppler.add(item.seconds()[0], _.bind(item.trigger, item));
  840. });
  841. };
  842. // This will be populated by video-specific javascript.
  843. Socrates.Data = {};
  844. Handlebars.registerPartial("submit-area", Templates.get("socrates.submit-area"));
  845. // todo(dmnd) only run this in edit mode
  846. $(Socrates.ControlPanel.onReady);