diff --git a/.gitignore b/.gitignore index 0ec1aa20..99c869f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules/ /dist/ /site/dist/ +/package-lock.json diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 43c97e71..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/css/ladda-themed.scss b/css/ladda-themed.scss index 050b999c..806548e7 100644 --- a/css/ladda-themed.scss +++ b/css/ladda-themed.scss @@ -34,7 +34,7 @@ $mint: #16a085; -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - &:hover { + &:hover:not([data-failed]):not([data-success]) { border-color: rgba( 0, 0, 0, 0.07 ); background-color: #888; } @@ -45,14 +45,13 @@ $mint: #16a085; @include buttonColor( 'purple', $purple ); @include buttonColor( 'mint', $mint ); - &[disabled], - &[data-loading] { + &[disabled], &[data-loading] { border-color: rgba( 0, 0, 0, 0.07 ); + cursor: default; + } - &, &:hover { - cursor: default; - background-color: #999; - } + &[disabled]:not([data-failed]):not([data-success]), &[data-loading] { + background-color: #999; } &[data-size=xs] { diff --git a/css/ladda.scss b/css/ladda.scss index 3622ebc0..ce18dd52 100644 --- a/css/ladda.scss +++ b/css/ladda.scss @@ -19,11 +19,11 @@ $spinnerSize: 32px !default; */ @mixin buttonColor( $name, $color ) { - &[data-color=#{$name}] { + &[data-color=#{$name}]:not([data-failed]):not([data-success]) { background: $color; - &:hover { - background-color: lighten( $color, 5% ); + &:hover:not([data-loading]):not([data-failed]):not([data-success]) { + background: lighten( $color, 5% ); } } } @@ -86,6 +86,15 @@ $spinnerSize: 32px !default; display: block; } +.ladda-button[data-failed] { + background: #dc3545; + border-color: #dc3545; /* necessary when using Bootstrap */ +} + +.ladda-button[data-success] { + background:#198754; + border-color: #198754; /* necessary when using Bootstrap */ +} /************************************* * EASING @@ -434,10 +443,12 @@ $spinnerSize: 32px !default; margin-left: 0; } - &[data-loading] { + &[data-loading], &[data-success], &[data-failed] { border-radius: 50%; width: 52px; + } + &[data-loading] { .ladda-label { opacity: 0; } diff --git a/js/ladda.d.ts b/js/ladda.d.ts index 079bdc24..492a3020 100644 --- a/js/ladda.d.ts +++ b/js/ladda.d.ts @@ -6,6 +6,8 @@ export interface LaddaButton { setProgress(progress: number): void, isLoading(): boolean, remove(): void, + succeed(timeout: number = 1250): LaddaButton, + fail(timeout: number = 1250): LaddaButton, } export interface BindOptions { diff --git a/js/ladda.js b/js/ladda.js index 022722bd..8269c2b0 100644 --- a/js/ladda.js +++ b/js/ladda.js @@ -12,11 +12,9 @@ import {Spinner} from 'spin.js'; 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. + * Creates a new instance of Ladda which wraps the target button element. + * @param {HTMLElement} button + * @return An API object that can be used to control the loading animation state. */ export function create(button) { if (typeof button === 'undefined') { @@ -34,139 +32,14 @@ export function create(button) { button.setAttribute('data-style', 'expand-right'); } - // The text contents must be wrapped in a ladda-label - // element, create one if it doesn't already exist - if (!button.querySelector('.ladda-label')) { - var laddaLabel = document.createElement('span'); - laddaLabel.className = 'ladda-label'; - wrapContent(button, laddaLabel); - } - - // The spinner component - var spinnerWrapper = button.querySelector('.ladda-spinner'); - - // Wrapper element for the spinner - if (!spinnerWrapper) { - spinnerWrapper = document.createElement('span'); - spinnerWrapper.className = 'ladda-spinner'; - } - - button.appendChild(spinnerWrapper); - - // Timer used to delay starting/stopping - var timer; - var spinner; - - var instance = { - /** - * Enter the loading state. - */ - start: function() { - // Create the spinner if it doesn't already exist - if (!spinner) { - spinner = createSpinner(button); - } - - button.disabled = true; - 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() { - if (instance.isLoading()) { - button.disabled = false; - 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() { - return this.isLoading() ? this.stop() : this.start(); - }, - - /** - * 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'; - } - }, - - isLoading: function() { - return button.hasAttribute('data-loading'); - }, - - remove: function() { - clearTimeout(timer); - button.disabled = false; - button.removeAttribute('data-loading'); - - if (spinner) { - spinner.stop(); - spinner = null; - } - - ALL_INSTANCES.splice(ALL_INSTANCES.indexOf(instance), 1); - } - }; - + var instance = new LaddaButton(button); ALL_INSTANCES.push(instance); return instance; } /** - * Binds the target buttons to automatically enter the - * loading state when clicked. + * Binds the target buttons to automatically enter the loading state when clicked. * * @param target Either an HTML element or a CSS selector. * @param options @@ -203,11 +76,10 @@ export function stopAll() { } /** -* Get the first ancestor node from an element, having a -* certain type. +* Get the first ancestor node with a given tag name from an element. * * @param elem An HTML element -* @param type an HTML tag type (uppercased) +* @param type an HTML tag type (uppercase) * * @return An HTML element */ @@ -304,7 +176,7 @@ function bindElement(element, options) { // Set a loading timeout if one is specified if (typeof options.timeout === 'number') { clearTimeout(timeout); - timeout = setTimeout(instance.stop, options.timeout); + timeout = setTimeout(function () { instance.stop(); }, options.timeout); } // Invoke callbacks @@ -315,3 +187,202 @@ function bindElement(element, options) { }, false); } + +/** + * LaddaButton class constructor + * @param {HTMLElement} button + */ +function LaddaButton(button) { + this._button = button; + // The text contents must be wrapped in a ladda-label + // element, create one if it doesn't already exist + this._laddaLabel = this._button.querySelector('.ladda-label'); + + if (!this._laddaLabel) { + this._laddaLabel = document.createElement('span'); + this._laddaLabel.className = 'ladda-label'; + wrapContent(this._button, this._laddaLabel); + } + + this._statusEl = document.createElement('span'); + this._statusEl.className = 'ladda-label'; + this._spinnerWrapper = this._button.querySelector('.ladda-spinner'); + + // Wrapper element for the spinner + if (!this._spinnerWrapper) { + this._spinnerWrapper = document.createElement('span'); + this._spinnerWrapper.className = 'ladda-spinner'; + } + + this._button.appendChild(this._spinnerWrapper); + + this._timer = null; // Timer used to delay starting/stopping the spinner + this._statusTimer = null; // Timer used to delay removing a success/failure status + this._spinner = null; + this._removing = false; +} + +/** + * Enter the loading state. + */ +LaddaButton.prototype.start = function () { + // Create the spinner if it doesn't already exist + if (!this._spinner) { + this._spinner = createSpinner(this._button); + } + + this._clearStatus(); + clearTimeout(this._timer); + clearTimeout(this._statusTimer); + + this._button.disabled = true; + this._button.setAttribute('data-loading', ''); + this._spinner.spin(this._spinnerWrapper); + + this.setProgress(0); + + return this; // chain +}; + +/** + * Enter the loading state after a delay. + * @param {number} delay + */ +LaddaButton.prototype.startAfter = function (delay) { + clearTimeout(this._timer); + this._timer = setTimeout(function () { this.start(); }.bind(this), delay); + + return this; // chain +}; + +LaddaButton.prototype._clearStatus = function () { + if (this._button.hasAttribute('data-success') || this._button.hasAttribute('data-failed')) { + this._button.disabled = false; + this._button.removeAttribute('data-success'); + this._button.removeAttribute('data-failed'); + this._button.removeChild(this._statusEl); + this._laddaLabel.style.display = 'block'; + } +}; + +/** + * Exit the loading, success, or failure state. + */ +LaddaButton.prototype.stop = function () { + if (this.isLoading()) { + this._button.disabled = false; + this._button.removeAttribute('data-loading'); + } else { + this._clearStatus(); + } + + clearTimeout(this._timer); + clearTimeout(this._statusTimer); + + if (this._spinner) { + if (this._removing) { + this._spinner.stop(); + this._spinner = null; + } else { + // Kill the animation after a delay to make sure it + // runs for the duration of the button transition + this._timer = setTimeout(function () { this._spinner.stop(); }.bind(this), 375); + } + } + + return this; // chain +}; + +/** + * Toggle the loading state on/off. + */ +LaddaButton.prototype.toggle = function () { + return this.isLoading() ? this.stop() : this.start(); +}; + +/** + * Sets the width of the visual progress bar inside of this Ladda button + * + * @param {number} progress in the range of 0-1 + */ +LaddaButton.prototype.setProgress = function (progress) { + // Cap it + progress = Math.max(Math.min(progress, 1), 0); + + var progressElement = this._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'; + this._button.appendChild(progressElement); + } + + progressElement.style.width = ((progress || 0) * this._button.offsetWidth) + 'px'; + } +}; + +LaddaButton.prototype.isLoading = function () { + return this._button.hasAttribute('data-loading'); +}; + +LaddaButton.prototype.remove = function () { + this._removing = true; + this.stop(); + ALL_INSTANCES.splice(ALL_INSTANCES.indexOf(this), 1); +}; + +/** + * Stops any progress and changes the button color to green with a check mark. + * + * @param {number} timeout The time in milliseconds before the button returns to its default state. + * If timeout is 0 the button will remain in a success state until the stop() method is called. + */ +LaddaButton.prototype.succeed = function (timeout) { + if (timeout === undefined) { + timeout = 1250; + } + + this.stop(); + this._button.disabled = true; + this._button.setAttribute('data-success', ''); + + this._statusEl.textContent = '✔'; + this._button.appendChild(this._statusEl); + this._laddaLabel.style.display = 'none'; + + if (timeout !== 0) { + this._statusTimer = setTimeout(function () { this.stop(); }.bind(this), timeout); + } + + return this; // chain +}; + +/** + * Stops any progress and changes the button color to red with a failure icon. + * + * @param {number} timeout The time in milliseconds before the button returns to its default state. + * If timeout is 0 the button will remain in a failure state until the stop() method is called. + */ +LaddaButton.prototype.fail = function (timeout) { + if (timeout === undefined) { + timeout = 1250; + } + + this.stop(); + this._button.disabled = true; + this._button.setAttribute('data-failed', ''); + + this._statusEl.textContent = '✘'; + this._button.appendChild(this._statusEl); + this._laddaLabel.style.display = 'none'; + + if (timeout !== 0) { + this._statusTimer = setTimeout(function () { this.stop(); }.bind(this), timeout); + } + + return this; // chain +}; diff --git a/site/index.js b/site/index.js index 8d2fb6d4..db0de75b 100644 --- a/site/index.js +++ b/site/index.js @@ -12,7 +12,10 @@ Ladda.bind('.progress-demo button', { instance.setProgress(progress); if (progress === 1) { - instance.stop(); + // instance.stop(); + + (Math.random() > 0.5) ? instance.succeed() : instance.fail(); + clearInterval(interval); } }, 200); @@ -25,6 +28,8 @@ Ladda.bind('.progress-demo button', { // var l = Ladda.create( document.querySelector( 'button' ) ); // l.start(); // l.stop(); +// l.succeed(number); +// l.fail(number); // l.toggle(); // l.isLoading(); // l.setProgress( 0-1 );