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 * Whether to break in debugger and alert when an assertion fails.
21 * Set it to true for debugging.
24 base.debug.throwOnAssert = false;
27 * Assert that |expr| is true else print the |opt_msg|.
28 * @param {boolean} expr
29 * @param {string=} opt_msg
31 base.debug.assert = function(expr, opt_msg) {
34 opt_msg = 'Assertion Failed.';
36 console.error(opt_msg);
37 if (base.debug.throwOnAssert) {
38 throw new Error(opt_msg);
44 * @return {string} The callstack of the current method.
46 base.debug.callstack = function() {
49 } catch (/** @type {Error} */ error) {
50 var callstack = error.stack
51 .replace(/^\s+(at eval )?at\s+/gm, '') // Remove 'at' and indentation.
53 callstack.splice(0,2); // Remove the stack of the current function.
55 return callstack.join('\n');
61 base.Disposable = function() {};
62 base.Disposable.prototype.dispose = function() {};
66 * @param {...base.Disposable} var_args
67 * @implements {base.Disposable}
69 base.Disposables = function(var_args) {
71 * @type {Array<base.Disposable>}
74 this.disposables_ = Array.prototype.slice.call(arguments, 0);
78 * @param {...base.Disposable} var_args
80 base.Disposables.prototype.add = function(var_args) {
81 var disposables = Array.prototype.slice.call(arguments, 0);
82 for (var i = 0; i < disposables.length; i++) {
83 var current = /** @type {base.Disposable} */ (disposables[i]);
84 if (this.disposables_.indexOf(current) === -1) {
85 this.disposables_.push(current);
91 * @param {...base.Disposable} var_args Dispose |var_args| and remove
92 * them from the current object.
94 base.Disposables.prototype.remove = function(var_args) {
95 var disposables = Array.prototype.slice.call(arguments, 0);
96 for (var i = 0; i < disposables.length; i++) {
97 var disposable = /** @type {base.Disposable} */ (disposables[i]);
98 var index = this.disposables_.indexOf(disposable);
100 this.disposables_.splice(index, 1);
101 disposable.dispose();
106 base.Disposables.prototype.dispose = function() {
107 for (var i = 0; i < this.disposables_.length; i++) {
108 this.disposables_[i].dispose();
110 this.disposables_ = null;
114 * A utility function to invoke |obj|.dispose without a null check on |obj|.
115 * @param {base.Disposable} obj
117 base.dispose = function(obj) {
119 base.debug.assert(typeof obj.dispose == 'function');
125 * Copy all properties from src to dest.
126 * @param {Object} dest
127 * @param {Object} src
129 base.mix = function(dest, src) {
130 for (var prop in src) {
131 if (src.hasOwnProperty(prop) && !(prop in dest)) {
132 dest[prop] = src[prop];
138 * Adds a mixin to a class.
139 * @param {Object} dest
140 * @param {Object} src
141 * @suppress {checkTypes|reportUnknownTypes}
143 base.extend = function(dest, src) {
144 base.mix(dest.prototype, src.prototype || src);
148 * Inherits properties and methods from |parentCtor| at object construction time
149 * using prototypical inheritance. e.g.
151 * var ParentClass = function(parentArg) {
152 * this.parentProperty = parentArg;
155 * var ChildClass = function() {
156 * base.inherits(this, ParentClass, 'parentArg'); // must be the first line.
159 * var child = new ChildClass();
160 * child instanceof ParentClass // true
162 * See base_inherits_unittest.js for the guaranteed behavior of base.inherits().
163 * This lazy approach is chosen so that it is not necessary to maintain proper
164 * script loading order between the parent class and the child class.
166 * @param {*} childObject
167 * @param {*} parentCtor
168 * @param {...} parentCtorArgs
169 * @suppress {checkTypes|reportUnknownTypes}
171 base.inherits = function(childObject, parentCtor, parentCtorArgs) {
172 base.debug.assert(parentCtor && parentCtor.prototype,
173 'Invalid parent constructor.');
174 var parentArgs = Array.prototype.slice.call(arguments, 2);
176 // Mix in the parent's prototypes so that they're available during the parent
178 base.mix(childObject, parentCtor.prototype);
179 parentCtor.apply(childObject, parentArgs);
181 // Note that __proto__ is deprecated.
182 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
183 // Global_Objects/Object/proto.
184 // It is used so that childObject instanceof parentCtor will
186 childObject.__proto__.__proto__ = parentCtor.prototype;
187 base.debug.assert(childObject instanceof parentCtor);
190 base.doNothing = function() {};
193 * Returns an array containing the values of |dict|.
194 * @param {!Object} dict
197 base.values = function(dict) {
198 return Object.keys(dict).map(
199 /** @param {string} key */
207 * @return {*} a recursive copy of |value| or null if |value| is not copyable
208 * (e.g. undefined, NaN).
210 base.deepCopy = function(value) {
212 return JSON.parse(JSON.stringify(value));
218 * @type {boolean|undefined}
221 base.isAppsV2_ = undefined;
224 * @return {boolean} True if this is a v2 app; false if it is a legacy app.
226 base.isAppsV2 = function() {
227 if (base.isAppsV2_ === undefined) {
228 var manifest = chrome.runtime.getManifest();
230 Boolean(manifest && manifest.app && manifest.app.background);
232 return base.isAppsV2_;
236 * Joins the |url| with optional query parameters defined in |opt_params|
237 * See unit test for usage.
238 * @param {string} url
239 * @param {Object<string>=} opt_params
242 base.urlJoin = function(url, opt_params) {
246 var queryParameters = [];
247 for (var key in opt_params) {
248 queryParameters.push(encodeURIComponent(key) + "=" +
249 encodeURIComponent(opt_params[key]));
251 return url + '?' + queryParameters.join('&');
256 * @return {Object<string, string>} The URL parameters.
258 base.getUrlParameters = function() {
260 var parts = window.location.search.substring(1).split('&');
261 for (var i = 0; i < parts.length; i++) {
262 var pair = parts[i].split('=');
263 result[pair[0]] = decodeURIComponent(pair[1]);
269 * Convert special characters (e.g. &, < and >) to HTML entities.
271 * @param {string} str
274 base.escapeHTML = function(str) {
275 var div = document.createElement('div');
276 div.appendChild(document.createTextNode(str));
277 return div.innerHTML;
281 * Promise is a great tool for writing asynchronous code. However, the construct
282 * var p = new promise(function init(resolve, reject) {
283 * ... // code that fulfills the Promise.
285 * forces the Promise-resolving logic to reside in the |init| function
286 * of the constructor. This is problematic when you need to resolve the
287 * Promise in a member function(which is quite common for event callbacks).
289 * base.Deferred comes to the rescue. It encapsulates a Promise
290 * object and exposes member methods (resolve/reject) to fulfill it.
292 * Here are the recommended steps to follow when implementing an asynchronous
293 * function that returns a Promise:
294 * 1. Create a deferred object by calling
295 * var deferred = new base.Deferred();
296 * 2. Call deferred.resolve() when the asynchronous operation finishes.
297 * 3. Call deferred.reject() when the asynchronous operation fails.
298 * 4. Return deferred.promise() to the caller so that it can subscribe
299 * to status changes using the |then| handler.
302 * function myAsyncAPI() {
303 * var deferred = new base.Deferred();
304 * window.setTimeout(function() {
305 * deferred.resolve();
307 * return deferred.promise();
313 base.Deferred = function() {
315 * @private {?function(?):void}
317 this.resolve_ = null;
320 * @private {?function(?):void}
325 * @this {base.Deferred}
326 * @param {function(?):void} resolve
327 * @param {function(*):void} reject
329 var initPromise = function(resolve, reject) {
330 this.resolve_ = resolve;
331 this.reject_ = reject;
335 * @private {!Promise<T>}
337 this.promise_ = new Promise(initPromise.bind(this));
340 /** @param {*} reason */
341 base.Deferred.prototype.reject = function(reason) {
342 this.reject_(reason);
345 /** @param {*=} opt_value */
346 base.Deferred.prototype.resolve = function(opt_value) {
347 this.resolve_(opt_value);
350 /** @return {!Promise<T>} */
351 base.Deferred.prototype.promise = function() {
352 return this.promise_;
355 base.Promise = function() {};
358 * @param {number} delay
359 * @return {Promise} a Promise that will be fulfilled after |delay| ms.
361 base.Promise.sleep = function(delay) {
363 /** @param {function(*):void} fulfill */
365 window.setTimeout(fulfill, delay);
371 * @param {Promise} promise
372 * @return {Promise} a Promise that will be fulfilled iff the specified Promise
375 base.Promise.negate = function(promise) {
377 /** @return {Promise} */
379 return Promise.reject();
381 /** @return {Promise} */
383 return Promise.resolve();
388 * Converts a |method| with callbacks into a Promise.
390 * @param {Function} method
391 * @param {Array} params
392 * @param {*=} opt_context
393 * @param {boolean=} opt_hasErrorHandler whether the method has an error handler
396 base.Promise.as = function(method, params, opt_context, opt_hasErrorHandler) {
397 return new Promise(function(resolve, reject) {
398 params.push(resolve);
399 if (opt_hasErrorHandler) {
403 method.apply(opt_context, params);
404 } catch (/** @type {*} */ e) {
411 * A mixin for classes with events.
413 * For example, to create an alarm event for SmokeDetector:
414 * functionSmokeDetector() {
415 * base.inherits(this, base.EventSourceImpl);
416 * this.defineEvents(['alarm']);
420 * SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
421 * var param = {} // optional parameters
422 * this.raiseEvent('alarm', param);
425 * To listen to an event:
426 * var smokeDetector = new SmokeDetector();
427 * smokeDetector.addEventListener('alarm', listenerObj.someCallback)
432 * Helper interface for the EventSource.
435 base.EventEntry = function() {
436 /** @type {Array<function():void>} */
442 base.EventSource = function() {};
445 * Add a listener |fn| to listen to |type| event.
446 * @param {string} type
447 * @param {Function} fn
449 base.EventSource.prototype.addEventListener = function(type, fn) {};
452 * Remove a listener |fn| to listen to |type| event.
453 * @param {string} type
454 * @param {Function} fn
456 base.EventSource.prototype.removeEventListener = function(type, fn) {};
461 * Since this class is implemented as a mixin, the constructor may not be
462 * called. All initializations should be done in defineEvents.
463 * @implements {base.EventSource}
465 base.EventSourceImpl = function() {
466 /** @type {Object<string, base.EventEntry>} */
471 * @param {base.EventSourceImpl} obj
472 * @param {string} type
474 base.EventSourceImpl.isDefined = function(obj, type) {
475 base.debug.assert(Boolean(obj.eventMap_),
476 "The object doesn't support events");
477 base.debug.assert(Boolean(obj.eventMap_[type]), 'Event <' + type +
478 '> is undefined for the current object');
481 base.EventSourceImpl.prototype = {
483 * Define |events| for this event source.
484 * @param {Array<string>} events
486 defineEvents: function(events) {
487 base.debug.assert(!Boolean(this.eventMap_),
488 'defineEvents can only be called once.');
492 * @this {base.EventSourceImpl}
493 * @param {string} type
496 base.debug.assert(typeof type == 'string');
497 this.eventMap_[type] = new base.EventEntry();
502 * @param {string} type
503 * @param {Function} fn
505 addEventListener: function(type, fn) {
506 base.debug.assert(typeof fn == 'function');
507 base.EventSourceImpl.isDefined(this, type);
509 var listeners = this.eventMap_[type].listeners;
514 * @param {string} type
515 * @param {Function} fn
517 removeEventListener: function(type, fn) {
518 base.debug.assert(typeof fn == 'function');
519 base.EventSourceImpl.isDefined(this, type);
521 var listeners = this.eventMap_[type].listeners;
522 // find the listener to remove.
523 for (var i = 0; i < listeners.length; i++) {
524 var listener = listeners[i];
525 if (listener == fn) {
526 listeners.splice(i, 1);
533 * Fire an event of a particular type on this object.
534 * @param {string} type
535 * @param {*=} opt_details The type of |opt_details| should be ?= to
536 * match what is defined in add(remove)EventListener. However, JSCompile
537 * cannot handle invoking an unknown type as an argument to |listener|
538 * As a hack, we set the type to *=.
540 raiseEvent: function(type, opt_details) {
541 base.EventSourceImpl.isDefined(this, type);
543 var entry = this.eventMap_[type];
544 var listeners = entry.listeners.slice(0); // Make a copy of the listeners.
547 /** @param {function(*=):void} listener */
550 listener(opt_details);
558 * A lightweight object that helps manage the lifetime of an event listener.
560 * For example, do the following if you want to automatically unhook events
561 * when your object is disposed:
563 * var MyConstructor = function(domElement) {
564 * this.eventHooks_ = new base.Disposables(
565 * new base.EventHook(domElement, 'click', this.onClick_.bind(this)),
566 * new base.EventHook(domElement, 'keydown', this.onClick_.bind(this)),
567 * new base.ChromeEventHook(chrome.runtime.onMessage,
568 * this.onMessage_.bind(this))
572 * MyConstructor.prototype.dispose = function() {
573 * this.eventHooks_.dispose();
574 * this.eventHooks_ = null;
577 * @param {base.EventSource} src
578 * @param {string} eventName
579 * @param {Function} listener
582 * @implements {base.Disposable}
584 base.EventHook = function(src, eventName, listener) {
586 this.eventName_ = eventName;
587 this.listener_ = listener;
588 src.addEventListener(eventName, listener);
591 base.EventHook.prototype.dispose = function() {
592 this.src_.removeEventListener(this.eventName_, this.listener_);
596 * An event hook implementation for DOM Events.
598 * @param {HTMLElement|Element|Window|HTMLDocument} src
599 * @param {string} eventName
600 * @param {Function} listener
601 * @param {boolean} capture
604 * @implements {base.Disposable}
606 base.DomEventHook = function(src, eventName, listener, capture) {
608 this.eventName_ = eventName;
609 this.listener_ = listener;
610 this.capture_ = capture;
611 src.addEventListener(eventName, listener, capture);
614 base.DomEventHook.prototype.dispose = function() {
615 this.src_.removeEventListener(this.eventName_, this.listener_, this.capture_);
620 * An event hook implementation for Chrome Events.
622 * @param {chrome.Event} src
623 * @param {Function} listener
626 * @implements {base.Disposable}
628 base.ChromeEventHook = function(src, listener) {
630 this.listener_ = listener;
631 src.addListener(listener);
634 base.ChromeEventHook.prototype.dispose = function() {
635 this.src_.removeListener(this.listener_);
639 * A disposable repeating timer.
642 * @implements {base.Disposable}
644 base.RepeatingTimer = function(/** Function */callback, /** number */interval) {
646 this.intervalId_ = window.setInterval(callback, interval);
649 base.RepeatingTimer.prototype.dispose = function() {
650 window.clearInterval(this.intervalId_);
651 this.intervalId_ = null;
655 * Converts UTF-8 string to ArrayBuffer.
657 * @param {string} string
658 * @return {ArrayBuffer}
660 base.encodeUtf8 = function(string) {
661 var utf8String = unescape(encodeURIComponent(string));
662 var result = new Uint8Array(utf8String.length);
663 for (var i = 0; i < utf8String.length; i++)
664 result[i] = utf8String.charCodeAt(i);
665 return result.buffer;
669 * Decodes UTF-8 string from ArrayBuffer.
671 * @param {ArrayBuffer} buffer
674 base.decodeUtf8 = function(buffer) {
675 return decodeURIComponent(
676 escape(String.fromCharCode.apply(null, new Uint8Array(buffer))));
680 * Generate a nonce, to be used as an xsrf protection token.
682 * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
683 base.generateXsrfToken = function() {
684 var random = new Uint8Array(16);
685 window.crypto.getRandomValues(random);
686 var base64Token = window.btoa(String.fromCharCode.apply(null, random));
687 return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
691 * @param {string} jsonString A JSON-encoded string.
692 * @return {Object|undefined} The decoded object, or undefined if the string
695 base.jsonParseSafe = function(jsonString) {
697 return /** @type {Object} */ (JSON.parse(jsonString));
704 * Size the current window to fit its content vertically.
706 base.resizeWindowToContent = function() {
707 var appWindow = chrome.app.window.current();
708 var outerBounds = appWindow.outerBounds;
709 var borderY = outerBounds.height - appWindow.innerBounds.height;
710 appWindow.resizeTo(outerBounds.width, document.body.clientHeight + borderY);
711 // Sometimes, resizing the window causes its position to be reset to (0, 0),
712 // so restore it explicitly.
713 appWindow.moveTo(outerBounds.left, outerBounds.top);