summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimotej Lazar <timotej.lazar@fri.uni-lj.si>2015-10-21 16:00:39 +0200
committerTimotej Lazar <timotej.lazar@fri.uni-lj.si>2015-10-21 16:00:39 +0200
commit967d8ca786f57d1cad86dc1c7d1f2dc99b9ae7f7 (patch)
tree844c9c57bebdc43f7008e845493a84ffac450388
parentfbcca83fc33f3a622364ae90c5d82bbbdc9a9dc6 (diff)
Show a spinner on hint/test buttons when clicked
-rw-r--r--css/ladda-themeless.min.css7
-rw-r--r--index.html14
-rw-r--r--js/codeq/prolog.js14
-rw-r--r--js/codeq/python.js18
-rw-r--r--js/codeq/robot.js7
-rw-r--r--js/ladda/ladda.jquery.js46
-rw-r--r--js/ladda/ladda.js412
-rw-r--r--js/ladda/spin.js349
8 files changed, 852 insertions, 15 deletions
diff --git a/css/ladda-themeless.min.css b/css/ladda-themeless.min.css
new file mode 100644
index 0000000..126612d
--- /dev/null
+++ b/css/ladda-themeless.min.css
@@ -0,0 +1,7 @@
+/*!
+ * Ladda
+ * http://lab.hakim.se/ladda
+ * MIT licensed
+ *
+ * Copyright (C) 2014 Hakim El Hattab, http://hakim.se
+ */.ladda-button{position:relative}.ladda-button .ladda-spinner{position:absolute;z-index:2;display:inline-block;width:32px;height:32px;top:50%;margin-top:0;opacity:0;pointer-events:none}.ladda-button .ladda-label{position:relative;z-index:3}.ladda-button .ladda-progress{position:absolute;width:0;height:100%;left:0;top:0;background:rgba(0,0,0,0.2);visibility:hidden;opacity:0;-webkit-transition:0.1s linear all !important;-moz-transition:0.1s linear all !important;-ms-transition:0.1s linear all !important;-o-transition:0.1s linear all !important;transition:0.1s linear all !important}.ladda-button[data-loading] .ladda-progress{opacity:1;visibility:visible}.ladda-button,.ladda-button .ladda-spinner,.ladda-button .ladda-label{-webkit-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-moz-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-ms-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-o-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important}.ladda-button[data-style=zoom-in],.ladda-button[data-style=zoom-in] .ladda-spinner,.ladda-button[data-style=zoom-in] .ladda-label,.ladda-button[data-style=zoom-out],.ladda-button[data-style=zoom-out] .ladda-spinner,.ladda-button[data-style=zoom-out] .ladda-label{-webkit-transition:0.3s ease all !important;-moz-transition:0.3s ease all !important;-ms-transition:0.3s ease all !important;-o-transition:0.3s ease all !important;transition:0.3s ease all !important}.ladda-button[data-style=expand-right] .ladda-spinner{right:-6px}.ladda-button[data-style=expand-right][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-right][data-size="xs"] .ladda-spinner{right:-12px}.ladda-button[data-style=expand-right][data-loading]{padding-right:56px}.ladda-button[data-style=expand-right][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-right][data-loading][data-size="s"],.ladda-button[data-style=expand-right][data-loading][data-size="xs"]{padding-right:40px}.ladda-button[data-style=expand-left] .ladda-spinner{left:26px}.ladda-button[data-style=expand-left][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-left][data-size="xs"] .ladda-spinner{left:4px}.ladda-button[data-style=expand-left][data-loading]{padding-left:56px}.ladda-button[data-style=expand-left][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-left][data-loading][data-size="s"],.ladda-button[data-style=expand-left][data-loading][data-size="xs"]{padding-left:40px}.ladda-button[data-style=expand-up]{overflow:hidden}.ladda-button[data-style=expand-up] .ladda-spinner{top:-32px;left:50%;margin-left:0}.ladda-button[data-style=expand-up][data-loading]{padding-top:54px}.ladda-button[data-style=expand-up][data-loading] .ladda-spinner{opacity:1;top:26px;margin-top:0}.ladda-button[data-style=expand-up][data-loading][data-size="s"],.ladda-button[data-style=expand-up][data-loading][data-size="xs"]{padding-top:32px}.ladda-button[data-style=expand-up][data-loading][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-up][data-loading][data-size="xs"] .ladda-spinner{top:4px}.ladda-button[data-style=expand-down]{overflow:hidden}.ladda-button[data-style=expand-down] .ladda-spinner{top:62px;left:50%;margin-left:0}.ladda-button[data-style=expand-down][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-down][data-size="xs"] .ladda-spinner{top:40px}.ladda-button[data-style=expand-down][data-loading]{padding-bottom:54px}.ladda-button[data-style=expand-down][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-down][data-loading][data-size="s"],.ladda-button[data-style=expand-down][data-loading][data-size="xs"]{padding-bottom:32px}.ladda-button[data-style=slide-left]{overflow:hidden}.ladda-button[data-style=slide-left] .ladda-label{position:relative}.ladda-button[data-style=slide-left] .ladda-spinner{left:100%;margin-left:0}.ladda-button[data-style=slide-left][data-loading] .ladda-label{opacity:0;left:-100%}.ladda-button[data-style=slide-left][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-right]{overflow:hidden}.ladda-button[data-style=slide-right] .ladda-label{position:relative}.ladda-button[data-style=slide-right] .ladda-spinner{right:100%;margin-left:0;left:16px}.ladda-button[data-style=slide-right][data-loading] .ladda-label{opacity:0;left:100%}.ladda-button[data-style=slide-right][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-up]{overflow:hidden}.ladda-button[data-style=slide-up] .ladda-label{position:relative}.ladda-button[data-style=slide-up] .ladda-spinner{left:50%;margin-left:0;margin-top:1em}.ladda-button[data-style=slide-up][data-loading] .ladda-label{opacity:0;top:-1em}.ladda-button[data-style=slide-up][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=slide-down]{overflow:hidden}.ladda-button[data-style=slide-down] .ladda-label{position:relative}.ladda-button[data-style=slide-down] .ladda-spinner{left:50%;margin-left:0;margin-top:-2em}.ladda-button[data-style=slide-down][data-loading] .ladda-label{opacity:0;top:1em}.ladda-button[data-style=slide-down][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=zoom-out]{overflow:hidden}.ladda-button[data-style=zoom-out] .ladda-spinner{left:50%;margin-left:32px;-webkit-transform:scale(2.5);-moz-transform:scale(2.5);-ms-transform:scale(2.5);-o-transform:scale(2.5);transform:scale(2.5)}.ladda-button[data-style=zoom-out] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-out][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(0.5);-moz-transform:scale(0.5);-ms-transform:scale(0.5);-o-transform:scale(0.5);transform:scale(0.5)}.ladda-button[data-style=zoom-out][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=zoom-in]{overflow:hidden}.ladda-button[data-style=zoom-in] .ladda-spinner{left:50%;margin-left:-16px;-webkit-transform:scale(0.2);-moz-transform:scale(0.2);-ms-transform:scale(0.2);-o-transform:scale(0.2);transform:scale(0.2)}.ladda-button[data-style=zoom-in] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-in][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(2.2);-moz-transform:scale(2.2);-ms-transform:scale(2.2);-o-transform:scale(2.2);transform:scale(2.2)}.ladda-button[data-style=zoom-in][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=contract]{overflow:hidden;width:100px}.ladda-button[data-style=contract] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract][data-loading]{border-radius:50%;width:52px}.ladda-button[data-style=contract][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=contract-overlay]{overflow:hidden;width:100px;box-shadow:0px 0px 0px 2000px transparent}.ladda-button[data-style=contract-overlay] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract-overlay][data-loading]{border-radius:50%;width:52px;box-shadow:0px 0px 0px 2000px rgba(0,0,0,0.8)}.ladda-button[data-style=contract-overlay][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract-overlay][data-loading] .ladda-spinner{opacity:1}
diff --git a/index.html b/index.html
index 68b2212..b5e8e89 100644
--- a/index.html
+++ b/index.html
@@ -34,6 +34,7 @@
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/bootstrap-theme.min.css" rel="stylesheet">
+ <link rel="stylesheet" href="css/ladda-themeless.min.css">
<!-- App -->
<link rel="stylesheet" href="css/codeq.css" type="text/css">
<link rel="stylesheet" href="css/codeq/console.css" type="text/css">
@@ -278,8 +279,8 @@
<nav class="navbar navbar-default block-toolbar">
<div class="container-fluid">
<button type="button" class="btn btn-default navbar-btn btn-plan" data-tkey="btn_plan">Plan</button>
- <button type="button" class="btn btn-default navbar-btn btn-hint" data-tkey="btn_hint">Hint</button>
- <button type="button" class="btn btn-default navbar-btn btn-test" data-tkey="btn_test">Test</button>
+ <button type="button" class="btn btn-default navbar-btn ladda-button btn-hint" data-style="slide-up" data-spinner-color="blue"><span class="ladda-label" data-tkey="btn_hint">Hint</span></button>
+ <button type="button" class="btn btn-default navbar-btn ladda-button btn-test" data-style="slide-up" data-spinner-color="blue"><span class="ladda-label" data-tkey="btn_test">Test</span></button>
</div>
</nav>
<div class="code_editor"></div>
@@ -319,8 +320,8 @@
<nav class="navbar navbar-default block-toolbar">
<div class="container-fluid">
<button type="button" class="btn btn-default navbar-btn btn-plan" data-tkey="btn_plan">Plan</button>
- <button type="button" class="btn btn-default navbar-btn btn-hint" data-tkey="btn_hint">Hint</button>
- <button type="button" class="btn btn-default navbar-btn btn-test" data-tkey="btn_test">Test</button>
+ <button type="button" class="btn btn-default navbar-btn ladda-button btn-hint" data-style="slide-up" data-spinner-color="blue"><span class="ladda-label" data-tkey="btn_hint">Hint</span></button>
+ <button type="button" class="btn btn-default navbar-btn ladda-button btn-test" data-style="slide-up" data-spinner-color="blue"><span class="ladda-label" data-tkey="btn_test">Test</span></button>
<button type="button" class="btn btn-default navbar-btn btn-run" data-tkey="btn_run">Run</button>
<button type="button" class="btn btn-default navbar-btn btn-stop" data-tkey="btn_stop">Stop</button>
</div>
@@ -351,7 +352,7 @@
<nav class="navbar navbar-default block-toolbar">
<div class="container-fluid">
<button type="button" class="btn btn-default navbar-btn btn-plan">Plan</button>
- <button type="button" class="btn btn-default navbar-btn btn-hint">Hint</button>
+ <button type="button" class="btn btn-default navbar-btn ladda-button btn-hint" data-style="slide-up" data-spinner-color="blue"><span class="ladda-label" data-tkey="btn_hint">Hint</span></button>
<button type="button" class="btn btn-default navbar-btn btn-run">Run</button>
<button type="button" class="btn btn-default navbar-btn btn-stop">Stop</button>
</div>
@@ -461,6 +462,9 @@
<script src="js/q.js"></script>
<!-- Bootstrap -->
<script src="js/bootstrap/bootstrap.min.js"></script>
+ <script src="js/ladda/spin.js"></script>
+ <script src="js/ladda/ladda.js"></script>
+ <script src="js/ladda/ladda.jquery.js"></script>
<!-- CodeMirror stuff -->
<script src="js/codemirror/codemirror.js"></script>
<script src="js/codemirror/matchbrackets.js"></script>
diff --git a/js/codeq/prolog.js b/js/codeq/prolog.js
index b73d037..6b7da9c 100644
--- a/js/codeq/prolog.js
+++ b/js/codeq/prolog.js
@@ -16,8 +16,8 @@
jqAllQuadrants = jqDescription.add(jqCode).add(jqConsole).add(jqInfo), // all the quadrants
// buttons
jqBtnPlan = jqScreen.find('.btn-plan'),
- jqBtnHint = jqScreen.find('.btn-hint'),
- jqBtnTest = jqScreen.find('.btn-test'),
+ jqBtnHint = jqScreen.find('.btn-hint').ladda(),
+ jqBtnTest = jqScreen.find('.btn-test').ladda(),
jqAllButtons = jqBtnPlan.add(jqBtnHint).add(jqBtnTest), // all the buttons
// misc
currentSubState = null,
@@ -245,7 +245,8 @@
jqBtnHint.on('click', function () {
terminal.append('hint.\n', 'input');
terminal.inputDisable();
- var doc = editor.getDoc();
+ jqBtnTest.prop('disabled', true);
+ jqBtnHint.ladda('start');
codeq.comms.sendHint({
'language': 'prolog',
'program': editor.getDoc().getValue(),
@@ -264,13 +265,16 @@
.fin(function () {
terminal.inputEnable();
terminal.append('?- ', 'output');
+ jqBtnHint.ladda('stop');
+ jqBtnTest.prop('disabled', false);
})
.done();
});
jqBtnTest.on('click', function () {
terminal.append('test.\n', 'input');
terminal.inputDisable();
- var doc = editor.getDoc();
+ jqBtnHint.prop('disabled', true);
+ jqBtnTest.ladda('start');
codeq.comms.sendTest({
'language': 'prolog',
'program': editor.getDoc().getValue(),
@@ -289,6 +293,8 @@
.fin(function () {
terminal.inputEnable();
terminal.append('?- ', 'output');
+ jqBtnTest.ladda('stop');
+ jqBtnHint.prop('disabled', false);
})
.done();
});
diff --git a/js/codeq/python.js b/js/codeq/python.js
index 35c4be8..bd5d96e 100644
--- a/js/codeq/python.js
+++ b/js/codeq/python.js
@@ -18,8 +18,8 @@
jqAllQuadrants = jqDescription.add(jqCode).add(jqConsole).add(jqInfo), // all the quadrants
// buttons
jqBtnPlan = jqScreen.find('.btn-plan'),
- jqBtnHint = jqScreen.find('.btn-hint'),
- jqBtnTest = jqScreen.find('.btn-test'),
+ jqBtnHint = jqScreen.find('.btn-hint').ladda(),
+ jqBtnTest = jqScreen.find('.btn-test').ladda(),
jqBtnRun = jqScreen.find('.btn-run'),
jqBtnStop = jqScreen.find('.btn-stop'),
jqInfoButtons = jqBtnPlan.add(jqBtnHint).add(jqBtnTest), // all info-focusing buttons
@@ -203,7 +203,8 @@
}
});
jqBtnHint.on('click', function () {
- var doc = editor.getDoc();
+ jqBtnTest.prop('disabled', true);
+ jqBtnHint.ladda('start');
codeq.comms.sendHint({
'language': 'python',
'program': editor.getDoc().getValue(),
@@ -219,10 +220,15 @@
}
})
.fail(commError)
+ .fin(function () {
+ jqBtnHint.ladda('stop');
+ jqBtnTest.prop('disabled', false);
+ })
.done();
});
jqBtnTest.on('click', function () {
- var doc = editor.getDoc();
+ jqBtnHint.prop('disabled', true);
+ jqBtnTest.ladda('start');
codeq.comms.sendTest({
'language': 'python',
'program': editor.getDoc().getValue(),
@@ -238,6 +244,10 @@
}
})
.fail(commError)
+ .fin(function () {
+ jqBtnTest.ladda('stop');
+ jqBtnHint.prop('disabled', false);
+ })
.done();
});
jqBtnRun.on('click', function () {
diff --git a/js/codeq/robot.js b/js/codeq/robot.js
index a1b76f1..be164ee 100644
--- a/js/codeq/robot.js
+++ b/js/codeq/robot.js
@@ -14,7 +14,7 @@
jqAllQuadrants = jqDescription.add(jqCode).add(jqConsole).add(jqInfo), // all the quadrants
// buttons
jqBtnPlan = jqScreen.find('.btn-plan'),
- jqBtnHint = jqScreen.find('.btn-hint'),
+ jqBtnHint = jqScreen.find('.btn-hint').ladda(),
jqBtnRun = jqScreen.find('.btn-run'),
jqBtnStop = jqScreen.find('.btn-stop'),
jqInfoButtons = jqBtnPlan.add(jqBtnHint), // all info-focusing buttons
@@ -199,7 +199,7 @@
}
});
jqBtnHint.on('click', function () {
- var doc = editor.getDoc();
+ jqBtnHint.ladda('start');
codeq.comms.sendHint({
'language': 'robot',
'program': editor.getDoc().getValue(),
@@ -215,6 +215,9 @@
}
})
.fail(commError)
+ .fin(function () {
+ jqBtnHint.ladda('stop');
+ })
.done();
});
jqBtnRun.on('click', function () {
diff --git a/js/ladda/ladda.jquery.js b/js/ladda/ladda.jquery.js
new file mode 100644
index 0000000..42c3c80
--- /dev/null
+++ b/js/ladda/ladda.jquery.js
@@ -0,0 +1,46 @@
+/*!
+ * Ladda for jQuery
+ * http://lab.hakim.se/ladda
+ * MIT licensed
+ *
+ * Copyright (C) 2014 Hakim El Hattab, http://hakim.se
+ */
+
+(function( Ladda, $ ) {
+ if ($ === undefined)
+ return console.error( 'jQuery required for Ladda.jQuery' );
+
+ var arr = [];
+
+ $ = $.extend( $, {
+ ladda: function( arg ) {
+ if( arg === 'stopAll' )
+ Ladda.stopAll();
+ }
+ });
+
+ $.fn = $.extend( $.fn, {
+ ladda: function( arg ) {
+ var args = arr.slice.call( arguments, 1 );
+
+ if( arg === 'bind' ) {
+ args.unshift( $( this ).selector );
+ Ladda.bind.apply( Ladda, args );
+ }
+ else {
+ $( this ).each( function() {
+ var $this = $( this ), ladda;
+
+ if( arg === undefined )
+ $this.data( 'ladda', Ladda.create( this ) );
+ else {
+ ladda = $this.data( 'ladda' );
+ ladda[arg].apply( ladda, args );
+ }
+ });
+ }
+
+ return this;
+ }
+ });
+}( this.Ladda, this.jQuery )); \ No newline at end of file
diff --git a/js/ladda/ladda.js b/js/ladda/ladda.js
new file mode 100644
index 0000000..40f2960
--- /dev/null
+++ b/js/ladda/ladda.js
@@ -0,0 +1,412 @@
+/*!
+ * 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
+
+ };
+
+}));
diff --git a/js/ladda/spin.js b/js/ladda/spin.js
new file mode 100644
index 0000000..a3812e2
--- /dev/null
+++ b/js/ladda/spin.js
@@ -0,0 +1,349 @@
+/**
+ * Copyright (c) 2011-2014 Felix Gnass
+ * Licensed under the MIT license
+ */
+(function(root, factory) {
+
+ /* CommonJS */
+ if (typeof exports == 'object') module.exports = factory()
+
+ /* AMD module */
+ else if (typeof define == 'function' && define.amd) define(factory)
+
+ /* Browser global */
+ else root.Spinner = factory()
+}
+(this, function() {
+ "use strict";
+
+ var prefixes = ['webkit', 'Moz', 'ms', 'O'] /* Vendor prefixes */
+ , animations = {} /* Animation rules keyed by their name */
+ , useCssAnimations /* Whether to use CSS animations or setTimeout */
+
+ /**
+ * Utility function to create elements. If no tag name is given,
+ * a DIV is created. Optionally properties can be passed.
+ */
+ function createEl(tag, prop) {
+ var el = document.createElement(tag || 'div')
+ , n
+
+ for(n in prop) el[n] = prop[n]
+ return el
+ }
+
+ /**
+ * Appends children and returns the parent.
+ */
+ function ins(parent /* child1, child2, ...*/) {
+ for (var i=1, n=arguments.length; i<n; i++)
+ parent.appendChild(arguments[i])
+
+ return parent
+ }
+
+ /**
+ * Insert a new stylesheet to hold the @keyframe or VML rules.
+ */
+ var sheet = (function() {
+ var el = createEl('style', {type : 'text/css'})
+ ins(document.getElementsByTagName('head')[0], el)
+ return el.sheet || el.styleSheet
+ }())
+
+ /**
+ * Creates an opacity keyframe animation rule and returns its name.
+ * Since most mobile Webkits have timing issues with animation-delay,
+ * we create separate rules for each line/segment.
+ */
+ function addAnimation(alpha, trail, i, lines) {
+ var name = ['opacity', trail, ~~(alpha*100), i, lines].join('-')
+ , start = 0.01 + i/lines * 100
+ , z = Math.max(1 - (1-alpha) / trail * (100-start), alpha)
+ , prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase()
+ , pre = prefix && '-' + prefix + '-' || ''
+
+ if (!animations[name]) {
+ sheet.insertRule(
+ '@' + pre + 'keyframes ' + name + '{' +
+ '0%{opacity:' + z + '}' +
+ start + '%{opacity:' + alpha + '}' +
+ (start+0.01) + '%{opacity:1}' +
+ (start+trail) % 100 + '%{opacity:' + alpha + '}' +
+ '100%{opacity:' + z + '}' +
+ '}', sheet.cssRules.length)
+
+ animations[name] = 1
+ }
+
+ return name
+ }
+
+ /**
+ * Tries various vendor prefixes and returns the first supported property.
+ */
+ function vendor(el, prop) {
+ var s = el.style
+ , pp
+ , i
+
+ prop = prop.charAt(0).toUpperCase() + prop.slice(1)
+ for(i=0; i<prefixes.length; i++) {
+ pp = prefixes[i]+prop
+ if(s[pp] !== undefined) return pp
+ }
+ if(s[prop] !== undefined) return prop
+ }
+
+ /**
+ * Sets multiple style properties at once.
+ */
+ function css(el, prop) {
+ for (var n in prop)
+ el.style[vendor(el, n)||n] = prop[n]
+
+ return el
+ }
+
+ /**
+ * Fills in default values.
+ */
+ function merge(obj) {
+ for (var i=1; i < arguments.length; i++) {
+ var def = arguments[i]
+ for (var n in def)
+ if (obj[n] === undefined) obj[n] = def[n]
+ }
+ return obj
+ }
+
+ /**
+ * Returns the absolute page-offset of the given element.
+ */
+ function pos(el) {
+ var o = { x:el.offsetLeft, y:el.offsetTop }
+ while((el = el.offsetParent))
+ o.x+=el.offsetLeft, o.y+=el.offsetTop
+
+ return o
+ }
+
+ /**
+ * Returns the line color from the given string or array.
+ */
+ function getColor(color, idx) {
+ return typeof color == 'string' ? color : color[idx % color.length]
+ }
+
+ // Built-in defaults
+
+ var defaults = {
+ lines: 12, // The number of lines to draw
+ length: 7, // The length of each line
+ width: 5, // The line thickness
+ radius: 10, // The radius of the inner circle
+ rotate: 0, // Rotation offset
+ corners: 1, // Roundness (0..1)
+ color: '#000', // #rgb or #rrggbb
+ direction: 1, // 1: clockwise, -1: counterclockwise
+ speed: 1, // Rounds per second
+ trail: 100, // Afterglow percentage
+ opacity: 1/4, // Opacity of the lines
+ fps: 20, // Frames per second when using setTimeout()
+ zIndex: 2e9, // Use a high z-index by default
+ className: 'spinner', // CSS class to assign to the element
+ top: '50%', // center vertically
+ left: '50%', // center horizontally
+ position: 'absolute' // element position
+ }
+
+ /** The constructor */
+ function Spinner(o) {
+ this.opts = merge(o || {}, Spinner.defaults, defaults)
+ }
+
+ // Global defaults that override the built-ins:
+ Spinner.defaults = {}
+
+ merge(Spinner.prototype, {
+
+ /**
+ * Adds the spinner to the given target element. If this instance is already
+ * spinning, it is automatically removed from its previous target b calling
+ * stop() internally.
+ */
+ spin: function(target) {
+ this.stop()
+
+ var self = this
+ , o = self.opts
+ , el = self.el = css(createEl(0, {className: o.className}), {position: o.position, width: 0, zIndex: o.zIndex})
+ , mid = o.radius+o.length+o.width
+
+ css(el, {
+ left: o.left,
+ top: o.top
+ })
+
+ if (target) {
+ target.insertBefore(el, target.firstChild||null)
+ }
+
+ el.setAttribute('role', 'progressbar')
+ self.lines(el, self.opts)
+
+ if (!useCssAnimations) {
+ // No CSS animation support, use setTimeout() instead
+ var i = 0
+ , start = (o.lines - 1) * (1 - o.direction) / 2
+ , alpha
+ , fps = o.fps
+ , f = fps/o.speed
+ , ostep = (1-o.opacity) / (f*o.trail / 100)
+ , astep = f/o.lines
+
+ ;(function anim() {
+ i++;
+ for (var j = 0; j < o.lines; j++) {
+ alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity)
+
+ self.opacity(el, j * o.direction + start, alpha, o)
+ }
+ self.timeout = self.el && setTimeout(anim, ~~(1000/fps))
+ })()
+ }
+ return self
+ },
+
+ /**
+ * Stops and removes the Spinner.
+ */
+ stop: function() {
+ var el = this.el
+ if (el) {
+ clearTimeout(this.timeout)
+ if (el.parentNode) el.parentNode.removeChild(el)
+ this.el = undefined
+ }
+ return this
+ },
+
+ /**
+ * Internal method that draws the individual lines. Will be overwritten
+ * in VML fallback mode below.
+ */
+ lines: function(el, o) {
+ var i = 0
+ , start = (o.lines - 1) * (1 - o.direction) / 2
+ , seg
+
+ function fill(color, shadow) {
+ return css(createEl(), {
+ position: 'absolute',
+ width: (o.length+o.width) + 'px',
+ height: o.width + 'px',
+ background: color,
+ boxShadow: shadow,
+ transformOrigin: 'left',
+ transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)',
+ borderRadius: (o.corners * o.width>>1) + 'px'
+ })
+ }
+
+ for (; i < o.lines; i++) {
+ seg = css(createEl(), {
+ position: 'absolute',
+ top: 1+~(o.width/2) + 'px',
+ transform: o.hwaccel ? 'translate3d(0,0,0)' : '',
+ opacity: o.opacity,
+ animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * o.direction, o.lines) + ' ' + 1/o.speed + 's linear infinite'
+ })
+
+ if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'}))
+ ins(el, ins(seg, fill(getColor(o.color, i), '0 0 1px rgba(0,0,0,.1)')))
+ }
+ return el
+ },
+
+ /**
+ * Internal method that adjusts the opacity of a single line.
+ * Will be overwritten in VML fallback mode below.
+ */
+ opacity: function(el, i, val) {
+ if (i < el.childNodes.length) el.childNodes[i].style.opacity = val
+ }
+
+ })
+
+
+ function initVML() {
+
+ /* Utility function to create a VML tag */
+ function vml(tag, attr) {
+ return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr)
+ }
+
+ // No CSS transforms but VML support, add a CSS rule for VML elements:
+ sheet.addRule('.spin-vml', 'behavior:url(#default#VML)')
+
+ Spinner.prototype.lines = function(el, o) {
+ var r = o.length+o.width
+ , s = 2*r
+
+ function grp() {
+ return css(
+ vml('group', {
+ coordsize: s + ' ' + s,
+ coordorigin: -r + ' ' + -r
+ }),
+ { width: s, height: s }
+ )
+ }
+
+ var margin = -(o.width+o.length)*2 + 'px'
+ , g = css(grp(), {position: 'absolute', top: margin, left: margin})
+ , i
+
+ function seg(i, dx, filter) {
+ ins(g,
+ ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}),
+ ins(css(vml('roundrect', {arcsize: o.corners}), {
+ width: r,
+ height: o.width,
+ left: o.radius,
+ top: -o.width>>1,
+ filter: filter
+ }),
+ vml('fill', {color: getColor(o.color, i), opacity: o.opacity}),
+ vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change
+ )
+ )
+ )
+ }
+
+ if (o.shadow)
+ for (i = 1; i <= o.lines; i++)
+ seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)')
+
+ for (i = 1; i <= o.lines; i++) seg(i)
+ return ins(el, g)
+ }
+
+ Spinner.prototype.opacity = function(el, i, val, o) {
+ var c = el.firstChild
+ o = o.shadow && o.lines || 0
+ if (c && i+o < c.childNodes.length) {
+ c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild
+ if (c) c.opacity = val
+ }
+ }
+ }
+
+ var probe = css(createEl('group'), {behavior: 'url(#default#VML)'})
+
+ if (!vendor(probe, 'transform') && probe.adj) initVML()
+ else useCssAnimations = vendor(probe, 'animation')
+
+ return Spinner
+
+}));