Source: ImageViewer.js

/**
 * An image slideshow viewer
 * <pre><strong>options:</strong> {
 *  element : «Element | ID»,
 *  name : «String»,
 *  perimeter : «Integer»,
 *  sizeSnap : «Integer»,
 *  margin : «Integer»,
 *  ease : «Function»,
 *  easeEnd : «Function»,
 *  easeAuto : «Function»,
 *  easeReturn : «Function»,
 *  transition : «Integer»,
 *  transitionEnd : «Integer»,
 *  transitionReturn : «Integer»,
 *  images : «Array»,
 *  listener : «Object»
 * }
 * </pre>
 * @constructor
 */
hui.ui.ImageViewer = function(options) {

  this.options = hui.override({
    maxWidth : 800,
    maxHeight : 600,
    perimeter : 100,
    sizeSnap : 100,
    margin : 0,
    ease : hui.ease.slowFastSlow,
    easeEnd : hui.ease.bounce,
    easeAuto : hui.ease.slowFastSlow,
    easeReturn : hui.ease.cubicInOut,
    transition : 400,
    transitionEnd : 1000,
    transitionReturn : 300,
    images : []
  },options);

  // Collect elements ...
  this.element = hui.get(options.element);

  this.box = this.options.box;

  // State ...
  this.dirty = false;
  this.width = 600;
  this.height = 460;
  this.index = 0;
  this.position = 0; // pixels
  this.playing = false;
  this.name = options.name;
  this.images = options.images || [];

  hui.ui.extend(this);

  // Behavior ...
  this.box.listen(this);
  this._attach();
  this._attachDrag();

  if (options.listener) {
    this.listen(options.listener);
  }
};

/**
 * Creates a new image viewer
 */
hui.ui.ImageViewer.create = function(options) {
  options = options || {};
  var element = options.element = hui.build('div',
    {'class':'hui_imageviewer',
    html:
    '<div class="hui_imageviewer_viewer"><div class="hui_imageviewer_inner_viewer"></div></div>'+
    '<div class="hui_imageviewer_text"></div>'+
    '<div class="hui_imageviewer_status"></div>'+
    '<div class="hui_imageviewer_controller"><div><div>'+
    '<a class="hui_imageviewer_previous"></a>'+
    '<a class="hui_imageviewer_play"></a>'+
    '<a class="hui_imageviewer_next"></a>'+
    '<a class="hui_imageviewer_close"></a>'+
    '</div></div></div>'});
  var box = options.box = hui.ui.Box.create({variant:'plain',absolute:true,modal:true,closable:true});
  box.add(element);
  box.addToDocument();
  return new hui.ui.ImageViewer(options);
};

hui.ui.ImageViewer.prototype = {

  nodes : {
    viewer : '.hui_imageviewer_viewer',
    innerViewer : '.hui_imageviewer_inner_viewer',

    status : '.hui_imageviewer_status',
    text : '.hui_imageviewer_text',

    previous : '.hui_imageviewer_previous',
    controller : '.hui_imageviewer_controller',
    next : '.hui_imageviewer_next',
    play : '.hui_imageviewer_play',
    close : '.hui_imageviewer_close'
  },

  _attach : function() {
    var self = this;
    this.nodes.next.onclick = function() {
      self.next(true);
    };
    this.nodes.previous.onclick = function() {
      self.previous(true);
    };
    this.nodes.play.onclick = function() {
      self.playOrPause();
    };
    this.nodes.close.onclick = this.hide.bind(this);

    this._timer = function() {
      self.next(false);
    };
    this._keyListener = function(e) {
      e = hui.event(e);
      if (e.escapeKey) {
        self.hide();
      } else if (!self.zoomed) {
        if (e.rightKey) {
          self.next(true);
        } else if (e.leftKey) {
          self.previous(true);
        } else if (e.returnKey) {
          self.playOrPause();
        }
      }
    };
    hui.listen(this.nodes.viewer,'mousemove',this._onMouseMove.bind(this));
    hui.listen(this.nodes.controller,'mouseover',function() {
      self.overController = true;
    });
    hui.listen(this.nodes.controller,'mouseout',function() {
      self.overController = false;
    });
    hui.listen(this.nodes.viewer,'mouseout',function(e) {
      if (!hui.ui.isWithin(e,this.nodes.viewer)) {
        self._hideController();
      }
    }.bind(this));
  },
  _draw : function(pos) {
    if (hui.browser.webkit) {
      this.nodes.innerViewer.style.webkitTransform = 'translate3d(' + this.position + 'px,0,0)';
    } else {
      this.nodes.innerViewer.style.marginLeft = this.position + 'px';
    }
  },
  _attachDrag : function() {
    var initial = 0;
    var left = 0;
    var scrl = 0;
    var viewer = this.nodes.viewer;
    var inner = this.nodes.innerViewer;
    var max = 0;
    hui.drag.register({
      touch : true,
      element : this.nodes.innerViewer,
      onBeforeMove : function(e) {
        initial = e.getLeft();
        scrl = this.position;
        max = (this.images.length-1) * this.width * -1;
      }.bind(this),
      onMove : function(e) {
        left = e.getLeft();
        var pos = (scrl - (initial - left));
        if (pos > 0) {
          pos = (Math.exp(pos * -0.013) -1) * -80;
        }
        if (pos < max) {
          pos = (Math.exp((pos - max) * 0.013) -1) * 80 + max;
        }
        this.position = pos;
        this._draw();
      }.bind(this),
      onAfterMove : function() {
        var func = (initial - left) < 0 ? Math.floor : Math.ceil;
        this.index = func(this.position * -1 / this.width);
        var num = this.images.length - 1;
        if (this.index==this.images.length) {
          this.index = 0;
        } else if (this.index < 0) {
          this.index = this.images.length - 1;
        } else {
          num = 1;
        }

        this._goToImage(true,num,false,true);
      }.bind(this),
      onNotMoved : this._zoom.bind(this)
    });
  },
  _onMouseMove : function() {
    window.clearTimeout(this.ctrlHider);
    if (this._shouldShowController()) {
      this.ctrlHider = window.setTimeout(this._hideController.bind(this),2000);
      if (!hui.browser.opacity) {
        this.nodes.controller.style.display='block';
      } else {
        hui.effect.fadeIn({element:this.nodes.controller,duration:200});
      }
    }
  },
  _hideController : function() {
    if (!this.overController) {
      if (!hui.browser.opacity) {
        this.nodes.controller.style.display='none';
      } else {
        hui.effect.fadeOut({element:this.nodes.controller,duration:500});
      }
    }
  },
  _getLargestSize : function(canvas,image) {
    return hui.fit(image,canvas,{upscale:false});
  },
  _calculateSize : function() {
    var snap = this.options.sizeSnap;
    var newWidth = hui.window.getViewWidth() - this.options.perimeter;
    newWidth = Math.floor(newWidth / snap) * snap;
    newWidth = Math.min(newWidth, this.options.maxWidth);
    var newHeight = hui.window.getViewHeight() - this.options.perimeter;
    newHeight = Math.floor(newHeight / snap) * snap;
    newHeight = Math.min(newHeight, this.options.maxHeight);
    var maxWidth = 0;
    var maxHeight = 0;
    for (var i = 0; i < this.images.length; i++) {
      var dims = this._getLargestSize({
        width: newWidth,
        height: newHeight
      }, this.images[i]);
      maxWidth = Math.max(maxWidth, dims.width);
      maxHeight = Math.max(maxHeight, dims.height);
    }
    newHeight = Math.floor(Math.min(newHeight, maxHeight));
    newWidth = Math.floor(Math.min(newWidth, maxWidth));

    if (newWidth != this.width || newHeight != this.height) {
      this.width = newWidth;
      this.height = newHeight;
      this.dirty = true;
    }

  },
  _updateUI : function() {
    if (this.dirty) {
      this.nodes.innerViewer.innerHTML='';
      for (var i=0; i < this.images.length; i++) {
        var element = hui.build('div',{'class':'hui_imageviewer_image'});
        hui.style.set(element,{width: (this.width + this.options.margin) + 'px',height : (this.height-1)+'px' });
        this.nodes.innerViewer.appendChild(element);
      }
      this.nodes.controller.style.display = this._shouldShowController() ? 'block' : 'none';
      this.dirty = false;
      this._preload();
    }
  },
  _shouldShowController : function() {
    return this.images.length > 1;
  },
  _goToImage : function(animate,num,user,drag) {
    var initial = this.position;
    var target = this.position = this.index * (this.width + this.options.margin) * -1;
    if (animate) {
      var duration, ease;
      if (drag) {
        duration = 200 * num;
        ease = hui.ease.fastSlow;
        ease = hui.ease.quadOut;
      }
      else if (num > 1) {
        duration = Math.min(num * this.options.transitionReturn, 2000);
        ease = this.options.easeReturn;
      } else {
        var end = this.index === 0 || this.index == this.images.length - 1;
        ease = (end ? this.options.easeEnd : this.options.ease);
        if (!user) {
          ease = this.options.easeAuto;
        }
        duration = (end ? this.options.transitionEnd : this.options.transition);
      }
      hui.animate({
        node : this.nodes.innerViewer,
        css : {marginLeft : target + 'px'},
        duration : duration,
        ease : ease,
        $render : function(node,v) {
          this.position = initial + (target - initial) * v;
          this._draw();
        }.bind(this)
      });
    } else {
      this._draw();
    }
    this._drawText();
  },

  _drawText : function() {
    var text = this.images[this.index].text;
    if (text) {
      this.nodes.text.innerHTML = text;
      this.nodes.text.style.display = 'block';
    } else {
      this.nodes.text.innerHTML = '';
      this.nodes.text.style.display = 'none';
    }
  },

  // Show / hide ...

  /** Show the image viewer starting at the image with a certain id. Will not show if image is not found
   * @param {Integer} id The id if the image to start with
   */
  showById: function(id) {
    for (var i=0; i < this.images.length; i++) {
      if (this.images[i].id==id) {
        this.show(i);
        break;
      }
    }
  },
  /** Show the image viewer
   * @param {Integer} index? Optional index to start from (zero-based)
   */
  show: function(index) {
    this.index = index || 0;
    this._calculateSize();
    this._updateUI();
    var margin = this.options.margin;
    hui.style.set(this.element, {
      width: (this.width + margin) + 'px',
      height: (this.height + margin * 2 - 1) + 'px'
    });
    hui.style.set(this.nodes.viewer, {
      width: (this.width + margin) + 'px',
      height: (this.height - 1) + 'px'
    });
    hui.style.set(this.nodes.innerViewer, {
      width: ((this.width + margin) * this.images.length) + 'px',
      height: (this.height - 1) + 'px'
    });
    hui.style.set(this.nodes.controller, {
      marginLeft: ((this.width - 160) / 2 + margin * 0.5) + 'px',
      display: 'none'
    });
    this.box.show();
    this._goToImage(false,0,false);
    hui.listen(document,'keydown',this._keyListener);
    this.visible = true;
    this._setHash(true);
  },
  _setHash : function(visible) {
    return; // Disabled
    /*
    if (!this._listening) {
      this._listening = true;
      if (!hui.browser.msie6 && !hui.browser.msie7) {
        hui.listen(window,'hashchange',this._onHashChange.bind(this));
      }
    }
    if (visible) {
      document.location='#imageviewer';
    } else {
      hui.location.clearHash();
    }*/
  },
  _onHashChange : function() {
    if (this._changing) return;
    this._changing = true;
    if (hui.location.hasHash('imageviewer') && !this.visible) {
      this.show();
    } else if (!hui.location.hasHash('imageviewer') && this.visible) {
      this.hide();
    }
    this._changing = false;
  },
  /** Hide the image viewer */
  hide: function() {
    this._hide();
  },
  _hide : function() {
    this.pause();
    this.box.hide();
    this._endZoom();
    hui.unListen(document,'keydown',this._keyListener);
    this.visible = false;
    this._setHash(false);
  },


  // Listeners ...

  /** @private */
  $boxCurtainWasClicked : function() {
    this.hide();
  },
  /** @private */
  $boxWasClosed : function() {
    this.hide();
  },


  // Data handling ...

  /** Clear all images in the stack */
  clearImages : function() {
    this.images = [];
    this.dirty = true;
  },
  /**
   * Add multiple images to the stack
   * @param {Array} images An array of image objects
   */
  addImages : function(images) {
    for (var i=0; i < images.length; i++) {
      this.addImage(images[i]);
    }
  },
  /**
   * Add an image to the stack
   * @param {Object} img An image object representing an image
   */
  addImage : function(img) {
    this.images.push(img);
    this.dirty = true;
  },


  // Playback...

  /** Start playing slideshow */
  play : function() {
    if (!this.interval) {
      this.interval = window.setInterval(this._timer,6000);
    }
    this.next(false);
    this.playing=true;
    this.nodes.play.className='hui_imageviewer_pause';
  },
  /** Pauseslideshow */
  pause : function() {
    window.clearInterval(this.interval);
    this.interval = null;
    this.nodes.play.className='hui_imageviewer_play';
    this.playing = false;
  },
  /** Start or pause slideshow */
  playOrPause : function() {
    if (this.playing) {
      this.pause();
    } else {
      this.play();
    }
  },
  _resetPlay : function() {
    if (this.playing) {
      window.clearInterval(this.interval);
      this.interval = window.setInterval(this._timer,6000);
    }
  },
  /** Go to the previous image
   * @param {Boolean} user If it is initiated by the user
   */
  previous : function(user) {
    var num = 1;
    this.index--;
    if (this.index < 0) {
      this.index = this.images.length - 1;
      num = this.images.length - 1;
    }
    this._goToImage(true,num,user);
    this._resetPlay();
  },
  /** Go to the next image
   * @param {Boolean} user If it is initiated by the user
   */
  next : function(user) {
    var num = 1;
    this.index++;
    if (this.index==this.images.length) {
      this.index = 0;
      num = this.images.length - 1;
    }
    this._goToImage(true,num,user);
    this._resetPlay();
  },






  // Preloading ...

  _preload : function() {
    var guiLoader = new hui.Preloader();
    guiLoader.addImages(hui.ui.getURL('gfx/imageviewer_controls.png'));
    var self = this;
    guiLoader.setDelegate({
      allImagesDidLoad: function() {
        self._preloadImages();
      }
    });
    guiLoader.load();
  },
  _preloadImages : function() {
    var loader = new hui.Preloader();
    loader.setDelegate(this);
    for (var i=0; i < this.images.length; i++) {
      var url = hui.ui.resolveImageUrl(this,this.images[i],this.width,this.height);
      if (url!==null) {
        loader.addImages(url);
      }
    }
    this.nodes.status.innerHTML = '0%';
    this.nodes.status.style.display = '';
    loader.load(this.index);
  },
  /** @private */
  allImagesDidLoad : function() {
    this.nodes.status.style.display = 'none';
  },
  /** @private */
  imageDidLoad : function(loaded,total,index) {
    this.nodes.status.innerHTML = Math.round(loaded/total*100)+'%';
    var url = hui.ui.resolveImageUrl(this,this.images[index],this.width,this.height);
    url = url.replace(/&amp;/g,'&');
    this.nodes.innerViewer.childNodes[index].style.backgroundImage="url('"+url+"')";
    hui.cls.set(this.nodes.innerViewer.childNodes[index],'hui_imageviewer_image_abort',false);
    hui.cls.set(this.nodes.innerViewer.childNodes[index],'hui_imageviewer_image_error',false);
  },
  /** @private */
  imageDidGiveError : function(loaded,total,index) {
    hui.cls.set(this.nodes.innerViewer.childNodes[index],'hui_imageviewer_image_error',true);
  },
  /** @private */
  imageDidAbort : function(loaded,total,index) {
    hui.cls.set(this.nodes.innerViewer.childNodes[index],'hui_imageviewer_image_abort',true);
  },




  // Zooming ...

  zoomed : false,

  _zoom : function(e) {
    var img = this.images[this.index];
    if (img.width <= this.width && img.height <= this.height) {
      return; // Don't zoom if small
    }
    if (!this.zoomer) {
      this.zoomer = hui.build('div',{
        'class' : 'hui_imageviewer_zoomer',
        'style' : 'width:'+this.nodes.viewer.clientWidth+'px;height:'+this.nodes.viewer.clientHeight+'px'
      });
      this.element.insertBefore(this.zoomer,hui.dom.firstChild(this.element));
      hui.listen(this.zoomer,'mousemove',this._onZoomMove.bind(this));
      hui.listen(this.zoomer,'click',this._endZoom.bind(this));
    }
    this._hideController();
    this.pause();
    var size = this._getLargestSize({width:2000,height:2000},img);
    var url = hui.ui.resolveImageUrl(this,img,size.width,size.height);
    var top = Math.max(0, Math.round((this.nodes.viewer.clientHeight - size.height) / 2));
    this.zoomer.innerHTML = '<div style="width:'+size.width+'px;height:'+size.height+'px; margin: 0 auto;"><img src="'+url+'" style="margin-top: '+ top + 'px" /></div>';
    this.zoomer.style.display = 'block';
    this.zoomInfo = {width:size.width,height:size.height};
    this._onZoomMove(e);
    this.zoomed = true;
  },
  _onZoomMove : function(e) {
    if (!this.zoomInfo) {
      return;
    }
    var offset = hui.position.get(this.zoomer);
    e = new hui.Event(e);
    var x = (e.getLeft() - offset.left) / this.zoomer.clientWidth * (this.zoomInfo.width - this.zoomer.clientWidth);
    var y = (e.getTop() - offset.top) / this.zoomer.clientHeight * (this.zoomInfo.height - this.zoomer.clientHeight);

    this.zoomer.scrollLeft = x;
    this.zoomer.scrollTop = y;
  },
  _endZoom : function() {
    if (this.zoomer) {
      this.zoomer.style.display='none';
      this.zoomed = false;
    }
  }

};

hui.define('hui.ui.ImageViewer',hui.ui.ImageViewer);