ladda.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /*!
  2. * Ladda
  3. * http://lab.hakim.se/ladda
  4. * MIT licensed
  5. *
  6. * Copyright (C) 2015 Hakim El Hattab, http://hakim.se
  7. */
  8. /* jshint node:true, browser:true */
  9. (function( root, factory ) {
  10. // CommonJS
  11. if( typeof exports === 'object' ) {
  12. module.exports = factory(require('spin.js'));
  13. }
  14. // AMD module
  15. else if( typeof define === 'function' && define.amd ) {
  16. define( [ 'spin' ], factory );
  17. }
  18. // Browser global
  19. else {
  20. root.Ladda = factory( root.Spinner );
  21. }
  22. }
  23. (this, function( Spinner ) {
  24. 'use strict';
  25. // All currently instantiated instances of Ladda
  26. var ALL_INSTANCES = [];
  27. /**
  28. * Creates a new instance of Ladda which wraps the
  29. * target button element.
  30. *
  31. * @return An API object that can be used to control
  32. * the loading animation state.
  33. */
  34. function create( button ) {
  35. if( typeof button === 'undefined' ) {
  36. console.warn( "Ladda button target must be defined." );
  37. return;
  38. }
  39. // The text contents must be wrapped in a ladda-label
  40. // element, create one if it doesn't already exist
  41. if( !button.querySelector( '.ladda-label' ) ) {
  42. button.innerHTML = '<span class="ladda-label">'+ button.innerHTML +'</span>';
  43. }
  44. // The spinner component
  45. var spinner,
  46. spinnerWrapper = button.querySelector( '.ladda-spinner' );
  47. // Wrapper element for the spinner
  48. if( !spinnerWrapper ) {
  49. spinnerWrapper = document.createElement( 'span' );
  50. spinnerWrapper.className = 'ladda-spinner';
  51. }
  52. button.appendChild( spinnerWrapper );
  53. // Timer used to delay starting/stopping
  54. var timer;
  55. var instance = {
  56. /**
  57. * Enter the loading state.
  58. */
  59. start: function() {
  60. // Create the spinner if it doesn't already exist
  61. if( !spinner ) spinner = createSpinner( button );
  62. button.setAttribute( 'disabled', '' );
  63. button.setAttribute( 'data-loading', '' );
  64. clearTimeout( timer );
  65. spinner.spin( spinnerWrapper );
  66. this.setProgress( 0 );
  67. return this; // chain
  68. },
  69. /**
  70. * Enter the loading state, after a delay.
  71. */
  72. startAfter: function( delay ) {
  73. clearTimeout( timer );
  74. timer = setTimeout( function() { instance.start(); }, delay );
  75. return this; // chain
  76. },
  77. /**
  78. * Exit the loading state.
  79. */
  80. stop: function() {
  81. button.removeAttribute( 'disabled' );
  82. button.removeAttribute( 'data-loading' );
  83. // Kill the animation after a delay to make sure it
  84. // runs for the duration of the button transition
  85. clearTimeout( timer );
  86. if( spinner ) {
  87. timer = setTimeout( function() { spinner.stop(); }, 1000 );
  88. }
  89. return this; // chain
  90. },
  91. /**
  92. * Toggle the loading state on/off.
  93. */
  94. toggle: function() {
  95. if( this.isLoading() ) {
  96. this.stop();
  97. }
  98. else {
  99. this.start();
  100. }
  101. return this; // chain
  102. },
  103. /**
  104. * Sets the width of the visual progress bar inside of
  105. * this Ladda button
  106. *
  107. * @param {Number} progress in the range of 0-1
  108. */
  109. setProgress: function( progress ) {
  110. // Cap it
  111. progress = Math.max( Math.min( progress, 1 ), 0 );
  112. var progressElement = button.querySelector( '.ladda-progress' );
  113. // Remove the progress bar if we're at 0 progress
  114. if( progress === 0 && progressElement && progressElement.parentNode ) {
  115. progressElement.parentNode.removeChild( progressElement );
  116. }
  117. else {
  118. if( !progressElement ) {
  119. progressElement = document.createElement( 'div' );
  120. progressElement.className = 'ladda-progress';
  121. button.appendChild( progressElement );
  122. }
  123. progressElement.style.width = ( ( progress || 0 ) * button.offsetWidth ) + 'px';
  124. }
  125. },
  126. enable: function() {
  127. this.stop();
  128. return this; // chain
  129. },
  130. disable: function () {
  131. this.stop();
  132. button.setAttribute( 'disabled', '' );
  133. return this; // chain
  134. },
  135. isLoading: function() {
  136. return button.hasAttribute( 'data-loading' );
  137. },
  138. remove: function() {
  139. clearTimeout( timer );
  140. button.removeAttribute( 'disabled', '' );
  141. button.removeAttribute( 'data-loading', '' );
  142. if( spinner ) {
  143. spinner.stop();
  144. spinner = null;
  145. }
  146. for( var i = 0, len = ALL_INSTANCES.length; i < len; i++ ) {
  147. if( instance === ALL_INSTANCES[i] ) {
  148. ALL_INSTANCES.splice( i, 1 );
  149. break;
  150. }
  151. }
  152. }
  153. };
  154. ALL_INSTANCES.push( instance );
  155. return instance;
  156. }
  157. /**
  158. * Get the first ancestor node from an element, having a
  159. * certain type.
  160. *
  161. * @param elem An HTML element
  162. * @param type an HTML tag type (uppercased)
  163. *
  164. * @return An HTML element
  165. */
  166. function getAncestorOfTagType( elem, type ) {
  167. while ( elem.parentNode && elem.tagName !== type ) {
  168. elem = elem.parentNode;
  169. }
  170. return ( type === elem.tagName ) ? elem : undefined;
  171. }
  172. /**
  173. * Returns a list of all inputs in the given form that
  174. * have their `required` attribute set.
  175. *
  176. * @param form The from HTML element to look in
  177. *
  178. * @return A list of elements
  179. */
  180. function getRequiredFields( form ) {
  181. var requirables = [ 'input', 'textarea', 'select' ];
  182. var inputs = [];
  183. for( var i = 0; i < requirables.length; i++ ) {
  184. var candidates = form.getElementsByTagName( requirables[i] );
  185. for( var j = 0; j < candidates.length; j++ ) {
  186. if ( candidates[j].hasAttribute( 'required' ) ) {
  187. inputs.push( candidates[j] );
  188. }
  189. }
  190. }
  191. return inputs;
  192. }
  193. /**
  194. * Binds the target buttons to automatically enter the
  195. * loading state when clicked.
  196. *
  197. * @param target Either an HTML element or a CSS selector.
  198. * @param options
  199. * - timeout Number of milliseconds to wait before
  200. * automatically cancelling the animation.
  201. */
  202. function bind( target, options ) {
  203. options = options || {};
  204. var targets = [];
  205. if( typeof target === 'string' ) {
  206. targets = toArray( document.querySelectorAll( target ) );
  207. }
  208. else if( typeof target === 'object' && typeof target.nodeName === 'string' ) {
  209. targets = [ target ];
  210. }
  211. for( var i = 0, len = targets.length; i < len; i++ ) {
  212. (function() {
  213. var element = targets[i];
  214. // Make sure we're working with a DOM element
  215. if( typeof element.addEventListener === 'function' ) {
  216. var instance = create( element );
  217. var timeout = -1;
  218. element.addEventListener( 'click', function( event ) {
  219. // If the button belongs to a form, make sure all the
  220. // fields in that form are filled out
  221. var valid = true;
  222. var form = getAncestorOfTagType( element, 'FORM' );
  223. if( typeof form !== 'undefined' ) {
  224. var requireds = getRequiredFields( form );
  225. for( var i = 0; i < requireds.length; i++ ) {
  226. // Alternatively to this trim() check,
  227. // we could have use .checkValidity() or .validity.valid
  228. if( requireds[i].value.replace( /^\s+|\s+$/g, '' ) === '' ) {
  229. valid = false;
  230. }
  231. // Radiobuttons and Checkboxes need to be checked for the "checked" attribute
  232. if( (requireds[i].type === 'checkbox' || requireds[i].type === 'radio' ) && !requireds[i].checked ) {
  233. valid = false;
  234. }
  235. }
  236. }
  237. if( valid ) {
  238. // This is asynchronous to avoid an issue where setting
  239. // the disabled attribute on the button prevents forms
  240. // from submitting
  241. instance.startAfter( 1 );
  242. // Set a loading timeout if one is specified
  243. if( typeof options.timeout === 'number' ) {
  244. clearTimeout( timeout );
  245. timeout = setTimeout( instance.stop, options.timeout );
  246. }
  247. // Invoke callbacks
  248. if( typeof options.callback === 'function' ) {
  249. options.callback.apply( null, [ instance ] );
  250. }
  251. }
  252. }, false );
  253. }
  254. })();
  255. }
  256. }
  257. /**
  258. * Stops ALL current loading animations.
  259. */
  260. function stopAll() {
  261. for( var i = 0, len = ALL_INSTANCES.length; i < len; i++ ) {
  262. ALL_INSTANCES[i].stop();
  263. }
  264. }
  265. function createSpinner( button ) {
  266. var height = button.offsetHeight,
  267. spinnerColor;
  268. if( height === 0 ) {
  269. // We may have an element that is not visible so
  270. // we attempt to get the height in a different way
  271. height = parseFloat( window.getComputedStyle( button ).height );
  272. }
  273. // If the button is tall we can afford some padding
  274. if( height > 32 ) {
  275. height *= 0.8;
  276. }
  277. // Prefer an explicit height if one is defined
  278. if( button.hasAttribute( 'data-spinner-size' ) ) {
  279. height = parseInt( button.getAttribute( 'data-spinner-size' ), 10 );
  280. }
  281. // Allow buttons to specify the color of the spinner element
  282. if( button.hasAttribute( 'data-spinner-color' ) ) {
  283. spinnerColor = button.getAttribute( 'data-spinner-color' );
  284. }
  285. var lines = 12,
  286. radius = height * 0.2,
  287. length = radius * 0.6,
  288. width = radius < 7 ? 2 : 3;
  289. return new Spinner( {
  290. color: spinnerColor || '#fff',
  291. lines: lines,
  292. radius: radius,
  293. length: length,
  294. width: width,
  295. zIndex: 'auto',
  296. top: 'auto',
  297. left: 'auto',
  298. className: ''
  299. } );
  300. }
  301. function toArray( nodes ) {
  302. var a = [];
  303. for ( var i = 0; i < nodes.length; i++ ) {
  304. a.push( nodes[ i ] );
  305. }
  306. return a;
  307. }
  308. // Public API
  309. return {
  310. bind: bind,
  311. create: create,
  312. stopAll: stopAll
  313. };
  314. }));