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.
7 * A module that contains basic utility components and methods for the
14 /** @suppress {duplicate} */
15 var base
= base
|| {};
20 base
.Disposable = function() {};
21 base
.Disposable
.prototype.dispose = function() {};
25 * @param {...base.Disposable} var_args
26 * @implements {base.Disposable}
27 * @suppress {reportUnknownTypes}
29 base
.Disposables = function(var_args
) {
31 * @type {Array<base.Disposable>}
34 this.disposables_
= Array
.prototype.slice
.call(arguments
, 0);
38 * @param {...base.Disposable} var_args
39 * @suppress {reportUnknownTypes}
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
);
52 * @param {...base.Disposable} var_args Dispose |var_args| and remove
53 * them from the current object.
54 * @suppress {reportUnknownTypes}
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
);
62 this.disposables_
.splice(index
, 1);
68 base
.Disposables
.prototype.dispose = function() {
69 for (var i
= 0; i
< this.disposables_
.length
; i
++) {
70 this.disposables_
[i
].dispose();
72 this.disposables_
= null;
76 * A utility function to invoke |obj|.dispose without a null check on |obj|.
77 * @param {base.Disposable} obj
79 base
.dispose = function(obj
) {
81 console
.assert(typeof obj
.dispose
== 'function',
82 'dispose() should have type function, not ' +
83 typeof obj
.dispose
+ '.');
89 * Copy all properties from src to dest.
90 * @param {Object} dest
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
];
102 * Adds a mixin to a class.
103 * @param {Object} dest
104 * @param {Object} src
105 * @suppress {checkTypes|reportUnknownTypes}
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;
119 * var ChildClass = function() {
120 * base.inherits(this, ParentClass, 'parentArg'); // must be the first line.
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}
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
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
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
162 base
.values = function(dict
) {
163 return Object
.keys(dict
).map(
164 /** @param {string} key */
172 * @return {*} a recursive copy of |value| or null if |value| is not copyable
173 * (e.g. undefined, NaN).
175 base
.deepCopy = function(value
) {
177 return JSON
.parse(JSON
.stringify(value
));
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>}
190 base
.copyWithoutNullFields = function(input
) {
191 /** @const {!Object} */
194 for (var field
in input
) {
195 var value
= /** @type {*} */ (input
[field
]);
197 result
[field
] = value
;
205 * @param {!Object} object
206 * @return {boolean} True if the object is empty (equal to {}); false otherwise.
208 base
.isEmptyObject = function(object
) {
209 return Object
.keys(object
).length
=== 0;
213 * @type {boolean|undefined}
216 base
.isAppsV2_
= undefined;
219 * @return {boolean} True if this is a v2 app; false if it is a legacy app.
221 base
.isAppsV2 = function() {
222 if (base
.isAppsV2_
=== undefined) {
223 var manifest
= chrome
.runtime
.getManifest();
225 Boolean(manifest
&& manifest
.app
&& manifest
.app
.background
);
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
237 base
.urlJoin = function(url
, opt_params
) {
241 var queryParameters
= [];
242 for (var key
in opt_params
) {
243 queryParameters
.push(encodeURIComponent(key
) + "=" +
244 encodeURIComponent(opt_params
[key
]));
246 return url
+ '?' + queryParameters
.join('&');
251 * @return {Object<string>} The URL parameters.
253 base
.getUrlParameters = function() {
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]);
264 * Convert special characters (e.g. &, < and >) to HTML entities.
266 * @param {string} str
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.
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.
297 * function myAsyncAPI() {
298 * var deferred = new base.Deferred();
299 * window.setTimeout(function() {
300 * deferred.resolve();
302 * return deferred.promise();
308 base
.Deferred = function() {
310 * @private {?function(?):void}
312 this.resolve_
= null;
315 * @private {?function(?):void}
320 * @this {base.Deferred}
321 * @param {function(?):void} resolve
322 * @param {function(*):void} reject
324 var initPromise = function(resolve
, reject
) {
325 this.resolve_
= resolve
;
326 this.reject_
= reject
;
330 * @private {!Promise<T>}
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|
358 base
.Promise
.sleep = function(delay
, opt_value
) {
361 window
.setTimeout(function() {
368 * @param {Promise} promise
369 * @return {Promise} a Promise that will be fulfilled iff the specified Promise
372 base
.Promise
.negate = function(promise
) {
374 /** @return {Promise} */
376 return Promise
.reject();
378 /** @return {Promise} */
380 return Promise
.resolve();
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
400 * @return {!Promise<T>} A new promise.
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
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
) {
423 method
.apply(opt_context
, params
);
424 } catch (/** @type {*} */ e
) {
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']);
440 * SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
441 * var param = {} // optional parameters
442 * this.raiseEvent('alarm', param);
445 * To listen to an event:
446 * var smokeDetector = new SmokeDetector();
447 * smokeDetector.addEventListener('alarm', listenerObj.someCallback)
452 * Helper interface for the EventSource.
455 base
.EventEntry = function() {
456 /** @type {Array<function():void>} */
462 base
.EventSource = function() {};
465 * Add a listener |fn| to listen to |type| event.
466 * @param {string} type
467 * @param {Function} fn
469 base
.EventSource
.prototype.addEventListener = function(type
, fn
) {};
472 * Remove a listener |fn| to listen to |type| event.
473 * @param {string} type
474 * @param {Function} fn
476 base
.EventSource
.prototype.removeEventListener = function(type
, fn
) {};
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}
485 base
.EventSourceImpl = function() {
486 /** @type {Object<base.EventEntry>} */
491 * @param {base.EventSourceImpl} obj
492 * @param {string} type
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 = {
504 * Define |events| for this event source.
505 * @param {Array<string>} events
507 defineEvents: function(events
) {
508 console
.assert(!Boolean(this.eventMap_
),
509 'defineEvents() can only be called once.');
513 * @this {base.EventSourceImpl}
514 * @param {string} type
517 console
.assert(typeof type
== 'string',
518 'Event name must be a string; found ' + type
+ '.');
519 this.eventMap_
[type
] = new base
.EventEntry();
524 * @param {string} type
525 * @param {Function} fn
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
;
538 * @param {string} type
539 * @param {Function} fn
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);
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 *=.
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.
573 /** @param {function(*=):void} listener */
576 listener(opt_details
);
584 * A lightweight object that helps manage the lifetime of an event listener.
586 * For example, do the following if you want to automatically unhook events
587 * when your object is disposed:
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))
598 * MyConstructor.prototype.dispose = function() {
599 * this.eventHooks_.dispose();
600 * this.eventHooks_ = null;
603 * @param {base.EventSource} src
604 * @param {string} eventName
605 * @param {Function} listener
608 * @implements {base.Disposable}
610 base
.EventHook = function(src
, eventName
, listener
) {
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.
624 * @param {HTMLElement|Element|Window|HTMLDocument} src
625 * @param {string} eventName
626 * @param {Function} listener
627 * @param {boolean} capture
630 * @implements {base.Disposable}
632 base
.DomEventHook = function(src
, eventName
, listener
, capture
) {
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.
648 * @param {ChromeEvent|chrome.contextMenus.ClickedEvent|ChromeObjectEvent} src
649 * @param {!Function} listener
652 * @implements {base.Disposable}
654 base
.ChromeEventHook = function(src
, listener
) {
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
673 * @implements {base.Disposable}
675 base
.RepeatingTimer = function(callback
, interval
, opt_invokeNow
) {
677 this.intervalId_
= window
.setInterval(callback
, interval
);
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
695 * @implements {base.Disposable}
697 base
.OneShotTimer = function(callback
, timeout
) {
701 this.timerId_
= window
.setTimeout(function() {
702 that
.timerId_
= null;
707 base
.OneShotTimer
.prototype.dispose = function() {
708 if (this.timerId_
!== null) {
709 window
.clearTimeout(this.timerId_
);
710 this.timerId_
= null;
715 * Converts UTF-8 string to ArrayBuffer.
717 * @param {string} string
718 * @return {ArrayBuffer}
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.
731 * @param {ArrayBuffer} buffer
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.
753 base
.generateUuid = function() {
754 var random
= new Uint16Array(8);
755 window
.crypto
.getRandomValues(random
);
756 /** @type {Array<string>} */
758 for (var i
= 0; i
< 8; i
++) {
759 e
[i
] = (/** @type {number} */ (random
[i
]) + 0x10000).
760 toString(16).substring(1);
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
771 base
.jsonParseSafe = function(jsonString
) {
773 return /** @type {Object} */ (JSON
.parse(jsonString
));
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]
785 base
.timestamp = function() {
786 return '[' + new Date().toISOString() + ']';
791 * A online function that can be stubbed by unit tests.
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.
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);