import {EventEmitter, NgZone} from 'angular2/core'; import {Config} from '../config/config'; import {getQuerystring} from '../util/util'; import {ready, windowDimensions, flushDimensionCache} from '../util/dom'; /** * @name Platform * @description * Platform returns the availble information about your current platform. * Platforms in Ionic 2 are much more complex then in V1, returns not just a single platform, * but a hierarchy of information, such as a devices OS, phone vs tablet, or mobile vs browser. * With this information you can completely custimize your app to fit any device and platform. * * @usage * ```ts * import {Platform} from 'ionic-angular'; * * @Page({...}) * export MyPage { * constructor(platform: Platform){ * this.platform = platform; * } * } * ``` * @demo /docs/v2/demos/platform/ */ export class Platform { private _platforms: Array; private _versions: {[name: string]: PlatformVersion} = {}; private _dir: string; private _lang: string; private _url: string; private _qs: any; private _ua: string; private _bPlt: string; private _onResizes: Array = []; private _readyPromise: Promise; private _readyResolve: any; private _resizeTm: any; private _zone: NgZone; constructor(platforms = []) { this._platforms = platforms; this._readyPromise = new Promise(res => { this._readyResolve = res; } ); } // Methods // ********************************************** /** * @param {string} platformName * @returns {boolean} returns true/false based on platform. * @description * Depending on the platform the user is on, `is(platformName)` will * return `true` or `false`. Note that the same app can return `true` * for more than one platform name. For example, an app running from * an iPad would return `true` for the platform names: `mobile`, * `ios`, `ipad`, and `tablet`. Additionally, if the app was running * from Cordova then `cordova` would be true, and if it was running * from a web browser on the iPad then then `mobileweb` would also * be `true`. * * Possible built-in platform names: * * - `android` * - `cordova` * - `core` * - `ios` * - `ipad` * - `iphone` * - `mobile` * - `mobileweb` * - `phablet` * - `tablet` * - `windows` * * ``` * import {Platform} from 'ionic-angular'; * * @Page({...}) * export MyPage { * constructor(platform: Platform) { * if (platform.is('ios')) { * // what ever you need to do * // if the platform is ios * } * } * } * ``` */ is(platformName: string): boolean { return (this._platforms.indexOf(platformName) > -1); } /** * @returns {array} the array of platforms * @description * Depending on what device you are on, `platforms` can return multiple values. * Each possible value is a hierarchy of platforms. For example, on an iPhone, * it would return mobile, ios, and iphone. * * ``` * import {Platform} from 'ionic-angular'; * export MyPage { * constructor(platform: Platform) { * this.platform = platform; * console.log(this.platform.platforms()); * // This will return an array of all the availble platforms * // From if your on mobile, to mobile os, and device name * } * } * ``` */ platforms(): Array { // get the array of active platforms, which also knows the hierarchy, // with the last one the most important return this._platforms; } /** * Returns an object containing information about the platforms. * * ``` * import {Platform} from 'ionic-angular'; * * @Page({...}) * export MyPage { * constructor(platform: Platform) { * console.log(platform.versions()); * } * } * ``` * @param {string} [platformName] optional platformName * @returns {object} An object with various platform info * */ versions(): {[name: string]: PlatformVersion} { // get all the platforms that have a valid parsed version return this._versions; } /** * @private */ version(): PlatformVersion { for (let platformName in this._versions) { if (this._versions[platformName]) { return this._versions[platformName]; } } return {}; } /** * Returns a promise when the platform is ready and native functionality * can be called. If the app is running from within a web browser, then * the promise will resolve when the DOM is ready. When the app is running * from an application engine such as Cordova, then the promise * will resolve when Cordova triggers the `deviceready` event. * * ``` * import {Platform} from 'ionic-angular'; * * @Page({...}) * export MyPage { * constructor(platform: Platform) { * platform.ready().then(() => { * console.log('Platform ready'); * // The platform is now ready, execute any native code you want * }); * } * } * ``` * @returns {promise} */ ready(): Promise { // this is the default if it's not replaced by the engine // if there was no custom ready method from the engine // then use the default DOM ready return this._readyPromise; } /** * @private */ triggerReady() { this._zone.run(() => { this._readyResolve(); }) } /** * @private */ prepareReady(zone: NgZone) { // this is the default prepareReady if it's not replaced by the engine // if there was no custom ready method from the engine // then use the default DOM ready this._zone = zone; ready(this.triggerReady.bind(this)); } /** * Set the app's language direction, which will update the `dir` attribute * on the app's root `` element. We recommend the app's `index.html` * file already has the correct `dir` attribute value set, such as * `` or ``. This method is useful if the * direction needs to be dynamically changed per user/session. * [W3C: Structural markup and right-to-left text in HTML](http://www.w3.org/International/questions/qa-html-dir) * @param {string} dir Examples: `rtl`, `ltr` */ setDir(dir: string, updateDocument: boolean) { this._dir = (dir || '').toLowerCase(); if (updateDocument !== false) { document.documentElement.setAttribute('dir', dir); } } /** * Returns app's language direction. * We recommend the app's `index.html` file already has the correct `dir` * attribute value set, such as `` or ``. * [W3C: Structural markup and right-to-left text in HTML](http://www.w3.org/International/questions/qa-html-dir) * @returns {string} */ dir(): string { return this._dir; } /** * Returns if this app is using right-to-left language direction or not. * We recommend the app's `index.html` file already has the correct `dir` * attribute value set, such as `` or ``. * [W3C: Structural markup and right-to-left text in HTML](http://www.w3.org/International/questions/qa-html-dir) * @returns {boolean} */ isRTL(): boolean { return (this._dir === 'rtl'); } /** * Set the app's language and optionally the country code, which will update * the `lang` attribute on the app's root `` element. * We recommend the app's `index.html` file already has the correct `lang` * attribute value set, such as ``. This method is useful if * the language needs to be dynamically changed per user/session. * [W3C: Declaring language in HTML](http://www.w3.org/International/questions/qa-html-language-declarations) * @param {string} language Examples: `en-US`, `en-GB`, `ar`, `de`, `zh`, `es-MX` */ setLang(language: string, updateDocument: boolean) { this._lang = language; if (updateDocument !== false) { document.documentElement.setAttribute('lang', language); } } /** * Returns app's language and optional country code. * We recommend the app's `index.html` file already has the correct `lang` * attribute value set, such as ``. * [W3C: Declaring language in HTML](http://www.w3.org/International/questions/qa-html-language-declarations) * @returns {string} */ lang(): string { return this._lang; } // Methods meant to be overridden by the engine // ********************************************** // Provided NOOP methods so they do not error when // called by engines (the browser)that do not provide them /** * @private */ exitApp() {} // Events meant to be triggered by the engine // ********************************************** /** * @private */ backButton: EventEmitter = new EventEmitter(); /** * @private */ pause: EventEmitter = new EventEmitter(); /** * @private */ resume: EventEmitter = new EventEmitter(); // Getter/Setter Methods // ********************************************** /** * @private */ setUrl(url: string) { this._url = url; this._qs = getQuerystring(url); } /** * @private */ url(): string { return this._url; } /** * @private */ query(key: string): string { return (this._qs || {})[key]; } /** * @private */ setUserAgent(userAgent: string) { this._ua = userAgent; } /** * @private */ userAgent(): string { return this._ua || ''; } /** * @private */ setNavigatorPlatform(navigatorPlatform: string) { this._bPlt = navigatorPlatform; } /** * @private */ navigatorPlatform(): string { return this._bPlt || ''; } /** * @private */ width(): number { return windowDimensions().width; } /** * @private */ height(): number { return windowDimensions().height; } /** * @private */ isPortrait(): boolean { return this.width() < this.height(); } /** * @private */ isLandscape(): boolean { return !this.isPortrait(); } /** * @private */ windowResize() { let self = this; clearTimeout(self._resizeTm); self._resizeTm = setTimeout(() => { flushDimensionCache(); for (let i = 0; i < self._onResizes.length; i++) { try { self._onResizes[i](); } catch (e) { console.error(e); } } }, 200); } /** * @private * @returns Unregister function */ onResize(cb: Function): Function { let self = this; self._onResizes.push(cb); return function() { let index = self._onResizes.indexOf(cb); if (index > -1) { self._onResizes.splice(index, 1); } }; } // Platform Registry // ********************************************** /** * @private */ static register(platformConfig: PlatformConfig) { platformRegistry[platformConfig.name] = platformConfig; } /** * @private */ static registry() { return platformRegistry; } /** * @private */ static get(platformName: string): PlatformConfig { return platformRegistry[platformName] || {}; } /** * @private */ static setDefault(platformName: string) { platformDefault = platformName; } /** * @private */ testQuery(queryValue: string, queryTestValue: string): boolean { let valueSplit = queryValue.toLowerCase().split(';'); return valueSplit.indexOf(queryTestValue) > -1; } /** * @private */ testNavigatorPlatform(navigatorPlatformExpression: string): boolean { let rgx = new RegExp(navigatorPlatformExpression, 'i'); return rgx.test(this._bPlt); } /** * @private */ matchUserAgentVersion(userAgentExpression: RegExp): any { if (this._ua && userAgentExpression) { let val = this._ua.match(userAgentExpression); if (val) { return { major: val[1], minor: val[2] }; } } } /** * @private */ isPlatformMatch(queryStringName: string, userAgentAtLeastHas?: string[], userAgentMustNotHave: string[] = []): boolean { let queryValue = this.query('ionicplatform'); if (queryValue) { return this.testQuery(queryValue, queryStringName); } userAgentAtLeastHas = userAgentAtLeastHas || [queryStringName]; let userAgent = this._ua.toLowerCase(); for (var i = 0; i < userAgentAtLeastHas.length; i++) { if (userAgent.indexOf(userAgentAtLeastHas[i]) > -1) { for (var j = 0; j < userAgentMustNotHave.length; j++) { if (userAgent.indexOf(userAgentMustNotHave[j]) > -1) { return false; } } return true; } } return false; } /** * @private */ load(config: Config) { let rootPlatformNode: PlatformNode; let enginePlatformNode: PlatformNode; let self = this; // figure out the most specific platform and active engine let tmpPlatform: PlatformNode; for (let platformName in platformRegistry) { tmpPlatform = this.matchPlatform(platformName); if (tmpPlatform) { // we found a platform match! // check if its more specific than the one we already have if (tmpPlatform.isEngine) { // because it matched then this should be the active engine // you cannot have more than one active engine enginePlatformNode = tmpPlatform; } else if (!rootPlatformNode || tmpPlatform.depth > rootPlatformNode.depth) { // only find the root node for platforms that are not engines // set this node as the root since we either don't already // have one, or this one is more specific that the current one rootPlatformNode = tmpPlatform; } } } if (!rootPlatformNode) { rootPlatformNode = new PlatformNode(platformDefault); } // build a Platform instance filled with the // hierarchy of active platforms and settings if (rootPlatformNode) { // check if we found an engine node (cordova/node-webkit/etc) if (enginePlatformNode) { // add the engine to the first in the platform hierarchy // the original rootPlatformNode now becomes a child // of the engineNode, which is not the new root enginePlatformNode.child = rootPlatformNode; rootPlatformNode.parent = enginePlatformNode; rootPlatformNode = enginePlatformNode; } let platformNode = rootPlatformNode; while (platformNode) { insertSuperset(platformNode); platformNode = platformNode.child; } // make sure the root noot is actually the root // incase a node was inserted before the root platformNode = rootPlatformNode.parent; while (platformNode) { rootPlatformNode = platformNode; platformNode = platformNode.parent; } platformNode = rootPlatformNode; while (platformNode) { platformNode.initialize(this, config); // set the array of active platforms with // the last one in the array the most important this._platforms.push(platformNode.name); // get the platforms version if a version parser was provided this._versions[platformNode.name] = platformNode.version(this); // go to the next platform child platformNode = platformNode.child; } } if (this._platforms.indexOf('mobile') > -1 && this._platforms.indexOf('cordova') === -1) { this._platforms.push('mobileweb'); } } /** * @private */ private matchPlatform(platformName: string): PlatformNode { // build a PlatformNode and assign config data to it // use it's getRoot method to build up its hierarchy // depending on which platforms match let platformNode = new PlatformNode(platformName); let rootNode = platformNode.getRoot(this); if (rootNode) { rootNode.depth = 0; let childPlatform = rootNode.child; while (childPlatform) { rootNode.depth++; childPlatform = childPlatform.child; } } return rootNode; } } function insertSuperset(platformNode: PlatformNode) { let supersetPlaformName = platformNode.superset(); if (supersetPlaformName) { // add a platform in between two exist platforms // so we can build the correct hierarchy of active platforms let supersetPlatform = new PlatformNode(supersetPlaformName); supersetPlatform.parent = platformNode.parent; supersetPlatform.child = platformNode; if (supersetPlatform.parent) { supersetPlatform.parent.child = supersetPlatform; } platformNode.parent = supersetPlatform; } } /** * @private */ class PlatformNode { private c: PlatformConfig; parent: PlatformNode; child: PlatformNode; name: string; isEngine: boolean; depth: number; constructor(platformName: string) { this.c = Platform.get(platformName); this.name = platformName; this.isEngine = this.c.isEngine; } settings(): any { return this.c.settings || {}; } superset(): any { return this.c.superset; } isMatch(p: Platform): boolean { return this.c.isMatch && this.c.isMatch(p) || false; } initialize(platform: Platform, config: Config) { this.c.initialize && this.c.initialize(platform, config); } version(p: Platform): PlatformVersion { if (this.c.versionParser) { let v = this.c.versionParser(p); if (v) { let str = v.major + '.' + v.minor; return { str: str, num: parseFloat(str), major: parseInt(v.major, 10), minor: parseInt(v.minor, 10) }; } } } getRoot(p: Platform): PlatformNode { if (this.isMatch(p)) { let parents = this.getSubsetParents(this.name); if (!parents.length) { return this; } let platform = null; let rootPlatform = null; for (let i = 0; i < parents.length; i++) { platform = new PlatformNode(parents[i]); platform.child = this; rootPlatform = platform.getRoot(p); if (rootPlatform) { this.parent = platform; return rootPlatform; } } } return null; } getSubsetParents(subsetPlatformName: string): Array { let platformRegistry = Platform.registry(); let parentPlatformNames = []; let platform = null; for (let platformName in platformRegistry) { platform = platformRegistry[platformName]; if (platform.subsets && platform.subsets.indexOf(subsetPlatformName) > -1) { parentPlatformNames.push(platformName); } } return parentPlatformNames; } } let platformRegistry: {[name: string]: PlatformConfig} = {}; let platformDefault: string = null; export interface PlatformConfig { name?: string; isEngine?: boolean; initialize?: Function; isMatch?: Function; superset?: string; subsets?: string[]; settings?: any; versionParser?: any; } export interface PlatformVersion { str?: string; num?: number; major?: number; minor?: number }