/*! * 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 = ''+ button.innerHTML +''; } // 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 }; }));