// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // Portions ©2010 Strobe Inc. // License: Licensed under MIT license (see license.js) // ==========================================================================

SC.IMAGE_STATE_NONE = 'none'; SC.IMAGE_STATE_LOADING = 'loading'; SC.IMAGE_STATE_LOADED = 'loaded'; SC.IMAGE_STATE_FAILED = 'failed';

SC.IMAGE_TYPE_NONE = 'NONE'; SC.IMAGE_TYPE_URL = 'URL'; SC.IMAGE_TYPE_CSS_CLASS = 'CSS_CLASS';

/**

URL to a transparent GIF.  Used for spriting.

*/ SC.BLANK_IMAGE_DATAURL = “data:image/gif;base64,R0lGODlhAQABAJAAAP///wAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==”;

SC.BLANK_IMAGE_URL = SC.browser.isIE && SC.browser.compare(SC.browser.version, '8') <= 0 ? sc_static('blank.gif') : SC.BLANK_IMAGE_DATAURL;

SC.BLANK_IMAGE = new Image(); SC.BLANK_IMAGE.src = SC.BLANK_IMAGE_URL; SC.BLANK_IMAGE.width = SC.BLANK_IMAGE.height = 1;

/**

@class

Displays an image in the browser.

The ImageView can be used to efficiently display images in the browser.
It includes a built in support for a number of features that can improve
your page load time if you use a lot of images including a image loading
cache and automatic support for CSS spriting.

Note that there are actually many controls that will natively include
images using an icon property name.

@extends SC.View
@extends SC.Control
@extends SC.InnerFrame
@since SproutCore 1.0

*/ SC.ImageView = SC.View.extend(SC.Control, SC.InnerFrame, /** @scope SC.ImageView.prototype */ {

classNames: 'sc-image-view',

// Don't apply this role until each image view can assign a non-empty string value for @aria-label <rdar://problem/9941887>
// ariaRole: 'img',

displayProperties: ['align', 'scale', 'value', 'displayToolTip'],

/** @private */
renderDelegateName: function () {
  return (this.get('useCanvas') ? 'canvasImage' : 'image') + "RenderDelegate";
}.property('useCanvas').cacheable(),

/** @private */
tagName: function () {
  return this.get('useCanvas') ? 'canvas' : 'div';
}.property('useCanvas').cacheable(),

// ..........................................................
// Properties
//

/**
  If YES, this image can load in the background.  Otherwise, it is treated
  as a foreground image.  If the image is not visible on screen, it will
  always be treated as a background image.
*/
canLoadInBackground: NO,

/** @private
  @type Image
  @default SC.BLANK_IMAGE
*/
image: SC.BLANK_IMAGE,

/** @private
  The frame for the inner img element or for the canvas to draw within, altered according to the scale
  and align properties provided by SC.InnerFrame.

  @type Object
*/
innerFrame: function () {
  var image = this.get('image'),
    imageWidth = image.width,
    imageHeight = image.height,
    frame = this.get('frame');

  if (SC.none(frame)) return { x: 0, y: 0, width: 0, height: 0 };  // frame is 'null' until rendered when useStaticLayout === YES

  return this.innerFrameForSize(imageWidth, imageHeight, frame.width, frame.height);
}.property('align', 'image', 'scale', 'frame').cacheable(),

/**
  If YES, any specified toolTip will be localized before display.

  @type Boolean
  @default YES
*/
localize: YES,

/**
  Current load status of the image.

  This status changes as an image is loaded from the server.  If spriting
  is used, this will always be loaded.  Must be one of the following
  constants: SC.IMAGE_STATE_NONE, SC.IMAGE_STATE_LOADING,
  SC.IMAGE_STATE_LOADED, SC.IMAGE_STATE_FAILED

  @type String
*/
status: SC.IMAGE_STATE_NONE,

/**
  Will be one of the following constants: SC.IMAGE_TYPE_URL or
  SC.IMAGE_TYPE_CSS_CLASS

  @type String
  @observes value
*/
type: function () {
  var value = this.get('value');

  if (SC.ImageView.valueIsUrl(value)) return SC.IMAGE_TYPE_URL;
  else if (!SC.none(value)) return SC.IMAGE_TYPE_CSS_CLASS;
  return SC.IMAGE_TYPE_NONE;
}.property('value').cacheable(),

/**
  The canvas element performs better than the img element since we can
  update the canvas image without causing browser reflow.  As an additional
  benefit, canvas images are less easily copied, which is generally in line
  with acting as an 'application'.

  @type Boolean
  @default YES if supported
  @since SproutCore 1.5
*/
useCanvas: function () {
  return SC.platform.supportsCanvas && !this.get('useStaticLayout');
}.property('useStaticLayout').cacheable(),

/**
  If YES, image view will use the SC.imageQueue to control loading.  This
  setting is generally preferred.

  @type Boolean
  @default YES
*/
useImageQueue: YES,

/**
  A url or CSS class name.

  This is the image you want the view to display.  It should be either a
  url or css class name.  You can also set the content and
  contentValueKey properties to have this value extracted
  automatically.

  If you want to use CSS spriting, set this value to a CSS class name.  If
  you need to use multiple class names to set your icon, separate them by
  spaces.

  Note that if you provide a URL, it must contain at least one '/' as this
  is how we autodetect URLs.

  @type String
*/
value: null,
valueBindingDefault: SC.Binding.oneWay(),

/**
  Recalculate our innerFrame if the outer frame has changed.

  @returns {void}
*/
viewDidResize: function () {
  sc_super();

  // Note: SC.View's updateLayer() will call viewDidResize() if useStaticLayout is true.  The result of this
  // is that since our display depends on the frame, when the view or parent view resizes, viewDidResize
  // notifies that the frame has changed, so we update our view, which calls viewDidResize, which notifies
  // that the frame has changed, so we update our view, etc. in an infinite loop.
  if (this.get('useStaticLayout')) {
    if (this._updatingOnce) {
      this._updatingOnce = false;
    } else {
      // Allow a single update when the view resizes to avoid an infinite loop.
      this._updatingOnce = true;
      this.updateLayerIfNeeded();
    }
  } else {
    this.updateLayerIfNeeded();
  }
},

// ..........................................................
// Methods
//

/** @private */
init: function () {
  sc_super();

  // Start loading the image immediately on creation.
  this._valueDidChange();

  if (this.get('useImageCache') !== undefined) {
    //@if(debug)
    SC.warn("Developer Warning: %@ has useImageCache set, please set useImageQueue instead".fmt(this));
    //@endif
    this.set('useImageQueue', this.get('useImageCache'));
  }
},

// ..........................................................
// Rendering
//

/**
  Called when the element is attached to the document.

  If the image uses static layout (i.e. we don't know the frame beforehand),
  then this method will call updateLayerIfNeeded in order to adjust the inner
  frame of the image according to its rendered frame.
*/
didAppendToDocument: function () {
  // If using static layout, we can still support image scaling and aligning,
  // but we need to do it post-render.
  if (this.get('useStaticLayout')) {
    // Call updateLayer manually, because we can't have innerFrame be a
    // display property.  It causes an infinite loop with static layout.
    this.updateLayerIfNeeded();
  }
},

/**
  Called when the element is created.

  If the view is using a canvas element, then we can not draw to the canvas
  until it exists.  This method will call updateLayerIfNeeded in order to draw
  to the canvas.
*/
didCreateLayer: function () {
  if (this.get('useCanvas')) {
    this.updateLayerIfNeeded();
  }
},

// ..........................................................
// Value handling
//

/** @private
  Whenever the value changes, update the image state and possibly schedule
  an image to load.
*/
_valueDidChange: function () {
  var value = this.get('value'),
    type = this.get('type');

  // Reset the backing image object every time.
  this.set('image', SC.BLANK_IMAGE);

  if (type == SC.IMAGE_TYPE_URL) {
    // Load the image.
    this.set('status', SC.IMAGE_STATE_LOADING);

    // order: image cache, normal load
    if (!this._loadImageUsingCache()) {
      this._loadImage();
    }
  }
}.observes('value'),

/** @private
  Tries to load the image value using the SC.imageQueue object. If the value is not
  a URL, it won't attempt to load it using this method.

  @returns YES if loading using SC.imageQueue, NO otherwise
*/
_loadImageUsingCache: function () {
  var value = this.get('value'),
      type = this.get('type');

  // now update local state as needed....
  if (type === SC.IMAGE_TYPE_URL && this.get('useImageQueue')) {
    var isBackground = this.get('isVisibleInWindow') || this.get('canLoadInBackground');

    SC.imageQueue.loadImage(value, this, this._loadImageUsingCacheDidComplete, isBackground);
    return YES;
  }

  return NO;
},

/** @private */
_loadImageUsingCacheDidComplete: function (url, image) {
  var value = this.get('value');

  if (value === url) {
    if (SC.ok(image)) {
      this.didLoad(image);
    } else {
      // if loading it using the cache didn't work, it's useless to try loading the image normally
      this.didError(image);
    }
  }
},

/** @private
  Loads an image using a normal Image object, without using the SC.imageQueue.

  @returns YES if it will load, NO otherwise
*/
_loadImage: function () {
  var value = this.get('value'),
      type = this.get('type'),
      that = this,
      image,
      jqImage;

  if (type === SC.IMAGE_TYPE_URL) {
    image = new Image();

    var errorFunc = function () {
      SC.run(function () {
        that._loadImageDidComplete(value, SC.$error("SC.Image.FailedError", "Image", -101));
      });
    };

    var loadFunc = function () {
      SC.run(function () {
        that._loadImageDidComplete(value, image);
      });
    };

    // Don't grab the jQuery object repeatedly
    jqImage = $(image);

    // Using bind here instead of setting onabort/onerror/onload directly
    // fixes an issue with images having 0 width and height
    jqImage.bind('error', errorFunc);
    jqImage.bind('abort', errorFunc);
    jqImage.bind('load', loadFunc);

    image.src = value;
    return YES;
  }

  return NO;
},

/** @private */
_loadImageDidComplete: function (url, image) {
  var value = this.get('value');

  if (value === url) {
    if (SC.ok(image)) {
      this.didLoad(image);
    } else {
      this.didError(image);
    }
  }
},

didLoad: function (image) {
  this.set('status', SC.IMAGE_STATE_LOADED);
  if (!image) image = SC.BLANK_IMAGE;
  this.set('image', image);
  this.displayDidChange();
},

didError: function (error) {
  this.set('status', SC.IMAGE_STATE_FAILED);
  this.set('image', SC.BLANK_IMAGE);
}

});

/**

Returns YES if the passed value looks like an URL and not a CSS class
name.

*/ SC.ImageView.valueIsUrl = function (value) {

return value && SC.typeOf(value) === SC.T_STRING ? value.indexOf('/') >= 0 : NO;

};