blob: e9921e5eee0fbbf3f295fd6cf0072e4ec2c648f7 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* A module that contains basic utility components and methods for the
* chromoting project
*
*/
'use strict';
/** @suppress {duplicate} */
var base = base || {};
/**
* @interface
*/
base.Disposable = function() {};
base.Disposable.prototype.dispose = function() {};
/**
* @constructor
* @param {...base.Disposable} var_args
* @implements {base.Disposable}
* @suppress {reportUnknownTypes}
*/
base.Disposables = function(var_args) {
/**
* @type {Array<base.Disposable>}
* @private
*/
this.disposables_ = Array.prototype.slice.call(arguments, 0);
};
/**
* @param {...base.Disposable} var_args
* @suppress {reportUnknownTypes}
*/
base.Disposables.prototype.add = function(var_args) {
var disposables = Array.prototype.slice.call(arguments, 0);
for (var i = 0; i < disposables.length; i++) {
var current = /** @type {base.Disposable} */ (disposables[i]);
if (this.disposables_.indexOf(current) === -1) {
this.disposables_.push(current);
}
}
};
/**
* @param {...base.Disposable} var_args Dispose |var_args| and remove
* them from the current object.
* @suppress {reportUnknownTypes}
*/
base.Disposables.prototype.remove = function(var_args) {
var disposables = Array.prototype.slice.call(arguments, 0);
for (var i = 0; i < disposables.length; i++) {
var disposable = /** @type {base.Disposable} */ (disposables[i]);
var index = this.disposables_.indexOf(disposable);
if(index !== -1) {
this.disposables_.splice(index, 1);
disposable.dispose();
}
}
};
base.Disposables.prototype.dispose = function() {
for (var i = 0; i < this.disposables_.length; i++) {
this.disposables_[i].dispose();
}
this.disposables_ = null;
};
/**
* A utility function to invoke |obj|.dispose without a null check on |obj|.
* @param {base.Disposable} obj
*/
base.dispose = function(obj) {
if (obj) {
console.assert(typeof obj.dispose == 'function',
'dispose() should have type function, not ' +
typeof obj.dispose + '.');
obj.dispose();
}
};
/**
* Copy all properties from src to dest.
* @param {Object} dest
* @param {Object} src
*/
base.mix = function(dest, src) {
for (var prop in src) {
if (src.hasOwnProperty(prop) && !(prop in dest)) {
dest[prop] = src[prop];
}
}
};
/**
* Adds a mixin to a class.
* @param {Object} dest
* @param {Object} src
* @suppress {checkTypes|reportUnknownTypes}
*/
base.extend = function(dest, src) {
base.mix(dest.prototype, src.prototype || src);
};
/**
* Inherits properties and methods from |parentCtor| at object construction time
* using prototypical inheritance. e.g.
*
* var ParentClass = function(parentArg) {
* this.parentProperty = parentArg;
* }
*
* var ChildClass = function() {
* base.inherits(this, ParentClass, 'parentArg'); // must be the first line.
* }
*
* var child = new ChildClass();
* child instanceof ParentClass // true
*
* See base_inherits_unittest.js for the guaranteed behavior of base.inherits().
* This lazy approach is chosen so that it is not necessary to maintain proper
* script loading order between the parent class and the child class.
*
* @param {*} childObject
* @param {*} parentCtor
* @param {...} parentCtorArgs
* @suppress {checkTypes|reportUnknownTypes}
*/
base.inherits = function(childObject, parentCtor, parentCtorArgs) {
console.assert(parentCtor && parentCtor.prototype,
'Invalid parent constructor.');
var parentArgs = Array.prototype.slice.call(arguments, 2);
// Mix in the parent's prototypes so that they're available during the parent
// ctor.
base.mix(childObject, parentCtor.prototype);
parentCtor.apply(childObject, parentArgs);
// Note that __proto__ is deprecated.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
// Global_Objects/Object/proto.
// It is used so that childObject instanceof parentCtor will
// return true.
childObject.__proto__.__proto__ = parentCtor.prototype;
console.assert(childObject instanceof parentCtor,
'child is not an instance of parent.');
};
base.doNothing = function() {};
/**
* Returns an array containing the values of |dict|.
* @param {!Object} dict
* @return {Array}
*/
base.values = function(dict) {
return Object.keys(dict).map(
/** @param {string} key */
function(key) {
return dict[key];
});
};
/**
* @param {*} value
* @return {*} a recursive copy of |value| or null if |value| is not copyable
* (e.g. undefined, NaN).
*/
base.deepCopy = function(value) {
try {
return JSON.parse(JSON.stringify(value));
} catch (e) {}
return null;
};
/**
* Returns a copy of the input object with all null/undefined fields
* removed. Returns an empty object for a null/undefined input.
*
* @param {Object<?T>|undefined} input
* @return {!Object<T>}
* @template T
*/
base.copyWithoutNullFields = function(input) {
var result = {};
base.mergeWithoutNullFields(result, input);
return result;
};
/**
* Merge non-null fields of |src| into |dest|.
*
* @param {!Object<T>} dest
* @param {Object<?T>|undefined} src
* @template T
*/
base.mergeWithoutNullFields = function(dest, src) {
if (src) {
for (var field in src) {
var value = /** @type {*} */ (src[field]);
if (value != null) {
dest[field] = base.deepCopy(value);
}
}
}
};
/**
* @param {!Object} object
* @return {boolean} True if the object is empty (equal to {}); false otherwise.
*/
base.isEmptyObject = function(object) {
return Object.keys(object).length === 0;
};
/**
* @type {boolean|undefined}
* @private
*/
base.isAppsV2_ = undefined;
/**
* @return {boolean} True if this is a v2 app; false if it is a legacy app.
*/
base.isAppsV2 = function() {
if (base.isAppsV2_ === undefined) {
var manifest = chrome.runtime.getManifest();
base.isAppsV2_ =
Boolean(manifest && manifest.app && manifest.app.background);
}
return base.isAppsV2_;
};
/**
* Joins the |url| with optional query parameters defined in |opt_params|
* See unit test for usage.
* @param {string} url
* @param {Object<string>=} opt_params
* @return {string}
*/
base.urlJoin = function(url, opt_params) {
if (!opt_params) {
return url;
}
var queryParameters = [];
for (var key in opt_params) {
queryParameters.push(encodeURIComponent(key) + "=" +
encodeURIComponent(opt_params[key]));
}
return url + '?' + queryParameters.join('&');
};
/**
* @return {Object<string>} The URL parameters.
*/
base.getUrlParameters = function() {
var result = {};
var parts = window.location.search.substring(1).split('&');
for (var i = 0; i < parts.length; i++) {
var pair = parts[i].split('=');
result[pair[0]] = decodeURIComponent(pair[1]);
}
return result;
};
/**
* Convert special characters (e.g. &, < and >) to HTML entities.
*
* @param {string} str
* @return {string}
*/
base.escapeHTML = function(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
};
/**
* Promise is a great tool for writing asynchronous code. However, the construct
* var p = new promise(function init(resolve, reject) {
* ... // code that fulfills the Promise.
* });
* forces the Promise-resolving logic to reside in the |init| function
* of the constructor. This is problematic when you need to resolve the
* Promise in a member function(which is quite common for event callbacks).
*
* base.Deferred comes to the rescue. It encapsulates a Promise
* object and exposes member methods (resolve/reject) to fulfill it.
*
* Here are the recommended steps to follow when implementing an asynchronous
* function that returns a Promise:
* 1. Create a deferred object by calling
* var deferred = new base.Deferred();
* 2. Call deferred.resolve() when the asynchronous operation finishes.
* 3. Call deferred.reject() when the asynchronous operation fails.
* 4. Return deferred.promise() to the caller so that it can subscribe
* to status changes using the |then| handler.
*
* Sample Usage:
* function myAsyncAPI() {
* var deferred = new base.Deferred();
* window.setTimeout(function() {
* deferred.resolve();
* }, 100);
* return deferred.promise();
* };
*
* @constructor
* @template T
*/
base.Deferred = function() {
/**
* @private {?function(?):void}
*/
this.resolve_ = null;
/**
* @private {?function(?):void}
*/
this.reject_ = null;
/**
* @this {base.Deferred}
* @param {function(?):void} resolve
* @param {function(*):void} reject
*/
var initPromise = function(resolve, reject) {
this.resolve_ = resolve;
this.reject_ = reject;
};
/**
* @private {!Promise<T>}
*/
this.promise_ = new Promise(initPromise.bind(this));
};
/** @param {*} reason */
base.Deferred.prototype.reject = function(reason) {
this.reject_(reason);
};
/** @param {*=} opt_value */
base.Deferred.prototype.resolve = function(opt_value) {
this.resolve_(opt_value);
};
/** @return {!Promise<T>} */
base.Deferred.prototype.promise = function() {
return this.promise_;
};
base.Promise = function() {};
/**
* @param {number} delay
* @param {*=} opt_value
* @return {!Promise} a Promise that will be fulfilled with |opt_value|
* after |delay| ms.
*/
base.Promise.sleep = function(delay, opt_value) {
return new Promise(
function(resolve) {
window.setTimeout(function() {
resolve(opt_value);
}, delay);
});
};
/**
* @param {Promise} promise
* @return {Promise} a Promise that will be fulfilled iff the specified Promise
* is rejected.
*/
base.Promise.negate = function(promise) {
return promise.then(
/** @return {Promise} */
function() {
return Promise.reject();
},
/** @return {Promise} */
function() {
return Promise.resolve();
});
};
/**
* Creates a promise that will be fulfilled within a certain timeframe.
*
* This function creates a result promise |R| that will be resolved to
* either |promise| or |opt_defaultValue|. If |promise| is fulfulled
* (i.e. resolved or rejected) within |delay| milliseconds, then |R|
* is resolved with |promise|. Otherwise, |R| is resolved with
* |opt_defaultValue|.
*
* Avoid passing a promise as |opt_defaultValue|, as this could result
* in |R| remaining unfulfilled after |delay| milliseconds.
*
* @param {!Promise<T>} promise The promise to wrap.
* @param {number} delay The number of milliseconds to wait.
* @param {*=} opt_defaultValue The default value used to resolve the
* result.
* @return {!Promise<T>} A new promise.
* @template T
*/
base.Promise.withTimeout = function(promise, delay, opt_defaultValue) {
return Promise.race([promise, base.Promise.sleep(delay, opt_defaultValue)]);
};
/**
* Creates a promise that will be rejected if it is not fulfilled within a
* certain timeframe.
*
* This function creates a result promise |R|. If |promise| is fulfilled
* (i.e. resolved or rejected) within |delay| milliseconds, then |R|
* is resolved or rejected, respectively. Otherwise, |R| is rejected with
* |opt_defaultError|.
*
* @param {!Promise<T>} promise The promise to wrap.
* @param {number} delay The number of milliseconds to wait.
* @param {*=} opt_defaultError The default error used to reject the promise.
* @return {!Promise<T>} A new promise.
* @template T
*/
base.Promise.rejectAfterTimeout = function(promise, delay, opt_defaultError) {
return Promise.race([
promise,
base.Promise.sleep(delay).then(function() {
return Promise.reject(opt_defaultError);
})
]);
};
/**
* Converts a |method| with callbacks into a Promise.
*
* @param {Function} method
* @param {Array} params
* @param {*=} opt_context
* @param {boolean=} opt_hasErrorHandler whether the method has an error handler
* @return {Promise}
*/
base.Promise.as = function(method, params, opt_context, opt_hasErrorHandler) {
return new Promise(function(resolve, reject) {
params.push(resolve);
if (opt_hasErrorHandler) {
params.push(reject);
}
try {
method.apply(opt_context, params);
} catch (/** @type {*} */ e) {
reject(e);
}
});
};
/**
* A mixin for classes with events.
*
* For example, to create an alarm event for SmokeDetector:
* functionSmokeDetector() {
* base.inherits(this, base.EventSourceImpl);
* this.defineEvents(['alarm']);
* };
*
* To fire an event:
* SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
* var param = {} // optional parameters
* this.raiseEvent('alarm', param);
* }
*
* To listen to an event:
* var smokeDetector = new SmokeDetector();
* smokeDetector.addEventListener('alarm', listenerObj.someCallback)
*
*/
/**
* Helper interface for the EventSource.
* @constructor
*/
base.EventEntry = function() {
/** @type {Array<Function>} */
this.listeners = [];
};
/** @interface */
base.EventSource = function() {};
/**
* Add a listener |fn| to listen to |type| event.
* @param {string} type
* @param {Function} fn
*/
base.EventSource.prototype.addEventListener = function(type, fn) {};
/**
* Remove a listener |fn| to listen to |type| event.
* @param {string} type
* @param {Function} fn
*/
base.EventSource.prototype.removeEventListener = function(type, fn) {};
/**
* @constructor
* Since this class is implemented as a mixin, the constructor may not be
* called. All initializations should be done in defineEvents.
* @implements {base.EventSource}
*/
base.EventSourceImpl = function() {
/** @type {Object<base.EventEntry>} */
this.eventMap_;
};
/**
* @param {base.EventSourceImpl} obj
* @param {string} type
* @private
*/
base.EventSourceImpl.assertHasEvent_ = function(obj, type) {
console.assert(Boolean(obj.eventMap_),
"The object doesn't support events.");
console.assert(Boolean(obj.eventMap_[type]),
'Event <' + type +'> is undefined for the current object.');
};
base.EventSourceImpl.prototype = {
/**
* Define |events| for this event source.
* @param {Array<string>} events
*/
defineEvents: function(events) {
console.assert(!Boolean(this.eventMap_),
'defineEvents() can only be called once.');
this.eventMap_ = {};
events.forEach(
/**
* @this {base.EventSourceImpl}
* @param {string} type
*/
function(type) {
console.assert(typeof type == 'string',
'Event name must be a string; found ' + type + '.');
this.eventMap_[type] = new base.EventEntry();
}, this);
},
/**
* @param {string} type
* @param {Function} fn
*/
addEventListener: function(type, fn) {
console.assert(typeof fn == 'function',
'addEventListener(): event listener for ' + type +
' must be function, not ' + typeof fn + '.');
base.EventSourceImpl.assertHasEvent_(this, type);
var listeners = this.eventMap_[type].listeners;
listeners.push(fn);
},
/**
* @param {string} type
* @param {Function} fn
*/
removeEventListener: function(type, fn) {
console.assert(typeof fn == 'function',
'removeEventListener(): event listener for ' + type +
' must be function, not ' + typeof fn + '.');
base.EventSourceImpl.assertHasEvent_(this, type);
var listeners = this.eventMap_[type].listeners;
// find the listener to remove.
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
if (listener == fn) {
listeners.splice(i, 1);
break;
}
}
},
/**
* Fire an event of a particular type on this object.
* @param {string} type
* @param {*=} opt_details The type of |opt_details| should be ?= to
* match what is defined in add(remove)EventListener. However, JSCompile
* cannot handle invoking an unknown type as an argument to |listener|
* As a hack, we set the type to *=.
*/
raiseEvent: function(type, opt_details) {
base.EventSourceImpl.assertHasEvent_(this, type);
var entry = this.eventMap_[type];
var listeners = entry.listeners.slice(0); // Make a copy of the listeners.
listeners.forEach(
/** @param {Function} listener */
function(listener){
if (listener) {
listener(opt_details);
}
});
}
};
/**
* A lightweight object that helps manage the lifetime of an event listener.
*
* For example, do the following if you want to automatically unhook events
* when your object is disposed:
*
* var MyConstructor = function(domElement) {
* this.eventHooks_ = new base.Disposables(
* new base.EventHook(domElement, 'click', this.onClick_.bind(this)),
* new base.EventHook(domElement, 'keydown', this.onClick_.bind(this)),
* new base.ChromeEventHook(chrome.runtime.onMessage,
* this.onMessage_.bind(this))
* );
* }
*
* MyConstructor.prototype.dispose = function() {
* this.eventHooks_.dispose();
* this.eventHooks_ = null;
* }
*
* @param {base.EventSource} src
* @param {string} eventName
* @param {Function} listener
*
* @constructor
* @implements {base.Disposable}
*/
base.EventHook = function(src, eventName, listener) {
this.src_ = src;
this.eventName_ = eventName;
this.listener_ = listener;
src.addEventListener(eventName, listener);
};
base.EventHook.prototype.dispose = function() {
this.src_.removeEventListener(this.eventName_, this.listener_);
};
/**
* An event hook implementation for DOM Events.
*
* @param {HTMLElement|Element|Window|HTMLDocument} src
* @param {string} eventName
* @param {Function} listener
* @param {boolean} capture
*
* @constructor
* @implements {base.Disposable}
*/
base.DomEventHook = function(src, eventName, listener, capture) {
this.src_ = src;
this.eventName_ = eventName;
this.listener_ = listener;
this.capture_ = capture;
src.addEventListener(eventName, listener, capture);
};
base.DomEventHook.prototype.dispose = function() {
this.src_.removeEventListener(this.eventName_, this.listener_, this.capture_);
};
/**
* An event hook implementation for Chrome Events.
*
* @param {ChromeEvent|chrome.contextMenus.ClickedEvent|ChromeObjectEvent} src
* @param {!Function} listener
*
* @constructor
* @implements {base.Disposable}
*/
base.ChromeEventHook = function(src, listener) {
this.src_ = src;
this.listener_ = listener;
src.addListener(listener);
};
base.ChromeEventHook.prototype.dispose = function() {
this.src_.removeListener(this.listener_);
};
/**
* A disposable repeating timer.
*
* @param {Function} callback
* @param {number} interval
* @param {boolean=} opt_invokeNow Whether to invoke the callback now, default
* to false.
*
* @constructor
* @implements {base.Disposable}
*/
base.RepeatingTimer = function(callback, interval, opt_invokeNow) {
/** @private */
this.intervalId_ = window.setInterval(callback, interval);
if (opt_invokeNow) {
callback();
}
};
base.RepeatingTimer.prototype.dispose = function() {
window.clearInterval(this.intervalId_);
this.intervalId_ = null;
};
/**
* A disposable one shot timer.
*
* @param {Function} callback
* @param {number} timeout
*
* @constructor
* @implements {base.Disposable}
*/
base.OneShotTimer = function(callback, timeout) {
var that = this;
/** @private */
this.timerId_ = window.setTimeout(function() {
that.timerId_ = null;
callback();
}, timeout);
};
base.OneShotTimer.prototype.dispose = function() {
if (this.timerId_ !== null) {
window.clearTimeout(this.timerId_);
this.timerId_ = null;
}
};
/**
* Converts UTF-8 string to ArrayBuffer.
*
* @param {string} string
* @return {!ArrayBuffer}
*/
base.encodeUtf8 = function(string) {
var utf8String = unescape(encodeURIComponent(string));
var result = new Uint8Array(utf8String.length);
for (var i = 0; i < utf8String.length; i++)
result[i] = utf8String.charCodeAt(i);
return result.buffer;
};
/**
* Decodes UTF-8 string from ArrayBuffer.
*
* @param {ArrayBuffer} buffer
* @return {string}
*/
base.decodeUtf8 = function(buffer) {
return decodeURIComponent(
escape(String.fromCharCode.apply(null, new Uint8Array(buffer))));
};
/**
* Generate a nonce, to be used as an xsrf protection token.
*
* @return {string} A URL-Safe Base64-encoded 128-bit random value. */
base.generateXsrfToken = function() {
var random = new Uint8Array(16);
window.crypto.getRandomValues(random);
var base64Token = window.btoa(String.fromCharCode.apply(null, random));
return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
/**
* @return {string} A random UUID.
*/
base.generateUuid = function() {
var random = new Uint16Array(8);
window.crypto.getRandomValues(random);
/** @type {Array<string>} */
var e = new Array();
for (var i = 0; i < 8; i++) {
e[i] = (/** @type {number} */ (random[i]) + 0x10000).
toString(16).substring(1);
}
return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' +
e[4] + '-' + e[5] + e[6] + e[7];
};
/**
* @param {string} jsonString A JSON-encoded string.
* @return {Object|undefined} The decoded object, or undefined if the string
* cannot be parsed.
*/
base.jsonParseSafe = function(jsonString) {
try {
return /** @type {Object} */ (JSON.parse(jsonString));
} catch (err) {
return undefined;
}
};
/**
* Return the current time as a formatted string suitable for logging.
*
* @return {string} The current time, formatted as the standard ISO string.
* [yyyy-mm-ddDhh:mm:ss.xyz]
*/
base.timestamp = function() {
return '[' + new Date().toISOString() + ']';
};
/**
* A online function that can be stubbed by unit tests.
* @return {boolean}
*/
base.isOnline = function() {
return navigator.onLine;
};
/**
* Size the current window to fit its content.
* @param {boolean=} opt_centerWindow If true, position the window in the
* center of the screen after resizing it.
*/
base.resizeWindowToContent = function(opt_centerWindow) {
var appWindow = chrome.app.window.current();
var borderX = appWindow.outerBounds.width - appWindow.innerBounds.width;
var borderY = appWindow.outerBounds.height - appWindow.innerBounds.height;
var width = Math.ceil(document.documentElement.scrollWidth + borderX);
var height = Math.ceil(document.documentElement.scrollHeight + borderY);
appWindow.outerBounds.width = width;
appWindow.outerBounds.height = height;
if (opt_centerWindow) {
var screenWidth = screen.availWidth;
var screenHeight = screen.availHeight;
appWindow.outerBounds.left = Math.round((screenWidth - width) / 2);
appWindow.outerBounds.top = Math.round((screenHeight - height) / 2);
}
};
/**
* @return {boolean} Whether NaCL is enabled in chrome://plugins.
*/
base.isNaclEnabled = function() {
for (var i = 0; i < navigator.mimeTypes.length; i++) {
if (navigator.mimeTypes.item(i).type == 'application/x-pnacl') {
return true;
}
}
return false;
};
/**
* Alias for document.getElementById that returns an HTMLElement
* @param {string} id The ID of the element to find.
* @return {?HTMLElement} The found element or null if not found.
*/
base.getHtmlElement = function(id) {
var el = document.getElementById(id);
if (el)
console.assert(el instanceof HTMLElement);
return /** @type {HTMLElement} */(el);
};