/*!
* Ladda
* http://lab.hakim.se/ladda
* MIT licensed
*
* Copyright (C) 2014 Hakim El Hattab, http://hakim.se
*/
/* jshint node:true, browser:true */
(function( root, factory ) {
// CommonJS
if( typeof exports === 'object' ) {
module.exports = factory(require('spin.js'));
}
// 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' );
},
remove: function() {
clearTimeout( timer );
button.removeAttribute( 'disabled', '' );
button.removeAttribute( 'data-loading', '' );
if( spinner ) {
spinner.stop();
spinner = null;
}
for( var i = 0, len = ALL_INSTANCES.length; i < len; i++ ) {
if( instance === ALL_INSTANCES[i] ) {
ALL_INSTANCES.splice( i, 1 );
break;
}
}
}
};
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
};
}));