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 || {};
17 base.debug = function() {};
20 * @return {string} The callstack of the current method.
22 base.debug.callstack = function() {
25 } catch (/** @type {Error} */ error) {
26 var callstack = error.stack
27 .replace(/^\s+(at eval )?at\s+/gm, '') // Remove 'at' and indentation.
29 callstack.splice(0,2); // Remove the stack of the current function.
31 return callstack.join('\n');
37 base.Disposable = function() {};
38 base.Disposable.prototype.dispose = function() {};
42 * @param {...base.Disposable} var_args
43 * @implements {base.Disposable}
44 * @suppress {reportUnknownTypes}
46 base.Disposables = function(var_args) {
48 * @type {Array<base.Disposable>}
51 this.disposables_ = Array.prototype.slice.call(arguments, 0);
55 * @param {...base.Disposable} var_args
56 * @suppress {reportUnknownTypes}
58 base.Disposables.prototype.add = function(var_args) {
59 var disposables = Array.prototype.slice.call(arguments, 0);
60 for (var i = 0; i < disposables.length; i++) {
61 var current = /** @type {base.Disposable} */ (disposables[i]);
62 if (this.disposables_.indexOf(current) === -1) {
63 this.disposables_.push(current);
69 * @param {...base.Disposable} var_args Dispose |var_args| and remove
70 * them from the current object.
71 * @suppress {reportUnknownTypes}
73 base.Disposables.prototype.remove = function(var_args) {
74 var disposables = Array.prototype.slice.call(arguments, 0);
75 for (var i = 0; i < disposables.length; i++) {
76 var disposable = /** @type {base.Disposable} */ (disposables[i]);
77 var index = this.disposables_.indexOf(disposable);
79 this.disposables_.splice(index, 1);
85 base.Disposables.prototype.dispose = function() {
86 for (var i = 0; i < this.disposables_.length; i++) {
87 this.disposables_[i].dispose();
89 this.disposables_ = null;
93 * A utility function to invoke |obj|.dispose without a null check on |obj|.
94 * @param {base.Disposable} obj
96 base.dispose = function(obj) {
98 console.assert(typeof obj.dispose == 'function',
99 'dispose() should have type function, not ' +
100 typeof obj.dispose + '.');
106 * Copy all properties from src to dest.
107 * @param {Object} dest
108 * @param {Object} src
110 base.mix = function(dest, src) {
111 for (var prop in src) {
112 if (src.hasOwnProperty(prop) && !(prop in dest)) {
113 dest[prop] = src[prop];
119 * Adds a mixin to a class.
120 * @param {Object} dest
121 * @param {Object} src
122 * @suppress {checkTypes|reportUnknownTypes}
124 base.extend = function(dest, src) {
125 base.mix(dest.prototype, src.prototype || src);
129 * Inherits properties and methods from |parentCtor| at object construction time
130 * using prototypical inheritance. e.g.
132 * var ParentClass = function(parentArg) {
133 * this.parentProperty = parentArg;
136 * var ChildClass = function() {
137 * base.inherits(this, ParentClass, 'parentArg'); // must be the first line.
140 * var child = new ChildClass();
141 * child instanceof ParentClass // true
143 * See base_inherits_unittest.js for the guaranteed behavior of base.inherits().
144 * This lazy approach is chosen so that it is not necessary to maintain proper
145 * script loading order between the parent class and the child class.
147 * @param {*} childObject
148 * @param {*} parentCtor
149 * @param {...} parentCtorArgs
150 * @suppress {checkTypes|reportUnknownTypes}
152 base.inherits = function(childObject, parentCtor, parentCtorArgs) {
153 console.assert(parentCtor && parentCtor.prototype,
154 'Invalid parent constructor.');
155 var parentArgs = Array.prototype.slice.call(arguments, 2);
157 // Mix in the parent's prototypes so that they're available during the parent
159 base.mix(childObject, parentCtor.prototype);
160 parentCtor.apply(childObject, parentArgs);
162 // Note that __proto__ is deprecated.
163 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
164 // Global_Objects/Object/proto.
165 // It is used so that childObject instanceof parentCtor will
167 childObject.__proto__.__proto__ = parentCtor.prototype;
168 console.assert(childObject instanceof parentCtor,
169 'child is not an instance of parent.');
172 base.doNothing = function() {};
175 * Returns an array containing the values of |dict|.
176 * @param {!Object} dict
179 base.values = function(dict) {
180 return Object.keys(dict).map(
181 /** @param {string} key */
189 * @return {*} a recursive copy of |value| or null if |value| is not copyable
190 * (e.g. undefined, NaN).
192 base.deepCopy = function(value) {
194 return JSON.parse(JSON.stringify(value));
200 * Returns a copy of the input object with all null/undefined fields
201 * removed. Returns an empty object for a null/undefined input.
203 * @param {Object<?T>|undefined} input
204 * @return {!Object<T>}
207 base.copyWithoutNullFields = function(input) {
208 /** @const {!Object} */
211 for (var field in input) {
212 var value = /** @type {*} */ (input[field]);
214 result[field] = value;
222 * @param {!Object} object
223 * @return {boolean} True if the object is empty (equal to {}); false otherwise.
225 base.isEmptyObject = function(object) {
226 return Object.keys(object).length === 0;
230 * @type {boolean|undefined}
233 base.isAppsV2_ = undefined;
236 * @return {boolean} True if this is a v2 app; false if it is a legacy app.
238 base.isAppsV2 = function() {
239 if (base.isAppsV2_ === undefined) {
240 var manifest = chrome.runtime.getManifest();
242 Boolean(manifest && manifest.app && manifest.app.background);
244 return base.isAppsV2_;
248 * Joins the |url| with optional query parameters defined in |opt_params|
249 * See unit test for usage.
250 * @param {string} url
251 * @param {Object<string>=} opt_params
254 base.urlJoin = function(url, opt_params) {
258 var queryParameters = [];
259 for (var key in opt_params) {
260 queryParameters.push(encodeURIComponent(key) + "=" +
261 encodeURIComponent(opt_params[key]));
263 return url + '?' + queryParameters.join('&');
268 * @return {Object<string>} The URL parameters.
270 base.getUrlParameters = function() {
272 var parts = window.location.search.substring(1).split('&');
273 for (var i = 0; i < parts.length; i++) {
274 var pair = parts[i].split('=');
275 result[pair[0]] = decodeURIComponent(pair[1]);
281 * Convert special characters (e.g. &, < and >) to HTML entities.
283 * @param {string} str
286 base.escapeHTML = function(str) {
287 var div = document.createElement('div');
288 div.appendChild(document.createTextNode(str));
289 return div.innerHTML;
293 * Promise is a great tool for writing asynchronous code. However, the construct
294 * var p = new promise(function init(resolve, reject) {
295 * ... // code that fulfills the Promise.
297 * forces the Promise-resolving logic to reside in the |init| function
298 * of the constructor. This is problematic when you need to resolve the
299 * Promise in a member function(which is quite common for event callbacks).
301 * base.Deferred comes to the rescue. It encapsulates a Promise
302 * object and exposes member methods (resolve/reject) to fulfill it.
304 * Here are the recommended steps to follow when implementing an asynchronous
305 * function that returns a Promise:
306 * 1. Create a deferred object by calling
307 * var deferred = new base.Deferred();
308 * 2. Call deferred.resolve() when the asynchronous operation finishes.
309 * 3. Call deferred.reject() when the asynchronous operation fails.
310 * 4. Return deferred.promise() to the caller so that it can subscribe
311 * to status changes using the |then| handler.
314 * function myAsyncAPI() {
315 * var deferred = new base.Deferred();
316 * window.setTimeout(function() {
317 * deferred.resolve();
319 * return deferred.promise();
325 base.Deferred = function() {
327 * @private {?function(?):void}
329 this.resolve_ = null;
332 * @private {?function(?):void}
337 * @this {base.Deferred}
338 * @param {function(?):void} resolve
339 * @param {function(*):void} reject
341 var initPromise = function(resolve, reject) {
342 this.resolve_ = resolve;
343 this.reject_ = reject;
347 * @private {!Promise<T>}
349 this.promise_ = new Promise(initPromise.bind(this));
352 /** @param {*} reason */
353 base.Deferred.prototype.reject = function(reason) {
354 this.reject_(reason);
357 /** @param {*=} opt_value */
358 base.Deferred.prototype.resolve = function(opt_value) {
359 this.resolve_(opt_value);
362 /** @return {!Promise<T>} */
363 base.Deferred.prototype.promise = function() {
364 return this.promise_;
367 base.Promise = function() {};
370 * @param {number} delay
371 * @param {*=} opt_value
372 * @return {!Promise} a Promise that will be fulfilled with |opt_value|
375 base.Promise.sleep = function(delay, opt_value) {
378 window.setTimeout(function() {
385 * @param {Promise} promise
386 * @return {Promise} a Promise that will be fulfilled iff the specified Promise
389 base.Promise.negate = function(promise) {
391 /** @return {Promise} */
393 return Promise.reject();
395 /** @return {Promise} */
397 return Promise.resolve();
402 * Creates a promise that will be fulfilled within a certain timeframe.
404 * This function creates a result promise |R| that will be resolved to
405 * either |promise| or |opt_defaultValue|. If |promise| is fulfulled
406 * (i.e. resolved or rejected) within |delay| milliseconds, then |R|
407 * is resolved with |promise|. Otherwise, |R| is resolved with
408 * |opt_defaultValue|.
410 * Avoid passing a promise as |opt_defaultValue|, as this could result
411 * in |R| remaining unfulfilled after |delay| milliseconds.
413 * @param {!Promise<T>} promise The promise to wrap.
414 * @param {number} delay The number of milliseconds to wait.
415 * @param {*=} opt_defaultValue The default value used to resolve the
417 * @return {!Promise<T>} A new promise.
420 base.Promise.withTimeout = function(promise, delay, opt_defaultValue) {
421 return Promise.race([promise, base.Promise.sleep(delay, opt_defaultValue)]);
425 * Converts a |method| with callbacks into a Promise.
427 * @param {Function} method
428 * @param {Array} params
429 * @param {*=} opt_context
430 * @param {boolean=} opt_hasErrorHandler whether the method has an error handler
433 base.Promise.as = function(method, params, opt_context, opt_hasErrorHandler) {
434 return new Promise(function(resolve, reject) {
435 params.push(resolve);
436 if (opt_hasErrorHandler) {
440 method.apply(opt_context, params);
441 } catch (/** @type {*} */ e) {
448 * A mixin for classes with events.
450 * For example, to create an alarm event for SmokeDetector:
451 * functionSmokeDetector() {
452 * base.inherits(this, base.EventSourceImpl);
453 * this.defineEvents(['alarm']);
457 * SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
458 * var param = {} // optional parameters
459 * this.raiseEvent('alarm', param);
462 * To listen to an event:
463 * var smokeDetector = new SmokeDetector();
464 * smokeDetector.addEventListener('alarm', listenerObj.someCallback)
469 * Helper interface for the EventSource.
472 base.EventEntry = function() {
473 /** @type {Array<function():void>} */
479 base.EventSource = function() {};
482 * Add a listener |fn| to listen to |type| event.
483 * @param {string} type
484 * @param {Function} fn
486 base.EventSource.prototype.addEventListener = function(type, fn) {};
489 * Remove a listener |fn| to listen to |type| event.
490 * @param {string} type
491 * @param {Function} fn
493 base.EventSource.prototype.removeEventListener = function(type, fn) {};
498 * Since this class is implemented as a mixin, the constructor may not be
499 * called. All initializations should be done in defineEvents.
500 * @implements {base.EventSource}
502 base.EventSourceImpl = function() {
503 /** @type {Object<base.EventEntry>} */
508 * @param {base.EventSourceImpl} obj
509 * @param {string} type
512 base.EventSourceImpl.assertHasEvent_ = function(obj, type) {
513 console.assert(Boolean(obj.eventMap_),
514 "The object doesn't support events.");
515 console.assert(Boolean(obj.eventMap_[type]),
516 'Event <' + type +'> is undefined for the current object.');
519 base.EventSourceImpl.prototype = {
521 * Define |events| for this event source.
522 * @param {Array<string>} events
524 defineEvents: function(events) {
525 console.assert(!Boolean(this.eventMap_),
526 'defineEvents() can only be called once.');
530 * @this {base.EventSourceImpl}
531 * @param {string} type
534 console.assert(typeof type == 'string',
535 'Event name must be a string; found ' + type + '.');
536 this.eventMap_[type] = new base.EventEntry();
541 * @param {string} type
542 * @param {Function} fn
544 addEventListener: function(type, fn) {
545 console.assert(typeof fn == 'function',
546 'addEventListener(): event listener for ' + type +
547 ' must be function, not ' + typeof fn + '.');
548 base.EventSourceImpl.assertHasEvent_(this, type);
550 var listeners = this.eventMap_[type].listeners;
555 * @param {string} type
556 * @param {Function} fn
558 removeEventListener: function(type, fn) {
559 console.assert(typeof fn == 'function',
560 'removeEventListener(): event listener for ' + type +
561 ' must be function, not ' + typeof fn + '.');
562 base.EventSourceImpl.assertHasEvent_(this, type);
564 var listeners = this.eventMap_[type].listeners;
565 // find the listener to remove.
566 for (var i = 0; i < listeners.length; i++) {
567 var listener = listeners[i];
568 if (listener == fn) {
569 listeners.splice(i, 1);
576 * Fire an event of a particular type on this object.
577 * @param {string} type
578 * @param {*=} opt_details The type of |opt_details| should be ?= to
579 * match what is defined in add(remove)EventListener. However, JSCompile
580 * cannot handle invoking an unknown type as an argument to |listener|
581 * As a hack, we set the type to *=.
583 raiseEvent: function(type, opt_details) {
584 base.EventSourceImpl.assertHasEvent_(this, type);
586 var entry = this.eventMap_[type];
587 var listeners = entry.listeners.slice(0); // Make a copy of the listeners.
590 /** @param {function(*=):void} listener */
593 listener(opt_details);
601 * A lightweight object that helps manage the lifetime of an event listener.
603 * For example, do the following if you want to automatically unhook events
604 * when your object is disposed:
606 * var MyConstructor = function(domElement) {
607 * this.eventHooks_ = new base.Disposables(
608 * new base.EventHook(domElement, 'click', this.onClick_.bind(this)),
609 * new base.EventHook(domElement, 'keydown', this.onClick_.bind(this)),
610 * new base.ChromeEventHook(chrome.runtime.onMessage,
611 * this.onMessage_.bind(this))
615 * MyConstructor.prototype.dispose = function() {
616 * this.eventHooks_.dispose();
617 * this.eventHooks_ = null;
620 * @param {base.EventSource} src
621 * @param {string} eventName
622 * @param {Function} listener
625 * @implements {base.Disposable}
627 base.EventHook = function(src, eventName, listener) {
629 this.eventName_ = eventName;
630 this.listener_ = listener;
631 src.addEventListener(eventName, listener);
634 base.EventHook.prototype.dispose = function() {
635 this.src_.removeEventListener(this.eventName_, this.listener_);
639 * An event hook implementation for DOM Events.
641 * @param {HTMLElement|Element|Window|HTMLDocument} src
642 * @param {string} eventName
643 * @param {Function} listener
644 * @param {boolean} capture
647 * @implements {base.Disposable}
649 base.DomEventHook = function(src, eventName, listener, capture) {
651 this.eventName_ = eventName;
652 this.listener_ = listener;
653 this.capture_ = capture;
654 src.addEventListener(eventName, listener, capture);
657 base.DomEventHook.prototype.dispose = function() {
658 this.src_.removeEventListener(this.eventName_, this.listener_, this.capture_);
663 * An event hook implementation for Chrome Events.
665 * @param {ChromeEvent|
666 * chrome.contextMenus.ClickedEvent|
667 * chrome.app.runtime.LaunchEvent} src
668 * @param {!Function} listener
671 * @implements {base.Disposable}
673 base.ChromeEventHook = function(src, listener) {
675 this.listener_ = listener;
676 src.addListener(listener);
679 base.ChromeEventHook.prototype.dispose = function() {
680 this.src_.removeListener(this.listener_);
684 * A disposable repeating timer.
686 * @param {Function} callback
687 * @param {number} interval
688 * @param {boolean=} opt_invokeNow Whether to invoke the callback now, default
692 * @implements {base.Disposable}
694 base.RepeatingTimer = function(callback, interval, opt_invokeNow) {
696 this.intervalId_ = window.setInterval(callback, interval);
702 base.RepeatingTimer.prototype.dispose = function() {
703 window.clearInterval(this.intervalId_);
704 this.intervalId_ = null;
708 * A disposable one shot timer.
710 * @param {Function} callback
711 * @param {number} timeout
714 * @implements {base.Disposable}
716 base.OneShotTimer = function(callback, timeout) {
720 this.timerId_ = window.setTimeout(function() {
721 that.timerId_ = null;
726 base.OneShotTimer.prototype.dispose = function() {
727 if (this.timerId_ !== null) {
728 window.clearTimeout(this.timerId_);
729 this.timerId_ = null;
734 * Converts UTF-8 string to ArrayBuffer.
736 * @param {string} string
737 * @return {ArrayBuffer}
739 base.encodeUtf8 = function(string) {
740 var utf8String = unescape(encodeURIComponent(string));
741 var result = new Uint8Array(utf8String.length);
742 for (var i = 0; i < utf8String.length; i++)
743 result[i] = utf8String.charCodeAt(i);
744 return result.buffer;
748 * Decodes UTF-8 string from ArrayBuffer.
750 * @param {ArrayBuffer} buffer
753 base.decodeUtf8 = function(buffer) {
754 return decodeURIComponent(
755 escape(String.fromCharCode.apply(null, new Uint8Array(buffer))));
759 * Generate a nonce, to be used as an xsrf protection token.
761 * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
762 base.generateXsrfToken = function() {
763 var random = new Uint8Array(16);
764 window.crypto.getRandomValues(random);
765 var base64Token = window.btoa(String.fromCharCode.apply(null, random));
766 return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
770 * @return {string} A random UUID.
772 base.generateUuid = function() {
773 var random = new Uint16Array(8);
774 window.crypto.getRandomValues(random);
775 /** @type {Array<string>} */
777 for (var i = 0; i < 8; i++) {
778 e[i] = (/** @type {number} */ (random[i]) + 0x10000).
779 toString(16).substring(1);
781 return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' +
782 e[4] + '-' + e[5] + e[6] + e[7];
786 * @param {string} jsonString A JSON-encoded string.
787 * @return {Object|undefined} The decoded object, or undefined if the string
790 base.jsonParseSafe = function(jsonString) {
792 return /** @type {Object} */ (JSON.parse(jsonString));
799 * Return the current time as a formatted string suitable for logging.
801 * @return {string} The current time, formatted as the standard ISO string.
802 * [yyyy-mm-ddDhh:mm:ss.xyz]
804 base.timestamp = function() {
805 return '[' + new Date().toISOString() + ']';
810 * A online function that can be stubbed by unit tests.
813 base.isOnline = function() {
814 return navigator.onLine;
818 * Size the current window to fit its content.
819 * @param {boolean=} opt_centerWindow If true, position the window in the
820 * center of the screen after resizing it.
822 base.resizeWindowToContent = function(opt_centerWindow) {
823 var appWindow = chrome.app.window.current();
824 var borderX = appWindow.outerBounds.width - appWindow.innerBounds.width;
825 var borderY = appWindow.outerBounds.height - appWindow.innerBounds.height;
826 var width = Math.ceil(document.documentElement.scrollWidth + borderX);
827 var height = Math.ceil(document.documentElement.scrollHeight + borderY);
828 appWindow.outerBounds.width = width;
829 appWindow.outerBounds.height = height;
830 if (opt_centerWindow) {
831 var screenWidth = screen.availWidth;
832 var screenHeight = screen.availHeight;
833 appWindow.outerBounds.left = Math.round((screenWidth - width) / 2);
834 appWindow.outerBounds.top = Math.round((screenHeight - height) / 2);