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