text: /*!
   * Ladda
   * http://lab.hakim.se/ladda
   * MIT licensed
   *
   * Copyright (C) 2013 Hakim El Hattab, http://hakim.se
   */
  (function( root, factory ) {
  
  	// CommonJS
  	if( typeof exports === 'object' )  {
  		module.exports = factory();
  	}
  	// AMD module
  	else if( typeof define === 'function' && define.amd ) {
  		define( [ 'spin' ], factory );
  	}
  	// Browser global
  	else {
  		root.Ladda = factory( root.Spinner );
  	}
  
  }
  (this, function( Spinner ) {
  	'use strict';
  
  	// All currently instantiated instances of Ladda
  	var ALL_INSTANCES = [];
  
  	/**
  	 * Creates a new instance of Ladda which wraps the
  	 * target button element.
  	 *
  	 * @return An API object that can be used to control
  	 * the loading animation state.
  	 */
  	function create( button ) {
  
  		if( typeof button === 'undefined' ) {
  			console.warn( "Ladda button target must be defined." );
  			return;
  		}
  
  		// The text contents must be wrapped in a ladda-label
  		// element, create one if it doesn't already exist
  		if( !button.querySelector( '.ladda-label' ) ) {
  			button.innerHTML = '<span class="ladda-label">'+ button.innerHTML +'</span>';
  		}
  
  		// The spinner component
  		var spinner;
  
  		// Wrapper element for the spinner
  		var spinnerWrapper = document.createElement( 'span' );
  		spinnerWrapper.className = 'ladda-spinner';
  		button.appendChild( spinnerWrapper );
  
  		// Timer used to delay starting/stopping
  		var timer;
  
  		var instance = {
  
  			/**
  			 * Enter the loading state.
  			 */
  			start: function() {
  
  				// Create the spinner if it doesn't already exist
  				if( !spinner ) spinner = createSpinner( button );
  
  				button.setAttribute( 'disabled', '' );
  				button.setAttribute( 'data-loading', '' );
  
  				clearTimeout( timer );
  				spinner.spin( spinnerWrapper );
  
  				this.setProgress( 0 );
  
  				return this; // chain
  
  			},
  
  			/**
  			 * Enter the loading state, after a delay.
  			 */
  			startAfter: function( delay ) {
  
  				clearTimeout( timer );
  				timer = setTimeout( function() { instance.start(); }, delay );
  
  				return this; // chain
  
  			},
  
  			/**
  			 * Exit the loading state.
  			 */
  			stop: function() {
  
  				button.removeAttribute( 'disabled' );
  				button.removeAttribute( 'data-loading' );
  
  				// Kill the animation after a delay to make sure it
  				// runs for the duration of the button transition
  				clearTimeout( timer );
  
  				if( spinner ) {
  					timer = setTimeout( function() { spinner.stop(); }, 1000 );
  				}
  
  				return this; // chain
  
  			},
  
  			/**
  			 * Toggle the loading state on/off.
  			 */
  			toggle: function() {
  
  				if( this.isLoading() ) {
  					this.stop();
  				}
  				else {
  					this.start();
  				}
  
  				return this; // chain
  
  			},
  
  			/**
  			 * Sets the width of the visual progress bar inside of
  			 * this Ladda button
  			 *
  			 * @param {Number} progress in the range of 0-1
  			 */
  			setProgress: function( progress ) {
  
  				// Cap it
  				progress = Math.max( Math.min( progress, 1 ), 0 );
  
  				var progressElement = button.querySelector( '.ladda-progress' );
  
  				// Remove the progress bar if we're at 0 progress
  				if( progress === 0 && progressElement && progressElement.parentNode ) {
  					progressElement.parentNode.removeChild( progressElement );
  				}
  				else {
  					if( !progressElement ) {
  						progressElement = document.createElement( 'div' );
  						progressElement.className = 'ladda-progress';
  						button.appendChild( progressElement );
  					}
  
  					progressElement.style.width = ( ( progress || 0 ) * button.offsetWidth ) + 'px';
  				}
  
  			},
  
  			enable: function() {
  
  				this.stop();
  
  				return this; // chain
  
  			},
  
  			disable: function () {
  
  				this.stop();
  				button.setAttribute( 'disabled', '' );
  
  				return this; // chain
  
  			},
  
  			isLoading: function() {
  
  				return button.hasAttribute( 'data-loading' );
  
  			}
  
  		};
  
  		ALL_INSTANCES.push( instance );
  
  		return instance;
  
  	}
  
  	/**
  	* Get the first ancestor node from an element, having a
  	* certain type.
  	*
  	* @param elem An HTML element
  	* @param type an HTML tag type (uppercased)
  	*
  	* @return An HTML element
  	*/
  	function getAncestorOfTagType( elem, type ) {
  
  		while ( elem.parentNode && elem.tagName !== type ) {
  			elem = elem.parentNode;
  		}
  
  		return ( type === elem.tagName ) ? elem : undefined;
  
  	}
  
  	/**
  	 * Returns a list of all inputs in the given form that
  	 * have their `required` attribute set.
  	 *
  	 * @param form The from HTML element to look in
  	 *
  	 * @return A list of elements
  	 */
  	function getRequiredFields( form ) {
  
  		var requirables = [ 'input', 'textarea' ];
  		var inputs = [];
  
  		for( var i = 0; i < requirables.length; i++ ) {
  			var candidates = form.getElementsByTagName( requirables[i] );
  			for( var j = 0; j < candidates.length; j++ ) {
  				if ( candidates[j].hasAttribute( 'required' ) ) {
  					inputs.push( candidates[j] );
  				}
  			}
  		}
  
  		return inputs;
  
  	}
  
  
  	/**
  	 * Binds the target buttons to automatically enter the
  	 * loading state when clicked.
  	 *
  	 * @param target Either an HTML element or a CSS selector.
  	 * @param options
  	 *          - timeout Number of milliseconds to wait before
  	 *            automatically cancelling the animation.
  	 */
  	function bind( target, options ) {
  
  		options = options || {};
  
  		var targets = [];
  
  		if( typeof target === 'string' ) {
  			targets = toArray( document.querySelectorAll( target ) );
  		}
  		else if( typeof target === 'object' && typeof target.nodeName === 'string' ) {
  			targets = [ target ];
  		}
  
  		for( var i = 0, len = targets.length; i < len; i++ ) {
  
  			(function() {
  				var element = targets[i];
  
  				// Make sure we're working with a DOM element
  				if( typeof element.addEventListener === 'function' ) {
  					var instance = create( element );
  					var timeout = -1;
  
  					element.addEventListener( 'click', function( event ) {
  
  						// If the button belongs to a form, make sure all the
  						// fields in that form are filled out
  						var valid = true;
  						var form = getAncestorOfTagType( element, 'FORM' );
  
  						if( typeof form !== 'undefined' ) {
  							var requireds = getRequiredFields( form );
  							for( var i = 0; i < requireds.length; i++ ) {
  								// Alternatively to this trim() check,
  								// we could have use .checkValidity() or .validity.valid
  								if( requireds[i].value.replace( /^\s+|\s+$/g, '' ) === '' ) {
  									valid = false;
  								}
  							}
  						}
  
  						if( valid ) {
  							// This is asynchronous to avoid an issue where setting
  							// the disabled attribute on the button prevents forms
  							// from submitting
  							instance.startAfter( 1 );
  
  							// Set a loading timeout if one is specified
  							if( typeof options.timeout === 'number' ) {
  								clearTimeout( timeout );
  								timeout = setTimeout( instance.stop, options.timeout );
  							}
  
  							// Invoke callbacks
  							if( typeof options.callback === 'function' ) {
  								options.callback.apply( null, [ instance ] );
  							}
  						}
  
  					}, false );
  				}
  			})();
  
  		}
  
  	}
  
  	/**
  	 * Stops ALL current loading animations.
  	 */
  	function stopAll() {
  
  		for( var i = 0, len = ALL_INSTANCES.length; i < len; i++ ) {
  			ALL_INSTANCES[i].stop();
  		}
  
  	}
  
  	function createSpinner( button ) {
  
  		var height = button.offsetHeight,
  			spinnerColor;
  
  		if( height === 0 ) {
  			// We may have an element that is not visible so
  			// we attempt to get the height in a different way
  			height = parseFloat( window.getComputedStyle( button ).height );
  		}
  
  		// If the button is tall we can afford some padding
  		if( height > 32 ) {
  			height *= 0.8;
  		}
  
  		// Prefer an explicit height if one is defined
  		if( button.hasAttribute( 'data-spinner-size' ) ) {
  			height = parseInt( button.getAttribute( 'data-spinner-size' ), 10 );
  		}
  
  		// Allow buttons to specify the color of the spinner element
  		if( button.hasAttribute( 'data-spinner-color' ) ) {
  			spinnerColor = button.getAttribute( 'data-spinner-color' );
  		}
  
  		var lines = 12,
  			radius = height * 0.2,
  			length = radius * 0.6,
  			width = radius < 7 ? 2 : 3;
  
  		return new Spinner( {
  			color: spinnerColor || '#fff',
  			lines: lines,
  			radius: radius,
  			length: length,
  			width: width,
  			zIndex: 'auto',
  			top: 'auto',
  			left: 'auto',
  			className: ''
  		} );
  
  	}
  
  	function toArray( nodes ) {
  
  		var a = [];
  
  		for ( var i = 0; i < nodes.length; i++ ) {
  			a.push( nodes[ i ] );
  		}
  
  		return a;
  
  	}
  
  	// Public API
  	return {
  
  		bind: bind,
  		create: create,
  		stopAll: stopAll
  
  	};
  
  }));
  