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  * 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, '&amp;')
    426                  .replace(/</g, '&lt;')
    427                  .replace(/>/g, '&gt;')
    428                  .replace(/"/g, '&quot;')
    429                  .replace(/'/g, '&#39;');
    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