blob: 7899d43960851b7410b808faadbed0623d669710 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ------------------------------------------------------------------------
// 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.
// AssertType(): assert that a value has a particular type
//
// - Cookies
// SetCookie(): Sets a cookie.
// ExpireCookie(): Expires 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
// GetParentNode(): Get the parent of an element
// GetAttribute(): Get attribute value of a DOM node
// GetInnerHTML(): get the inner HTML of a node
// SetCssStyle(): Sets a CSS property of a node.
// GetStyleProperty(): Get CSS property from a style attribute string
// GetCellIndex(): Get the index of a table cell in a table row
// ShowElement(): Show/hide element by setting the "display" css property.
// ShowBlockElement(): Show/hide block element
// SetButtonText(): Set the text of a button element.
// AppendNewElement(): Create and append a html element to a parent node.
// CreateDIV(): Create a DIV element and append to the document.
// 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
// GetPageOffsetRight() : 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
// IsScrollAtEnd(): check if window scrollbar has reached its maximum offset
// ScrollTo(): scroll window to a position
// ScrollIntoView(): scroll window so that an element is in view.
// GetWindowWidth(): get width of a window.
// GetWindowHeight(): get height of a window
// GetAvailScreenWidth(): get available screen width
// GetAvailScreenHeight(): get available screen height
// GetNiceWindowHeight(): get a nice height for a new browser window.
// Open{External/Internal}Window(): open a separate window
// CloseWindow(): close a window
//
// - DOM walking utilities
// AnnotateTerms(): find terms in a node and decorate them with some tag
// AnnotateText(): find terms in a text node and decorate them with some tag
//
// - String utilties
// HtmlEscape(): html escapes a string
// HtmlUnescape(): remove html-escaping.
// QuoteEscape(): escape " quotes.
// 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
// ConvertEOLToLF(): normalize the new-lines of a string.
// HtmlEscapeInsertWbrs(): HtmlEscapes and inserts <wbr>s (word break tags)
// after every n non-space chars and/or after or before certain special chars
//
// - TextArea utilities
// GetCursorPos(): finds the cursor position of a textfield
// 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.
// CloneObject(): clone an object, copying its values recursively.
// CloneEvent(): clone an event; cannot use CloneObject because it
// suffers from infinite recursion
//
// - Formatting utilities
// PrintArray(): used to print/generate HTML by combining static text
// and dynamic strings.
// ImageHtml(): create html for an img tag
// FormatJSLink(): formats a link that invokes js code when clicked.
// MakeId3(): formats an id that has two id numbers, eg, foo_3_7
//
// - Timeouts
// SafeTimeout(): sets a timeout with protection against ugly JS-errors
// CancelTimeout(): cancels a timeout with a given ID
// CancelAllTimeouts(): cancels all timeouts on a given window
//
// - Miscellaneous
// IsDefined(): returns true if argument is not undefined
// ------------------------------------------------------------------------
// browser detection
function BR_AgentContains_(str) {
if (str in BR_AgentContains_cache_) {
return BR_AgentContains_cache_[str];
}
return BR_AgentContains_cache_[str] =
(navigator.userAgent.toLowerCase().indexOf(str) != -1);
}
// We cache the results of the indexOf operation. This gets us a 10x benefit in
// Gecko, 8x in Safari and 4x in MSIE for all of the browser checks
var BR_AgentContains_cache_ = {};
function BR_IsIE() {
return (BR_AgentContains_('msie') || BR_AgentContains_('trident')) &&
!window.opera;
}
function BR_IsKonqueror() {
return BR_AgentContains_('konqueror');
}
function BR_IsSafari() {
return BR_AgentContains_('safari') || BR_IsKonqueror();
}
function BR_IsNav() {
return !BR_IsIE() &&
!BR_IsSafari() &&
BR_AgentContains_('mozilla');
}
var BACKSPACE_KEYNAME = 'Backspace';
var COMMA_KEYNAME = ',';
var DELETE_KEYNAME = 'Delete';
var UP_KEYNAME = 'ArrowUp';
var DOWN_KEYNAME = 'ArrowDown';
var LEFT_KEYNAME = 'ArrowLeft';
var RIGHT_KEYNAME = 'ArrowRight';
var ENTER_KEYNAME = 'Enter';
var ESC_KEYNAME = 'Escape';
var SPACE_KEYNAME = ' ';
var TAB_KEYNAME = 'Tab';
var SHIFT_KEYNAME = 'Shift';
var PAGE_DOWN_KEYNAME = 'PageDown';
var PAGE_UP_KEYNAME = 'PageUp';
var MAX_EMAIL_ADDRESS_LENGTH = 320; // 64 + '@' + 255
var MAX_SIGNATURE_LENGTH = 1000; // 1000 chars of maximum signature
// ------------------------------------------------------------------------
// 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 they should add a
* case to the switch to handle the new value they have introduced.
*
* @param {string} opt_msg to display for failure
* DEFAULT: "Assertion failed"
*/
function Fail(opt_msg) {
opt_msg = 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 expression to evaluate
* @param {string} opt_msg to display if the assertion fails
*
*/
function AssertTrue(expression, opt_msg) {
if (!expression) {
opt_msg = opt_msg || 'Assertion failed';
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 value
* @param type A constructor function
* @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 */}
}
let makeMsg = opt_msg === undefined;
if (makeMsg) {
if (typeof type == 'function') {
let 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,
};
var EXPIRED_COOKIE_VALUE = 'EXPIRED';
// ------------------------------------------------------------------------
// 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) {
let x = el.offsetLeft;
if (el.offsetParent != null) {
x += GetPageOffsetLeft(el.offsetParent);
}
return x;
}
// Get page offset of an element
function GetPageOffsetTop(el) {
let y = el.offsetTop;
if (el.offsetParent != null) {
y += GetPageOffsetTop(el.offsetParent);
}
return y;
}
// Get page offset of an element
function GetPageOffset(el) {
let x = el.offsetLeft;
let y = el.offsetTop;
if (el.offsetParent != null) {
let pos = GetPageOffset(el.offsetParent);
x += pos.x;
y += pos.y;
}
return {x: x, y: y};
}
// Get the y position scroll offset.
function GetScrollTop(win) {
return GetWindowPropertyByBrowser_(win, getScrollTopGetters_);
}
var getScrollTopGetters_ = {
ieQuirks_: function(win) {
return win.document.body.scrollTop;
},
ieStandards_: function(win) {
return win.document.documentElement.scrollTop;
},
dom_: function(win) {
return win.pageYOffset;
},
};
// Get the x position scroll offset.
function GetScrollLeft(win) {
return GetWindowPropertyByBrowser_(win, getScrollLeftGetters_);
}
var getScrollLeftGetters_ = {
ieQuirks_: function(win) {
return win.document.body.scrollLeft;
},
ieStandards_: function(win) {
return win.document.documentElement.scrollLeft;
},
dom_: function(win) {
return win.pageXOffset;
},
};
// Scroll so that as far as possible the entire element is in view.
var ALIGN_BOTTOM = 'b';
var ALIGN_MIDDLE = 'm';
var ALIGN_TOP = 't';
var getWindowWidthGetters_ = {
ieQuirks_: function(win) {
return win.document.body.clientWidth;
},
ieStandards_: function(win) {
return win.document.documentElement.clientWidth;
},
dom_: function(win) {
return win.innerWidth;
},
};
function GetWindowHeight(win) {
return GetWindowPropertyByBrowser_(win, getWindowHeightGetters_);
}
var getWindowHeightGetters_ = {
ieQuirks_: function(win) {
return win.document.body.clientHeight;
},
ieStandards_: function(win) {
return win.document.documentElement.clientHeight;
},
dom_: function(win) {
return win.innerHeight;
},
};
/**
* Allows the easy use of different getters for IE quirks mode, IE standards
* mode and fully DOM-compliant browers.
*
* @param win window to get the property for
* @param getters object with various getters. Invoked with the passed window.
* There are three properties:
* - ieStandards_: IE 6.0 standards mode
* - ieQuirks_: IE 6.0 quirks mode and IE 5.5 and older
* - dom_: Mozilla, Safari and other fully DOM compliant browsers
*
* @private
*/
function GetWindowPropertyByBrowser_(win, getters) {
try {
if (BR_IsSafari()) {
return getters.dom_(win);
} else if (!window.opera &&
'compatMode' in win.document &&
win.document.compatMode == 'CSS1Compat') {
return getters.ieStandards_(win);
} else if (BR_IsIE()) {
return getters.ieQuirks_(win);
}
} catch (e) {
// Ignore for now and fall back to DOM method
}
return getters.dom_(win);
}
function GetAvailScreenWidth(win) {
return win.screen.availWidth;
}
// Used for horizontally centering a new window of the given width in the
// available screen. Set the new window's distance from the left of the screen
// equal to this function's return value.
// Params: width: the width of the new window
// Returns: the distance from the left edge of the screen for the new window to
// be horizontally centered
function GetCenteringLeft(win, width) {
return (win.screen.availWidth - width) >> 1;
}
// Used for vertically centering a new window of the given height in the
// available screen. Set the new window's distance from the top of the screen
// equal to this function's return value.
// Params: height: the height of the new window
// Returns: the distance from the top edge of the screen for the new window to
// be vertically aligned.
function GetCenteringTop(win, height) {
return (win.screen.availHeight - height) >> 1;
}
/**
* Opens a child popup window that has no browser toolbar/decorations.
* (Copied from caribou's common.js library with small modifications.)
*
* @param url the URL for the new window (Note: this will be unique-ified)
* @param opt_name the name of the new window
* @param opt_width the width of the new window
* @param opt_height the height of the new window
* @param opt_center if true, the new window is centered in the available screen
* @param opt_hide_scrollbars if true, the window hides the scrollbars
* @param opt_noresize if true, makes window unresizable
* @param opt_blocked_msg message warning that the popup has been blocked
* @return {Window} a reference to the new child window
*/
function Popup(url, opt_name, opt_width, opt_height, opt_center,
opt_hide_scrollbars, opt_noresize, opt_blocked_msg) {
if (!opt_height) {
opt_height = Math.floor(GetWindowHeight(window.top) * 0.8);
}
if (!opt_width) {
opt_width = Math.min(GetAvailScreenWidth(window), opt_height);
}
let features = 'resizable=' + (opt_noresize ? 'no' : 'yes') + ',' +
'scrollbars=' + (opt_hide_scrollbars ? 'no' : 'yes') + ',' +
'width=' + opt_width + ',height=' + opt_height;
if (opt_center) {
features += ',left=' + GetCenteringLeft(window, opt_width) + ',' +
'top=' + GetCenteringTop(window, opt_height);
}
return OpenWindow(window, url, opt_name, features, opt_blocked_msg);
}
/**
* Opens a new window. Returns the new window handle. Tries to open the new
* window using top.open() first. If that doesn't work, then tries win.open().
* If that still doesn't work, prints an alert.
* (Copied from caribou's common.js library with small modifications.)
*
* @param win the parent window from which to open the new child window
* @param url the URL for the new window (Note: this will be unique-ified)
* @param opt_name the name of the new window
* @param opt_features the properties of the new window
* @param opt_blocked_msg message warning that the popup has been blocked
* @return {Window} a reference to the new child window
*/
function OpenWindow(win, url, opt_name, opt_features, opt_blocked_msg) {
let newwin = OpenWindowHelper(top, url, opt_name, opt_features);
if (!newwin || newwin.closed || !newwin.focus) {
newwin = OpenWindowHelper(win, url, opt_name, opt_features);
}
if (!newwin || newwin.closed || !newwin.focus) {
if (opt_blocked_msg) alert(opt_blocked_msg);
} else {
// Make sure that the window has the focus
newwin.focus();
}
return newwin;
}
/*
* Helper for OpenWindow().
* (Copied from caribou's common.js library with small modifications.)
*/
function OpenWindowHelper(win, url, name, features) {
let newwin;
if (features) {
newwin = win.open(url, name, features);
} else if (name) {
newwin = win.open(url, name);
} else {
newwin = win.open(url);
}
return newwin;
}
// ------------------------------------------------------------------------
// String utilities
// ------------------------------------------------------------------------
// Do html escaping
var amp_re_ = /&/g;
var lt_re_ = /</g;
var gt_re_ = />/g;
// 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_ = / $/;
var newline_re_ = /\r?\n/g;
var spctab_re_ = /[ \t]+/g;
var nbsp_re_ = /\xa0/g;
// URL-decodes the string. We need to specially handle '+'s because
// the javascript library doesn't properly convert them to spaces
var plus_re_ = /\+/g;
// Converts any instances of "\r" or "\r\n" style EOLs into "\n" (Line Feed),
// and also trim the extra newlines and whitespaces at the end.
var eol_re_ = /\r\n?/g;
var trailingspc_re_ = /[\n\t ]+$/;
// Converts a string to its canonicalized label form.
var illegal_chars_re_ = /[ \/(){}&|\\\"\000]/g;
// ------------------------------------------------------------------------
// TextArea utilities
// ------------------------------------------------------------------------
// Gets the cursor pos in a text area. Returns -1 if the cursor pos cannot
// be determined or if the cursor out of the textfield.
function GetCursorPos(win, textfield) {
try {
if (IsDefined(textfield.selectionEnd)) {
// Mozilla directly supports this
return textfield.selectionEnd;
} else if (win.document.selection && win.document.selection.createRange) {
// IE doesn't export an accessor for the endpoints of a selection.
// Instead, it uses the TextRange object, which has an extremely obtuse
// API. Here's what seems to work:
// (1) Obtain a textfield from the current selection (cursor)
let tr = win.document.selection.createRange();
// Check if the current selection is in the textfield
if (tr.parentElement() != textfield) {
return -1;
}
// (2) Make a text range encompassing the textfield
let tr2 = tr.duplicate();
tr2.moveToElementText(textfield);
// (3) Move the end of the copy to the beginning of the selection
tr2.setEndPoint('EndToStart', tr);
// (4) The span of the textrange copy is equivalent to the cursor pos
let cursor = tr2.text.length;
// Finally, perform a sanity check to make sure the cursor is in the
// textfield. IE sometimes screws this up when the window is activated
if (cursor > textfield.value.length) {
return -1;
}
return cursor;
} else {
Debug('Unable to get cursor position for: ' + navigator.userAgent);
// Just return the size of the textfield
// TODO: Investigate how to get cursor pos in Safari!
return textfield.value.length;
}
} catch (e) {
DumpException(e, 'Cannot get cursor pos');
}
return -1;
}
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
let 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 (let i = 0; i < array.length; i++) {
if (array[i] == x) {
return i;
}
}
return -1;
}
// Delete an element from an array
function DeleteArrayElement(array, x) {
let i = 0;
while (i < array.length && array[i] != x) {
i++;
}
array.splice(i, 1);
}
// Clean up email address:
// - remove extra spaces
// - Surround name with quotes if it contains special characters
// to check if we need " quotes
// Note: do not use /g in the regular expression, otherwise the
// regular expression cannot be reusable.
var specialchars_re_ = /[()<>@,;:\\\".\[\]]/;
// ------------------------------------------------------------------------
// Timeouts
//
// It is easy to forget to put a try/catch block around a timeout function,
// and the result is an ugly user visible javascript error.
// Also, it would be nice if a timeout associated with a window is
// automatically cancelled when the user navigates away from that window.
//
// When storing timeouts in a window, we can't let that variable be renamed
// since the window could be top.js, and renaming such a property could
// clash with any of the variables/functions defined in top.js.
// ------------------------------------------------------------------------
/**
* Sets a timeout safely.
* @param win the window object. If null is passed in, then a timeout if set
* on the js frame. If the window is closed, or freed, the timeout is
* automaticaaly cancelled
* @param fn the callback function: fn(win) will be called.
* @param ms number of ms the callback should be called later
*/
function SafeTimeout(win, fn, ms) {
if (!win) win = window;
if (!win._tm) {
win._tm = [];
}
let timeoutfn = SafeTimeoutFunction_(win, fn);
let id = win.setTimeout(timeoutfn, ms);
// Save the id so that it can be removed from the _tm array
timeoutfn.id = id;
// Safe the timeout in the _tm array
win._tm[id] = 1;
return id;
}
/** Creates a callback function for a timeout*/
function SafeTimeoutFunction_(win, fn) {
var timeoutfn = function() {
try {
fn(win);
let t = win._tm;
if (t) {
delete t[timeoutfn.id];
}
} catch (e) {
DumpException(e);
}
};
return timeoutfn;
}
// ------------------------------------------------------------------------
// Misc
// ------------------------------------------------------------------------
// Check if a value is defined
function IsDefined(value) {
return (typeof value) != 'undefined';
}