From 63c34cade40896fd0e2011ce6446c0ceb0774ba6 Mon Sep 17 00:00:00 2001 From: Ibby Hadeed Date: Sat, 11 Mar 2017 11:03:40 -0500 Subject: [PATCH] chore(): organize and enhance decorators (#1171) * fix/add decorators * fix google maps design * chore(): add root tsconfig to resolve imports in IDE * updates * more fixes --- src/@ionic-native/core/bootstrap.ts | 5 +- src/@ionic-native/core/decorators.ts | 339 +++++++++++ src/@ionic-native/core/index.ts | 2 + src/@ionic-native/core/plugin.ts | 533 ++++-------------- src/@ionic-native/core/util.ts | 63 ++- src/@ionic-native/plugins/file/index.ts | 15 +- .../plugins/google-maps/index.ts | 150 ++--- src/@ionic-native/plugins/media/index.ts | 9 +- src/@ionic-native/plugins/sqlite/index.ts | 12 +- tsconfig.json | 18 + 10 files changed, 601 insertions(+), 545 deletions(-) create mode 100644 src/@ionic-native/core/decorators.ts create mode 100644 tsconfig.json diff --git a/src/@ionic-native/core/bootstrap.ts b/src/@ionic-native/core/bootstrap.ts index 059e6f61e..a8557a182 100644 --- a/src/@ionic-native/core/bootstrap.ts +++ b/src/@ionic-native/core/bootstrap.ts @@ -3,7 +3,6 @@ declare var window; export function checkReady() { const DEVICE_READY_TIMEOUT = 5000; - // To help developers using cordova, we listen for the device ready event and // log an error if it didn't fire in a reasonable amount of time. Generally, // when this happens, developers should remove and reinstall plugins, since @@ -12,13 +11,13 @@ export function checkReady() { let didFireReady = false; document.addEventListener('deviceready', () => { - console.log('DEVICE READY FIRED AFTER', (Date.now() - before), 'ms'); + console.log(`Ionic Native: deviceready event fired after ${(Date.now() - before)} ms`); didFireReady = true; }); setTimeout(() => { if (!didFireReady && window.cordova) { - console.warn(`Native: deviceready did not fire within ${DEVICE_READY_TIMEOUT}ms. This can happen when plugins are in an inconsistent state. Try removing plugins from plugins/ and reinstalling them.`); + console.warn(`Ionic Native: deviceready did not fire within ${DEVICE_READY_TIMEOUT}ms. This can happen when plugins are in an inconsistent state. Try removing plugins from plugins/ and reinstalling them.`); } }, DEVICE_READY_TIMEOUT); } diff --git a/src/@ionic-native/core/decorators.ts b/src/@ionic-native/core/decorators.ts new file mode 100644 index 000000000..f9eac67bb --- /dev/null +++ b/src/@ionic-native/core/decorators.ts @@ -0,0 +1,339 @@ +import { instanceAvailability, checkAvailability, wrap, wrapInstance, overrideFunction } from './plugin'; +import { getPlugin, getPromise } from './util'; +import { Observable } from 'rxjs/Observable'; + +export interface PluginConfig { + /** + * Plugin name, this should match the class name + */ + pluginName: string; + /** + * Plugin NPM package name + */ + plugin: string; + /** + * Plugin object reference + */ + pluginRef?: string; + /** + * Github repository URL + */ + repo?: string; + /** + * Custom install command + */ + install?: string; + /** + * Available installation variables + */ + installVariables?: string[]; + /** + * Supported platforms + */ + platforms?: string[]; +} + +export interface CordovaOptions { + /** + * Set to true if the wrapped method is a sync function + */ + sync?: boolean; + /** + * Callback order. Set to reverse if the success/error callbacks are the first 2 arguments that the wrapped method takes. + */ + callbackOrder?: 'reverse'; + /** + * Callback style + */ + callbackStyle?: 'node' | 'object'; + /** + * Set a custom index for the success callback function. This doesn't work if callbackOrder or callbackStyle are set. + */ + successIndex?: number; + /** + * Set a custom index for the error callback function. This doesn't work if callbackOrder or callbackStyle are set. + */ + errorIndex?: number; + /** + * Success function property name. This must be set if callbackStyle is set to object. + */ + successName?: string; + /** + * Error function property name. This must be set if callbackStyle is set to object. + */ + errorName?: string; + /** + * Set to true to return an observable + */ + observable?: boolean; + /** + * If observable is set to true, this can be set to a different function name that will cancel the observable. + */ + clearFunction?: string; + /** + * This can be used if clearFunction is set. Set this to true to call the clearFunction with the same arguments used in the initial function. + */ + clearWithArgs?: boolean; + /** + * Creates an observable that wraps a global event. Replaces document.addEventListener + */ + eventObservable?: boolean; + /** + * Event name, this must be set if eventObservable is set to true + */ + event?: string; + /** + * Element to attach the event listener to, this is optional, defaults to `window` + */ + element?: any; + /** + * Set to true if the wrapped method returns a promise + */ + otherPromise?: boolean; + /** + * Supported platforms + */ + platforms?: string[]; +} + +export interface CordovaCheckOptions { + promise?: boolean; + observable?: boolean; +} + +export interface CordovaFiniteObservableOptions extends CordovaOptions { + /** + * Function that gets a result returned from plugin's success callback, and decides whether it is last value and observable should complete. + */ + resultFinalPredicate?: (result: any) => boolean; + /** + * Function that gets called after resultFinalPredicate, and removes service data that indicates end of stream from the result. + */ + resultTransform?: (result: any) => any; +} + +/** + * @private + */ +export function InstanceCheck() { + return (pluginObj: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { + return { + value: function(...args: any[]) { + if (instanceAvailability(pluginObj, methodName)) { + descriptor.value.apply(this, args); + } else { + return null; + } + } + }; + }; +} + +/** + * Executes function only if plugin is available + * @private + */ +export function CordovaCheck(opts: CordovaCheckOptions = {}) { + return (pluginObj: Object, methodName: string, descriptor: TypedPropertyDescriptor): TypedPropertyDescriptor => { + return { + value: function(...args: any[]): any { + if (checkAvailability(pluginObj, methodName) === true) { + descriptor.value.apply(this, args); + } else { + + if (opts.promise) { + return getPromise(() => {}); + } else if (opts.observable) { + return new Observable(observer => observer.complete()); + } + + return null; + + } + } + }; + }; +} + + +/** + * @private + * + * Class decorator specifying Plugin metadata. Required for all plugins. + * + * @usage + * ```typescript + * @Plugin({ + * pluginName: 'MyPlugin', + * plugin: 'cordova-plugin-myplugin', + * pluginRef: 'window.myplugin' + * }) + * export class MyPlugin { + * + * // Plugin wrappers, properties, and functions go here ... + * + * } + * ``` + */ +export function Plugin(config: PluginConfig) { + return function(cls) { + + // Add these fields to the class + for (let k in config) { + cls[k] = config[k]; + } + + cls['installed'] = function(printWarning?: boolean) { + return !!getPlugin(config.pluginRef); + }; + + cls['getPlugin'] = function() { + return getPlugin(config.pluginRef); + }; + + cls['checkInstall'] = function() { + return checkAvailability(cls) === true; + }; + + cls['getPluginName'] = function() { + return config.pluginName; + }; + cls['getPluginRef'] = function() { + return config.pluginRef; + }; + cls['getPluginInstallName'] = function() { + return config.plugin; + }; + cls['getPluginRepo'] = function() { + return config.repo; + }; + cls['getSupportedPlatforms'] = function() { + return config.platforms; + }; + + return cls; + }; +} + +/** + * @private + * + * Wrap a stub function in a call to a Cordova plugin, checking if both Cordova + * and the required plugin are installed. + */ +export function Cordova(opts: CordovaOptions = {}) { + return (target: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { + return { + value: function(...args: any[]) { + return wrap(this, methodName, opts).apply(this, args); + } + }; + }; +} + +/** + * @private + * + * Wrap an instance method + */ +export function CordovaInstance(opts: any = {}) { + return (target: Object, methodName: string) => { + return { + value: function(...args: any[]) { + return wrapInstance(this, methodName, opts).apply(this, args); + } + }; + }; +} + +/** + * @private + * + * + * Before calling the original method, ensure Cordova and the plugin are installed. + */ +export function CordovaProperty(target: any, key: string) { + Object.defineProperty(target, key, { + get: () => { + if (checkAvailability(target, key) === true) { + return getPlugin(target.constructor.getPluginRef())[key]; + } else { + return null; + } + }, + set: (value) => { + if (checkAvailability(target, key) === true) { + getPlugin(target.constructor.getPluginRef())[key] = value; + } + } + }); +} + +/** + * @private + * @param target + * @param key + * @constructor + */ +export function InstanceProperty(target: any, key: string) { + Object.defineProperty(target, key, { + get: function(){ + return this._objectInstance[key]; + }, + set: function(value){ + this._objectInstance[key] = value; + } + }); +} + +/** + * @private + * + * Wrap a stub function in a call to a Cordova plugin, checking if both Cordova + * and the required plugin are installed. + */ +export function CordovaFunctionOverride(opts: any = {}) { + return (target: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { + return { + value: function(...args: any[]) { + return overrideFunction(this, methodName, opts); + } + }; + }; +} + + +/** + * @private + * + * Wraps method that returns an observable that can be completed. Provided opts.resultFinalPredicate dictates when the observable completes. + * + */ +export function CordovaFiniteObservable(opts: CordovaFiniteObservableOptions = {}) { + if (opts.observable === false) { + throw new Error('CordovaFiniteObservable decorator can only be used on methods that returns observable. Please provide correct option.'); + } + opts.observable = true; + return (target: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { + return { + value: function(...args: any[]) { + let wrappedObservable: Observable = wrap(this, methodName, opts).apply(this, args); + return new Observable((observer) => { + let wrappedSubscription = wrappedObservable.subscribe({ + next: (x) => { + observer.next(opts.resultTransform ? opts.resultTransform(x) : x); + if (opts.resultFinalPredicate && opts.resultFinalPredicate(x)) { + observer.complete(); + } + }, + error: (err) => { observer.error(err); }, + complete: () => { observer.complete(); } + }); + return () => { + wrappedSubscription.unsubscribe(); + }; + }); + } + }; + }; +} diff --git a/src/@ionic-native/core/index.ts b/src/@ionic-native/core/index.ts index 1110b6451..483a69500 100644 --- a/src/@ionic-native/core/index.ts +++ b/src/@ionic-native/core/index.ts @@ -1 +1,3 @@ export * from './plugin'; +export * from './decorators'; +export * from './util'; diff --git a/src/@ionic-native/core/plugin.ts b/src/@ionic-native/core/plugin.ts index 2aaac517b..9344ec385 100644 --- a/src/@ionic-native/core/plugin.ts +++ b/src/@ionic-native/core/plugin.ts @@ -1,151 +1,61 @@ -import { get } from './util'; +import { get, getPlugin, getPromise, cordovaWarn, pluginWarn } from './util'; import { checkReady } from './bootstrap'; +import { CordovaOptions } from './decorators'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/fromEvent'; - checkReady(); declare var window; declare var Promise; + /** + * Checks if plugin/cordova is available + * @return {boolean | { error: string } } * @private */ -export interface PluginConfig { - /** - * Plugin name, this should match the class name - */ - pluginName: string; - /** - * Plugin NPM package name - */ - plugin: string; - /** - * Plugin object reference - */ - pluginRef: string; - /** - * Github repository URL - */ - repo: string; - /** - * Custom install command - */ - install?: string; - /** - * Available installation variables - */ - installVariables?: string[]; - /** - * Supported platforms - */ - platforms?: string[]; +export function checkAvailability(pluginRef: string, methodName?: string, pluginName?: string); +export function checkAvailability(pluginObj: any, methodName?: string, pluginName?: string); +export function checkAvailability(plugin: any, methodName?: string, pluginName?: string): boolean | { error: string } { + + let pluginRef, pluginInstance; + + if (typeof plugin === 'string') { + pluginRef = plugin; + } else { + pluginRef = plugin.constructor.getPluginRef(); + pluginName = plugin.constructor.getPluginName(); + } + + pluginInstance = getPlugin(pluginRef); + + if (!pluginInstance || (!!methodName && pluginInstance[methodName] === 'undefined')) { + if (!window.cordova) { + cordovaWarn(pluginName, methodName); + return { + error: 'cordova_not_available' + }; + } + + pluginWarn(pluginName, methodName); + return { + error: 'plugin_not_installed' + }; + } + + return true; } /** + * Checks if _objectInstance exists and has the method/property * @private */ -export interface CordovaOptions { - /** - * Set to true if the wrapped method is a sync function - */ - sync?: boolean; - /** - * Callback order. Set to reverse if the success/error callbacks are the first 2 arguments that the wrapped method takes. - */ - callbackOrder?: 'reverse'; - /** - * Callback style - */ - callbackStyle?: 'node' | 'object'; - /** - * Set a custom index for the success callback function. This doesn't work if callbackOrder or callbackStyle are set. - */ - successIndex?: number; - /** - * Set a custom index for the error callback function. This doesn't work if callbackOrder or callbackStyle are set. - */ - errorIndex?: number; - /** - * Success function property name. This must be set if callbackStyle is set to object. - */ - successName?: string; - /** - * Error function property name. This must be set if callbackStyle is set to object. - */ - errorName?: string; - /** - * Set to true to return an observable - */ - observable?: boolean; - /** - * If observable is set to true, this can be set to a different function name that will cancel the observable. - */ - clearFunction?: string; - /** - * This can be used if clearFunction is set. Set this to true to call the clearFunction with the same arguments used in the initial function. - */ - clearWithArgs?: boolean; - /** - * Creates an observable that wraps a global event. Replaces document.addEventListener - */ - eventObservable?: boolean; - /** - * Event name, this must be set if eventObservable is set to true - */ - event?: string; - /** - * Element to attach the event listener to, this is optional, defaults to `window` - */ - element?: any; - /** - * Set to true if the wrapped method returns a promise - */ - otherPromise?: boolean; - /** - * Supported platforms - */ - platforms?: string[]; +export function instanceAvailability(pluginObj: any, methodName: string): boolean { + return pluginObj._objectInstance && pluginObj._objectInstance[methodName] !== 'undefined'; } -/** - * @private - * @param pluginRef - * @returns {null|*} - */ -export const getPlugin = function(pluginRef: string): any { - return get(window, pluginRef); -}; - -/** - * @private - * @param pluginObj - * @param method - */ -export const pluginWarn = function(pluginObj: any, method?: string) { - let pluginName = pluginObj.constructor.getPluginName(), plugin = pluginObj.constructor.getPlugin(); - if (method) { - console.warn('Native: tried calling ' + pluginName + '.' + method + ', but the ' + pluginName + ' plugin is not installed.'); - } else { - console.warn('Native: tried accessing the ' + pluginName + ' plugin but it\'s not installed.'); - } - console.warn('Install the ' + pluginName + ' plugin: \'ionic plugin add ' + plugin + '\''); -}; - -/** - * @private - * @param pluginName - * @param method - */ -export const cordovaWarn = function(pluginName: string, method: string) { - if (method) { - console.warn('Native: tried calling ' + pluginName + '.' + method + ', but Cordova is not available. Make sure to include cordova.js or run in a device/simulator'); - } else { - console.warn('Native: tried accessing the ' + pluginName + ' plugin but Cordova is not available. Make sure to include cordova.js or run in a device/simulator'); - } -}; function setIndex(args: any[], opts: any = {}, resolve?: Function, reject?: Function): any { // ignore resolve and reject in case sync if (opts.sync) { @@ -212,42 +122,15 @@ function callCordovaPlugin(pluginObj: any, methodName: string, args: any[], opts // to our promise resolve/reject handlers. args = setIndex(args, opts, resolve, reject); - let pluginInstance = getPlugin(pluginObj.constructor.getPluginRef()); + const availabilityCheck = checkAvailability(pluginObj, methodName); - if (!pluginInstance || pluginInstance[methodName] === 'undefined') { - // Do this check in here in the case that the Web API for this plugin is available (for example, Geolocation). - if (!window.cordova) { - cordovaWarn(pluginObj.constructor.getPluginName(), methodName); - return { - error: 'cordova_not_available' - }; - } - - pluginWarn(pluginObj, methodName); - return { - error: 'plugin_not_installed' - }; + if (availabilityCheck === true) { + const pluginInstance = getPlugin(pluginObj.constructor.getPluginRef()); + return pluginInstance[methodName].apply(pluginInstance, args); + } else { + return availabilityCheck; } - return pluginInstance[methodName].apply(pluginInstance, args); -} - -/** - * @private - */ -export function getPromise(cb) { - - const tryNativePromise = () => { - if (window.Promise) { - return new Promise((resolve, reject) => { - cb(resolve, reject); - }); - } else { - console.error('No Promise support or polyfill found. To enable Ionic Native support, please add the es6-promise polyfill before this script, or run with a library like Angular 2 or on a recent browser.'); - } - }; - - return tryNativePromise(); } function wrapPromise(pluginObj: any, methodName: string, args: any[], opts: any = {}) { @@ -281,6 +164,7 @@ function wrapObservable(pluginObj: any, methodName: string, args: any[], opts: a let pluginResult = callCordovaPlugin(pluginObj, methodName, args, opts, observer.next.bind(observer), observer.error.bind(observer)); if (pluginResult && pluginResult.error) { observer.error(pluginResult.error); + observer.complete(); } return () => { try { @@ -299,50 +183,23 @@ function wrapObservable(pluginObj: any, methodName: string, args: any[], opts: a } function callInstance(pluginObj: any, methodName: string, args: any[], opts: any = {}, resolve?: Function, reject?: Function) { - args = setIndex(args, opts, resolve, reject); - return pluginObj._objectInstance[methodName].apply(pluginObj._objectInstance, args); -} -function wrapInstance(pluginObj: any, methodName: string, opts: any = {}) { - return (...args) => { - if (opts.sync) { - // Sync doesn't wrap the plugin with a promise or observable, it returns the result as-is - return callInstance(pluginObj, methodName, args, opts); - } else if (opts.observable) { - return new Observable(observer => { - let pluginResult = callInstance(pluginObj, methodName, args, opts, observer.next.bind(observer), observer.error.bind(observer)); - return () => { - try { - if (opts.clearWithArgs) { - return pluginObj._objectInstance[opts.clearFunction].apply(pluginObj._objectInstance, args); - } - return pluginObj._objectInstance[opts.clearFunction].call(pluginObj, pluginResult); - } catch (e) { - console.warn('Unable to clear the previous observable watch for', pluginObj.constructor.getPluginName(), methodName); - console.error(e); - } - }; - }); - } else if (opts.otherPromise) { - return getPromise((resolve, reject) => { - let result = callInstance(pluginObj, methodName, args, opts, resolve, reject); - result.then(resolve, reject); - }); - } else { - return getPromise((resolve, reject) => { - callInstance(pluginObj, methodName, args, opts, resolve, reject); - }); - } - }; + args = setIndex(args, opts, resolve, reject); + + if (instanceAvailability(pluginObj, methodName)) { + return pluginObj._objectInstance[methodName].apply(pluginObj._objectInstance, args); + } + } /** * Wrap the event with an observable + * @private * @param event even name * @param element The element to attach the event listener to * @returns {Observable} */ -function wrapEventObservable(event: string, element: any = window): Observable { +export function wrapEventObservable(event: string, element: any = window): Observable { return Observable.fromEvent(element, event); } @@ -352,39 +209,28 @@ function wrapEventObservable(event: string, element: any = window): Observable { +export function overrideFunction(pluginObj: any, methodName: string, args: any[], opts: any = {}): Observable { return new Observable(observer => { - let pluginInstance = getPlugin(pluginObj.constructor.getPluginRef()); + const availabilityCheck = checkAvailability(pluginObj, methodName); - if (!pluginInstance) { - // Do this check in here in the case that the Web API for this plugin is available (for example, Geolocation). - if (!window.cordova) { - cordovaWarn(pluginObj.constructor.getPluginName(), methodName); - observer.error({ - error: 'cordova_not_available' - }); - } - - pluginWarn(pluginObj, methodName); - observer.error({ - error: 'plugin_not_installed' - }); - return; + if (availabilityCheck === true) { + const pluginInstance = getPlugin(pluginObj.constructor.getPluginRef()); + pluginInstance[methodName] = observer.next.bind(observer); + return () => pluginInstance[methodName] = () => {}; + } else { + observer.error(availabilityCheck); + observer.complete(); } - pluginInstance[methodName] = observer.next.bind(observer); }); } /** * @private - * @param pluginObj - * @param methodName - * @param opts - * @returns {function(...[any]): (undefined|*|Observable|*|*)} */ export const wrap = function(pluginObj: any, methodName: string, opts: CordovaOptions = {}) { return (...args) => { @@ -403,213 +249,60 @@ export const wrap = function(pluginObj: any, methodName: string, opts: CordovaOp }; }; - /** * @private - * - * Class decorator specifying Plugin metadata. Required for all plugins. - * - * @usage - * ```typescript - * @Plugin({ - * pluginName: 'MyPlugin', - * plugin: 'cordova-plugin-myplugin', - * pluginRef: 'window.myplugin' - * }) - * export class MyPlugin { - * - * // Plugin wrappers, properties, and functions go here ... - * - * } - * ``` */ -export function Plugin(config: PluginConfig) { - return function(cls) { +export function wrapInstance(pluginObj: any, methodName: string, opts: any = {}) { + return (...args) => { + if (opts.sync) { + + return callInstance(pluginObj, methodName, args, opts); + + } else if (opts.observable) { + + return new Observable(observer => { + let pluginResult = callInstance(pluginObj, methodName, args, opts, observer.next.bind(observer), observer.error.bind(observer)); + + if (pluginResult && pluginResult.error) { + observer.error(pluginResult.error); + observer.complete(); + } + + return () => { + try { + if (opts.clearWithArgs) { + return pluginObj._objectInstance[opts.clearFunction].apply(pluginObj._objectInstance, args); + } + return pluginObj._objectInstance[opts.clearFunction].call(pluginObj, pluginResult); + } catch (e) { + console.warn('Unable to clear the previous observable watch for', pluginObj.constructor.getPluginName(), methodName); + console.error(e); + } + }; + }); + + } else if (opts.otherPromise) { + + return getPromise((resolve, reject) => { + let result = callInstance(pluginObj, methodName, args, opts, resolve, reject); + if (result && !result.error) { + result.then(resolve, reject); + } + }); + + } else { + + let pluginResult, rej; + const p = getPromise((resolve, reject) => { + pluginResult = callInstance(pluginObj, methodName, args, opts, resolve, reject); + rej = reject; + }); + if (pluginResult && pluginResult.error) { + p.catch(() => { }); + rej(pluginResult.error); + } + return p; - // Add these fields to the class - for (let k in config) { - cls[k] = config[k]; } - - cls['installed'] = function(printWarning?: boolean) { - return !!getPlugin(config.pluginRef); - }; - - cls['getPlugin'] = function() { - return getPlugin(config.pluginRef); - }; - - cls['checkInstall'] = function() { - let pluginInstance = getPlugin(config.pluginRef); - - if (!pluginInstance) { - pluginWarn(cls); - return false; - } - return true; - }; - - cls['getPluginName'] = function() { - return config.pluginName; - }; - cls['getPluginRef'] = function() { - return config.pluginRef; - }; - cls['getPluginInstallName'] = function() { - return config.plugin; - }; - cls['getPluginRepo'] = function() { - return config.repo; - }; - cls['getSupportedPlatforms'] = function() { - return config.platforms; - }; - - return cls; - }; -} - -/** - * @private - * - * Wrap a stub function in a call to a Cordova plugin, checking if both Cordova - * and the required plugin are installed. - */ -export function Cordova(opts: CordovaOptions = {}) { - return (target: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { - return { - value: function(...args: any[]) { - return wrap(this, methodName, opts).apply(this, args); - } - }; - }; -} - -/** - * @private - * - * Wrap an instance method - */ -export function CordovaInstance(opts: any = {}) { - return (target: Object, methodName: string) => { - return { - value: function(...args: any[]) { - return wrapInstance(this, methodName, opts).apply(this, args); - } - }; - }; -} - -/** - * @private - * - * - * Before calling the original method, ensure Cordova and the plugin are installed. - */ -export function CordovaProperty(target: any, key: string) { - const exists = () => { - let pluginInstance = getPlugin(target.pluginRef); - if (!pluginInstance || typeof pluginInstance[key] === 'undefined') { - pluginWarn(target, key); - return false; - } - return true; - }; - - Object.defineProperty(target, key, { - get: () => { - if (exists()) { - return getPlugin(target.pluginRef)[key]; - } else { - return null; - } - }, - set: (value) => { - if (exists()) { - getPlugin(target.pluginRef)[key] = value; - } - } - }); -} - -/** - * @private - * @param target - * @param key - * @constructor - */ -export function InstanceProperty(target: any, key: string) { - Object.defineProperty(target, key, { - get: function(){ - return this._objectInstance[key]; - }, - set: function(value){ - this._objectInstance[key] = value; - } - }); -} - -/** - * @private - * - * Wrap a stub function in a call to a Cordova plugin, checking if both Cordova - * and the required plugin are installed. - */ -export function CordovaFunctionOverride(opts: any = {}) { - return (target: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { - return { - value: function(...args: any[]) { - return overrideFunction(this, methodName, opts); - } - }; - }; -} - -/** - * @private - */ -export interface CordovaFiniteObservableOptions extends CordovaOptions { - /** - * Function that gets a result returned from plugin's success callback, and decides whether it is last value and observable should complete. - */ - resultFinalPredicate?: (result: any) => boolean; - /** - * Function that gets called after resultFinalPredicate, and removes service data that indicates end of stream from the result. - */ - resultTransform?: (result: any) => any; -} - -/** - * @private - * - * Wraps method that returns an observable that can be completed. Provided opts.resultFinalPredicate dictates when the observable completes. - * - */ -export function CordovaFiniteObservable(opts: CordovaFiniteObservableOptions = {}) { - if (opts.observable === false) { - throw new Error('CordovaFiniteObservable decorator can only be used on methods that returns observable. Please provide correct option.'); - } - opts.observable = true; - return (target: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { - return { - value: function(...args: any[]) { - let wrappedObservable: Observable = wrap(this, methodName, opts).apply(this, args); - return new Observable((observer) => { - let wrappedSubscription = wrappedObservable.subscribe({ - next: (x) => { - observer.next(opts.resultTransform ? opts.resultTransform(x) : x); - if (opts.resultFinalPredicate && opts.resultFinalPredicate(x)) { - observer.complete(); - } - }, - error: (err) => { observer.error(err); }, - complete: () => { observer.complete(); } - }); - return () => { - wrappedSubscription.unsubscribe(); - }; - }); - } - }; }; } diff --git a/src/@ionic-native/core/util.ts b/src/@ionic-native/core/util.ts index 397bce0e2..cfc43e5f1 100644 --- a/src/@ionic-native/core/util.ts +++ b/src/@ionic-native/core/util.ts @@ -1,7 +1,68 @@ +declare var window: any; + +/** + * @private + */ export function get(obj, path) { - for (var i = 0, path = path.split('.'), len = path.length; i < len; i++) { + path = path.split('.'); + for (let i = 0; i < path.length; i++) { if (!obj) { return null; } obj = obj[path[i]]; } return obj; +} + + +/** + * @private + */ +export function getPromise(cb) { + + const tryNativePromise = () => { + if (window.Promise) { + return new Promise((resolve, reject) => { + cb(resolve, reject); + }); + } else { + console.error('No Promise support or polyfill found. To enable Ionic Native support, please add the es6-promise polyfill before this script, or run with a library like Angular 2 or on a recent browser.'); + } + }; + + return tryNativePromise(); +} + +/** + * @private + * @param pluginRef + * @returns {null|*} + */ +export function getPlugin(pluginRef: string): any { + return get(window, pluginRef); +}; + +/** + * @private + */ +export const pluginWarn = function(pluginName: string, plugin?: string, method?: string) { + if (method) { + console.warn('Native: tried calling ' + pluginName + '.' + method + ', but the ' + pluginName + ' plugin is not installed.'); + } else { + console.warn('Native: tried accessing the ' + pluginName + ' plugin but it\'s not installed.'); + } + if (plugin) { + console.warn('Install the ' + pluginName + ' plugin: \'ionic plugin add ' + plugin + '\''); + } +}; + +/** + * @private + * @param pluginName + * @param method + */ +export const cordovaWarn = function(pluginName: string, method?: string) { + if (method) { + console.warn('Native: tried calling ' + pluginName + '.' + method + ', but Cordova is not available. Make sure to include cordova.js or run in a device/simulator'); + } else { + console.warn('Native: tried accessing the ' + pluginName + ' plugin but Cordova is not available. Make sure to include cordova.js or run in a device/simulator'); + } }; diff --git a/src/@ionic-native/plugins/file/index.ts b/src/@ionic-native/plugins/file/index.ts index 7c50d8f5c..18d7ab140 100644 --- a/src/@ionic-native/plugins/file/index.ts +++ b/src/@ionic-native/plugins/file/index.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { CordovaProperty, Plugin, pluginWarn } from '@ionic-native/core'; +import { CordovaProperty, Plugin, CordovaCheck } from '@ionic-native/core'; declare var window: any; declare var cordova: any; @@ -461,17 +461,12 @@ export class File { * Get free disk space in Bytes * @returns {Promise} Returns a promise that resolves with the remaining free disk space in Bytes */ + @CordovaCheck({ + promise: true + }) getFreeDiskSpace(): Promise { return new Promise((resolve, reject) => { - if (!cordova || !cordova.exec) { - pluginWarn({ - pluginName: 'File', - plugin: 'cordova-plugin-file' - }); - reject({ error: 'plugin_not_installed' }); - } else { - cordova.exec(resolve, reject, 'File', 'getFreeDiskSpace', []); - } + cordova.exec(resolve, reject, 'File', 'getFreeDiskSpace', []); }); } diff --git a/src/@ionic-native/plugins/google-maps/index.ts b/src/@ionic-native/plugins/google-maps/index.ts index 16864a984..08cc16f93 100644 --- a/src/@ionic-native/plugins/google-maps/index.ts +++ b/src/@ionic-native/plugins/google-maps/index.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Cordova, CordovaInstance, Plugin, InstanceProperty, getPlugin, pluginWarn } from '@ionic-native/core'; +import { Cordova, CordovaInstance, CordovaCheck, Plugin, InstanceProperty, InstanceCheck, checkAvailability } from '@ionic-native/core'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/fromEvent'; @@ -53,28 +53,19 @@ export const GoogleMapsMapTypeId = { /** * @private */ +@Plugin({ + pluginName: 'GoogleMaps', + plugin: 'cordova-plugin-googlemaps' +}) export class GoogleMap { _objectInstance: any; - /** - * Checks if a map object has been created and is available. - * - * @returns {Promise} - */ - @Cordova() - isAvailable(): Promise { return; } - constructor(element: string | HTMLElement, options?: any) { - if (!!getPlugin('plugin.google.maps.Map')) { + if (checkAvailability('plugin.google.maps.Map', null, 'GoogleMaps') === true) { if (typeof element === 'string') { element = document.getElementById(element); } this._objectInstance = plugin.google.maps.Map.getMap(element, options); - } else { - pluginWarn({ - pluginName: 'GoogleMap', - plugin: 'plugin.google.maps.Map' - }); } } @@ -83,6 +74,7 @@ export class GoogleMap { * * @returns {Observable} */ + @InstanceCheck() addEventListener(eventName: string): Observable { return Observable.fromEvent(this._objectInstance, eventName); } @@ -92,13 +84,9 @@ export class GoogleMap { * * @returns {Promise} */ + @InstanceCheck() addListenerOnce(eventName: string): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - resolve => this._objectInstance.addListenerOnce(eventName, resolve) - ); + return new Promise(resolve => this._objectInstance.addListenerOnce(eventName, resolve)); } /** @@ -121,19 +109,12 @@ export class GoogleMap { * * @returns {Observable} */ + @InstanceCheck() on(eventName: string): Observable { - if (!this._objectInstance) { - return new Observable((observer) => { - observer.error({ error: 'plugin_not_installed' }); - }); - } - - return new Observable( - (observer) => { + return new Observable((observer) => { this._objectInstance.on(eventName, observer.next.bind(observer)); return () => this._objectInstance.off(event); - } - ); + }); } /** @@ -141,13 +122,9 @@ export class GoogleMap { * * @returns {Promise} */ + @InstanceCheck() one(eventName: string): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - resolve => this._objectInstance.one(eventName, resolve) - ); + return new Promise(resolve => this._objectInstance.one(eventName, resolve)); } /** @@ -237,12 +214,9 @@ export class GoogleMap { /** * @returns {Promise} */ + @InstanceCheck() addMarker(options: MarkerOptions): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - (resolve, reject) => { + return new Promise((resolve, reject) => { this._objectInstance.addMarker(options, (marker: any) => { if (marker) { resolve(new Marker(marker)); @@ -250,19 +224,15 @@ export class GoogleMap { reject(); } }); - } - ); + }); } /** * @returns {Promise} */ + @InstanceCheck() addCircle(options: CircleOptions): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - (resolve, reject) => { + return new Promise((resolve, reject) => { this._objectInstance.addCircle(options, (circle: any) => { if (circle) { resolve(new Circle(circle)); @@ -270,19 +240,15 @@ export class GoogleMap { reject(); } }); - } - ); + }); } /** * @returns {Promise} */ + @InstanceCheck() addPolygon(options: PolygonOptions): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - (resolve, reject) => { + return new Promise((resolve, reject) => { this._objectInstance.addPolygon(options, (polygon: any) => { if (polygon) { resolve(new Polygon(polygon)); @@ -290,19 +256,15 @@ export class GoogleMap { reject(); } }); - } - ); + }); } /** * @returns {Promise} */ + @InstanceCheck() addPolyline(options: PolylineOptions): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - (resolve, reject) => { + return new Promise((resolve, reject) => { this._objectInstance.addPolyline(options, (polyline: any) => { if (polyline) { resolve(new Polyline(polyline)); @@ -310,19 +272,15 @@ export class GoogleMap { reject(); } }); - } - ); + }); } /** * @returns {Promise} */ + @InstanceCheck() addTileOverlay(options: TileOverlayOptions): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - (resolve, reject) => { + return new Promise((resolve, reject) => { this._objectInstance.addTileOverlay(options, (tileOverlay: any) => { if (tileOverlay) { resolve(new TileOverlay(tileOverlay)); @@ -330,19 +288,15 @@ export class GoogleMap { reject(); } }); - } - ); + }); } /** * @returns {Promise} */ + @InstanceCheck() addGroundOverlay(options: GroundOverlayOptions): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - (resolve, reject) => { + return new Promise((resolve, reject) => { this._objectInstance.addGroundOverlay(options, (groundOverlay: any) => { if (groundOverlay) { resolve(new GroundOverlay(groundOverlay)); @@ -350,19 +304,15 @@ export class GoogleMap { reject(); } }); - } - ); + }); } /** * @returns {Promise} */ + @InstanceCheck() addKmlOverlay(options: KmlOverlayOptions): Promise { - if (!this._objectInstance) { - return Promise.reject({ error: 'plugin_not_installed' }); - } - return new Promise( - (resolve, reject) => { + return new Promise((resolve, reject) => { this._objectInstance.addKmlOverlay(options, (kmlOverlay: any) => { if (kmlOverlay) { resolve(new KmlOverlay(kmlOverlay)); @@ -370,8 +320,7 @@ export class GoogleMap { reject(); } }); - } - ); + }); } @CordovaInstance({ sync: true }) @@ -500,6 +449,14 @@ export class GoogleMap { @Injectable() export class GoogleMaps { + /** + * Checks if a map object has been created and is available. + * + * @returns {Promise} + */ + @Cordova() + isAvailable(): Promise { return; } + /** * Creates a new GoogleMap instance * @param element {string | HTMLElement} Element ID or reference to attach the map to @@ -1782,23 +1739,24 @@ export interface GeocoderResult { /** * @private */ +@Plugin({ + pluginName: 'Geocoder', + pluginRef: 'plugins.google.maps.Geocoder', + plugin: 'cordova-plugin-googlemaps', + repo: '' +}) export class Geocoder { /** * Converts position to address and vice versa * @param {GeocoderRequest} request Request object with either an address or a position * @returns {Promise} */ + @CordovaCheck({ + promise: true + }) static geocode(request: GeocoderRequest): Promise { - return new Promise((resolve, reject) => { - if (!plugin || !plugin.google || !plugin.google.maps || !plugin.google.maps.Geocoder) { - pluginWarn({ - pluginName: 'GoogleMap', - plugin: 'plugin.google.maps.Map' - }); - reject({ error: 'plugin_not_installed' }); - } else { - plugin.google.maps.Geocoder.geocode(request, resolve); - } + return new Promise(resolve => { + plugin.google.maps.Geocoder.geocode(request, resolve); }); } } diff --git a/src/@ionic-native/plugins/media/index.ts b/src/@ionic-native/plugins/media/index.ts index 85bec9464..3a6a00c94 100644 --- a/src/@ionic-native/plugins/media/index.ts +++ b/src/@ionic-native/plugins/media/index.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { CordovaInstance, Plugin, getPlugin, pluginWarn } from '@ionic-native/core'; +import { CordovaInstance, Plugin, checkAvailability } from '@ionic-native/core'; declare var Media: any; @@ -64,15 +64,10 @@ export class MediaObject { * @param onStatusUpdate {Function} A callback function to be invoked when the status of the file changes */ constructor(src: string, onStatusUpdate?: Function) { - if (!!getPlugin('Media')) { + if (checkAvailability('Media', null, 'Media') === true) { this.init = new Promise((resolve, reject) => { this._objectInstance = new Media(src, resolve, reject, onStatusUpdate); }); - } else { - pluginWarn({ - pluginName: 'MediaPlugin', - plugin: 'cordova-plugin-media' - }); } } diff --git a/src/@ionic-native/plugins/sqlite/index.ts b/src/@ionic-native/plugins/sqlite/index.ts index 4d7812218..18d48cce8 100644 --- a/src/@ionic-native/plugins/sqlite/index.ts +++ b/src/@ionic-native/plugins/sqlite/index.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Cordova, CordovaInstance, Plugin, pluginWarn, InstanceProperty } from '@ionic-native/core'; +import { Cordova, CordovaInstance, Plugin, CordovaCheck, InstanceProperty } from '@ionic-native/core'; declare var sqlitePlugin; @@ -199,16 +199,12 @@ export class SQLite { * @param config the config for opening the database. * @return Promise */ + @CordovaCheck({ + promise: true + }) create(config: any): Promise { return new Promise((resolve, reject) => { - if (typeof sqlitePlugin !== 'undefined') { sqlitePlugin.openDatabase(config, db => resolve(new SQLiteObject(db)), reject); - } else { - pluginWarn({ - pluginName: 'SQLite', - plugin: 'cordova-sqlite-storage' - }); - } }); } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..d5607c05a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "stripInternal": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "module": "es2015", + "moduleResolution": "node", + "paths": { + "@ionic-native/core": ["./dist/packages-dist/@ionic-native/core"] + }, + "rootDir": ".", + "target": "es5", + "skipLibCheck": true, + "lib": ["es2015", "dom"] + } +}