diff --git a/ionic/components.core.scss b/ionic/components.core.scss index b34aa092cd..44814293c9 100644 --- a/ionic/components.core.scss +++ b/ionic/components.core.scss @@ -20,7 +20,8 @@ "components/modal/modal", "components/scroll/scroll", "components/scroll/pull-to-refresh", - "components/slides/slides"; + "components/slides/slides", + "components/spinner/spinner"; // Ionicons (to be replaced with SVGs) diff --git a/ionic/components/spinner/spinner.scss b/ionic/components/spinner/spinner.scss new file mode 100644 index 0000000000..cce517e8b4 --- /dev/null +++ b/ionic/components/spinner/spinner.scss @@ -0,0 +1,119 @@ + + +// Spinners +// -------------------------------------------------- + +ion-spinner { + display: inline-block; + position: relative; + width: 28px; + height: 28px; +} + +ion-spinner svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + transform: translateZ(0); +} + +ion-spinner.spinner-paused svg { + animation-play-state: paused; +} + + +// Spinner: ios / ios-small +// -------------------------------------------------- + +.spinner-ios line, +.spinner-ios-small line { + stroke: #69717d;; + stroke-width: 4px; + stroke-linecap: round; +} + +.spinner-ios svg, +.spinner-ios-small svg { + animation: spinner-fade-out 1s linear infinite; +} + + +// Spinner: bubbles +// -------------------------------------------------- + +.spinner-bubbles circle { + fill: black; +} + +.spinner-bubbles svg { + animation: spinner-scale-out 1s linear infinite; +} + + +// Spinner: circles +// -------------------------------------------------- + +.spinner-circles circle { + fill: #69717d; +} + +.spinner-circles svg { + animation: spinner-fade-out 1s linear infinite; +} + + +// Spinner: crescent +// -------------------------------------------------- + +.spinner-crescent circle { + fill: transparent; + stroke: black; + stroke-width: 4px; + stroke-dasharray: 128px; + stroke-dashoffset: 82px; +} + +.spinner-crescent svg { + animation: spinner-rotate 1s linear infinite; +} + + +// Spinner: dots +// -------------------------------------------------- + +.spinner-dots circle { + fill: #444; + stroke-width: 0; +} + +.spinner-dots svg { + animation: spinner-dots 1s linear infinite; + transform-origin: center; +} + + +// Animation Keyframes +// -------------------------------------------------- + +@keyframes spinner-fade-out { + 0% { opacity: 1; } + 100% { opacity: 0; } +} + +@keyframes spinner-scale-out { + 0% { transform: scale(1, 1); } + 100% { transform: scale(0, 0); } +} + +@keyframes spinner-rotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes spinner-dots { + 0% { opacity: 0.9; transform: scale(1, 1); } + 50% { opacity: 0.3; transform: scale(0.4, 0.4); } + 100% { opacity: 0.9; transform: scale(1, 1); } +} diff --git a/ionic/components/spinner/spinner.ts b/ionic/components/spinner/spinner.ts new file mode 100644 index 0000000000..1a300d7402 --- /dev/null +++ b/ionic/components/spinner/spinner.ts @@ -0,0 +1,283 @@ +import {Component, Input} from 'angular2/core'; +import {NgStyle} from 'angular2/common'; + +import {Config} from '../../config/config'; + + +/** + * @name Spinner + * @description + * The `ion-spinner` component provides a variety of animated SVG spinners. + * Spinners enables you to give users feedback that the app is actively + * processing/thinking/waiting/chillin’ out, or whatever you’d like it to indicate. + * By default, the `ion-refresher` feature uses this spinner component while it's + * the refresher is in the `refreshing` state. + * + * Ionic offers a handful of spinners out of the box, and by default, it will use + * the appropriate spinner for the platform on which it’s running. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
+ * ios + * + * + *
+ * ios-small + * + * + *
+ * bubbles + * + * + *
+ * circles + * + * + *
+ * crescent + * + * + *
+ * dots + * + * + *
+ * + * @usage + * The following code would use the default spinner for the platform it's + * running from. If it's neither iOS or Android, it'll default to use `ios`. + * + * ```html + * + * ``` + * + * By setting the `name` property, you can specify which predefined spinner to + * use, no matter what the platform is. + * + * ```html + * + * ``` + * + * ## Styling SVG with CSS + * One cool thing about SVG is its ability to be styled with CSS! One thing to note + * is that some of the CSS properties on an SVG element have different names. For + * example, SVG uses the term `stroke` instead of `border`, and `fill` instead + * of `background-color`. + * + * ```css + * ion-spinner svg { + * width: 28px; + * height: 28px; + * stroke: #444; + * fill: #222; + * } + * ``` + */ +@Component({ + selector: 'ion-spinner', + template: + '' + + '' + + '' + + '' + + '' + + '', + directives: [NgStyle], + host: { + '[class]': '_applied', + '[class.spinner-paused]': 'paused' + } +}) +export class Spinner { + private _c: any[]; + private _l: any[]; + private _name: string; + private _dur: number = null; + private _init: boolean; + private _applied: string; + + /** + * @input {string} SVG spinner name. + */ + @Input() + get name(): string { + return this._name; + } + + set name(val: string) { + this._name = val; + this.load(); + } + + /** + * @input {string} How long it takes it to do one loop. + */ + @Input() + get duration(): number { + return this._dur; + } + + set duration(val: number) { + this._dur = val; + this.load(); + } + + /** + * @input {string} If the animation is paused or not. Defaults to `false`. + */ + @Input() paused: boolean = false; + + constructor(private _config: Config) {} + + ngOnInit() { + this._init = true; + this.load(); + } + + load() { + if (this._init) { + this._l = []; + this._c = []; + + var name = this._name || this._config.get('spinner', 'ios'); + + const spinner = SPINNERS[name]; + if (spinner) { + this._applied = 'spinner-' + name; + + if (spinner.lines) { + for (var i = 0, l = spinner.lines; i < l; i++) { + this._l.push( this._loadEle(spinner, i, l) ); + } + + } else if (spinner.circles) { + for (var i = 0, l = spinner.circles; i < l; i++) { + this._c.push( this._loadEle(spinner, i, l) ); + } + } + + } + } + } + + _loadEle(spinner: any, index: number, total: number) { + let duration = this._dur || spinner.dur + let data = spinner.fn(duration, index, total); + data.style.animationDuration = duration + 'ms'; + return data; + } + +} + +const SPINNERS = { + + ios: { + dur: 1000, + lines: 12, + fn: function(dur, index, total) { + return { + y1: 17, + y2: 29, + style: { + transform: 'rotate(' + (30 * index + (index < 6 ? 180 : -180)) + 'deg)', + animationDelay: -(dur - ((dur / total) * index)) + 'ms' + } + } + } + }, + + 'ios-small': { + dur: 1000, + lines: 12, + fn: function(dur, index, total) { + return { + y1: 12, + y2: 20, + style: { + transform: 'rotate(' + (30 * index + (index < 6 ? 180 : -180)) + 'deg)', + animationDelay: -(dur - ((dur / total) * index)) + 'ms' + } + } + } + }, + + bubbles: { + dur: 1000, + circles: 9, + fn: function(dur, index, total) { + return { + r: 5, + style: { + top: 9 * Math.sin(2 * Math.PI * index / total), + left: 9 * Math.cos(2 * Math.PI * index / total), + animationDelay: -(dur - ((dur / total) * index)) + 'ms' + } + } + } + }, + + circles: { + dur: 1000, + circles: 8, + fn: function(dur, index, total) { + return { + r: 5, + style: { + top: 9 * Math.sin(2 * Math.PI * index / total), + left: 9 * Math.cos(2 * Math.PI * index / total), + animationDelay: -(dur - ((dur / total) * index)) + 'ms' + } + } + } + }, + + crescent: { + dur: 750, + circles: 1, + fn: function(dur) { + return { + r: 26, + style: {} + } + } + }, + + dots: { + dur: 750, + circles: 3, + fn: function(dur, index, total) { + return { + r: 6, + style: { + left: (9 - (9 * index)), + animationDelay: -(110 * index) + 'ms' + } + } + } + } + +}; diff --git a/ionic/components/spinner/test/basic/index.ts b/ionic/components/spinner/test/basic/index.ts new file mode 100644 index 0000000000..e578203808 --- /dev/null +++ b/ionic/components/spinner/test/basic/index.ts @@ -0,0 +1,13 @@ +import {App} from 'ionic-angular'; + + +@App({ + templateUrl: 'main.html' +}) +class E2EApp { + paused: boolean = false; + + toggleState() { + this.paused = !this.paused; + } +} diff --git a/ionic/components/spinner/test/basic/main.html b/ionic/components/spinner/test/basic/main.html new file mode 100644 index 0000000000..af93f162fe --- /dev/null +++ b/ionic/components/spinner/test/basic/main.html @@ -0,0 +1,55 @@ + + + Spinners + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Platform Default + +
ios + +
ios-small + +
bubbles + +
circles + +
crescent + +
dots + +
+ + + +
diff --git a/ionic/config/directives.ts b/ionic/config/directives.ts index 9fc4c72274..dc8da6d53a 100644 --- a/ionic/config/directives.ts +++ b/ionic/config/directives.ts @@ -19,6 +19,7 @@ import {Item} from '../components/item/item'; import {ItemSliding} from '../components/item/item-sliding'; import {Toolbar, ToolbarTitle, ToolbarItem} from '../components/toolbar/toolbar'; import {Icon} from '../components/icon/icon'; +import {Spinner} from '../components/spinner/spinner'; import {Checkbox} from '../components/checkbox/checkbox'; import {Select} from '../components/select/select'; import {Option} from '../components/option/option'; @@ -80,6 +81,7 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when'; * * **Media** * - Icon + * - Spinner * * **Forms** * - Searchbar @@ -146,6 +148,7 @@ export const IONIC_DIRECTIVES = [ // Media Icon, + Spinner, // Forms Searchbar, diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts index 8cba1a0cca..d2dd2355ee 100644 --- a/ionic/config/modes.ts +++ b/ionic/config/modes.ts @@ -25,6 +25,8 @@ Config.setModeConfig('ios', { pageTransition: 'ios-transition', pageTransitionDelay: 16, + spinner: 'ios', + tabbarPlacement: 'bottom', }); @@ -52,6 +54,8 @@ Config.setModeConfig('md', { pageTransition: 'md-transition', pageTransitionDelay: 96, + spinner: 'crescent', + tabbarHighlight: true, tabbarPlacement: 'top',