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