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 */
12var 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 */
19function $(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 */
29function 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 */
49function 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 */
65function 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 */
82function 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 */
104function 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 */
120function 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 */
146function 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 */
165function 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
177function 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 */
192function 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
200function 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 */
219function 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 */
237function 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 */
252function 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 */
263function 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.
271document.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 */
306function 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 */
322function 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 */
340function 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 */
353function 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 */
367function 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 */
385function 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 */
395function 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 */
405function 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 */
415function 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 */
424function 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 */
441function elide(original, maxLength) {
442  if (original.length <= maxLength)
443    return original;
444  return original.substring(0, maxLength - 1) + '\u2026';
445}
446