Add ICU message format support
[chromium-blink-merge.git] / ui / webui / resources / js / util.js
blob63ca9bb1fb0d211d9125cc0cf1d00152a31efed2
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.
5 // <include src="assert.js">
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);
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);
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;
48     var args = Array.prototype.slice.call(arguments);
49     return callback.apply(global, args);
50   };
51   chrome.send(name, params);
54 /**
55  * Returns the scale factors supported by this platform for webui
56  * resources.
57  * @return {Array} The supported scale factors.
58  */
59 function getSupportedScaleFactors() {
60   var supportedScaleFactors = [];
61   if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) {
62     // All desktop platforms support zooming which also updates the
63     // renderer's device scale factors (a.k.a devicePixelRatio), and
64     // these platforms has high DPI assets for 2.0x. Use 1x and 2x in
65     // image-set on these platforms so that the renderer can pick the
66     // closest image for the current device scale factor.
67     supportedScaleFactors.push(1);
68     supportedScaleFactors.push(2);
69   } else {
70     // For other platforms that use fixed device scale factor, use
71     // the window's device pixel ratio.
72     // TODO(oshima): Investigate if Android/iOS need to use image-set.
73     supportedScaleFactors.push(window.devicePixelRatio);
74   }
75   return supportedScaleFactors;
78 /**
79  * Generates a CSS url string.
80  * @param {string} s The URL to generate the CSS url for.
81  * @return {string} The CSS url string.
82  */
83 function url(s) {
84   // http://www.w3.org/TR/css3-values/#uris
85   // Parentheses, commas, whitespace characters, single quotes (') and double
86   // quotes (") appearing in a URI must be escaped with a backslash
87   var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
88   // WebKit has a bug when it comes to URLs that end with \
89   // https://bugs.webkit.org/show_bug.cgi?id=28885
90   if (/\\\\$/.test(s2)) {
91     // Add a space to work around the WebKit bug.
92     s2 += ' ';
93   }
94   return 'url("' + s2 + '")';
97 /**
98  * Returns the URL of the image, or an image set of URLs for the profile avatar.
99  * Default avatars have resources available for multiple scalefactors, whereas
100  * the GAIA profile image only comes in one size.
102  * @param {string} path The path of the image.
103  * @return {string} The url, or an image set of URLs of the avatar image.
104  */
105 function getProfileAvatarIcon(path) {
106   var chromeThemePath = 'chrome://theme';
107   var isDefaultAvatar =
108       (path.slice(0, chromeThemePath.length) == chromeThemePath);
109   return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path);
113  * Generates a CSS -webkit-image-set for a chrome:// url.
114  * An entry in the image set is added for each of getSupportedScaleFactors().
115  * The scale-factor-specific url is generated by replacing the first instance of
116  * 'scalefactor' in |path| with the numeric scale factor.
117  * @param {string} path The URL to generate an image set for.
118  *     'scalefactor' should be a substring of |path|.
119  * @return {string} The CSS -webkit-image-set.
120  */
121 function imageset(path) {
122   var supportedScaleFactors = getSupportedScaleFactors();
124   var replaceStartIndex = path.indexOf('scalefactor');
125   if (replaceStartIndex < 0)
126     return url(path);
128   var s = '';
129   for (var i = 0; i < supportedScaleFactors.length; ++i) {
130     var scaleFactor = supportedScaleFactors[i];
131     var pathWithScaleFactor = path.substr(0, replaceStartIndex) + scaleFactor +
132         path.substr(replaceStartIndex + 'scalefactor'.length);
134     s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x';
136     if (i != supportedScaleFactors.length - 1)
137       s += ', ';
138   }
139   return '-webkit-image-set(' + s + ')';
143  * Parses query parameters from Location.
144  * @param {Location} location The URL to generate the CSS url for.
145  * @return {Object} Dictionary containing name value pairs for URL
146  */
147 function parseQueryParams(location) {
148   var params = {};
149   var query = unescape(location.search.substring(1));
150   var vars = query.split('&');
151   for (var i = 0; i < vars.length; i++) {
152     var pair = vars[i].split('=');
153     params[pair[0]] = pair[1];
154   }
155   return params;
159  * Creates a new URL by appending or replacing the given query key and value.
160  * Not supporting URL with username and password.
161  * @param {Location} location The original URL.
162  * @param {string} key The query parameter name.
163  * @param {string} value The query parameter value.
164  * @return {string} The constructed new URL.
165  */
166 function setQueryParam(location, key, value) {
167   var query = parseQueryParams(location);
168   query[encodeURIComponent(key)] = encodeURIComponent(value);
170   var newQuery = '';
171   for (var q in query) {
172     newQuery += (newQuery ? '&' : '?') + q + '=' + query[q];
173   }
175   return location.origin + location.pathname + newQuery + location.hash;
179  * @param {Node} el A node to search for ancestors with |className|.
180  * @param {string} className A class to search for.
181  * @return {Element} A node with class of |className| or null if none is found.
182  */
183 function findAncestorByClass(el, className) {
184   return /** @type {Element} */(findAncestor(el, function(el) {
185     return el.classList && el.classList.contains(className);
186   }));
190  * Return the first ancestor for which the {@code predicate} returns true.
191  * @param {Node} node The node to check.
192  * @param {function(Node):boolean} predicate The function that tests the
193  *     nodes.
194  * @return {Node} The found ancestor or null if not found.
195  */
196 function findAncestor(node, predicate) {
197   var last = false;
198   while (node != null && !(last = predicate(node))) {
199     node = node.parentNode;
200   }
201   return last ? node : null;
204 function swapDomNodes(a, b) {
205   var afterA = a.nextSibling;
206   if (afterA == b) {
207     swapDomNodes(b, a);
208     return;
209   }
210   var aParent = a.parentNode;
211   b.parentNode.replaceChild(a, b);
212   aParent.insertBefore(b, afterA);
216  * Disables text selection and dragging, with optional whitelist callbacks.
217  * @param {function(Event):boolean=} opt_allowSelectStart Unless this function
218  *    is defined and returns true, the onselectionstart event will be
219  *    surpressed.
220  * @param {function(Event):boolean=} opt_allowDragStart Unless this function
221  *    is defined and returns true, the ondragstart event will be surpressed.
222  */
223 function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) {
224   // Disable text selection.
225   document.onselectstart = function(e) {
226     if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e)))
227       e.preventDefault();
228   };
230   // Disable dragging.
231   document.ondragstart = function(e) {
232     if (!(opt_allowDragStart && opt_allowDragStart.call(this, e)))
233       e.preventDefault();
234   };
238  * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead.
239  * Call this to stop clicks on <a href="#"> links from scrolling to the top of
240  * the page (and possibly showing a # in the link).
241  */
242 function preventDefaultOnPoundLinkClicks() {
243   document.addEventListener('click', function(e) {
244     var anchor = findAncestor(/** @type {Node} */(e.target), function(el) {
245       return el.tagName == 'A';
246     });
247     // Use getAttribute() to prevent URL normalization.
248     if (anchor && anchor.getAttribute('href') == '#')
249       e.preventDefault();
250   });
254  * Check the directionality of the page.
255  * @return {boolean} True if Chrome is running an RTL UI.
256  */
257 function isRTL() {
258   return document.documentElement.dir == 'rtl';
262  * Get an element that's known to exist by its ID. We use this instead of just
263  * calling getElementById and not checking the result because this lets us
264  * satisfy the JSCompiler type system.
265  * @param {string} id The identifier name.
266  * @return {!HTMLElement} the Element.
267  */
268 function getRequiredElement(id) {
269   return assertInstanceof($(id), HTMLElement,
270                           'Missing required element: ' + id);
274  * Query an element that's known to exist by a selector. We use this instead of
275  * just calling querySelector and not checking the result because this lets us
276  * satisfy the JSCompiler type system.
277  * @param {(!Document|!DocumentFragment|!Element)} context The context object
278  *     for querySelector.
279  * @param {string} selectors CSS selectors to query the element.
280  * @return {!HTMLElement} the Element.
281  */
282 function queryRequiredElement(context, selectors) {
283   var element = context.querySelector(selectors);
284   return assertInstanceof(element, HTMLElement,
285                           'Missing required element: ' + selectors);
288 // Handle click on a link. If the link points to a chrome: or file: url, then
289 // call into the browser to do the navigation.
290 document.addEventListener('click', function(e) {
291   if (e.defaultPrevented)
292     return;
294   var el = e.target;
295   if (el.nodeType == Node.ELEMENT_NODE &&
296       el.webkitMatchesSelector('A, A *')) {
297     while (el.tagName != 'A') {
298       el = el.parentElement;
299     }
301     if ((el.protocol == 'file:' || el.protocol == 'about:') &&
302         (e.button == 0 || e.button == 1)) {
303       chrome.send('navigateToUrl', [
304         el.href,
305         el.target,
306         e.button,
307         e.altKey,
308         e.ctrlKey,
309         e.metaKey,
310         e.shiftKey
311       ]);
312       e.preventDefault();
313     }
314   }
318  * Creates a new URL which is the old URL with a GET param of key=value.
319  * @param {string} url The base URL. There is not sanity checking on the URL so
320  *     it must be passed in a proper format.
321  * @param {string} key The key of the param.
322  * @param {string} value The value of the param.
323  * @return {string} The new URL.
324  */
325 function appendParam(url, key, value) {
326   var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
328   if (url.indexOf('?') == -1)
329     return url + '?' + param;
330   return url + '&' + param;
334  * Creates a CSS -webkit-image-set for a favicon request.
335  * @param {string} url The url for the favicon.
336  * @param {number=} opt_size Optional preferred size of the favicon.
337  * @param {string=} opt_type Optional type of favicon to request. Valid values
338  *     are 'favicon' and 'touch-icon'. Default is 'favicon'.
339  * @return {string} -webkit-image-set for the favicon.
340  */
341 function getFaviconImageSet(url, opt_size, opt_type) {
342   var size = opt_size || 16;
343   var type = opt_type || 'favicon';
344   return imageset(
345       'chrome://' + type + '/size/' + size + '@scalefactorx/' + url);
349  * Creates a new URL for a favicon request for the current device pixel ratio.
350  * The URL must be updated when the user moves the browser to a screen with a
351  * different device pixel ratio. Use getFaviconImageSet() for the updating to
352  * occur automatically.
353  * @param {string} url The url for the favicon.
354  * @param {number=} opt_size Optional preferred size of the favicon.
355  * @param {string=} opt_type Optional type of favicon to request. Valid values
356  *     are 'favicon' and 'touch-icon'. Default is 'favicon'.
357  * @return {string} Updated URL for the favicon.
358  */
359 function getFaviconUrlForCurrentDevicePixelRatio(url, opt_size, opt_type) {
360   var size = opt_size || 16;
361   var type = opt_type || 'favicon';
362   return 'chrome://' + type + '/size/' + size + '@' +
363       window.devicePixelRatio + 'x/' + url;
367  * Creates an element of a specified type with a specified class name.
368  * @param {string} type The node type.
369  * @param {string} className The class name to use.
370  * @return {Element} The created element.
371  */
372 function createElementWithClassName(type, className) {
373   var elm = document.createElement(type);
374   elm.className = className;
375   return elm;
379  * webkitTransitionEnd does not always fire (e.g. when animation is aborted
380  * or when no paint happens during the animation). This function sets up
381  * a timer and emulate the event if it is not fired when the timer expires.
382  * @param {!HTMLElement} el The element to watch for webkitTransitionEnd.
383  * @param {number} timeOut The maximum wait time in milliseconds for the
384  *     webkitTransitionEnd to happen.
385  */
386 function ensureTransitionEndEvent(el, timeOut) {
387   var fired = false;
388   el.addEventListener('webkitTransitionEnd', function f(e) {
389     el.removeEventListener('webkitTransitionEnd', f);
390     fired = true;
391   });
392   window.setTimeout(function() {
393     if (!fired)
394       cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true);
395   }, timeOut);
399  * Alias for document.scrollTop getter.
400  * @param {!HTMLDocument} doc The document node where information will be
401  *     queried from.
402  * @return {number} The Y document scroll offset.
403  */
404 function scrollTopForDocument(doc) {
405   return doc.documentElement.scrollTop || doc.body.scrollTop;
409  * Alias for document.scrollTop setter.
410  * @param {!HTMLDocument} doc The document node where information will be
411  *     queried from.
412  * @param {number} value The target Y scroll offset.
413  */
414 function setScrollTopForDocument(doc, value) {
415   doc.documentElement.scrollTop = doc.body.scrollTop = value;
419  * Alias for document.scrollLeft getter.
420  * @param {!HTMLDocument} doc The document node where information will be
421  *     queried from.
422  * @return {number} The X document scroll offset.
423  */
424 function scrollLeftForDocument(doc) {
425   return doc.documentElement.scrollLeft || doc.body.scrollLeft;
429  * Alias for document.scrollLeft setter.
430  * @param {!HTMLDocument} doc The document node where information will be
431  *     queried from.
432  * @param {number} value The target X scroll offset.
433  */
434 function setScrollLeftForDocument(doc, value) {
435   doc.documentElement.scrollLeft = doc.body.scrollLeft = value;
439  * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding.
440  * @param {string} original The original string.
441  * @return {string} The string with all the characters mentioned above replaced.
442  */
443 function HTMLEscape(original) {
444   return original.replace(/&/g, '&amp;')
445                  .replace(/</g, '&lt;')
446                  .replace(/>/g, '&gt;')
447                  .replace(/"/g, '&quot;')
448                  .replace(/'/g, '&#39;');
452  * Shortens the provided string (if necessary) to a string of length at most
453  * |maxLength|.
454  * @param {string} original The original string.
455  * @param {number} maxLength The maximum length allowed for the string.
456  * @return {string} The original string if its length does not exceed
457  *     |maxLength|. Otherwise the first |maxLength| - 1 characters with '...'
458  *     appended.
459  */
460 function elide(original, maxLength) {
461   if (original.length <= maxLength)
462     return original;
463   return original.substring(0, maxLength - 1) + '\u2026';