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