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