| // Copyright (C) 2010 Adam Barth. All rights reserved. |
| // Copyright (C) 2019 Apple Inc. All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, |
| // this list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND ANY |
| // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
| // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH |
| // DAMAGE. |
| var CODE_REVIEW_UNITTEST; |
| |
| (function() { |
| /** |
| * Create a new function with some of its arguements |
| * pre-filled. |
| * Taken from goog.partial in the Closure library. |
| * @param {Function} fn A function to partially apply. |
| * @param {...*} var_args Additional arguments that are partially |
| * applied to fn. |
| * @return {!Function} A partially-applied form of the function. |
| */ |
| function partial(fn, var_args) { |
| var args = Array.prototype.slice.call(arguments, 1); |
| return function() { |
| // Prepend the bound arguments to the current arguments. |
| var newArgs = Array.prototype.slice.call(arguments); |
| newArgs.unshift.apply(newArgs, args); |
| return fn.apply(this, newArgs); |
| }; |
| }; |
| |
| function determineAttachmentID() { |
| try { |
| return /id=(\d+)/.exec(window.location.search)[1] |
| } catch (ex) { |
| return; |
| } |
| } |
| |
| // Attempt to activate only in the "Review Patch" context. |
| if (window.top != window) |
| return; |
| |
| if (!CODE_REVIEW_UNITTEST && !window.location.search.match(/action=review/) |
| && !window.location.toString().match(/bugs\.webkit\.org\/PrettyPatch/)) |
| return; |
| |
| var attachment_id = determineAttachmentID(); |
| if (!attachment_id) |
| console.log('No attachment ID'); |
| |
| var minLeftSideRatio = 10; |
| var maxLeftSideRatio = 90; |
| var file_diff_being_resized = null; |
| var files = {}; |
| var original_file_contents = {}; |
| var patched_file_contents = {}; |
| var WEBKIT_BASE_DIR = "//raw.githubusercontent.com/WebKit/WebKit/main/"; |
| var SIDE_BY_SIDE_DIFFS_KEY = 'sidebysidediffs'; |
| var g_displayed_draft_comments = false; |
| var g_next_line_id = 0; |
| var KEY_CODE = { |
| down: 40, |
| enter: 13, |
| escape: 27, |
| j: 74, |
| k: 75, |
| n: 78, |
| p: 80, |
| r: 82, |
| up: 38 |
| } |
| |
| function idForLine(number) { |
| return 'line' + number; |
| } |
| |
| function forEachLine(callback) { |
| var i = 0; |
| for (var i = 0; i < g_next_line_id; i++) { |
| var line = $('#' + idForLine(i)); |
| if (line[0]) |
| callback(line); |
| } |
| } |
| |
| function hoverify() { |
| $(this).hover(function() { |
| $(this).addClass('hot'); |
| }, |
| function () { |
| $(this).removeClass('hot'); |
| }); |
| } |
| |
| function fileDiffFor(line) { |
| return $(line).parents('.FileDiff'); |
| } |
| |
| function diffSectionFor(line) { |
| return $(line).parents('.DiffSection'); |
| } |
| |
| function activeCommentFor(line) { |
| // Scope to the diffSection as a performance improvement. |
| return $('textarea[data-comment-for~="' + line[0].id + '"]', fileDiffFor(line)); |
| } |
| |
| function previousCommentsFor(line) { |
| // Scope to the diffSection as a performance improvement. |
| return $('div[data-comment-for~="' + line[0].id + '"].previousComment', fileDiffFor(line)); |
| } |
| |
| function findCommentPositionFor(line) { |
| var previous_comments = previousCommentsFor(line); |
| var num_previous_comments = previous_comments.size(); |
| if (num_previous_comments) |
| return $(previous_comments[num_previous_comments - 1]) |
| return line; |
| } |
| |
| function findCommentBlockFor(line) { |
| var comment_block = findCommentPositionFor(line).next(); |
| if (!comment_block.hasClass('comment')) |
| return; |
| return comment_block; |
| } |
| |
| function insertCommentFor(line, block) { |
| findCommentPositionFor(line).after(block); |
| } |
| |
| function addDraftComment(start_line_id, end_line_id, contents) { |
| var line = $('#' + end_line_id); |
| var start = numberFrom(start_line_id); |
| var end = numberFrom(end_line_id); |
| for (var i = start; i <= end; i++) { |
| addDataCommentBaseLine($('#line' + i), end_line_id); |
| } |
| |
| var comment_block = createCommentFor(line); |
| $(comment_block).children('textarea').val(contents); |
| freezeComment(comment_block); |
| } |
| |
| function ensureDraftCommentsDisplayed() { |
| if (g_displayed_draft_comments) |
| return; |
| g_displayed_draft_comments = true; |
| |
| var comments = g_draftCommentSaver.saved_comments(); |
| var errors = []; |
| $(comments.comments).each(function() { |
| try { |
| addDraftComment(this.start_line_id, this.end_line_id, this.contents); |
| } catch (e) { |
| errors.push({'start': this.start_line_id, 'end': this.end_line_id, 'contents': this.contents}); |
| } |
| }); |
| |
| if (errors.length) { |
| console.log('DRAFT COMMENTS WITH ERRORS:', JSON.stringify(errors)); |
| alert('Some draft comments failed to be added. See the console to manually resolve.'); |
| } |
| |
| var overall_comments = comments['overall-comments']; |
| if (overall_comments) { |
| openOverallComments(); |
| $('.overallComments textarea').val(overall_comments); |
| } |
| } |
| |
| function DraftCommentSaver(opt_attachment_id, opt_localStorage) { |
| this._attachment_id = opt_attachment_id || attachment_id; |
| this._localStorage = opt_localStorage || localStorage; |
| this._save_comments = true; |
| } |
| |
| DraftCommentSaver.prototype._json = function() { |
| var comments = $('.comment'); |
| var comment_store = []; |
| comments.each(function () { |
| var file_diff = fileDiffFor(this); |
| var textarea = $('textarea', this); |
| |
| var contents = textarea.val().trim(); |
| if (!contents) |
| return; |
| |
| var comment_base_line = textarea.attr('data-comment-for'); |
| var lines = contextLinesFor(comment_base_line, file_diff); |
| |
| comment_store.push({ |
| start_line_id: lines.first().attr('id'), |
| end_line_id: comment_base_line, |
| contents: contents |
| }); |
| }); |
| |
| var overall_comments = $('.overallComments textarea').val().trim(); |
| return JSON.stringify({'born-on': Date.now(), 'comments': comment_store, 'overall-comments': overall_comments}); |
| } |
| |
| DraftCommentSaver.prototype.localStorageKey = function() { |
| return DraftCommentSaver._keyPrefix + this._attachment_id; |
| } |
| |
| DraftCommentSaver.prototype.saved_comments = function() { |
| var serialized_comments = this._localStorage.getItem(this.localStorageKey()); |
| if (!serialized_comments) |
| return []; |
| |
| var comments = {}; |
| try { |
| comments = JSON.parse(serialized_comments); |
| } catch (e) { |
| this._erase_corrupt_comments(); |
| return {}; |
| } |
| |
| var individual_comments = comments.comments; |
| if (!comments || !comments['born-on'] || !individual_comments || (individual_comments.length && !individual_comments[0].contents)) { |
| this._erase_corrupt_comments(); |
| return {}; |
| } |
| return comments; |
| } |
| |
| DraftCommentSaver.prototype._erase_corrupt_comments = function() { |
| // FIXME: Show an error to the user instead of logging. |
| console.log('Draft comments were corrupted. Erasing comments.'); |
| this.erase(); |
| } |
| |
| DraftCommentSaver.prototype.save = function() { |
| if (!this._save_comments) |
| return; |
| |
| var key = this.localStorageKey(); |
| var value = this._json(); |
| |
| if (this._attemptToWrite(key, value)) |
| return; |
| |
| this._eraseOldCommentsForAllReviews(); |
| if (this._attemptToWrite(key, value)) |
| return; |
| |
| var remove_comments = this._should_remove_comments(); |
| if (!remove_comments) { |
| this._save_comments = false; |
| return; |
| } |
| |
| this._eraseCommentsForAllReviews(); |
| if (this._attemptToWrite(key, value)) |
| return; |
| |
| this._save_comments = false; |
| // FIXME: Show an error to the user. |
| } |
| |
| DraftCommentSaver.prototype._should_remove_comments = function(message) { |
| return prompt('Local storage quota is full. Remove draft comments from all previous reviews to make room?'); |
| } |
| |
| DraftCommentSaver.prototype._attemptToWrite = function(key, value) { |
| try { |
| this._localStorage.setItem(key, value); |
| return true; |
| } catch (e) { |
| return false; |
| } |
| } |
| |
| DraftCommentSaver._keyPrefix = 'draft-comments-for-attachment-'; |
| |
| DraftCommentSaver.prototype.erase = function() { |
| this._localStorage.removeItem(this.localStorageKey()); |
| } |
| |
| DraftCommentSaver.prototype._eraseOldCommentsForAllReviews = function() { |
| this._eraseComments(true); |
| } |
| DraftCommentSaver.prototype._eraseCommentsForAllReviews = function() { |
| this._eraseComments(false); |
| } |
| |
| var MONTH_IN_MS = 1000 * 60 * 60 * 24 * 30; |
| |
| DraftCommentSaver.prototype._eraseComments = function(only_old_reviews) { |
| var length = this._localStorage.length; |
| var keys_to_delete = []; |
| for (var i = 0; i < length; i++) { |
| var key = this._localStorage.key(i); |
| if (key.indexOf(DraftCommentSaver._keyPrefix) != 0) |
| continue; |
| |
| if (only_old_reviews) { |
| try { |
| var born_on = JSON.parse(this._localStorage.getItem(key))['born-on']; |
| if (Date.now() - born_on < MONTH_IN_MS) |
| continue; |
| } catch (e) { |
| console.log('Deleting JSON. JSON for code review is corrupt: ' + key); |
| } |
| } |
| keys_to_delete.push(key); |
| } |
| |
| for (var i = 0; i < keys_to_delete.length; i++) { |
| this._localStorage.removeItem(keys_to_delete[i]); |
| } |
| } |
| |
| var g_draftCommentSaver = new DraftCommentSaver(); |
| |
| function saveDraftComments() { |
| ensureDraftCommentsDisplayed(); |
| g_draftCommentSaver.save(); |
| setAutoSaveStateIndicator('saved'); |
| } |
| |
| function setAutoSaveStateIndicator(state) { |
| var container = $('.autosave-state'); |
| container.text(state); |
| |
| if (state == 'saving') |
| container.addClass(state); |
| else |
| container.removeClass('saving'); |
| } |
| |
| function unfreezeCommentFor(line) { |
| // FIXME: This query is overly complex because we place comment blocks |
| // after Lines. Instead, comment blocks should be children of Lines. |
| findCommentPositionFor(line).next().next().filter('.frozenComment').each(handleUnfreezeComment); |
| } |
| |
| function createCommentFor(line) { |
| if (line.attr('data-has-comment')) { |
| unfreezeCommentFor(line); |
| return; |
| } |
| line.attr('data-has-comment', 'true'); |
| line.addClass('commentContext'); |
| |
| var comment_block = $('<div class="comment"><textarea data-comment-for="' + line.attr('id') + '"></textarea><div class="actions"><button class="ok">OK</button><button class="discard">Discard</button></div></div>'); |
| $('textarea', comment_block).bind('input', handleOverallCommentsInput); |
| insertCommentFor(line, comment_block); |
| return comment_block; |
| } |
| |
| function addCommentFor(line) { |
| var comment_block = createCommentFor(line); |
| if (!comment_block) |
| return; |
| |
| comment_block.hide().slideDown('fast', function() { |
| $(this).children('textarea').focus(); |
| }); |
| return comment_block; |
| } |
| |
| function addCommentField(comment_block) { |
| var id = $(comment_block).attr('data-comment-for'); |
| if (!id) |
| id = comment_block.id; |
| return addCommentFor($('#' + id)); |
| } |
| |
| function handleAddCommentField() { |
| addCommentField(this); |
| } |
| |
| function addPreviousComment(line, author, comment_text) { |
| var line_id = $(line).attr('id'); |
| var comment_block = $('<div data-comment-for="' + line_id + '" class="previousComment"></div>'); |
| var author_block = $('<div class="author"></div>').text(author + ':'); |
| var text_block = $('<div class="content"></div>').text(comment_text); |
| comment_block.append(author_block).append(text_block).each(hoverify).click(handleAddCommentField); |
| addDataCommentBaseLine($(line), line_id); |
| insertCommentFor($(line), comment_block); |
| } |
| |
| function displayPreviousComments(comments) { |
| for (var i = 0; i < comments.length; ++i) { |
| var author = comments[i].author; |
| var file_name = comments[i].file_name; |
| var line_number = comments[i].line_number; |
| var comment_text = comments[i].comment_text; |
| |
| var file = files[file_name]; |
| |
| var query = '.Line .to'; |
| if (line_number[0] == '-') { |
| // The line_number represent a removal. We need to adjust the query to |
| // look at the "from" lines. |
| query = '.Line .from'; |
| // Trim off the '-' control character. |
| line_number = line_number.substr(1); |
| } |
| |
| $(file).find(query).each(function() { |
| if ($(this).text() != line_number) |
| return; |
| var line = lineContainerFromDescendant($(this)); |
| addPreviousComment(line, author, comment_text); |
| }); |
| } |
| |
| if (comments.length == 0) { |
| return; |
| } |
| |
| descriptor = comments.length + ' comment'; |
| if (comments.length > 1) |
| descriptor += 's'; |
| $('.help .more').before(' This patch has ' + descriptor + '. Scroll through them with the "n" and "p" keys. '); |
| } |
| |
| function showMoreHelp() { |
| $('.more-help').removeClass('inactive'); |
| } |
| |
| function hideMoreHelp() { |
| $('.more-help').addClass('inactive'); |
| } |
| |
| function scanForStyleQueueComments(text) { |
| var comments = [] |
| var lines = text.split('\n'); |
| for (var i = 0; i < lines.length; ++i) { |
| var parts = lines[i].match(/^([^:]+):(-?\d+):(.*)$/); |
| if (!parts) |
| continue; |
| |
| var file_name = parts[1]; |
| var line_number = parts[2]; |
| var comment_text = parts[3].trim(); |
| |
| if (!file_name in files) { |
| console.log('Filename in style queue output is not in the patch: ' + file_name); |
| continue; |
| } |
| |
| comments.push({ |
| 'author': 'StyleQueue', |
| 'file_name': file_name, |
| 'line_number': line_number, |
| 'comment_text': comment_text |
| }); |
| } |
| return comments; |
| } |
| |
| function scanForComments(author, text) { |
| var comments = [] |
| var lines = text.split('\n'); |
| for (var i = 0; i < lines.length; ++i) { |
| var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/); |
| if (!parts) |
| continue; |
| var quote_markers = parts[1]; |
| var file_name = parts[2]; |
| // FIXME: Store multiple lines for multiline comments and correctly import them here. |
| var line_number = parts[3]; |
| if (!file_name in files) |
| continue; |
| while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>') |
| ++i; |
| var comment_lines = []; |
| while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) { |
| comment_lines.push(lines[i]); |
| ++i; |
| } |
| --i; // Decrement i because the for loop will increment it again in a second. |
| var comment_text = comment_lines.join('\n').trim(); |
| comments.push({ |
| 'author': author, |
| 'file_name': file_name, |
| 'line_number': line_number, |
| 'comment_text': comment_text |
| }); |
| } |
| return comments; |
| } |
| |
| function isReviewFlag(select) { |
| return $(select).attr('title') == 'Request for patch review.'; |
| } |
| |
| function isCommitQueueFlag(select) { |
| return $(select).attr('title').match(/commit-queue/); |
| } |
| |
| function findControlForFlag(select) { |
| if (isReviewFlag(select)) |
| return $('#toolbar .review select'); |
| else if (isCommitQueueFlag(select)) |
| return $('#toolbar .commitQueue select'); |
| return $(); |
| } |
| |
| function addFlagsForAttachment(details) { |
| var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>"; |
| $('#flagContainer').append( |
| $('<span class="review"> r: ' + flag_control + '</span>')).append( |
| $('<span class="commitQueue"> cq: ' + flag_control + '</span>')); |
| |
| details.find('#flags select').each(function() { |
| var requestee = $(this).parent().siblings('td:first-child').text().trim(); |
| if (requestee.length) { |
| // Remove trailing ':'. |
| requestee = requestee.substr(0, requestee.length - 1); |
| requestee = ' (' + requestee + ')'; |
| } |
| var control = findControlForFlag(this) |
| control.attr('selectedIndex', $(this).attr('selectedIndex')); |
| control.parent().prepend(requestee); |
| }); |
| } |
| |
| window.addEventListener('message', handleStatusBubbleMessage, false); |
| |
| function fetchHistory() { |
| $.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) { |
| var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1]; |
| $.get('show_bug.cgi?id=' + bug_id, function(data) { |
| var comments = []; |
| $(data).find('.bz_comment').each(function() { |
| var author = $(this).find('.email').text().trim(); |
| var text = $(this).find('.bz_comment_text').text(); |
| |
| var comment_marker = 'Comment on attachment ' + attachment_id + ' .details.'; |
| if (text.match(comment_marker)) { |
| $.merge(comments, scanForComments(author, text)); |
| } else { |
| comment_marker = '(From update of attachment ' + attachment_id + ' .details.)'; |
| if (text.match(comment_marker)) |
| $.merge(comments, scanForComments(author, text)); |
| } |
| |
| var style_queue_comment_marker = 'Attachment ' + attachment_id + ' .details. did not pass style-queue.' |
| if (text.match(style_queue_comment_marker)) |
| $.merge(comments, scanForStyleQueueComments(text)); |
| }); |
| displayPreviousComments(comments); |
| ensureDraftCommentsDisplayed(); |
| }); |
| |
| var details = $(data); |
| addFlagsForAttachment(details); |
| |
| var statusBubble = document.createElement('iframe'); |
| statusBubble.src = 'https://ews.webkit.org/status-bubble/' + attachment_id + '?hide_icons=True'; |
| statusBubble.scrolling = 'no'; |
| // Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM. |
| statusBubble.onload = function () { handleStatusBubbleLoad(this); }; |
| $('.statusBubble').append(statusBubble); |
| |
| $('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>'); |
| }); |
| } |
| |
| function firstLine(file_diff) { |
| var container = $('.LineContainer:not(.context)', file_diff)[0]; |
| if (!container) |
| return 0; |
| |
| var from = fromLineNumber(container); |
| var to = toLineNumber(container); |
| return from || to; |
| } |
| |
| function crawlDiff() { |
| g_next_line_id = 0; |
| var idify = function() { |
| this.id = idForLine(g_next_line_id++); |
| } |
| |
| $('.Line').each(idify).each(hoverify); |
| $('.FileDiff').each(function() { |
| var header = $(this).children('h1'); |
| var url_hash = '#L' + firstLine(this); |
| |
| var file_name = header.text(); |
| files[file_name] = this; |
| |
| addExpandLinks(file_name); |
| |
| var diff_links = $('<div class="FileDiffLinkContainer LinkContainer">' + |
| diffLinksHtml() + |
| '</div>'); |
| |
| var file_link = $('a', header)[0]; |
| // If the base directory in the file path does not match a WebKit top level directory, |
| // then PrettyPatch.rb doesn't linkify the header. |
| if (file_link) { |
| file_link.target = "_blank"; |
| file_link.href += url_hash; |
| diff_links.append(tracLinks(file_name, url_hash)); |
| } |
| |
| $('h1', this).after(diff_links); |
| updateDiffLinkVisibility(this); |
| }); |
| } |
| |
| function tracLinks(file_name, url_hash) { |
| var trac_links = $('<a target="_blank">annotate</a><a target="_blank">revision log</a>'); |
| trac_links[0].href = 'https://github.com/WebKit/WebKit/blame/main/' + file_name + url_hash; |
| trac_links[1].href = 'https://github.com/WebKit/WebKit/commits/main/' + file_name; |
| var implementation_suffix_list = ['.cpp', '.mm']; |
| for (var i = 0; i < implementation_suffix_list.length; ++i) { |
| var suffix = implementation_suffix_list[i]; |
| if (file_name.lastIndexOf(suffix) == file_name.length - suffix.length) { |
| var new_link = $('<a target="_blank">header</a>'); |
| var stem = file_name.substr(0, file_name.length - suffix.length); |
| new_link[0].href= 'https://github.com/WebKit/WebKit/commits/main/' + stem + '.h'; |
| trac_links = $.merge(new_link, trac_links); |
| } |
| } |
| return trac_links; |
| } |
| |
| function isChangeLog(file_name) { |
| return file_name.match(/\/ChangeLog$/) || file_name == 'ChangeLog'; |
| } |
| |
| function addExpandLinks(file_name) { |
| if (isChangeLog(file_name)) |
| return; |
| |
| var file_diff = files[file_name]; |
| |
| // Don't show the links to expand upwards/downwards if the patch starts/ends without context |
| // lines, i.e. starts/ends with add/remove lines. |
| var first_line = file_diff.querySelector('.LineContainer:not(.context)'); |
| |
| // If there is no element with a "Line" class, then this is an image diff. |
| if (!first_line) |
| return; |
| |
| var expand_bar_index = 0; |
| if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove')) |
| $('h1', file_diff).after(expandBarHtml(BELOW)) |
| |
| $('br', file_diff).replaceWith(expandBarHtml()); |
| |
| // jquery doesn't support :last-of-type, so use querySelector instead. |
| var last_line = file_diff.querySelector('.LineContainer:last-of-type'); |
| // Some patches for new files somehow end up with an empty context line at the end |
| // with a from line number of 0. Don't show expand links in that case either. |
| if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0) |
| $(file_diff.querySelector('.DiffSection:last-of-type')).after(expandBarHtml(ABOVE)); |
| } |
| |
| function expandBarHtml(opt_direction) { |
| var html = '<div class="ExpandBar">' + |
| '<div class="ExpandArea Expand' + ABOVE + '"></div>' + |
| '<div class="ExpandLinkContainer LinkContainer"><span class="ExpandText">expand: </span>'; |
| |
| // FIXME: If there are <100 line to expand, don't show the expand-100 link. |
| // If there are <20 lines to expand, don't show the expand-20 link. |
| if (!opt_direction || opt_direction == ABOVE) { |
| html += expandLinkHtml(ABOVE, 100) + |
| expandLinkHtml(ABOVE, 20); |
| } |
| |
| html += expandLinkHtml(ALL); |
| |
| if (!opt_direction || opt_direction == BELOW) { |
| html += expandLinkHtml(BELOW, 20) + |
| expandLinkHtml(BELOW, 100); |
| } |
| |
| html += '</div><div class="ExpandArea Expand' + BELOW + '"></div></div>'; |
| return html; |
| } |
| |
| function expandLinkHtml(direction, amount) { |
| return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" + |
| (amount ? amount + " " : "") + direction + "</a>"; |
| } |
| |
| function handleExpandLinkClick() { |
| var expand_bar = $(this).parents('.ExpandBar'); |
| var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent; |
| var expand_function = partial(expand, expand_bar[0], file_name, this.getAttribute('data-direction'), Number(this.getAttribute('data-amount'))); |
| if (file_name in original_file_contents) |
| expand_function(); |
| else |
| getWebKitSourceFile(file_name, expand_function, expand_bar); |
| } |
| |
| function handleSideBySideLinkClick() { |
| convertDiff('sidebyside', this); |
| } |
| |
| function handleUnifyLinkClick() { |
| convertDiff('unified', this); |
| } |
| |
| function convertDiff(difftype, convert_link) { |
| var file_diffs = $(convert_link).parents('.FileDiff'); |
| if (!file_diffs.size()) { |
| localStorage.setItem('code-review-diffstate', difftype); |
| file_diffs = $('.FileDiff'); |
| } |
| |
| convertAllFileDiffs(difftype, file_diffs); |
| } |
| |
| function patchRevision() { |
| var revision = $('.revision'); |
| return revision[0] ? revision.first().text() : null; |
| } |
| |
| function setFileContents(file_name, original_contents, patched_contents) { |
| original_file_contents[file_name] = original_contents; |
| patched_file_contents[file_name] = patched_contents; |
| } |
| |
| function getWebKitSourceFile(file_name, onLoad, expand_bar) { |
| function handleLoad(contents) { |
| var split_contents = contents.split('\n'); |
| setFileContents(file_name, split_contents, applyDiff(split_contents, file_name)); |
| onLoad(); |
| }; |
| |
| var revision = patchRevision(); |
| var queryParameters = revision ? '?p=' + revision : ''; |
| |
| $.ajax({ |
| url: WEBKIT_BASE_DIR + file_name + queryParameters, |
| context: document.body, |
| complete: function(xhr, data) { |
| if (xhr.status == 0) |
| handleLoadError(expand_bar); |
| else |
| handleLoad(xhr.responseText); |
| } |
| }); |
| } |
| |
| function replaceExpandLinkContainers(expand_bar, text) { |
| $('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>'); |
| } |
| |
| function handleLoadError(expand_bar) { |
| replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?"); |
| } |
| |
| var ABOVE = 'above'; |
| var BELOW = 'below'; |
| var ALL = 'all'; |
| |
| function lineNumbersFromSet(set, is_last) { |
| var to = -1; |
| var from = -1; |
| |
| var size = set.size(); |
| var start = is_last ? (size - 1) : 0; |
| var end = is_last ? -1 : size; |
| var offset = is_last ? -1 : 1; |
| |
| for (var i = start; i != end; i += offset) { |
| if (to != -1 && from != -1) |
| return {to: to, from: from}; |
| |
| var line_number = set[i]; |
| if ($(line_number).hasClass('to')) { |
| if (to == -1) |
| to = Number(line_number.textContent); |
| } else { |
| if (from == -1) |
| from = Number(line_number.textContent); |
| } |
| } |
| } |
| |
| function removeContextBarBelow(expand_bar) { |
| $('.context', expand_bar.nextElementSibling).detach(); |
| } |
| |
| function expand(expand_bar, file_name, direction, amount) { |
| if (file_name in original_file_contents && !patched_file_contents[file_name]) { |
| // FIXME: In this case, try fetching the source file at the revision the patch was created at. |
| // Might need to modify webkit-patch to include that data in the diff. |
| replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree."); |
| return; |
| } |
| |
| var above_expansion = expand_bar.querySelector('.Expand' + ABOVE) |
| var below_expansion = expand_bar.querySelector('.Expand' + BELOW) |
| |
| var above_line_numbers = $('.expansionLineNumber', above_expansion); |
| if (!above_line_numbers[0]) { |
| var diff_section = expand_bar.previousElementSibling; |
| above_line_numbers = $('.Line:not(.context) .lineNumber', diff_section); |
| } |
| |
| var above_last_line_num, above_last_from_line_num; |
| if (above_line_numbers[0]) { |
| var above_numbers = lineNumbersFromSet(above_line_numbers, true); |
| above_last_line_num = above_numbers.to; |
| above_last_from_line_num = above_numbers.from; |
| } else |
| above_last_from_line_num = above_last_line_num = 0; |
| |
| var below_line_numbers = $('.expansionLineNumber', below_expansion); |
| if (!below_line_numbers[0]) { |
| var diff_section = expand_bar.nextElementSibling; |
| if (diff_section) |
| below_line_numbers = $('.Line:not(.context) .lineNumber', diff_section); |
| } |
| |
| var below_first_line_num, below_first_from_line_num; |
| if (below_line_numbers[0]) { |
| var below_numbers = lineNumbersFromSet(below_line_numbers, false); |
| below_first_line_num = below_numbers.to - 1; |
| below_first_from_line_num = below_numbers.from - 1; |
| } else |
| below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1; |
| |
| var start_line_num, start_from_line_num; |
| var end_line_num; |
| |
| if (direction == ABOVE) { |
| start_from_line_num = above_last_from_line_num; |
| start_line_num = above_last_line_num; |
| end_line_num = Math.min(start_line_num + amount, below_first_line_num); |
| } else if (direction == BELOW) { |
| end_line_num = below_first_line_num; |
| start_line_num = Math.max(end_line_num - amount, above_last_line_num) |
| start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num) |
| } else { // direction == ALL |
| start_line_num = above_last_line_num; |
| start_from_line_num = above_last_from_line_num; |
| end_line_num = below_first_line_num; |
| } |
| |
| var lines = expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num); |
| |
| var expansion_area; |
| // Filling in all the remaining lines. Overwrite the expand links. |
| if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) { |
| $('.ExpandLinkContainer', expand_bar).detach(); |
| below_expansion.insertBefore(lines, below_expansion.firstChild); |
| removeContextBarBelow(expand_bar); |
| } else if (direction == ABOVE) { |
| above_expansion.appendChild(lines); |
| } else { |
| below_expansion.insertBefore(lines, below_expansion.firstChild); |
| removeContextBarBelow(expand_bar); |
| } |
| } |
| |
| function unifiedLine(from, to, contents, is_expansion_line, opt_className, opt_attributes) { |
| var className = is_expansion_line ? 'ExpansionLine' : 'LineContainer Line'; |
| if (opt_className) |
| className += ' ' + opt_className; |
| |
| var lineNumberClassName = is_expansion_line ? 'expansionLineNumber' : 'lineNumber'; |
| |
| var line = $('<div class="' + className + '" ' + (opt_attributes || '') + '>' + |
| '<span class="from ' + lineNumberClassName + '">' + (from || ' ') + |
| '</span><span class="to ' + lineNumberClassName + '">' + (to || ' ') + |
| '</span><span class="text"></span>' + |
| '</div>'); |
| |
| $('.text', line).replaceWith(contents); |
| return line; |
| } |
| |
| function unifiedExpansionLine(from, to, contents) { |
| return unifiedLine(from, to, contents, true); |
| } |
| |
| function sideBySideExpansionLine(from, to, contents) { |
| var line = $('<div class="ExpansionLine"></div>'); |
| // Clone the contents so we have two copies we can put back in the DOM. |
| line.append(lineSide('from', contents.clone(true), true, from)); |
| line.append(lineSide('to', contents, true, to)); |
| return line; |
| } |
| |
| function lineSide(side, contents, is_expansion_line, opt_line_number, opt_attributes, opt_class) { |
| var class_name = ''; |
| if (opt_attributes || opt_class) { |
| class_name = 'class="'; |
| if (opt_attributes) |
| class_name += is_expansion_line ? 'ExpansionLine' : 'Line'; |
| class_name += ' ' + (opt_class || '') + '"'; |
| } |
| |
| var attributes = opt_attributes || ''; |
| |
| var line_side = $('<div class="LineSide">' + |
| '<div ' + attributes + ' ' + class_name + '>' + |
| '<span class="' + side + ' ' + (is_expansion_line ? 'expansionLineNumber' : 'lineNumber') + '">' + |
| (opt_line_number || ' ') + |
| '</span>' + |
| '<span class="text"></span>' + |
| '</div>' + |
| '</div>'); |
| |
| $('.text', line_side).replaceWith(contents); |
| return line_side; |
| } |
| |
| function expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) { |
| var fragment = document.createDocumentFragment(); |
| var is_side_by_side = isDiffSideBySide(files[file_name]); |
| |
| for (var i = 0; i < end_line_num - start_line_num; i++) { |
| var from = start_from_line_num + i + 1; |
| var to = start_line_num + i + 1; |
| var contents = $('<span class="text"></span>'); |
| contents.text(patched_file_contents[file_name][start_line_num + i]); |
| var line = is_side_by_side ? sideBySideExpansionLine(from, to, contents) : unifiedExpansionLine(from, to, contents); |
| fragment.appendChild(line[0]); |
| } |
| |
| return fragment; |
| } |
| |
| function hunkStartingLine(patched_file, context, prev_line, hunk_num) { |
| var current_line = -1; |
| var last_context_line = context[context.length - 1]; |
| if (patched_file[prev_line] == last_context_line) |
| current_line = prev_line + 1; |
| else { |
| console.log('Hunk #' + hunk_num + ' FAILED.'); |
| return -1; |
| } |
| |
| // For paranoia sake, confirm the rest of the context matches; |
| for (var i = 0; i < context.length - 1; i++) { |
| if (patched_file[current_line - context.length + i] != context[i]) { |
| console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.'); |
| return -1; |
| } |
| } |
| |
| return current_line; |
| } |
| |
| function fromLineNumber(line) { |
| var node = line.querySelector('.from'); |
| return node ? Number(node.textContent) : 0; |
| } |
| |
| function toLineNumber(line) { |
| var node = line.querySelector('.to'); |
| return node ? Number(node.textContent) : 0; |
| } |
| |
| function textContentsFor(line) { |
| // Just get the first match since a side-by-side diff has two lines with text inside them for |
| // unmodified lines in the diff. |
| return $('.text', line).first().text(); |
| } |
| |
| function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) { |
| if (context.length) { |
| var prev_line_num = fromLineNumber(prev_line) - 1; |
| return hunkStartingLine(patched_file, context, prev_line_num, hunk_num); |
| } |
| |
| if (toLineNumber(line) == 1 || fromLineNumber(line) == 1) |
| return 0; |
| |
| console.log('Failed to apply patch. Adds or removes lines before any context lines.'); |
| return -1; |
| } |
| |
| function applyDiff(original_file, file_name) { |
| var diff_sections = files[file_name].getElementsByClassName('DiffSection'); |
| var patched_file = original_file.concat([]); |
| |
| // Apply diffs in reverse order to avoid needing to keep track of changing line numbers. |
| for (var i = diff_sections.length - 1; i >= 0; i--) { |
| var section = diff_sections[i]; |
| var lines = $('.Line:not(.context)', section); |
| var current_line = -1; |
| var context = []; |
| var hunk_num = i + 1; |
| |
| for (var j = 0, lines_len = lines.length; j < lines_len; j++) { |
| var line = lines[j]; |
| var line_contents = textContentsFor(line); |
| if ($(line).hasClass('add')) { |
| if (current_line == -1) { |
| current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num); |
| if (current_line == -1) |
| return null; |
| } |
| |
| patched_file.splice(current_line, 0, line_contents); |
| current_line++; |
| } else if ($(line).hasClass('remove')) { |
| if (current_line == -1) { |
| current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num); |
| if (current_line == -1) |
| return null; |
| } |
| |
| if (patched_file[current_line] != line_contents) { |
| console.log('Hunk #' + hunk_num + ' FAILED.'); |
| return null; |
| } |
| |
| patched_file.splice(current_line, 1); |
| } else if (current_line == -1) { |
| context.push(line_contents); |
| } else if (line_contents != patched_file[current_line]) { |
| console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match'); |
| return null; |
| } else { |
| current_line++; |
| } |
| } |
| } |
| |
| return patched_file; |
| } |
| |
| function openOverallComments(e) { |
| $('.overallComments textarea').addClass('open'); |
| $('.statusBubble').addClass('wrap'); |
| } |
| |
| var g_overallCommentsInputTimer; |
| |
| function handleOverallCommentsInput() { |
| setAutoSaveStateIndicator('saving'); |
| // Save draft comments after we haven't received an input event in 1 second. |
| if (g_overallCommentsInputTimer) |
| clearTimeout(g_overallCommentsInputTimer); |
| g_overallCommentsInputTimer = setTimeout(saveDraftComments, 1000); |
| } |
| |
| function diffLinksHtml() { |
| return '<a href="javascript:" class="unify-link">unified</a>' + |
| '<a href="javascript:" class="side-by-side-link">side-by-side</a>'; |
| } |
| |
| function appendToolbar() { |
| $(document.body).append('<div id="toolbar">' + |
| '<div class="overallComments">' + |
| '<textarea placeholder="Overall comments"></textarea>' + |
| '</div>' + |
| '<div>' + |
| '<span class="statusBubble"></span>' + |
| '<span class="actions">' + |
| '<span class="links"><span class="bugLink"></span></span>' + |
| '<span id="flagContainer"></span>' + |
| '<button id="preview_comments">Preview</button>' + |
| '<button id="post_comments">Publish</button> ' + |
| '</span>' + |
| '<div class="clear_float"></div>' + |
| '</div>' + |
| '<div class="autosave-state"></div>' + |
| '</div>'); |
| |
| $('.overallComments textarea').bind('click', openOverallComments); |
| $('.overallComments textarea').bind('input', handleOverallCommentsInput); |
| |
| var toolbar = $('#toolbar'); |
| toolbar.css('position', '-webkit-sticky'); |
| var supportsSticky = toolbar.css('position') == '-webkit-sticky'; |
| document.body.style.marginBottom = supportsSticky ? 0 : '40px'; |
| } |
| |
| function handleDocumentReady() { |
| crawlDiff(); |
| fetchHistory(); |
| |
| $(document.body).prepend('<div id="message">' + |
| '<div class="help">Select line numbers to add a comment. Scroll though diffs with the "j" and "k" keys.' + |
| '<div class="DiffLinks LinkContainer">' + |
| '<input type="checkbox" id="line-number-on-copy"><label for="line-number-on-copy">Skip line numbers on copy</label>' + |
| diffLinksHtml() + |
| '</div>' + |
| '<a href="javascript:" class="more">[more]</a>' + |
| '<div class="more-help inactive">' + |
| '<div class="winter"></div>' + |
| '<div class="lightbox"><table>' + |
| '<tr><td>enter</td><td>add/edit comment for focused item</td></tr>' + |
| '<tr><td>escape</td><td>accept current comment / close preview and help popups</td></tr>' + |
| '<tr><td>j</td><td>focus next diff</td></tr>' + |
| '<tr><td>k</td><td>focus previous diff</td></tr>' + |
| '<tr><td>shift + j</td><td>focus next line</td></tr>' + |
| '<tr><td>shift + k</td><td>focus previous line</td></tr>' + |
| '<tr><td>n</td><td>focus next comment</td></tr>' + |
| '<tr><td>p</td><td>focus previous comment</td></tr>' + |
| '<tr><td>r</td><td>focus review select element</td></tr>' + |
| '<tr><td>ctrl + shift + up</td><td>extend context of the focused comment</td></tr>' + |
| '<tr><td>ctrl + shift + down</td><td>shrink context of the focused comment</td></tr>' + |
| '</table></div>' + |
| '</div>' + |
| '</div>' + |
| '</div>'); |
| |
| appendToolbar(); |
| |
| $(document.body).prepend('<div id="comment_form" class="inactive"><div class="winter"></div><div class="lightbox"><iframe id="reviewform" src="attachment.cgi?id=' + attachment_id + '&action=reviewform"></iframe></div></div>'); |
| $('#reviewform').bind('load', handleReviewFormLoad); |
| |
| loadDiffState(); |
| generateFileDiffResizeStyleElement(); |
| updateLineNumberOnCopyLinkContents(); |
| |
| document.body.addEventListener('copy', handleCopy); |
| }; |
| |
| function forEachNode(nodeList, callback) { |
| Array.prototype.forEach.call(nodeList, callback); |
| } |
| |
| $('#line-number-on-copy').live('click', toggleShouldStripLineNumbersOnCopy); |
| |
| function updateLineNumberOnCopyLinkContents() { |
| document.getElementById('line-number-on-copy').checked = shouldStripLineNumbersOnCopy(); |
| } |
| |
| function shouldStripLineNumbersOnCopy() { |
| return localStorage.getItem('code-review-line-numbers-on-copy') == 'true'; |
| } |
| |
| function toggleShouldStripLineNumbersOnCopy() { |
| localStorage.setItem('code-review-line-numbers-on-copy', !shouldStripLineNumbersOnCopy()); |
| updateLineNumberOnCopyLinkContents(); |
| } |
| |
| function sanitizeFragmentForCopy(fragment, shouldStripLineNumbers) { |
| var classesToRemove = ['LinkContainer']; |
| if (shouldStripLineNumbers) |
| classesToRemove.push('lineNumber'); |
| |
| classesToRemove.forEach(function(className) { |
| forEachNode(fragment.querySelectorAll('.' + className), function(node) { |
| $(node).remove(); |
| }); |
| }); |
| |
| // Ensure that empty newlines show up in the copy now that |
| // the line might collapse since the line number doesn't take up space. |
| forEachNode(fragment.querySelectorAll('.text'), function(node) { |
| if (node.textContent.match(/^\s*$/)) |
| node.innerHTML = '<br>'; |
| }); |
| } |
| |
| function handleCopy(event) { |
| if (event.target.tagName == 'TEXTAREA') |
| return; |
| var selection = window.getSelection(); |
| var range = selection.getRangeAt(0); |
| var selectionFragment = range.cloneContents(); |
| sanitizeFragmentForCopy(selectionFragment, shouldStripLineNumbersOnCopy()) |
| |
| // FIXME: When event.clipboardData.setData supports text/html, remove all the code below. |
| // https://bugs.webkit.org/show_bug.cgi?id=104179 |
| var container = document.createElement('div'); |
| container.appendChild(selectionFragment); |
| document.body.appendChild(container); |
| selection.selectAllChildren(container); |
| |
| setTimeout(function() { |
| $(container).remove(); |
| selection.removeAllRanges(); |
| selection.addRange(range); |
| }); |
| } |
| |
| function handleReviewFormLoad() { |
| var review_form_contents = $('#reviewform').contents(); |
| if (review_form_contents[0].querySelector('#form-controls #flags')) { |
| review_form_contents.bind('keydown', function(e) { |
| if (e.keyCode == KEY_CODE.escape) |
| hideCommentForm(); |
| }); |
| |
| // This is the intial load of the review form iframe. |
| var form = review_form_contents.find('form')[0]; |
| form.addEventListener('submit', eraseDraftComments); |
| form.target = ''; |
| return; |
| } |
| |
| // Review form iframe have the publish button has been pressed. |
| var email_sent_to = review_form_contents[0].querySelector('#bugzilla-body dl'); |
| // If the email_send_to DL is not in the tree that means the publish failed for some reason, |
| // e.g., you're not logged in. Show the comment form to allow you to login. |
| if (!email_sent_to) { |
| showCommentForm(); |
| return; |
| } |
| |
| eraseDraftComments(); |
| // FIXME: Once WebKit supports seamless iframes, we can just make the review-form |
| // iframe fill the page instead of redirecting back to the bug. |
| window.location.replace($('#toolbar .bugLink a').attr('href')); |
| } |
| |
| function eraseDraftComments() { |
| g_draftCommentSaver.erase(); |
| } |
| |
| function loadDiffState() { |
| var diffstate = localStorage.getItem('code-review-diffstate'); |
| if (diffstate != 'sidebyside' && diffstate != 'unified') |
| return; |
| |
| convertAllFileDiffs(diffstate, $('.FileDiff')); |
| } |
| |
| function isDiffSideBySide(file_diff) { |
| return diffState(file_diff) == 'sidebyside'; |
| } |
| |
| function diffState(file_diff) { |
| var diff_state = $(file_diff).attr('data-diffstate'); |
| return diff_state || 'unified'; |
| } |
| |
| function unifyLine(line, from, to, contents, classNames, attributes, id) { |
| var new_line = unifiedLine(from, to, contents, false, classNames, attributes); |
| var old_line = $(line); |
| if (!old_line.hasClass('LineContainer')) |
| old_line = old_line.parents('.LineContainer'); |
| |
| var comments = commentsToTransferFor($(document.getElementById(id))); |
| old_line.after(comments); |
| old_line.replaceWith(new_line); |
| } |
| |
| function updateDiffLinkVisibility(file_diff) { |
| if (diffState(file_diff) == 'unified') { |
| $('.side-by-side-link', file_diff).show(); |
| $('.unify-link', file_diff).hide(); |
| } else { |
| $('.side-by-side-link', file_diff).hide(); |
| $('.unify-link', file_diff).show(); |
| } |
| } |
| |
| function convertAllFileDiffs(diff_type, file_diffs) { |
| file_diffs.each(function() { |
| convertFileDiff(diff_type, this); |
| }); |
| } |
| |
| function convertFileDiff(diff_type, file_diff) { |
| if (diffState(file_diff) == diff_type) |
| return; |
| |
| if (!$('.resizeHandle', file_diff).length) |
| $(file_diff).append('<div class="resizeHandle"></div>'); |
| |
| $(file_diff).removeClass('sidebyside unified'); |
| $(file_diff).addClass(diff_type); |
| |
| $(file_diff).attr('data-diffstate', diff_type); |
| updateDiffLinkVisibility(file_diff); |
| |
| $('.context', file_diff).each(function() { |
| convertLine(diff_type, this); |
| }); |
| |
| $('.shared .Line', file_diff).each(function() { |
| convertLine(diff_type, this); |
| }); |
| |
| $('.ExpansionLine', file_diff).each(function() { |
| convertExpansionLine(diff_type, this); |
| }); |
| } |
| |
| function convertLine(diff_type, line) { |
| var convert_function = diff_type == 'sidebyside' ? sideBySideifyLine : unifyLine; |
| var from = fromLineNumber(line); |
| var to = toLineNumber(line); |
| var contents = $('.text', line).first(); |
| var classNames = classNamesForMovingLine(line); |
| var attributes = attributesForMovingLine(line); |
| var id = line.id; |
| convert_function(line, from, to, contents, classNames, attributes, id) |
| } |
| |
| function classNamesForMovingLine(line) { |
| var classParts = line.className.split(' '); |
| var classBuffer = []; |
| for (var i = 0; i < classParts.length; i++) { |
| var part = classParts[i]; |
| if (part != 'LineContainer' && part != 'Line') |
| classBuffer.push(part); |
| } |
| return classBuffer.join(' '); |
| } |
| |
| function attributesForMovingLine(line) { |
| var attributesBuffer = ['id=' + line.id]; |
| // Make sure to keep all data- attributes. |
| $(line.attributes).each(function() { |
| if (this.name.indexOf('data-') == 0) |
| attributesBuffer.push(this.name + '=' + this.value); |
| }); |
| return attributesBuffer.join(' '); |
| } |
| |
| function sideBySideifyLine(line, from, to, contents, classNames, attributes, id) { |
| var from_class = ''; |
| var to_class = ''; |
| var from_attributes = ''; |
| var to_attributes = ''; |
| // Clone the contents so we have two copies we can put back in the DOM. |
| var from_contents = contents.clone(true); |
| var to_contents = contents; |
| |
| var container_class = 'LineContainer'; |
| var container_attributes = ''; |
| |
| if (from && !to) { // This is a remove line. |
| from_class = classNames; |
| from_attributes = attributes; |
| to_contents = ''; |
| } else if (to && !from) { // This is an add line. |
| to_class = classNames; |
| to_attributes = attributes; |
| from_contents = ''; |
| } else { |
| container_attributes = attributes; |
| container_class += ' Line ' + classNames; |
| } |
| |
| var new_line = $('<div ' + container_attributes + ' class="' + container_class + '"></div>'); |
| new_line.append(lineSide('from', from_contents, false, from, from_attributes, from_class)); |
| new_line.append(lineSide('to', to_contents, false, to, to_attributes, to_class)); |
| |
| $(line).replaceWith(new_line); |
| |
| if (!line.classList.contains('context')) { |
| var line = $(document.getElementById(id)); |
| line.after(commentsToTransferFor(line)); |
| } |
| } |
| |
| function convertExpansionLine(diff_type, line) { |
| var convert_function = diff_type == 'sidebyside' ? sideBySideExpansionLine : unifiedExpansionLine; |
| var contents = $('.text', line).first(); |
| var from = fromLineNumber(line); |
| var to = toLineNumber(line); |
| var new_line = convert_function(from, to, contents); |
| $(line).replaceWith(new_line); |
| } |
| |
| function commentsToTransferFor(line) { |
| var fragment = document.createDocumentFragment(); |
| |
| previousCommentsFor(line).each(function() { |
| fragment.appendChild(this); |
| }); |
| |
| var active_comments = activeCommentFor(line); |
| var num_active_comments = active_comments.size(); |
| if (num_active_comments > 0) { |
| if (num_active_comments > 1) |
| console.log('ERROR: There is more than one active comment for ' + line.attr('id') + '.'); |
| |
| var parent = active_comments[0].parentNode; |
| var frozenComment = parent.nextSibling; |
| fragment.appendChild(parent); |
| fragment.appendChild(frozenComment); |
| } |
| |
| return fragment; |
| } |
| |
| function discardComment(comment_block) { |
| var line_id = $(comment_block).find('textarea').attr('data-comment-for'); |
| var line = $('#' + line_id) |
| $(comment_block).slideUp('fast', function() { |
| $(this).remove(); |
| line.removeAttr('data-has-comment'); |
| trimCommentContextToBefore(line, line_id); |
| saveDraftComments(); |
| }); |
| } |
| |
| function handleUnfreezeComment() { |
| unfreezeComment(this); |
| } |
| |
| function unfreezeComment(comment) { |
| var unfrozen_comment = $(comment).prev(); |
| unfrozen_comment.show(); |
| $(comment).remove(); |
| unfrozen_comment.find('textarea')[0].focus(); |
| } |
| |
| function showFileDiffLinks() { |
| $('.LinkContainer', this).each(function() { this.style.opacity = 1; }); |
| } |
| |
| function hideFileDiffLinks() { |
| $('.LinkContainer', this).each(function() { this.style.opacity = 0; }); |
| } |
| |
| function handleDiscardComment() { |
| discardComment($(this).parents('.comment')); |
| } |
| |
| function handleAcceptComment() { |
| acceptComment($(this).parents('.comment')); |
| } |
| |
| function acceptComment(comment) { |
| var frozen_comment = freezeComment($(comment)); |
| focusOn(frozen_comment); |
| saveDraftComments(); |
| return frozen_comment; |
| } |
| |
| $('.FileDiff').live('mouseenter', showFileDiffLinks); |
| $('.FileDiff').live('mouseleave', hideFileDiffLinks); |
| $('.side-by-side-link').live('click', handleSideBySideLinkClick); |
| $('.unify-link').live('click', handleUnifyLinkClick); |
| $('.ExpandLink').live('click', handleExpandLinkClick); |
| $('.frozenComment').live('click', handleUnfreezeComment); |
| $('.comment .discard').live('click', handleDiscardComment); |
| $('.comment .ok').live('click', handleAcceptComment); |
| $('.more').live('click', showMoreHelp); |
| $('.more-help .winter').live('click', hideMoreHelp); |
| |
| function freezeComment(comment_block) { |
| var comment_textarea = comment_block.find('textarea'); |
| if (comment_textarea.val().trim() == '') { |
| discardComment(comment_block); |
| return; |
| } |
| var line_id = comment_textarea.attr('data-comment-for'); |
| var line = $('#' + line_id) |
| var frozen_comment = $('<div class="frozenComment"></div>').text(comment_textarea.val()); |
| findCommentBlockFor(line).hide().after(frozen_comment); |
| return frozen_comment; |
| } |
| |
| function focusOn(node, opt_is_backward) { |
| if (node.length == 0) |
| return; |
| |
| // Give a tabindex so the element can receive actual browser focus. |
| // -1 makes the element focusable without actually putting in in the tab order. |
| node.attr('tabindex', -1); |
| node.focus(); |
| // Remove the tabindex on blur to avoid having the node be mouse-focusable. |
| node.bind('blur', function() { node.removeAttr('tabindex'); }); |
| |
| var node_top = node.offset().top; |
| var is_top_offscreen = node_top <= $(document).scrollTop(); |
| |
| var half_way_point = $(document).scrollTop() + window.innerHeight / 2; |
| var is_top_past_halfway = opt_is_backward ? node_top < half_way_point : node_top > half_way_point; |
| |
| if (is_top_offscreen || is_top_past_halfway) |
| $(document).scrollTop(node_top - window.innerHeight / 2); |
| } |
| |
| function visibleNodeFilterFunction(is_backward) { |
| var y = is_backward ? $('#toolbar')[0].offsetTop - 1 : 0; |
| var x = window.innerWidth / 2; |
| var reference_element = document.elementFromPoint(x, y); |
| |
| if (reference_element.nodeName == 'HTML' || reference_element.nodeName == 'BODY') { |
| // In case we hit test a margin between file diffs, shift a fudge factor and try again. |
| // FIXME: Is there a better way to do this? |
| var file_diffs = $('.FileDiff'); |
| var first_diff = file_diffs.first(); |
| var second_diff = $(file_diffs[1]); |
| var distance_between_file_diffs = second_diff.position().top - first_diff.position().top - first_diff.height(); |
| |
| if (is_backward) |
| y -= distance_between_file_diffs; |
| else |
| y += distance_between_file_diffs; |
| |
| reference_element = document.elementFromPoint(x, y); |
| } |
| |
| if (reference_element.nodeName == 'HTML' || reference_element.nodeName == 'BODY') |
| return null; |
| |
| return function(node) { |
| var compare = reference_element.compareDocumentPosition(node[0]); |
| if (is_backward) |
| return compare & Node.DOCUMENT_POSITION_PRECEDING; |
| return compare & Node.DOCUMENT_POSITION_FOLLOWING; |
| } |
| } |
| |
| function focusNext(filter, direction) { |
| var focusable_nodes = $('a,.Line,.frozenComment,.previousComment,.DiffBlock,.overallComments').filter(function() { |
| return !$(this).hasClass('DiffBlock') || $('.add,.remove', this).size(); |
| }); |
| |
| var is_backward = direction == DIRECTION.BACKWARD; |
| var index = focusable_nodes.index($(document.activeElement)); |
| |
| var extra_filter = null; |
| |
| if (index == -1) { |
| if (is_backward) |
| index = focusable_nodes.length; |
| extra_filter = visibleNodeFilterFunction(is_backward); |
| } |
| |
| var offset = is_backward ? -1 : 1; |
| var end = is_backward ? -1 : focusable_nodes.size(); |
| for (var i = index + offset; i != end; i = i + offset) { |
| var node = $(focusable_nodes[i]); |
| if (filter(node) && (!extra_filter || extra_filter(node))) { |
| focusOn(node, is_backward); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| var DIRECTION = {FORWARD: 1, BACKWARD: 2}; |
| |
| function isComment(node) { |
| return node.hasClass('frozenComment') || node.hasClass('previousComment') || node.hasClass('overallComments'); |
| } |
| |
| function isDiffBlock(node) { |
| return node.hasClass('DiffBlock'); |
| } |
| |
| function isLine(node) { |
| return node.hasClass('Line'); |
| } |
| |
| function commentTextareaForKeyTarget(key_target) { |
| if (key_target.nodeName == 'TEXTAREA') |
| return $(key_target); |
| |
| var comment_textarea = $(document.activeElement).prev().find('textarea'); |
| if (!comment_textarea.size()) |
| return null; |
| return comment_textarea; |
| } |
| |
| function extendCommentContextUp(key_target) { |
| var comment_textarea = commentTextareaForKeyTarget(key_target); |
| if (!comment_textarea) |
| return; |
| |
| var comment_base_line = comment_textarea.attr('data-comment-for'); |
| var diff_section = diffSectionFor(comment_textarea); |
| var lines = $('.Line', diff_section); |
| for (var i = 0; i < lines.length - 1; i++) { |
| if (hasDataCommentBaseLine(lines[i + 1], comment_base_line)) { |
| addDataCommentBaseLine(lines[i], comment_base_line); |
| break; |
| } |
| } |
| } |
| |
| function shrinkCommentContextDown(key_target) { |
| var comment_textarea = commentTextareaForKeyTarget(key_target); |
| if (!comment_textarea) |
| return; |
| |
| var comment_base_line = comment_textarea.attr('data-comment-for'); |
| var diff_section = diffSectionFor(comment_textarea); |
| var lines = contextLinesFor(comment_base_line, diff_section); |
| if (lines.size() > 1) |
| removeDataCommentBaseLine(lines[0], comment_base_line); |
| } |
| |
| function handleModifyContextKey(e) { |
| var handled = false; |
| |
| if (e.shiftKey && e.ctrlKey) { |
| switch (e.keyCode) { |
| case KEY_CODE.up: |
| extendCommentContextUp(e.target); |
| handled = true; |
| break; |
| |
| case KEY_CODE.down: |
| shrinkCommentContextDown(e.target); |
| handled = true; |
| break; |
| } |
| } |
| |
| if (handled) |
| e.preventDefault(); |
| |
| return handled; |
| } |
| |
| $('textarea').live('keydown', function(e) { |
| if (handleModifyContextKey(e)) |
| return; |
| |
| if (e.keyCode == KEY_CODE.escape) |
| handleEscapeKeyInTextarea(this); |
| }); |
| |
| $('body').live('keydown', function(e) { |
| // FIXME: There's got to be a better way to avoid seeing these keypress |
| // events. |
| if (e.target.nodeName == 'TEXTAREA') |
| return; |
| |
| // Don't want to override browser shortcuts like ctrl+r. |
| if (e.metaKey || e.ctrlKey) |
| return; |
| |
| if (handleModifyContextKey(e)) |
| return; |
| |
| var handled = false; |
| switch (e.keyCode) { |
| case KEY_CODE.r: |
| $('.review select').focus(); |
| handled = true; |
| break; |
| |
| case KEY_CODE.n: |
| handled = focusNext(isComment, DIRECTION.FORWARD); |
| break; |
| |
| case KEY_CODE.p: |
| handled = focusNext(isComment, DIRECTION.BACKWARD); |
| break; |
| |
| case KEY_CODE.j: |
| if (e.shiftKey) |
| handled = focusNext(isLine, DIRECTION.FORWARD); |
| else |
| handled = focusNext(isDiffBlock, DIRECTION.FORWARD); |
| break; |
| |
| case KEY_CODE.k: |
| if (e.shiftKey) |
| handled = focusNext(isLine, DIRECTION.BACKWARD); |
| else |
| handled = focusNext(isDiffBlock, DIRECTION.BACKWARD); |
| break; |
| |
| case KEY_CODE.enter: |
| handled = handleEnterKey(); |
| break; |
| |
| case KEY_CODE.escape: |
| hideMoreHelp(); |
| handled = true; |
| break; |
| } |
| |
| if (handled) |
| e.preventDefault(); |
| }); |
| |
| function handleEscapeKeyInTextarea(textarea) { |
| var comment = $(textarea).parents('.comment'); |
| if (comment.size()) |
| acceptComment(comment); |
| |
| textarea.blur(); |
| document.body.focus(); |
| } |
| |
| function handleEnterKey() { |
| if (document.activeElement.nodeName == 'BODY') |
| return; |
| |
| var focused = $(document.activeElement); |
| |
| if (focused.hasClass('frozenComment')) { |
| unfreezeComment(focused); |
| return true; |
| } |
| |
| if (focused.hasClass('overallComments')) { |
| openOverallComments(); |
| focused.find('textarea')[0].focus(); |
| return true; |
| } |
| |
| if (focused.hasClass('previousComment')) { |
| addCommentField(focused); |
| return true; |
| } |
| |
| var lines = focused.hasClass('Line') ? focused : $('.Line', focused); |
| var last = lines.last(); |
| if (last.attr('data-has-comment')) { |
| unfreezeCommentFor(last); |
| return true; |
| } |
| |
| addCommentForLines(lines); |
| return true; |
| } |
| |
| function contextLinesFor(comment_base_lines, file_diff) { |
| var base_lines = comment_base_lines.split(' '); |
| return $('div[data-comment-base-line]', file_diff).filter(function() { |
| return $(this).attr('data-comment-base-line').split(' ').some(function(item) { |
| return base_lines.indexOf(item) != -1; |
| }); |
| }); |
| } |
| |
| function numberFrom(line_id) { |
| return Number(line_id.replace('line', '')); |
| } |
| |
| function trimCommentContextToBefore(line, comment_base_line) { |
| var line_to_trim_to = numberFrom(line.attr('id')); |
| contextLinesFor(comment_base_line, fileDiffFor(line)).each(function() { |
| var id = $(this).attr('id'); |
| if (numberFrom(id) > line_to_trim_to) |
| return; |
| |
| if (!$('[data-comment-for=' + comment_base_line + ']').length) |
| removeDataCommentBaseLine(this, comment_base_line); |
| }); |
| } |
| |
| var drag_select_start_index = -1; |
| |
| function lineOffsetFrom(line, offset) { |
| var file_diff = line.parents('.FileDiff'); |
| var all_lines = $('.Line', file_diff); |
| var index = all_lines.index(line); |
| return $(all_lines[index + offset]); |
| } |
| |
| function previousLineFor(line) { |
| return lineOffsetFrom(line, -1); |
| } |
| |
| function nextLineFor(line) { |
| return lineOffsetFrom(line, 1); |
| } |
| |
| $('.resizeHandle').live('mousedown', function(event) { |
| file_diff_being_resized = $(this).parent('.FileDiff'); |
| }); |
| |
| function generateFileDiffResizeStyleElement() { |
| // FIXME: Once we support calc, we can replace this with something that uses the attribute value. |
| var styleText = ''; |
| for (var i = minLeftSideRatio; i <= maxLeftSideRatio; i++) { |
| // FIXME: Once we support calc, put the resize handle at calc(i% - 5) so it doesn't cover up |
| // the right-side line numbers. |
| styleText += '.FileDiff[leftsidewidth="' + i + '"] .resizeHandle {' + |
| 'left: ' + i + '%' + |
| '}' + |
| '.FileDiff[leftsidewidth="' + i + '"] .LineSide:first-child,' + |
| '.FileDiff[leftsidewidth="' + i + '"].sidebyside .DiffBlockPart.remove {' + |
| 'width:' + i + '%;' + |
| '}' + |
| '.FileDiff[leftsidewidth="' + i + '"] .LineSide:last-child,' + |
| '.FileDiff[leftsidewidth="' + i + '"].sidebyside .DiffBlockPart.add {' + |
| 'width:' + (100 - i) + '%;' + |
| '}'; |
| } |
| var styleElement = document.createElement('style'); |
| styleElement.innerText = styleText; |
| document.head.appendChild(styleElement); |
| } |
| |
| $(document).bind('mousemove', function(event) { |
| if (!file_diff_being_resized) |
| return; |
| |
| var ratio = event.pageX / window.innerWidth; |
| var percentage = Math.floor(ratio * 100); |
| if (percentage < minLeftSideRatio) |
| percentage = minLeftSideRatio; |
| if (percentage > maxLeftSideRatio) |
| percentage = maxLeftSideRatio; |
| file_diff_being_resized.attr('leftsidewidth', percentage); |
| event.preventDefault(); |
| }); |
| |
| $(document).bind('mouseup', function(event) { |
| file_diff_being_resized = null; |
| processSelectedLines(); |
| }); |
| |
| $('.lineNumber').live('click', function(e) { |
| var line = lineFromLineDescendant($(this)); |
| if (line.hasClass('commentContext')) { |
| var previous_line = previousLineFor(line); |
| if (previous_line[0]) |
| trimCommentContextToBefore(previous_line, line.attr('data-comment-base-line')); |
| } else if (e.shiftKey) |
| extendCommentContextTo(line); |
| }).live('mousedown', function(e) { |
| // preventDefault to avoid selecting text when dragging to select comment context lines. |
| // FIXME: should we use user-modify CSS instead? |
| e.preventDefault(); |
| if (e.shiftKey) |
| return; |
| |
| var line = lineFromLineDescendant($(this)); |
| if (line.hasClass('context')) |
| return; |
| |
| drag_select_start_index = numberFrom(line.attr('id')); |
| line.addClass('selected'); |
| }); |
| |
| $('.LineContainer:not(.context)').live('mouseenter', function(e) { |
| if (drag_select_start_index == -1 || e.shiftKey) |
| return; |
| selectToLineContainer(this); |
| }).live('mouseup', function(e) { |
| if (drag_select_start_index == -1 || e.shiftKey) |
| return; |
| |
| selectToLineContainer(this); |
| processSelectedLines(); |
| }); |
| |
| function extendCommentContextTo(line) { |
| var diff_section = diffSectionFor(line); |
| var lines = $('.Line', diff_section); |
| var lines_to_modify = []; |
| var have_seen_start_line = false; |
| var data_comment_base_line = null; |
| lines.each(function() { |
| if (data_comment_base_line) |
| return; |
| |
| have_seen_start_line = have_seen_start_line || this == line[0]; |
| |
| if (have_seen_start_line) { |
| if ($(this).hasClass('commentContext')) |
| data_comment_base_line = $(this).attr('data-comment-base-line'); |
| else |
| lines_to_modify.push(this); |
| } |
| }); |
| |
| // There is no comment context to extend. |
| if (!data_comment_base_line) |
| return; |
| |
| $(lines_to_modify).each(function() { |
| $(this).addClass('commentContext'); |
| $(this).attr('data-comment-base-line', data_comment_base_line); |
| }); |
| } |
| |
| function selectTo(focus_index) { |
| var selected = $('.selected').removeClass('selected'); |
| var is_backward = drag_select_start_index > focus_index; |
| var current_index = is_backward ? focus_index : drag_select_start_index; |
| var last_index = is_backward ? drag_select_start_index : focus_index; |
| while (current_index <= last_index) { |
| $('#line' + current_index).addClass('selected') |
| current_index++; |
| } |
| } |
| |
| function selectToLineContainer(line_container) { |
| var line = lineFromLineContainer(line_container); |
| |
| // Ensure that the selected lines are all contained in the same DiffSection. |
| var selected_lines = $('.selected'); |
| var selected_diff_section = diffSectionFor(selected_lines.first()); |
| var new_diff_section = diffSectionFor(line); |
| if (new_diff_section[0] != selected_diff_section[0]) { |
| var lines = $('.Line', selected_diff_section); |
| if (numberFrom(selected_lines.first().attr('id')) == drag_select_start_index) |
| line = lines.last(); |
| else |
| line = lines.first(); |
| } |
| |
| selectTo(numberFrom(line.attr('id'))); |
| } |
| |
| function processSelectedLines() { |
| drag_select_start_index = -1; |
| addCommentForLines($('.selected')); |
| } |
| |
| function addCommentForLines(lines) { |
| if (!lines.size()) |
| return; |
| |
| var already_has_comment = lines.last().hasClass('commentContext'); |
| |
| var comment_base_line; |
| if (already_has_comment) |
| comment_base_line = lines.last().attr('data-comment-base-line'); |
| else { |
| var last = lineFromLineDescendant(lines.last()); |
| addCommentFor($(last)); |
| comment_base_line = last.attr('id'); |
| } |
| |
| lines.each(function() { |
| addDataCommentBaseLine(this, comment_base_line); |
| $(this).removeClass('selected'); |
| }); |
| |
| saveDraftComments(); |
| } |
| |
| function hasDataCommentBaseLine(line, id) { |
| var val = $(line).attr('data-comment-base-line'); |
| if (!val) |
| return false; |
| |
| var parts = val.split(' '); |
| for (var i = 0; i < parts.length; i++) { |
| if (parts[i] == id) |
| return true; |
| } |
| return false; |
| } |
| |
| function addDataCommentBaseLine(line, id) { |
| $(line).addClass('commentContext'); |
| if (hasDataCommentBaseLine(line, id)) |
| return; |
| |
| var val = $(line).attr('data-comment-base-line'); |
| var parts = val ? val.split(' ') : []; |
| parts.push(id); |
| $(line).attr('data-comment-base-line', parts.join(' ')); |
| } |
| |
| function removeDataCommentBaseLine(line, comment_base_lines) { |
| var val = $(line).attr('data-comment-base-line'); |
| if (!val) |
| return; |
| |
| var base_lines = comment_base_lines.split(' '); |
| var parts = val.split(' '); |
| var new_parts = []; |
| for (var i = 0; i < parts.length; i++) { |
| if (base_lines.indexOf(parts[i]) == -1) |
| new_parts.push(parts[i]); |
| } |
| |
| var new_comment_base_line = new_parts.join(' '); |
| if (new_comment_base_line) |
| $(line).attr('data-comment-base-line', new_comment_base_line); |
| else { |
| $(line).removeAttr('data-comment-base-line'); |
| $(line).removeClass('commentContext'); |
| } |
| } |
| |
| function lineFromLineDescendant(descendant) { |
| return descendant.hasClass('Line') ? descendant : descendant.parents('.Line'); |
| } |
| |
| function lineContainerFromDescendant(descendant) { |
| return descendant.hasClass('LineContainer') ? descendant : descendant.parents('.LineContainer'); |
| } |
| |
| function lineFromLineContainer(lineContainer) { |
| var line = $(lineContainer); |
| if (!line.hasClass('Line')) |
| line = $('.Line', line); |
| return line; |
| } |
| |
| function contextSnippetFor(line, indent) { |
| var snippets = [] |
| contextLinesFor(line.attr('id'), fileDiffFor(line)).each(function() { |
| var action = ' '; |
| if ($(this).hasClass('add')) |
| action = '+'; |
| else if ($(this).hasClass('remove')) |
| action = '-'; |
| snippets.push(indent + action + textContentsFor(this)); |
| }); |
| return snippets.join('\n'); |
| } |
| |
| function fileNameFor(line) { |
| return fileDiffFor(line).find('h1').text(); |
| } |
| |
| function indentFor(depth) { |
| return (new Array(depth + 1)).join('>') + ' '; |
| } |
| |
| function snippetFor(line, indent) { |
| var file_name = fileNameFor(line); |
| var line_number = line.hasClass('remove') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]); |
| return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent); |
| } |
| |
| function quotePreviousComments(comments) { |
| var quoted_comments = []; |
| var depth = comments.size(); |
| comments.each(function() { |
| var indent = indentFor(depth--); |
| var text = $(this).children('.content').text(); |
| quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent)); |
| }); |
| return quoted_comments.join('\n'); |
| } |
| |
| $('#comment_form .winter').live('click', hideCommentForm); |
| |
| function serializedComments() { |
| var comments_in_context = [] |
| forEachLine(function(line) { |
| if (line.attr('data-has-comment') != 'true') |
| return; |
| var comment = findCommentBlockFor(line).children('textarea').val().trim(); |
| if (comment == '') |
| return; |
| var previous_comments = previousCommentsFor(line); |
| var snippet = snippetFor(line, indentFor(previous_comments.size() + 1)); |
| var quoted_comments = quotePreviousComments(previous_comments); |
| var comment_with_context = []; |
| comment_with_context.push(snippet); |
| if (quoted_comments != '') |
| comment_with_context.push(quoted_comments); |
| comment_with_context.push('\n' + comment); |
| comments_in_context.push(comment_with_context.join('\n')); |
| }); |
| var comment = $('.overallComments textarea').val().trim(); |
| if (comment != '') |
| comment += '\n\n'; |
| comment += comments_in_context.join('\n\n'); |
| if (comments_in_context.length > 0) |
| comment = 'View in context: ' + window.location + '\n\n' + comment; |
| return comment; |
| } |
| |
| function fillInReviewForm() { |
| var review_form = $('#reviewform').contents(); |
| review_form.find('#comment').val(serializedComments()); |
| review_form.find('#flags select').each(function() { |
| var control = findControlForFlag(this); |
| if (!control.size()) |
| return; |
| $(this).attr('selectedIndex', control.attr('selectedIndex')); |
| }); |
| } |
| |
| function showCommentForm() { |
| $('#comment_form').removeClass('inactive'); |
| $('#reviewform').contents().find('#submitBtn').focus(); |
| } |
| |
| function hideCommentForm() { |
| $('#comment_form').addClass('inactive'); |
| |
| // Make sure the top document has focus so key events don't keep going to the review form. |
| document.body.tabIndex = -1; |
| document.body.focus(); |
| } |
| |
| $('#preview_comments').live('click', function() { |
| fillInReviewForm(); |
| showCommentForm(); |
| }); |
| |
| $('#post_comments').live('click', function() { |
| fillInReviewForm(); |
| $('#reviewform').contents().find('form').submit(); |
| }); |
| |
| if (CODE_REVIEW_UNITTEST) { |
| window.DraftCommentSaver = DraftCommentSaver; |
| window.addCommentFor = addCommentFor; |
| window.addPreviousComment = addPreviousComment; |
| window.tracLinks = tracLinks; |
| window.crawlDiff = crawlDiff; |
| window.convertAllFileDiffs = convertAllFileDiffs; |
| window.sanitizeFragmentForCopy = sanitizeFragmentForCopy; |
| window.displayPreviousComments = displayPreviousComments; |
| window.discardComment = discardComment; |
| window.addCommentField = addCommentField; |
| window.acceptComment = acceptComment; |
| window.appendToolbar = appendToolbar; |
| window.eraseDraftComments = eraseDraftComments; |
| window.serializedComments = serializedComments; |
| window.setFileContents = setFileContents; |
| window.unfreezeComment = unfreezeComment; |
| window.g_draftCommentSaver = g_draftCommentSaver; |
| window.isChangeLog = isChangeLog; |
| } else { |
| $(document).ready(handleDocumentReady) |
| } |
| })(); |