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 */ 74CodewalkViewer.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 */ 81CodewalkViewer.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 */ 102CodewalkViewer.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 */ 111CodewalkViewer.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 */ 120CodewalkViewer.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 */ 178CodewalkViewer.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 */ 189CodewalkViewer.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 */ 213CodewalkViewer.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 */ 222CodewalkViewer.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 */ 235CodewalkViewer.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 */ 252CodewalkViewer.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 */ 288CodewalkViewer.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 299window.initFuncs.push(function() { 300 var viewer = new CodewalkViewer(jQuery('#codewalk-main')); 301 viewer.selectFirstComment(); 302 viewer.targetCommentLinksAtBlank(); 303 viewer.installEventHandlers(); 304 viewer.updateHeight(); 305}); 306