Home | History | Annotate | Download | only in codewalk
      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 
    299 window.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