refactor: improve tree shaking abilities

Internal refactor completed in order to improve tree shaking and dead
code removal. The public API, with an exception to ion-slides, has
stayed the same. However, internally many changes were required so
bundlers could better exclude modules which should not be bundled.
Ultimately most changes resorted to removing references to `window` or
`document`, or a module that referenced one of those.

BREAKING CHANGES

ion-slides was refactored to remove the external dependencies, and
rewritten in TypeScript/ES6 modules to again improve tree shaking
abilities.
This commit is contained in:
Adam Bradley
2017-01-09 09:51:39 -06:00
parent 13cf6a6cb7
commit 7000b1b173
191 changed files with 9401 additions and 13664 deletions

View File

@ -1,7 +1,7 @@
import { EventEmitter, NgZone, OpaqueToken } from '@angular/core';
import { EventEmitter, NgZone } from '@angular/core';
import { getCss, isTextInput } from '../util/dom';
import { QueryParams } from './query-params';
import { ready, windowDimensions, flushDimensionCache } from '../util/dom';
import { removeArrayItem } from '../util/util';
@ -22,24 +22,25 @@ import { removeArrayItem } from '../util/util';
*
* @Component({...})
* export MyPage {
* constructor(platform: Platform) {
* this.platform = platform;
* constructor(public plt: Platform) {
*
* }
* }
* ```
* @demo /docs/v2/demos/src/platform/
*/
export class Platform {
private _win: Window;
private _doc: HTMLDocument;
private _versions: {[name: string]: PlatformVersion} = {};
private _dir: string;
private _lang: string;
private _ua: string;
private _qp: QueryParams;
private _bPlt: string;
private _qp = new QueryParams();
private _nPlt: string;
private _onResizes: Array<Function> = [];
private _readyPromise: Promise<any>;
private _readyResolve: any;
private _resizeTm: any;
private _bbActions: BackButtonAction[] = [];
private _registry: {[name: string]: PlatformConfig};
private _default: string;
@ -48,11 +49,25 @@ export class Platform {
private _lW = 0;
private _lH = 0;
private _isPortrait: boolean = null;
private _uiEvtOpts = false;
/** @private */
zone: NgZone;
/** @private */
/** @internal */
Css: {
transform?: string;
transition?: string;
transitionDuration?: string;
transitionDelay?: string;
transitionTimingFn?: string;
transitionStart?: string;
transitionEnd?: string;
transformOrigin?: string;
animationDelay?: string;
};
/** @internal */
_platforms: string[] = [];
constructor() {
@ -67,6 +82,34 @@ export class Platform {
});
}
/**
* @private
*/
setWindow(win: Window) {
this._win = win;
}
/**
* @private
*/
win() {
return this._win;
}
/**
* @private
*/
setDocument(doc: HTMLDocument) {
this._doc = doc;
}
/**
* @private
*/
doc() {
return this._doc;
}
/**
* @private
*/
@ -74,6 +117,13 @@ export class Platform {
this.zone = zone;
}
/**
* @private
*/
setCssProps(docElement: HTMLElement) {
this.Css = getCss(docElement);
}
// Methods
// **********************************************
@ -94,10 +144,8 @@ export class Platform {
*
* @Component({...})
* export MyPage {
* constructor(platform: Platform) {
* this.platform = platform;
*
* if (this.platform.is('ios')) {
* constructor(public plt: Platform) {
* if (this.plt.is('ios')) {
* // This will only print when on iOS
* console.log("I'm an iOS device!");
* }
@ -137,11 +185,9 @@ export class Platform {
*
* @Component({...})
* export MyPage {
* constructor(platform: Platform) {
* this.platform = platform;
*
* constructor(public plt: Platform) {
* // This will print an array of the current platforms
* console.log(this.platform.platforms());
* console.log(this.plt.platforms());
* }
* }
* ```
@ -161,12 +207,10 @@ export class Platform {
*
* @Component({...})
* export MyPage {
* constructor(platform: Platform) {
* this.platform = platform;
*
* constructor(public plt: Platform) {
* // This will print an object containing
* // all of the platforms and their versions
* console.log(platform.versions());
* console.log(plt.versions());
* }
* }
* ```
@ -210,8 +254,8 @@ export class Platform {
*
* @Component({...})
* export MyApp {
* constructor(platform: Platform) {
* platform.ready().then((readySource) => {
* constructor(public plt: Platform) {
* this.plt.ready().then((readySource) => {
* console.log('Platform ready from', readySource);
* // Platform now ready, execute any required native code
* });
@ -245,9 +289,21 @@ export class Platform {
* value is `dom`.
*/
prepareReady() {
ready(() => {
this.triggerReady('dom');
});
const self = this;
if (self._doc.readyState === 'complete' || self._doc.readyState === 'interactive') {
self.triggerReady('dom');
} else {
self._doc.addEventListener('DOMContentLoaded', completed, false);
self._win.addEventListener('load', completed, false);
}
function completed() {
self._doc.removeEventListener('DOMContentLoaded', completed, false);
self._win.removeEventListener('load', completed, false);
self.triggerReady('dom');
}
}
/**
@ -262,7 +318,7 @@ export class Platform {
setDir(dir: string, updateDocument: boolean) {
this._dir = (dir || '').toLowerCase();
if (updateDocument !== false) {
document.documentElement.setAttribute('dir', dir);
this._doc['documentElement'].setAttribute('dir', dir);
}
}
@ -300,7 +356,7 @@ export class Platform {
setLang(language: string, updateDocument: boolean) {
this._lang = language;
if (updateDocument !== false) {
document.documentElement.setAttribute('lang', language);
this._doc['documentElement'].setAttribute('lang', language);
}
}
@ -408,8 +464,22 @@ export class Platform {
/**
* @private
*/
setQueryParams(queryParams: QueryParams) {
this._qp = queryParams;
setQueryParams(url: string) {
this._qp.parseUrl(url);
}
/**
* Get the query string parameter
*/
getQueryParam(key: string) {
return this._qp.get(key);
}
/**
* Get the current url.
*/
url() {
return this._win['location']['href'];
}
/**
@ -422,15 +492,15 @@ export class Platform {
/**
* @private
*/
setNavigatorPlatform(navigatorPlatform: string) {
this._bPlt = navigatorPlatform;
setNavigatorPlatform(navigatorPlt: string) {
this._nPlt = navigatorPlt;
}
/**
* @private
*/
navigatorPlatform(): string {
return this._bPlt || '';
return this._nPlt || '';
}
/**
@ -453,6 +523,27 @@ export class Platform {
return this._isPortrait ? this._pH : this._lH;
}
/**
* @private
*/
getElementComputedStyle(ele: HTMLElement, pseudoEle?: string) {
return this._win['getComputedStyle'](ele, pseudoEle);
}
/**
* @private
*/
getElementFromPoint(x: number, y: number) {
return <HTMLElement>this._doc['elementFromPoint'](x, y);
}
/**
* @private
*/
getElementBoundingClientRect(ele: HTMLElement) {
return ele['getBoundingClientRect']();
}
/**
* Returns `true` if the app is in portait mode.
*/
@ -468,31 +559,33 @@ export class Platform {
return !this.isPortrait();
}
/**
* @private
*/
_calcDim() {
private _calcDim() {
// we're caching window dimensions so that
// we're not forcing many layouts
// if _isPortrait is null then that means
// the dimensions needs to be looked up again
if (this._isPortrait === null) {
const winDimensions = windowDimensions();
const screenWidth = window.screen.width || winDimensions.width;
const screenHeight = window.screen.height || winDimensions.height;
var win = this._win;
if (screenWidth < screenHeight) {
// we're keeping track of portrait and landscape dimensions
// separately because the virtual keyboard can really mess
// up accurate values when the keyboard up
if (win.screen.width < win.screen.height) {
this._isPortrait = true;
if (this._pW < winDimensions.width) {
this._pW = winDimensions.width;
if (this._pW < win['innerWidth']) {
this._pW = win['innerWidth'];
}
if (this._pH < winDimensions.height) {
this._pH = winDimensions.height;
if (this._pH < win['innerHeight']) {
this._pH = win['innerHeight'];
}
} else {
this._isPortrait = false;
if (this._lW < winDimensions.width) {
this._lW = winDimensions.width;
if (this._lW < win['innerWidth']) {
this._lW = win['innerWidth'];
}
if (this._lH < winDimensions.height) {
this._lH = winDimensions.height;
if (this._lH < win['innerHeight']) {
this._lH = win['innerHeight'];
}
}
}
@ -500,22 +593,121 @@ export class Platform {
/**
* @private
* This requestAnimationFrame will NOT be wrapped by zone.
*/
windowResize() {
clearTimeout(this._resizeTm);
raf(callback: {(timeStamp?: number): void}|Function): number {
const win: any = this._win;
return win['__zone_symbol__requestAnimationFrame'](callback);
}
this._resizeTm = setTimeout(() => {
flushDimensionCache();
this._isPortrait = null;
/**
* @private
*/
cancelRaf(rafId: number) {
const win: any = this._win;
return win['__zone_symbol__cancelAnimationFrame'](rafId);
}
for (let i = 0; i < this._onResizes.length; i++) {
try {
this._onResizes[i]();
} catch (e) {
console.error(e);
}
/**
* @private
* This setTimeout will NOT be wrapped by zone.
*/
timeout(callback: Function, timeout?: number): number {
const win: any = this._win;
return win['__zone_symbol__setTimeout'](callback, timeout);
}
/**
* @private
* This setTimeout will NOT be wrapped by zone.
*/
cancelTimeout(timeoutId: number) {
const win: any = this._win;
win['__zone_symbol__clearTimeout'](timeoutId);
}
/**
* @private
* Built to use modern event listener options, like "passive".
* If options are not supported, then just return a boolean which
* represents "capture". Returns a method to remove the listener.
*/
registerListener(ele: any, eventName: string, callback: {(ev?: UIEvent)}, opts: EventListenerOptions, unregisterListenersCollection?: Function[]): Function {
// use event listener options when supported
// otherwise it's just a boolean for the "capture" arg
const listenerOpts: any = this._uiEvtOpts ? {
'capture': !!opts.capture,
'passive': !!opts.passive,
} : !!opts.capture;
let unReg: Function;
if (!opts.zone && ele['__zone_symbol__addEventListener']) {
// do not wrap this event in zone and we've verified we can use the raw addEventListener
ele['__zone_symbol__addEventListener'](eventName, callback, listenerOpts);
unReg = function unregisterListener() {
ele['__zone_symbol__removeEventListener'](eventName, callback, listenerOpts);
};
} else {
// use the native addEventListener, which is wrapped with zone
ele['addEventListener'](eventName, callback, listenerOpts);
unReg = function unregisterListener() {
ele['removeEventListener'](eventName, callback, listenerOpts);
};
}
if (unregisterListenersCollection) {
unregisterListenersCollection.push(unReg);
}
return unReg;
}
/**
* @private
*/
transitionEnd(el: HTMLElement, callback: {(ev?: TransitionEvent)}, zone = true) {
const unRegs: Function[] = [];
function unregister() {
unRegs.forEach(unReg => {
unReg();
});
}
function onTransitionEnd(ev: TransitionEvent) {
if (el === ev.target) {
unregister();
callback(ev);
}
}, 200);
}
if (el) {
this.registerListener(el, 'webkitTransitionEnd', <any>onTransitionEnd, { zone: zone }, unRegs);
this.registerListener(el, 'transitionend', <any>onTransitionEnd, { zone: zone }, unRegs);
}
return unregister;
}
/**
* @private
*/
windowLoad(callback: Function) {
const win = this._win;
const doc = this._doc;
let unreg: Function;
if (doc.readyState === 'complete') {
callback(win, doc);
} else {
unreg = this.registerListener(win, 'load', () => {
unreg && unreg();
callback(win, doc);
}, { zone: false });
}
}
/**
@ -530,6 +722,80 @@ export class Platform {
};
}
/**
* @private
*/
isActiveElement(ele: HTMLElement) {
return !!(ele && (this.getActiveElement() === ele));
}
/**
* @private
*/
getActiveElement() {
return this._doc['activeElement'];
}
/**
* @private
*/
hasFocus(ele: HTMLElement) {
return !!((ele && (this.getActiveElement() === ele)) && (ele.parentElement.querySelector(':focus') === ele));
}
/**
* @private
*/
hasFocusedTextInput() {
const ele = this.getActiveElement();
if (isTextInput(ele)) {
return (ele.parentElement.querySelector(':focus') === ele);
}
return false;
}
/**
* @private
*/
focusOutActiveElement() {
const activeElement: any = this.getActiveElement();
activeElement && activeElement.blur && activeElement.blur();
}
private _initEvents() {
// Test via a getter in the options object to see if the passive property is accessed
try {
var opts = Object.defineProperty({}, 'passive', {
get: () => {
this._uiEvtOpts = true;
}
});
this._win.addEventListener('optsTest', null, opts);
} catch (e) { }
// add the window resize event listener XXms after
this.timeout(() => {
var timerId: number;
this.registerListener(this._win, 'resize', () => {
clearTimeout(timerId);
timerId = setTimeout(() => {
// setting _isPortrait to null means the
// dimensions will need to be looked up again
this._isPortrait = null;
for (let i = 0; i < this._onResizes.length; i++) {
try {
this._onResizes[i]();
} catch (e) {
console.error(e);
}
}
}, 200);
}, { passive: true, zone: true });
}, 2000);
}
// Platform Registry
// **********************************************
@ -575,7 +841,7 @@ export class Platform {
*/
testNavigatorPlatform(navigatorPlatformExpression: string): boolean {
const rgx = new RegExp(navigatorPlatformExpression, 'i');
return rgx.test(this._bPlt);
return rgx.test(this._nPlt);
}
/**
@ -629,28 +895,30 @@ export class Platform {
/** @private */
init() {
this._initEvents();
let rootPlatformNode: PlatformNode;
let enginePlatformNode: PlatformNode;
// figure out the most specific platform and active engine
let tmpPlatform: PlatformNode;
let tmpPlt: PlatformNode;
for (let platformName in this._registry) {
tmpPlatform = this.matchPlatform(platformName);
if (tmpPlatform) {
tmpPlt = this.matchPlatform(platformName);
if (tmpPlt) {
// we found a platform match!
// check if its more specific than the one we already have
if (tmpPlatform.isEngine) {
if (tmpPlt.isEngine) {
// because it matched then this should be the active engine
// you cannot have more than one active engine
enginePlatformNode = tmpPlatform;
enginePlatformNode = tmpPlt;
} else if (!rootPlatformNode || tmpPlatform.depth > rootPlatformNode.depth) {
} else if (!rootPlatformNode || tmpPlt.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;
rootPlatformNode = tmpPlt;
}
}
}
@ -777,13 +1045,13 @@ class PlatformNode {
return this.c.isMatch && this.c.isMatch(p) || false;
}
initialize(platform: Platform) {
this.c.initialize && this.c.initialize(platform);
initialize(plt: Platform) {
this.c.initialize && this.c.initialize(plt);
}
version(p: Platform): PlatformVersion {
version(plt: Platform): PlatformVersion {
if (this.c.versionParser) {
const v = this.c.versionParser(p);
const v = this.c.versionParser(plt);
if (v) {
const str = v.major + '.' + v.minor;
return {
@ -796,8 +1064,8 @@ class PlatformNode {
}
}
getRoot(p: Platform): PlatformNode {
if (this.isMatch(p)) {
getRoot(plt: Platform): PlatformNode {
if (this.isMatch(plt)) {
let parents = this.getSubsetParents(this.name);
@ -812,7 +1080,7 @@ class PlatformNode {
platformNode = new PlatformNode(this.registry, parents[i]);
platformNode.child = this;
rootPlatformNode = platformNode.getRoot(p);
rootPlatformNode = platformNode.getRoot(plt);
if (rootPlatformNode) {
this.parent = platformNode;
return rootPlatformNode;
@ -825,12 +1093,12 @@ class PlatformNode {
getSubsetParents(subsetPlatformName: string): string[] {
const parentPlatformNames: string[] = [];
let platform: PlatformConfig = null;
let pltConfig: PlatformConfig = null;
for (let platformName in this.registry) {
platform = this.registry[platformName];
pltConfig = this.registry[platformName];
if (platform.subsets && platform.subsets.indexOf(subsetPlatformName) > -1) {
if (pltConfig.subsets && pltConfig.subsets.indexOf(subsetPlatformName) > -1) {
parentPlatformNames.push(platformName);
}
}
@ -864,37 +1132,45 @@ interface BackButtonAction {
}
/**
* @private
*/
export function setupPlatform(platformConfigs: {[key: string]: PlatformConfig}, queryParams: QueryParams, userAgent: string, navigatorPlatform: string, docDirection: string, docLanguage: string, zone: NgZone): Platform {
const p = new Platform();
p.setDefault('core');
p.setPlatformConfigs(platformConfigs);
p.setUserAgent(userAgent);
p.setQueryParams(queryParams);
p.setNavigatorPlatform(navigatorPlatform);
p.setDir(docDirection, false);
p.setLang(docLanguage, false);
p.setZone(zone);
p.init();
return p;
export interface EventListenerOptions {
capture?: boolean;
passive?: boolean;
zone?: boolean;
}
/**
* @private
*/
export const UserAgentToken = new OpaqueToken('USERAGENT');
/**
* @private
*/
export const NavigatorPlatformToken = new OpaqueToken('NAVPLT');
/**
* @private
*/
export const DocumentDirToken = new OpaqueToken('DOCDIR');
/**
* @private
*/
export const DocLangToken = new OpaqueToken('DOCLANG');
export function setupPlatform(doc: HTMLDocument, platformConfigs: {[key: string]: PlatformConfig}, zone: NgZone): Platform {
const plt = new Platform();
plt.setDefault('core');
plt.setPlatformConfigs(platformConfigs);
plt.setZone(zone);
// set values from "document"
const docElement = doc.documentElement;
plt.setDocument(doc);
plt.setDir(docElement.dir, false);
plt.setLang(docElement.lang, false);
// set css properties
plt.setCssProps(docElement);
// set values from "window"
const win = doc.defaultView;
plt.setWindow(win);
plt.setNavigatorPlatform(win.navigator.platform);
plt.setUserAgent(win.navigator.userAgent);
// set location values
plt.setQueryParams(win.location.href);
plt.init();
// add the platform obj to the window
win['Ionic'] = win['Ionic'] || {};
win['Ionic']['platform'] = plt;
return plt;
}