// 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. // /** * 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 getUrlForCss(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, '''); } /** * 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} 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); }); } // // Polyfill 'key' in KeyboardEvent for iOS. // This function is not intended to be complete but should // be sufficient enough to have iOS work correctly while // it does not support key yet. if (!('key' in KeyboardEvent.prototype)) { Object.defineProperty(KeyboardEvent.prototype, 'key', { /** @this {KeyboardEvent} */ get: function() { // 0-9 if (this.keyCode >= 0x30 && this.keyCode <= 0x39) return String.fromCharCode(this.keyCode); // A-Z if (this.keyCode >= 0x41 && this.keyCode <= 0x5a) { var result = String.fromCharCode(this.keyCode).toLowerCase(); if (this.shiftKey) result = result.toUpperCase(); return result; } // Special characters switch (this.keyCode) { case 0x08: return 'Backspace'; case 0x09: return 'Tab'; case 0x0d: return 'Enter'; case 0x10: return 'Shift'; case 0x11: return 'Control'; case 0x12: return 'Alt'; case 0x1b: return 'Escape'; case 0x20: return ' '; case 0x21: return 'PageUp'; case 0x22: return 'PageDown'; case 0x23: return 'End'; case 0x24: return 'Home'; case 0x25: return 'ArrowLeft'; case 0x26: return 'ArrowUp'; case 0x27: return 'ArrowRight'; case 0x28: return 'ArrowDown'; case 0x2d: return 'Insert'; case 0x2e: return 'Delete'; case 0x5b: return 'Meta'; case 0x70: return 'F1'; case 0x71: return 'F2'; case 0x72: return 'F3'; case 0x73: return 'F4'; case 0x74: return 'F5'; case 0x75: return 'F6'; case 0x76: return 'F7'; case 0x77: return 'F8'; case 0x78: return 'F9'; case 0x79: return 'F10'; case 0x7a: return 'F11'; case 0x7b: return 'F12'; case 0xbb: return '='; case 0xbd: return '-'; case 0xdb: return '['; case 0xdd: return ']'; } return 'Unidentified'; } }); } else { window.console.log('KeyboardEvent.Key polyfill not required'); } // /* is_ios */ /** * @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); }