800 lines
23 KiB
JavaScript

// Copied from .../google3/javascript/common.js
//------------------------------------------------------------------------
// This file contains common utilities and basic javascript infrastructure.
//
// Notes:
// * Press 'D' to toggle debug mode.
//
// Functions:
//
// - Assertions
// DEPRECATED: Use assert.js
// AssertTrue(): assert an expression. Throws an exception if false.
// Fail(): Throws an exception. (Mark block of code that should be unreachable)
// AssertEquals(): assert that two values are equal.
// AssertNumArgs(): assert number of arguments for the function
// AssertType(): assert that a value has a particular type
//
// - Cookies
// SetCookie(): Sets a cookie.
// GetCookie(): Gets a cookie value.
//
// - Dynamic HTML/DOM utilities
// MaybeGetElement(): get an element by its id
// GetElement(): get an element by its id
// ShowElement(): Show/hide element by setting the "display" css property.
// ShowBlockElement(): Show/hide block element
// AppendNewElement(): Create and append a html element to a parent node.
// HasClass(): check if element has a given class
// AddClass(): add a class to an element
// RemoveClass(): remove a class from an element
//
// - Window/Screen utiltiies
// GetPageOffsetLeft(): get the X page offset of an element
// GetPageOffsetTop(): get the Y page offset of an element
// GetPageOffset(): get the X and Y page offsets of an element
// GetPageOffsetRight() : get X page offset of the right side of an element
// GetPageOffsetBottom() : get Y page offset of the bottom of an element
// GetScrollTop(): get the vertical scrolling pos of a window.
// GetScrollLeft(): get the horizontal scrolling pos of a window
//
// - String utilties
// HtmlEscape(): html escapes a string
// HtmlUnescape(): remove html-escaping.
// CollapseWhitespace(): collapse multiple whitespace into one whitespace.
// Trim(): trim whitespace on ends of string
// IsEmpty(): check if CollapseWhiteSpace(String) == ""
// IsLetterOrDigit(): check if a character is a letter or a digit
//
// - TextArea utilities
// SetCursorPos(): sets the cursor position in a textfield
//
// - Array utilities
// FindInArray(): do a linear search to find an element value.
// DeleteArrayElement(): return a new array with a specific value removed.
//
// - Miscellaneous
// IsDefined(): returns true if argument is not undefined
//------------------------------------------------------------------------
// browser detection
var agent = navigator.userAgent.toLowerCase();
var is_ie = (agent.indexOf('msie') != -1);
//var is_ie5 = (agent.indexOf('msie 5') != -1 && document.all);
var is_konqueror = (agent.indexOf('konqueror') != -1);
var is_safari = (agent.indexOf('safari') != -1) || is_konqueror;
var is_nav = !is_ie && !is_safari && (agent.indexOf('mozilla') != -1);
var is_win = (agent.indexOf('win') != -1);
delete agent;
var BACKSPACE_KEYCODE = 8;
var COMMA_KEYCODE = 188; // ',' key
var DEBUG_KEYCODE = 68; // 'D' key
var DELETE_KEYCODE = 46;
var DOWN_KEYCODE = 40; // DOWN arrow key
var ENTER_KEYCODE = 13; // ENTER key
var ESC_KEYCODE = 27; // ESC key
var LEFT_KEYCODE = 37; // LEFT arrow key
var RIGHT_KEYCODE = 39; // RIGHT arrow key
var SPACE_KEYCODE = 32; // space bar
var TAB_KEYCODE = 9; // TAB key
var UP_KEYCODE = 38; // UP arrow key
var SHIFT_KEYCODE = 16;
//------------------------------------------------------------------------
// Assertions
// DEPRECATED: Use assert.js
//------------------------------------------------------------------------
/**
* DEPRECATED: Use assert.js
*/
function raise(msg) {
if (typeof Error != 'undefined') {
throw new Error(msg || 'Assertion Failed');
} else {
throw (msg);
}
}
/**
* DEPRECATED: Use assert.js
*
* Fail() is useful for marking logic paths that should
* not be reached. For example, if you have a class that uses
* ints for enums:
*
* MyClass.ENUM_FOO = 1;
* MyClass.ENUM_BAR = 2;
* MyClass.ENUM_BAZ = 3;
*
* And a switch statement elsewhere in your code that
* has cases for each of these enums, then you can
* "protect" your code as follows:
*
* switch(type) {
* case MyClass.ENUM_FOO: doFooThing(); break;
* case MyClass.ENUM_BAR: doBarThing(); break;
* case MyClass.ENUM_BAZ: doBazThing(); break;
* default:
* Fail("No enum in MyClass with value: " + type);
* }
*
* This way, if someone introduces a new value for this enum
* without noticing this switch statement, then the code will
* fail if the logic allows it to reach the switch with the
* new value, alerting the developer that he should add a
* case to the switch to handle the new value he has introduced.
*
* @param {string} opt_msg to display for failure
* DEFAULT: "Assertion failed"
*/
function Fail(opt_msg) {
if (opt_msg === undefined) opt_msg = 'Assertion failed';
if (IsDefined(DumpError)) DumpError(opt_msg + '\n');
raise(opt_msg);
}
/**
* DEPRECATED: Use assert.js
*
* Asserts that an expression is true (non-zero and non-null).
*
* Note that it is critical not to pass logic
* with side-effects as the expression for AssertTrue
* because if the assertions are removed by the
* JSCompiler, then the expression will be removed
* as well, in which case the side-effects will
* be lost. So instead of this:
*
* AssertTrue( criticalComputation() );
*
* Do this:
*
* var result = criticalComputation();
* AssertTrue(result);
*
* @param {anything} expression to evaluate
* @param {string} opt_msg to display if the assertion fails
*
*/
function AssertTrue(expression, opt_msg) {
if (!expression) {
if (opt_msg === undefined) opt_msg = 'Assertion failed';
Fail(opt_msg);
}
}
/**
* DEPRECATED: Use assert.js
*
* Asserts that two values are the same.
*
* @param {anything} val1
* @param {anything} val2
* @param {string} opt_msg to display if the assertion fails
*/
function AssertEquals(val1, val2, opt_msg) {
if (val1 != val2) {
if (opt_msg === undefined) {
opt_msg = "AssertEquals failed: <" + val1 + "> != <" + val2 + ">";
}
Fail(opt_msg);
}
}
/**
* DEPRECATED: Use assert.js
*
* Asserts that a value is of the provided type.
*
* AssertType(6, Number);
* AssertType("ijk", String);
* AssertType([], Array);
* AssertType({}, Object);
* AssertType(ICAL_Date.now(), ICAL_Date);
*
* @param {anything} value
* @param {constructor function} type
* @param {string} opt_msg to display if the assertion fails
*/
function AssertType(value, type, opt_msg) {
// for backwards compatability only
if (typeof value == type) return;
if (value || value == "") {
try {
if (type == AssertTypeMap[typeof value] || value instanceof type) return;
} catch (e) { /* failure, type was an illegal argument to instanceof */ }
}
if (opt_msg === undefined) {
if (typeof type == 'function') {
var match = type.toString().match(/^\s*function\s+([^\s\{]+)/);
if (match) type = match[1];
}
opt_msg = "AssertType failed: <" + value + "> not typeof "+ type;
}
Fail(opt_msg);
}
var AssertTypeMap = {
'string' : String,
'number' : Number,
'boolean' : Boolean
};
/**
* DEPRECATED: Use assert.js
*
* Asserts that the number of arguments to a
* function is num. For example:
*
* function myFunc(one, two, three) [
* AssertNumArgs(3);
* ...
* }
*
* myFunc(1, 2); // assertion fails!
*
* Note that AssertNumArgs does not take the function
* as an argument; it is simply used in the context
* of the function.
*
* @param {int} number of arguments expected
* @param {string} opt_msg to display if the assertion fails
*/
function AssertNumArgs(num, opt_msg) {
var caller = AssertNumArgs.caller; // This is not supported in safari 1.0
if (caller && caller.arguments.length != num) {
if (opt_msg === undefined) {
opt_msg = caller.name + ' expected ' + num + ' arguments '
+ ' but received ' + caller.arguments.length;
}
Fail(opt_msg);
}
}
//------------------------------------------------------------------------
// Cookies
//------------------------------------------------------------------------
var ILLEGAL_COOKIE_CHARS_RE = /[\s;]/
/**
* Sets a cookie.
* The max_age can be -1 to set a session cookie. To expire cookies, use
* ExpireCookie() instead.
*
* @param name The cookie name.
* @param value The cookie value.
* @param opt_max_age The max age in seconds (from now). Use -1 to set a
* session cookie. If not provided, the default is -1 (i.e. set a session
* cookie).
* @param opt_path The path of the cookie, or null to not specify a path
* attribute (browser will use the full request path). If not provided, the
* default is '/' (i.e. path=/).
* @param opt_domain The domain of the cookie, or null to not specify a domain
* attribute (brower will use the full request host name). If not provided,
* the default is null (i.e. let browser use full request host name).
* @return Void.
*/
function SetCookie(name, value, opt_max_age, opt_path, opt_domain) {
value = '' + value;
AssertTrue((typeof name == 'string' &&
typeof value == 'string' &&
!name.match(ILLEGAL_COOKIE_CHARS_RE) &&
!value.match(ILLEGAL_COOKIE_CHARS_RE)),
'trying to set an invalid cookie');
if (!IsDefined(opt_max_age)) opt_max_age = -1;
if (!IsDefined(opt_path)) opt_path = '/';
if (!IsDefined(opt_domain)) opt_domain = null;
var domain_str = (opt_domain == null) ? '' : ';domain=' + opt_domain;
var path_str = (opt_path == null) ? '' : ';path=' + opt_path;
var expires_str;
// Case 1: Set a session cookie.
if (opt_max_age < 0) {
expires_str = '';
// Case 2: Expire the cookie.
// Note: We don't tell people about this option in the function doc because
// we prefer people to use ExpireCookie() to expire cookies.
} else if (opt_max_age == 0) {
// Note: Don't use Jan 1, 1970 for date because NS 4.76 will try to convert
// it to local time, and if the local time is before Jan 1, 1970, then the
// browser will ignore the Expires attribute altogether.
var pastDate = new Date(1970, 1 /*Feb*/, 1); // Feb 1, 1970
expires_str = ';expires=' + pastDate.toUTCString();
// Case 3: Set a persistent cookie.
} else {
var futureDate = new Date(Now() + opt_max_age * 1000);
expires_str = ';expires=' + futureDate.toUTCString();
}
document.cookie = name + '=' + value + domain_str + path_str + expires_str;
}
/** Returns the value for the first cookie with the given name
* @param name : string
* @return a string or the empty string if no cookie found.
*/
function GetCookie(name) {
var nameeq = name + "=";
var cookie = String(document.cookie);
for (var pos = -1; (pos = cookie.indexOf(nameeq, pos + 1)) >= 0;) {
var i = pos;
// walk back along string skipping whitespace and looking for a ; before
// the name to make sure that we don't match cookies whose name contains
// the given name as a suffix.
while (--i >= 0) {
var ch = cookie.charAt(i);
if (ch == ';') {
i = -1; // indicate success
break;
} else if (' \t'.indexOf(ch) < 0) {
break;
}
}
if (-1 === i) { // first cookie in the string or we found a ;
var end = cookie.indexOf(';', pos);
if (end < 0) { end = cookie.length; }
return cookie.substring(pos + nameeq.length, end);
}
}
return "";
}
//------------------------------------------------------------------------
// Time
//------------------------------------------------------------------------
function Now() {
return (new Date()).getTime();
}
//------------------------------------------------------------------------
// Dynamic HTML/DOM utilities
//------------------------------------------------------------------------
// Gets a element by its id, may return null
function MaybeGetElement(win, id) {
return win.document.getElementById(id);
}
// Same as MaybeGetElement except that it throws an exception if it's null
function GetElement(win, id) {
var el = win.document.getElementById(id);
if (!el) {
DumpError("Element " + id + " not found.");
}
return el;
}
// Gets elements by its id/name
// IE treats getElementsByName as searching over ids, while Moz use names.
// so tags must have both id and name as the same string
function GetElements(win, id) {
return win.document.getElementsByName(id);
}
// Show/hide an element.
function ShowElement(el, show) {
el.style.display = show ? "" : "none";
}
// Show/hide a block element.
// ShowElement() doesn't work if object has an initial class with display:none
function ShowBlockElement(el, show) {
el.style.display = show ? "block" : "none";
}
// Show/hide an inline element.
// ShowElement() doesn't work when an element starts off display:none.
function ShowInlineElement(el, show) {
el.style.display = show ? "inline" : "none";
}
// Append a new HTML element to a HTML node.
function AppendNewElement(win, parent, tag) {
var e = win.document.createElement(tag);
parent.appendChild(e);
return e;
}
// Create a new TR containing the given td's
function Tr(win, tds) {
var tr = win.document.createElement("TR");
for (var i = 0; i < tds.length; i++) {
tr.appendChild(tds[i]);
}
return tr;
}
// Create a new TD, with an optional colspan
function Td(win, opt_colspan) {
var td = win.document.createElement("TD");
if (opt_colspan) {
td.colSpan = opt_colspan;
}
return td;
}
// Check if an element has a given class
function HasClass(el, cl) {
if (el == null || el.className == null) return false;
var classes = el.className.split(" ");
for (var i = 0; i < classes.length; i++) {
if (classes[i] == cl) {
return true;
}
}
return false;
}
// Add a class to element
function AddClass(el, cl) {
if (HasClass(el, cl)) return;
el.className += " " + cl;
}
// Remove a class from an element
function RemoveClass(el, cl) {
if (el.className == null) return;
var classes = el.className.split(" ");
var result = [];
var changed = false;
for (var i = 0; i < classes.length; i++) {
if (classes[i] != cl) {
if (classes[i]) { result.push(classes[i]); }
} else {
changed = true;
}
}
if (changed) { el.className = result.join(" "); }
}
// Performs an in-order traversal of the tree rooted at the given node
// (excluding the root node) and returns an array of nodes that match the
// given selector. The selector must implement the method:
//
// boolean select(node);
//
// This method is a generalization of the DOM method "getElementsByTagName"
//
function GetElementsBySelector(root, selector) {
var nodes = [];
for (var child = root.firstChild; child; child = child.nextSibling) {
AddElementBySelector_(child, selector, nodes);
}
return nodes;
}
// Recursive helper for GetElemnetsBySelector()
function AddElementBySelector_(root, selector, nodes) {
// First test the parent
if (selector.select(root)) {
nodes.push(root);
}
// Then recurse through the children
for (var child = root.firstChild; child; child = child.nextSibling) {
AddElementBySelector_(child, selector, nodes);
}
}
//------------------------------------------------------------------------
// Window/screen utilities
// TODO: these should be renamed (e.g. GetWindowWidth to GetWindowInnerWidth
// and moved to geom.js)
//------------------------------------------------------------------------
// Get page offset of an element
function GetPageOffsetLeft(el) {
var x = el.offsetLeft;
if (el.offsetParent != null)
x += GetPageOffsetLeft(el.offsetParent);
return x;
}
// Get page offset of an element
function GetPageOffsetTop(el) {
var y = el.offsetTop;
if (el.offsetParent != null)
y += GetPageOffsetTop(el.offsetParent);
return y;
}
// Get page offset of an element
function GetPageOffset(el) {
var x = el.offsetLeft;
var y = el.offsetTop;
if (el.offsetParent != null) {
var pos = GetPageOffset(el.offsetParent);
x += pos.x;
y += pos.y;
}
return {x: x, y: y};
}
function GetPageOffsetRight(el) {
return GetPageOffsetLeft(el) + el.offsetWidth;
}
function GetPageOffsetBottom(el) {
return GetPageOffsetTop(el) + el.offsetHeight;
}
// Get the y position scroll offset.
function GetScrollTop(win) {
// all except Explorer
if ("pageYOffset" in win) {
return win.pageYOffset;
}
// Explorer 6 Strict Mode
else if ("documentElement" in win.document &&
"scrollTop" in win.document.documentElement) {
return win.document.documentElement.scrollTop;
}
// other Explorers
else if ("scrollTop" in win.document.body) {
return win.document.body.scrollTop;
}
return 0;
}
// Get the x position scroll offset.
function GetScrollLeft(win) {
// all except Explorer
if ("pageXOffset" in win) {
return win.pageXOffset;
}
// Explorer 6 Strict Mode
else if ("documentElement" in win.document &&
"scrollLeft" in win.document.documentElement) {
return win.document.documentElement.scrollLeft;
}
// other Explorers
else if ("scrollLeft" in win.document.body) {
return win.document.body.scrollLeft;
}
return 0;
}
//------------------------------------------------------------------------
// String utilities
//------------------------------------------------------------------------
// Do html escaping
var amp_re_ = /&/g;
var lt_re_ = /</g;
var gt_re_ = />/g;
// Convert text to HTML format. For efficiency, we just convert '&', '<', '>'
// characters.
// Note: Javascript >= 1.3 supports lambda expression in the replacement
// argument. But it's slower on IE.
// Note: we can also implement HtmlEscape by setting the value
// of a textnode and then reading the 'innerHTML' value, but that
// that turns out to be slower.
// Params: str: String to be escaped.
// Returns: The escaped string.
function HtmlEscape(str) {
if (!str) return "";
return str.replace(amp_re_, "&amp;").replace(lt_re_, "&lt;").
replace(gt_re_, "&gt;").replace(quote_re_, "&quot;");
}
/** converts html entities to plain text. It covers the most common named
* entities and numeric entities.
* It does not cover all named entities -- it covers &{lt,gt,amp,quot,nbsp}; but
* does not handle some of the more obscure ones like &{ndash,eacute};.
*/
function HtmlUnescape(str) {
if (!str) return "";
return str.
replace(/&#(\d+);/g,
function (_, n) { return String.fromCharCode(parseInt(n, 10)); }).
replace(/&#x([a-f0-9]+);/gi,
function (_, n) { return String.fromCharCode(parseInt(n, 16)); }).
replace(/&(\w+);/g, function (_, entity) {
entity = entity.toLowerCase();
return entity in HtmlUnescape.unesc ? HtmlUnescape.unesc[entity] : '?';
});
}
HtmlUnescape.unesc = { lt: '<', gt: '>', quot: '"', nbsp: ' ', amp: '&' };
// Escape double quote '"' characters in addition to '&', '<', '>' so that a
// string can be included in an HTML tag attribute value within double quotes.
// Params: str: String to be escaped.
// Returns: The escaped string.
var quote_re_ = /\"/g;
var JS_SPECIAL_RE_ = /[\'\\\r\n\b\"<>&]/g;
function JSEscOne_(s) {
if (!JSEscOne_.js_escs_) {
var escapes = {};
escapes['\\'] = '\\\\';
escapes['\''] = '\\047';
escapes['\n'] = '\\n';
escapes['\r'] = '\\r';
escapes['\b'] = '\\b';
escapes['\"'] = '\\042';
escapes['<'] = '\\074';
escapes['>'] = '\\076';
escapes['&'] = '\\046';
JSEscOne_.js_escs_ = escapes;
}
return JSEscOne_.js_escs_[s];
}
// converts multiple ws chars to a single space, and strips
// leading and trailing ws
var spc_re_ = /\s+/g;
var beg_spc_re_ = /^ /;
var end_spc_re_ = / $/;
function CollapseWhitespace(str) {
if (!str) return "";
return str.replace(spc_re_, " ").replace(beg_spc_re_, "").
replace(end_spc_re_, "");
}
var newline_re_ = /\r?\n/g;
var spctab_re_ = /[ \t]+/g;
var nbsp_re_ = /\xa0/g;
function HtmlifyNewlines(str) {
if (!str) return "";
return str.replace(newline_re_, "<br>");
}
// URL encodes the string.
function UrlEncode(str) {
return encodeURIComponent(str);
}
function Trim(str) {
if (!str) return "";
return str.replace(/^\s+/, "").replace(/\s+$/, "");
}
function EndsWith(str, suffix) {
if (!str) return !suffix;
return (str.lastIndexOf(suffix) == (str.length - suffix.length));
}
// Check if a string is empty
function IsEmpty(str) {
return CollapseWhitespace(str) == "";
}
// Check if a character is a letter
function IsLetterOrDigit(ch) {
return ((ch >= "a" && ch <= "z") ||
(ch >= "A" && ch <= "Z") ||
(ch >= '0' && ch <= '9'));
}
// Check if a character is a space character
function IsSpace(ch) {
return (" \t\r\n".indexOf(ch) >= 0);
}
//------------------------------------------------------------------------
// TextArea utilities
//------------------------------------------------------------------------
function SetCursorPos(win, textfield, pos) {
if (IsDefined(textfield.selectionEnd) &&
IsDefined(textfield.selectionStart)) {
// Mozilla directly supports this
textfield.selectionStart = pos;
textfield.selectionEnd = pos;
} else if (win.document.selection && textfield.createTextRange) {
// IE has textranges. A textfield's textrange encompasses the
// entire textfield's text by default
var sel = textfield.createTextRange();
sel.collapse(true);
sel.move("character", pos);
sel.select();
}
}
//------------------------------------------------------------------------
// Array utilities
//------------------------------------------------------------------------
// Find an item in an array, returns the key, or -1 if not found
function FindInArray(array, x) {
for (var i = 0; i < array.length; i++) {
if (array[i] == x) {
return i;
}
}
return -1;
}
// Inserts an item into an array, if it's not already in the array
function InsertArray(array, x) {
if (FindInArray(array, x) == -1) {
array[array.length] = x;
}
}
// Delete an element from an array
function DeleteArrayElement(array, x) {
var i = 0;
while (i < array.length && array[i] != x)
i++;
array.splice(i, 1);
}
function GetEventTarget(/*Event*/ ev) {
// Event is not a type in IE; IE uses Object for events
// AssertType(ev, Event, 'arg passed to GetEventTarget not an Event');
return ev.srcElement || ev.target;
}
//------------------------------------------------------------------------
// Misc
//------------------------------------------------------------------------
// Check if a value is defined
function IsDefined(value) {
return (typeof value) != 'undefined';
}
function GetKeyCode(event) {
var code;
if (event.keyCode) {
code = event.keyCode;
} else if (event.which) {
code = event.which;
}
return code;
}
// define a forid function to fetch a DOM node by id.
function forid_1(id) {
return document.getElementById(id);
}
function forid_2(id) {
return document.all[id];
}
/**
* Fetch an HtmlElement by id.
* DEPRECATED: use $ in dom.js
*/
var forid = document.getElementById ? forid_1 : forid_2;
function log(msg) {
/* a top level window is its own parent. Use != or else fails on IE with
* infinite loop.
*/
try {
if (window.parent != window && window.parent.log) {
window.parent.log(window.name + '::' + msg);
return;
}
} catch (e) {
// Error: uncaught exception: Permission denied to get property Window.log
}
var logPane = forid('log');
if (logPane) {
var logText = '<p class=logentry><span class=logdate>' + new Date() +
'</span><span class=logmsg>' + msg + '</span></p>';
logPane.innerHTML = logText + logPane.innerHTML;
} else {
window.status = msg;
}
}