imageloader.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Image loader utility class. Useful when an application needs
  16. * to preload multiple images, for example so they can be sized.
  17. *
  18. * @author attila@google.com (Attila Bodis)
  19. */
  20. goog.provide('goog.net.ImageLoader');
  21. goog.require('goog.array');
  22. goog.require('goog.dom');
  23. goog.require('goog.dom.TagName');
  24. goog.require('goog.events.EventHandler');
  25. goog.require('goog.events.EventTarget');
  26. goog.require('goog.events.EventType');
  27. goog.require('goog.net.EventType');
  28. goog.require('goog.object');
  29. goog.require('goog.userAgent');
  30. /**
  31. * Image loader utility class. Raises a {@link goog.events.EventType.LOAD}
  32. * event for each image loaded, with an {@link Image} object as the target of
  33. * the event, normalized to have {@code naturalHeight} and {@code naturalWidth}
  34. * attributes.
  35. *
  36. * To use this class, run:
  37. *
  38. * <pre>
  39. * var imageLoader = new goog.net.ImageLoader();
  40. * goog.events.listen(imageLoader, goog.net.EventType.COMPLETE,
  41. * function(e) { ... });
  42. * imageLoader.addImage("image_id", "http://path/to/image.gif");
  43. * imageLoader.start();
  44. * </pre>
  45. *
  46. * The start() method must be called to start image loading. Images can be
  47. * added and removed after loading has started, but only those images added
  48. * before start() was called will be loaded until start() is called again.
  49. * A goog.net.EventType.COMPLETE event will be dispatched only once all
  50. * outstanding images have completed uploading.
  51. *
  52. * @param {Element=} opt_parent An optional parent element whose document object
  53. * should be used to load images.
  54. * @constructor
  55. * @extends {goog.events.EventTarget}
  56. * @final
  57. */
  58. goog.net.ImageLoader = function(opt_parent) {
  59. goog.events.EventTarget.call(this);
  60. /**
  61. * Map of image IDs to their request including their image src, used to keep
  62. * track of the images to load. Once images have started loading, they're
  63. * removed from this map.
  64. * @type {!Object<!goog.net.ImageLoader.ImageRequest_>}
  65. * @private
  66. */
  67. this.imageIdToRequestMap_ = {};
  68. /**
  69. * Map of image IDs to their image element, used only for images that are in
  70. * the process of loading. Used to clean-up event listeners and to know
  71. * when we've completed loading images.
  72. * @type {!Object<string, !Element>}
  73. * @private
  74. */
  75. this.imageIdToImageMap_ = {};
  76. /**
  77. * Event handler object, used to keep track of onload and onreadystatechange
  78. * listeners.
  79. * @type {!goog.events.EventHandler<!goog.net.ImageLoader>}
  80. * @private
  81. */
  82. this.handler_ = new goog.events.EventHandler(this);
  83. /**
  84. * The parent element whose document object will be used to load images.
  85. * Useful if you want to load the images from a window other than the current
  86. * window in order to control the Referer header sent when the image is
  87. * loaded.
  88. * @type {Element|undefined}
  89. * @private
  90. */
  91. this.parent_ = opt_parent;
  92. };
  93. goog.inherits(goog.net.ImageLoader, goog.events.EventTarget);
  94. /**
  95. * The type of image request to dispatch, if this is a CORS-enabled image
  96. * request. CORS-enabled images can be reused in canvas elements without them
  97. * being tainted. The server hosting the image should include the appropriate
  98. * CORS header.
  99. * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image
  100. * @enum {string}
  101. */
  102. goog.net.ImageLoader.CorsRequestType = {
  103. ANONYMOUS: 'anonymous',
  104. USE_CREDENTIALS: 'use-credentials'
  105. };
  106. /**
  107. * Describes a request for an image. This includes its URL and its CORS-request
  108. * type, if any.
  109. * @typedef {{
  110. * src: string,
  111. * corsRequestType: ?goog.net.ImageLoader.CorsRequestType
  112. * }}
  113. * @private
  114. */
  115. goog.net.ImageLoader.ImageRequest_;
  116. /**
  117. * An array of event types to listen to on images. This is browser dependent.
  118. *
  119. * For IE 10 and below, Internet Explorer doesn't reliably raise LOAD events
  120. * on images, so we must use READY_STATE_CHANGE. Since the image is cached
  121. * locally, IE won't fire the LOAD event while the onreadystate event is fired
  122. * always. On the other hand, the ERROR event is always fired whenever the image
  123. * is not loaded successfully no matter whether it's cached or not.
  124. *
  125. * In IE 11, onreadystatechange is removed and replaced with onload:
  126. *
  127. * http://msdn.microsoft.com/en-us/library/ie/ms536957(v=vs.85).aspx
  128. * http://msdn.microsoft.com/en-us/library/ie/bg182625(v=vs.85).aspx
  129. *
  130. * @type {!Array<string>}
  131. * @private
  132. */
  133. goog.net.ImageLoader.IMAGE_LOAD_EVENTS_ = [
  134. goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('11') ?
  135. goog.net.EventType.READY_STATE_CHANGE :
  136. goog.events.EventType.LOAD,
  137. goog.net.EventType.ABORT, goog.net.EventType.ERROR
  138. ];
  139. /**
  140. * Adds an image to the image loader, and associates it with the given ID
  141. * string. If an image with that ID already exists, it is silently replaced.
  142. * When the image in question is loaded, the target of the LOAD event will be
  143. * an {@code Image} object with {@code id} and {@code src} attributes based on
  144. * these arguments.
  145. * @param {string} id The ID of the image to load.
  146. * @param {string|Image} image Either the source URL of the image or the HTML
  147. * image element itself (or any object with a {@code src} property, really).
  148. * @param {!goog.net.ImageLoader.CorsRequestType=} opt_corsRequestType The type
  149. * of CORS request to use, if any.
  150. */
  151. goog.net.ImageLoader.prototype.addImage = function(
  152. id, image, opt_corsRequestType) {
  153. var src = goog.isString(image) ? image : image.src;
  154. if (src) {
  155. // For now, we just store the source URL for the image.
  156. this.imageIdToRequestMap_[id] = {
  157. src: src,
  158. corsRequestType: goog.isDef(opt_corsRequestType) ? opt_corsRequestType :
  159. null
  160. };
  161. }
  162. };
  163. /**
  164. * Removes the image associated with the given ID string from the image loader.
  165. * If the image was previously loading, removes any listeners for its events
  166. * and dispatches a COMPLETE event if all remaining images have now completed.
  167. * @param {string} id The ID of the image to remove.
  168. */
  169. goog.net.ImageLoader.prototype.removeImage = function(id) {
  170. delete this.imageIdToRequestMap_[id];
  171. var image = this.imageIdToImageMap_[id];
  172. if (image) {
  173. delete this.imageIdToImageMap_[id];
  174. // Stop listening for events on the image.
  175. this.handler_.unlisten(
  176. image, goog.net.ImageLoader.IMAGE_LOAD_EVENTS_, this.onNetworkEvent_);
  177. // If this was the last image, raise a COMPLETE event.
  178. if (goog.object.isEmpty(this.imageIdToImageMap_) &&
  179. goog.object.isEmpty(this.imageIdToRequestMap_)) {
  180. this.dispatchEvent(goog.net.EventType.COMPLETE);
  181. }
  182. }
  183. };
  184. /**
  185. * Starts loading all images in the image loader in parallel. Raises a LOAD
  186. * event each time an image finishes loading, and a COMPLETE event after all
  187. * images have finished loading.
  188. */
  189. goog.net.ImageLoader.prototype.start = function() {
  190. // Iterate over the keys, rather than the full object, to essentially clone
  191. // the initial queued images in case any event handlers decide to add more
  192. // images before this loop has finished executing.
  193. var imageIdToRequestMap = this.imageIdToRequestMap_;
  194. goog.array.forEach(goog.object.getKeys(imageIdToRequestMap), function(id) {
  195. var imageRequest = imageIdToRequestMap[id];
  196. if (imageRequest) {
  197. delete imageIdToRequestMap[id];
  198. this.loadImage_(imageRequest, id);
  199. }
  200. }, this);
  201. };
  202. /**
  203. * Creates an {@code Image} object with the specified ID and source URL, and
  204. * listens for network events raised as the image is loaded.
  205. * @param {!goog.net.ImageLoader.ImageRequest_} imageRequest The request data.
  206. * @param {string} id The unique ID of the image to load.
  207. * @private
  208. */
  209. goog.net.ImageLoader.prototype.loadImage_ = function(imageRequest, id) {
  210. if (this.isDisposed()) {
  211. // When loading an image in IE7 (and maybe IE8), the error handler
  212. // may fire before we yield JS control. If the error handler
  213. // dispose the ImageLoader, this method will throw exception.
  214. return;
  215. }
  216. /** @type {!HTMLImageElement} */
  217. var image;
  218. if (this.parent_) {
  219. var dom = goog.dom.getDomHelper(this.parent_);
  220. image = dom.createDom(goog.dom.TagName.IMG);
  221. } else {
  222. image = new Image();
  223. }
  224. if (imageRequest.corsRequestType) {
  225. image.crossOrigin = imageRequest.corsRequestType;
  226. }
  227. this.handler_.listen(
  228. image, goog.net.ImageLoader.IMAGE_LOAD_EVENTS_, this.onNetworkEvent_);
  229. this.imageIdToImageMap_[id] = image;
  230. image.id = id;
  231. image.src = imageRequest.src;
  232. };
  233. /**
  234. * Handles net events (READY_STATE_CHANGE, LOAD, ABORT, and ERROR).
  235. * @param {goog.events.Event} evt The network event to handle.
  236. * @private
  237. */
  238. goog.net.ImageLoader.prototype.onNetworkEvent_ = function(evt) {
  239. var image = /** @type {Element} */ (evt.currentTarget);
  240. if (!image) {
  241. return;
  242. }
  243. if (evt.type == goog.net.EventType.READY_STATE_CHANGE) {
  244. // This implies that the user agent is IE; see loadImage_().
  245. // Noe that this block is used to check whether the image is ready to
  246. // dispatch the COMPLETE event.
  247. if (image.readyState == goog.net.EventType.COMPLETE) {
  248. // This is the IE equivalent of a LOAD event.
  249. evt.type = goog.events.EventType.LOAD;
  250. } else {
  251. // This may imply that the load failed.
  252. // Note that the image has only the following states:
  253. // * uninitialized
  254. // * loading
  255. // * complete
  256. // When the ERROR or the ABORT event is fired, the readyState
  257. // will be either uninitialized or loading and we'd ignore those states
  258. // since they will be handled separately (eg: evt.type = 'ERROR').
  259. // Notes from MSDN : The states through which an object passes are
  260. // determined by that object. An object can skip certain states
  261. // (for example, interactive) if the state does not apply to that object.
  262. // see http://msdn.microsoft.com/en-us/library/ms534359(VS.85).aspx
  263. // The image is not loaded, ignore.
  264. return;
  265. }
  266. }
  267. // Add natural width/height properties for non-Gecko browsers.
  268. if (typeof image.naturalWidth == 'undefined') {
  269. if (evt.type == goog.events.EventType.LOAD) {
  270. image.naturalWidth = image.width;
  271. image.naturalHeight = image.height;
  272. } else {
  273. // This implies that the image fails to be loaded.
  274. image.naturalWidth = 0;
  275. image.naturalHeight = 0;
  276. }
  277. }
  278. // Redispatch the event on behalf of the image. Note that the external
  279. // listener may dispose this instance.
  280. this.dispatchEvent({type: evt.type, target: image});
  281. if (this.isDisposed()) {
  282. // If instance was disposed by listener, exit this function.
  283. return;
  284. }
  285. this.removeImage(image.id);
  286. };
  287. /** @override */
  288. goog.net.ImageLoader.prototype.disposeInternal = function() {
  289. delete this.imageIdToRequestMap_;
  290. delete this.imageIdToImageMap_;
  291. goog.dispose(this.handler_);
  292. goog.net.ImageLoader.superClass_.disposeInternal.call(this);
  293. };