loading-bar.js
301 lines
| 1 | /* |
| 2 | * angular-loading-bar |
| 3 | * |
| 4 | * intercepts XHR requests and creates a loading bar. |
| 5 | * Based on the excellent nprogress work by rstacruz (more info in readme) |
| 6 | * |
| 7 | * (c) 2013 Wes Cruver |
| 8 | * License: MIT |
| 9 | */ |
| 10 | |
| 11 | |
| 12 | (function() { |
| 13 | |
| 14 | 'use strict'; |
| 15 | |
| 16 | // Alias the loading bar for various backwards compatibilities since the project has matured: |
| 17 | angular.module('angular-loading-bar', ['cfp.loadingBarInterceptor']); |
| 18 | angular.module('chieffancypants.loadingBar', ['cfp.loadingBarInterceptor']); |
| 19 | |
| 20 | |
| 21 | /** |
| 22 | * loadingBarInterceptor service |
| 23 | * |
| 24 | * Registers itself as an Angular interceptor and listens for XHR requests. |
| 25 | */ |
| 26 | angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar']) |
| 27 | .config(['$httpProvider', function ($httpProvider) { |
| 28 | |
| 29 | var interceptor = ['$q', '$cacheFactory', '$timeout', '$rootScope', '$log', 'cfpLoadingBar', function ($q, $cacheFactory, $timeout, $rootScope, $log, cfpLoadingBar) { |
| 30 | |
| 31 | /** |
| 32 | * The total number of requests made |
| 33 | */ |
| 34 | var reqsTotal = 0; |
| 35 | |
| 36 | /** |
| 37 | * The number of requests completed (either successfully or not) |
| 38 | */ |
| 39 | var reqsCompleted = 0; |
| 40 | |
| 41 | /** |
| 42 | * The amount of time spent fetching before showing the loading bar |
| 43 | */ |
| 44 | var latencyThreshold = cfpLoadingBar.latencyThreshold; |
| 45 | |
| 46 | /** |
| 47 | * $timeout handle for latencyThreshold |
| 48 | */ |
| 49 | var startTimeout; |
| 50 | |
| 51 | |
| 52 | /** |
| 53 | * calls cfpLoadingBar.complete() which removes the |
| 54 | * loading bar from the DOM. |
| 55 | */ |
| 56 | function setComplete() { |
| 57 | $timeout.cancel(startTimeout); |
| 58 | cfpLoadingBar.complete(); |
| 59 | reqsCompleted = 0; |
| 60 | reqsTotal = 0; |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * Determine if the response has already been cached |
| 65 | * @param {Object} config the config option from the request |
| 66 | * @return {Boolean} retrns true if cached, otherwise false |
| 67 | */ |
| 68 | function isCached(config) { |
| 69 | var cache; |
| 70 | var defaultCache = $cacheFactory.get('$http'); |
| 71 | var defaults = $httpProvider.defaults; |
| 72 | |
| 73 | // Choose the proper cache source. Borrowed from angular: $http service |
| 74 | if ((config.cache || defaults.cache) && config.cache !== false && |
| 75 | (config.method === 'GET' || config.method === 'JSONP')) { |
| 76 | cache = angular.isObject(config.cache) ? config.cache |
| 77 | : angular.isObject(defaults.cache) ? defaults.cache |
| 78 | : defaultCache; |
| 79 | } |
| 80 | |
| 81 | var cached = cache !== undefined ? |
| 82 | cache.get(config.url) !== undefined : false; |
| 83 | |
| 84 | if (config.cached !== undefined && cached !== config.cached) { |
| 85 | return config.cached; |
| 86 | } |
| 87 | config.cached = cached; |
| 88 | return cached; |
| 89 | } |
| 90 | |
| 91 | |
| 92 | return { |
| 93 | 'request': function(config) { |
| 94 | return config; |
| 95 | }, |
| 96 | |
| 97 | 'response': function(response) { |
| 98 | if (!response || !response.config) { |
| 99 | $log.error('Broken interceptor detected: Config object not supplied in response:\n https://github.com/chieffancypants/angular-loading-bar/pull/50'); |
| 100 | } |
| 101 | return response; |
| 102 | }, |
| 103 | |
| 104 | 'responseError': function(rejection) { |
| 105 | if (!rejection || !rejection.config) { |
| 106 | $log.error('Broken interceptor detected: Config object not supplied in rejection:\n https://github.com/chieffancypants/angular-loading-bar/pull/50'); |
| 107 | } |
| 108 | return $q.reject(rejection); |
| 109 | } |
| 110 | }; |
| 111 | }]; |
| 112 | |
| 113 | $httpProvider.interceptors.push(interceptor); |
| 114 | }]); |
| 115 | |
| 116 | |
| 117 | /** |
| 118 | * Loading Bar |
| 119 | * |
| 120 | * This service handles adding and removing the actual element in the DOM. |
| 121 | * Generally, best practices for DOM manipulation is to take place in a |
| 122 | * directive, but because the element itself is injected in the DOM only upon |
| 123 | * XHR requests, and it's likely needed on every view, the best option is to |
| 124 | * use a service. |
| 125 | */ |
| 126 | angular.module('cfp.loadingBar', []) |
| 127 | .provider('cfpLoadingBar', function() { |
| 128 | |
| 129 | this.autoIncrement = true; |
| 130 | this.includeSpinner = true; |
| 131 | this.includeBar = true; |
| 132 | this.latencyThreshold = 100; |
| 133 | this.startSize = 0.02; |
| 134 | this.parentSelector = 'body'; |
| 135 | this.spinnerTemplate = '<div id="loading-bar-spinner"><div class="spinner-icon"></div></div>'; |
| 136 | this.loadingBarTemplate = '<div id="loading-bar"><div class="bar"><div class="peg"></div></div></div>'; |
| 137 | |
| 138 | this.$get = ['$injector', '$document', '$timeout', '$rootScope', function ($injector, $document, $timeout, $rootScope) { |
| 139 | var $animate; |
| 140 | var $parentSelector = this.parentSelector, |
| 141 | loadingBarContainer = angular.element(this.loadingBarTemplate), |
| 142 | loadingBar = loadingBarContainer.find('div').eq(0), |
| 143 | spinner = angular.element(this.spinnerTemplate); |
| 144 | |
| 145 | var incTimeout, |
| 146 | completeTimeout, |
| 147 | started = false, |
| 148 | status = 0; |
| 149 | |
| 150 | var autoIncrement = this.autoIncrement; |
| 151 | var includeSpinner = this.includeSpinner; |
| 152 | var includeBar = this.includeBar; |
| 153 | var startSize = this.startSize; |
| 154 | |
| 155 | /** |
| 156 | * Inserts the loading bar element into the dom, and sets it to 2% |
| 157 | */ |
| 158 | function _start() { |
| 159 | if (!$animate) { |
| 160 | $animate = $injector.get('$animate'); |
| 161 | } |
| 162 | |
| 163 | $timeout.cancel(completeTimeout); |
| 164 | |
| 165 | // do not continually broadcast the started event: |
| 166 | if (started) { |
| 167 | return; |
| 168 | } |
| 169 | |
| 170 | var document = $document[0]; |
| 171 | var parent = document.querySelector ? |
| 172 | document.querySelector($parentSelector) |
| 173 | : $document.find($parentSelector)[0] |
| 174 | ; |
| 175 | |
| 176 | if (! parent) { |
| 177 | parent = document.getElementsByTagName('body')[0]; |
| 178 | } |
| 179 | |
| 180 | var $parent = angular.element(parent); |
| 181 | var $after = parent.lastChild && angular.element(parent.lastChild); |
| 182 | |
| 183 | $rootScope.$broadcast('cfpLoadingBar:started'); |
| 184 | started = true; |
| 185 | |
| 186 | if (includeBar) { |
| 187 | $animate.enter(loadingBarContainer, $parent, $after); |
| 188 | } |
| 189 | |
| 190 | if (includeSpinner) { |
| 191 | $animate.enter(spinner, $parent, loadingBarContainer); |
| 192 | } |
| 193 | |
| 194 | _set(startSize); |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | * Set the loading bar's width to a certain percent. |
| 199 | * |
| 200 | * @param n any value between 0 and 1 |
| 201 | */ |
| 202 | function _set(n) { |
| 203 | if (!started) { |
| 204 | return; |
| 205 | } |
| 206 | var pct = (n * 100) + '%'; |
| 207 | loadingBar.css('width', pct); |
| 208 | status = n; |
| 209 | |
| 210 | // increment loadingbar to give the illusion that there is always |
| 211 | // progress but make sure to cancel the previous timeouts so we don't |
| 212 | // have multiple incs running at the same time. |
| 213 | if (autoIncrement) { |
| 214 | $timeout.cancel(incTimeout); |
| 215 | incTimeout = $timeout(function() { |
| 216 | _inc(); |
| 217 | }, 250); |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | /** |
| 222 | * Increments the loading bar by a random amount |
| 223 | * but slows down as it progresses |
| 224 | */ |
| 225 | function _inc() { |
| 226 | if (_status() >= 1) { |
| 227 | return; |
| 228 | } |
| 229 | |
| 230 | var rnd = 0; |
| 231 | |
| 232 | // TODO: do this mathmatically instead of through conditions |
| 233 | |
| 234 | var stat = _status(); |
| 235 | if (stat >= 0 && stat < 0.25) { |
| 236 | // Start out between 3 - 6% increments |
| 237 | rnd = (Math.random() * (5 - 3 + 1) + 3) / 100; |
| 238 | } else if (stat >= 0.25 && stat < 0.65) { |
| 239 | // increment between 0 - 3% |
| 240 | rnd = (Math.random() * 3) / 100; |
| 241 | } else if (stat >= 0.65 && stat < 0.9) { |
| 242 | // increment between 0 - 2% |
| 243 | rnd = (Math.random() * 2) / 100; |
| 244 | } else if (stat >= 0.9 && stat < 0.99) { |
| 245 | // finally, increment it .5 % |
| 246 | rnd = 0.005; |
| 247 | } else { |
| 248 | // after 99%, don't increment: |
| 249 | rnd = 0; |
| 250 | } |
| 251 | |
| 252 | var pct = _status() + rnd; |
| 253 | _set(pct); |
| 254 | } |
| 255 | |
| 256 | function _status() { |
| 257 | return status; |
| 258 | } |
| 259 | |
| 260 | function _completeAnimation() { |
| 261 | status = 0; |
| 262 | started = false; |
| 263 | } |
| 264 | |
| 265 | function _complete() { |
| 266 | if (!$animate) { |
| 267 | $animate = $injector.get('$animate'); |
| 268 | } |
| 269 | |
| 270 | _set(1); |
| 271 | $timeout.cancel(completeTimeout); |
| 272 | |
| 273 | // Attempt to aggregate any start/complete calls within 500ms: |
| 274 | completeTimeout = $timeout(function() { |
| 275 | var promise = $animate.leave(loadingBarContainer, _completeAnimation); |
| 276 | if (promise && promise.then) { |
| 277 | promise.then(_completeAnimation); |
| 278 | } |
| 279 | $animate.leave(spinner); |
| 280 | $rootScope.$broadcast('cfpLoadingBar:completed'); |
| 281 | }, 500); |
| 282 | } |
| 283 | |
| 284 | return { |
| 285 | start : _start, |
| 286 | set : _set, |
| 287 | status : _status, |
| 288 | inc : _inc, |
| 289 | complete : _complete, |
| 290 | autoIncrement : this.autoIncrement, |
| 291 | includeSpinner : this.includeSpinner, |
| 292 | latencyThreshold : this.latencyThreshold, |
| 293 | parentSelector : this.parentSelector, |
| 294 | startSize : this.startSize |
| 295 | }; |
| 296 | |
| 297 | |
| 298 | }]; // |
| 299 | }); // wtf javascript. srsly |
| 300 | })(); // |
| 301 |