• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, '&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 */
437function elide(original, maxLength) {
438  if (original.length <= maxLength)
439    return original;
440  return original.substring(0, maxLength - 1) + '\u2026';
441}
442