506 lines
16 KiB
JavaScript
506 lines
16 KiB
JavaScript
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// // Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
/**
|
|
* @fileoverview Assertion support.
|
|
*/
|
|
|
|
/**
|
|
* Verify |condition| is truthy and return |condition| if so.
|
|
* @template T
|
|
* @param {T} condition A condition to check for truthiness. Note that this
|
|
* may be used to test whether a value is defined or not, and we don't want
|
|
* to force a cast to Boolean.
|
|
* @param {string=} opt_message A message to show on failure.
|
|
* @return {T} A non-null |condition|.
|
|
*/
|
|
function assert(condition, opt_message) {
|
|
if (!condition) {
|
|
var message = 'Assertion failed';
|
|
if (opt_message)
|
|
message = message + ': ' + opt_message;
|
|
var error = new Error(message);
|
|
var global = function() {
|
|
return this;
|
|
}();
|
|
if (global.traceAssertionsForTesting)
|
|
console.warn(error.stack);
|
|
throw error;
|
|
}
|
|
return condition;
|
|
}
|
|
|
|
/**
|
|
* Call this from places in the code that should never be reached.
|
|
*
|
|
* For example, handling all the values of enum with a switch() like this:
|
|
*
|
|
* function getValueFromEnum(enum) {
|
|
* switch (enum) {
|
|
* case ENUM_FIRST_OF_TWO:
|
|
* return first
|
|
* case ENUM_LAST_OF_TWO:
|
|
* return last;
|
|
* }
|
|
* assertNotReached();
|
|
* return document;
|
|
* }
|
|
*
|
|
* This code should only be hit in the case of serious programmer error or
|
|
* unexpected input.
|
|
*
|
|
* @param {string=} opt_message A message to show when this is hit.
|
|
*/
|
|
function assertNotReached(opt_message) {
|
|
assert(false, opt_message || 'Unreachable code hit');
|
|
}
|
|
|
|
/**
|
|
* @param {*} value The value to check.
|
|
* @param {function(new: T, ...)} type A user-defined constructor.
|
|
* @param {string=} opt_message A message to show when this is hit.
|
|
* @return {T}
|
|
* @template T
|
|
*/
|
|
function assertInstanceof(value, type, opt_message) {
|
|
// We don't use assert immediately here so that we avoid constructing an error
|
|
// message if we don't have to.
|
|
if (!(value instanceof type)) {
|
|
assertNotReached(
|
|
opt_message ||
|
|
'Value ' + value + ' is not a[n] ' + (type.name || typeof type));
|
|
}
|
|
return value;
|
|
}
|
|
|
|
|
|
/**
|
|
* Alias for document.getElementById. Found elements must be HTMLElements.
|
|
* @param {string} id The ID of the element to find.
|
|
* @return {HTMLElement} The found element or null if not found.
|
|
*/
|
|
function $(id) {
|
|
// Disable getElementById restriction here, since we are instructing other
|
|
// places to re-use the $() that is defined here.
|
|
// eslint-disable-next-line no-restricted-properties
|
|
var el = document.getElementById(id);
|
|
return el ? assertInstanceof(el, HTMLElement) : null;
|
|
}
|
|
|
|
// TODO(devlin): This should return SVGElement, but closure compiler is missing
|
|
// those externs.
|
|
/**
|
|
* Alias for document.getElementById. Found elements must be SVGElements.
|
|
* @param {string} id The ID of the element to find.
|
|
* @return {Element} The found element or null if not found.
|
|
*/
|
|
function getSVGElement(id) {
|
|
// Disable getElementById restriction here, since it is not suitable for SVG
|
|
// elements.
|
|
// eslint-disable-next-line no-restricted-properties
|
|
var el = document.getElementById(id);
|
|
return el ? assertInstanceof(el, Element) : null;
|
|
}
|
|
|
|
/**
|
|
* Add an accessible message to the page that will be announced to
|
|
* users who have spoken feedback on, but will be invisible to all
|
|
* other users. It's removed right away so it doesn't clutter the DOM.
|
|
* @param {string} msg The text to be pronounced.
|
|
*/
|
|
function announceAccessibleMessage(msg) {
|
|
var element = document.createElement('div');
|
|
element.setAttribute('aria-live', 'polite');
|
|
element.style.position = 'fixed';
|
|
element.style.left = '-9999px';
|
|
element.style.height = '0px';
|
|
element.innerText = msg;
|
|
document.body.appendChild(element);
|
|
window.setTimeout(function() {
|
|
document.body.removeChild(element);
|
|
}, 0);
|
|
}
|
|
|
|
/**
|
|
* Generates a CSS url string.
|
|
* @param {string} s The URL to generate the CSS url for.
|
|
* @return {string} The CSS url string.
|
|
*/
|
|
function url(s) {
|
|
// http://www.w3.org/TR/css3-values/#uris
|
|
// Parentheses, commas, whitespace characters, single quotes (') and double
|
|
// quotes (") appearing in a URI must be escaped with a backslash
|
|
var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
|
|
// WebKit has a bug when it comes to URLs that end with \
|
|
// https://bugs.webkit.org/show_bug.cgi?id=28885
|
|
if (/\\\\$/.test(s2)) {
|
|
// Add a space to work around the WebKit bug.
|
|
s2 += ' ';
|
|
}
|
|
return 'url("' + s2 + '")';
|
|
}
|
|
|
|
/**
|
|
* Parses query parameters from Location.
|
|
* @param {Location} location The URL to generate the CSS url for.
|
|
* @return {Object} Dictionary containing name value pairs for URL
|
|
*/
|
|
function parseQueryParams(location) {
|
|
var params = {};
|
|
var query = unescape(location.search.substring(1));
|
|
var vars = query.split('&');
|
|
for (var i = 0; i < vars.length; i++) {
|
|
var pair = vars[i].split('=');
|
|
params[pair[0]] = pair[1];
|
|
}
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* Creates a new URL by appending or replacing the given query key and value.
|
|
* Not supporting URL with username and password.
|
|
* @param {Location} location The original URL.
|
|
* @param {string} key The query parameter name.
|
|
* @param {string} value The query parameter value.
|
|
* @return {string} The constructed new URL.
|
|
*/
|
|
function setQueryParam(location, key, value) {
|
|
var query = parseQueryParams(location);
|
|
query[encodeURIComponent(key)] = encodeURIComponent(value);
|
|
|
|
var newQuery = '';
|
|
for (var q in query) {
|
|
newQuery += (newQuery ? '&' : '?') + q + '=' + query[q];
|
|
}
|
|
|
|
return location.origin + location.pathname + newQuery + location.hash;
|
|
}
|
|
|
|
/**
|
|
* @param {Node} el A node to search for ancestors with |className|.
|
|
* @param {string} className A class to search for.
|
|
* @return {Element} A node with class of |className| or null if none is found.
|
|
*/
|
|
function findAncestorByClass(el, className) {
|
|
return /** @type {Element} */ (findAncestor(el, function(el) {
|
|
return el.classList && el.classList.contains(className);
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Return the first ancestor for which the {@code predicate} returns true.
|
|
* @param {Node} node The node to check.
|
|
* @param {function(Node):boolean} predicate The function that tests the
|
|
* nodes.
|
|
* @return {Node} The found ancestor or null if not found.
|
|
*/
|
|
function findAncestor(node, predicate) {
|
|
var last = false;
|
|
while (node != null && !(last = predicate(node))) {
|
|
node = node.parentNode;
|
|
}
|
|
return last ? node : null;
|
|
}
|
|
|
|
function swapDomNodes(a, b) {
|
|
var afterA = a.nextSibling;
|
|
if (afterA == b) {
|
|
swapDomNodes(b, a);
|
|
return;
|
|
}
|
|
var aParent = a.parentNode;
|
|
b.parentNode.replaceChild(a, b);
|
|
aParent.insertBefore(b, afterA);
|
|
}
|
|
|
|
/**
|
|
* Disables text selection and dragging, with optional whitelist callbacks.
|
|
* @param {function(Event):boolean=} opt_allowSelectStart Unless this function
|
|
* is defined and returns true, the onselectionstart event will be
|
|
* surpressed.
|
|
* @param {function(Event):boolean=} opt_allowDragStart Unless this function
|
|
* is defined and returns true, the ondragstart event will be surpressed.
|
|
*/
|
|
function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) {
|
|
// Disable text selection.
|
|
document.onselectstart = function(e) {
|
|
if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e)))
|
|
e.preventDefault();
|
|
};
|
|
|
|
// Disable dragging.
|
|
document.ondragstart = function(e) {
|
|
if (!(opt_allowDragStart && opt_allowDragStart.call(this, e)))
|
|
e.preventDefault();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check the directionality of the page.
|
|
* @return {boolean} True if Chrome is running an RTL UI.
|
|
*/
|
|
function isRTL() {
|
|
return document.documentElement.dir == 'rtl';
|
|
}
|
|
|
|
/**
|
|
* Get an element that's known to exist by its ID. We use this instead of just
|
|
* calling getElementById and not checking the result because this lets us
|
|
* satisfy the JSCompiler type system.
|
|
* @param {string} id The identifier name.
|
|
* @return {!HTMLElement} the Element.
|
|
*/
|
|
function getRequiredElement(id) {
|
|
return assertInstanceof(
|
|
$(id), HTMLElement, 'Missing required element: ' + id);
|
|
}
|
|
|
|
/**
|
|
* Query an element that's known to exist by a selector. We use this instead of
|
|
* just calling querySelector and not checking the result because this lets us
|
|
* satisfy the JSCompiler type system.
|
|
* @param {string} selectors CSS selectors to query the element.
|
|
* @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional
|
|
* context object for querySelector.
|
|
* @return {!HTMLElement} the Element.
|
|
*/
|
|
function queryRequiredElement(selectors, opt_context) {
|
|
var element = (opt_context || document).querySelector(selectors);
|
|
return assertInstanceof(
|
|
element, HTMLElement, 'Missing required element: ' + selectors);
|
|
}
|
|
|
|
// Handle click on a link. If the link points to a chrome: or file: url, then
|
|
// call into the browser to do the navigation.
|
|
['click', 'auxclick'].forEach(function(eventName) {
|
|
document.addEventListener(eventName, function(e) {
|
|
if (e.button > 1)
|
|
return; // Ignore buttons other than left and middle.
|
|
if (e.defaultPrevented)
|
|
return;
|
|
|
|
var eventPath = e.path;
|
|
var anchor = null;
|
|
if (eventPath) {
|
|
for (var i = 0; i < eventPath.length; i++) {
|
|
var element = eventPath[i];
|
|
if (element.tagName === 'A' && element.href) {
|
|
anchor = element;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback if Event.path is not available.
|
|
var el = e.target;
|
|
if (!anchor && el.nodeType == Node.ELEMENT_NODE &&
|
|
el.webkitMatchesSelector('A, A *')) {
|
|
while (el.tagName != 'A') {
|
|
el = el.parentElement;
|
|
}
|
|
anchor = el;
|
|
}
|
|
|
|
if (!anchor)
|
|
return;
|
|
|
|
anchor = /** @type {!HTMLAnchorElement} */ (anchor);
|
|
if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') &&
|
|
(e.button == 0 || e.button == 1)) {
|
|
chrome.send('navigateToUrl', [
|
|
anchor.href, anchor.target, e.button, e.altKey, e.ctrlKey, e.metaKey,
|
|
e.shiftKey
|
|
]);
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Creates a new URL which is the old URL with a GET param of key=value.
|
|
* @param {string} url The base URL. There is not sanity checking on the URL so
|
|
* it must be passed in a proper format.
|
|
* @param {string} key The key of the param.
|
|
* @param {string} value The value of the param.
|
|
* @return {string} The new URL.
|
|
*/
|
|
function appendParam(url, key, value) {
|
|
var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
|
|
|
|
if (url.indexOf('?') == -1)
|
|
return url + '?' + param;
|
|
return url + '&' + param;
|
|
}
|
|
|
|
/**
|
|
* Creates an element of a specified type with a specified class name.
|
|
* @param {string} type The node type.
|
|
* @param {string} className The class name to use.
|
|
* @return {Element} The created element.
|
|
*/
|
|
function createElementWithClassName(type, className) {
|
|
var elm = document.createElement(type);
|
|
elm.className = className;
|
|
return elm;
|
|
}
|
|
|
|
/**
|
|
* transitionend does not always fire (e.g. when animation is aborted
|
|
* or when no paint happens during the animation). This function sets up
|
|
* a timer and emulate the event if it is not fired when the timer expires.
|
|
* @param {!HTMLElement} el The element to watch for transitionend.
|
|
* @param {number=} opt_timeOut The maximum wait time in milliseconds for the
|
|
* transitionend to happen. If not specified, it is fetched from |el|
|
|
* using the transitionDuration style value.
|
|
*/
|
|
function ensureTransitionEndEvent(el, opt_timeOut) {
|
|
if (opt_timeOut === undefined) {
|
|
var style = getComputedStyle(el);
|
|
opt_timeOut = parseFloat(style.transitionDuration) * 1000;
|
|
|
|
// Give an additional 50ms buffer for the animation to complete.
|
|
opt_timeOut += 50;
|
|
}
|
|
|
|
var fired = false;
|
|
el.addEventListener('transitionend', function f(e) {
|
|
el.removeEventListener('transitionend', f);
|
|
fired = true;
|
|
});
|
|
window.setTimeout(function() {
|
|
if (!fired)
|
|
cr.dispatchSimpleEvent(el, 'transitionend', true);
|
|
}, opt_timeOut);
|
|
}
|
|
|
|
/**
|
|
* Alias for document.scrollTop getter.
|
|
* @param {!HTMLDocument} doc The document node where information will be
|
|
* queried from.
|
|
* @return {number} The Y document scroll offset.
|
|
*/
|
|
function scrollTopForDocument(doc) {
|
|
return doc.documentElement.scrollTop || doc.body.scrollTop;
|
|
}
|
|
|
|
/**
|
|
* Alias for document.scrollTop setter.
|
|
* @param {!HTMLDocument} doc The document node where information will be
|
|
* queried from.
|
|
* @param {number} value The target Y scroll offset.
|
|
*/
|
|
function setScrollTopForDocument(doc, value) {
|
|
doc.documentElement.scrollTop = doc.body.scrollTop = value;
|
|
}
|
|
|
|
/**
|
|
* Alias for document.scrollLeft getter.
|
|
* @param {!HTMLDocument} doc The document node where information will be
|
|
* queried from.
|
|
* @return {number} The X document scroll offset.
|
|
*/
|
|
function scrollLeftForDocument(doc) {
|
|
return doc.documentElement.scrollLeft || doc.body.scrollLeft;
|
|
}
|
|
|
|
/**
|
|
* Alias for document.scrollLeft setter.
|
|
* @param {!HTMLDocument} doc The document node where information will be
|
|
* queried from.
|
|
* @param {number} value The target X scroll offset.
|
|
*/
|
|
function setScrollLeftForDocument(doc, value) {
|
|
doc.documentElement.scrollLeft = doc.body.scrollLeft = value;
|
|
}
|
|
|
|
/**
|
|
* Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding.
|
|
* @param {string} original The original string.
|
|
* @return {string} The string with all the characters mentioned above replaced.
|
|
*/
|
|
function HTMLEscape(original) {
|
|
return original.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
/**
|
|
* Shortens the provided string (if necessary) to a string of length at most
|
|
* |maxLength|.
|
|
* @param {string} original The original string.
|
|
* @param {number} maxLength The maximum length allowed for the string.
|
|
* @return {string} The original string if its length does not exceed
|
|
* |maxLength|. Otherwise the first |maxLength| - 1 characters with '...'
|
|
* appended.
|
|
*/
|
|
function elide(original, maxLength) {
|
|
if (original.length <= maxLength)
|
|
return original;
|
|
return original.substring(0, maxLength - 1) + '\u2026';
|
|
}
|
|
|
|
/**
|
|
* Quote a string so it can be used in a regular expression.
|
|
* @param {string} str The source string.
|
|
* @return {string} The escaped string.
|
|
*/
|
|
function quoteString(str) {
|
|
return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
|
|
}
|
|
|
|
/**
|
|
* Calls |callback| and stops listening the first time any event in |eventNames|
|
|
* is triggered on |target|.
|
|
* @param {!EventTarget} target
|
|
* @param {!Array<string>|string} eventNames Array or space-delimited string of
|
|
* event names to listen to (e.g. 'click mousedown').
|
|
* @param {function(!Event)} callback Called at most once. The
|
|
* optional return value is passed on by the listener.
|
|
*/
|
|
function listenOnce(target, eventNames, callback) {
|
|
if (!Array.isArray(eventNames))
|
|
eventNames = eventNames.split(/ +/);
|
|
|
|
var removeAllAndCallCallback = function(event) {
|
|
eventNames.forEach(function(eventName) {
|
|
target.removeEventListener(eventName, removeAllAndCallCallback, false);
|
|
});
|
|
return callback(event);
|
|
};
|
|
|
|
eventNames.forEach(function(eventName) {
|
|
target.addEventListener(eventName, removeAllAndCallCallback, false);
|
|
});
|
|
}
|
|
|
|
// /* is_ios */
|
|
|
|
/**
|
|
* Helper to convert callback-based define() API to a promise-based API.
|
|
* @suppress {undefinedVars}
|
|
* @param {!Array<string>} moduleNames
|
|
* @return {!Promise}
|
|
*/
|
|
function importModules(moduleNames) {
|
|
return new Promise(function(resolve) {
|
|
define(moduleNames, function() {
|
|
resolve(Array.from(arguments));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} e
|
|
* @return {boolean} Whether a modifier key was down when processing |e|.
|
|
*/
|
|
function hasKeyModifiers(e) {
|
|
return !!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey);
|
|
}
|