diff --git a/demos/loading/index.ts b/demos/loading/index.ts new file mode 100644 index 0000000000..2cd8d824e2 --- /dev/null +++ b/demos/loading/index.ts @@ -0,0 +1,117 @@ +import {App, Page, ActionSheet, Loading, NavController, ViewController, Platform} from 'ionic-angular'; + + +@Page({ + templateUrl: 'main.html' +}) +class E2EPage { + constructor(private nav: NavController, private platform: Platform) {} + + presentLoadingIos() { + let loading = Loading.create({ + spinner: 'ios', + content: 'This is the "ios" spinner. It will dismiss after 3 seconds.', + duration: 3000 + }); + + this.nav.present(loading); + } + + presentLoadingDots() { + let loading = Loading.create({ + spinner: 'dots', + content: 'This is the "dots" spinner. It will dismiss after 3 seconds.', + duration: 3000 + }); + + this.nav.present(loading); + } + + presentLoadingBubbles() { + let loading = Loading.create({ + spinner: 'bubbles', + content: 'This is the "bubbles" spinner. It will dismiss after 3 seconds.', + duration: 3000 + }); + + this.nav.present(loading); + } + + presentLoadingCircles() { + let loading = Loading.create({ + spinner: 'circles', + content: 'This is the "circles" spinner. It will dismiss after 3 seconds.', + duration: 3000 + }); + + this.nav.present(loading); + } + + presentLoadingCrescent() { + let loading = Loading.create({ + spinner: 'crescent', + content: 'This is the "crescent" spinner. It will dismiss after 3 seconds.', + duration: 3000 + }); + + this.nav.present(loading); + } + + presentLoadingDefault() { + let loading = Loading.create({ + content: 'This is the mode specific spinner. It will dismiss after 3 seconds.', + duration: 3000 + }); + + this.nav.present(loading); + } + + presentLoadingCustom() { + let loading = Loading.create({ + spinner: 'hide', + content: ` +
+
+
+
This is a custom spinner. It will dismiss after 3 seconds.
`, + duration: 3000 + }); + + this.nav.present(loading); + } + + presentLoadingText() { + let loading = Loading.create({ + spinner: 'hide', + content: 'This has no spinner, only text. It will dismiss after 3 seconds.', + duration: 3000 + }); + + this.nav.present(loading); + } + + goToPage2() { + this.nav.push(Page2); + } +} + +@Page({ + template: ` + + Page 2 + + Some content + ` +}) +class Page2 { + constructor(private nav: NavController, private platform: Platform) {} +} + +@App({ + template: '' +}) +class E2EApp { + root = E2EPage; +} + +document.body.innerHTML += '' diff --git a/demos/loading/main.html b/demos/loading/main.html new file mode 100644 index 0000000000..76aaf2165c --- /dev/null +++ b/demos/loading/main.html @@ -0,0 +1,57 @@ + + Loading + + + + + + Spinner Loading Indicators + + + + + + + + + + + + Custom Loading Indicators + + + + + + + + + + + + diff --git a/demos/loading/styles.css b/demos/loading/styles.css new file mode 100644 index 0000000000..48888095ba --- /dev/null +++ b/demos/loading/styles.css @@ -0,0 +1,59 @@ +.custom-spinner-container { + position: relative; + display: inline-block; + box-sizing: border-box; + width: 100%; + margin-bottom: 20px; +} + +.custom-spinner-box { + margin: 0 auto; + position: relative; + box-sizing: border-box; + border: 4px solid #000; + width: 60px; + height: 60px; + animation: spin 3s infinite linear; +} + +.custom-spinner-box:before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-sizing: border-box; + border: 4px solid #000; + width: 40px; + height: 40px; + animation: pulse 1.5s infinite ease; +} + +.wp .custom-spinner-box, +.wp .custom-spinner-box:before { + border-color: #fff; +} + +@-webkit-keyframes pulse { + 50% { + border-width: 20px; + } +} +@keyframes pulse { + 50% { + border-width: 20px; + } +} + +@-webkit-keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} diff --git a/ionic/components.core.scss b/ionic/components.core.scss index 7d1105d6cb..1d97ed8540 100644 --- a/ionic/components.core.scss +++ b/ionic/components.core.scss @@ -17,6 +17,7 @@ "components/grid/grid", "components/icon/icon", "components/infinite-scroll/infinite-scroll", + "components/loading/loading", "components/menu/menu", "components/modal/modal", "components/refresher/refresher", diff --git a/ionic/components.ios.scss b/ionic/components.ios.scss index c75859bb2d..769e795b89 100644 --- a/ionic/components.ios.scss +++ b/ionic/components.ios.scss @@ -18,6 +18,7 @@ "components/item/item.ios", "components/label/label.ios", "components/list/list.ios", + "components/loading/loading.ios", "components/menu/menu.ios", "components/modal/modal.ios", "components/radio/radio.ios", diff --git a/ionic/components.md.scss b/ionic/components.md.scss index 82d53a8f34..3e3c695d29 100644 --- a/ionic/components.md.scss +++ b/ionic/components.md.scss @@ -18,6 +18,7 @@ "components/item/item.md", "components/label/label.md", "components/list/list.md", + "components/loading/loading.md", "components/menu/menu.md", "components/modal/modal.md", "components/radio/radio.md", diff --git a/ionic/components.ts b/ionic/components.ts index 362c737a3b..2a3d11b835 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -19,6 +19,7 @@ export * from './components/menu/menu-toggle' export * from './components/menu/menu-close' export * from './components/label/label' export * from './components/list/list' +export * from './components/loading/loading' export * from './components/show-hide-when/show-hide-when' export * from './components/modal/modal' export * from './components/nav/nav' diff --git a/ionic/components.wp.scss b/ionic/components.wp.scss index 5f5f5e7a42..962b6a62a5 100644 --- a/ionic/components.wp.scss +++ b/ionic/components.wp.scss @@ -18,6 +18,7 @@ "components/item/item.wp", "components/label/label.wp", "components/list/list.wp", + "components/loading/loading.wp", "components/menu/menu.wp", "components/modal/modal.wp", "components/radio/radio.wp", diff --git a/ionic/components/loading/loading.ios.scss b/ionic/components/loading/loading.ios.scss new file mode 100644 index 0000000000..7360fabba9 --- /dev/null +++ b/ionic/components/loading/loading.ios.scss @@ -0,0 +1,73 @@ +@import "../../globals.core"; +@import "./loading"; + +// iOS Loading Indicator +// -------------------------------------------------- + +$loading-ios-padding: 24px 34px !default; +$loading-ios-max-width: 270px !default; +$loading-ios-max-height: 90% !default; +$loading-ios-border-radius: 8px !default; +$loading-ios-text-color: #000 !default; +$loading-ios-background: #f8f8f8 !default; + +$loading-ios-content-font-weight: bold !default; + +$loading-ios-spinner-color: #69717d !default; + +$loading-ios-spinner-ios-color: $loading-ios-spinner-color !default; +$loading-ios-spinner-bubbles-color: $loading-ios-spinner-color !default; +$loading-ios-spinner-circles-color: $loading-ios-spinner-color !default; +$loading-ios-spinner-crescent-color: $loading-ios-spinner-color !default; +$loading-ios-spinner-dots-color: $loading-ios-spinner-color !default; + + +.loading-wrapper { + padding: $loading-ios-padding; + + max-width: $loading-ios-max-width; + max-height: $loading-ios-max-height; + + border-radius: $loading-ios-border-radius; + color: $loading-ios-text-color; + background: $loading-ios-background; +} + + +// iOS Loading Content +// ----------------------------------------- + +.loading-content { + font-weight: $loading-ios-content-font-weight; +} + +.loading-spinner + .loading-content { + margin-left: 16px; +} + + +// iOS Loading Spinner fill colors +// ----------------------------------------- + +.loading-spinner { + .spinner-ios line, + .spinner-ios-small line { + stroke: $loading-ios-spinner-ios-color; + } + + .spinner-bubbles circle { + fill: $loading-ios-spinner-bubbles-color; + } + + .spinner-circles circle { + fill: $loading-ios-spinner-circles-color; + } + + .spinner-crescent circle { + stroke: $loading-ios-spinner-crescent-color; + } + + .spinner-dots circle { + fill: $loading-ios-spinner-dots-color; + } +} diff --git a/ionic/components/loading/loading.md.scss b/ionic/components/loading/loading.md.scss new file mode 100644 index 0000000000..bc21e1febf --- /dev/null +++ b/ionic/components/loading/loading.md.scss @@ -0,0 +1,71 @@ +@import "../../globals.core"; +@import "./loading"; + +// Material Design Loading Indicator +// -------------------------------------------------- + +$loading-md-padding: 24px !default; +$loading-md-max-width: 280px !default; +$loading-md-max-height: 90% !default; +$loading-md-border-radius: 2px !default; +$loading-md-text-color: rgba(0, 0, 0, .5) !default; +$loading-md-background: #fafafa !default; +$loading-md-box-shadow-color: rgba(0, 0, 0, .4) !default; +$loading-md-box-shadow: 0 16px 20px $loading-md-box-shadow-color !default; + +$loading-md-spinner-color: color($colors-md, primary) !default; + +$loading-md-spinner-ios-color: $loading-md-spinner-color !default; +$loading-md-spinner-bubbles-color: $loading-md-spinner-color !default; +$loading-md-spinner-circles-color: $loading-md-spinner-color !default; +$loading-md-spinner-crescent-color: $loading-md-spinner-color !default; +$loading-md-spinner-dots-color: $loading-md-spinner-color !default; + + +.loading-wrapper { + padding: $loading-md-padding; + + max-width: $loading-md-max-width; + max-height: $loading-md-max-height; + + border-radius: $loading-md-border-radius; + color: $loading-md-text-color; + background: $loading-md-background; + + box-shadow: $loading-md-box-shadow; +} + + +// Material Design Loading Content +// ----------------------------------------- + +.loading-spinner + .loading-content { + margin-left: 16px; +} + + +// Material Design Loading Spinner fill colors +// ----------------------------------------- + +.loading-spinner { + .spinner-ios line, + .spinner-ios-small line { + stroke: $loading-md-spinner-ios-color; + } + + .spinner-bubbles circle { + fill: $loading-md-spinner-bubbles-color; + } + + .spinner-circles circle { + fill: $loading-md-spinner-circles-color; + } + + .spinner-crescent circle { + stroke: $loading-md-spinner-crescent-color; + } + + .spinner-dots circle { + fill: $loading-md-spinner-dots-color; + } +} diff --git a/ionic/components/loading/loading.scss b/ionic/components/loading/loading.scss new file mode 100644 index 0000000000..4a49505038 --- /dev/null +++ b/ionic/components/loading/loading.scss @@ -0,0 +1,35 @@ +@import "../../globals.core"; + +// Loading Indicator +// -------------------------------------------------- + + +ion-loading { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $z-index-overlay; + + display: flex; + + align-items: center; + justify-content: center; +} + +.loading-wrapper { + z-index: $z-index-overlay-wrapper; + display: flex; + + align-items: center; + + opacity: 0; +} + +// Loading Backdrop +// ----------------------------------------- + +.hide-backdrop { + display: none; +} diff --git a/ionic/components/loading/loading.ts b/ionic/components/loading/loading.ts new file mode 100644 index 0000000000..d13a298230 --- /dev/null +++ b/ionic/components/loading/loading.ts @@ -0,0 +1,353 @@ +import {Component, Renderer, ElementRef, HostListener} from 'angular2/core'; +import {NgFor, NgIf} from 'angular2/common'; + +import {Animation} from '../../animations/animation'; +import {Transition, TransitionOptions} from '../../transitions/transition'; +import {Config} from '../../config/config'; +import {Spinner} from '../spinner/spinner'; +import {isPresent, isUndefined, isDefined} from '../../util/util'; +import {NavParams} from '../nav/nav-params'; +import {ViewController} from '../nav/view-controller'; + + +/** + * @name Loading + * @description + * An overlay that can be used to indicate activity while blocking user + * interaction. The loading indicator appears on top of the app's content, + * and can be dismissed by the app to resume user interaction with + * the app. It includes an optional backdrop, which can be disabled + * by setting `showBackdrop: false` upon creation. + * + * ### Creating + * You can pass all of the loading options in the first argument of + * the create method: `Loading.create(opts)`. The spinner name should be + * passed in the `spinner` property, and any optional HTML can be passed + * in the `content` property. If you do not pass a value to `spinner` + * the loading indicator will use the spinner specified by the mode. To + * set the spinner name across the app, set the value of `loadingSpinner` + * in your app's config. To hide the spinner, set `loadingSpinner: 'hide'` + * in the apps' config or pass `spinner: 'hide'` in the loading + * options. See the create method below for all available options. + * + * ### Dismissing + * The loading indicator can be dismissed automatically after a specific + * amount of time by passing the number of milliseconds to display it in + * the `duration` of the loading options. By default the loading indicator + * will show even during page changes, but this can be disabled by setting + * `dismissOnPageChange` to `true`. To dismiss the loading indicator after + * creation, call the `dismiss()` method on the Loading instance. + * + * ### Limitations + * The element is styled to appear on top of other content by setting its + * `z-index` property. You must ensure no element has a stacking context with + * a higher `z-index` than this element. + * + * @usage + * ```ts + * constructor(nav: NavController) { + * this.nav = nav; + * } + * + * presentLoadingDefault() { + * let loading = Loading.create({ + * content: 'Please wait...' + * }); + * + * this.nav.present(loading); + * + * setTimeout(() => { + * loading.dismiss(); + * }, 5000); + * } + * + * presentLoadingCustom() { + * let loading = Loading.create({ + * spinner: 'hide', + * content: ` + *
+ *
+ *
`, + * duration: 5000 + * }); + * + * this.nav.present(loading); + * } + * + * presentLoadingText() { + * let loading = Loading.create({ + * spinner: 'hide', + * content: 'Loading Please Wait...' + * }); + * + * this.nav.present(loading); + * + * setTimeout(() => { + * this.nav.push(Page2); + * }, 1000); + * + * setTimeout(() => { + * loading.dismiss(); + * }, 5000); + * } + * ``` + * + * @demo /docs/v2/demos/loading/ + * @see {@link /docs/v2/api/components/spinner/Spinner Spinner API Docs} + */ +export class Loading extends ViewController { + + constructor(opts: LoadingOptions = {}) { + opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; + opts.dismissOnPageChange = isPresent(opts.dismissOnPageChange) ? !!opts.dismissOnPageChange : false; + + super(LoadingCmp, opts); + this.viewType = 'loading'; + this.isOverlay = true; + this.usePortal = true; + + // by default, loading indicators should not fire lifecycle events of other views + // for example, when an loading indicators enters, the current active view should + // not fire its lifecycle events because it's not conceptually leaving + this.fireOtherLifecycles = false; + } + + /** + * @private + */ + getTransitionName(direction: string) { + let key = (direction === 'back' ? 'loadingLeave' : 'loadingEnter'); + return this._nav && this._nav.config.get(key); + } + + /** + * Create a loading indicator with the following options + * + * | Option | Type | Description | + * |-----------------------|------------|------------------------------------------------------------------------------------------------------------------| + * | spinner |`string` | The name of the SVG spinner for the loading indicator. | + * | content |`string` | The html content for the loading indicator. | + * | cssClass |`string` | An additional class for custom styles. | + * | showBackdrop |`boolean` | Whether to show the backdrop. Default true. | + * | dismissOnPageChange |`boolean` | Whether to dismiss the indicator when navigating to a new page. Default false. | + * | duration |`number` | How many milliseconds to wait before hiding the indicator. By default, it will show until `hide()` is called. | + * + * + * @param {object} opts Loading options + */ + static create(opts: LoadingOptions = {}) { + return new Loading(opts); + } + + } + +/** +* @private +*/ +@Component({ + selector: 'ion-loading', + template: + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '
', + host: { + 'role': 'dialog' + }, + directives: [NgIf, Spinner] +}) +class LoadingCmp { + private d: any; + private id: number; + private created: number; + private showSpinner: boolean; + + constructor( + private _viewCtrl: ViewController, + private _config: Config, + private _elementRef: ElementRef, + params: NavParams, + renderer: Renderer + ) { + this.d = params.data; + this.created = Date.now(); + + if (this.d.cssClass) { + renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); + } + + this.id = (++loadingIds); + } + + ngOnInit() { + // If no spinner was passed in loading options we need to fall back + // to the loadingSpinner in the app's config, then the mode spinner + if (isUndefined(this.d.spinner)) { + this.d.spinner = this._config.get('loadingSpinner', this._config.get('spinner', 'ios')); + } + + // If the user passed hide to the spinner we don't want to show it + this.showSpinner = isDefined(this.d.spinner) && this.d.spinner !== 'hide'; + } + + onPageDidEnter() { + let activeElement: any = document.activeElement; + if (document.activeElement) { + activeElement.blur(); + } + + // If there is a duration, dismiss after that amount of time + this.d.duration ? setTimeout(() => this.dismiss('backdrop'), this.d.duration) : null; + } + + dismiss(role): Promise { + return this._viewCtrl.dismiss(null, role); + } + + isEnabled() { + let tm = this._config.getNumber('overlayCreatedDiff', 750); + return (this.created + tm < Date.now()); + } +} + +export interface LoadingOptions { + spinner?: string; + content?: string; + showBackdrop?: boolean; + dismissOnPageChange?: boolean; + delay?: number; + duration?: number; +} + +/** + * Animations for loading + */ + class LoadingPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.1', '1'); + backdrop.fromTo('opacity', '0.01', '0.3'); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-pop-in', LoadingPopIn); + + + class LoadingPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '0.9'); + backdrop.fromTo('opacity', '0.3', '0'); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-pop-out', LoadingPopOut); + + + class LoadingMdPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.1', '1'); + backdrop.fromTo('opacity', '0.01', '0.50'); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-md-pop-in', LoadingMdPopIn); + + + class LoadingMdPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '0.9'); + backdrop.fromTo('opacity', '0.50', '0'); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-md-pop-out', LoadingMdPopOut); + + + class LoadingWpPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.3', '1'); + backdrop.fromTo('opacity', '0.01', '0.16'); + + this + .easing('cubic-bezier(0,0 0.05,1)') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-wp-pop-in', LoadingWpPopIn); + + + class LoadingWpPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '1.3'); + backdrop.fromTo('opacity', '0.16', '0'); + + this + .easing('ease-out') + .duration(150) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-wp-pop-out', LoadingWpPopOut); + +let loadingIds = -1; diff --git a/ionic/components/loading/loading.wp.scss b/ionic/components/loading/loading.wp.scss new file mode 100644 index 0000000000..50bd066273 --- /dev/null +++ b/ionic/components/loading/loading.wp.scss @@ -0,0 +1,67 @@ +@import "../../globals.core"; +@import "./loading"; + +// Windows Loading Indicator +// -------------------------------------------------- + +$loading-wp-padding: 20px !default; +$loading-wp-max-width: 280px !default; +$loading-wp-max-height: 90% !default; +$loading-wp-border-radius: 2px !default; +$loading-wp-text-color: #fff !default; +$loading-wp-background: #000 !default; + +$loading-wp-spinner-color: $loading-wp-text-color !default; + +$loading-wp-spinner-ios-color: $loading-wp-spinner-color !default; +$loading-wp-spinner-bubbles-color: $loading-wp-spinner-color !default; +$loading-wp-spinner-circles-color: $loading-wp-spinner-color !default; +$loading-wp-spinner-crescent-color: $loading-wp-spinner-color !default; +$loading-wp-spinner-dots-color: $loading-wp-spinner-color !default; + + +.loading-wrapper { + padding: $loading-wp-padding; + + max-width: $loading-wp-max-width; + max-height: $loading-wp-max-height; + + border-radius: $loading-wp-border-radius; + color: $loading-wp-text-color; + background: $loading-wp-background; +} + + +// Windows Loading Content +// ----------------------------------------- + +.loading-spinner + .loading-content { + margin-left: 16px; +} + + +// Windows Loading Spinner fill colors +// ----------------------------------------- + +.loading-spinner { + .spinner-ios line, + .spinner-ios-small line { + stroke: $loading-wp-spinner-ios-color; + } + + .spinner-bubbles circle { + fill: $loading-wp-spinner-bubbles-color; + } + + .spinner-circles circle { + fill: $loading-wp-spinner-circles-color; + } + + .spinner-crescent circle { + stroke: $loading-wp-spinner-crescent-color; + } + + .spinner-dots circle { + fill: $loading-wp-spinner-dots-color; + } +} diff --git a/ionic/components/loading/test/basic/e2e.ts b/ionic/components/loading/test/basic/e2e.ts new file mode 100644 index 0000000000..338516c2fb --- /dev/null +++ b/ionic/components/loading/test/basic/e2e.ts @@ -0,0 +1,4 @@ + +it('should open default spinner', function() { + element(by.css('.e2eLoadingDefaultSpinner')).click(); +}); diff --git a/ionic/components/loading/test/basic/index.ts b/ionic/components/loading/test/basic/index.ts new file mode 100644 index 0000000000..3766769972 --- /dev/null +++ b/ionic/components/loading/test/basic/index.ts @@ -0,0 +1,229 @@ +import {App, Page, ActionSheet, Loading, NavController, ViewController, Platform} from 'ionic-angular'; + + +@Page({ + templateUrl: 'main.html' +}) +class E2EPage { + constructor(private nav: NavController, private platform: Platform) {} + + presentLoadingIos() { + let loading = Loading.create({ + spinner: 'ios', + duration: 1000 + }); + + this.nav.present(loading); + } + + presentLoadingDots() { + let loading = Loading.create({ + spinner: 'dots', + content: 'Loading...', + duration: 1000 + }); + + this.nav.present(loading); + } + + presentLoadingBubbles() { + let loading = Loading.create({ + spinner: 'bubbles', + content: 'Loading...', + duration: 1000 + }); + + this.nav.present(loading); + } + + presentLoadingCircles() { + let loading = Loading.create({ + spinner: 'circles', + content: 'Loading...', + duration: 1000 + }); + + this.nav.present(loading); + } + + presentLoadingCrescent() { + let loading = Loading.create({ + spinner: 'crescent', + content: 'Please wait...', + duration: 1000 + }); + + this.nav.present(loading); + } + + presentLoadingDefault() { + let loading = Loading.create({ + content: 'Please wait...', + }); + + this.nav.present(loading); + + setTimeout(() => { + loading.dismiss(); + }, 5000); + } + + presentLoadingCustom() { + let loading = Loading.create({ + spinner: 'hide', + content: ` +
+
+
`, + duration: 1000 + }); + + this.nav.present(loading); + } + + presentLoadingText() { + let loading = Loading.create({ + spinner: 'hide', + content: 'Loading Please Wait...' + }); + + this.nav.present(loading); + + setTimeout(() => { + this.nav.push(Page2); + }, 1000); + + setTimeout(() => { + loading.dismiss(); + }, 5000); + } + + goToPage2() { + this.nav.push(Page2); + } + + presentLoadingMultiple() { + let loading = Loading.create({ + spinner: 'hide', + content: 'Loading 1 Please Wait...' + }); + + this.nav.present(loading); + + let loading2 = Loading.create({ + spinner: 'hide', + content: 'Loading 2 Please Wait...' + }); + + setTimeout(() => { + this.nav.present(loading2); + }, 1000); + + let loading3 = Loading.create({ + spinner: 'hide', + content: 'Loading 3 Please Wait...' + }); + + setTimeout(() => { + this.nav.present(loading3); + + setTimeout(() => { + loading3.dismiss(); + }, 1000); + + setTimeout(() => { + loading2.dismiss(); + }, 2000); + + setTimeout(() => { + loading.dismiss(); + }, 3000); + }, 2000); + + } + + presentLoadingMultipleNav() { + let loading = Loading.create({ + spinner: 'hide', + content: 'Loading 1 Please Wait...', + dismissOnPageChange: true + }); + + this.nav.present(loading); + + let loading2 = Loading.create({ + spinner: 'hide', + content: 'Loading 2 Please Wait...', + dismissOnPageChange: true + }); + + setTimeout(() => { + this.nav.present(loading2); + }, 500); + + let loading3 = Loading.create({ + spinner: 'hide', + content: 'Loading 3 Please Wait...', + dismissOnPageChange: true + }); + + setTimeout(() => { + this.nav.present(loading3); + + setTimeout(() => { + this.nav.push(Page2); + }, 1000); + }, 1000); + } +} + +@Page({ + template: ` + + Page 2 + + Some content + + + + + + ` +}) +class Page2 { + constructor(private nav: NavController, private platform: Platform) {} + + onPageLoaded() { + setTimeout(() => { + this.nav.push(Page3); + }, 1000); + } + + goToPage3() { + this.nav.push(Page3); + } +} + +@Page({ + template: ` + + Page 3 + + Some content + ` +}) +class Page3 { + constructor(private nav: NavController, private platform: Platform) {} +} + +@App({ + template: '' +}) +class E2EApp { + root = E2EPage; +} + +document.body.innerHTML += '' diff --git a/ionic/components/loading/test/basic/main.html b/ionic/components/loading/test/basic/main.html new file mode 100644 index 0000000000..a25d3819b9 --- /dev/null +++ b/ionic/components/loading/test/basic/main.html @@ -0,0 +1,26 @@ + + + Loading + + + + + + + + + + + + + + + + + + + + diff --git a/ionic/components/loading/test/basic/styles.css b/ionic/components/loading/test/basic/styles.css new file mode 100644 index 0000000000..5291c34716 --- /dev/null +++ b/ionic/components/loading/test/basic/styles.css @@ -0,0 +1,56 @@ +.custom-spinner-container { + position: relative; + display: inline-block; + box-sizing: border-box; +} + +.custom-spinner-box { + position: relative; + box-sizing: border-box; + border: 4px solid #000; + width: 60px; + height: 60px; + animation: spin 3s infinite linear; +} + +.custom-spinner-box:before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-sizing: border-box; + border: 4px solid #000; + width: 40px; + height: 40px; + animation: pulse 1.5s infinite ease; +} + +.wp .custom-spinner-box, +.wp .custom-spinner-box:before { + border-color: #fff; +} + +@-webkit-keyframes pulse { + 50% { + border-width: 20px; + } +} +@keyframes pulse { + 50% { + border-width: 20px; + } +} + +@-webkit-keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} diff --git a/ionic/components/nav/nav-controller.ts b/ionic/components/nav/nav-controller.ts index 4e3477006b..e91ebaadb3 100644 --- a/ionic/components/nav/nav-controller.ts +++ b/ionic/components/nav/nav-controller.ts @@ -8,6 +8,7 @@ import {Keyboard} from '../../util/keyboard'; import {NavParams} from './nav-params'; import {NavRouter} from './nav-router'; import {pascalCaseToDashCase, isTrueProperty, isBlank} from '../../util/util'; +import {Portal} from './nav-portal'; import {raf} from '../../util/dom'; import {SwipeBackGesture} from './swipe-back'; import {Transition} from '../../transitions/transition'; @@ -109,6 +110,7 @@ export class NavController extends Ion { private _trans: Transition; private _sbGesture: SwipeBackGesture; private _sbThreshold: number; + private _portal: Portal; protected _sbEnabled: boolean; protected _ids: number = -1; @@ -170,6 +172,10 @@ export class NavController extends Ion { provide(NavController, {useValue: this}) ]); } + + setPortal(val: Portal) { + this._portal = val; + } /** * Set the root for the current navigation stack @@ -412,7 +418,7 @@ export class NavController extends Ion { if (rootNav['_tabs']) { // TODO: must have until this goes in // https://github.com/angular/angular/issues/5481 - console.error('A parent is required for ActionSheet/Alert/Modal'); + console.error('A parent is required for ActionSheet/Alert/Modal/Loading'); return; } @@ -433,7 +439,12 @@ export class NavController extends Ion { keyboardClose: false, direction: 'back', animation: enteringView.getTransitionName('back') - }); + }); + + if (enteringView.usePortal && this._portal) { + this._portal.present(enteringView); + return; + } // start the transition return rootNav._insertViews(-1, [enteringView], opts); @@ -534,7 +545,7 @@ export class NavController extends Ion { if (this._views[i] === enteringView) { // cool, so the last valid view is also our entering view!! - // this means we should animate that bad boy in so its the active view + // this means we should animate that bad boy in so it's the active view // return a promise and resolve when the transition has completed // get the leaving view which the _insert() already set @@ -730,8 +741,8 @@ export class NavController extends Ion { // get the view thats ready to enter let enteringView = this.getByState(STATE_INIT_ENTER); - if (!enteringView) { - // oh knows! no entering view to go to! + if (!enteringView && this._portal) { + // oh nos! no entering view to go to! // if there is no previous view that would enter in this nav stack // and the option is set to climb up the nav parent looking // for the next nav we could transition to instead @@ -1248,6 +1259,14 @@ export class NavController extends Ion { // see if we should add the swipe back gesture listeners or not this._sbCheck(); + + if (this._portal) { + this._portal._views.forEach(view => { + if (view.data && view.data.dismissOnPageChange) { + view.dismiss(); + } + }); + } } else { // darn, so this wasn't the most recent transition @@ -1640,7 +1659,7 @@ export class NavController extends Ion { } else { // this is the initial view - enteringView.setZIndex(INIT_ZINDEX, this._renderer); + enteringView.setZIndex(this._portal ? INIT_ZINDEX : PORTAL_ZINDEX, this._renderer); } } else if (direction === 'back') { @@ -1680,5 +1699,6 @@ const STATE_REMOVE = 'remove'; const STATE_REMOVE_AFTER_TRANS = 'remove_after_trans'; const STATE_FORCE_ACTIVE = 'force_active'; const INIT_ZINDEX = 100; +const PORTAL_ZINDEX = 9999; let ctrlIds = -1; diff --git a/ionic/components/nav/nav-portal.ts b/ionic/components/nav/nav-portal.ts new file mode 100644 index 0000000000..6e6786ab4a --- /dev/null +++ b/ionic/components/nav/nav-portal.ts @@ -0,0 +1,30 @@ +import {Directive, ElementRef, Input, Optional, NgZone, Compiler, AppViewManager, Renderer, Type, ContentChild} from 'angular2/core'; + +import {IonicApp} from '../app/app'; +import {Config} from '../../config/config'; +import {Keyboard} from '../../util/keyboard'; +import {NavController} from './nav-controller'; +import {ViewController} from './view-controller'; + +/** + * @private + */ +@Directive({ + selector: '[portal]' +}) +export class Portal extends NavController { + constructor( + @Optional() hostNavCtrl: NavController, + @Optional() viewCtrl: ViewController, + app: IonicApp, + config: Config, + keyboard: Keyboard, + elementRef: ElementRef, + compiler: Compiler, + viewManager: AppViewManager, + zone: NgZone, + renderer: Renderer + ) { + super(hostNavCtrl, app, config, keyboard, elementRef, null, compiler, viewManager, zone, renderer); + } +} diff --git a/ionic/components/nav/nav.ts b/ionic/components/nav/nav.ts index 86ceb92549..b808f57dcb 100644 --- a/ionic/components/nav/nav.ts +++ b/ionic/components/nav/nav.ts @@ -1,10 +1,11 @@ -import {Component, ElementRef, Input, Optional, NgZone, Compiler, AppViewManager, Renderer, Type} from 'angular2/core'; +import {Component, ElementRef, Input, Optional, NgZone, Compiler, AppViewManager, Renderer, Type, ViewChild} from 'angular2/core'; import {IonicApp} from '../app/app'; import {Config} from '../../config/config'; import {Keyboard} from '../../util/keyboard'; import {isTrueProperty} from '../../util/util'; import {NavController} from './nav-controller'; +import {Portal} from './nav-portal'; import {ViewController} from './view-controller'; /** @@ -104,7 +105,8 @@ import {ViewController} from './view-controller'; */ @Component({ selector: 'ion-nav', - template: '
' + template: '
', + directives: [Portal] }) export class Nav extends NavController { private _root: Type; @@ -172,5 +174,9 @@ export class Nav extends NavController { this.push(this._root); } } - + + @ViewChild(Portal) + private set _navPortal(val: Portal) { + this.setPortal(val); + } } diff --git a/ionic/components/nav/test/nav-controller.spec.ts b/ionic/components/nav/test/nav-controller.spec.ts index 7a960b1288..7c7bcbb0ce 100644 --- a/ionic/components/nav/test/nav-controller.spec.ts +++ b/ionic/components/nav/test/nav-controller.spec.ts @@ -1236,6 +1236,8 @@ export function run() { setElementClass: function(){}, setElementStyle: function(){} }; + + nav._portal = new NavController(null, null, config, null, elementRef, null, null, null, null, null); return nav; } diff --git a/ionic/components/nav/view-controller.ts b/ionic/components/nav/view-controller.ts index b974b69fde..30b1f11e7a 100644 --- a/ionic/components/nav/view-controller.ts +++ b/ionic/components/nav/view-controller.ts @@ -77,6 +77,11 @@ export class ViewController { */ isOverlay: boolean = false; + /** + * @private + */ + usePortal: boolean = false; + /** * @private */ diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts index 4e96d83a48..590952479a 100644 --- a/ionic/config/modes.ts +++ b/ionic/config/modes.ts @@ -17,6 +17,9 @@ Config.setModeConfig('ios', { iconMode: 'ios', + loadingEnter: 'loading-pop-in', + loadingLeave: 'loading-pop-out', + menuType: 'reveal', modalEnter: 'modal-slide-in', @@ -46,6 +49,9 @@ Config.setModeConfig('md', { iconMode: 'md', + loadingEnter: 'loading-md-pop-in', + loadingLeave: 'loading-md-pop-out', + menuType: 'overlay', modalEnter: 'modal-md-slide-in', @@ -78,6 +84,9 @@ Config.setModeConfig('wp', { iconMode: 'ios', + loadingEnter: 'loading-wp-pop-in', + loadingLeave: 'loading-wp-pop-out', + menuType: 'overlay', modalEnter: 'modal-md-slide-in', @@ -86,6 +95,8 @@ Config.setModeConfig('wp', { pageTransition: 'wp-transition', pageTransitionDelay: 96, + spinner: 'circles', + tabbarPlacement: 'top', tabSubPages: true,