1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5<include src="assert.js"> 6 7/** 8 * Alias for document.getElementById. 9 * @param {string} id The ID of the element to find. 10 * @return {HTMLElement} The found element or null if not found. 11 */ 12function $(id) { 13 return document.getElementById(id); 14} 15 16/** 17 * Add an accessible message to the page that will be announced to 18 * users who have spoken feedback on, but will be invisible to all 19 * other users. It's removed right away so it doesn't clutter the DOM. 20 * @param {string} msg The text to be pronounced. 21 */ 22function announceAccessibleMessage(msg) { 23 var element = document.createElement('div'); 24 element.setAttribute('aria-live', 'polite'); 25 element.style.position = 'relative'; 26 element.style.left = '-9999px'; 27 element.style.height = '0px'; 28 element.innerText = msg; 29 document.body.appendChild(element); 30 window.setTimeout(function() { 31 document.body.removeChild(element); 32 }, 0); 33} 34 35/** 36 * Calls chrome.send with a callback and restores the original afterwards. 37 * @param {string} name The name of the message to send. 38 * @param {!Array} params The parameters to send. 39 * @param {string} callbackName The name of the function that the backend calls. 40 * @param {!Function} callback The function to call. 41 */ 42function chromeSend(name, params, callbackName, callback) { 43 var old = global[callbackName]; 44 global[callbackName] = function() { 45 // restore 46 global[callbackName] = old; 47 48 var args = Array.prototype.slice.call(arguments); 49 return callback.apply(global, args); 50 }; 51 chrome.send(name, params); 52} 53 54/** 55 * Returns the scale factors supported by this platform. 56 * @return {Array} The supported scale factors. 57 */ 58function getSupportedScaleFactors() { 59 var supportedScaleFactors = []; 60 if (cr.isMac || cr.isChromeOS) { 61 supportedScaleFactors.push(1); 62 supportedScaleFactors.push(2); 63 } else { 64 // Windows must be restarted to display at a different scale factor. 65 supportedScaleFactors.push(window.devicePixelRatio); 66 } 67 return supportedScaleFactors; 68} 69 70/** 71 * Generates a CSS url string. 72 * @param {string} s The URL to generate the CSS url for. 73 * @return {string} The CSS url string. 74 */ 75function url(s) { 76 // http://www.w3.org/TR/css3-values/#uris 77 // Parentheses, commas, whitespace characters, single quotes (') and double 78 // quotes (") appearing in a URI must be escaped with a backslash 79 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); 80 // WebKit has a bug when it comes to URLs that end with \ 81 // https://bugs.webkit.org/show_bug.cgi?id=28885 82 if (/\\\\$/.test(s2)) { 83 // Add a space to work around the WebKit bug. 84 s2 += ' '; 85 } 86 return 'url("' + s2 + '")'; 87} 88 89/** 90 * Returns the URL of the image, or an image set of URLs for the profile avatar. 91 * Default avatars have resources available for multiple scalefactors, whereas 92 * the GAIA profile image only comes in one size. 93 * 94 * @param {string} path The path of the image. 95 * @return {string} The url, or an image set of URLs of the avatar image. 96 */ 97function getProfileAvatarIcon(path) { 98 var chromeThemePath = 'chrome://theme'; 99 var isDefaultAvatar = 100 (path.slice(0, chromeThemePath.length) == chromeThemePath); 101 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path); 102} 103 104/** 105 * Generates a CSS -webkit-image-set for a chrome:// url. 106 * An entry in the image set is added for each of getSupportedScaleFactors(). 107 * The scale-factor-specific url is generated by replacing the first instance of 108 * 'scalefactor' in |path| with the numeric scale factor. 109 * @param {string} path The URL to generate an image set for. 110 * 'scalefactor' should be a substring of |path|. 111 * @return {string} The CSS -webkit-image-set. 112 */ 113function imageset(path) { 114 var supportedScaleFactors = getSupportedScaleFactors(); 115 116 var replaceStartIndex = path.indexOf('scalefactor'); 117 if (replaceStartIndex < 0) 118 return url(path); 119 120 var s = ''; 121 for (var i = 0; i < supportedScaleFactors.length; ++i) { 122 var scaleFactor = supportedScaleFactors[i]; 123 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + scaleFactor + 124 path.substr(replaceStartIndex + 'scalefactor'.length); 125 126 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; 127 128 if (i != supportedScaleFactors.length - 1) 129 s += ', '; 130 } 131 return '-webkit-image-set(' + s + ')'; 132} 133 134/** 135 * Parses query parameters from Location. 136 * @param {Location} location The URL to generate the CSS url for. 137 * @return {Object} Dictionary containing name value pairs for URL 138 */ 139function parseQueryParams(location) { 140 var params = {}; 141 var query = unescape(location.search.substring(1)); 142 var vars = query.split('&'); 143 for (var i = 0; i < vars.length; i++) { 144 var pair = vars[i].split('='); 145 params[pair[0]] = pair[1]; 146 } 147 return params; 148} 149 150/** 151 * Creates a new URL by appending or replacing the given query key and value. 152 * Not supporting URL with username and password. 153 * @param {Location} location The original URL. 154 * @param {string} key The query parameter name. 155 * @param {string} value The query parameter value. 156 * @return {string} The constructed new URL. 157 */ 158function setQueryParam(location, key, value) { 159 var query = parseQueryParams(location); 160 query[encodeURIComponent(key)] = encodeURIComponent(value); 161 162 var newQuery = ''; 163 for (var q in query) { 164 newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; 165 } 166 167 return location.origin + location.pathname + newQuery + location.hash; 168} 169 170/** 171 * @param {Element} el An element to search for ancestors with |className|. 172 * @param {string} className A class to search for. 173 * @return {Element} A node with class of |className| or null if none is found. 174 */ 175function findAncestorByClass(el, className) { 176 return /** @type {Element} */(findAncestor(el, function(el) { 177 return el.classList && el.classList.contains(className); 178 })); 179} 180 181/** 182 * Return the first ancestor for which the {@code predicate} returns true. 183 * @param {Node} node The node to check. 184 * @param {function(Node):boolean} predicate The function that tests the 185 * nodes. 186 * @return {Node} The found ancestor or null if not found. 187 */ 188function findAncestor(node, predicate) { 189 var last = false; 190 while (node != null && !(last = predicate(node))) { 191 node = node.parentNode; 192 } 193 return last ? node : null; 194} 195 196function swapDomNodes(a, b) { 197 var afterA = a.nextSibling; 198 if (afterA == b) { 199 swapDomNodes(b, a); 200 return; 201 } 202 var aParent = a.parentNode; 203 b.parentNode.replaceChild(a, b); 204 aParent.insertBefore(b, afterA); 205} 206 207/** 208 * Disables text selection and dragging, with optional whitelist callbacks. 209 * @param {function(Event):boolean=} opt_allowSelectStart Unless this function 210 * is defined and returns true, the onselectionstart event will be 211 * surpressed. 212 * @param {function(Event):boolean=} opt_allowDragStart Unless this function 213 * is defined and returns true, the ondragstart event will be surpressed. 214 */ 215function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { 216 // Disable text selection. 217 document.onselectstart = function(e) { 218 if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) 219 e.preventDefault(); 220 }; 221 222 // Disable dragging. 223 document.ondragstart = function(e) { 224 if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) 225 e.preventDefault(); 226 }; 227} 228 229/** 230 * Call this to stop clicks on <a href="#"> links from scrolling to the top of 231 * the page (and possibly showing a # in the link). 232 */ 233function preventDefaultOnPoundLinkClicks() { 234 document.addEventListener('click', function(e) { 235 var anchor = findAncestor(/** @type {Node} */(e.target), function(el) { 236 return el.tagName == 'A'; 237 }); 238 // Use getAttribute() to prevent URL normalization. 239 if (anchor && anchor.getAttribute('href') == '#') 240 e.preventDefault(); 241 }); 242} 243 244/** 245 * Check the directionality of the page. 246 * @return {boolean} True if Chrome is running an RTL UI. 247 */ 248function isRTL() { 249 return document.documentElement.dir == 'rtl'; 250} 251 252/** 253 * Get an element that's known to exist by its ID. We use this instead of just 254 * calling getElementById and not checking the result because this lets us 255 * satisfy the JSCompiler type system. 256 * @param {string} id The identifier name. 257 * @return {!Element} the Element. 258 */ 259function getRequiredElement(id) { 260 var element = $(id); 261 assert(element, 'Missing required element: ' + id); 262 return /** @type {!Element} */(element); 263} 264 265// Handle click on a link. If the link points to a chrome: or file: url, then 266// call into the browser to do the navigation. 267document.addEventListener('click', function(e) { 268 if (e.defaultPrevented) 269 return; 270 271 var el = e.target; 272 if (el.nodeType == Node.ELEMENT_NODE && 273 el.webkitMatchesSelector('A, A *')) { 274 while (el.tagName != 'A') { 275 el = el.parentElement; 276 } 277 278 if ((el.protocol == 'file:' || el.protocol == 'about:') && 279 (e.button == 0 || e.button == 1)) { 280 chrome.send('navigateToUrl', [ 281 el.href, 282 el.target, 283 e.button, 284 e.altKey, 285 e.ctrlKey, 286 e.metaKey, 287 e.shiftKey 288 ]); 289 e.preventDefault(); 290 } 291 } 292}); 293 294/** 295 * Creates a new URL which is the old URL with a GET param of key=value. 296 * @param {string} url The base URL. There is not sanity checking on the URL so 297 * it must be passed in a proper format. 298 * @param {string} key The key of the param. 299 * @param {string} value The value of the param. 300 * @return {string} The new URL. 301 */ 302function appendParam(url, key, value) { 303 var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); 304 305 if (url.indexOf('?') == -1) 306 return url + '?' + param; 307 return url + '&' + param; 308} 309 310/** 311 * Creates a CSS -webkit-image-set for a favicon request. 312 * @param {string} url The url for the favicon. 313 * @param {number=} opt_size Optional preferred size of the favicon. 314 * @param {string=} opt_type Optional type of favicon to request. Valid values 315 * are 'favicon' and 'touch-icon'. Default is 'favicon'. 316 * @return {string} -webkit-image-set for the favicon. 317 */ 318function getFaviconImageSet(url, opt_size, opt_type) { 319 var size = opt_size || 16; 320 var type = opt_type || 'favicon'; 321 return imageset( 322 'chrome://' + type + '/size/' + size + '@scalefactorx/' + url); 323} 324 325/** 326 * Creates a new URL for a favicon request for the current device pixel ratio. 327 * The URL must be updated when the user moves the browser to a screen with a 328 * different device pixel ratio. Use getFaviconImageSet() for the updating to 329 * occur automatically. 330 * @param {string} url The url for the favicon. 331 * @param {number=} opt_size Optional preferred size of the favicon. 332 * @param {string=} opt_type Optional type of favicon to request. Valid values 333 * are 'favicon' and 'touch-icon'. Default is 'favicon'. 334 * @return {string} Updated URL for the favicon. 335 */ 336function getFaviconUrlForCurrentDevicePixelRatio(url, opt_size, opt_type) { 337 var size = opt_size || 16; 338 var type = opt_type || 'favicon'; 339 return 'chrome://' + type + '/size/' + size + '@' + 340 window.devicePixelRatio + 'x/' + url; 341} 342 343/** 344 * Creates an element of a specified type with a specified class name. 345 * @param {string} type The node type. 346 * @param {string} className The class name to use. 347 * @return {Element} The created element. 348 */ 349function createElementWithClassName(type, className) { 350 var elm = document.createElement(type); 351 elm.className = className; 352 return elm; 353} 354 355/** 356 * webkitTransitionEnd does not always fire (e.g. when animation is aborted 357 * or when no paint happens during the animation). This function sets up 358 * a timer and emulate the event if it is not fired when the timer expires. 359 * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. 360 * @param {number} timeOut The maximum wait time in milliseconds for the 361 * webkitTransitionEnd to happen. 362 */ 363function ensureTransitionEndEvent(el, timeOut) { 364 var fired = false; 365 el.addEventListener('webkitTransitionEnd', function f(e) { 366 el.removeEventListener('webkitTransitionEnd', f); 367 fired = true; 368 }); 369 window.setTimeout(function() { 370 if (!fired) 371 cr.dispatchSimpleEvent(el, 'webkitTransitionEnd'); 372 }, timeOut); 373} 374 375/** 376 * Alias for document.scrollTop getter. 377 * @param {!HTMLDocument} doc The document node where information will be 378 * queried from. 379 * @return {number} The Y document scroll offset. 380 */ 381function scrollTopForDocument(doc) { 382 return doc.documentElement.scrollTop || doc.body.scrollTop; 383} 384 385/** 386 * Alias for document.scrollTop setter. 387 * @param {!HTMLDocument} doc The document node where information will be 388 * queried from. 389 * @param {number} value The target Y scroll offset. 390 */ 391function setScrollTopForDocument(doc, value) { 392 doc.documentElement.scrollTop = doc.body.scrollTop = value; 393} 394 395/** 396 * Alias for document.scrollLeft getter. 397 * @param {!HTMLDocument} doc The document node where information will be 398 * queried from. 399 * @return {number} The X document scroll offset. 400 */ 401function scrollLeftForDocument(doc) { 402 return doc.documentElement.scrollLeft || doc.body.scrollLeft; 403} 404 405/** 406 * Alias for document.scrollLeft setter. 407 * @param {!HTMLDocument} doc The document node where information will be 408 * queried from. 409 * @param {number} value The target X scroll offset. 410 */ 411function setScrollLeftForDocument(doc, value) { 412 doc.documentElement.scrollLeft = doc.body.scrollLeft = value; 413} 414 415/** 416 * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. 417 * @param {string} original The original string. 418 * @return {string} The string with all the characters mentioned above replaced. 419 */ 420function HTMLEscape(original) { 421 return original.replace(/&/g, '&') 422 .replace(/</g, '<') 423 .replace(/>/g, '>') 424 .replace(/"/g, '"') 425 .replace(/'/g, '''); 426} 427 428/** 429 * Shortens the provided string (if necessary) to a string of length at most 430 * |maxLength|. 431 * @param {string} original The original string. 432 * @param {number} maxLength The maximum length allowed for the string. 433 * @return {string} The original string if its length does not exceed 434 * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' 435 * appended. 436 */ 437function elide(original, maxLength) { 438 if (original.length <= maxLength) 439 return original; 440 return original.substring(0, maxLength - 1) + '\u2026'; 441} 442