blob: 7bfcd39384144a5d27b6095f1c47bffd7579842c [file] [log] [blame]
Russ Cox2a591bd2010-04-26 22:35:12 -07001// 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
Andrew Gerrandd920d8d2013-07-30 14:22:14 +1000299window.initFuncs.push(function() {
Shenghou Ma40778192012-10-05 23:51:40 +0800300 var viewer = new CodewalkViewer(jQuery('#codewalk-main'));
Russ Cox2a591bd2010-04-26 22:35:12 -0700301 viewer.selectFirstComment();
302 viewer.targetCommentLinksAtBlank();
303 viewer.installEventHandlers();
304 viewer.updateHeight();
305});