1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // Portions ©2010 Strobe Inc. 6 // License: Licensed under MIT license (see license.js) 7 // ========================================================================== 8 9 SC.IMAGE_STATE_NONE = 'none'; 10 SC.IMAGE_STATE_LOADING = 'loading'; 11 SC.IMAGE_STATE_LOADED = 'loaded'; 12 SC.IMAGE_STATE_FAILED = 'failed'; 13 14 SC.IMAGE_TYPE_NONE = 'NONE'; 15 SC.IMAGE_TYPE_URL = 'URL'; 16 SC.IMAGE_TYPE_CSS_CLASS = 'CSS_CLASS'; 17 18 /** 19 URL to a transparent GIF. Used for spriting. 20 */ 21 SC.BLANK_IMAGE_DATAURL = "data:image/gif;base64,R0lGODlhAQABAJAAAP///wAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw=="; 22 23 SC.BLANK_IMAGE_URL = SC.browser.isIE && SC.browser.compare(SC.browser.version, '8') <= 0 ? sc_static('blank.gif') : SC.BLANK_IMAGE_DATAURL; 24 25 SC.BLANK_IMAGE = new Image(); 26 SC.BLANK_IMAGE.src = SC.BLANK_IMAGE_URL; 27 SC.BLANK_IMAGE.width = SC.BLANK_IMAGE.height = 1; 28 29 /** 30 @class 31 32 Displays an image in the browser. 33 34 The ImageView can be used to efficiently display images in the browser. 35 It includes a built in support for a number of features that can improve 36 your page load time if you use a lot of images including a image loading 37 cache and automatic support for CSS spriting. 38 39 Note that there are actually many controls that will natively include 40 images using an icon property name. 41 42 @extends SC.View 43 @extends SC.Control 44 @extends SC.InnerFrame 45 @since SproutCore 1.0 46 */ 47 SC.ImageView = SC.View.extend(SC.Control, SC.InnerFrame, 48 /** @scope SC.ImageView.prototype */ { 49 50 classNames: 'sc-image-view', 51 52 // Don't apply this role until each image view can assign a non-empty string value for @aria-label <rdar://problem/9941887> 53 // ariaRole: 'img', 54 55 displayProperties: ['align', 'scale', 'value', 'displayToolTip'], 56 57 /** @private */ 58 renderDelegateName: function () { 59 return (this.get('useCanvas') ? 'canvasImage' : 'image') + "RenderDelegate"; 60 }.property('useCanvas').cacheable(), 61 62 /** @private */ 63 tagName: function () { 64 return this.get('useCanvas') ? 'canvas' : 'div'; 65 }.property('useCanvas').cacheable(), 66 67 68 // .......................................................... 69 // Properties 70 // 71 72 /** 73 If YES, this image can load in the background. Otherwise, it is treated 74 as a foreground image. If the image is not visible on screen, it will 75 always be treated as a background image. 76 */ 77 canLoadInBackground: NO, 78 79 /** @private 80 @type Image 81 @default SC.BLANK_IMAGE 82 */ 83 image: SC.BLANK_IMAGE, 84 85 86 /** @private 87 The frame for the inner img element or for the canvas to draw within, altered according to the scale 88 and align properties provided by SC.InnerFrame. 89 90 @type Object 91 */ 92 innerFrame: function () { 93 var image = this.get('image'), 94 imageWidth = image.width, 95 imageHeight = image.height, 96 frame = this.get('frame'); 97 98 if (SC.none(frame)) return { x: 0, y: 0, width: 0, height: 0 }; // frame is 'null' until rendered when useStaticLayout === YES 99 100 return this.innerFrameForSize(imageWidth, imageHeight, frame.width, frame.height); 101 }.property('align', 'image', 'scale', 'frame').cacheable(), 102 103 /** 104 If YES, any specified toolTip will be localized before display. 105 106 @type Boolean 107 @default YES 108 */ 109 localize: YES, 110 111 /** 112 Current load status of the image. 113 114 This status changes as an image is loaded from the server. If spriting 115 is used, this will always be loaded. Must be one of the following 116 constants: SC.IMAGE_STATE_NONE, SC.IMAGE_STATE_LOADING, 117 SC.IMAGE_STATE_LOADED, SC.IMAGE_STATE_FAILED 118 119 @type String 120 */ 121 status: SC.IMAGE_STATE_NONE, 122 123 /** 124 Will be one of the following constants: SC.IMAGE_TYPE_URL or 125 SC.IMAGE_TYPE_CSS_CLASS 126 127 @type String 128 @observes value 129 */ 130 type: function () { 131 var value = this.get('value'); 132 133 if (SC.ImageView.valueIsUrl(value)) return SC.IMAGE_TYPE_URL; 134 else if (!SC.none(value)) return SC.IMAGE_TYPE_CSS_CLASS; 135 return SC.IMAGE_TYPE_NONE; 136 }.property('value').cacheable(), 137 138 /** 139 The canvas element performs better than the img element since we can 140 update the canvas image without causing browser reflow. As an additional 141 benefit, canvas images are less easily copied, which is generally in line 142 with acting as an 'application'. 143 144 @type Boolean 145 @default YES if supported 146 @since SproutCore 1.5 147 */ 148 useCanvas: function () { 149 return SC.platform.supportsCanvas && !this.get('useStaticLayout'); 150 }.property('useStaticLayout').cacheable(), 151 152 /** 153 If YES, image view will use the SC.imageQueue to control loading. This 154 setting is generally preferred. 155 156 @type Boolean 157 @default YES 158 */ 159 useImageQueue: YES, 160 161 /** 162 A url or CSS class name. 163 164 This is the image you want the view to display. It should be either a 165 url or css class name. You can also set the content and 166 contentValueKey properties to have this value extracted 167 automatically. 168 169 If you want to use CSS spriting, set this value to a CSS class name. If 170 you need to use multiple class names to set your icon, separate them by 171 spaces. 172 173 Note that if you provide a URL, it must contain at least one '/' as this 174 is how we autodetect URLs. 175 176 @type String 177 */ 178 value: null, 179 valueBindingDefault: SC.Binding.oneWay(), 180 181 /** 182 Recalculate our innerFrame if the outer frame has changed. 183 184 @returns {void} 185 */ 186 viewDidResize: function () { 187 sc_super(); 188 189 // Note: SC.View's updateLayer() will call viewDidResize() if useStaticLayout is true. The result of this 190 // is that since our display depends on the frame, when the view or parent view resizes, viewDidResize 191 // notifies that the frame has changed, so we update our view, which calls viewDidResize, which notifies 192 // that the frame has changed, so we update our view, etc. in an infinite loop. 193 if (this.get('useStaticLayout')) { 194 if (this._updatingOnce) { 195 this._updatingOnce = false; 196 } else { 197 // Allow a single update when the view resizes to avoid an infinite loop. 198 this._updatingOnce = true; 199 this.updateLayerIfNeeded(); 200 } 201 } else { 202 this.updateLayerIfNeeded(); 203 } 204 }, 205 206 // .......................................................... 207 // Methods 208 // 209 210 /** @private */ 211 init: function () { 212 sc_super(); 213 214 // Start loading the image immediately on creation. 215 this._valueDidChange(); 216 217 if (this.get('useImageCache') !== undefined) { 218 //@if(debug) 219 SC.warn("Developer Warning: %@ has useImageCache set, please set useImageQueue instead".fmt(this)); 220 //@endif 221 this.set('useImageQueue', this.get('useImageCache')); 222 } 223 }, 224 225 226 // .......................................................... 227 // Rendering 228 // 229 230 /** 231 Called when the element is attached to the document. 232 233 If the image uses static layout (i.e. we don't know the frame beforehand), 234 then this method will call updateLayerIfNeeded in order to adjust the inner 235 frame of the image according to its rendered frame. 236 */ 237 didAppendToDocument: function () { 238 // If using static layout, we can still support image scaling and aligning, 239 // but we need to do it post-render. 240 if (this.get('useStaticLayout')) { 241 // Call updateLayer manually, because we can't have innerFrame be a 242 // display property. It causes an infinite loop with static layout. 243 this.updateLayerIfNeeded(); 244 } 245 }, 246 247 /** 248 Called when the element is created. 249 250 If the view is using a canvas element, then we can not draw to the canvas 251 until it exists. This method will call updateLayerIfNeeded in order to draw 252 to the canvas. 253 */ 254 didCreateLayer: function () { 255 if (this.get('useCanvas')) { 256 this.updateLayerIfNeeded(); 257 } 258 }, 259 260 // .......................................................... 261 // Value handling 262 // 263 264 /** @private 265 Whenever the value changes, update the image state and possibly schedule 266 an image to load. 267 */ 268 _valueDidChange: function () { 269 var value = this.get('value'), 270 type = this.get('type'); 271 272 // Reset the backing image object every time. 273 this.set('image', SC.BLANK_IMAGE); 274 275 if (type == SC.IMAGE_TYPE_URL) { 276 // Load the image. 277 this.set('status', SC.IMAGE_STATE_LOADING); 278 279 // order: image cache, normal load 280 if (!this._loadImageUsingCache()) { 281 this._loadImage(); 282 } 283 } 284 }.observes('value'), 285 286 /** @private 287 Tries to load the image value using the SC.imageQueue object. If the value is not 288 a URL, it won't attempt to load it using this method. 289 290 @returns YES if loading using SC.imageQueue, NO otherwise 291 */ 292 _loadImageUsingCache: function () { 293 var value = this.get('value'), 294 type = this.get('type'); 295 296 // now update local state as needed.... 297 if (type === SC.IMAGE_TYPE_URL && this.get('useImageQueue')) { 298 var isBackground = this.get('isVisibleInWindow') || this.get('canLoadInBackground'); 299 300 SC.imageQueue.loadImage(value, this, this._loadImageUsingCacheDidComplete, isBackground); 301 return YES; 302 } 303 304 return NO; 305 }, 306 307 /** @private */ 308 _loadImageUsingCacheDidComplete: function (url, image) { 309 var value = this.get('value'); 310 311 if (value === url) { 312 if (SC.ok(image)) { 313 this.didLoad(image); 314 } else { 315 // if loading it using the cache didn't work, it's useless to try loading the image normally 316 this.didError(image); 317 } 318 } 319 }, 320 321 /** @private 322 Loads an image using a normal Image object, without using the SC.imageQueue. 323 324 @returns YES if it will load, NO otherwise 325 */ 326 _loadImage: function () { 327 var value = this.get('value'), 328 type = this.get('type'), 329 that = this, 330 image, 331 jqImage; 332 333 if (type === SC.IMAGE_TYPE_URL) { 334 image = new Image(); 335 336 var errorFunc = function () { 337 SC.run(function () { 338 that._loadImageDidComplete(value, SC.$error("SC.Image.FailedError", "Image", -101)); 339 }); 340 }; 341 342 var loadFunc = function () { 343 SC.run(function () { 344 that._loadImageDidComplete(value, image); 345 }); 346 }; 347 348 // Don't grab the jQuery object repeatedly 349 jqImage = $(image); 350 351 // Using bind here instead of setting onabort/onerror/onload directly 352 // fixes an issue with images having 0 width and height 353 jqImage.bind('error', errorFunc); 354 jqImage.bind('abort', errorFunc); 355 jqImage.bind('load', loadFunc); 356 357 image.src = value; 358 return YES; 359 } 360 361 return NO; 362 }, 363 364 /** @private */ 365 _loadImageDidComplete: function (url, image) { 366 var value = this.get('value'); 367 368 if (value === url) { 369 if (SC.ok(image)) { 370 this.didLoad(image); 371 } else { 372 this.didError(image); 373 } 374 } 375 }, 376 377 didLoad: function (image) { 378 this.set('status', SC.IMAGE_STATE_LOADED); 379 if (!image) image = SC.BLANK_IMAGE; 380 this.set('image', image); 381 this.displayDidChange(); 382 }, 383 384 didError: function (error) { 385 this.set('status', SC.IMAGE_STATE_FAILED); 386 this.set('image', SC.BLANK_IMAGE); 387 } 388 389 }); 390 391 /** 392 Returns YES if the passed value looks like an URL and not a CSS class 393 name. 394 */ 395 SC.ImageView.valueIsUrl = function (value) { 396 return value && SC.typeOf(value) === SC.T_STRING ? value.indexOf('/') >= 0 : NO; 397 }; 398