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..ae2b691a86 --- /dev/null +++ b/ionic/components/loading/loading.ios.scss @@ -0,0 +1,71 @@ +@import "../../globals.core"; +@import "./loading"; + +// iOS Loading Indicator +// -------------------------------------------------- + +$loading-ios-padding: 24px 34px !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-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..4af60b2dc8 --- /dev/null +++ b/ionic/components/loading/loading.md.scss @@ -0,0 +1,69 @@ +@import "../../globals.core"; +@import "./loading"; + +// Material Design Loading Indicator +// -------------------------------------------------- + +$loading-md-padding: 24px !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-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..3a0c3e4299 --- /dev/null +++ b/ionic/components/loading/loading.ts @@ -0,0 +1,278 @@ +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} from '../../util/util'; +import {NavParams} from '../nav/nav-params'; +import {ViewController} from '../nav/view-controller'; + + +/** + * @name Loading + * @description + */ +export class Loading extends ViewController { + + constructor(opts: LoadingOptions = {}) { + opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : false; + opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; + + super(LoadingCmp, opts); + this.viewType = 'loading'; + this.isOverlay = 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); + } + + /** + * Open a loading indicator with the following options + * + * | Option | Type | Description | + * |-----------------------|------------|------------------------------------------------------------------------------------------------------------------| + * | icon |`string` | The spinner icon 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. | + * | enableBackdropDismiss |`boolean` | If the loading should close when the user taps the backdrop. Default false. | + * | delay |`number` | How many milliseconds to delay showing the indicator. Default 0. | + * | 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; + + 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); + } + + 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; + } + + @HostListener('body:keyup', ['$event']) + private _keyUp(ev: KeyboardEvent) { + if (this.isEnabled() && this._viewCtrl.isLast()) { + if (ev.keyCode === 27) { + console.debug('loading, escape button'); + this.bdClick(); + } + } + } + + bdClick() { + if (this.isEnabled() && this.d.enableBackdropDismiss) { + this.dismiss('backdrop'); + } + } + + 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 { + icon?: string; + content?: string; + showBackdrop?: boolean; + dismissOnPageChange?: boolean; + enableBackdropDismiss?: 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..495996af1c --- /dev/null +++ b/ionic/components/loading/loading.wp.scss @@ -0,0 +1,65 @@ +@import "../../globals.core"; +@import "./loading"; + +// Windows Loading Indicator +// -------------------------------------------------- + +$loading-wp-padding: 20px !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-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..717ac2366d --- /dev/null +++ b/ionic/components/loading/test/basic/index.ts @@ -0,0 +1,115 @@ +import {App, Page, ActionSheet, Loading, NavController, ViewController, Platform} from 'ionic-angular'; + + +@Page({ + templateUrl: 'main.html' +}) +class E2EPage { + constructor(private nav: NavController, private platform: Platform) {} + + showLoadingIos() { + let loading = Loading.create({ + icon: 'ios', + enableBackdropDismiss: true + }); + + this.nav.present(loading); + } + + showLoadingDots() { + let loading = Loading.create({ + icon: 'dots', + content: 'Loading...', + enableBackdropDismiss: true + }); + + this.nav.present(loading); + } + + showLoadingBubbles() { + let loading = Loading.create({ + icon: 'bubbles', + content: 'Loading...', + enableBackdropDismiss: true + }); + + this.nav.present(loading); + } + + showLoadingCircles() { + let loading = Loading.create({ + icon: 'circles', + content: 'Loading...', + enableBackdropDismiss: true + }); + + this.nav.present(loading); + } + + showLoadingCrescent() { + let loading = Loading.create({ + icon: 'crescent', + content: 'Please wait...', + enableBackdropDismiss: true, + duration: 1500 + }); + + this.nav.present(loading); + } + + showLoadingDefault() { + let loading = Loading.create({ + icon: 'platform', + content: 'Please wait...', + enableBackdropDismiss: true, + }); + + this.nav.present(loading); + } + + showLoadingCustom() { + let loading = Loading.create({ + content: ` +
+
+
`, + enableBackdropDismiss: true + }); + + this.nav.present(loading); + } + + showLoadingText() { + let loading = Loading.create({ + content: 'Loading Please Wait...', + enableBackdropDismiss: true + }); + + 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/ionic/components/loading/test/basic/main.html b/ionic/components/loading/test/basic/main.html new file mode 100644 index 0000000000..d07fe33602 --- /dev/null +++ b/ionic/components/loading/test/basic/main.html @@ -0,0 +1,24 @@ + + + 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/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,