| // Copyright 2008 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview Image loader utility class. Useful when an application needs |
| * to preload multiple images, for example so they can be sized. |
| * |
| * @author attila@google.com (Attila Bodis) |
| */ |
| |
| goog.provide('goog.net.ImageLoader'); |
| |
| goog.require('goog.array'); |
| goog.require('goog.dom'); |
| goog.require('goog.events.EventHandler'); |
| goog.require('goog.events.EventTarget'); |
| goog.require('goog.events.EventType'); |
| goog.require('goog.net.EventType'); |
| goog.require('goog.object'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * Image loader utility class. Raises a {@link goog.events.EventType.LOAD} |
| * event for each image loaded, with an {@link Image} object as the target of |
| * the event, normalized to have {@code naturalHeight} and {@code naturalWidth} |
| * attributes. |
| * |
| * To use this class, run: |
| * |
| * <pre> |
| * var imageLoader = new goog.net.ImageLoader(); |
| * goog.events.listen(imageLoader, goog.net.EventType.COMPLETE, |
| * function(e) { ... }); |
| * imageLoader.addImage("image_id", "http://path/to/image.gif"); |
| * imageLoader.start(); |
| * </pre> |
| * |
| * The start() method must be called to start image loading. Images can be |
| * added and removed after loading has started, but only those images added |
| * before start() was called will be loaded until start() is called again. |
| * A goog.net.EventType.COMPLETE event will be dispatched only once all |
| * outstanding images have completed uploading. |
| * |
| * @param {Element=} opt_parent An optional parent element whose document object |
| * should be used to load images. |
| * @constructor |
| * @extends {goog.events.EventTarget} |
| * @final |
| */ |
| goog.net.ImageLoader = function(opt_parent) { |
| goog.events.EventTarget.call(this); |
| |
| /** |
| * Map of image IDs to their request including their image src, used to keep |
| * track of the images to load. Once images have started loading, they're |
| * removed from this map. |
| * @type {!Object<!goog.net.ImageLoader.ImageRequest_>} |
| * @private |
| */ |
| this.imageIdToRequestMap_ = {}; |
| |
| /** |
| * Map of image IDs to their image element, used only for images that are in |
| * the process of loading. Used to clean-up event listeners and to know |
| * when we've completed loading images. |
| * @type {!Object<string, !Element>} |
| * @private |
| */ |
| this.imageIdToImageMap_ = {}; |
| |
| /** |
| * Event handler object, used to keep track of onload and onreadystatechange |
| * listeners. |
| * @type {!goog.events.EventHandler<!goog.net.ImageLoader>} |
| * @private |
| */ |
| this.handler_ = new goog.events.EventHandler(this); |
| |
| /** |
| * The parent element whose document object will be used to load images. |
| * Useful if you want to load the images from a window other than the current |
| * window in order to control the Referer header sent when the image is |
| * loaded. |
| * @type {Element|undefined} |
| * @private |
| */ |
| this.parent_ = opt_parent; |
| }; |
| goog.inherits(goog.net.ImageLoader, goog.events.EventTarget); |
| |
| |
| /** |
| * The type of image request to dispatch, if this is a CORS-enabled image |
| * request. CORS-enabled images can be reused in canvas elements without them |
| * being tainted. The server hosting the image should include the appropriate |
| * CORS header. |
| * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image |
| * @enum {string} |
| */ |
| goog.net.ImageLoader.CorsRequestType = { |
| ANONYMOUS: 'anonymous', |
| USE_CREDENTIALS: 'use-credentials' |
| }; |
| |
| |
| /** |
| * Describes a request for an image. This includes its URL and its CORS-request |
| * type, if any. |
| * @typedef {{ |
| * src: string, |
| * corsRequestType: ?goog.net.ImageLoader.CorsRequestType |
| * }} |
| * @private |
| */ |
| goog.net.ImageLoader.ImageRequest_; |
| |
| |
| /** |
| * An array of event types to listen to on images. This is browser dependent. |
| * |
| * For IE 10 and below, Internet Explorer doesn't reliably raise LOAD events |
| * on images, so we must use READY_STATE_CHANGE. Since the image is cached |
| * locally, IE won't fire the LOAD event while the onreadystate event is fired |
| * always. On the other hand, the ERROR event is always fired whenever the image |
| * is not loaded successfully no matter whether it's cached or not. |
| * |
| * In IE 11, onreadystatechange is removed and replaced with onload: |
| * |
| * http://msdn.microsoft.com/en-us/library/ie/ms536957(v=vs.85).aspx |
| * http://msdn.microsoft.com/en-us/library/ie/bg182625(v=vs.85).aspx |
| * |
| * @type {!Array<string>} |
| * @private |
| */ |
| goog.net.ImageLoader.IMAGE_LOAD_EVENTS_ = [ |
| goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('11') ? |
| goog.net.EventType.READY_STATE_CHANGE : |
| goog.events.EventType.LOAD, |
| goog.net.EventType.ABORT, |
| goog.net.EventType.ERROR |
| ]; |
| |
| |
| /** |
| * Adds an image to the image loader, and associates it with the given ID |
| * string. If an image with that ID already exists, it is silently replaced. |
| * When the image in question is loaded, the target of the LOAD event will be |
| * an {@code Image} object with {@code id} and {@code src} attributes based on |
| * these arguments. |
| * @param {string} id The ID of the image to load. |
| * @param {string|Image} image Either the source URL of the image or the HTML |
| * image element itself (or any object with a {@code src} property, really). |
| * @param {!goog.net.ImageLoader.CorsRequestType=} opt_corsRequestType The type |
| * of CORS request to use, if any. |
| */ |
| goog.net.ImageLoader.prototype.addImage = function( |
| id, image, opt_corsRequestType) { |
| var src = goog.isString(image) ? image : image.src; |
| if (src) { |
| // For now, we just store the source URL for the image. |
| this.imageIdToRequestMap_[id] = { |
| src: src, |
| corsRequestType: goog.isDef(opt_corsRequestType) ? |
| opt_corsRequestType : null |
| }; |
| } |
| }; |
| |
| |
| /** |
| * Removes the image associated with the given ID string from the image loader. |
| * If the image was previously loading, removes any listeners for its events |
| * and dispatches a COMPLETE event if all remaining images have now completed. |
| * @param {string} id The ID of the image to remove. |
| */ |
| goog.net.ImageLoader.prototype.removeImage = function(id) { |
| delete this.imageIdToRequestMap_[id]; |
| |
| var image = this.imageIdToImageMap_[id]; |
| if (image) { |
| delete this.imageIdToImageMap_[id]; |
| |
| // Stop listening for events on the image. |
| this.handler_.unlisten(image, goog.net.ImageLoader.IMAGE_LOAD_EVENTS_, |
| this.onNetworkEvent_); |
| |
| // If this was the last image, raise a COMPLETE event. |
| if (goog.object.isEmpty(this.imageIdToImageMap_) && |
| goog.object.isEmpty(this.imageIdToRequestMap_)) { |
| this.dispatchEvent(goog.net.EventType.COMPLETE); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Starts loading all images in the image loader in parallel. Raises a LOAD |
| * event each time an image finishes loading, and a COMPLETE event after all |
| * images have finished loading. |
| */ |
| goog.net.ImageLoader.prototype.start = function() { |
| // Iterate over the keys, rather than the full object, to essentially clone |
| // the initial queued images in case any event handlers decide to add more |
| // images before this loop has finished executing. |
| var imageIdToRequestMap = this.imageIdToRequestMap_; |
| goog.array.forEach(goog.object.getKeys(imageIdToRequestMap), |
| function(id) { |
| var imageRequest = imageIdToRequestMap[id]; |
| if (imageRequest) { |
| delete imageIdToRequestMap[id]; |
| this.loadImage_(imageRequest, id); |
| } |
| }, this); |
| }; |
| |
| |
| /** |
| * Creates an {@code Image} object with the specified ID and source URL, and |
| * listens for network events raised as the image is loaded. |
| * @param {!goog.net.ImageLoader.ImageRequest_} imageRequest The request data. |
| * @param {string} id The unique ID of the image to load. |
| * @private |
| */ |
| goog.net.ImageLoader.prototype.loadImage_ = function(imageRequest, id) { |
| if (this.isDisposed()) { |
| // When loading an image in IE7 (and maybe IE8), the error handler |
| // may fire before we yield JS control. If the error handler |
| // dispose the ImageLoader, this method will throw exception. |
| return; |
| } |
| |
| var image; |
| if (this.parent_) { |
| var dom = goog.dom.getDomHelper(this.parent_); |
| image = dom.createDom('img'); |
| } else { |
| image = new Image(); |
| } |
| |
| if (imageRequest.corsRequestType) { |
| image.crossOrigin = imageRequest.corsRequestType; |
| } |
| |
| this.handler_.listen(image, goog.net.ImageLoader.IMAGE_LOAD_EVENTS_, |
| this.onNetworkEvent_); |
| this.imageIdToImageMap_[id] = image; |
| |
| image.id = id; |
| image.src = imageRequest.src; |
| }; |
| |
| |
| /** |
| * Handles net events (READY_STATE_CHANGE, LOAD, ABORT, and ERROR). |
| * @param {goog.events.Event} evt The network event to handle. |
| * @private |
| */ |
| goog.net.ImageLoader.prototype.onNetworkEvent_ = function(evt) { |
| var image = /** @type {Element} */ (evt.currentTarget); |
| |
| if (!image) { |
| return; |
| } |
| |
| if (evt.type == goog.net.EventType.READY_STATE_CHANGE) { |
| // This implies that the user agent is IE; see loadImage_(). |
| // Noe that this block is used to check whether the image is ready to |
| // dispatch the COMPLETE event. |
| if (image.readyState == goog.net.EventType.COMPLETE) { |
| // This is the IE equivalent of a LOAD event. |
| evt.type = goog.events.EventType.LOAD; |
| } else { |
| // This may imply that the load failed. |
| // Note that the image has only the following states: |
| // * uninitialized |
| // * loading |
| // * complete |
| // When the ERROR or the ABORT event is fired, the readyState |
| // will be either uninitialized or loading and we'd ignore those states |
| // since they will be handled separately (eg: evt.type = 'ERROR'). |
| |
| // Notes from MSDN : The states through which an object passes are |
| // determined by that object. An object can skip certain states |
| // (for example, interactive) if the state does not apply to that object. |
| // see http://msdn.microsoft.com/en-us/library/ms534359(VS.85).aspx |
| |
| // The image is not loaded, ignore. |
| return; |
| } |
| } |
| |
| // Add natural width/height properties for non-Gecko browsers. |
| if (typeof image.naturalWidth == 'undefined') { |
| if (evt.type == goog.events.EventType.LOAD) { |
| image.naturalWidth = image.width; |
| image.naturalHeight = image.height; |
| } else { |
| // This implies that the image fails to be loaded. |
| image.naturalWidth = 0; |
| image.naturalHeight = 0; |
| } |
| } |
| |
| // Redispatch the event on behalf of the image. Note that the external |
| // listener may dispose this instance. |
| this.dispatchEvent({type: evt.type, target: image}); |
| |
| if (this.isDisposed()) { |
| // If instance was disposed by listener, exit this function. |
| return; |
| } |
| |
| this.removeImage(image.id); |
| }; |
| |
| |
| /** @override */ |
| goog.net.ImageLoader.prototype.disposeInternal = function() { |
| delete this.imageIdToRequestMap_; |
| delete this.imageIdToImageMap_; |
| goog.dispose(this.handler_); |
| |
| goog.net.ImageLoader.superClass_.disposeInternal.call(this); |
| }; |