/* * angular-loading-bar * * intercepts XHR requests and creates a loading bar. * Based on the excellent nprogress work by rstacruz (more info in readme) * * (c) 2013 Wes Cruver * License: MIT */ (function () { 'use strict'; // Alias the loading bar for various backwards compatibilities since the project has matured: angular.module('angular-loading-bar', ['cfp.loadingBarInterceptor']); angular.module('chieffancypants.loadingBar', ['cfp.loadingBarInterceptor']); /** * loadingBarInterceptor service * * Registers itself as an Angular interceptor and listens for XHR requests. */ angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar']) .config(['$httpProvider', function ($httpProvider) { var interceptor = ['$q', '$cacheFactory', '$timeout', '$rootScope', '$log', 'cfpLoadingBar', function ($q, $cacheFactory, $timeout, $rootScope, $log, cfpLoadingBar) { /** * The total number of requests made */ var reqsTotal = 0; /** * The number of requests completed (either successfully or not) */ var reqsCompleted = 0; /** * The amount of time spent fetching before showing the loading bar */ var latencyThreshold = cfpLoadingBar.latencyThreshold; /** * $timeout handle for latencyThreshold */ var startTimeout; /** * calls cfpLoadingBar.complete() which removes the * loading bar from the DOM. */ function setComplete() { $timeout.cancel(startTimeout); cfpLoadingBar.complete(); reqsCompleted = 0; reqsTotal = 0; } /** * Determine if the response has already been cached * @param {Object} config the config option from the request * @return {Boolean} retrns true if cached, otherwise false */ function isCached(config) { var cache; var defaultCache = $cacheFactory.get('$http'); var defaults = $httpProvider.defaults; // Choose the proper cache source. Borrowed from angular: $http service if ((config.cache || defaults.cache) && config.cache !== false && (config.method === 'GET' || config.method === 'JSONP')) { cache = angular.isObject(config.cache) ? config.cache : angular.isObject(defaults.cache) ? defaults.cache : defaultCache; } var cached = cache !== undefined ? cache.get(config.url) !== undefined : false; if (config.cached !== undefined && cached !== config.cached) { return config.cached; } config.cached = cached; return cached; } return { 'request': function (config) { // Check to make sure this request hasn't already been cached and that // the requester didn't explicitly ask us to ignore this request: if (!config.ignoreLoadingBar && !isCached(config)) { $rootScope.$broadcast('cfpLoadingBar:loading', { url: config.url }); if (reqsTotal === 0) { startTimeout = $timeout(function () { cfpLoadingBar.start(); }, latencyThreshold); } reqsTotal++; cfpLoadingBar.set(reqsCompleted / reqsTotal); } return config; }, 'response': function (response) { if (!response || !response.config) { $log.error('Broken interceptor detected: Config object not supplied in response:\n https://github.com/chieffancypants/angular-loading-bar/pull/50'); return response; } if (!response.config.ignoreLoadingBar && !isCached(response.config)) { reqsCompleted++; $rootScope.$broadcast('cfpLoadingBar:loaded', { url: response.config.url, result: response }); if (reqsCompleted >= reqsTotal) { setComplete(); } else { cfpLoadingBar.set(reqsCompleted / reqsTotal); } } return response; }, 'responseError': function (rejection) { if (!rejection || !rejection.config) { $log.error('Broken interceptor detected: Config object not supplied in rejection:\n https://github.com/chieffancypants/angular-loading-bar/pull/50'); return $q.reject(rejection); } if (!rejection.config.ignoreLoadingBar && !isCached(rejection.config)) { reqsCompleted++; $rootScope.$broadcast('cfpLoadingBar:loaded', { url: rejection.config.url, result: rejection }); if (reqsCompleted >= reqsTotal) { setComplete(); } else { cfpLoadingBar.set(reqsCompleted / reqsTotal); } } return $q.reject(rejection); } }; }]; $httpProvider.interceptors.push(interceptor); }]); /** * Loading Bar * * This service handles adding and removing the actual element in the DOM. * Generally, best practices for DOM manipulation is to take place in a * directive, but because the element itself is injected in the DOM only upon * XHR requests, and it's likely needed on every view, the best option is to * use a service. */ angular.module('cfp.loadingBar', []) .provider('cfpLoadingBar', function () { this.includeSpinner = false; this.includeBar = true; this.latencyThreshold = 150; this.startSize = 0.02; this.parentSelector = '#view'; this.spinnerTemplate = '
'; this.loadingBarTemplate = '
'; this.$get = ['$injector', '$document', '$timeout', '$rootScope', function ($injector, $document, $timeout, $rootScope) { var $animate; var $parentSelector = this.parentSelector, loadingBarContainer = angular.element(this.loadingBarTemplate), loadingBar = loadingBarContainer.find('div').eq(0), spinner = angular.element(this.spinnerTemplate); var incTimeout, completeTimeout, started = false, status = 0; var includeSpinner = this.includeSpinner; var includeBar = this.includeBar; var startSize = this.startSize; /** * Inserts the loading bar element into the dom, and sets it to 2% */ function _start() { if (!$animate) { $animate = $injector.get('$animate'); } var $parent = $document.find($parentSelector).eq(0); $timeout.cancel(completeTimeout); // do not continually broadcast the started event: if (started) { return; } $rootScope.$broadcast('cfpLoadingBar:started'); started = true; if (includeBar) { $animate.enter(loadingBarContainer, $parent); } if (includeSpinner) { $animate.enter(spinner, $parent); } _set(startSize); } /** * Set the loading bar's width to a certain percent. * * @param n any value between 0 and 1 */ function _set(n) { if (!started) { return; } var pct = (n * 100) + '%'; loadingBar.css('width', pct); status = n; // increment loadingbar to give the illusion that there is always // progress but make sure to cancel the previous timeouts so we don't // have multiple incs running at the same time. $timeout.cancel(incTimeout); incTimeout = $timeout(function () { _inc(); }, 250); } /** * Increments the loading bar by a random amount * but slows down as it progresses */ function _inc() { if (_status() >= 1) { return; } var rnd = 0; // TODO: do this mathmatically instead of through conditions var stat = _status(); if (stat >= 0 && stat < 0.25) { // Start out between 3 - 6% increments rnd = (Math.random() * (5 - 3 + 1) + 3) / 100; } else if (stat >= 0.25 && stat < 0.65) { // increment between 0 - 3% rnd = (Math.random() * 3) / 100; } else if (stat >= 0.65 && stat < 0.9) { // increment between 0 - 2% rnd = (Math.random() * 2) / 100; } else if (stat >= 0.9 && stat < 0.99) { // finally, increment it .5 % rnd = 0.005; } else { // after 99%, don't increment: rnd = 0; } var pct = _status() + rnd; _set(pct); } function _status() { return status; } function _completeAnimation() { status = 0; started = false; } function _complete() { if (!$animate) { $animate = $injector.get('$animate'); } $rootScope.$broadcast('cfpLoadingBar:completed'); _set(1); $timeout.cancel(completeTimeout); // Attempt to aggregate any start/complete calls within 500ms: completeTimeout = $timeout(function () { var promise = $animate.leave(loadingBarContainer, _completeAnimation); if (promise && promise.then) { promise.then(_completeAnimation); } $animate.leave(spinner); }, 500); } return { start: _start, set: _set, status: _status, inc: _inc, complete: _complete, includeSpinner: this.includeSpinner, latencyThreshold: this.latencyThreshold, parentSelector: this.parentSelector, startSize: this.startSize }; }]; // }); // wtf javascript. srsly })(); //