/* * $Id$ * Copyright 2004-2006 - Michael Sinz * * Some JavaScript support routines for the svn log pages */ // Store references to the details objects here var revs = new Array(); /* * Get the detail element by the revision */ function getDetail(rev) { return (document.getElementById(':' + rev)); } /* * Get the specific file detail element by path and revision */ function getPathDetail(path,rev) { return (document.getElementById(path + ':' + rev)); } /* * This is used to toggle a given hidden element */ function toggle(rev) { var div=getDetail(rev); if (div) { if (!div.editButton) { if (div.style.display == "none") { div.style.display = ""; } else { div.style.display = "none"; } } } } /* * Clear any timeouts that this div may have * active... */ function clearTimeouts(div) { if (div.slider) { clearTimeout(div.slider); div.slider = null; } if (div.timer) { clearTimeout(div.timer); div.timer = null; } } // Keep track of any open popup object here var openedPopup; /* * Hide the currently open popup but only if * there is a timer set on it or I was told * to do it by force. */ function hidePopup(force) { if ((openedPopup) && (force || (openedPopup.timer))) { var div = openedPopup; openedPopup = null; clearTimeouts(div); div.showing = 0; div.style.display = 'none'; div.parentNode.style.background = 'transparent'; } } /* * This will show a given popup by first making sure * that the old popup is gone and then by setting up * the new popup. A timer is set such that if the * popup is not entered by the mouse within 5 seconds, * it will be removed. */ function showPopup(div) { if (div != openedPopup) { // Force the old popup to go away, if needed. hidePopup(true); // Note that this colour should match the stylesheet background for the div... div.parentNode.style.background = 'rgb(100%,100%,75%)'; // Set this one to be showing... div.showing = 1; div.sliding = 0; openedPopup = div; // Clear any timeouts... clearTimeouts(div); // take down the popup if the user has not entered it in 5 seconds. div.timer = setTimeout('hidePopup()',5000); // Start the sliding open process... div.slider = setTimeout('slideOpen()',10); // Try to initialize the clipping area... div.style.clip = 'rect(auto,auto,auto,auto)'; } } /* * Some silly visual candy for sliding/unfolding the menu * onto screen rather than just pop up instantly. */ function slideOpen() { var div = openedPopup; if (div.style.clip) { div.sliding++; if (div.sliding >= Insurrection.SliderSteps) { // No more timeouts we are done... div.slider = null; // No more clipping needed... div.style.clip = 'rect(auto,auto,auto,auto)'; } else { var clipx = Math.floor((div.offsetWidth * div.sliding) / Insurrection.SliderSteps); var clipy = Math.floor((div.offsetHeight * div.sliding) / Insurrection.SliderSteps); div.style.clip = 'rect(auto,' + clipx + 'px,' + clipy + 'px,auto)'; // Ok, we want to do this again since we are not done yet... div.slider = setTimeout('slideOpen()',10); } } div.style.display = 'block'; } /* * This is called in the onmouseout for the popup and will cause * a 500ms timer to run. If the mouse does not reenter the popup * within that timeout, the popup will be torn down. */ function offPopup(div) { // If the mouse leaves the popup, take it down in 1/2 second. div.timer = setTimeout('hidePopup()',500); } /* * This is called in the onmouseover for the popup and will cause * any still active timers to be cleared. This is how a popup stays * in place if the user has the mouse over it. */ function onPopup(div) { // If there is a takedown timer set, clear it if the // mouse enters the area. if (div.timer) { clearTimeout(div.timer); div.timer = null; } } // Get the initial state of the diff selection from the // cookies such that a selection can live across pages. var diffPath = GetCookie('diffPath'); var diffRev = GetCookie('diffRev'); if ((diffPath == null) || (diffPath == '') || (diffRev == null) || (diffRev == 0)) { diffPath = ''; diffRev = 0; } /* * Set the browser status bar with the diff selection info * if there is a selected file for diff. */ function setSelectMessage() { if ((diffPath != '') && (diffRev != 0)) { // Set a message about the fact that a revision was selected... var msg = diffPath + ' revision ' + diffRev + ' selected for diff'; window.defaultStatus = msg; window.status = msg; } } // Set status at load time (in case there is a cookie already) setSelectMessage(); /* * Select a given path and revision for future diff operation */ function selectForDiff(path,rev) { diffPath = path; diffRev = rev; // Limit the revision cookie to this repository cookiePath = Insurrection.SVN_URL + path.substring(0,path.indexOf('/')); SetTempCookie('diffPath',path,cookiePath); SetTempCookie('diffRev',rev,cookiePath); setSelectMessage(); } /* * Add a popup menu item line to the given element. * This is used by the addLink() function and directly * to add text without a link. */ function addPopupLine(el,text) { var d1 = document.createElement('div'); d1.className = 'pathpopupmenuitem'; el.appendChild(d1); d1.appendChild(document.createTextNode(text)); } /* * A simple function to build a link with a given URL * and link text for the popup menu. */ function addLink(div,link,text) { var a = document.createElement('a'); a.href = link; div.appendChild(a); var d1 = document.createElement('div'); d1.className = 'pathpopupmenuitem'; d1.title = text; a.appendChild(d1); d1.appendChild(document.createTextNode(text)); } /* * Create a menu segment element and, optionally, * add it to the provided parent element. */ function newSegment(parent) { var div = document.createElement('div'); div.className = 'menusegment'; if (parent) { parent.appendChild(div); } return(div); } /* * When we click on a detailed log entry path line, * we get a popup menu that lists certain actions, * depending on the commit event for the line. * We build the correct URLs that will then execute * that operation. */ function detailClick(repo,action,path,rev,current) { var div = getPathDetail(path,rev); if (div) { if ((div.diffPath != diffPath) || (div.diffRev != diffRev)) { div.diffPath = diffPath; div.diffRev = diffRev; div.innerHTML = ''; var d1 = document.createElement('div'); d1.className = 'pathpopupshadow'; div.appendChild(d1); var d2 = document.createElement('div'); d2.className = 'pathpopupmenu'; d1.appendChild(d2); var fullpath = repo + path; if (action != 'D') { addLink(d2,Insurrection.SVN_URL + fullpath + '?Insurrection=blame&r=' + rev,'Annotate'); addLink(d2,Insurrection.SVN_URL + fullpath + '?Insurrection=get&r=' + rev,'Download'); if ((diffPath == fullpath) && (diffRev == rev)) { d1 = document.createElement('div'); d1.className = 'pathpopupmenutext'; d1.appendChild(document.createTextNode('-- Selected/Marked revision --')); d1.title = 'Selected/Marked revision'; d2.appendChild(d1); } else { var a = document.createElement('a'); d2.appendChild(a); a.href='javascript:;'; a['onclick'] = function() { eval('selectForDiff("' + fullpath + '","' + rev + '");'); } d1 = document.createElement('div'); d1.className = 'pathpopupmenuitem'; a.appendChild(d1); d1.appendChild(document.createTextNode('Select/Mark this revision')); d1.title = 'Select/Mark this revision'; } var dDiff = newSegment(); var dPatch = newSegment(); if (action == 'M') { addLink(dDiff,Insurrection.SVN_URL + fullpath + '?Insurrection=diff&r=' + rev + '&r2=' + rev + '&r1=' + (rev-1),'Diff from previous'); addLink(dPatch,Insurrection.SVN_URL + fullpath + '?Insurrection=diff&getpatch=1&r=' + rev + '&r2=' + rev + '&r1=' + (rev-1),'Patch from previous'); } if (diffPath == fullpath) { if (diffRev < rev) { addLink(dDiff,Insurrection.SVN_URL + fullpath + '?Insurrection=diff&r=' + rev + '&r1=' + diffRev + '&r2=' + rev,'Diff from selected revision: ' + diffRev); addLink(dPatch,Insurrection.SVN_URL + fullpath + '?Insurrection=diff&getpatch=1&r=' + rev + '&r1=' + diffRev + '&r2=' + rev,'Patch from selected revision: ' + diffRev); } if (diffRev > rev) { addLink(dDiff,Insurrection.SVN_URL + fullpath + '?Insurrection=diff&r=' + rev + '&r2=' + diffRev + '&r1=' + rev,'Diff to selected revision: ' + diffRev); addLink(dPatch,Insurrection.SVN_URL + fullpath + '?Insurrection=diff&getpatch=1&r=' + rev + '&r2=' + diffRev + '&r1=' + rev,'Patch to selected revision: ' + diffRev); } } if ((rev != current) && ((diffPath != fullpath) || (diffRev != current))) { addLink(dDiff,Insurrection.SVN_URL + fullpath + '?Insurrection=diff&r=' + rev + '&r2=' + current + '&r1=' + rev,'Diff to revision ' + current); addLink(dPatch,Insurrection.SVN_URL + fullpath + '?Insurrection=diff&getpatch=1&r=' + rev + '&r2=' + current + '&r1=' + rev,'Patch to revision ' + current); } if (dDiff.childNodes.length > 0) { d2.appendChild(dDiff); } if (dPatch.childNodes.length > 0) { d2.appendChild(dPatch); } if (action == 'M') { addLink(newSegment(d2),Insurrection.SVN_URL + fullpath + '?Insurrection=log&r1=' + rev,'Revision history from ' + rev); } } else { rev--; addLink(d2,Insurrection.SVN_URL + fullpath + '?Insurrection=blame&r=' + rev,'Annotate previous'); addLink(d2,Insurrection.SVN_URL + fullpath + '?Insurrection=get&r=' + rev,'Download previous'); addLink(d2,Insurrection.SVN_URL + fullpath + '?Insurrection=log&r1=' + rev,'Revision history from ' + rev); rev++; } if (rev > 1) { d1 = newSegment(d2); addLink(d1,Insurrection.SVN_URL + repo + '/?Insurrection=diff&r=' + rev + '&r2=' + rev + '&r1=' + (rev-1),'Changeset for this revision'); addLink(d1,Insurrection.SVN_URL + repo + '/?Insurrection=diff&getpatch=1&r=' + rev + '&r2=' + rev + '&r1=' + (rev-1),'Patchset for this revision'); } } showPopup(div); } return true; } /* * This is some JavaScript to provide the feature of showing and hiding details * Note that we do some fun stuff to show the details slowly as it really takes * a long time in some cases where there are many revisions in the history. * This is why we do much of this work async... */ // This will store the reference to the show/hide // details button. It should be set by the loading // process. var details; /* * This method adds detail log sections to the array of details. * It also updates the details button such that it shows that * you can show/hide the details. Note that if there is no * details section, it does not add it, which could mean that * the SVN LOG was not done with verbose output. This keeps all * of this compatible with both forms. */ function addDetail(id) { var div=getDetail(id); if (div) { if (!details) { details = document.getElementById('details'); } // Since we had some details, we should show // the fact that you can display them... if (details) { revs.push(div); // For the first details entry, show it by default if (revs.length == 1) { div.style.display = ""; } // If there is more than one details entry, show the // "click to show" text. if (revs.length > 1) { details.innerHTML = 'click to show details'; details.showhide = 'show'; } } } } /* * This function shows detail sections by calling itself * from a timer every 50ms until it has shown all sections. * This starts at a low index and moves up. */ function showDetails(i) { if (i < revs.length) { revs[i].style.display = ""; i++; setTimeout('showDetails(' + i + ')',50); details.innerHTML = '...showing ' + i; } else { details.innerHTML = 'click to hide details'; details.showhide = 'hide'; } } /* * This function hides detail sections by calling itself * from a timer every 50ms until it has hidden all sections. * This starts at a high index and moves down. */ function hideDetails(i) { if (i > 0) { i--; revs[i].style.display = "none"; setTimeout('hideDetails(' + i + ')',50); details.innerHTML = '...hiding ' + i; } else { details.innerHTML = 'click to show details'; details.showhide = 'show'; } } /* * When the show all/hide all is clicked, we start * the process rolling. */ function toggleAll() { // Only if we have the details feature and // we have more than 1 revision do we support // the toggle feature. if ((details) && (revs.length > 1)) { if (details.showhide != 'working') { if (details.showhide == 'hide') { hideDetails(revs.length); } else { showDetails(0); } details.showhide = 'working'; } } } /* * Get the value of a named cookie. We use * these cookies to get/set configuration like * elements without the need for server interaction. */ function GetCookie(name) { var arg = name + "="; var alen = arg.length; var clen = document.cookie.length; var i = 0; while (i < clen) { var j = i + alen; if (document.cookie.substring(i, j) == arg) { var endstr = document.cookie.indexOf (";", j); if (endstr == -1) { endstr = document.cookie.length; } return unescape(document.cookie.substring(j, endstr)); } i = document.cookie.indexOf(" ", i) + 1; if (i == 0) { return null; } } return null; } /* * Set a session-temporary cookie... * Setting a cookie is just too easy... Too bad getting the * cookie is not as easy. */ function SetTempCookie(name, value, path) { document.cookie = name + '=' + escape(value) + '; path=' + path; } /* * Setting a cookie is just too easy... Too bad getting the * cookie is not as easy. */ function SetCookie(name, value) { // Ugly trick - we build a cookie expires string // that is set for 1 year from "now" such that the // cookies remain valid for more than a single session. var Expires = new Date(); Expires.setYear(Expires.getYear() + 1901); var CookieExpires = '; expires=' + Expires.toGMTString(); document.cookie = name + '=' + escape(value) + CookieExpires + '; path=/'; } /* * Start the editing process for a given log message. * This displays the textarea that will be used for the * edit work and hides the edit button since we are now * in active editing mode. We also read the log message * from the
that it is within in order that we * start with the same message as was being displayed. * (less overhead to the server) */ function editLog(edit,rev) { // This ugly little thing here is to force the other click // that is going to be registered to show the list of paths // for this revision. This way the log editing will be // done with the revisions showing... var details = getDetail(rev); details.style.display = ''; // remember the edit button and hide it... details.editButton = edit; edit.style.display = 'none'; // Get our log message since we are going to edit it... // This lets the browser deal with correct scaping of the nodes var log = document.getElementById('Log:' + rev); var t = log.firstChild; var msg = ''; while (t) { if (t.nodeName == '#text') { msg += t.nodeValue; } else { msg += "\n"; } t = t.nextSibling; } // Set up the edit textarea with the current log message var ed = document.getElementById('LogEd:' + rev); ed.value = msg; // remember to save the original in order to support cancel ed.original = msg + ''; document.getElementById('LogEdit:' + rev).style.display='block'; } /* * Take the msg string and put it into the log message * display for the given revision. This converts any "\n" * in the string into "
" HTML/DOM elements. */ function showLog(rev,msg) { var log = document.getElementById('Log:' + rev); log.innerHTML = ''; // Cear out the old log... // start putting the message into the log element var i; while((i = msg.indexOf("\n")) >= 0) { if (i > 0) { log.appendChild(document.createTextNode(msg.substr(0,i))); } msg = msg.substr(i+1); log.appendChild(document.createElement('br')); } if (msg.length > 0) { log.appendChild(document.createTextNode(msg)); } } /* * Show what we have been editing in the normal log view area * as a preview of what the real thing will look like. */ function previewLog(rev) { // Get the message and start putting it into the log showLog(rev,document.getElementById('LogEd:' + rev).value); } /* * End the editing - hides the edit textarea and displays * the edit button again. */ function endEdit(rev) { var details = getDetail(rev); details.style.display = ''; // remember the edit button and hide it... details.editButton.style.display = ''; details.editButton = null; // Hide the edit area document.getElementById('LogEdit:' + rev).style.display = 'none'; } /* * Cancel the log edit by restoring the original log message * and ending the edit state for this revision. */ function cancelLog(rev) { // Revert to the value before the edit... showLog(rev,document.getElementById('LogEd:' + rev).original); // And end the edit... endEdit(rev); } /* * Start the "save" operation for the edited log message. * This checks to see if any changes have been made and * if there are no changes it acts just like a cancel. * (No server traffic if there is no actual content change) */ function saveLog(rev) { // Hide the edit area as we are now "down" editing document.getElementById('LogEdit:' + rev).style.display = 'none'; // Now, lets get the XMLHTTP object var details = getDetail(rev); details.xml = getXMLHTTP(); var ed = document.getElementById('LogEd:' + rev); if (ed.value == ed.original) { // No change so just cancel the thing... cancelLog(rev); } else { if (details.xml) { details.xml.onreadystatechange = function() { saveLogCheck(rev); }; details.xml.open("POST",'?Insurrection=savelog',true); details.xml.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); details.xml.send('rev=' + rev + '&newLog=' + escape(ed.value)); // Show that we are saving... showLog(rev,'...saving...'); } else { // Revert to the value from before we edited... (no XMLHTTP support) showLog(rev,ed.original); // No need to restore the edit button if there is no XMLHTTP support details.editButton = null; } } } /* * This is the callback for the XMLHTTP request * It handles the final disposition of the save operation */ function saveLogCheck(rev) { var details = getDetail(rev); if ((details.xml) && (details.xml.readyState == 4)) { if (details.xml.status == 200) { previewLog(rev); endEdit(rev); } else { cancelLog(rev); alert('Log save failed: ' + details.xml.statusText); } details.xml = null; } }