Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / remoting / webapp / base / js / base.js
blob602d12915ca8e2ebf21b34db21abf66fb375156a
1 // Copyright 2014 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 /**
6  * @fileoverview
7  * A module that contains basic utility components and methods for the
8  * chromoting project
9  *
10  */
12 'use strict';
14 /** @suppress {duplicate} */
15 var base = base || {};
17 /**
18  * @interface
19  */
20 base.Disposable = function() {};
21 base.Disposable.prototype.dispose = function() {};
23 /**
24  * @constructor
25  * @param {...base.Disposable} var_args
26  * @implements {base.Disposable}
27  * @suppress {reportUnknownTypes}
28  */
29 base.Disposables = function(var_args) {
30   /**
31    * @type {Array<base.Disposable>}
32    * @private
33    */
34   this.disposables_ = Array.prototype.slice.call(arguments, 0);
37 /**
38  * @param {...base.Disposable} var_args
39  * @suppress {reportUnknownTypes}
40  */
41 base.Disposables.prototype.add = function(var_args) {
42   var disposables = Array.prototype.slice.call(arguments, 0);
43   for (var i = 0; i < disposables.length; i++) {
44     var current = /** @type {base.Disposable} */ (disposables[i]);
45     if (this.disposables_.indexOf(current) === -1) {
46       this.disposables_.push(current);
47     }
48   }
51 /**
52  * @param {...base.Disposable} var_args  Dispose |var_args| and remove
53  *    them from the current object.
54  * @suppress {reportUnknownTypes}
55  */
56 base.Disposables.prototype.remove = function(var_args) {
57   var disposables = Array.prototype.slice.call(arguments, 0);
58   for (var i = 0; i < disposables.length; i++) {
59     var disposable = /** @type {base.Disposable} */ (disposables[i]);
60     var index = this.disposables_.indexOf(disposable);
61     if(index !== -1) {
62       this.disposables_.splice(index, 1);
63       disposable.dispose();
64     }
65   }
68 base.Disposables.prototype.dispose = function() {
69   for (var i = 0; i < this.disposables_.length; i++) {
70     this.disposables_[i].dispose();
71   }
72   this.disposables_ = null;
75 /**
76  * A utility function to invoke |obj|.dispose without a null check on |obj|.
77  * @param {base.Disposable} obj
78  */
79 base.dispose = function(obj) {
80   if (obj) {
81     console.assert(typeof obj.dispose == 'function',
82                    'dispose() should have type function, not ' +
83                    typeof obj.dispose + '.');
84     obj.dispose();
85   }
88 /**
89  * Copy all properties from src to dest.
90  * @param {Object} dest
91  * @param {Object} src
92  */
93 base.mix = function(dest, src) {
94   for (var prop in src) {
95     if (src.hasOwnProperty(prop) && !(prop in dest)) {
96       dest[prop] = src[prop];
97     }
98   }
102  * Adds a mixin to a class.
103  * @param {Object} dest
104  * @param {Object} src
105  * @suppress {checkTypes|reportUnknownTypes}
106  */
107 base.extend = function(dest, src) {
108   base.mix(dest.prototype, src.prototype || src);
112  * Inherits properties and methods from |parentCtor| at object construction time
113  * using prototypical inheritance. e.g.
115  * var ParentClass = function(parentArg) {
116  *   this.parentProperty = parentArg;
117  * }
119  * var ChildClass = function() {
120  *   base.inherits(this, ParentClass, 'parentArg'); // must be the first line.
121  * }
123  * var child = new ChildClass();
124  * child instanceof ParentClass // true
126  * See base_inherits_unittest.js for the guaranteed behavior of base.inherits().
127  * This lazy approach is chosen so that it is not necessary to maintain proper
128  * script loading order between the parent class and the child class.
130  * @param {*} childObject
131  * @param {*} parentCtor
132  * @param {...} parentCtorArgs
133  * @suppress {checkTypes|reportUnknownTypes}
134  */
135 base.inherits = function(childObject, parentCtor, parentCtorArgs) {
136   console.assert(parentCtor && parentCtor.prototype,
137                  'Invalid parent constructor.');
138   var parentArgs = Array.prototype.slice.call(arguments, 2);
140   // Mix in the parent's prototypes so that they're available during the parent
141   // ctor.
142   base.mix(childObject, parentCtor.prototype);
143   parentCtor.apply(childObject, parentArgs);
145   // Note that __proto__ is deprecated.
146   //   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
147   //   Global_Objects/Object/proto.
148   // It is used so that childObject instanceof parentCtor will
149   // return true.
150   childObject.__proto__.__proto__ = parentCtor.prototype;
151   console.assert(childObject instanceof parentCtor,
152                  'child is not an instance of parent.');
155 base.doNothing = function() {};
158  * Returns an array containing the values of |dict|.
159  * @param {!Object} dict
160  * @return {Array}
161  */
162 base.values = function(dict) {
163   return Object.keys(dict).map(
164     /** @param {string} key */
165     function(key) {
166       return dict[key];
167     });
171  * @param {*} value
172  * @return {*} a recursive copy of |value| or null if |value| is not copyable
173  *   (e.g. undefined, NaN).
174  */
175 base.deepCopy = function(value) {
176   try {
177     return JSON.parse(JSON.stringify(value));
178   } catch (e) {}
179   return null;
183  * Returns a copy of the input object with all null/undefined fields
184  * removed.  Returns an empty object for a null/undefined input.
186  * @param {Object<?T>|undefined} input
187  * @return {!Object<T>}
188  * @template T
189  */
190 base.copyWithoutNullFields = function(input) {
191   /** @const {!Object} */
192   var result = {};
193   if (input) {
194     for (var field in input) {
195       var value = /** @type {*} */ (input[field]);
196       if (value != null) {
197         result[field] = value;
198       }
199     }
200   }
201   return result;
205  * @param {!Object} object
206  * @return {boolean} True if the object is empty (equal to {}); false otherwise.
207  */
208 base.isEmptyObject = function(object) {
209   return Object.keys(object).length === 0;
213  * @type {boolean|undefined}
214  * @private
215  */
216 base.isAppsV2_ = undefined;
219  * @return {boolean} True if this is a v2 app; false if it is a legacy app.
220  */
221 base.isAppsV2 = function() {
222   if (base.isAppsV2_ === undefined) {
223     var manifest = chrome.runtime.getManifest();
224     base.isAppsV2_ =
225         Boolean(manifest && manifest.app && manifest.app.background);
226   }
227   return base.isAppsV2_;
231  * Joins the |url| with optional query parameters defined in |opt_params|
232  * See unit test for usage.
233  * @param {string} url
234  * @param {Object<string>=} opt_params
235  * @return {string}
236  */
237 base.urlJoin = function(url, opt_params) {
238   if (!opt_params) {
239     return url;
240   }
241   var queryParameters = [];
242   for (var key in opt_params) {
243     queryParameters.push(encodeURIComponent(key) + "=" +
244                          encodeURIComponent(opt_params[key]));
245   }
246   return url + '?' + queryParameters.join('&');
251  * @return {Object<string>} The URL parameters.
252  */
253 base.getUrlParameters = function() {
254   var result = {};
255   var parts = window.location.search.substring(1).split('&');
256   for (var i = 0; i < parts.length; i++) {
257     var pair = parts[i].split('=');
258     result[pair[0]] = decodeURIComponent(pair[1]);
259   }
260   return result;
264  * Convert special characters (e.g. &, < and >) to HTML entities.
266  * @param {string} str
267  * @return {string}
268  */
269 base.escapeHTML = function(str) {
270   var div = document.createElement('div');
271   div.appendChild(document.createTextNode(str));
272   return div.innerHTML;
276  * Promise is a great tool for writing asynchronous code. However, the construct
277  *   var p = new promise(function init(resolve, reject) {
278  *     ... // code that fulfills the Promise.
279  *   });
280  * forces the Promise-resolving logic to reside in the |init| function
281  * of the constructor.  This is problematic when you need to resolve the
282  * Promise in a member function(which is quite common for event callbacks).
284  * base.Deferred comes to the rescue.  It encapsulates a Promise
285  * object and exposes member methods (resolve/reject) to fulfill it.
287  * Here are the recommended steps to follow when implementing an asynchronous
288  * function that returns a Promise:
289  * 1. Create a deferred object by calling
290  *      var deferred = new base.Deferred();
291  * 2. Call deferred.resolve() when the asynchronous operation finishes.
292  * 3. Call deferred.reject() when the asynchronous operation fails.
293  * 4. Return deferred.promise() to the caller so that it can subscribe
294  *    to status changes using the |then| handler.
296  * Sample Usage:
297  *  function myAsyncAPI() {
298  *    var deferred = new base.Deferred();
299  *    window.setTimeout(function() {
300  *      deferred.resolve();
301  *    }, 100);
302  *    return deferred.promise();
303  *  };
305  * @constructor
306  * @template T
307  */
308 base.Deferred = function() {
309   /**
310    * @private {?function(?):void}
311    */
312   this.resolve_ = null;
314   /**
315    * @private {?function(?):void}
316    */
317   this.reject_ = null;
319   /**
320    * @this {base.Deferred}
321    * @param {function(?):void} resolve
322    * @param {function(*):void} reject
323    */
324   var initPromise = function(resolve, reject) {
325     this.resolve_ = resolve;
326     this.reject_ = reject;
327   };
329   /**
330    * @private {!Promise<T>}
331    */
332   this.promise_ = new Promise(initPromise.bind(this));
335 /** @param {*} reason */
336 base.Deferred.prototype.reject = function(reason) {
337   this.reject_(reason);
340 /** @param {*=} opt_value */
341 base.Deferred.prototype.resolve = function(opt_value) {
342   this.resolve_(opt_value);
345 /** @return {!Promise<T>} */
346 base.Deferred.prototype.promise = function() {
347   return this.promise_;
350 base.Promise = function() {};
353  * @param {number} delay
354  * @param {*=} opt_value
355  * @return {!Promise} a Promise that will be fulfilled with |opt_value|
356  *     after |delay| ms.
357  */
358 base.Promise.sleep = function(delay, opt_value) {
359   return new Promise(
360     function(resolve) {
361       window.setTimeout(function() {
362         resolve(opt_value);
363       }, delay);
364     });
368  * @param {Promise} promise
369  * @return {Promise} a Promise that will be fulfilled iff the specified Promise
370  *     is rejected.
371  */
372 base.Promise.negate = function(promise) {
373   return promise.then(
374       /** @return {Promise} */
375       function() {
376         return Promise.reject();
377       },
378       /** @return {Promise} */
379       function() {
380         return Promise.resolve();
381       });
385  * Creates a promise that will be fulfilled within a certain timeframe.
387  * This function creates a result promise |R| that will be resolved to
388  * either |promise| or |opt_defaultValue|.  If |promise| is fulfulled
389  * (i.e. resolved or rejected) within |delay| milliseconds, then |R|
390  * is resolved with |promise|.  Otherwise, |R| is resolved with
391  * |opt_defaultValue|.
393  * Avoid passing a promise as |opt_defaultValue|, as this could result
394  * in |R| remaining unfulfilled after |delay| milliseconds.
396  * @param {!Promise<T>} promise The promise to wrap.
397  * @param {number} delay The number of milliseconds to wait.
398  * @param {*=} opt_defaultValue The default value used to resolve the
399  *     result.
400  * @return {!Promise<T>} A new promise.
401  * @template T
402  */
403 base.Promise.withTimeout = function(promise, delay, opt_defaultValue) {
404   return Promise.race([promise, base.Promise.sleep(delay, opt_defaultValue)]);
408  * Converts a |method| with callbacks into a Promise.
410  * @param {Function} method
411  * @param {Array} params
412  * @param {*=} opt_context
413  * @param {boolean=} opt_hasErrorHandler whether the method has an error handler
414  * @return {Promise}
415  */
416 base.Promise.as = function(method, params, opt_context, opt_hasErrorHandler) {
417   return new Promise(function(resolve, reject) {
418     params.push(resolve);
419     if (opt_hasErrorHandler) {
420       params.push(reject);
421     }
422     try {
423       method.apply(opt_context, params);
424     } catch (/** @type {*} */ e) {
425       reject(e);
426     }
427   });
431  * A mixin for classes with events.
433  * For example, to create an alarm event for SmokeDetector:
434  * functionSmokeDetector() {
435  *    base.inherits(this, base.EventSourceImpl);
436  *    this.defineEvents(['alarm']);
437  * };
439  * To fire an event:
440  * SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
441  *   var param = {} // optional parameters
442  *   this.raiseEvent('alarm', param);
443  * }
445  * To listen to an event:
446  * var smokeDetector = new SmokeDetector();
447  * smokeDetector.addEventListener('alarm', listenerObj.someCallback)
449  */
452   * Helper interface for the EventSource.
453   * @constructor
454   */
455 base.EventEntry = function() {
456   /** @type {Array<function():void>} */
457   this.listeners = [];
461 /** @interface */
462 base.EventSource = function() {};
464  /**
465   * Add a listener |fn| to listen to |type| event.
466   * @param {string} type
467   * @param {Function} fn
468   */
469 base.EventSource.prototype.addEventListener = function(type, fn) {};
471  /**
472   * Remove a listener |fn| to listen to |type| event.
473   * @param {string} type
474   * @param {Function} fn
475   */
476 base.EventSource.prototype.removeEventListener = function(type, fn) {};
480   * @constructor
481   * Since this class is implemented as a mixin, the constructor may not be
482   * called.  All initializations should be done in defineEvents.
483   * @implements {base.EventSource}
484   */
485 base.EventSourceImpl = function() {
486   /** @type {Object<base.EventEntry>} */
487   this.eventMap_;
491   * @param {base.EventSourceImpl} obj
492   * @param {string} type
493   * @private
494   */
495 base.EventSourceImpl.assertHasEvent_ = function(obj, type) {
496   console.assert(Boolean(obj.eventMap_),
497                  "The object doesn't support events.");
498   console.assert(Boolean(obj.eventMap_[type]),
499                  'Event <' + type +'> is undefined for the current object.');
502 base.EventSourceImpl.prototype = {
503   /**
504     * Define |events| for this event source.
505     * @param {Array<string>} events
506     */
507   defineEvents: function(events) {
508     console.assert(!Boolean(this.eventMap_),
509                    'defineEvents() can only be called once.');
510     this.eventMap_ = {};
511     events.forEach(
512       /**
513         * @this {base.EventSourceImpl}
514         * @param {string} type
515         */
516       function(type) {
517         console.assert(typeof type == 'string',
518                        'Event name must be a string; found ' + type + '.');
519         this.eventMap_[type] = new base.EventEntry();
520     }, this);
521   },
523   /**
524     * @param {string} type
525     * @param {Function} fn
526     */
527   addEventListener: function(type, fn) {
528     console.assert(typeof fn == 'function',
529                    'addEventListener(): event listener for ' + type +
530                    ' must be function, not ' + typeof fn + '.');
531     base.EventSourceImpl.assertHasEvent_(this, type);
533     var listeners = this.eventMap_[type].listeners;
534     listeners.push(fn);
535   },
537   /**
538     * @param {string} type
539     * @param {Function} fn
540     */
541   removeEventListener: function(type, fn) {
542     console.assert(typeof fn == 'function',
543                    'removeEventListener(): event listener for ' + type +
544                    ' must be function, not ' + typeof fn + '.');
545     base.EventSourceImpl.assertHasEvent_(this, type);
547     var listeners = this.eventMap_[type].listeners;
548     // find the listener to remove.
549     for (var i = 0; i < listeners.length; i++) {
550       var listener = listeners[i];
551       if (listener == fn) {
552         listeners.splice(i, 1);
553         break;
554       }
555     }
556   },
558   /**
559     * Fire an event of a particular type on this object.
560     * @param {string} type
561     * @param {*=} opt_details The type of |opt_details| should be ?= to
562     *     match what is defined in add(remove)EventListener.  However, JSCompile
563     *     cannot handle invoking an unknown type as an argument to |listener|
564     *     As a hack, we set the type to *=.
565     */
566   raiseEvent: function(type, opt_details) {
567     base.EventSourceImpl.assertHasEvent_(this, type);
569     var entry = this.eventMap_[type];
570     var listeners = entry.listeners.slice(0); // Make a copy of the listeners.
572     listeners.forEach(
573       /** @param {function(*=):void} listener */
574       function(listener){
575         if (listener) {
576           listener(opt_details);
577         }
578     });
579   }
584   * A lightweight object that helps manage the lifetime of an event listener.
585   *
586   * For example, do the following if you want to automatically unhook events
587   * when your object is disposed:
588   *
589   * var MyConstructor = function(domElement) {
590   *   this.eventHooks_ = new base.Disposables(
591   *     new base.EventHook(domElement, 'click', this.onClick_.bind(this)),
592   *     new base.EventHook(domElement, 'keydown', this.onClick_.bind(this)),
593   *     new base.ChromeEventHook(chrome.runtime.onMessage,
594   *                              this.onMessage_.bind(this))
595   *   );
596   * }
597   *
598   * MyConstructor.prototype.dispose = function() {
599   *   this.eventHooks_.dispose();
600   *   this.eventHooks_ = null;
601   * }
602   *
603   * @param {base.EventSource} src
604   * @param {string} eventName
605   * @param {Function} listener
606   *
607   * @constructor
608   * @implements {base.Disposable}
609   */
610 base.EventHook = function(src, eventName, listener) {
611   this.src_ = src;
612   this.eventName_ = eventName;
613   this.listener_ = listener;
614   src.addEventListener(eventName, listener);
617 base.EventHook.prototype.dispose = function() {
618   this.src_.removeEventListener(this.eventName_, this.listener_);
622   * An event hook implementation for DOM Events.
623   *
624   * @param {HTMLElement|Element|Window|HTMLDocument} src
625   * @param {string} eventName
626   * @param {Function} listener
627   * @param {boolean} capture
628   *
629   * @constructor
630   * @implements {base.Disposable}
631   */
632 base.DomEventHook = function(src, eventName, listener, capture) {
633   this.src_ = src;
634   this.eventName_ = eventName;
635   this.listener_ = listener;
636   this.capture_ = capture;
637   src.addEventListener(eventName, listener, capture);
640 base.DomEventHook.prototype.dispose = function() {
641   this.src_.removeEventListener(this.eventName_, this.listener_, this.capture_);
646   * An event hook implementation for Chrome Events.
647   *
648   * @param {ChromeEvent|chrome.contextMenus.ClickedEvent|ChromeObjectEvent} src
649   * @param {!Function} listener
650   *
651   * @constructor
652   * @implements {base.Disposable}
653   */
654 base.ChromeEventHook = function(src, listener) {
655   this.src_ = src;
656   this.listener_ = listener;
657   src.addListener(listener);
660 base.ChromeEventHook.prototype.dispose = function() {
661   this.src_.removeListener(this.listener_);
665  * A disposable repeating timer.
667  * @param {Function} callback
668  * @param {number} interval
669  * @param {boolean=} opt_invokeNow Whether to invoke the callback now, default
670  *    to false.
672  * @constructor
673  * @implements {base.Disposable}
674  */
675 base.RepeatingTimer = function(callback, interval, opt_invokeNow) {
676   /** @private */
677   this.intervalId_ = window.setInterval(callback, interval);
678   if (opt_invokeNow) {
679     callback();
680   }
683 base.RepeatingTimer.prototype.dispose = function() {
684   window.clearInterval(this.intervalId_);
685   this.intervalId_ = null;
689  * A disposable one shot timer.
691  * @param {Function} callback
692  * @param {number} timeout
694  * @constructor
695  * @implements {base.Disposable}
696  */
697 base.OneShotTimer = function(callback, timeout) {
698   var that = this;
700   /** @private */
701   this.timerId_ = window.setTimeout(function() {
702     that.timerId_ = null;
703     callback();
704   }, timeout);
707 base.OneShotTimer.prototype.dispose = function() {
708   if (this.timerId_ !== null) {
709     window.clearTimeout(this.timerId_);
710     this.timerId_ = null;
711   }
715   * Converts UTF-8 string to ArrayBuffer.
716   *
717   * @param {string} string
718   * @return {ArrayBuffer}
719   */
720 base.encodeUtf8 = function(string) {
721   var utf8String = unescape(encodeURIComponent(string));
722   var result = new Uint8Array(utf8String.length);
723   for (var i = 0; i < utf8String.length; i++)
724     result[i] = utf8String.charCodeAt(i);
725   return result.buffer;
729   * Decodes UTF-8 string from ArrayBuffer.
730   *
731   * @param {ArrayBuffer} buffer
732   * @return {string}
733   */
734 base.decodeUtf8 = function(buffer) {
735   return decodeURIComponent(
736       escape(String.fromCharCode.apply(null, new Uint8Array(buffer))));
740  * Generate a nonce, to be used as an xsrf protection token.
742  * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
743 base.generateXsrfToken = function() {
744   var random = new Uint8Array(16);
745   window.crypto.getRandomValues(random);
746   var base64Token = window.btoa(String.fromCharCode.apply(null, random));
747   return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
751  * @return {string} A random UUID.
752  */
753 base.generateUuid = function() {
754   var random = new Uint16Array(8);
755   window.crypto.getRandomValues(random);
756   /** @type {Array<string>} */
757   var e = new Array();
758   for (var i = 0; i < 8; i++) {
759     e[i] = (/** @type {number} */ (random[i]) + 0x10000).
760         toString(16).substring(1);
761   }
762   return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' +
763       e[4] + '-' + e[5] + e[6] + e[7];
767  * @param {string} jsonString A JSON-encoded string.
768  * @return {Object|undefined} The decoded object, or undefined if the string
769  *     cannot be parsed.
770  */
771 base.jsonParseSafe = function(jsonString) {
772   try {
773     return /** @type {Object} */ (JSON.parse(jsonString));
774   } catch (err) {
775     return undefined;
776   }
780  * Return the current time as a formatted string suitable for logging.
782  * @return {string} The current time, formatted as the standard ISO string.
783  *     [yyyy-mm-ddDhh:mm:ss.xyz]
784  */
785 base.timestamp = function() {
786   return '[' + new Date().toISOString() + ']';
791  * A online function that can be stubbed by unit tests.
792  * @return {boolean}
793  */
794 base.isOnline = function() {
795   return navigator.onLine;
799  * Size the current window to fit its content.
800  * @param {boolean=} opt_centerWindow If true, position the window in the
801  *     center of the screen after resizing it.
802  */
803 base.resizeWindowToContent = function(opt_centerWindow) {
804   var appWindow = chrome.app.window.current();
805   var borderX = appWindow.outerBounds.width - appWindow.innerBounds.width;
806   var borderY = appWindow.outerBounds.height - appWindow.innerBounds.height;
807   var width = Math.ceil(document.documentElement.scrollWidth + borderX);
808   var height = Math.ceil(document.documentElement.scrollHeight + borderY);
809   appWindow.outerBounds.width = width;
810   appWindow.outerBounds.height = height;
811   if (opt_centerWindow) {
812     var screenWidth = screen.availWidth;
813     var screenHeight = screen.availHeight;
814     appWindow.outerBounds.left = Math.round((screenWidth - width) / 2);
815     appWindow.outerBounds.top = Math.round((screenHeight - height) / 2);
816   }