blob: 463e24cdc82f8caf724f094a08bd8caff1b819fb [file] [log] [blame]
// Copyright 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Generic helpers
/**
* Create a new XMLHttpRequest in a cross-browser-compatible way.
* @return XMLHttpRequest object
*/
function M_getXMLHttpRequest() {
try {
return new XMLHttpRequest();
} catch (e) { }
try {
return new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) { }
try {
return new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) { }
return null;
}
/**
* Finds the element's parent in the DOM tree.
* @param {Element} element The element whose parent we want to find
* @return The parent element of the given element
*/
function M_getParent(element) {
if (element.parentNode) {
return element.parentNode;
} else if (element.parentElement) {
// IE compatibility. Why follow standards when you can make up your own?
return element.parentElement;
}
return null;
}
/**
* Finds the event's target in a way that works on all browsers.
* @param {Event} e The event object whose target we want to find
* @return The element receiving the event
*/
function M_getEventTarget(e) {
var src = e.srcElement ? e.srcElement : e.target;
return src;
}
/**
* Function to determine if we are in a KHTML-based browser(Konq/Safari).
* @return Boolean of whether we are in a KHTML browser
*/
function M_isKHTML() {
var agt = navigator.userAgent.toLowerCase();
return (agt.indexOf("safari") != -1) || (agt.indexOf("khtml") != -1);
}
/**
* Function to determine if we are running in an IE browser.
* @return Boolean of whether we are running in IE
*/
function M_isIE() {
return (navigator.userAgent.toLowerCase().indexOf("msie") != -1) &&
!window.opera;
}
/**
* Stop the event bubbling in a browser-independent way. Sometimes required
* when it is not easy to return true when an event is handled.
* @param {Window} win The window in which this event is happening
* @param {Event} e The event that we want to cancel
*/
function M_stopBubble(win, e) {
if (!e) {
e = win.event;
}
e.cancelBubble = true;
if (e.stopPropagation) {
e.stopPropagation();
}
}
/**
* Return distance in pixels from the top of the document to the given element.
* @param {Element} element The element whose offset we want to find
* @return Integer value of the height of the element from the top
*/
function M_getPageOffsetTop(element) {
var y = element.offsetTop;
if (element.offsetParent != null) {
y += M_getPageOffsetTop(element.offsetParent);
}
return y;
}
/**
* Return distance in pixels of the given element from the left of the document.
* @param {Element} element The element whose offset we want to find
* @return Integer value of the horizontal position of the element
*/
function M_getPageOffsetLeft(element) {
var x = element.offsetLeft;
if (element.offsetParent != null) {
x += M_getPageOffsetLeft(element.offsetParent);
}
return x;
}
/**
* Find the height of the window viewport.
* @param {Window} win The window whose viewport we would like to measure
* @return Integer value of the height of the given window
*/
function M_getWindowHeight(win) {
return M_getWindowPropertyByBrowser_(win, M_getWindowHeightGetters_);
}
/**
* Find the vertical scroll position of the given window.
* @param {Window} win The window whose scroll position we want to find
* @return Integer value of the scroll position of the given window
*/
function M_getScrollTop(win) {
return M_getWindowPropertyByBrowser_(win, M_getScrollTopGetters_);
}
/**
* Scroll the target element into view at 1/3rd of the window height only if
* the scrolling direction matches the direction that was asked for.
* @param {Window} win The window in which the element resides
* @param {Element} element The element that we want to bring into view
* @param {Integer} direction Positive for scroll down, negative for scroll up
*/
function M_scrollIntoView(win, element, direction) {
var elTop = M_getPageOffsetTop(element);
var winHeight = M_getWindowHeight(win);
var targetScroll = elTop - winHeight / 3;
var scrollTop = M_getScrollTop(win);
if ((direction > 0 && scrollTop < targetScroll) ||
(direction < 0 && scrollTop > targetScroll)) {
win.scrollTo(M_getPageOffsetLeft(element), targetScroll);
}
}
/**
* Returns whether the element is visible.
* @param {Window} win The window that the element resides in
* @param {Element} element The element whose visibility we want to determine
* @return Boolean of whether the element is visible in the window or not
*/
function M_isElementVisible(win, element) {
var elTop = M_getPageOffsetTop(element);
var winHeight = M_getWindowHeight(win);
var winTop = M_getScrollTop(win);
if (elTop < winTop || elTop > winTop + winHeight) {
return false;
}
return true;
}
// Cross-browser compatibility quirks and methodology borrowed from
// common.js
var M_getWindowHeightGetters_ = {
ieQuirks_: function(win) {
return win.document.body.clientHeight;
},
ieStandards_: function(win) {
return win.document.documentElement.clientHeight;
},
dom_: function(win) {
return win.innerHeight;
}
};
var M_getScrollTopGetters_ = {
ieQuirks_: function(win) {
return win.document.body.scrollTop;
},
ieStandards_: function(win) {
return win.document.documentElement.scrollTop;
},
dom_: function(win) {
return win.pageYOffset;
}
};
/**
* Slightly modified from common.js: Konqueror has the CSS1Compat property
* but requires the standard DOM functionlity, not the IE one.
*/
function M_getWindowPropertyByBrowser_(win, getters) {
try {
if (!M_isKHTML() && "compatMode" in win.document &&
win.document.compatMode == "CSS1Compat") {
return getters.ieStandards_(win);
} else if (M_isIE()) {
return getters.ieQuirks_(win);
}
} catch (e) {
// Ignore for now and fall back to DOM method
}
return getters.dom_(win);
}
// Global search box magic (global.html)
/**
* Handle the onblur action of the search box, replacing it with greyed out
* instruction text when it is empty.
* @param {Element} element The search box element
*/
function M_onSearchBlur(element) {
var defaultMsg = "Enter a changelist#, user, or group";
if (element.value.length == 0 || element.value == defaultMsg) {
element.style.color = "gray";
element.value = defaultMsg;
} else {
element.style.color = "";
}
}
/**
* Handle the onfocus action of the search box, emptying it out if no new text
* was entered.
* @param {Element} element The search box element
*/
function M_onSearchFocus(element) {
if (element.style.color == "gray") {
element.style.color = "";
element.value = "";
}
}
// Inline diffs (changelist.html)
/**
* Creates an iframe to load the diff in the background and when that's done,
* calls a function to transfer the contents of the iframe into the current DOM.
* @param {Integer} suffix The number associated with that diff
* @param {String} url The URL that the diff should be fetched from
* @return false (for event bubbling purposes)
*/
function M_showInlineDiff(suffix, url) {
var hide = document.getElementById("hide-" + suffix);
var show = document.getElementById("show-" + suffix);
var frameDiv = document.getElementById("frameDiv-" + suffix);
var dumpDiv = document.getElementById("dumpDiv-" + suffix);
var diffTR = document.getElementById("diffTR-" + suffix);
var hideAll = document.getElementById("hide-alldiffs");
var showAll = document.getElementById("show-alldiffs");
/* Twiddle the "show/hide all diffs" link */
if (hide.style.display != "") {
M_CL_hiddenInlineDiffCount -= 1;
if (M_CL_hiddenInlineDiffCount == M_CL_maxHiddenInlineDiffCount) {
showAll.style.display = "inline";
hideAll.style.display = "none";
} else {
showAll.style.display = "none";
hideAll.style.display = "inline";
}
}
hide.style.display = "";
show.style.display = "none";
dumpDiv.style.display = "block"; // XXX why not ""?
diffTR.style.display = "";
if (!frameDiv.innerHTML) {
if (M_isKHTML()) {
frameDiv.style.display = "block"; // XXX why not ""?
}
frameDiv.innerHTML = "<iframe src='" + url + "'" +
" onload='M_dumpInlineDiffContent(this, \"" + suffix + "\")'"+
"height=1>your browser does not support iframes!</iframe>";
}
return false;
}
/**
* Hides the diff that was retrieved with M_showInlineDiff.
* @param {Integer} suffix The number associated with the diff we want to hide
*/
function M_hideInlineDiff(suffix) {
var hide = document.getElementById("hide-" + suffix);
var show = document.getElementById("show-" + suffix);
var dumpDiv = document.getElementById("dumpDiv-" + suffix);
var diffTR = document.getElementById("diffTR-" + suffix);
var hideAll = document.getElementById("hide-alldiffs");
var showAll = document.getElementById("show-alldiffs");
/* Twiddle the "show/hide all diffs" link */
if (hide.style.display != "none") {
M_CL_hiddenInlineDiffCount += 1;
if (M_CL_hiddenInlineDiffCount == M_CL_maxHiddenInlineDiffCount) {
showAll.style.display = "inline";
hideAll.style.display = "none";
} else {
showAll.style.display = "none";
hideAll.style.display = "inline";
}
}
hide.style.display = "none";
show.style.display = "inline";
diffTR.style.display = "none";
dumpDiv.style.display = "none";
return false;
}
/**
* Dumps the content of the given iframe into the appropriate div in order
* for the diff to be displayed.
* @param {Element} iframe The IFRAME that contains the diff data
* @param {Integer} suffix The number associated with the diff
*/
function M_dumpInlineDiffContent(iframe, suffix) {
var dumpDiv = document.getElementById("dumpDiv-" + suffix);
dumpDiv.style.display = "block"; // XXX why not ""?
dumpDiv.innerHTML = iframe.contentWindow.document.body.innerHTML;
// TODO: The following should work on all browsers instead of the
// innerHTML hack above. At this point I don't remember what the exact
// problem was, but it didn't work for some reason.
// dumpDiv.appendChild(iframe.contentWindow.document.body);
if (M_isKHTML()) {
var frameDiv = document.getElementById("frameDiv-" + suffix);
frameDiv.style.display = "none";
}
}
/**
* Goes through all the diffs and triggers the onclick action on them which
* should start the mechanism for displaying them.
* @param {Integer} num The number of diffs to display (0-indexed)
*/
function M_showAllDiffs(num) {
for (var i = 0; i < num; i++) {
var link = document.getElementById('show-' + i);
// Since the user may not have JS, the template only shows the diff inline
// for the onclick action, not the href. In order to activate it, we must
// call the link's onclick action.
if (link.className.indexOf("reverted") == -1) {
link.onclick();
}
}
}
/**
* Goes through all the diffs and hides them by triggering the hide link.
* @param {Integer} num The number of diffs to hide (0-indexed)
*/
function M_hideAllDiffs(num) {
for (var i = 0; i < num; i++) {
var link = document.getElementById('hide-' + i);
// If the user tries to hide, that means they have JS, which in turn means
// that we can just set href in the href of the hide link.
link.onclick();
}
}
// Inline comment submission forms (changelist.html, file.html)
/**
* Changes the elements display style to "" which renders it visible.
* @param {String} id The id of the target element
*/
function M_showElement(id) {
var elt = document.getElementById(id);
if (elt) elt.style.display = "";
}
/**
* Changes the elements display style to "none" which renders it invisible.
* @param {String} id The id of the target element
*/
function M_hideElement(id) {
var elt = document.getElementById(id);
if (elt) elt.style.display = "none";
}
/**
* Toggle the visibility of a section. The little indicator triangle will also
* be toggled.
* @param {String} id The id of the target element
*/
var isSectionOpen = new Array();
function M_toggleSection(id) {
var sectionStyle = document.getElementById(id).style;
var pointerStyle = document.getElementById(id + "-pointer").style;
if (sectionStyle.display == "none") {
sectionStyle.display = "";
pointerStyle.backgroundImage = "url('/static/opentriangle.gif')";
isSectionOpen[id] = true;
} else {
sectionStyle.display = "none";
pointerStyle.backgroundImage = "url('/static/closedtriangle.gif')";
isSectionOpen[id] = false;
}
}
var patchSetState = new Array();
function M_onPatchSetReady() {
if (http_request.readyState != 4)
return;
var id = http_request.div_id;
var s = document.getElementById(id);
if (http_request.status == 200) {
patchSetState[id] = 1;
s.innerHTML = http_request.responseText;
} else {
patchSetState[id] = -1;
s.innerHTML = '<div style="color:red">'
+ 'Could not load the patchset.<br />'
+ http_request.status
+ '</div>';
}
}
/**
* Toggle the visiblity of a PatchSet section.
* @param {String} change_id the id of the change.
* @param {String} patchset_id the id of the patchset.
*/
function M_togglePatchSetSection(change_id, patchset_id) {
var id = 'ps-' + patchset_id;
M_toggleSection(id);
if (isSectionOpen[id]) {
if (patchSetState[id] == 1)
return;
var s = document.getElementById(id);
http_request = M_getXMLHttpRequest();
if (!http_request) {
patchSetState[id] = -1;
s.innerHTML = '<div style="color:red">Could not load.</div>';
return;
}
s.innerHTML = '<div>Loading...</div>';
var u = '/' + change_id + '/ajax_patchset/' + patchset_id;
http_request.open('GET', u, true);
http_request.onreadystatechange = M_onPatchSetReady;
http_request.div_id = id;
http_request.send(null);
}
}
function M_onPagedReady () {
if (http_request.readyState != 4)
return;
var section = http_request.section_name;
var links = document.getElementById('paged-' + section + '-links');
var progs = document.getElementById('paged-' + section + '-progress');
if (http_request.status != 200) {
links.style.display = '';
progs.style.display = 'none';
return;
}
var t = document.getElementById(http_request.table_id);
var last;
for (var i = 0; i < t.rows.length;) {
var r = t.rows[i];
if (r.className.indexOf('pagedrow-'+section) >= 0) {
t.deleteRow(i);
last = t.rows[i];
} else {
i++;
}
}
var text = http_request.responseText;
var lf = text.indexOf('\n');
var info = text.substring(0,lf).split(',');
var tmp = document.createElement('table');
tmp.innerHTML = text.substring(lf + 1);
while (0 < tmp.rows.length) {
if (last)
last.parentNode.insertBefore(tmp.rows[0], last);
else
t.tBodies[0].appendChild(tmp.rows[0]);
}
var opos = document.getElementById('paged-'+section+'-opos');
var oend = document.getElementById('paged-'+section+'-oend');
var opre = document.getElementById('paged-'+section+'-opre');
var onex = document.getElementById('paged-'+section+'-onex');
var prev = document.getElementById('paged-'+section+'-prev');
var next = document.getElementById('paged-'+section+'-next');
opos.innerHTML = info[0];
oend.innerHTML = info[1];
opre.innerHTML = info[2];
onex.innerHTML = info[3];
prev.style.display = info[2]!='' && parseInt(info[2]) ? '' : 'none';
next.style.display = info[3]!='' && parseInt(info[3]) ? '' : 'none';
links.style.display = '';
progs.style.display = 'none';
if (http_request.after_update)
http_request.after_update();
}
function M_PagedExec(base_url, section, table_id, offset, after) {
http_request = M_getXMLHttpRequest();
if (!http_request)
return;
var links = document.getElementById('paged-' + section + '-links');
var progs = document.getElementById('paged-' + section + '-progress');
links.style.display = 'none';
progs.style.display = '';
http_request.open('GET', base_url + '/' + offset, true);
http_request.onreadystatechange = M_onPagedReady;
http_request.section_name = section;
http_request.table_id = table_id;
http_request.after_update = after;
http_request.send(null);
}
function M_PagedPrev(base_url, section, table_id, after) {
var p = document.getElementById('paged-' + section + '-opre');
M_PagedExec(base_url, section, table_id, p.textContent, after);
}
function M_PagedNext(base_url, section, table_id, after) {
var p = document.getElementById('paged-' + section + '-onex');
M_PagedExec(base_url, section, table_id, p.textContent, after);
}
/**
* Toggle the visibility of the "Quick LGTM" link on the changelist page.
* @param {String} id The id of the target element
*/
function M_toggleQuickLGTM(id) {
M_toggleSection(id);
window.scrollTo(0, document.body.offsetHeight);
}
// Comment expand/collapse
/**
* Toggles whether the specified changelist comment is expanded/collapsed.
* @param {Integer} cid The comment id, 0-indexed
*/
function M_switchChangelistComment(cid) {
M_switchCommentCommon_('cl', String(cid));
}
/**
* Toggles whether the specified file comment is expanded/collapsed.
* @param {Integer} cid The comment id, 0-indexed
*/
function M_switchFileComment(cid) {
M_switchCommentCommon_('file', String(cid));
}
/**
* Toggles whether the specified inline comment is expanded/collapsed.
* @param {Integer} cid The comment id, 0-indexed
* @param {Integer} lineno The lineno associated with the comment
* @param {String} side The side (a/b) associated with the comment
*/
function M_switchInlineComment(cid, lineno, side) {
M_switchCommentCommon_('inline', String(cid) + "-" + lineno + "-" + side);
}
/**
* Toggles whether the specified comment is expanded/collapsed on
* comment_form.html.
* @param {Integer} cid The comment id, 0-indexed
*/
function M_switchReviewComment(cid) {
M_switchCommentCommon_('cl', String(cid));
}
/**
* Toggle whether a moved_out region is expanded/collapsed.
* @param {Integer} start_line the line number of the first line to toggle
* @param {Integer} end_line the line number of the first line not to toggle
* We toggle all lines in [first_line, end_line).
*/
function M_switchMoveOut(start_line, end_line) {
for (var x = start_line; x < end_line; x++) {
var regionname = "move_out-" + x;
var region = document.getElementById(regionname);
if (region.style.display == "none") {
region.style.display = "";
} else {
region.style.display = "none";
}
}
hookState.gotoHook(0);
}
/**
* Used to expand all comments, hiding the preview and showing the comment.
* @param {String} prefix The level of the comment -- one of
* ('cl', 'file', 'inline')
* @param {Integer} num_comments The number of comments to show
*/
function M_showAllComments(prefix, num_comments) {
for (var i = 0; i < num_comments; i++) {
document.getElementById(prefix + "-preview-" + i).style.visibility =
"hidden";
document.getElementById(prefix + "-comment-" + i).style.display = "";
}
}
/**
* Used to collpase all comments, showing the preview and hiding the comment.
* @param {String} prefix The level of the comment -- one of
* ('cl', 'file', 'inline')
* @param {Integer} num_comments The number of comments to hide
*/
function M_hideAllComments(prefix, num_comments) {
for (var i = 0; i < num_comments; i++) {
document.getElementById(prefix + "-preview-" + i).style.visibility =
"visible";
document.getElementById(prefix + "-comment-" + i).style.display = "none";
}
}
// Common methods for comment handling (changelist.html, file.html,
// comment_form.html)
/**
* Toggles whether the specified comment is expanded/collapsed. Works in
* the review form.
* @param {String} prefix The prefix of the comment element name.
* @param {String} suffix The suffix of the comment element name.
*/
function M_switchCommentCommon_(prefix, suffix) {
prefix && (prefix += '-');
suffix && (suffix = '-' + suffix);
var previewSpan = document.getElementById(prefix + 'preview' + suffix);
var commentDiv = document.getElementById(prefix + 'comment' + suffix);
if (!previewSpan || !commentDiv) {
alert('Failed to find comment element: ' +
prefix + 'comment' + suffix + '. Please send ' +
'this message with the URL to the app owner');
return;
}
if (previewSpan.style.visibility == 'hidden') {
previewSpan.style.visibility = 'visible';
commentDiv.style.display = 'none';
} else {
previewSpan.style.visibility = 'hidden';
commentDiv.style.display = '';
}
}
/**
* Expands all inline comments.
*/
function M_expandAllInlineComments() {
M_showAllInlineComments();
var comments = document.getElementsByName("inline-comment");
var commentsLength = comments.length;
for (var i = 0; i < commentsLength; i++) {
comments[i].style.display = "";
}
var previews = document.getElementsByName("inline-preview");
var previewsLength = previews.length;
for (var i = 0; i < previewsLength; i++) {
previews[i].style.display = "none";
}
}
/**
* Collapses all inline comments.
*/
function M_collapseAllInlineComments() {
M_showAllInlineComments();
var comments = document.getElementsByName("inline-comment");
var commentsLength = comments.length;
for (var i = 0; i < commentsLength; i++) {
comments[i].style.display = "none";
}
var previews = document.getElementsByName("inline-preview");
var previewsLength = previews.length;
for (var i = 0; i < previewsLength; i++) {
previews[i].style.display = "";
}
}
// Non-inline comment actions
/**
* Sets up a reply form for a given comment (non-inline).
* @param {String} author The author of the comment being replied to
* @param {String} written_time The formatted time when that comment was written
* @param {String} ccs A string containing the ccs to default to
* @param {Integer} cid The number of the comment being replied to, so that the
* form may be placed in the appropriate location
* @param {String} prefix The level of the comment -- one of
* ('cl', 'file', 'inline')
* @param {Integer} opt_lineno (optional) The line number the comment should be
* attached to
* @param {String} opt_snapshot (optional) The snapshot ID of the comment being
* replied to
*/
function M_replyToComment(author, written_time, ccs, cid, prefix, opt_lineno,
opt_snapshot) {
var form = document.getElementById("comment-form-" + cid);
if (!form) {
form = document.getElementById("dareplyform");
if (!form) {
form = document.getElementById("daform"); // XXX for file.html
}
form = form.cloneNode(true);
form.name = form.id = "comment-form-" + cid;
M_createResizer_(form, cid);
document.getElementById(prefix + "-comment-" + cid).appendChild(form);
}
form.style.display = "";
form.reply_to.value = cid;
form.ccs.value = ccs;
if (typeof opt_lineno != 'undefined' && typeof opt_snapshot != 'undefined') {
form.lineno.value = opt_lineno;
form.snapshot.value = opt_snapshot;
}
form.text.value = "On " + written_time + ", " + author + " wrote:\n";
var divs = document.getElementsByName("comment-text-" + cid);
M_setValueFromDivs(divs, form.text);
form.text.value += "\n";
form.text.focus();
}
/**
/* TODO(andi): docstring
*/
function M_replyToMessage(message_id, written_time, author) {
var form = document.getElementById('message-reply-form');
form = form.cloneNode(true);
if (typeof form.message == 'undefined') {
var form_template = document.getElementById('message-reply-form');
form = document.createElement('form');
form.setAttribute('method', 'POST');
form.setAttribute('action', form_template.getAttribute('action'));
form.innerHTML = form_template.innerHTML;
}
container = document.getElementById('message-reply-'+message_id);
container.appendChild(form);
container.style.display = '';
form.discard.onclick = function () {
document.getElementById('message-reply-href-'+message_id).style.display = "";
document.getElementById('message-reply-'+message_id).innerHTML = "";
document.getElementById('message-reply-'+message_id).style.display = "none";
}
form.send_mail.id = 'message-reply-send-mail-'+message_id;
var lbl = document.getElementById(form.send_mail.id).nextSibling.nextSibling;
lbl.setAttribute('for', form.send_mail.id);
form.message.value = "On " + written_time + ", " + author + " wrote:\n";
var divs = document.getElementsByName("cl-message-" + message_id);
M_setValueFromDivs(divs, form.message);
form.message.value += "\n";
form.message.focus();
M_addTextResizer_(form);
document.getElementById('message-reply-href-'+message_id).style.display = "none";
}
/**
* Edits a non-inline draft comment.
* @param {Integer} cid The number of the comment to be edited
*/
function M_editComment(cid) {
var suffix = String(cid);
var form = document.getElementById("comment-form-" + suffix);
if (!form) {
alert("Form " + suffix + " does not exist. Please send this message " +
"with the URL to the app owner");
return false;
}
var texts = document.getElementsByName("comment-text-" + suffix);
var textsLength = texts.length;
for (var i = 0; i < textsLength; i++) {
texts[i].style.display = "none";
}
M_hideElement("edit-link-" + suffix);
M_hideElement("undo-link-" + suffix);
form.style.display = "";
form.text.focus();
}
/**
* Used to cancel comment editing, this will revert the text of the comment
* and hide its form.
* @param {Element} form The form that contains this comment
* @param {Integer} cid The number of the comment being hidden
*/
function M_resetAndHideComment(form, cid) {
form.text.blur();
form.text.value = form.oldtext.value;
form.style.display = "none";
var texts = document.getElementsByName("comment-text-" + cid);
var textsLength = texts.length;
for (var i = 0; i < textsLength; i++) {
texts[i].style.display = "";
}
M_showElement("edit-link-" + cid);
}
/**
* Removing a draft comment is the same as setting its text contents to nothing.
* @param {Element} form The form containing the draft comment to be discarded
* @return true in order for the form submission to continue
*/
function M_removeComment(form) {
form.text.value = "";
return true;
}
// Inline comments (file.html)
/**
* Helper method to assign an onclick handler to an inline 'Cancel' button.
* @param {Element} form The form containing the cancel button
* @param {Function} cancelHandler A function with one 'form' argument
* @param {Array} opt_handlerParams An array whose first three elements are:
* {String} cid The number of the comment
* {String} lineno The line number of the comment
* {String} side 'a' or 'b'
*/
function M_assignToCancel_(form, cancelHandler, opt_handlerParams) {
var elementsLength = form.elements.length;
for (var i = 0; i < elementsLength; ++i) {
if (form.elements[i].getAttribute("name") == "cancel") {
form.elements[i].onclick = function() {
if (typeof opt_handlerParams != "undefined") {
var cid = opt_handlerParams[0];
var lineno = opt_handlerParams[1];
var side = opt_handlerParams[2];
cancelHandler(form, cid, lineno, side);
} else {
cancelHandler(form);
}
};
return;
}
}
}
/**
* Helper method to assign an onclick handler to an inline '[+]' link.
* @param {Element} form The form containing the resizer
* @param {String} suffix The suffix of the comment form id: lineno-side
*/
function M_createResizer_(form, suffix) {
if (!form.hasResizer) {
var resizer = document.getElementById("resizer").cloneNode(true);
resizer.onclick = function() {
var form = document.getElementById("comment-form-" + suffix);
if (!form) return;
form.text.rows += 5;
form.text.focus();
};
// Using form.elements would be far more concise, but this hack is
// necessary because Konqueror/Safari don't populate form.elements at this
// point if the form is cloned.
var formContainer = null;
for (formContainer = form.firstChild; formContainer;
formContainer = formContainer.nextSibling) {
if (formContainer.getAttribute &&
formContainer.getAttribute("name") == "form-container") break;
}
if (!formContainer) return;
for (var n = formContainer.firstChild; n; n = n.nextSibling) {
if (n.nodeName == "TEXTAREA") {
formContainer.insertBefore(resizer, n.nextSibling);
resizer.style.display = "";
}
}
form.hasResizer = true;
}
}
/**
* Like M_createResizer_(), but updates the form's first textarea field.
* This is assumed not to be the last field.
* @param {Element} form The form whose textarea field to update.
*/
function M_addTextResizer_(form) {
var elementsLength = form.elements.length;
for (var i = 0; i < elementsLength; ++i) {
var node = form.elements[i];
if (node.nodeName == "TEXTAREA") {
var parent = M_getParent(node);
var resizer = document.getElementById("resizer").cloneNode(true);
var next = node.nextSibling;
parent.insertBefore(resizer, next);
resizer.onclick = function() {
node.rows += 5;
node.focus();
};
resizer.style.display = "";
if (next && next.className == "resizer") { // Remove old resizer.
parent.removeChild(next);
}
break;
}
}
}
/**
* Helper method to assign an onclick handler to an inline 'Save' button.
* @param {Element} form The form containing the save button
* @param {String} cid The number of the comment
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
*/
function M_assignToSave_(form, cid, lineno, side) {
var elementsLength = form.elements.length;
for (var i = 0; i < elementsLength; ++i) {
if (form.elements[i].getAttribute("name") == "save") {
form.elements[i].onclick = function() {
return M_submitInlineComment(form, cid, lineno, side);
};
return;
}
}
}
/**
* Creates an inline comment at the given line number and side of the diff.
* @param {String} lineno The line number of the new comment
* @param {String} side Either 'a' or 'b' signifying the side of the diff
*/
function M_createInlineComment(lineno, side) {
// The first field of the suffix is typically the cid, but we choose '-1'
// here since the backend has not assigned the new comment a cid yet.
var suffix = "-1-" + lineno + "-" + side;
var form = document.getElementById("comment-form-" + suffix);
if (!form) {
form = document.getElementById("dainlineform").cloneNode(true);
if (typeof form.save == "undefined") {
// For Opera form elements of the cloned form aren't accessible
// by name but using innerHTML works.
form = document.createElement("form");
form.innerHTML = document.getElementById("dainlineform").innerHTML;
}
form.name = form.id = "comment-form-" + suffix;
M_assignToCancel_(form, M_removeTempInlineComment);
M_createResizer_(form, suffix);
M_assignToSave_(form, "-1", lineno, side);
// There is a "text" node before the "div" node
form.childNodes[1].setAttribute("name", "comment-border");
var id = (side == 'a' ? "old" : "new") + "-line-" + lineno;
var td = document.getElementById(id);
td.appendChild(form);
var tr = M_getParent(td);
tr.setAttribute("name", "hook");
hookState.updateHooks();
}
form.style.display = "";
form.lineno.value = lineno;
if (side == 'b') {
form.snapshot.value = new_snapshot;
} else {
form.snapshot.value = old_snapshot;
}
form.side.value = side;
var savedDraftKey = "new-" + form.lineno.value + "-" + form.snapshot.value;
M_restoreDraftText_(savedDraftKey, form);
form.text.focus();
hookState.gotoHook(0);
}
/**
* Removes a never-submitted 'Reply' inline comment from existence (created via
* M_replyToInlineComment).
* @param {Element} form The form that contains the comment to be removed
* @param {String} cid The number of the comment
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
*/
function M_removeTempReplyInlineComment(form, cid, lineno, side) {
var divInlineComment = M_getParent(form);
var divCommentBorder = M_getParent(divInlineComment);
var td = M_getParent(divCommentBorder);
var tr = M_getParent(td);
form.cancel.blur();
// The order of the subsequent lines is sensitive to browser compatibility.
var suffix = cid + "-" + lineno + "-" + side;
M_saveDraftText_("reply-" + suffix, form.text.value);
divInlineComment.removeChild(form);
M_updateRowHook(tr);
}
/**
* Removes a never-submitted inline comment from existence (created via
* M_createInlineComment). Saves the existing text for the next time a draft is
* created on the same line.
* @param {Element} form The form that contains the comment to be removed
*/
function M_removeTempInlineComment(form) {
var td = M_getParent(form);
var tr = M_getParent(td);
// The order of the subsequent lines is sensitive to browser compatibility.
var savedDraftKey = "new-" + form.lineno.value + "-" + form.snapshot.value;
M_saveDraftText_(savedDraftKey, form.text.value);
form.cancel.blur();
td.removeChild(form);
M_updateRowHook(tr);
}
/**
* Helper to edit a draft inline comment.
* @param {String} cid The number of the comment
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
* @return {Element} The form that contains the comment
*/
function M_editInlineCommentCommon_(cid, lineno, side) {
var suffix = cid + "-" + lineno + "-" + side;
var form = document.getElementById("comment-form-" + suffix);
if (!form) {
alert("Form " + suffix + " does not exist. Please send this message " +
"with the URL to the app owner");
return false;
}
M_createResizer_(form, suffix);
var texts = document.getElementsByName("comment-text-" + suffix);
var textsLength = texts.length;
for (var i = 0; i < textsLength; i++) {
texts[i].style.display = "none";
}
M_hideElement("edit-link-" + suffix);
M_hideElement("undo-link-" + suffix);
form.style.display = "";
var parent = document.getElementById("inline-comment-" + suffix);
if (parent && parent.style.display == "none") {
M_switchInlineComment(cid, lineno, side);
}
form.text.focus();
hookState.gotoHook(0);
return form;
}
/**
* Edits a draft inline comment.
* @param {String} cid The number of the comment
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
*/
function M_editInlineComment(cid, lineno, side) {
M_editInlineCommentCommon_(cid, lineno, side);
}
/**
* Restores a canceled draft inline comment for editing.
* @param {String} cid The number of the comment
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
*/
function M_restoreEditInlineComment(cid, lineno, side) {
var form = M_editInlineCommentCommon_(cid, lineno, side);
var savedDraftKey = "edit-" + cid + "-" + lineno + "-" + side;
M_restoreDraftText_(savedDraftKey, form, false);
}
/**
* Helper to reply to an inline comment.
* @param {String} author The author of the comment being replied to
* @param {String} written_time The formatted time when that comment was written
* @param {String} ccs A string containing the ccs to default to
* @param {String} cid The number of the comment being replied to, so that the
* form may be placed in the appropriate location
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
* @param {String} opt_reply The response to pre-fill with.
* @param {Boolean} opt_submit This will submit the comment right after
* creation. Only makes sense when opt_reply is set
* @return {Element} The form that contains the comment
*/
function M_replyToInlineCommentCommon_(author, written_time, cid, lineno,
side, opt_reply, opt_submit) {
var suffix = cid + "-" + lineno + "-" + side;
var form = document.getElementById("comment-form-" + suffix);
if (!form) {
form = document.getElementById("dainlineform").cloneNode(true);
form.name = form.id = "comment-form-" + suffix;
M_assignToCancel_(form, M_removeTempReplyInlineComment,
[cid, lineno, side]);
M_assignToSave_(form, cid, lineno, side);
M_createResizer_(form, suffix);
var parent = document.getElementById("inline-comment-" + suffix);
if (parent.style.display == "none") {
M_switchInlineComment(cid, lineno, side);
}
parent.appendChild(form);
}
form.style.display = "";
form.lineno.value = lineno;
if (side == 'b') {
form.snapshot.value = new_snapshot;
} else {
form.snapshot.value = old_snapshot;
}
form.side.value = side;
if (!M_restoreDraftText_("reply-" + suffix, form, false) ||
typeof opt_reply != "undefined") {
form.text.value = "On " + written_time + ", " + author + " wrote:\n";
var divs = document.getElementsByName("comment-text-" + suffix);
M_setValueFromDivs(divs, form.text);
form.text.value += "\n";
if (typeof opt_reply != "undefined") {
form.text.value += opt_reply;
}
if (opt_submit) {
M_submitInlineComment(form, cid, lineno, side);
return;
}
}
form.text.focus();
hookState.gotoHook(0);
return form;
}
/**
* Replies to an inline comment.
* @param {String} author The author of the comment being replied to
* @param {String} written_time The formatted time when that comment was written
* @param {String} ccs A string containing the ccs to default to
* @param {String} cid The number of the comment being replied to, so that the
* form may be placed in the appropriate location
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
* @param {String} opt_reply The response to pre-fill with.
* @param {Boolean} opt_submit This will submit the comment right after
* creation. Only makes sense when opt_reply is set
*/
function M_replyToInlineComment(author, written_time, cid, lineno, side,
opt_reply, opt_submit) {
M_replyToInlineCommentCommon_(author, written_time, cid, lineno, side,
opt_reply, opt_submit);
}
/**
* Restores a canceled draft inline comment for reply.
* @param {String} author The author of the comment being replied to
* @param {String} written_time The formatted time when that comment was written
* @param {String} ccs A string containing the ccs to default to
* @param {String} cid The number of the comment being replied to, so that the
* form may be placed in the appropriate location
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
*/
function M_restoreReplyInlineComment(author, written_time, cid, lineno,
side) {
var form = M_replyToInlineCommentCommon_(author, written_time, cid,
lineno, side);
var savedDraftKey = "reply-" + cid + "-" + lineno + "-" + side;
M_restoreDraftText_(savedDraftKey, form, false);
}
/**
* Updates an inline comment td with the given HTML.
* @param {Element} td The TD that contains the inline comment
* @param {String} html The text to be put into .innerHTML of the td
*/
function M_updateInlineComment(td, html) {
var tr = M_getParent(td);
if (!tr) {
alert("TD had no parent. Please notify the app owner.");
return;
}
// The server sends back " " to make things empty, for Safari
if (html.length <= 1) {
td.innerHTML = "";
M_updateRowHook(tr);
} else {
td.innerHTML = html;
tr.name = "hook";
hookState.updateHooks();
}
}
/**
* Updates a comment tr's name, depending on whether there are now comments
* in it or not. Also updates the hook cache if required. Assumes that the
* given TR already has name == "hook" and only tries to remove it if all
* are empty.
* @param {Element} tr The TR containing the potential comments
*/
function M_updateRowHook(tr) {
if (!(tr && tr.cells)) return;
// If all of the TR's cells are empty, remove the hook name
var i = 0;
var numCells = tr.cells.length;
for (i = 0; i < numCells; i++) {
if (tr.cells[i].innerHTML != "") {
break;
}
}
if (i == numCells) {
tr.setAttribute("name", "");
hookState.updateHooks();
}
hookState.gotoHook(0);
}
/**
* Submits an inline comment and updates the DOM in AJAX fashion with the new
* comment data for that line.
* @param {Element} form The form containing the submitting comment
* @param {String} cid The number of the comment
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
* @return true if AJAX fails and the form should be submitted the "old" way,
* or false if the form is submitted using AJAX, preventing the regular
* form submission from proceeding
*/
function M_submitInlineComment(form, cid, lineno, side) {
var td = null;
if (form.side.value == 'a') {
td = document.getElementById("old-line-" + form.lineno.value);
} else {
td = document.getElementById("new-line-" + form.lineno.value);
}
if (!td) {
alert("Could not find snapshot " + form.snapshot.value + "! Please let " +
"the app owner know.");
return true;
}
// Clear saved draft state for affected new, edited, and replied comments
if (typeof cid != "undefined" && typeof lineno != "undefined" && side) {
var suffix = cid + "-" + lineno + "-" + side;
M_clearDraftText_("new-" + lineno + "-" + form.snapshot.value);
M_clearDraftText_("edit-" + suffix);
M_clearDraftText_("reply-" + suffix);
M_hideElement("undo-link-" + suffix);
}
var httpreq = M_getXMLHttpRequest();
if (!httpreq) {
// No AJAX. Oh well. Go ahead and submit this the old way.
return true;
}
// Konqueror jumps to a random location for some reason
var scrollTop = M_getScrollTop(window);
var aborted = false;
reenable_form = function() {
form.save.disabled = false;
form.cancel.disabled = false;
if (form.discard != null) {
form.discard.disabled = false;
}
form.text.disabled = false;
form.style.cursor = "auto";
};
// This timeout can potentially race with the request coming back OK. In
// general, if it hasn't come back for 60 seconds, it won't ever come back.
var httpreq_timeout = setTimeout(function() {
aborted = true;
httpreq.abort();
reenable_form();
alert("Comment could not be submitted for 60 seconds. Please ensure " +
"connectivity (and that the server is up) and try again.");
}, 60000);
httpreq.onreadystatechange = function () {
// Firefox 2.0, at least, runs this with readyState = 4 but all other
// fields unset when the timeout aborts the request, against all
// documentation.
if (httpreq.readyState == 4 && !aborted) {
clearTimeout(httpreq_timeout);
if (httpreq.status == 200) {
M_updateInlineComment(td, httpreq.responseText);
} else {
reenable_form();
alert("An error occurred while trying to submit the comment: " +
httpreq.statusText);
}
if (M_isKHTML()) {
window.scrollTo(0, scrollTop);
}
}
}
httpreq.open("POST", "/inline_draft", true);
httpreq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var req = [];
var len = form.elements.length;
for (var i = 0; i < len; i++) {
var element = form.elements[i];
if (element.type == "hidden" || element.type == "textarea") {
req.push(element.name + "=" + encodeURIComponent(element.value));
}
}
req.push("side=" + side);
// Disable forever. If this succeeds, then the form will end up getting
// rewritten, and if it fails, the page should get a refresh anyways.
form.save.blur();
form.save.disabled = true;
form.cancel.blur();
form.cancel.disabled = true;
if (form.discard != null) {
form.discard.blur();
form.discard.disabled = true;
}
form.text.blur();
form.text.disabled = true;
form.style.cursor = "wait";
// Send the request
httpreq.send(req.join("&"));
// No need to resubmit this form.
return false;
}
/**
* Removes a draft inline comment.
* @param {Element} form The form that contains the comment to be removed
* @param {String} cid The number of the comment
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
*/
function M_removeInlineComment(form, cid, lineno, side) {
// Update state to save the canceled edit text
var snapshot = side == "a" ? old_snapshot : new_snapshot;
var savedDraftKey = "new-" + lineno + "-" + snapshot;
var savedText = form.text.value;
form.text.value = "";
var ret = M_submitInlineComment(form, cid, lineno, side);
M_saveDraftText_(savedDraftKey, savedText);
return ret;
}
/**
* Combines all the divs from a single comment (generated by multiple buckets)
* and undoes the escaping work done by Django filters, and inserts the result
* into a given textarea.
* @param {Array} divs An array of div elements to be combined
* @param {Element} text The textarea whose value needs to be updated
*/
function M_setValueFromDivs(divs, text) {
lines = [];
var divsLength = divs.length;
for (var i = 0; i < divsLength; i++) {
lines = lines.concat(divs[i].innerHTML.split("\n"));
}
for (var i = 0; i < lines.length; i++) {
// Undo the <a> tags added by urlize and enliven
lines[i] = lines[i].replace(/<a[^>]*>/ig, "");
lines[i] = lines[i].replace(/<\/a>/ig, "");
// Undo the escape Django filter
lines[i] = lines[i].replace(/&gt;/ig, ">");
lines[i] = lines[i].replace(/&lt;/ig, "<");
lines[i] = lines[i].replace(/&quot;/ig, "\"");
lines[i] = lines[i].replace(/&#39;/ig, "'");
lines[i] = lines[i].replace(/&amp;/ig, "&"); // Must be last
text.value += "> " + lines[i] + "\n";
}
}
/**
* Undo an edit of a draft inline comment, i.e. discard changes.
* @param {Element} form The form containing the edits
* @param {String} cid The number of the comment
* @param {String} lineno The line number of the comment
* @param {String} side 'a' or 'b'
*/
function M_resetAndHideInlineComment(form, cid, lineno, side) {
// Update canceled edit state
var suffix = cid + "-" + lineno + "-" + side;
M_saveDraftText_("edit-" + suffix, form.text.value);
if (form.text.value != form.oldtext.value) {
M_showElement("undo-link-" + suffix);
}
form.text.blur();
form.text.value = form.oldtext.value;
form.style.display = "none";
var texts = document.getElementsByName("comment-text-" + suffix);
var textsLength = texts.length;
for (var i = 0; i < textsLength; i++) {
texts[i].style.display = "";
}
M_showElement("edit-link-" + suffix);
hookState.gotoHook(0);
}
/**
* Toggles whether we display quoted text or not, both for inline and regular
* comments. Inline comments will have lineno and side defined.
* @param {String} cid The comment number
* @param {String} bid The bucket number in that comment
* @param {String} lineno (optional) Line number of the comment
* @param {String} side (optional) 'a' or 'b'
*/
function M_switchQuotedText(cid, bid, lineno, side) {
var tmp = ""
if (typeof lineno != 'undefined' && typeof side != 'undefined')
tmp = "-" + lineno + "-" + side;
var div = document.getElementById("comment-text-" + cid + tmp + "-" + bid)
if (div.style.display == "none") {
div.style.display = "";
} else {
div.style.display = "none";
}
if (tmp != "") {
hookState.gotoHook(0);
}
}
/**
* Handler for the double click event in the code table element. Creates a new
* inline comment for that line of code on the right side of the diff.
* @param {Event} evt The event object for this double-click event
*/
function M_handleTableDblClick(evt) {
if (!logged_in) {
if (!login_warned) {
login_warned = true;
alert("Please sign in to enter inline comments.");
}
return;
}
var evt = evt ? evt : (event ? event : null);
var target = M_getEventTarget(evt);
if (target.tagName == 'INPUT' || target.tagName == 'TEXTAREA') {
return;
}
while (target != null && target.tagName != 'TD') {
target = M_getParent(target);
}
if (target == null) {
return;
}
var side = null;
if (target.id.substr(0, 7) == "newcode") {
side = 'b';
} else if (target.id.substr(0, 7) == "oldcode") {
side = 'a';
}
if (side != null) {
M_createInlineComment(parseInt(target.id.substr(7)), side);
}
}
/**
* Makes all inline comments visible. This is the default view.
*/
function M_showAllInlineComments() {
var hide = document.getElementById("hide-all-inline");
var show = document.getElementById("show-all-inline");
hide.style.display = "";
var elements = document.getElementsByName("comment-border");
var elementsLength = elements.length;
for (var i = 0; i < elementsLength; i++) {
var tr = M_getParent(M_getParent(elements[i]));
tr.style.display = "";
tr.name = "hook";
}
show.style.display = "none";
hookState.updateHooks();
}
/**
* Hides all inline comments, to make code easier ot read.
*/
function M_hideAllInlineComments() {
var hide = document.getElementById("hide-all-inline");
var show = document.getElementById("show-all-inline");
show.style.display = "";
var elements = document.getElementsByName("comment-border");
var elementsLength = elements.length;
for (var i = 0; i < elementsLength; i++) {
var tr = M_getParent(M_getParent(elements[i]));
tr.style.display = "none";
tr.name = "";
}
hide.style.display = "none";
hookState.updateHooks();
}
/**
* Flips between making inline comments visible and invisible.
*/
function M_toggleAllInlineComments() {
var show = document.getElementById("show-all-inline");
if (!show) {
return;
}
if (show.style.display == "none") {
M_hideAllInlineComments();
} else {
M_showAllInlineComments();
}
}
// File view keyboard navigation
/**
* M_HookState class. Keeps track of the current 'hook' that we are on and
* responds to n/p/N/P events.
* @param {Window} win The window that the table is in.
* @constructor
*/
function M_HookState(win) {
/**
* -2 == top of page; -1 == diff; or index into hooks array
* @type Integer
*/
this.hookPos = -2;
/**
* A cache of visible table rows with tr.name == "hook"
* @type Array
*/
this.visibleHookCache = [];
/**
* The indicator element that we move around
* @type Element
*/
this.indicator = document.getElementById("hook-sel");
/**
* Caches whether we are in an IE browser
* @type Boolean
*/
this.isIE = M_isIE();
/**
* The window that the table with the hooks is in
* @type Window
*/
this.win = win;
}
/**
* Find all the hook locations in a browser-portable fashion, and store them
* in a cache.
* @return Array of TR elements.
*/
M_HookState.prototype.computeHooks_ = function() {
var allHooks = null;
if (this.isIE) {
// IE only recognizes the 'name' attribute on tags that are supposed to
// have one, such as... not TR.
var tmpHooks = document.getElementsByTagName("TR");
var tmpHooksLength = tmpHooks.length;
allHooks = [];
for (var i = 0; i < tmpHooksLength; i++) {
if (tmpHooks[i].name == "hook") {
allHooks.push(tmpHooks[i]);
}
}
} else {
allHooks = document.getElementsByName("hook");
}
var visibleHooks = [];
var allHooksLength = allHooks.length;
for (var i = 0; i < allHooksLength; i++) {
var hook = allHooks[i];
if (hook.style.display == "") {
visibleHooks.push(hook);
}
}
this.visibleHookCache = visibleHooks;
return visibleHooks;
};
/**
* Recompute all the hook positions, update the hookPos, and update the
* indicator's position if necessary, but do not scroll.
*/
M_HookState.prototype.updateHooks = function() {
var curHook = null;
if (this.hookPos >= 0 && this.hookPos < this.visibleHookCache.length) {
curHook = this.visibleHookCache[this.hookPos];
}
this.computeHooks_();
var newHookPos = -1;
if (curHook != null) {
for (var i = 0; i < this.visibleHookCache.length; i++) {
if (this.visibleHookCache[i] == curHook) {
newHookPos = i;
break;
}
}
}
if (newHookPos != -1) {
this.hookPos = newHookPos;
}
this.gotoHook(0);
};
/**
* Update the indicator's position to be at the top of the table row.
* @param {Element} tr The tr whose top the indicator will be lined up with.
*/
M_HookState.prototype.updateIndicator_ = function(tr) {
// Find out where the table's top is, and add one so that when we align
// the position indicator, it takes off 1px from one tr and 1px from another.
// This must be computed every time since the top of the table may move due
// to window resizing.
var tableTop = M_getPageOffsetTop(document.getElementById("table-top")) + 1;
this.indicator.style.top = String(M_getPageOffsetTop(tr) -
tableTop) + "px";
var totWidth = 0;
var numCells = tr.cells.length;
for (var i = 0; i < numCells; i++) {
totWidth += tr.cells[i].clientWidth;
}
this.indicator.style.left = "0px";
this.indicator.style.width = totWidth + "px";
this.indicator.style.display = "";
};
/**
* Update the indicator's position, and potentially scroll to the proper
* location. Computes the new position based on current scroll position, and
* whether the previously selected hook was visible.
* @param {Integer} direction Scroll direction: -1 for up only, 1 for down only,
* 0 for no scrolling.
*/
M_HookState.prototype.gotoHook = function(direction) {
var hooks = this.visibleHookCache;
// Hide the current selection image
this.indicator.style.display = "none";
// Add a border to all td's in the selected row
if (this.hookPos < -1) {
if (direction != 0) {
window.scrollTo(0, 0);
}
this.hookPos = -2;
} else if (this.hookPos == -1) {
var diffs = document.getElementsByName("diffs");
if (diffs && diffs.length >= 1) {
diffs = diffs[0];
}
if (diffs && direction != 0) {
window.scrollTo(0, M_getPageOffsetTop(diffs));
}
this.updateIndicator_(document.getElementById("thecode").rows[0]);
} else {
if (this.hookPos < hooks.length) {
var hook = hooks[this.hookPos];
for (var i = 0; i < hook.cells.length; i++) {
var td = hook.cells[i];
if (td.id != null && td.id != "") {
if (direction != 0) {
M_scrollIntoView(this.win, td, direction);
}
break;
}
}
// Found one!
this.updateIndicator_(hook);
} else {
if (direction != 0) {
window.scrollTo(0, document.body.offsetHeight);
}
this.hookPos = hooks.length;
var thecode = document.getElementById("thecode");
this.updateIndicator_(thecode.rows[thecode.rows.length - 1]);
}
}
};
/**
* Set this.hookPos to the next desired hook.
* @param {Boolean} findComment Whether to look only for comment hooks
*/
M_HookState.prototype.incrementHook_ = function(findComment) {
var hooks = this.visibleHookCache;
if (findComment) {
this.hookPos = Math.max(0, this.hookPos + 1);
while (this.hookPos < hooks.length &&
hooks[this.hookPos].className != "inline-comments") {
this.hookPos++;
}
} else {
this.hookPos = Math.min(hooks.length, this.hookPos + 1);
}
};
/**
* Set this.hookPos to the previous desired hook.
* @param {Boolean} findComment Whether to look only for comment hooks
*/
M_HookState.prototype.decrementHook_ = function(findComment) {
var hooks = this.visibleHookCache;
if (findComment) {
this.hookPos = Math.min(hooks.length - 1, this.hookPos - 1);
while (this.hookPos >= 0 &&
hooks[this.hookPos].className != "inline-comments") {
this.hookPos--;
}
} else {
this.hookPos = Math.max(-2, this.hookPos - 1);
}
};
/**
* Find the first document element in sorted array elts whose vertical position
* is greater than the given height from the top of the document. Optionally
* look only for comment elements.
*
* @param {Integer} height The height in pixels from the top
* @param {Array.<Element>} elts Document elements
* @param {Boolean} findComment Whether to look only for comment elements
* @return {Integer} The index of such an element, or elts.length otherwise
*/
function M_findElementAfter_(height, elts, findComment) {
for (var i = 0; i < elts.length; ++i) {
if (M_getPageOffsetTop(elts[i]) > height) {
if (!findComment || elts[i].className == "inline-comments") {
return i;
}
}
}
return elts.length;
}
/**
* Find the last document element in sorted array elts whose vertical position
* is less than the given height from the top of the document. Optionally
* look only for comment elements.
*
* @param {Integer} height The height in pixels from the top
* @param {Array.<Element>} elts Document elements
* @param {Boolean} findComment Whether to look only for comment elements
* @return {Integer} The index of such an element, or -1 otherwise
*/
function M_findElementBefore_(height, elts, findComment) {
for (var i = elts.length - 1; i >= 0; --i) {
if (M_getPageOffsetTop(elts[i]) < height) {
if (!findComment || elts[i].className == "inline-comments") {
return i;
}
}
}
return -1;
}
/**
* Move to the next hook indicator and scroll.
* @param opt_findComment {Boolean} Whether to look only for comment hooks
*/
M_HookState.prototype.gotoNextHook = function(opt_findComment) {
// If the current hook is not on the page, select the first hook that is
// either on the screen or below.
var hooks = this.visibleHookCache;
var diffs = document.getElementsByName("diffs");
var thecode = document.getElementById("thecode");
var findComment = Boolean(opt_findComment);
if (diffs && diffs.length >= 1) {
diffs = diffs[0];
}
if (this.hookPos >= 0 && this.hookPos < hooks.length &&
M_isElementVisible(this.win, hooks[this.hookPos].cells[0])) {
this.incrementHook_(findComment);
} else if (this.hookPos == -2 &&
(M_isElementVisible(this.win, diffs) ||
M_getScrollTop(this.win) < M_getPageOffsetTop(diffs))) {
this.incrementHook_(findComment)
} else if (this.hookPos < hooks.length || (this.hookPos >= hooks.length &&
!M_isElementVisible(
this.win, thecode.rows[thecode.rows.length - 1].cells[0]))) {
var scrollTop = M_getScrollTop(this.win);
this.hookPos = M_findElementAfter_(scrollTop, hooks, findComment);
}
this.gotoHook(1);
};
/**
* Move to the previous hook indicator and scroll.
* @param opt_findComment {Boolean} Whether to look only for comment hooks
*/
M_HookState.prototype.gotoPrevHook = function(opt_findComment) {
// If the current hook is not on the page, select the last hook that is
// above the bottom of the screen window.
var hooks = this.visibleHookCache;
var diffs = document.getElementsByName("diffs");
var findComment = Boolean(opt_findComment);
if (diffs && diffs.length >= 1) {
diffs = diffs[0];
}
if (this.hookPos == 0 && findComment) {
this.hookPos = -2;
} else if (this.hookPos >= 0 && this.hookPos < hooks.length &&
M_isElementVisible(this.win, hooks[this.hookPos].cells[0])) {
this.decrementHook_(findComment);
} else if (this.hookPos > hooks.length) {
this.hookPos = hooks.length;
} else if (this.hookPos == -1 && M_isElementVisible(this.win, diffs)) {
this.decrementHook_(findComment);
} else if (this.hookPos == -2 && M_getScrollTop(this.win) == 0) {
} else {
var scrollBot = M_getScrollTop(this.win) + M_getWindowHeight(this.win);
this.hookPos = M_findElementBefore_(scrollBot, hooks, findComment);
}
// The top of the diffs table is irrelevant if we want comment hooks.
if (findComment && this.hookPos <= -1) {
this.hookPos = -2;
}
this.gotoHook(-1);
};
/**
* If the currently selected hook is a comment, either respond to it or edit
* the draft if there is one already. Prefer the right side of the table.
*/
M_HookState.prototype.respond = function() {
var hooks = this.visibleHookCache;
if (this.hookPos >= 0 && this.hookPos < hooks.length &&
M_isElementVisible(this.win, hooks[this.hookPos].cells[0])) {
// Go through this tr and try responding to the last comment. The general
// hope is that these are returned in DOM order
var comments = hooks[this.hookPos].getElementsByTagName("div");
var commentsLength = comments.length;
if (comments && commentsLength > 0) {
var last = null;
for (var i = commentsLength - 1; i >= 0; i--) {
if (comments[i].getAttribute("name") == "comment-border") {
last = comments[i];
break;
}
}
if (last) {
var links = last.getElementsByTagName("a");
if (links) {
for (var i = links.length - 1; i >= 0; i--) {
if (links[i].getAttribute("name") == "comment-reply" &&
links[i].style.display != "none") {
document.location.href = links[i].href;
return;
}
}
}
}
}
// Create a comment at this line
// TODO: Implement this in a sane fashion, e.g. opens up a comment
// at the end of the diff chunk.
/*
var tr = hooks[this.hookPos];
for (var i = tr.cells.length - 1; i >= 0; i--) {
if (tr.cells[i].id.substr(0, 7) == "newcode") {
createInlineComment(parseInt(tr.cells[i].id.substr(7)), 'b');
return;
} else if (tr.cells[i].id.substr(0, 7) == "oldcode") {
createInlineComment(parseInt(tr.cells[i].id.substr(7)), 'a');
return;
}
}
*/
}
};
// Intra-line diff handling
/**
* IntraLineDiff class. Initializes structures to keep track of highlighting
* state.
* @constructor
*/
function M_IntraLineDiff() {
/**
* Whether we are showing intra-line changes or not
* @type Boolean
*/
this.intraLine = true;
/**
* "oldreplace" css rule
* @type CSSStyleRule
*/
this.oldReplace = null;
/**
* "oldlight" css rule
* @type CSSStyleRule
*/
this.oldLight = null;
/**
* "newreplace" css rule
* @type CSSStyleRule
*/
this.newReplace = null;
/**
* "newlight" css rule
* @type CSSStyleRule
*/
this.newLight = null;
/**
* backup of the "oldreplace" css rule's background color
* @type DOMString
*/
this.saveOldReplaceBkgClr = null;
/**
* backup of the "newreplace" css rule's background color
* @type DOMString
*/
this.saveNewReplaceBkgClr = null;
/**
* "oldreplace1" css rule's background color
* @type DOMString
*/
this.oldIntraBkgClr = null;
/**
* "newreplace1" css rule's background color
* @type DOMString
*/
this.newIntraBkgClr = null;
this.findStyles_();
}
/**
* Finds the styles in the document and keeps references to them in this class
* instance.
*/
M_IntraLineDiff.prototype.findStyles_ = function() {
var ss = document.styleSheets[0];
var rules = [];
if (ss.cssRules) {
rules = ss.cssRules;
} else if (ss.rules) {
rules = ss.rules;
}
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (rule.selectorText == ".oldreplace1") {
this.oldIntraBkgClr = rule.style.backgroundColor;
} else if (rule.selectorText == ".newreplace1") {
this.newIntraBkgClr = rule.style.backgroundColor;
} else if (rule.selectorText == ".oldreplace") {
this.oldReplace = rule;
this.saveOldReplaceBkgClr = this.oldReplace.style.backgroundColor;
} else if (rule.selectorText == ".newreplace") {
this.newReplace = rule;
this.saveNewReplaceBkgClr = this.newReplace.style.backgroundColor;
} else if (rule.selectorText == ".oldlight") {
this.oldLight = rule;
} else if (rule.selectorText == ".newlight") {
this.newLight = rule;
}
}
};
/**
* Toggle the highlighting of the intra line diffs, alternatively turning
* them on and off.
*/
M_IntraLineDiff.prototype.toggle = function() {
if (this.intraLine) {
this.oldReplace.style.backgroundColor = this.oldIntraBkgClr;
this.oldLight.style.backgroundColor = this.oldIntraBkgClr;
this.newReplace.style.backgroundColor = this.newIntraBkgClr;
this.newLight.style.backgroundColor = this.newIntraBkgClr;
this.intraLine = false;
} else {
this.oldReplace.style.backgroundColor = this.saveOldReplaceBkgClr;
this.oldLight.style.backgroundColor = this.saveOldReplaceBkgClr;
this.newReplace.style.backgroundColor = this.saveNewReplaceBkgClr;
this.newLight.style.backgroundColor = this.saveNewReplaceBkgClr;
this.intraLine = true;
}
};
/**
* A click handler common to just about every page, set in global.html.
* @param {Event} evt The event object that triggered this handler.
* @return false if the event was handled.
*/
function M_clickCommon(evt) {
if (helpDisplayed) {
var help = document.getElementById("help");
help.style.display = "none";
helpDisplayed = false;
return false;
}
return true;
}
/**
* Common keypress handling code for all pages.
* @param {Event} evt The event object that triggered this callback
* @param {function(string)} handler Handles the specific key pressed;
* returns false if the key press was handled.
* @param {function(Event, Node, int, string)} input_handler
* Handles the event in case that the event source is an input field.
* returns false if the key press was handled.
* @return false if the event was handled
*/
function M_keyPressCommon(evt, handler, input_handler) {
var evt = (evt) ? evt : ((event) ? event : null);
if (evt) {
var src = M_getEventTarget(evt);
var nodename = src.nodeName;
var key, code;
if (evt.keyCode) {
code = evt.keyCode;
} else if (evt.which) {
code = evt.which;
}
key = String.fromCharCode(code);
if (nodename == "TEXTAREA" || nodename == "INPUT" ) {
if (typeof input_handler != 'undefined') {
return input_handler(evt, src, code, key);
}
return true;
}
if (evt.altKey || evt.altLeft ||
evt.ctrlKey || evt.ctrlLeft ||
evt.metaKey) {
// Ignore if any modifier keys are set
return true;
}
if (key == '?' ||
code == (window.event ? 27 /* ESC */ : evt.DOM_VK_ESCAPE)) {
var help = document.getElementById("help");
if (help && typeof helpDisplayed != "undefined") {
// Only allow the help to be turned on with the ? key.
if (helpDisplayed || key == '?') {
helpDisplayed = !helpDisplayed;
}
help.style.display = helpDisplayed ? "" : "none";
}
return false;
}
return handler(key);
}
return true;
}
/**
* Helper event handler for the keypress event in a comment textarea.
* @param {Event} evt The event object that triggered this callback
* @param {Node} src The textarea document element
* @param {int} code The key code of the key press
* @param {String} key The string describing the key press
* @return false if the key press was handled
*/
function M_commentTextKeyPress_(evt, src, code, key) {
if (src.nodeName == "TEXTAREA") {
if (evt.ctrlKey || evt.ctrlLeft) {
if (key == 's' || code == 19 /* ASCII code for ^S */) {
// Save the form corresponding to this text area.
M_disableCarefulUnload();
if (src.form.save.onclick) {
return src.form.save.onclick();
} else {
src.form.submit();
return false;
}
}
} else if (evt.altKey || evt.altLeft) {
} else if (evt.shiftKey || evt.shiftLeft) {
} else if (evt.metaKey) {
} else {
if (code == (window.event ? 27 /* ASCII code for Escape */
: evt.DOM_VK_ESCAPE)) {
return src.form.cancel.onclick();
}
}
}
return true;
}
/**
* Event handler for the keypress event in the file view.
* @param {Event} evt The event object that triggered this callback
* @return false if the key press was handled
*/
function M_keyPress(evt) {
return M_keyPressCommon(evt, function(key) {
if (key == 'n') {
// next diff
if (hookState) hookState.gotoNextHook();
} else if (key == 'p') {
// previous diff
if (hookState) hookState.gotoPrevHook();
} else if (key == 'N') {
// next comment
if (hookState) hookState.gotoNextHook(true);
} else if (key == 'P') {
// previous comment
if (hookState) hookState.gotoPrevHook(true);
} else if (key == 'j') {
// next file
var nextFile = document.getElementById('nextFile');
if (nextFile) {
document.location.href = nextFile.href;
} else {
M_upToChangelist();
}
} else if (key == 'k') {
// prev file
var prevFile = document.getElementById('prevFile');
if (prevFile) {
document.location.href = prevFile.href;
} else {
M_upToChangelist();
}
} else if (key == 'm') {
document.location.href = publish_link;
} else if (key == 'u') {
// up to CL
M_upToChangelist();
} else if (key == 'i') {
// toggle intra line diff
if (intraLineDiff) intraLineDiff.toggle();
} else if (key == 's') {
// toggle show/hide inline comments
M_toggleAllInlineComments();
} else if (key == 'e') {
M_expandAllInlineComments();
} else if (key == 'c') {
M_collapseAllInlineComments();
} else if (key == '\r' || key == '\n') {
// respond to current comment
if (hookState) hookState.respond();
} else {
return true;
}
return false;
}, M_commentTextKeyPress_);
}
/**
* Event handler for the keypress event in the changelist view.
* @param {Event} evt The event object that triggered this callback
* @return false if the key press was handled
*/
function M_changelistKeyPress(evt) {
return M_keyPressCommon(evt, function(key) {
if (key == 'o' || key == '\r' || key == '\n') {
if (dashboardState) {
var child = dashboardState.curTR.cells[1].firstChild;
while (child && child.nextSibling && child.nodeName != "A") {
child = child.nextSibling;
}
if (child && child.nodeName == "A") {
location.href = child.href;
}
}
} else if (key == 'i') {
if (dashboardState) {
var child = dashboardState.curTR.cells[2].firstChild;
while (child && child.nextSibling &&
(child.nodeName != "A" || child.style.display == "none")) {
child = child.nextSibling;
}
if (child && child.nodeName == "A") {
child.onclick();
}
}
} else if (key == 'I') {
if (M_CL_hiddenInlineDiffCount == M_CL_maxHiddenInlineDiffCount) {
M_showAllDiffs(M_CL_maxHiddenInlineDiffCount);
} else {
M_hideAllDiffs(M_CL_maxHiddenInlineDiffCount);
}
} else if (key == 'k') {
if (dashboardState) dashboardState.gotoPrev();
} else if (key == 'j') {
if (dashboardState) dashboardState.gotoNext();
} else if (key == 'm') {
document.location.href = publish_link;
} else if (key == 'u') {
// back to dashboard
document.location.href = '/';
} else {
return true;
}
return false;
});
}
/**
* Goes from the file view back up to the changelist view.
*/
function M_upToChangelist() {
var upCL = document.getElementById('upCL');
if (upCL) {
document.location.href = upCL.href;
}
}
/**
* Asynchronously request static analysis warnings as comments.
* @param {String} cl The current changelist
* @param {String} depot_path The id of the target element
* @param {String} a The version number of the left side to be analyzed
* @param {String} b The version number of the right side to be analyzed
*/
function M_getBugbotComments(cl, depot_path, a, b) {
var httpreq = M_getXMLHttpRequest();
if (!httpreq) {
return;
}
// Konqueror jumps to a random location for some reason
var scrollTop = M_getScrollTop(window);
httpreq.onreadystatechange = function () {
// Firefox 2.0, at least, runs this with readyState = 4 but all other
// fields unset when the timeout aborts the request, against all
// documentation.
if (httpreq.readyState == 4) {
if (httpreq.status == 200) {
M_updateWarningStatus(httpreq.responseText);
}
if (M_isKHTML()) {
window.scrollTo(0, scrollTop);
}
}
}
httpreq.open("GET", "/warnings/" + cl + "/" + depot_path +
"?a=" + a + "&b=" + b, true);
httpreq.send(null);
}
/**
* Updates a warning status td with the given HTML.
* @param {String} result The new html to replace the existing content
*/
function M_updateWarningStatus(result) {
var elem = document.getElementById("warnings");
elem.innerHTML = result;
if (hookState) hookState.updateHooks();
}
/* Ripped off from Caribou */
var M_CONFIRM_DISCARD_NEW_MSG = "Your draft comment has not been saved " +
"or sent.\n\nDiscard your comment?";
var M_useCarefulUnload = true;
/**
* Return an alert if the specified textarea is visible and non-empty.
*/
function M_carefulUnload(text_area_id) {
return function () {
var text_area = document.getElementById(text_area_id);
if (!text_area) return;
var text_parent = M_getParent(text_area);
if (M_useCarefulUnload && text_area.style.display != "none"
&& text_parent.style.display != "none"
&& goog.string.trim(text_area.value)) {
return M_CONFIRM_DISCARD_NEW_MSG;
}
};
}
function M_disableCarefulUnload() {
M_useCarefulUnload = false;
}
// History Table
/**
* Toggles visibility of the snapshots that belong to the given parent.
* @param {String} parent The parent's index
* @param {Boolean} opt_show If present, whether to show or hide the group
*/
function M_toggleGroup(parent, opt_show) {
var children = M_historyChildren[parent];
if (children.length == 1) { // No children.
return;
}
var show = (typeof opt_show != "undefined") ? opt_show :
(document.getElementById("history-" + children[1]).style.display != "");
for (var i = 1; i < children.length; i++) {
var child = document.getElementById("history-" + children[i]);
child.style.display = show ? "" : "none";
}
var arrow = document.getElementById("triangle-" + parent);
if (arrow) {
arrow.className = "triangle-" + (show ? "open" : "closed");
}
}
/**
* Makes the given groups visible.
* @param {Array.<Number>} parents The indexes of the parents of the groups
* to show.
*/
function M_expandGroups(parents) {
for (var i = 0; i < parents.length; i++) {
M_toggleGroup(parents[i], true);
}
document.getElementById("history-expander").style.display = "none";
document.getElementById("history-collapser").style.display = "";
}
/**
* Hides the given parents, except for groups that contain the
* selected radio buttons.
* @param {Array.<Number>} parents The indexes of the parents of the groups
* to hide.
*/
function M_collapseGroups(parents) {
// Find the selected snapshots
var parentsToLeaveOpen = {};
var form = document.getElementById("history-form");
var formLength = form.a.length;
for (var i = 0; i < formLength; i++) {
if (form.a[i].checked || form.b[i].checked) {
var element = "history-" + form.a[i].value;
var name = document.getElementById(element).getAttribute("name");
if (name != "parent") {
// The name of a child is "parent-%d" % parent_index.
var parentIndex = Number(name.match(/parent-(\d+)/)[1]);
parentsToLeaveOpen[parentIndex] = true;
}
}
}
// Collapse the parents we need to collapse.
for (var i = 0; i < parents.length; i++) {
if (!(parents[i] in parentsToLeaveOpen)) {
M_toggleGroup(parents[i], false);
}
}
document.getElementById("history-expander").style.display = "";
document.getElementById("history-collapser").style.display = "none";
}
/**
* Expands the reverted files section of the files list in the changelist view.
*
* @param {String} tableid The id of the table element that contains hidden TR's
* @param {String} hide The id of the element to hide after this is completed.
*/
function M_showRevertedFiles(tableid, hide) {
var table = document.getElementById(tableid);
if (!table) return;
var rowsLength = table.rows.length;
for (var i = 0; i < rowsLength; i++) {
var row = table.rows[i];
if (row.getAttribute("name") == "afile") row.style.display = "";
}
if (dashboardState) dashboardState.initialize();
var h = document.getElementById(hide);
if (h) h.style.display = "none";
}
// Undo draft cancel
/**
* An associative array mapping keys that identify inline comments to draft
* text values.
* New inline comments have keys 'new-lineno-snapshot_id'
* Edit inline comments have keys 'edit-cid-lineno-side'
* Reply inline comments have keys 'reply-cid-lineno-side'
* @type Object
*/
var M_savedInlineDrafts = new Object();
/**
* Saves draft text from a form.
* @param {String} draftKey The key identifying the saved draft text
* @param {String} text The draft text to be saved
*/
function M_saveDraftText_(draftKey, text) {
M_savedInlineDrafts[draftKey] = text;
}
/**
* Clears saved draft text. Does nothing with an invalid key.
* @param {String} draftKey The key identifying the saved draft text
*/
function M_clearDraftText_(draftKey) {
delete M_savedInlineDrafts[draftKey];
}
/**
* Restores saved draft text to a form. Does nothing with an invalid key.
* @param {String} draftKey The key identifying the saved draft text
* @param {Element} form The form that contains the comment to be restored
* @param {Element} opt_selectAll Whether the restored text should be selected.
* True by default.
* @return {Boolean} true if we found a saved draft and false otherwise
*/
function M_restoreDraftText_(draftKey, form, opt_selectAll) {
if (M_savedInlineDrafts[draftKey]) {
form.text.value = M_savedInlineDrafts[draftKey];
if (typeof opt_selectAll == 'undefined' || opt_selectAll) {
form.text.select();
}
return true;
}
return false;
}
// Dashboard CL navigation
/**
* M_DashboardState class. Keeps track of the current position of
* the selector on the dashboard, and moves it on keypress.
* @param {Window} win The window that the dashboard table is in.
* @param {String} trName The name of TRs that we will move between.
* @param {String} cookieName The cookie name to store the marker position into.
* @constructor
*/
function M_DashboardState(win, trName, cookieName) {
/**
* The position of the marker, 0-indexed into the trCache array.
* @ype Integer
*/
this.trPos = 0;
/**
* The current TR object that the marker is pointing at.
* @type Element
*/
this.curTR = null;
/**
* Array of tr rows that we are moving between. Computed once (updateable).
* @type Array
*/
this.trCache = [];
/**
* The window that the table is in, used for positioning information.
* @type Window
*/
this.win = win;
/**
* The expected name of tr's that we are going to cache.
* @type String
*/
this.trName = trName;
/**
* The name of the cookie value where the marker position is stored.
* @type String
*/
this.cookieName = cookieName;
this.initialize();
}
/**
* Initializes the clCache array, and moves the marker into the first position.
*/
M_DashboardState.prototype.initialize = function() {
var filter = function(arr, lambda) {
var ret = [];
var arrLength = arr.length;
for (var i = 0; i < arrLength; i++) {
if (lambda(arr[i])) {
ret.push(arr[i]);
}
}
return ret;
};
var cache;
if (M_isIE()) {
// IE does not recognize the 'name' attribute on TR tags
cache = filter(document.getElementsByTagName("TR"),
function (elem) { return elem.name == this.trName; });
} else {
cache = document.getElementsByName(this.trName);
}
this.trCache = filter(cache, function (elem) {
return elem.style.display != "none";
});
if (document.cookie && this.cookieName) {
cookie_values = document.cookie.split(";");
for (var i=0; i<cookie_values.length; i++) {
name = cookie_values[i].split("=")[0].replace(/ /g, '');
if (name == this.cookieName) {
this.trPos = cookie_values[i].split("=")[1];
}
}
}
this.goto_(0);
}
/**
* Moves the cursor to the curCL position, and potentially scrolls the page to
* bring the cursor into view.
* @param {Integer} direction Positive for scrolling down, negative for
* scrolling up, and 0 for no scrolling.
*/
M_DashboardState.prototype.goto_ = function(direction) {
var oldTR = this.curTR;
if (oldTR) {
oldTR.cells[0].firstChild.style.visibility = "hidden";
}
this.curTR = this.trCache[this.trPos];
this.curTR.cells[0].firstChild.style.visibility = "";
if (this.cookieName) {
document.cookie = this.cookieName+'='+this.trPos;
}
if (!M_isElementVisible(this.win, this.curTR)) {
M_scrollIntoView(this.win, this.curTR, direction);
}
}
/**
* Moves the cursor up one.
*/
M_DashboardState.prototype.gotoPrev = function() {
if (this.trPos > 0) this.trPos--;
this.goto_(-1);
}
/**
* Moves the cursor down one.
*/
M_DashboardState.prototype.gotoNext = function() {
if (this.trPos < this.trCache.length - 1) this.trPos++;
this.goto_(1);
}
/**
* Event handler for dashboard key presses. Dispatches cursor moves, as well as
* opening CLs.
*/
function M_dashboardKeyPress(evt) {
return M_keyPressCommon(evt, function(key) {
if (key == 'k') {
if (dashboardState) dashboardState.gotoPrev();
} else if (key == 'j') {
if (dashboardState) dashboardState.gotoNext();
} else if (key == 'o' || key == '\r' || key == '\n') {
if (dashboardState) {
var child = dashboardState.curTR.cells[2].firstChild;
while (child && child.nodeName != "A") {
child = child.firstChild;
}
if (child) {
location.href = child.href;
}
}
} else {
return true;
}
return false;
});
}
/*
* Function to request more context between diff chunks.
* See _ShortenBuffer() in codereview/engine.py.
*/
function M_expandSkipped(id_before, id_after, where, id_skip) {
links = document.getElementById('skiplinks-'+id_skip).childNodes;
for (var i=0; i<links.length; i++) {
links[i].href = '#skiplinks-'+id_skip;
}
tr = document.getElementById('skip-'+id_skip);
var httpreq = M_getXMLHttpRequest();
if (!httpreq) {
html = '<td colspan="2" style="text-align: center;">';
html = html + 'Failed to retrieve additional lines. ';
html = html + 'Please update your context settings.';
html = html + '</td>';
tr.innerHTML = html;
}
aborted = false;
httpreq.onreadystatechange = function () {
if (httpreq.readyState == 4 && !aborted) {
if (httpreq.status == 200) {
response = eval('('+httpreq.responseText+')');
for (var i=0; i<response.length; i++) {
var data = response[i];
var row = document.createElement("tr");
for (var j=0; j<data[0].length; j++) {
row.setAttribute(data[0][j][0], data[0][j][1]);
}
if ( where == 't' ) {
tr.parentNode.insertBefore(row, tr);
} else {
tr.parentNode.insertBefore(row, tr.nextSibling);
}
row.innerHTML = data[1];
}
var curr = document.getElementById('skipcount-'+id_skip);
var new_count = parseInt(curr.innerHTML)-response.length/2;
if ( new_count > 0 ) {
if ( where == 'b' ) {
var new_before = id_before;
var new_after = id_after-response.length/2;
} else {
var new_before = id_before+response.length/2;
var new_after = id_after;
}
curr.innerHTML = new_count;
if ( new_count <= 10 ) {
html = '<a href="javascript:M_expandSkipped('+new_before;
html += ','+new_after+',\'b\','+id_skip+');">Show</a> ';
} else {
var html = '<a href="javascript:M_expandSkipped('+new_before;
html += ','+new_after+',\'t\', '+id_skip+');">Show 10 above</a> ';
html += '<a href="javascript:M_expandSkipped('+new_before;
html += ','+new_after+',\'b\','+id_skip+');">Show 10 below</a> ';
}
document.getElementById('skiplinks-'+(id_skip)).innerHTML = html;
} else {
tr.parentNode.removeChild(tr);
}
if (hookState.hookPos != -2 &&
M_isElementVisible(window, hookState.indicator)) {
hookState.gotoHook(-1);
}
}
}
}
url = skipped_lines_url+id_before+'/'+id_after+'/'+where;
httpreq.open('GET', url, true);
httpreq.send('');
}
/**
* Finds the element position.
*/
function M_getElementPosition(obj) {
var curleft = curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
}
return [curleft,curtop];
}
/**
* Position the user info popup according to the mouse event coordinates
*/
function M_positionUserInfoPopup(obj, userPopupDiv) {
pos = M_getElementPosition(obj);
userPopupDiv.style.left = pos[0] + "px";
userPopupDiv.style.top = pos[1] + 20 + "px";
}
/**
* Brings up user info popup using ajax
*/
function M_showUserInfoPopup(obj) {
var DIV_ID = "userPopupDiv";
var userPopupDiv = document.getElementById(DIV_ID);
var url = obj.getAttribute("href")
var index = url.indexOf("/user/");
var user_key = url.substring(index + 6);
if (!userPopupDiv) {
var userPopupDiv = document.createElement("div");
userPopupDiv.className = "popup";
userPopupDiv.id = DIV_ID;
userPopupDiv.filter = 'alpha(opacity=85)';
userPopupDiv.opacity = '0.85';
userPopupDiv.innerHTML = "";
userPopupDiv.onmouseout = function() {
userPopupDiv.style.visibility = 'hidden';
}
document.body.appendChild(userPopupDiv);
}
M_positionUserInfoPopup(obj, userPopupDiv);
var httpreq = M_getXMLHttpRequest();
if (!httpreq) {
return true;
}
var aborted = false;
var httpreq_timeout = setTimeout(function() {
aborted = true;
httpreq.abort();
}, 5000);
httpreq.onreadystatechange = function () {
if (httpreq.readyState == 4 && !aborted) {
clearTimeout(httpreq_timeout);
if (httpreq.status == 200) {
userPopupDiv = document.getElementById(DIV_ID);
userPopupDiv.innerHTML=httpreq.responseText;
userPopupDiv.style.visibility = "visible";
} else {
//Better fail silently here because it's not
//critical functionality
}
}
}
httpreq.open("GET", "/user_popup/" + user_key, true);
httpreq.send(null);
obj.onmouseout = function() {
aborted = true;
userPopupDiv.style.visibility = 'hidden';
obj.onmouseout = null;
}
}
/**
* TODO(jiayao,andi): docstring
*/
function M_showPopUp(obj, id) {
var popup = document.getElementById(id);
var pos = M_getElementPosition(obj);
popup.style.left = pos[0]+'px';
popup.style.top = pos[1]+20+'px';
popup.style.visibility = 'visible';
obj.onmouseout = function() {
popup.style.visibility = 'hidden';
obj.onmouseout = null;
}
}
/**
* TODO(andi): docstring
*/
function M_jumpToPatch(select, change, patchset, unified) {
if ( unified ) {
part = 'patch';
} else {
part = 'diff';
}
document.location.href = '/'+change+'/'+part+'/'+patchset+'/'+select.value;
}
/**
* Add or remove a star to/from the given change.
* @param {Integer} id The change id.
* @param {String} url The url fragment to append: "/star" or "/unstar".
*/
function M_setChangeStar_(id, url, xsrf) {
var httpreq = M_getXMLHttpRequest();
if (!httpreq) {
return true;
}
httpreq.onreadystatechange = function () {
if (httpreq.readyState == 4) {
if (httpreq.status == 200) {
var elem = document.getElementById("change-star-" + id);
elem.innerHTML = httpreq.responseText;
}
}
}
body = ''
body += 'change_id=' + escape('' + id);
body += '&'
body += 'xsrf=' + xsrf.replace(/\+/g, '%2B')
httpreq.open("POST", url, true);
httpreq.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded');
httpreq.send(body)
}