Source of “common.js”.
577 lines, 15.7 KBytes.   Last modified 9:18 pm, 5th December 2011 PST.
1 // Emacs settings: -*- mode: Fundamental; tab-width: 4; -*- 2 3 //////////////////////////////////////////////////////////////////////////// 4 // // 5 // General-purpose Javascript functions and classes // 6 // // 7 // Copyright (c) 2004-2009, Andrew Birrell // 8 // // 9 //////////////////////////////////////////////////////////////////////////// 10 11 12 // 13 // String manipulation 14 // 15 16 function utf8(str) { 17 // Return a string whose charCodes are the UTF-8 bytes for the given 18 // Unicode string "str". 19 // 20 return unescape(encodeURIComponent(str)); 21 } 22 23 function caseSort(a, b) { 24 // case-insensitive sort-order function 25 // 26 var la = a.toLowerCase(); 27 var lb = b.toLowerCase(); 28 return (la < lb ? -1 : (la > lb ? 1 : 0)); 29 } 30 31 32 // 33 // Key-value cache with LRU replacement 34 // 35 36 function Cache(lruSize) { 37 // Object constructor for a cache keeping at least 38 // lruSize (and at most lruSize*2) most-recently-used entries. 39 // Instantiate as an object, e.g. var myCache = new Cache(50); 40 // then call methods, e.g. myCache.write("key", "value"); 41 // 42 this.lruSize = lruSize; 43 this.recent = new Object(); 44 this.old = new Object(); 45 this.count = 0; 46 } 47 48 Cache.prototype.write = function(key, value) { 49 // Include given (key,value) in the cache 50 // 51 if (!this.recent[key]) this.count++; 52 this.recent[key] = value; 53 if (this.count >= this.lruSize) { 54 this.old = this.recent; 55 this.recent = new Object(); 56 this.count = 0; 57 } 58 } 59 60 Cache.prototype.read = function(key) { 61 // If key is in the cache, return value, else null 62 // 63 var value; 64 if (value = this.recent[key]) return value; 65 if (value = this.old[key]) { 66 this.write(key, value); 67 return value; 68 } 69 return null; 70 } 71 72 Cache.prototype.flush = function(key) { 73 // ensure key is no longer in the cache 74 // 75 if (this.recent[key]) { 76 this.recent[key] = null; 77 this.count--; 78 } 79 if (this.old[key]) this.old[key] = null; 80 } 81 82 83 // 84 // HTTP support: encoding, cookies, query string 85 // 86 87 function htmlspecials(str) { 88 // Return a string with critical HTML characters escaped 89 // 90 var res = str.replace(/&/g, '&amp;'); 91 res = res.replace(/[<]/g, '&lt;'); 92 res = res.replace(/>/g, '&gt;'); 93 res = res.replace(/"/g, '&quot;'); 94 return res; 95 } 96 97 function getCookie(key) { 98 // If there's a cookie named "key" return its value, else false 99 // 100 var regExp = new RegExp("(^|.*; )" + key + "="); 101 var value = document.cookie.replace(regExp, ""); 102 if (value == document.cookie) return null; 103 return decodeURIComponent(value.replace(/;.*$/, "")); 104 } 105 106 function setCookie(key, value, expires, path, domain, secure) { 107 // Set the cookie named "key" to "value" 108 // 109 document.cookie = key + "=" + encodeURIComponent(value) + 110 (expires ? "; expires=" + expires.toGMTString() : "") + 111 (path ? "; path=" + path : "") + 112 (domain ? "; domain=" + domain : "") + 113 (secure ? "; secure" : ""); 114 } 115 116 function deleteCookie(key, path, domain, secure) { 117 // Delete the cookie, by setting an obsolete expiry date 118 // 119 document.cookie = key + "=xxx" + 120 "; expires=Fri, 31 Dec 1999 23:59:59 GMT" + 121 (path ? "; path=" + path : "") + 122 (domain ? "; domain=" + domain : "") + 123 (secure ? "; secure" : ""); 124 } 125 126 function getQueryArg(key) { 127 // If our URL had a search string, and there's a key=value for given 128 // key, return unescaped value; else return null 129 // 130 if (!location.search) return null; 131 var regExp = new RegExp("^(\\?|.*&)" + key + "="); 132 var value = location.search.replace(regExp, ""); 133 if (value == location.search) return null; 134 return decodeURIComponent(value.replace(/&.*$/, "")); 135 } 136 137 function isIphone() { 138 // Return true iff userAgent matches iPhone or iPod 139 // 140 var agent = navigator.userAgent; 141 return agent && agent.match(/iPod;|iPhone;/i); 142 } 143 144 145 // 146 // Manipulating DOM elements 147 // 148 149 function showHide(id, show) { 150 // Show or hide the DOM element with given ID (assumed to be a block) 151 // 152 var elt = document.getElementById(id); 153 if (elt) elt.style.display = (show ? "block" : "none"); 154 } 155 156 function showHideInline(id, show) { 157 // Show or hide the DOM element with given ID (assumed to be inline) 158 // 159 var elt = document.getElementById(id); 160 if (elt) elt.style.display = (show ? "inline" : "none"); 161 } 162 163 function xableControl(id, yes) { 164 var elt = document.getElementById(id); 165 elt.disabled = !yes; 166 } 167 168 function getOptSelection(id) { 169 // Return the current selection in a SELECT, or < 0 if none 170 // 171 var selector = document.getElementById(id); 172 return selector.selectedIndex; 173 } 174 175 function setOptSelection(id, n) { 176 // Set the current selection of a SELECT, or deselect if < 0 177 // 178 var selector = document.getElementById(id); 179 selector.selectedIndex = n; 180 } 181 182 function getOptionPrompt(id) { 183 // Return the text of the selected option in given selector 184 // 185 var selector = document.getElementById(id); 186 var selected = selector.selectedIndex; 187 if (selected < 0) return null; 188 return selector.options[selected].text; 189 } 190 191 function getOption(id, andDelete) { 192 // Return the value of the selected option in given selector, or null 193 // if there's no selection. Optionally, also delete the selected item. 194 // 195 var selector = document.getElementById(id); 196 var selected = selector.selectedIndex; 197 if (selected < 0) return null; 198 var value = selector.options[selected].value; 199 if (andDelete) selector.options[selected] = null; 200 return value; 201 } 202 203 function setOption(id, value) { 204 // Find the element of given SELECT object with given value, 205 // make it the selected index, and return true. If no such value 206 // (including all cases where the argument is null), return false. 207 // 208 if (value === null) return false; 209 var selector = document.getElementById(id); 210 var options = selector.options; 211 for (var i = 0; i < options.length; i++) { 212 if (options[i].value == value) { 213 selector.selectedIndex = i; 214 return true; 215 } 216 } 217 return false; 218 } 219 220 function appendOption(selector, prompt, value) { 221 selector.options[selector.options.length] = 222 new Option(prompt, value); 223 } 224 225 function insertOption(id, fixed, prompt, value, select) { 226 // Insert option in sorted position, ignoring initial fixed area 227 // 228 var selector = document.getElementById(id); 229 var options = selector.options; 230 var pos = fixed; 231 while (pos < options.length) { 232 var old = options[pos].text; 233 if (caseSort(old, prompt) > 0) break; 234 pos++; 235 } 236 for (var i = options.length; i > pos; i--) { 237 var old = options[i-1]; 238 options[i] = new Option(old.text, old.value); 239 } 240 options[pos] = new Option(prompt, value); 241 if (select) selector.selectedIndex = pos; 242 } 243 244 function deleteOption(id, value) { 245 // Delete option with given value from given selector 246 // 247 var selector = document.getElementById(id); 248 var options = selector.options; 249 for (var i = 0; i < options.length; i++) { 250 if (options[i].value == value) { 251 options[i] = null; 252 break; 253 } 254 } 255 } 256 257 function truncateOptions(id, count) { 258 // Truncate a selector to have this many options 259 260 var selector = document.getElementById(id); 261 var options = selector.options; 262 while (options.length > count) options[options.length-1] = null; 263 } 264 265 function getCheckbox(id) { 266 // Return whether checkbox "id" is currently checked 267 // 268 return document.getElementById(id).checked; 269 } 270 271 function setCheckbox(id, yes) { 272 // Set the "checked" attribute of the given checkbox 273 // 274 document.getElementById(id).checked = yes; 275 } 276 277 function stopBubbling(event) { 278 // general-purpose all-browser bubble prevention 279 // 280 event.cancelBubble = true; 281 if (event.stopPropagation) event.stopPropagation(); 282 } 283 284 function addMouseWheel(id, handler) { 285 // Add a mouse wheel event handler to the named element. 286 // The handler gets called with the wheel delta, normalized 287 // to one notch = +1 or -1. 288 // 289 var elt = document.getElementById(id); 290 var myHandler = function(event) { 291 if (!event) event = window.event; // IE versus the rest 292 var delta = (event.wheelDelta ? (event.wheelDelta / 120) : 293 (-event.detail / 3)); 294 if (window.opera) delta = -delta; 295 if (!handler(delta)) { 296 if (event.preventDefault) event.preventDefault(); 297 return false; 298 } else { 299 return true; 300 } 301 }; 302 if (elt.addEventListener) { 303 elt.addEventListener("DOMMouseScroll", myHandler, false); 304 elt.addEventListener("mousewheel", myHandler, false); 305 } else if (elt.attachEvent) { 306 elt.attachEvent("onmousewheel", myHandler); 307 } 308 } 309 310 311 // 312 // position calculations 313 // 314 315 function windowSize() { 316 // return an object with the window's available width and height 317 // With thanks to www.quirksmode.org 318 // 319 var size = Object(); 320 if (self.innerWidth) { 321 size.x = self.innerWidth; 322 size.y = self.innerHeight; 323 } else if (document.documentElement && 324 document.documentElement.clientWidth) { 325 size.x = document.documentElement.clientWidth; 326 size.y = document.documentElement.clientHeight; 327 } else if (document.body) { 328 size.x = document.body.clientWidth; 329 size.y = document.body.clientHeight; 330 } 331 return size; 332 } 333 334 function getElementPos(element) { 335 // Return (x,y) for top-left of the given element, relative to document 336 // 337 var res = Object(); 338 res.x = 0; 339 res.y = 0; 340 for (var obj = element; obj.offsetParent; obj = obj.offsetParent) { 341 res.x += obj.offsetLeft; 342 res.y += obj.offsetTop; 343 } 344 return res; 345 } 346 347 function getMousePos(event) { 348 // Return (x,y) for mouse coordinates, relative to the document 349 // Thanks to quirksmode.org for browser-specific details. 350 // 351 var mouse = Object(); 352 if (event.pageX) { 353 mouse.x = event.pageX; 354 mouse.y = event.pageY; 355 } else { 356 mouse.x = event.clientX + document.body.scrollLeft + 357 document.documentElement.scrollLeft; 358 mouse.y = event.clientY + document.body.scrollTop + 359 document.documentElement.scrollTop; 360 } 361 return mouse; 362 } 363 364 function moveToCenter(element, size) { 365 // Move given element to center of given size 366 // 367 element.style.left = "" + Math.floor((size.x-element.offsetWidth)/2) + 368 "px"; 369 element.style.top = "" + Math.floor((size.y-element.offsetHeight)/2) + 370 "px"; 371 } 372 373 function getActualStyle(elt) { 374 // Get the actual style data in use for the element, cross-browser 375 // 376 if (elt.currentStyle) { 377 return elt.currentStyle; 378 } else { 379 return window.getComputedStyle(elt); 380 } 381 } 382 383 384 // 385 // Sliding screen transition 386 // 387 388 // Construct one "Screen" object for each possible screen. "id" will be 389 // the index of the object in "screens"; the actual element involved is 390 // id + "Screen". Each screen's element should have "position: absolute", 391 // and all but one should have "display: none". The other element should 392 // have "display: block", and the client should assign that element's 393 // "Screen" object to screens.cur. 394 // 395 // Most likely, each element will have width equal to the window width 396 // (perhaps "width: 100%"), and most likely all the elements will have 397 // the same DOM parent, and absolute position (0,0), and the same height. 398 // 399 // "seq" controls left-right choices for "show", and the effect of "goBack" 400 // 401 // The machinery requires a browser that supports webkit transitions, unless 402 // "slidingScreens" is set to "false". 403 // 404 // Note the declaration of globals "slidingScreens" and "screens". 405 406 var slidingScreens = true; // enable the animation 407 var screens = new Object(); // available screens; .cur is being displayed 408 409 function Screen(id, seq) { 410 // Constructor for Screen object. 411 // 412 // "seq" controls prev/goBack, and the left-right behavior for 413 // sliding screen transitions; prev/goBack use floor(seq); the 414 // animations use the full value. 415 // 416 // Assigns new object to field of "screens". 417 this.id = id; 418 this.elt = document.getElementById(id + "Screen"); 419 this.seq = seq; 420 this.floorSeq = Math.floor(seq); 421 screens[id] = this; 422 if (slidingScreens) { 423 var thisStyle = this.elt.style; 424 thisStyle.webkitTransitionProperty = "webkitTransform"; 425 var myself = this; 426 this.elt.addEventListener('webkitTransitionEnd', 427 function(event) { 428 if (myself != screens.cur) thisStyle.display = "none"; 429 }, false); 430 } 431 } 432 433 Screen.prototype.show = function() { 434 // Replace curScreen with this 435 var cur = screens.cur; 436 if (cur != this) { 437 var thisStyle = this.elt.style; 438 var curStyle = cur.elt.style; 439 if (slidingScreens && this.seq != cur.seq) { 440 var delta = (this.seq < cur.seq ? 1 : -1) * windowSize().x; 441 thisStyle.visibility = "hidden"; 442 thisStyle.display = "block"; 443 thisStyle.webkitTransitionDuration = "0"; 444 thisStyle.webkitTransform = "translateX(" + (-delta) + "px)"; 445 thisStyle.visibility = "visible"; 446 setTimeout(function() { 447 var duration = "500ms"; 448 curStyle.webkitTransitionDuration = duration; 449 curStyle.webkitTransform = 450 "translateX(" + delta + "px)"; 451 thisStyle.webkitTransitionDuration = duration; 452 thisStyle.webkitTransform = ""; 453 }, 0); 454 } else { 455 thisStyle.display = "block"; 456 if (slidingScreens) { 457 thisStyle.webkitTransitionDuration = "0"; 458 thisStyle.webkitTransform = ""; 459 } 460 curStyle.display = "none"; 461 } 462 if (cur.floorSeq < this.floorSeq) { 463 this.prev = cur; 464 } else if (cur.floorSeq == this.floorSeq) { 465 this.prev = cur.prev; 466 } 467 screens.cur = this; 468 } 469 } 470 471 Screen.prototype.goBack = function() { 472 // Go to previous lower-sequenced screen 473 if (this.prev) this.prev.show(); 474 return false; 475 } 476 477 478 // 479 // XMLHTTP access 480 // 481 482 // Use: 483 // - create an object with the following attributes: 484 // - "url" 485 // - optional "postData", which implies method "POST" instead of "GET" 486 // - optional "synchronous" boolean, defaulting to false 487 // - method "handleFailure" 488 // - method "handleResult" 489 // - pass the object to "initiateXMLHttp" 490 // - when request completes or fails, handleResult or handleFailure gets 491 // called. Exactly one of them will be called eventually. 492 // - initiateXMLHttp2 and reqChange are private 493 494 function initiateXMLHttp(thisReq) { 495 // Fetch a URL via XMLHttpRequest machinery. 496 // 497 initiateXMLHttp2(thisReq, false); 498 } 499 500 function initiateXMLHttp2(thisReq, retrying) { 501 // Internal subroutine for initiateXMLHttp, distinguishing retries 502 // 503 // Internal subroutine for initiateXMLHttp 504 // 505 var xmlhttp = null; 506 if (window.XMLHttpRequest) { 507 xmlhttp = new XMLHttpRequest(); 508 } else if (window.ActiveXObject) { 509 try { 510 xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); 511 } catch(e) { } 512 } 513 if (xmlhttp) { 514 var postData = thisReq.postData; 515 var async = (thisReq.synchronous ? false : true); 516 xmlhttp.onreadystatechange = function() { 517 reqChange(xmlhttp, thisReq, retrying); 518 }; 519 xmlhttp.open((postData ? "POST" : "GET"), thisReq.url, async); 520 if (postData) xmlhttp.setRequestHeader("Content-type", 521 "application/x-www-form-urlencoded"); 522 xmlhttp.send((postData ? postData : null)); 523 } else { 524 alert("This browser has no known XMLHttp support"); 525 } 526 } 527 528 function reqChange(xmlhttp, thisReq, retrying) { 529 // Called on XMLHTTP state changes 530 // 531 // Internal subroutine for initiateXMLHttp 532 // 533 if (xmlhttp.readyState == 4) { 534 xmlhttp.onreadystatechange = function() { }; 535 // garbage collection assistance 536 if (!xmlhttp.status || xmlhttp.status == 12029) { 537 // Connection failures (Safari delivers null, IE gives 12029) 538 // We retry once, after a short delay 539 if (!retrying) { 540 window.setTimeout(function() { 541 initiateXMLHttp2(thisReq, true); 542 }, 543 100); 544 } else { 545 thisReq.handleFailure(xmlhttp); 546 } 547 } else if (xmlhttp.status == 200 || xmlhttp.status == 1) { 548 thisReq.handleResult(xmlhttp); 549 } else { 550 thisReq.handleFailure(xmlhttp); 551 } 552 } 553 } 554 555 556 // 557 // Parsing XML from a string 558 // 559 560 function parseXML(txt) { 561 // Parse an XML sub-tree fragment, and return the DOM root node 562 // I.e., res.documentElement is the top-level XML node 563 // 564 // Intended to work on any modern browser. 565 if (window.DOMParser) { 566 var parser = new window.DOMParser(); 567 return parser.parseFromString(txt, "text/xml"); 568 } else if (window.ActiveXObject) { 569 try { 570 var res = new ActiveXObject("Microsoft.XMLDOM"); 571 res.async = false; 572 res.loadXML(txt); 573 return res; 574 } catch(e) { } 575 } 576 return null; 577 }
End of listing