| // Copyright 2010 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| /** | |
| * A class to hold information about the Codewalk Viewer. | |
| * @param {jQuery} context The top element in whose context the viewer should | |
| * operate. It will not touch any elements above this one. | |
| * @constructor | |
| */ | |
| var CodewalkViewer = function(context) { | |
| this.context = context; | |
| /** | |
| * The div that contains all of the comments and their controls. | |
| */ | |
| this.commentColumn = this.context.find('#comment-column'); | |
| /** | |
| * The div that contains the comments proper. | |
| */ | |
| this.commentArea = this.context.find('#comment-area'); | |
| /** | |
| * The div that wraps the iframe with the code, as well as the drop down menu | |
| * listing the different files. | |
| * @type {jQuery} | |
| */ | |
| this.codeColumn = this.context.find('#code-column'); | |
| /** | |
| * The div that contains the code but excludes the options strip. | |
| * @type {jQuery} | |
| */ | |
| this.codeArea = this.context.find('#code-area'); | |
| /** | |
| * The iframe that holds the code (from Sourcerer). | |
| * @type {jQuery} | |
| */ | |
| this.codeDisplay = this.context.find('#code-display'); | |
| /** | |
| * The overlaid div used as a grab handle for sizing the code/comment panes. | |
| * @type {jQuery} | |
| */ | |
| this.sizer = this.context.find('#sizer'); | |
| /** | |
| * The full-screen overlay that ensures we don't lose track of the mouse | |
| * while dragging. | |
| * @type {jQuery} | |
| */ | |
| this.overlay = this.context.find('#overlay'); | |
| /** | |
| * The hidden input field that we use to hold the focus so that we can detect | |
| * shortcut keypresses. | |
| * @type {jQuery} | |
| */ | |
| this.shortcutInput = this.context.find('#shortcut-input'); | |
| /** | |
| * The last comment that was selected. | |
| * @type {jQuery} | |
| */ | |
| this.lastSelected = null; | |
| }; | |
| /** | |
| * Minimum width of the comments or code pane, in pixels. | |
| * @type {number} | |
| */ | |
| CodewalkViewer.MIN_PANE_WIDTH = 200; | |
| /** | |
| * Navigate the code iframe to the given url and update the code popout link. | |
| * @param {string} url The target URL. | |
| * @param {Object} opt_window Window dependency injection for testing only. | |
| */ | |
| CodewalkViewer.prototype.navigateToCode = function(url, opt_window) { | |
| if (!opt_window) opt_window = window; | |
| // Each iframe is represented by two distinct objects in the DOM: an iframe | |
| // object and a window object. These do not expose the same capabilities. | |
| // Here we need to get the window representation to get the location member, | |
| // so we access it directly through window[] since jQuery returns the iframe | |
| // representation. | |
| // We replace location rather than set so as not to create a history for code | |
| // navigation. | |
| opt_window['code-display'].location.replace(url); | |
| var k = url.indexOf('&'); | |
| if (k != -1) url = url.slice(0, k); | |
| k = url.indexOf('fileprint='); | |
| if (k != -1) url = url.slice(k+10, url.length); | |
| this.context.find('#code-popout-link').attr('href', url); | |
| }; | |
| /** | |
| * Selects the first comment from the list and forces a refresh of the code | |
| * view. | |
| */ | |
| CodewalkViewer.prototype.selectFirstComment = function() { | |
| // TODO(rsc): handle case where there are no comments | |
| var firstSourcererLink = this.context.find('.comment:first'); | |
| this.changeSelectedComment(firstSourcererLink); | |
| }; | |
| /** | |
| * Sets the target on all links nested inside comments to be _blank. | |
| */ | |
| CodewalkViewer.prototype.targetCommentLinksAtBlank = function() { | |
| this.context.find('.comment a[href], #description a[href]').each(function() { | |
| if (!this.target) this.target = '_blank'; | |
| }); | |
| }; | |
| /** | |
| * Installs event handlers for all the events we care about. | |
| */ | |
| CodewalkViewer.prototype.installEventHandlers = function() { | |
| var self = this; | |
| this.context.find('.comment') | |
| .click(function(event) { | |
| if (jQuery(event.target).is('a[href]')) return true; | |
| self.changeSelectedComment(jQuery(this)); | |
| return false; | |
| }); | |
| this.context.find('#code-selector') | |
| .change(function() {self.navigateToCode(jQuery(this).val());}); | |
| this.context.find('#description-table .quote-feet.setting') | |
| .click(function() {self.toggleDescription(jQuery(this)); return false;}); | |
| this.sizer | |
| .mousedown(function(ev) {self.startSizerDrag(ev); return false;}); | |
| this.overlay | |
| .mouseup(function(ev) {self.endSizerDrag(ev); return false;}) | |
| .mousemove(function(ev) {self.handleSizerDrag(ev); return false;}); | |
| this.context.find('#prev-comment') | |
| .click(function() { | |
| self.changeSelectedComment(self.lastSelected.prev()); return false; | |
| }); | |
| this.context.find('#next-comment') | |
| .click(function() { | |
| self.changeSelectedComment(self.lastSelected.next()); return false; | |
| }); | |
| // Workaround for Firefox 2 and 3, which steal focus from the main document | |
| // whenever the iframe content is (re)loaded. The input field is not shown, | |
| // but is a way for us to bring focus back to a place where we can detect | |
| // keypresses. | |
| this.context.find('#code-display') | |
| .load(function(ev) {self.shortcutInput.focus();}); | |
| jQuery(document).keypress(function(ev) { | |
| switch(ev.which) { | |
| case 110: // 'n' | |
| self.changeSelectedComment(self.lastSelected.next()); | |
| return false; | |
| case 112: // 'p' | |
| self.changeSelectedComment(self.lastSelected.prev()); | |
| return false; | |
| default: // ignore | |
| } | |
| }); | |
| window.onresize = function() {self.updateHeight();}; | |
| }; | |
| /** | |
| * Starts dragging the pane sizer. | |
| * @param {Object} ev The mousedown event that started us dragging. | |
| */ | |
| CodewalkViewer.prototype.startSizerDrag = function(ev) { | |
| this.initialCodeWidth = this.codeColumn.width(); | |
| this.initialCommentsWidth = this.commentColumn.width(); | |
| this.initialMouseX = ev.pageX; | |
| this.overlay.show(); | |
| }; | |
| /** | |
| * Handles dragging the pane sizer. | |
| * @param {Object} ev The mousemove event updating dragging position. | |
| */ | |
| CodewalkViewer.prototype.handleSizerDrag = function(ev) { | |
| var delta = ev.pageX - this.initialMouseX; | |
| if (this.codeColumn.is('.right')) delta = -delta; | |
| var proposedCodeWidth = this.initialCodeWidth + delta; | |
| var proposedCommentWidth = this.initialCommentsWidth - delta; | |
| var mw = CodewalkViewer.MIN_PANE_WIDTH; | |
| if (proposedCodeWidth < mw) delta = mw - this.initialCodeWidth; | |
| if (proposedCommentWidth < mw) delta = this.initialCommentsWidth - mw; | |
| proposedCodeWidth = this.initialCodeWidth + delta; | |
| proposedCommentWidth = this.initialCommentsWidth - delta; | |
| // If window is too small, don't even try to resize. | |
| if (proposedCodeWidth < mw || proposedCommentWidth < mw) return; | |
| this.codeColumn.width(proposedCodeWidth); | |
| this.commentColumn.width(proposedCommentWidth); | |
| this.options.codeWidth = parseInt( | |
| this.codeColumn.width() / | |
| (this.codeColumn.width() + this.commentColumn.width()) * 100); | |
| this.context.find('#code-column-width').text(this.options.codeWidth + '%'); | |
| }; | |
| /** | |
| * Ends dragging the pane sizer. | |
| * @param {Object} ev The mouseup event that caused us to stop dragging. | |
| */ | |
| CodewalkViewer.prototype.endSizerDrag = function(ev) { | |
| this.overlay.hide(); | |
| this.updateHeight(); | |
| }; | |
| /** | |
| * Toggles the Codewalk description between being shown and hidden. | |
| * @param {jQuery} target The target that was clicked to trigger this function. | |
| */ | |
| CodewalkViewer.prototype.toggleDescription = function(target) { | |
| var description = this.context.find('#description'); | |
| description.toggle(); | |
| target.find('span').text(description.is(':hidden') ? 'show' : 'hide'); | |
| this.updateHeight(); | |
| }; | |
| /** | |
| * Changes the side of the window on which the code is shown and saves the | |
| * setting in a cookie. | |
| * @param {string?} codeSide The side on which the code should be, either | |
| * 'left' or 'right'. | |
| */ | |
| CodewalkViewer.prototype.changeCodeSide = function(codeSide) { | |
| var commentSide = codeSide == 'left' ? 'right' : 'left'; | |
| this.context.find('#set-code-' + codeSide).addClass('selected'); | |
| this.context.find('#set-code-' + commentSide).removeClass('selected'); | |
| // Remove previous side class and add new one. | |
| this.codeColumn.addClass(codeSide).removeClass(commentSide); | |
| this.commentColumn.addClass(commentSide).removeClass(codeSide); | |
| this.sizer.css(codeSide, 'auto').css(commentSide, 0); | |
| this.options.codeSide = codeSide; | |
| }; | |
| /** | |
| * Adds selected class to newly selected comment, removes selected style from | |
| * previously selected comment, changes drop down options so that the correct | |
| * file is selected, and updates the code popout link. | |
| * @param {jQuery} target The target that was clicked to trigger this function. | |
| */ | |
| CodewalkViewer.prototype.changeSelectedComment = function(target) { | |
| var currentFile = target.find('.comment-link').attr('href'); | |
| if (!currentFile) return; | |
| if (!(this.lastSelected && this.lastSelected.get(0) === target.get(0))) { | |
| if (this.lastSelected) this.lastSelected.removeClass('selected'); | |
| target.addClass('selected'); | |
| this.lastSelected = target; | |
| var targetTop = target.position().top; | |
| var parentTop = target.parent().position().top; | |
| if (targetTop + target.height() > parentTop + target.parent().height() || | |
| targetTop < parentTop) { | |
| var delta = targetTop - parentTop; | |
| target.parent().animate( | |
| {'scrollTop': target.parent().scrollTop() + delta}, | |
| Math.max(delta / 2, 200), 'swing'); | |
| } | |
| var fname = currentFile.match(/(?:select=|fileprint=)\/[^&]+/)[0]; | |
| fname = fname.slice(fname.indexOf('=')+2, fname.length); | |
| this.context.find('#code-selector').val(fname); | |
| this.context.find('#prev-comment').toggleClass( | |
| 'disabled', !target.prev().length); | |
| this.context.find('#next-comment').toggleClass( | |
| 'disabled', !target.next().length); | |
| } | |
| // Force original file even if user hasn't changed comments since they may | |
| // have nagivated away from it within the iframe without us knowing. | |
| this.navigateToCode(currentFile); | |
| }; | |
| /** | |
| * Updates the viewer by changing the height of the comments and code so that | |
| * they fit within the height of the window. The function is typically called | |
| * after the user changes the window size. | |
| */ | |
| CodewalkViewer.prototype.updateHeight = function() { | |
| var windowHeight = jQuery(window).height() - 5 // GOK | |
| var areaHeight = windowHeight - this.codeArea.offset().top | |
| var footerHeight = this.context.find('#footer').outerHeight(true) | |
| this.commentArea.height(areaHeight - footerHeight - this.context.find('#comment-options').outerHeight(true)) | |
| var codeHeight = areaHeight - footerHeight - 15 // GOK | |
| this.codeArea.height(codeHeight) | |
| this.codeDisplay.height(codeHeight - this.codeDisplay.offset().top + this.codeArea.offset().top); | |
| this.sizer.height(codeHeight); | |
| }; | |
| window.initFuncs.push(function() { | |
| var viewer = new CodewalkViewer(jQuery('#codewalk-main')); | |
| viewer.selectFirstComment(); | |
| viewer.targetCommentLinksAtBlank(); | |
| viewer.installEventHandlers(); | |
| viewer.updateHeight(); | |
| }); |