codewalk.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. // Copyright 2010 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. /**
  5. * A class to hold information about the Codewalk Viewer.
  6. * @param {jQuery} context The top element in whose context the viewer should
  7. * operate. It will not touch any elements above this one.
  8. * @constructor
  9. */
  10. var CodewalkViewer = function(context) {
  11. this.context = context;
  12. /**
  13. * The div that contains all of the comments and their controls.
  14. */
  15. this.commentColumn = this.context.find('#comment-column');
  16. /**
  17. * The div that contains the comments proper.
  18. */
  19. this.commentArea = this.context.find('#comment-area');
  20. /**
  21. * The div that wraps the iframe with the code, as well as the drop down menu
  22. * listing the different files.
  23. * @type {jQuery}
  24. */
  25. this.codeColumn = this.context.find('#code-column');
  26. /**
  27. * The div that contains the code but excludes the options strip.
  28. * @type {jQuery}
  29. */
  30. this.codeArea = this.context.find('#code-area');
  31. /**
  32. * The iframe that holds the code (from Sourcerer).
  33. * @type {jQuery}
  34. */
  35. this.codeDisplay = this.context.find('#code-display');
  36. /**
  37. * The overlaid div used as a grab handle for sizing the code/comment panes.
  38. * @type {jQuery}
  39. */
  40. this.sizer = this.context.find('#sizer');
  41. /**
  42. * The full-screen overlay that ensures we don't lose track of the mouse
  43. * while dragging.
  44. * @type {jQuery}
  45. */
  46. this.overlay = this.context.find('#overlay');
  47. /**
  48. * The hidden input field that we use to hold the focus so that we can detect
  49. * shortcut keypresses.
  50. * @type {jQuery}
  51. */
  52. this.shortcutInput = this.context.find('#shortcut-input');
  53. /**
  54. * The last comment that was selected.
  55. * @type {jQuery}
  56. */
  57. this.lastSelected = null;
  58. };
  59. /**
  60. * Minimum width of the comments or code pane, in pixels.
  61. * @type {number}
  62. */
  63. CodewalkViewer.MIN_PANE_WIDTH = 200;
  64. /**
  65. * Navigate the code iframe to the given url and update the code popout link.
  66. * @param {string} url The target URL.
  67. * @param {Object} opt_window Window dependency injection for testing only.
  68. */
  69. CodewalkViewer.prototype.navigateToCode = function(url, opt_window) {
  70. if (!opt_window) opt_window = window;
  71. // Each iframe is represented by two distinct objects in the DOM: an iframe
  72. // object and a window object. These do not expose the same capabilities.
  73. // Here we need to get the window representation to get the location member,
  74. // so we access it directly through window[] since jQuery returns the iframe
  75. // representation.
  76. // We replace location rather than set so as not to create a history for code
  77. // navigation.
  78. opt_window['code-display'].location.replace(url);
  79. var k = url.indexOf('&');
  80. if (k != -1) url = url.slice(0, k);
  81. k = url.indexOf('fileprint=');
  82. if (k != -1) url = url.slice(k+10, url.length);
  83. this.context.find('#code-popout-link').attr('href', url);
  84. };
  85. /**
  86. * Selects the first comment from the list and forces a refresh of the code
  87. * view.
  88. */
  89. CodewalkViewer.prototype.selectFirstComment = function() {
  90. // TODO(rsc): handle case where there are no comments
  91. var firstSourcererLink = this.context.find('.comment:first');
  92. this.changeSelectedComment(firstSourcererLink);
  93. };
  94. /**
  95. * Sets the target on all links nested inside comments to be _blank.
  96. */
  97. CodewalkViewer.prototype.targetCommentLinksAtBlank = function() {
  98. this.context.find('.comment a[href], #description a[href]').each(function() {
  99. if (!this.target) this.target = '_blank';
  100. });
  101. };
  102. /**
  103. * Installs event handlers for all the events we care about.
  104. */
  105. CodewalkViewer.prototype.installEventHandlers = function() {
  106. var self = this;
  107. this.context.find('.comment')
  108. .click(function(event) {
  109. if (jQuery(event.target).is('a[href]')) return true;
  110. self.changeSelectedComment(jQuery(this));
  111. return false;
  112. });
  113. this.context.find('#code-selector')
  114. .change(function() {self.navigateToCode(jQuery(this).val());});
  115. this.context.find('#description-table .quote-feet.setting')
  116. .click(function() {self.toggleDescription(jQuery(this)); return false;});
  117. this.sizer
  118. .mousedown(function(ev) {self.startSizerDrag(ev); return false;});
  119. this.overlay
  120. .mouseup(function(ev) {self.endSizerDrag(ev); return false;})
  121. .mousemove(function(ev) {self.handleSizerDrag(ev); return false;});
  122. this.context.find('#prev-comment')
  123. .click(function() {
  124. self.changeSelectedComment(self.lastSelected.prev()); return false;
  125. });
  126. this.context.find('#next-comment')
  127. .click(function() {
  128. self.changeSelectedComment(self.lastSelected.next()); return false;
  129. });
  130. // Workaround for Firefox 2 and 3, which steal focus from the main document
  131. // whenever the iframe content is (re)loaded. The input field is not shown,
  132. // but is a way for us to bring focus back to a place where we can detect
  133. // keypresses.
  134. this.context.find('#code-display')
  135. .load(function(ev) {self.shortcutInput.focus();});
  136. jQuery(document).keypress(function(ev) {
  137. switch(ev.which) {
  138. case 110: // 'n'
  139. self.changeSelectedComment(self.lastSelected.next());
  140. return false;
  141. case 112: // 'p'
  142. self.changeSelectedComment(self.lastSelected.prev());
  143. return false;
  144. default: // ignore
  145. }
  146. });
  147. window.onresize = function() {self.updateHeight();};
  148. };
  149. /**
  150. * Starts dragging the pane sizer.
  151. * @param {Object} ev The mousedown event that started us dragging.
  152. */
  153. CodewalkViewer.prototype.startSizerDrag = function(ev) {
  154. this.initialCodeWidth = this.codeColumn.width();
  155. this.initialCommentsWidth = this.commentColumn.width();
  156. this.initialMouseX = ev.pageX;
  157. this.overlay.show();
  158. };
  159. /**
  160. * Handles dragging the pane sizer.
  161. * @param {Object} ev The mousemove event updating dragging position.
  162. */
  163. CodewalkViewer.prototype.handleSizerDrag = function(ev) {
  164. var delta = ev.pageX - this.initialMouseX;
  165. if (this.codeColumn.is('.right')) delta = -delta;
  166. var proposedCodeWidth = this.initialCodeWidth + delta;
  167. var proposedCommentWidth = this.initialCommentsWidth - delta;
  168. var mw = CodewalkViewer.MIN_PANE_WIDTH;
  169. if (proposedCodeWidth < mw) delta = mw - this.initialCodeWidth;
  170. if (proposedCommentWidth < mw) delta = this.initialCommentsWidth - mw;
  171. proposedCodeWidth = this.initialCodeWidth + delta;
  172. proposedCommentWidth = this.initialCommentsWidth - delta;
  173. // If window is too small, don't even try to resize.
  174. if (proposedCodeWidth < mw || proposedCommentWidth < mw) return;
  175. this.codeColumn.width(proposedCodeWidth);
  176. this.commentColumn.width(proposedCommentWidth);
  177. this.options.codeWidth = parseInt(
  178. this.codeColumn.width() /
  179. (this.codeColumn.width() + this.commentColumn.width()) * 100);
  180. this.context.find('#code-column-width').text(this.options.codeWidth + '%');
  181. };
  182. /**
  183. * Ends dragging the pane sizer.
  184. * @param {Object} ev The mouseup event that caused us to stop dragging.
  185. */
  186. CodewalkViewer.prototype.endSizerDrag = function(ev) {
  187. this.overlay.hide();
  188. this.updateHeight();
  189. };
  190. /**
  191. * Toggles the Codewalk description between being shown and hidden.
  192. * @param {jQuery} target The target that was clicked to trigger this function.
  193. */
  194. CodewalkViewer.prototype.toggleDescription = function(target) {
  195. var description = this.context.find('#description');
  196. description.toggle();
  197. target.find('span').text(description.is(':hidden') ? 'show' : 'hide');
  198. this.updateHeight();
  199. };
  200. /**
  201. * Changes the side of the window on which the code is shown and saves the
  202. * setting in a cookie.
  203. * @param {string?} codeSide The side on which the code should be, either
  204. * 'left' or 'right'.
  205. */
  206. CodewalkViewer.prototype.changeCodeSide = function(codeSide) {
  207. var commentSide = codeSide == 'left' ? 'right' : 'left';
  208. this.context.find('#set-code-' + codeSide).addClass('selected');
  209. this.context.find('#set-code-' + commentSide).removeClass('selected');
  210. // Remove previous side class and add new one.
  211. this.codeColumn.addClass(codeSide).removeClass(commentSide);
  212. this.commentColumn.addClass(commentSide).removeClass(codeSide);
  213. this.sizer.css(codeSide, 'auto').css(commentSide, 0);
  214. this.options.codeSide = codeSide;
  215. };
  216. /**
  217. * Adds selected class to newly selected comment, removes selected style from
  218. * previously selected comment, changes drop down options so that the correct
  219. * file is selected, and updates the code popout link.
  220. * @param {jQuery} target The target that was clicked to trigger this function.
  221. */
  222. CodewalkViewer.prototype.changeSelectedComment = function(target) {
  223. var currentFile = target.find('.comment-link').attr('href');
  224. if (!currentFile) return;
  225. if (!(this.lastSelected && this.lastSelected.get(0) === target.get(0))) {
  226. if (this.lastSelected) this.lastSelected.removeClass('selected');
  227. target.addClass('selected');
  228. this.lastSelected = target;
  229. var targetTop = target.position().top;
  230. var parentTop = target.parent().position().top;
  231. if (targetTop + target.height() > parentTop + target.parent().height() ||
  232. targetTop < parentTop) {
  233. var delta = targetTop - parentTop;
  234. target.parent().animate(
  235. {'scrollTop': target.parent().scrollTop() + delta},
  236. Math.max(delta / 2, 200), 'swing');
  237. }
  238. var fname = currentFile.match(/(?:select=|fileprint=)\/[^&]+/)[0];
  239. fname = fname.slice(fname.indexOf('=')+2, fname.length);
  240. this.context.find('#code-selector').val(fname);
  241. this.context.find('#prev-comment').toggleClass(
  242. 'disabled', !target.prev().length);
  243. this.context.find('#next-comment').toggleClass(
  244. 'disabled', !target.next().length);
  245. }
  246. // Force original file even if user hasn't changed comments since they may
  247. // have navigated away from it within the iframe without us knowing.
  248. this.navigateToCode(currentFile);
  249. };
  250. /**
  251. * Updates the viewer by changing the height of the comments and code so that
  252. * they fit within the height of the window. The function is typically called
  253. * after the user changes the window size.
  254. */
  255. CodewalkViewer.prototype.updateHeight = function() {
  256. var windowHeight = jQuery(window).height() - 5 // GOK
  257. var areaHeight = windowHeight - this.codeArea.offset().top
  258. var footerHeight = this.context.find('#footer').outerHeight(true)
  259. this.commentArea.height(areaHeight - footerHeight - this.context.find('#comment-options').outerHeight(true))
  260. var codeHeight = areaHeight - footerHeight - 15 // GOK
  261. this.codeArea.height(codeHeight)
  262. this.codeDisplay.height(codeHeight - this.codeDisplay.offset().top + this.codeArea.offset().top);
  263. this.sizer.height(codeHeight);
  264. };
  265. window.initFuncs.push(function() {
  266. var viewer = new CodewalkViewer(jQuery('#codewalk-main'));
  267. viewer.selectFirstComment();
  268. viewer.targetCommentLinksAtBlank();
  269. viewer.installEventHandlers();
  270. viewer.updateHeight();
  271. });