2 * L.Control.Loading is a control that shows a loading indicator when tiles are
3 * loading or when map-related AJAX requests are taking place.
8 var console = window.console || {
13 function defineLeafletLoading(L) {
14 L.Control.Loading = L.Control.extend({
31 initialize: function(options) {
32 L.setOptions(this, options);
33 this._dataLoaders = {};
35 // Try to set the zoom control this control is attached to from
37 if (this.options.zoomControl !== null) {
38 this.zoomControl = this.options.zoomControl;
42 onAdd: function(map) {
43 if (this.options.spinjs && (typeof Spinner !== 'function')) {
44 return console.error("Leaflet.loading cannot load because you didn't load spin.js (http://fgnass.github.io/spin.js/), even though you set it in options.");
46 this._addLayerListeners(map);
47 this._addMapListeners(map);
49 // Try to set the zoom control this control is attached to from the map
50 // the control is being added to
51 if (!this.options.separate && !this.zoomControl) {
52 if (map.zoomControl) {
53 this.zoomControl = map.zoomControl;
54 } else if (map.zoomsliderControl) {
55 this.zoomControl = map.zoomsliderControl;
59 // Create the loading indicator
60 var classes = 'leaflet-control-loading';
62 if (this.zoomControl && !this.options.separate) {
63 // If there is a zoom control, hook into the bottom of it
64 container = this.zoomControl._container;
65 // These classes are no longer used as of Leaflet 0.6
66 classes += ' leaflet-bar-part-bottom leaflet-bar-part last';
68 // Loading control will be added to the zoom control. So the visible last element is not the
69 // last dom element anymore. So add the part-bottom class.
70 L.DomUtil.addClass(this._getLastControlButton(), 'leaflet-bar-part-bottom');
73 // Otherwise, create a container for the indicator
74 container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-control-layer-container leaflet-bar');
76 this._indicatorContainer = container;
77 this._indicator = L.DomUtil.create('a', classes, container);
78 if (this.options.spinjs) {
79 this._spinner = new Spinner(this.options.spin).spin();
80 this._indicator.appendChild(this._spinner.el);
85 onRemove: function(map) {
86 this._removeLayerListeners(map);
87 this._removeMapListeners(map);
90 removeFrom: function (map) {
91 if (this.zoomControl && !this.options.separate) {
92 // Override Control.removeFrom() to avoid clobbering the entire
93 // _container, which is the same as zoomControl's
94 this._container.removeChild(this._indicator);
100 // If this control is separate from the zoomControl, call the
101 // parent method so we don't leave behind an empty container
102 return L.Control.prototype.removeFrom.call(this, map);
106 addLoader: function(id) {
107 this._dataLoaders[id] = true;
108 if (this.options.delayIndicator && !this.delayIndicatorTimeout) {
109 // If we are delaying showing the indicator and we're not
110 // already waiting for that delay, set up a timeout.
112 this.delayIndicatorTimeout = setTimeout(function () {
113 that.updateIndicator();
114 that.delayIndicatorTimeout = null;
115 }, this.options.delayIndicator);
118 // Otherwise show the indicator immediately
119 this.updateIndicator();
123 removeLoader: function(id) {
124 delete this._dataLoaders[id];
125 this.updateIndicator();
127 // If removing this loader means we're in no danger of loading,
128 // clear the timeout. This prevents old delays from instantly
129 // triggering the indicator.
130 if (this.options.delayIndicator && this.delayIndicatorTimeout && !this.isLoading()) {
131 clearTimeout(this.delayIndicatorTimeout);
132 this.delayIndicatorTimeout = null;
136 updateIndicator: function() {
137 if (this.isLoading()) {
138 this._showIndicator();
141 this._hideIndicator();
145 isLoading: function() {
146 return this._countLoaders() > 0;
149 _countLoaders: function() {
151 for (key in this._dataLoaders) {
152 if (this._dataLoaders.hasOwnProperty(key)) size++;
157 _showIndicator: function() {
158 // Show loading indicator
159 L.DomUtil.addClass(this._indicator, 'is-loading');
160 L.DomUtil.addClass(this._indicatorContainer, 'is-loading');
162 // If zoomControl exists, make the zoom-out button not last
163 if (!this.options.separate) {
164 if (this.zoomControl instanceof L.Control.Zoom) {
165 L.DomUtil.removeClass(this._getLastControlButton(), 'leaflet-bar-part-bottom');
167 else if (typeof L.Control.Zoomslider === 'function' && this.zoomControl instanceof L.Control.Zoomslider) {
168 L.DomUtil.removeClass(this.zoomControl._ui.zoomOut, 'leaflet-bar-part-bottom');
173 _hideIndicator: function() {
174 // Hide loading indicator
175 L.DomUtil.removeClass(this._indicator, 'is-loading');
176 L.DomUtil.removeClass(this._indicatorContainer, 'is-loading');
178 // If zoomControl exists, make the zoom-out button last
179 if (!this.options.separate) {
180 if (this.zoomControl instanceof L.Control.Zoom) {
181 L.DomUtil.addClass(this._getLastControlButton(), 'leaflet-bar-part-bottom');
183 else if (typeof L.Control.Zoomslider === 'function' && this.zoomControl instanceof L.Control.Zoomslider) {
184 L.DomUtil.addClass(this.zoomControl._ui.zoomOut, 'leaflet-bar-part-bottom');
189 _getLastControlButton: function() {
190 var container = this.zoomControl._container,
191 index = container.children.length - 1;
193 // Find the last visible control button that is not our loading
196 var button = container.children[index];
197 if (!(this._indicator === button || button.offsetWidth === 0 || button.offsetHeight === 0)) {
203 return container.children[index];
206 _handleLoading: function(e) {
207 this.addLoader(this.getEventId(e));
210 _handleBaseLayerChange: function (e) {
213 // Check for a target 'layer' that contains multiple layers, such as
214 // L.LayerGroup. This will happen if you have an L.LayerGroup in an
216 if (e.layer && e.layer.eachLayer && typeof e.layer.eachLayer === 'function') {
217 e.layer.eachLayer(function (layer) {
218 that._handleBaseLayerChange({ layer: layer });
222 // If we're changing to a canvas layer, don't handle loading
223 // as canvas layers will not fire load events.
224 if (!(L.TileLayer.Canvas && e.layer instanceof L.TileLayer.Canvas)) {
225 that._handleLoading(e);
230 _handleLoad: function(e) {
231 this.removeLoader(this.getEventId(e));
234 getEventId: function(e) {
239 return e.layer._leaflet_id;
241 return e.target._leaflet_id;
244 _layerAdd: function(e) {
245 if (!e.layer || !e.layer.on) return
248 loading: this._handleLoading,
249 load: this._handleLoad
253 console.warn('L.Control.Loading: Tried and failed to add ' +
254 ' event handlers to layer', e.layer);
255 console.warn('L.Control.Loading: Full details', exception);
259 _layerRemove: function(e) {
260 if (!e.layer || !e.layer.off) return;
263 loading: this._handleLoading,
264 load: this._handleLoad
268 console.warn('L.Control.Loading: Tried and failed to remove ' +
269 'event handlers from layer', e.layer);
270 console.warn('L.Control.Loading: Full details', exception);
274 _addLayerListeners: function(map) {
275 // Add listeners for begin and end of load to any layers already
277 map.eachLayer(function(layer) {
278 if (!layer.on) return;
280 loading: this._handleLoading,
281 load: this._handleLoad
285 // When a layer is added to the map, add listeners for begin and
287 map.on('layeradd', this._layerAdd, this);
288 map.on('layerremove', this._layerRemove, this);
291 _removeLayerListeners: function(map) {
292 // Remove listeners for begin and end of load from all layers
293 map.eachLayer(function(layer) {
294 if (!layer.off) return;
296 loading: this._handleLoading,
297 load: this._handleLoad
301 // Remove layeradd/layerremove listener from map
302 map.off('layeradd', this._layerAdd, this);
303 map.off('layerremove', this._layerRemove, this);
306 _addMapListeners: function(map) {
307 // Add listeners to the map for (custom) dataloading and dataload
308 // events, eg, for AJAX calls that affect the map but will not be
309 // reflected in the above layer events.
311 baselayerchange: this._handleBaseLayerChange,
312 dataloading: this._handleLoading,
313 dataload: this._handleLoad,
314 layerremove: this._handleLoad
318 _removeMapListeners: function(map) {
320 baselayerchange: this._handleBaseLayerChange,
321 dataloading: this._handleLoading,
322 dataload: this._handleLoad,
323 layerremove: this._handleLoad
328 L.Map.addInitHook(function () {
329 if (this.options.loadingControl) {
330 this.loadingControl = new L.Control.Loading();
331 this.addControl(this.loadingControl);
335 L.Control.loading = function(options) {
336 return new L.Control.Loading(options);
340 if (typeof define === 'function' && define.amd) {
341 // Try to add leaflet.loading to Leaflet using AMD
342 define(['leaflet'], function (L) {
343 defineLeafletLoading(L);
347 // Else use the global L
348 defineLeafletLoading(L);