/*! * Ladda * http://lab.hakim.se/ladda * MIT licensed * * Copyright (C) 2018 Hakim El Hattab, http://hakim.se */ import {Spinner} from 'spin.js'; // 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. */ export function create(button) { if (typeof button === 'undefined') { console.warn("Ladda button target must be defined."); return; } // The button must have the class "ladda-button" if (!button.classList.contains('ladda-button')) { button.classList.add('ladda-button'); } // Style is required, default to "expand-right" if (!button.hasAttribute('data-style')) { 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); } }; ALL_INSTANCES.push(instance); return instance; } /** * 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. * - callback A function to be called with the Ladda * instance when a target button is clicked. */ export function bind(target, options) { var targets; if (typeof target === 'string') { targets = document.querySelectorAll(target); } else if (typeof target === 'object') { targets = [target]; } else { throw new Error('target must be string or object'); } options = options || {}; for (var i = 0; i < targets.length; i++) { bindElement(targets[i], options); } } /** * Stops ALL current loading animations. */ export function stopAll() { for (var i = 0, len = ALL_INSTANCES.length; i < len; i++) { ALL_INSTANCES[i].stop(); } } /** * 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; } function createSpinner(button) { var height = button.offsetHeight, spinnerColor, spinnerLines; 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'); } // Allow buttons to specify the number of lines of the spinner if (button.hasAttribute('data-spinner-lines')) { spinnerLines = parseInt(button.getAttribute('data-spinner-lines'), 10); } var radius = height * 0.2, length = radius * 0.6, width = radius < 7 ? 2 : 3; return new Spinner({ color: spinnerColor || '#fff', lines: spinnerLines || 12, radius: radius, length: length, width: width, animation: 'ladda-spinner-line-fade', zIndex: 'auto', top: 'auto', left: 'auto', className: '' }); } function wrapContent(node, wrapper) { var r = document.createRange(); r.selectNodeContents(node); r.surroundContents(wrapper); node.appendChild(wrapper); } function bindElement(element, options) { if (typeof element.addEventListener !== 'function') { return; } var instance = create(element); var timeout = -1; element.addEventListener('click', function() { // 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' && !form.hasAttribute('novalidate')) { // Modern form validation if (typeof form.checkValidity === 'function') { valid = form.checkValidity(); } } if (valid) { // This is asynchronous to avoid an issue where disabling // 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); }