Home | History | Annotate | Download | only in js
      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  */
     12 function $(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  */
     22 function 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  */
     42 function 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  */
     58 function 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  */
     75 function 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  */
     97 function 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  */
    113 function 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  */
    139 function 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  */
    158 function 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  */
    175 function 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  */
    188 function 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 
    196 function 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  */
    215 function 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  */
    233 function 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  */
    248 function 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  */
    259 function 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.
    267 document.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  */
    302 function 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  */
    318 function 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  */
    336 function 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  */
    349 function 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  */
    363 function 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  */
    381 function 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  */
    391 function 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  */
    401 function 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  */
    411 function 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  */
    420 function HTMLEscape(original) {
    421   return original.replace(/&/g, '&amp;')
    422                  .replace(/</g, '&lt;')
    423                  .replace(/>/g, '&gt;')
    424                  .replace(/"/g, '&quot;')
    425                  .replace(/'/g, '&#39;');
    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  */
    437 function elide(original, maxLength) {
    438   if (original.length <= maxLength)
    439     return original;
    440   return original.substring(0, maxLength - 1) + '\u2026';
    441 }
    442