PageRenderTime 81ms CodeModel.GetById 43ms RepoModel.GetById 0ms app.codeStats 0ms

/website/static/js/page.js

https://github.com/dazh/Spacelog
JavaScript | 556 lines | 446 code | 82 blank | 28 comment | 65 complexity | 1721ddee45cbb23b984a41e886729bfa MD5 | raw file
  1. Artemis.LogLine = Backbone.Model.extend({
  2. initialize: function(options) {
  3. this.id = parseInt(options.el.attr('id').slice(9), 10);
  4. options.model = this;
  5. this.view = new Artemis.LogLineView(options);
  6. },
  7. getURL: function() {
  8. return this.view.el.find('.time a').attr('href');
  9. },
  10. getTimestamp: function() {
  11. return this.getURL().split('/')[1];
  12. },
  13. getPageURL: function() {
  14. return '/page/'+this.getTimestamp()+'/#log-line-'+this.id;
  15. },
  16. getTranscriptPage: function() {
  17. return this.view.el.attr('data-transcript-page');
  18. },
  19. getText: function() {
  20. return this.view.el.find('dd').text().replace(/^\s+/, '').replace(/\s+$/, '');
  21. },
  22. getTweetableQuote: function() {
  23. var tweetableQuote = this.getText();
  24. if(tweetableQuote.length > 103) {
  25. tweetableQuote = tweetableQuote.substring(0, 100) + '...';
  26. }
  27. return '"' + tweetableQuote + '"';
  28. }
  29. });
  30. Artemis.HighlightedLogLineCollection = Backbone.Collection.extend({
  31. model: Artemis.LogLine,
  32. comparator: function(line) {
  33. return line.id;
  34. },
  35. add: function(model, options) {
  36. Backbone.Collection.prototype.add.call(this, model, options);
  37. this.highlight();
  38. },
  39. remove: function(model, options) {
  40. model.view.unHighlight();
  41. Backbone.Collection.prototype.remove.call(this, model, options);
  42. this.highlight();
  43. },
  44. // This could be more efficient by working it out in _add and _remove
  45. highlight: function() {
  46. this.each(_.bind(function(line) {
  47. line.view.highlight();
  48. }, this));
  49. },
  50. getURL: function() {
  51. var first = this.first().getURL().split('/')[1];
  52. var last = this.last().getURL().split('/')[1];
  53. var l = document.location;
  54. var out = [l.protocol, '//', l.host, '/', first, '/'];
  55. if (first != last) {
  56. out.push(last);
  57. out.push('/');
  58. }
  59. // console.log( this.first().id );
  60. out.push('#log-line-' + this.first().id);
  61. return out.join('');
  62. }
  63. });
  64. Artemis.LogLineView = Backbone.View.extend({
  65. events: {
  66. 'click dd #expand-previous': 'expandPrevious',
  67. 'click dd #expand-next': 'expandNext',
  68. 'click dd #contract-previous': 'contractPrevious',
  69. 'click dd #contract-next': 'contractNext'
  70. },
  71. rangeAdvisoryTemplate: '<p id="range-advisory">\
  72. Spoken on <%= time %><span>. </span><i>Link to this<span> transcript range is</span>:</i> <input type="text" name="" value="<%= permalink %>">\
  73. <iframe allowtransparency="true" frameborder="0" scrolling="no" tabindex="0" class="twitter-share-button twitter-count-horizontal" src="<%= twitter_iframe_url %>" title="Twitter For Websites: Tweet Button"></iframe>\
  74. </p>',
  75. highlight: function() {
  76. this.el.addClass('highlighted');
  77. this.el.css({'cursor': 'auto'});
  78. // Reset range UI
  79. this.el.find('.range-ui').remove();
  80. if (this.model.collection.first().id == this.model.id) {
  81. this.el.addClass('first');
  82. if (this.previousElement()) {
  83. this.addRangeUI('expand-previous');
  84. }
  85. else if (Artemis.transcriptView.loadPreviousButton) {
  86. Artemis.transcriptView.loadPreviousButton.loadMore();
  87. }
  88. this.addRangeUI('selection-close');
  89. if (this.model.collection.size() > 1) {
  90. this.addRangeUI('contract-previous');
  91. }
  92. Artemis.phasesView.setOriginalTranscriptPage(this.model.getTranscriptPage());
  93. }
  94. else {
  95. this.el.removeClass('first');
  96. }
  97. this.removeRangeAdvisory();
  98. if (this.model.collection.last().id == this.model.id) {
  99. this.el.addClass('last');
  100. if (this.nextElement()) {
  101. this.addRangeUI('expand-next');
  102. }
  103. else if (Artemis.transcriptView.loadMoreButton) {
  104. Artemis.transcriptView.loadMoreButton.loadMore();
  105. }
  106. if (this.model.collection.size() > 1) {
  107. this.addRangeUI('contract-next');
  108. }
  109. this.createRangeAdvisory();
  110. }
  111. else {
  112. this.el.removeClass('last');
  113. }
  114. },
  115. unHighlight: function() {
  116. this.el.removeClass('highlighted first last');
  117. this.el.css({'cursor': 'pointer'});
  118. this.el.find('.range-ui').remove();
  119. this.removeRangeAdvisory();
  120. },
  121. addRangeUI: function(id) {
  122. var href = '#';
  123. if (id == 'selection-close') {
  124. href = this.model.getPageURL();
  125. }
  126. this.el.children('dd').append('<a href="'+href+'" class="range-ui" id="'+id+'"></a>');
  127. },
  128. expandPrevious: function() {
  129. var prev = this.previousElement();
  130. if (prev) {
  131. var line = new Artemis.LogLine({el: prev});
  132. this.model.collection.add(line);
  133. }
  134. return false;
  135. },
  136. expandNext: function() {
  137. var next = this.nextElement();
  138. if (next) {
  139. var line = new Artemis.LogLine({el: next});
  140. this.model.collection.add(line);
  141. }
  142. return false;
  143. },
  144. contractPrevious: function() {
  145. this.model.collection.remove(this.model.collection.first());
  146. return false;
  147. },
  148. contractNext: function() {
  149. this.model.collection.remove(this.model.collection.last());
  150. return false;
  151. },
  152. createRangeAdvisory: function() {
  153. if (!this.el.children('#range-advisory').length) {
  154. var twitterURL = "http://platform.twitter.com/widgets/tweet_button.html?count=horizontal&amp;lang=en&amp;" +
  155. "text=" + encodeURIComponent( this.model.collection.first().getTweetableQuote() ) + "&amp;" +
  156. "url=" + encodeURIComponent( this.model.collection.getURL() );
  157. var rangeAdvisory = $(_.template(this.rangeAdvisoryTemplate, {
  158. time: this.model.collection.first().view.el.find('time').data('range-advisory'),
  159. permalink: this.model.collection.getURL(),
  160. twitter_iframe_url: twitterURL
  161. }));
  162. // Select text in text field on focus
  163. rangeAdvisory.find('input').click(function() {
  164. $(this).focus().select();
  165. });
  166. this.el.append(rangeAdvisory);
  167. }
  168. },
  169. removeRangeAdvisory: function() {
  170. this.el.find('#range-advisory').remove();
  171. },
  172. previousElement: function() {
  173. var el = this.el.prevAll('div').get(0);
  174. if (el) {
  175. return $(el);
  176. }
  177. },
  178. nextElement: function() {
  179. var el = this.el.nextAll('div').get(0);
  180. if (el) {
  181. return $(el);
  182. }
  183. }
  184. });
  185. Artemis.LoadMoreButtonView = Backbone.View.extend({
  186. events: {
  187. 'click a': 'loadMore',
  188. },
  189. initialize: function(options) {
  190. _.bindAll(this, 'loadMore', 'loadMoreCallback');
  191. this.isPrevious = options.isPrevious;
  192. },
  193. loadMore: function() {
  194. var a = this.el.children('a');
  195. if (a.size()) {
  196. this.elLast = this.el.clone();
  197. $.getJSON(a.attr('href')+'?json', this.loadMoreCallback);
  198. _gaq.push(['_trackPageview', a.attr('href')]);
  199. Artemis.replaceWithSpinner(a);
  200. }
  201. return false;
  202. },
  203. loadMoreCallback: function(data) {
  204. var content = $(data.content);
  205. var crest = $(data.crest);
  206. var topLogLine = $('#transcript div')[0];
  207. var transcriptTop = $('#transcript')[0].offsetTop;
  208. var initialWindowTop = $( window ).scrollTop();
  209. // We've hit the start of a new phase
  210. if (crest.children().size()) {
  211. // If we're going backwards, show the new crest
  212. if (this.isPrevious) {
  213. $('#crest').replaceWith(data.crest);
  214. }
  215. // Don't load anything if we're highlighted and reached the end of
  216. // a phase
  217. else if (Artemis.transcriptView.highlightedLines.size()) {
  218. return;
  219. }
  220. // If going forwards, skip to next phase
  221. else {
  222. window.location = this.elLast.children('a').attr('href');
  223. return;
  224. }
  225. }
  226. else if (this.isPrevious) {
  227. $('#crest').html('');
  228. }
  229. // To start with, get rid of the spinner
  230. this.el.children().replaceWith(this.elLast.clone().children());
  231. // See if the new content has a button
  232. var newEl = content.find('#'+this.el.attr('id'));
  233. if (newEl.size() && newEl.children().size()) {
  234. this.el.children().replaceWith(newEl.children());
  235. }
  236. else {
  237. this.el.children().remove();
  238. }
  239. // With lines highlighted, hide the button
  240. if (Artemis.transcriptView.highlightedLines.size()) {
  241. this.el.children().hide();
  242. }
  243. // Allow clicking of links in new content
  244. Artemis.transcriptView.bustPreventDefault(content.filter('#transcript'));
  245. // Insert new lines
  246. if (this.isPrevious) {
  247. $('#transcript').prepend(content.filter('#transcript').children());
  248. }
  249. else {
  250. $('#transcript').append(content.filter('#transcript').children());
  251. }
  252. // Rehighlight all rows to add any missing "+" buttons
  253. Artemis.transcriptView.highlightedLines.highlight();
  254. // Readjust height of overlay
  255. Artemis.transcriptView.setOverlayHeight();
  256. // Mark the page boundaries for the window onscroll handler
  257. Artemis.transcriptView.markTranscriptPageBoundaries();
  258. // Keep the topmost item in (almost the same place)
  259. $( window ).scrollTop(
  260. topLogLine.offsetTop - transcriptTop + initialWindowTop
  261. );
  262. // HACK: on detail pages, for some reason a redraw is needed before we
  263. // get the right offsetTop for topLogLine.
  264. setTimeout(function () {
  265. $( window ).scrollTop(
  266. topLogLine.offsetTop - transcriptTop + initialWindowTop
  267. );
  268. }, 0);
  269. },
  270. hide: function() {
  271. this.el.children().fadeOut(Artemis.animationTime);
  272. },
  273. show: function() {
  274. this.el.children().fadeIn(Artemis.animationTime);
  275. }
  276. });
  277. Artemis.TranscriptView = Backbone.View.extend({
  278. el: $('#transcript').parent(),
  279. overlay: $('<div id="highlight-overlay"></div>'),
  280. events: {
  281. 'click #transcript > div': 'selectionOpen',
  282. 'click #transcript > div .time a': 'selectionOpen',
  283. 'click #transcript > div dd #selection-close': 'selectionClose'
  284. },
  285. // The log lines which are currently highlighted
  286. highlightedLines: new Artemis.HighlightedLogLineCollection(),
  287. initialize: function() {
  288. _.bindAll(this, 'selectionClose', 'setOverlayHeight', 'scrollWindow', 'keyDown');
  289. if ($('#load-previous').size()) {
  290. this.loadPreviousButton = new Artemis.LoadMoreButtonView({
  291. el: $('#load-previous'),
  292. isPrevious: true,
  293. });
  294. }
  295. if ($('#load-more').size()) {
  296. this.loadMoreButton = new Artemis.LoadMoreButtonView({
  297. el: $('#load-more'),
  298. });
  299. }
  300. this.overlay.click(this.selectionClose);
  301. this.el.find('#transcript').css({'cursor': 'pointer'});
  302. this.markTranscriptPageBoundaries();
  303. $(window).scroll(this.scrollWindow);
  304. $('body').keydown(this.keyDown);
  305. this.bustPreventDefault(this.el.find('#transcript'));
  306. },
  307. markTranscriptPageBoundaries: function() {
  308. // Mark elements at the end of source transcript pages
  309. // This will give us fewer elements to look at in the window.onscroll handler
  310. var logLineElements = this.el.find('#transcript > div'),
  311. currentPage, i;
  312. for(i = logLineElements.length - 1; i >= 0; i--) {
  313. var ll = $(logLineElements[i]),
  314. page = ll.attr('data-transcript-page');
  315. if(page != currentPage) {
  316. ll.attr('data-end-transcript-page', true);
  317. currentPage = page;
  318. }
  319. }
  320. },
  321. gatherCurrentSelection: function() {
  322. // Gather any currently selected lines
  323. var content = $('#content');
  324. if (content.hasClass('with-highlight')) {
  325. content.removeClass('with-highlight');
  326. if (this.loadPreviousButton) this.loadPreviousButton.hide();
  327. if (this.loadMoreButton) this.loadMoreButton.hide();
  328. _.each($('#transcript > .highlighted'), _.bind(function(e) {
  329. this.highlightedLines.add(
  330. new Artemis.LogLine({el: $(e)})
  331. );
  332. }, this));
  333. this.showOverlay(false);
  334. }
  335. },
  336. selectionOpen: function(e) {
  337. if (this.highlightedLines.size() == 0) {
  338. var target = $(e.currentTarget).closest('div');
  339. var line = new Artemis.LogLine({el: target});
  340. this.highlightedLines.add(line);
  341. if (this.loadPreviousButton) this.loadPreviousButton.hide();
  342. if (this.loadMoreButton) this.loadMoreButton.hide();
  343. this.showOverlay();
  344. line.view.el.find('#range-advisory').hide().show('blind', {}, Artemis.animationTime);
  345. }
  346. return false;
  347. },
  348. selectionClose: function(e) {
  349. this.el.find('.range-ui').fadeOut(Artemis.animationTime);
  350. this.el.find('#range-advisory').hide('blind', Artemis.animationTime, _.bind(function() {
  351. this.highlightedLines.each(function(line) {
  352. line.view.unHighlight();
  353. });
  354. this.highlightedLines = new Artemis.HighlightedLogLineCollection();
  355. }, this));
  356. this.hideOverlay();
  357. if (this.loadPreviousButton) this.loadPreviousButton.show();
  358. if (this.loadMoreButton) this.loadMoreButton.show();
  359. return false;
  360. },
  361. showOverlay: function(animate) {
  362. if (animate === undefined) {
  363. animate = true;
  364. }
  365. this.overlay.css({
  366. 'background-color': 'black',
  367. 'opacity': '0.5'
  368. });
  369. if (animate) {
  370. this.overlay.css({'opacity': '0'});
  371. this.overlay.animate({'opacity': '0.5'}, Artemis.animationTime);
  372. }
  373. // HACK: on detail pages, for some reason a redraw is needed before we
  374. // get the right height
  375. setTimeout(this.setOverlayHeight, 0);
  376. this.overlay.appendTo($('body'));
  377. },
  378. setOverlayHeight: function() {
  379. this.overlay.css({
  380. 'height': ($(document).height() - 38) + 'px'
  381. });
  382. },
  383. hideOverlay: function() {
  384. this.overlay.animate({'opacity': 0}, Artemis.animationTime, _.bind(function() {
  385. this.overlay.detach();
  386. }, this));
  387. },
  388. scrollWindow: function(e) {
  389. if (this.highlightedLines.size() > 0) {
  390. return true;
  391. }
  392. var target = $(window).scrollTop();
  393. var visible = _.detect(
  394. this.el.find('#transcript > div[data-end-transcript-page]'),
  395. function(el) { return el.offsetTop >= target; }
  396. );
  397. if(!visible) {
  398. return;
  399. }
  400. var page = $(visible).attr('data-transcript-page');
  401. Artemis.phasesView.setOriginalTranscriptPage(page);
  402. },
  403. bustPreventDefault: function(transcriptElement) {
  404. // Bust through the div's click event to allow all links to work apart from
  405. // the time link
  406. transcriptElement.find('dt.speaker a, dd a').click(function(e) {
  407. e.stopImmediatePropagation();
  408. return true;
  409. });
  410. },
  411. keyDown: function(e) {
  412. if(e.keyCode === 27) {
  413. this.selectionClose();
  414. return false;
  415. }
  416. else {
  417. return true;
  418. }
  419. }
  420. });
  421. Artemis.PhasesView = Backbone.View.extend({
  422. el: $('#phases'),
  423. events: {
  424. 'click .map': 'toggleMap'
  425. },
  426. cookieName: 'mapIsOpen',
  427. openHeight: 150,
  428. closedHeight: 40.4,
  429. initialize: function() {
  430. if(this.el.find('img.orbital').length === 0) {
  431. return;
  432. }
  433. this.el.find('ul').append('<li><a href="#" class="map">Show map</a></li>');
  434. if (this.getIsOpen()) {
  435. this.el.css({height: this.openHeight});
  436. this.el.addClass("open");
  437. }
  438. },
  439. toggleMap: function() {
  440. var height;
  441. var isOpen = this.getIsOpen();
  442. if (isOpen) {
  443. height = this.closedHeight;
  444. }
  445. else {
  446. height = this.openHeight;
  447. }
  448. this.el.toggleClass('open', !isOpen);
  449. this.el.stop().animate({height: height});
  450. this.setIsOpen(!isOpen);
  451. return false;
  452. },
  453. getIsOpen: function() {
  454. if ($.cookie(this.cookieName) == 'true') {
  455. return true;
  456. }
  457. else {
  458. return false;
  459. }
  460. },
  461. setIsOpen: function(v) {
  462. $.cookie(this.cookieName, v, {path: '/'});
  463. },
  464. setOriginalTranscriptPage: function(page) {
  465. this.el.find('.original a')
  466. .attr('href', '/original/' + page + '/')
  467. .attr('title', 'View original transcript, page ' + page);
  468. }
  469. });
  470. $(function() {
  471. Artemis.phasesView = new Artemis.PhasesView();
  472. Artemis.transcriptView = new Artemis.TranscriptView();
  473. Artemis.transcriptView.gatherCurrentSelection();
  474. });