338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
// 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);
|
|
};
|