// Emacs settings: -*- mode: Fundamental; tab-width: 4; -*- //////////////////////////////////////////////////////////////////////////// // // // General-purpose Javascript functions and classes // // // // Copyright (c) 2004-2005, Andrew Birrell // // // //////////////////////////////////////////////////////////////////////////// // // String manipulation, cookies, and the search string // function utf8(str) { // Return a string whose charCodes are the UTF-8 bytes for the given // Unicode string "str". return unescape(encodeURIComponent(str)); } function htmlspecials(str) { // Return a string with critical HTML characters escaped var res = str.replace(/&/g, '&'); res = res.replace(//g, '>'); res = res.replace(/"/g, '"'); return res; } function getCookie(key) { // If there's a cookie named "key" return its value, else null var regExp = new RegExp("(^|.*; )" + key + "="); var value = document.cookie.replace(regExp, ""); if (value == document.cookie) return null; return decodeURIComponent(value.replace(/;.*$/, "")); } function setCookie(key, value, expires, path, domain, secure) { // Set the cookie named "key" to "value" document.cookie = key + "=" + encodeURIComponent(value) + (expires ? "; expires=" + expires.toGMTString() : "") + (path ? "; path=" + path : "") + (domain ? "; domain=" + domain : "") + (secure ? "; secure" : ""); } function deleteCookie(key, path, domain, secure) { // Delete the cookie, by setting an obsolete expiry date document.cookie = key + "=xxx; expires=Fri, 31 Dec 1999 23:59:59 GMT" + (path ? "; path=" + path : "") + (domain ? "; domain=" + domain : "") + (secure ? "; secure" : ""); } function getQueryArg(key) { // If our URL had a search string, and there's a key=value for given // key, return unescaped value; else return null if (!location.search) return null; var regExp = new RegExp("^(\\?|.*&)" + key + "="); var value = location.search.replace(regExp, ""); if (value == location.search) return null; return decodeURIComponent(value.replace(/&.*$/, "")); } // // Manipulating form elements // function getOption(id, andDelete) { // Return the value of the selected option in given selector // Optionally, also delete it var selector = document.getElementById(id); var selected = selector.selectedIndex; if (selected < 0) return null; var value = selector.options[selected].value; if (andDelete) selector.options[selected] = null; return value; } function setOption(id, value) { // Find the element of given SELECT object with given value, // and make it the selected index var selector = document.getElementById(id); var options = selector.options; for (var i = 0; i < options.length; i++) { if (options[i].value == value) selector.selectedIndex = i; } } function appendOption(selector, prompt, value) { selector.options[selector.options.length] = new Option(prompt, value); } function caseSort(a, b) { // case-insensitive sort-order function var la = a.toLowerCase(); var lb = b.toLowerCase(); return (la < lb ? -1 : (la > lb ? 1 : 0)); } function insertOption(id, fixed, prompt, value, select) { // Insert option in sorted position, ignoring initial fixed area var selector = document.getElementById(id); var options = selector.options; var pos = fixed; while (pos < options.length) { var old = options[pos].text; if (caseSort(old, prompt) > 0) break; pos++; } for (var i = options.length; i > pos; i--) { var old = options[i-1]; options[i] = new Option(old.text, old.value); } options[pos] = new Option(prompt, value); if (select) selector.selectedIndex = pos; } function deleteOption(id, value) { // Delete option with given value from given selector var selector = document.getElementById(id); var options = selector.options; for (var i = 0; i < options.length; i++) { if (options[i].value == value) { options[i] = null; break; } } } function truncateOptions(selector, count) { // Truncate a selector to have this many options var options = selector.options; while (options.length > count) options[options.length-1] = null; } function getCheckbox(id) { // Return whether chekcbox "id" is currently checked return document.getElementById(id).checked; } function setCheckbox(id, yes) { // Set the "checked" attribute of the given checkbox document.getElementById(id).checked = yes; } // // XMLHTTP access // function createXMLHttp() { // Create an XMLHttp object, if possible var xmlhttp = null; if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest(); } else if (window.ActiveXObject) { try { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) { } } return xmlhttp; } function initiateXMLHttp(thisReq) { // Internal subroutine for "get" and for retrying in "reqChange" var req = createXMLHttp(); if (req) { thisReq.xmlhttp = req; thisReq.attempt++; var postData = thisReq.postData; var async = (thisReq.synchronous ? false : true); req.onreadystatechange = function() { reqChange(thisReq); }; req.open((postData ? "POST" : "GET"), thisReq.url, async); if (postData) req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); req.send((postData ? postData : null)); } else { alert("Failed to create XMLHTTP object"); handleFailure(thisReq); } } function get(thisReq, url, postData, synchronous) { // Fetch a URL asynchronously, using the given request object (chosen // arbitrarily by the client, and passed to the completion procedures). // The function closure avoids using global variables, and allows // the use of multiple requests in parallel. // Calls either "handleFailure" or "handleResult" eventually. thisReq.url = url; thisReq.postData = postData; thisReq.synchronous = synchronous; thisReq.attempt = 0; initiateXMLHttp(thisReq); } function reqChange(thisReq) { // Called on XMLHTTP state changes // Reports completion to handleFailure or to handleResult var req = thisReq.xmlhttp; if (req.readyState == 4) { req.onreadystatechange = function() { }; // garbage collection assistance if (!req.status || req.status == 12029) { // Connection failures (Safari delivers null, IE gives 12029) // We retry once, after a short delay if (thisReq.attempt <= 1) { window.setTimeout(function() { initiateXMLHttp(thisReq); }, 100); } else { handleFailure(thisReq); } } else if (req.status != 200) { handleFailure(thisReq); } else { handleResult(thisReq); } } } // // position calculations // function windowSize() { // return an object with the window's available width and height // With thanks to www.quirksmode.org var size = Object(); if (self.innerWidth) { size.x = self.innerWidth; size.y = self.innerHeight; } else if (document.documentElement && document.documentElement.clientWidth) { size.x = document.documentElement.clientWidth; size.y = document.documentElement.clientHeight; } else if (document.body) { size.x = document.body.clientWidth; size.y = document.body.clientHeight; } return size; } function getElementPos(element) { // Return (x,y) for top-left of the given element, relative to document var res = Object(); res.x = 0; res.y = 0; for (var obj = element; obj.offsetParent; obj = obj.offsetParent) { res.x += obj.offsetLeft; res.y += obj.offsetTop; } return res; } function moveToCenter(element, size) { // Move given element to center of given size element.style.left = "" + Math.floor((size.x-element.offsetWidth)/2) + "px"; element.style.top = "" + Math.floor((size.y-element.offsetHeight)/2) + "px"; } // // Dragging machinery // var dragObject = null; // victim of the current drag, if any var dragHighlight; // element whose class is changed during drag var dragStartM = Object(); // initial mouse coordinates var dragStartO; // initial object coordinates, doc relative var dragSaveClass; // initial class of dragHighlight var dragConstraint; function dragStart(event, id, highlightId, highlightClass, constraint) { // Called on mousedown on the drag handle. // Sets things up to drag the element named by "id". // If "highlightId" is present (non-null), it's the name of an element // whose className should be changed to "highlightClass" during the // drag. dragObject = document.getElementById(id); if (dragObject) { dragStartO = getElementPos(dragObject); dragStartM.x = event.clientX; dragStartM.y = event.clientY; document.onmousemove = dragMove; document.onmouseup = dragStop; document.onmouseout = dragOut; if (highlightId) { dragHighlight = document.getElementById(highlightId); } else { dragHighlight = null; } if (dragHighlight) { dragSaveClass = dragHighlight.className; dragHighlight.className = highlightClass; } } dragConstraint = constraint; return false; } function dragMove(event) { // Called on mouse movement (in IE, only if it happens in our window) // Perform the drag if (dragObject) { if (!event) event = window.event; // IE versus the rest var lMin = (dragConstraint ? dragConstraint.lMin : 0); var lMax = (dragConstraint ? dragConstraint.lMax : 999999); var tMin = (dragConstraint ? dragConstraint.tMin : 0); var tMax = (dragConstraint ? dragConstraint.tMax : 999999); var newLeft = Math.max(lMin, Math.min(lMax, dragStartO.x + event.clientX - dragStartM.x)); var newTop = Math.max(tMin, Math.min(tMax, dragStartO.y + event.clientY - dragStartM.y)); dragObject.style.left = "" + newLeft + "px"; dragObject.style.right = "auto"; dragObject.style.top = "" + newTop + "px"; dragObject.style.bottom = "auto"; if (dragConstraint && dragConstraint.draggedTo) { dragConstraint.draggedTo(newLeft, newTop); } } return false; } function dragStop(event) { // Called on mouse up (in IE, only if it happens in our window) if (dragObject) { if (!event) event = window.event; // IE versus the rest if (dragHighlight) dragHighlight.className = dragSaveClass; dragObject = null; document.onmousemove = null; document.onmouseup = null; document.onmouseout = null; if (dragConstraint && dragConstraint.dragEnded) { dragConstraint.dragEnded(); } } return false; } function dragOut(event) { // Called on mouse leaving its target element // We want to terminate the drag if the mouse leaves the window // (it can transiently leave dragObject during fast dragging) if (dragObject) { if (!event) event = window.event; // IE versus the rest if (!event.relatedTarget && !event.toElement) { // It didn't go anywhere, so it must have left completely dragStop(event); } } return false; } // // Progress bar animation // // This is purely for entertainment: it doesn't actually measure progress. // It assumes that the bar widget should be sized within its parent. // var progressBarWidget = null; // the widget being scaled // maximum width is the widget's parent var progressBarTimer = null; // timer for reading progress bar var progressBarW = 0; // reading progress bar state function stepProgressBar() { // Animate the readProgress widget var maxWidth = progressBarWidget.parentNode.offsetWidth; progressBarW += (progressBarW <= 20 ? 4 : (progressBarW <= 40 ? 3 : (progressBarW <= 60 ? 2 : 1))); if (progressBarW > maxWidth-4) progressBarW = 0; progressBarWidget.style.width = "" + progressBarW + "px"; } function startProgressBar(widget) { // Start a progress bar using the element named "widget" progressBarWidget = document.getElementById(widget); progressBarWidget.style.width = "4px"; // amount initially visible progressBarW = 4; progressBarTimer = setInterval(stepProgressBar, 500); } function stopProgressBar() { // Stop animating the progress bar if (progressBarTimer) clearInterval(progressBarTimer); progressBarTimer = null; } // // Key-value cache with LRU replacement // function Cache(lruSize) { // Object constructor for a cache keeping at least // lruSize (and at most lruSize*2) most-recently-used entries. // Instantiate as an object, e.g. var myCache = new Cache(50); // then call methods, e.g. myCache.write("key", "value"); this.lruSize = lruSize; this.recent = new Object(); this.old = new Object(); this.count = 0; } Cache.prototype.write = function(key, value) { // Include given (key,value) in the cache if (!this.recent[key]) this.count++; this.recent[key] = value; if (this.count >= this.lruSize) { this.old = this.recent; this.recent = new Object(); this.count = 0; } } Cache.prototype.read = function(key) { // If key is in the cache, return value, else null var value; if (value = this.recent[key]) return value; if (value = this.old[key]) { this.write(key, value); return value; } return null; } Cache.prototype.flush = function(key) { // ensure key is no longer in the cache if (this.recent[key]) { this.recent[key] = null; this.count--; } if (this.old[key]) this.old[key] = null; }