}
}
}
-});
\ No newline at end of file
+});
+
+/**
+ * Provides a focused image viewer for WCF.
+ *
+ * Usage:
+ * $('.triggerElement').wcfImageViewer({
+ * shiftBy: 5,
+ *
+ * enableSlideshow: 1,
+ * speed: 5,
+ *
+ * className: 'wcf\\data\\foo\\FooAction'
+ * });
+ */
+$.widget('ui.wcfImageViewer', {
+ /**
+ * active image index
+ * @var integer
+ */
+ _active: -1,
+
+ /**
+ * active image object id
+ * @var integer
+ */
+ _activeImage: null,
+
+ /**
+ * image viewer container object
+ * @var jQuery
+ */
+ _container: null,
+
+ /**
+ * initialization state
+ * @var boolean
+ */
+ _didInit: false,
+
+ /**
+ * list of available images
+ * @var array<object>
+ */
+ _images: [ ],
+
+ /**
+ * true if image viewer is open
+ * @var boolean
+ */
+ _isOpen: false,
+
+ /**
+ * number of total images
+ * @var integer
+ */
+ _items: -1,
+
+ /**
+ * maximum dimensions for enlarged view
+ * @var object<integer>
+ */
+ _maxDimensions: {
+ height: 0,
+ width: 0
+ },
+
+ /**
+ * action proxy object
+ * @var WCF.Action.Proxy
+ */
+ _proxy: null,
+
+ /**
+ * true if slideshow is currently running
+ * @var boolean
+ */
+ _slideshowEnabled: false,
+
+ /**
+ * visible width of thumbnail container
+ * @var integer
+ */
+ _thumbnailContainerWidth: 0,
+
+ /**
+ * right margin of a thumbnail
+ * @var integer
+ */
+ _thumbnailMarginRight: 0,
+
+ /**
+ * left offset of thumbnail list
+ * @var integer
+ */
+ _thumbnailOffset: 0,
+
+ /**
+ * outer width of a thumbnail (includes margin)
+ * @var integer
+ */
+ _thumbnailWidth: 0,
+
+ /**
+ * slideshow timer object
+ * @var WCF.PeriodicalExecuter
+ */
+ _timer: null,
+
+ /**
+ * list of interface elements
+ * @var object<jQuery>
+ */
+ _ui: {
+ buttonNext: null,
+ buttonPrevious: null,
+ header: null,
+ image: null,
+ imageContainer: null,
+ imageList: null,
+ slideshow: {
+ container: null,
+ enlarge: null,
+ next: null,
+ previous: null,
+ toggle: null
+ }
+ },
+
+ /**
+ * list of options parsed during init
+ * @var object<mixed>
+ */
+ options: {
+ // navigation
+ shiftBy: 5, // thumbnail slider control
+
+ // slideshow
+ enableSlideshow: 1,
+ speed: 5, // time in seconds
+
+ // ajax
+ className: '' // must be an instance of \wcf\data\IImageViewerAction
+ },
+
+ /**
+ * Creates a new wcfImageViewer instance.
+ */
+ _create: function() {
+ this._active = -1;
+ this._activeImage = null;
+ this._container = null;
+ this._didInit = false;
+ this._images = [ ];
+ this._isOpen = false;
+ this._items = -1;
+ this._maxDimensions = {
+ height: document.documentElement.clientHeight,
+ width: document.documentElement.clientWidth
+ };
+ this._proxy = new WCF.Action.Proxy({
+ success: $.proxy(this._success, this)
+ });
+ this._slideshowEnabled = false;
+ this._thumbnailContainerWidth = 0;
+ this._thumbnailMarginRight = 0;
+ this._thumbnailOffset = 0;
+ this._thumbnaiLWidth = 0;
+ this._timer = null;
+ this._ui = { };
+
+ this.element.click($.proxy(this.open, this));
+ },
+
+ /**
+ * Opens the image viewer.
+ *
+ * @return boolean
+ */
+ open: function() {
+ if (this._isOpen) {
+ return false;
+ }
+
+ if (this._images.length === 0) {
+ this._loadNextImages();
+ }
+ else {
+ this._render();
+
+ if (this._items > 1 && this._slideshowEnabled) {
+ this.startSlideshow();
+ }
+ }
+
+ this._isOpen = true;
+
+ return true;
+ },
+
+ /**
+ * Closes the image viewer.
+ *
+ * @return boolean
+ */
+ close: function() {
+ if (!this._isOpen) {
+ return false;
+ }
+
+ this._container.removeClass('open');
+ if (this._timer !== null) {
+ this._timer.stop();
+ }
+
+ this._isOpen = false;
+
+ return true;
+ },
+
+ /**
+ * Enables the slideshow.
+ *
+ * @return boolean
+ */
+ startSlideshow: function() {
+ if (this._slideshowEnabled) {
+ return false;
+ }
+
+ this._timer = new WCF.PeriodicalExecuter($.proxy(function() {
+ var $index = this._active + 1;
+ if ($index == this._items) {
+ $index = 0;
+ }
+
+ this.showImage($index);
+ }, this), this.options.speed * 1000);
+
+ this._slideshowEnabled = true;
+
+ this._ui.slideshow.toggle.children('span').removeClass('icon-play').addClass('icon-pause');
+
+ return true;
+ },
+
+ /**
+ * Disables the slideshow.
+ *
+ * @return boolean
+ */
+ stopSlideshow: function() {
+ if (!this._slideshowEnabled) {
+ return false;
+ }
+
+ this._timer.stop();
+ this._ui.slideshow.toggle.children('span').removeClass('icon-pause').addClass('icon-play');
+
+ this._slideshowEnabled = false;
+
+ return true;
+ },
+
+ /**
+ * Renders the image viewer UI.
+ *
+ * @param boolean initialized
+ */
+ _render: function(initialized) {
+ this._container.addClass('open');
+
+ if (initialized) {
+ var $thumbnail = this._ui.imageList.children('li:eq(0)');;
+ this._thumbnailMarginRight = parseInt($thumbnail.css('marginRight').replace(/px$/, '')) || 0;
+ this._thumbnailWidth = $thumbnail.outerWidth(true);
+ this._thumbnailContainerWidth = this._ui.imageList.parent().innerWidth();
+
+ $thumbnail.trigger('click');
+
+ if (this._items > 1 && this.options.enableSlideshow) {
+ this.startSlideshow();
+ }
+ }
+
+ this._toggleButtons();
+
+ // check if there is enough space to load more thumbnails
+ this._preload();
+ },
+
+ /**
+ * Attempts to load the next images.
+ */
+ _preload: function() {
+ if (this._images.length < this._items) {
+ var $thumbnailsWidth = this._images.length * this._thumbnailWidth;
+ if ($thumbnailsWidth - this._thumbnailOffset < this._thumbnailContainerWidth) {
+ this._loadNextImages();
+ }
+ }
+ },
+
+ /**
+ * Displays image on thumbnail click.
+ *
+ * @param object event
+ */
+ _showImage: function(event) {
+ this.stopSlideshow();
+ this.showImage($(event.currentTarget).data('index'));
+ },
+
+ /**
+ * Displays an image by index.
+ *
+ * @param integer index
+ * @return boolean
+ */
+ showImage: function(index) {
+ if (this._active == index) {
+ return false;
+ }
+
+ // reset active marking
+ if (this._active != -1) {
+ this._images[this._active].listItem.removeClass('active');
+ }
+
+ this._active = index;
+ var $image = this._images[index];
+
+ this._ui.imageList.children('li').removeClass('active');
+ $image.listItem.addClass('active');
+
+ var $dimensions = this._ui.imageContainer.getDimensions('inner');
+
+ if (this._activeImage === null) {
+ this._activeImage = 0;
+ this._renderImage(this._activeImage, $image, $dimensions);
+ }
+ else {
+ var $newImageIndex = (this._activeImage ? 0 : 1);
+ this._renderImage($newImageIndex, $image, $dimensions);
+
+ this._ui.images[this._activeImage].removeClass('active');
+ this._ui.images[$newImageIndex].addClass('active');
+
+ this._activeImage = $newImageIndex;
+ }
+
+ // user
+ var $link = this._ui.header.find('> div > a').prop('href', $image.user.link).prop('title', $image.user.username);
+ $link.children('img').prop('src', $image.user.avatarURL);
+
+ // meta data
+ var $title = WCF.String.escapeHTML($image.image.title);
+ if ($image.image.link) $title = '<a href="' + $image.image.link + '">' + $image.image.title + '</a>';
+ this._ui.header.find('> div > h1').html($title);
+
+ var $seriesTitle = ($image.series && $image.series.title ? WCF.String.escapeHTML($image.series.title) : '');
+ if ($image.series.link) $seriesTitle = '<a href="' + $image.series.link + '">' + $seriesTitle + '</a>';
+ this._ui.header.find('> div > h2').html($seriesTitle);
+
+ this._ui.header.find('> div > h3').text(($image.listItem.data('index') + 1) + " von " + this._items);
+
+ this._ui.slideshow.full.data('link', ($image.image.fullURL ? $image.image.fullURL : $image.image.url));
+
+ this.moveToImage($image.listItem.data('index'));
+
+ this._toggleButtons();
+
+ return true;
+ },
+
+ /**
+ * Renders target image, leaving 'imageData' undefined will invoke the rendering process for the currently active image.
+ *
+ * @param integer targetIndex
+ * @param object imageData
+ * @param object containerDimensions
+ */
+ _renderImage: function(targetIndex, imageData, containerDimensions) {
+ if (!imageData) {
+ targetIndex = this._activeImage;
+ imageData = this._images[this._active];
+
+ containerDimensions = {
+ height: $(window).height() - (this._container.hasClass('maximized') ? 0 : 200),
+ width: this._ui.imageContainer.innerWidth()
+ };
+ }
+
+ // simulate padding
+ containerDimensions.height -= 22;
+ containerDimensions.width -= 20;
+
+ this._ui.images[targetIndex].prop('src', imageData.image.url);
+
+ var $height = imageData.image.height;
+ var $width = imageData.image.width;
+ var $ratio = 0.0;
+
+ // check if image exceeds dimensions on the Y axis
+ if ($height > containerDimensions.height) {
+ $ratio = containerDimensions.height / $height;
+ $height = containerDimensions.height;
+ $width = Math.floor($width * $ratio);
+ }
+
+ // check if image exceeds dimensions on the X axis
+ if ($width > containerDimensions.width) {
+ $ratio = containerDimensions.width / $width;
+ $width = containerDimensions.width;
+ $height = Math.floor($height * $ratio);
+ }
+
+ var $left = Math.floor((containerDimensions.width - $width) / 2);
+ var $top = Math.floor((containerDimensions.height - $height) / 2);
+
+ this._ui.images[targetIndex].css({
+ height: $height + 'px',
+ left: $left + 'px',
+ marginTop: (Math.round($height / 2) * -1) + 'px',
+ width: $width + 'px'
+ });
+ },
+
+ /**
+ * Initialites the user interface.
+ *
+ * @return boolean
+ */
+ _initUI: function() {
+ if (this._didInit) {
+ return false;
+ }
+
+ this._didInit = true;
+
+ this._container = $('<div class="wcfImageViewer" />').appendTo(document.body);
+ var $imageContainer = $('<div><img class="active" /><img /></div>').appendTo(this._container);
+ var $imageList = $('<footer><span class="wcfImageViewerButtonPrevious icon icon-double-angle-left" /><div><ul /></div><span class="wcfImageViewerButtonNext icon icon-double-angle-right" /></footer>').appendTo(this._container);
+ var $slideshowContainer = $('<ul />').appendTo($imageContainer);
+ var $slideshowButtonPrevious = $('<li class="wcfImageViewerSlideshowButtonPrevious"><span class="icon icon48 icon-angle-left" /></li>').appendTo($slideshowContainer);
+ var $slideshowButtonToggle = $('<li class="wcfImageViewerSlideshowButtonToggle pointer"><span class="icon icon48 icon-play" /></li>').appendTo($slideshowContainer);
+ var $slideshowButtonNext = $('<li class="wcfImageViewerSlideshowButtonNext"><span class="icon icon48 icon-angle-right" /></li>').appendTo($slideshowContainer);
+ var $slideshowButtonEnlarge = $('<li class="wcfImageViewerSlideshowButtonEnlarge pointer jsTooltip" title="' + WCF.Language.get('wcf.imageViewer.button.enlarge') + '"><span class="icon icon48 icon-resize-full" /></li>').appendTo($slideshowContainer);
+ var $slideshowButtonFull = $('<li class="wcfImageViewerSlideshowButtonFull pointer jsTooltip" title="' + WCF.Language.get('wcf.imageViewer.button.full') + '"><span class="icon icon48 icon-external-link" /></li>').appendTo($slideshowContainer);
+
+ this._ui = {
+ buttonNext: $imageList.children('span.wcfImageViewerButtonNext'),
+ buttonPrevious: $imageList.children('span.wcfImageViewerButtonPrevious'),
+ header: $('<header><div class="box64"><a class="framed jsTooltip"><img /></a><h1 /><h2 /><h3 /></div></header>').appendTo(this._container),
+ imageContainer: $imageContainer,
+ images: [
+ $imageContainer.children('img:eq(0)').on('webkitTransitionEnd transitionend msTransitionEnd oTransitionEnd', function() { $(this).removeClass('animateTransformation'); }),
+ $imageContainer.children('img:eq(1)').on('webkitTransitionEnd transitionend msTransitionEnd oTransitionEnd', function() { $(this).removeClass('animateTransformation'); })
+ ],
+ imageList: $imageList.find('> div > ul'),
+ slideshow: {
+ container: $slideshowContainer,
+ enlarge: $slideshowButtonEnlarge,
+ full: $slideshowButtonFull,
+ next: $slideshowButtonNext,
+ previous: $slideshowButtonPrevious,
+ toggle: $slideshowButtonToggle
+ }
+ };
+
+ this._ui.buttonNext.click($.proxy(this._next, this));
+ this._ui.buttonPrevious.click($.proxy(this._previous, this));
+
+ $slideshowButtonNext.click($.proxy(this._nextImage, this));
+ $slideshowButtonPrevious.click($.proxy(this._previousImage, this));
+ $slideshowButtonEnlarge.click($.proxy(this._toggleView, this));
+ $slideshowButtonToggle.click($.proxy(function() {
+ if (this._slideshowEnabled) {
+ this.stopSlideshow();
+ }
+ else {
+ this.startSlideshow();
+ }
+ }, this));
+ $slideshowButtonFull.click(function(event) { window.location = $(event.currentTarget).data('link'); });
+
+ // close button
+ $('<span class="wcfImageViewerButtonClose icon icon48 icon-remove pointer jsTooltip" title="' + WCF.Language.get('wcf.global.button.close') + '" />').appendTo(this._ui.header).click($.proxy(this.close, this));
+
+ return true;
+ },
+
+ /**
+ * Toggles between normal and fullscreen view.
+ */
+ _toggleView: function() {
+ this._ui.images[this._activeImage].addClass('animateTransformation');
+ this._container.toggleClass('maximized');
+ this._ui.slideshow.enlarge.toggleClass('active').children('span').toggleClass('icon-resize-full').toggleClass('icon-resize-small');
+
+ this._renderImage(null, undefined, null);
+ },
+
+ /**
+ * Shifts the thumbnail list.
+ *
+ * @param object event
+ * @param integer shiftBy
+ */
+ _next: function(event, shiftBy) {
+ if (this._ui.buttonNext.hasClass('pointer')) {
+ if (shiftBy == undefined) {
+ this.stopSlideshow();
+ }
+
+ var $maximumOffset = Math.max((this._items * this._thumbnailWidth) - this._thumbnailContainerWidth - this._thumbnailMarginRight, 0);
+ this._thumbnailOffset = Math.min(this._thumbnailOffset + (this._thumbnailWidth * (shiftBy ? shiftBy : this.options.shiftBy)), $maximumOffset);
+ this._ui.imageList.css('marginLeft', (this._thumbnailOffset * -1));
+ }
+
+ this._preload();
+
+ this._toggleButtons();
+ },
+
+ /**
+ * Unshifts the thumbnail list.
+ *
+ * @param object event
+ * @param integer shiftBy
+ */
+ _previous: function(event, unshiftBy) {
+ if (this._ui.buttonPrevious.hasClass('pointer')) {
+ if (unshiftBy == undefined) {
+ this.stopSlideshow();
+ }
+
+ this._thumbnailOffset = Math.max(this._thumbnailOffset - (this._thumbnailWidth * (unshiftBy ? unshiftBy : this.options.shiftBy)), 0);
+ this._ui.imageList.css('marginLeft', (this._thumbnailOffset * -1));
+ }
+
+ this._toggleButtons();
+ },
+
+ /**
+ * Displays the next image.
+ *
+ * @param object event
+ */
+ _nextImage: function(event) {
+ if (this._ui.slideshow.next.hasClass('pointer')) {
+ this.stopSlideshow();
+ this.showImage(this._active + 1);
+ }
+ },
+
+ /**
+ * Displays the previous image.
+ *
+ * @param object event
+ */
+ _previousImage: function(event) {
+ if (this._ui.slideshow.previous.hasClass('pointer')) {
+ this.stopSlideshow();
+ this.showImage(this._active - 1);
+ }
+ },
+
+ /**
+ * Moves thumbnail list to target thumbnail.
+ *
+ * @param integer seriesIndex
+ */
+ moveToImage: function(seriesIndex) {
+ // calculate start and end of thumbnail
+ var $start = (seriesIndex - 3) * this._thumbnailWidth;
+ var $end = $start + (this._thumbnailWidth * 5);
+
+ // calculate visible offsets
+ var $left = this._thumbnailOffset;
+ var $right = this._thumbnailOffset + this._thumbnailContainerWidth;
+
+ // check if thumbnail is within boundaries
+ var $shouldMove = false;
+ if ($start < $left || $end > $right) {
+ $shouldMove = true;
+ }
+
+ // try to shift until the thumbnail itself and the next/previous 2 thumbnails are visible
+ if ($shouldMove) {
+ var $shiftBy = 0;
+
+ // unshift
+ if ($start < $left) {
+ while ($start < $left) {
+ $shiftBy++;
+ $left -= this._thumbnailWidth;
+ }
+
+ this._previous(null, $shiftBy);
+ }
+ else {
+ // shift
+ while ($end > $right) {
+ $shiftBy++;
+ $right += this._thumbnailWidth;
+ }
+
+ this._next(null, $shiftBy);
+ }
+ }
+ },
+
+ /**
+ * Toggles control buttons.
+ */
+ _toggleButtons: function() {
+ // button 'previous'
+ if (this._thumbnailOffset > 0) {
+ this._ui.buttonPrevious.addClass('pointer');
+ }
+ else {
+ this._ui.buttonPrevious.removeClass('pointer');
+ }
+
+ // button 'next'
+ var $maximumOffset = (this._images.length * this._thumbnailWidth) - this._thumbnailContainerWidth - this._thumbnailMarginRight;
+ if (this._thumbnailOffset >= $maximumOffset) {
+ this._ui.buttonNext.removeClass('pointer');
+ }
+ else {
+ this._ui.buttonNext.addClass('pointer');
+ }
+
+ // slideshow controls
+ if (this._active > 0) {
+ this._ui.slideshow.previous.addClass('pointer');
+ }
+ else {
+ this._ui.slideshow.previous.removeClass('pointer');
+ }
+
+ if (this._active + 1 < this._images.length) {
+ this._ui.slideshow.next.addClass('pointer');
+ }
+ else {
+ this._ui.slideshow.next.removeClass('pointer');
+ }
+ },
+
+ /**
+ * Inserts thumbnails.
+ *
+ * @param array<object> images
+ */
+ _createThumbnails: function(images) {
+ for (var $i = 0, $length = images.length; $i < $length; $i++) {
+ var $image = images[$i];
+
+ var $listItem = $('<li><a style="background-image: url(' + $image.thumbnail.url + ')"></a></li>').appendTo(this._ui.imageList);
+ $listItem.data('index', this._images.length).click($.proxy(this._showImage, this));
+
+ $image.listItem = $listItem;
+ this._images.push($image);
+ }
+ },
+
+ /**
+ * Loads the next images via AJAX.
+ */
+ _loadNextImages: function() {
+ this._proxy.setOption('data', {
+ actionName: 'loadNextImages',
+ className: this.options.className,
+ interfaceName: 'wcf\\data\\IImageViewerAction',
+ objectIDs: [ this.element.data('objectID') ],
+ parameters: {
+ maximumHeight: this._maxDimensions.height,
+ maximumWidth: this._maxDimensions.width,
+ offset: this._images.length
+ }
+ });
+ this._proxy.sendRequest();
+ },
+
+ /**
+ * Handles successful AJAX requests.
+ *
+ * @param object data
+ * @param string textStatus
+ * @param jQuery jqXHR
+ */
+ _success: function(data, textStatus, jqXHR) {
+ if (data.returnValues.items) {
+ this._items = data.returnValues.items;
+ }
+
+ var $initialized = this._initUI();
+
+ this._createThumbnails(data.returnValues.images);
+
+ this._render($initialized);
+ }
+});