Added an advanced image viewer
authorAlexander Ebert <ebert@woltlab.com>
Mon, 10 Feb 2014 23:56:03 +0000 (00:56 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 10 Feb 2014 23:56:03 +0000 (00:56 +0100)
wcfsetup/install/files/js/WCF.ImageViewer.js
wcfsetup/install/files/lib/data/IImageViewerAction.class.php [new file with mode: 0644]
wcfsetup/install/files/style/imageViewer.less

index d13381c80f051b46fad67d89112cf1c40d5b27ed..8618aae01e321ef5047a1cba3de68d7a80a48c1b 100644 (file)
@@ -106,4 +106,707 @@ WCF.ImageViewer = Class.extend({
                        }
                }
        }
-});
\ 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);
+       }
+});
diff --git a/wcfsetup/install/files/lib/data/IImageViewerAction.class.php b/wcfsetup/install/files/lib/data/IImageViewerAction.class.php
new file mode 100644 (file)
index 0000000..8e55a8a
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Every database object action providing images for .wcfImageViewer() must implement this interface.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data
+ * @category   Community Framework
+ */
+interface IImageViewerAction {
+       /**
+        * Validates parameters to load the next images.
+        */
+       public function validateLoadNextImages();
+       
+       /**
+        * Returns a list of images, array indices given for 'images' are discarded (series and series 'url' are optional).
+        * The first response (offset = 0) must return the total number of images.
+        * 
+        * Each requests contains three parameters:
+        *  - offset: number of already loaded image
+        *  - maximumHeight: image provided in 'url' must be as close as possible to this value
+        *  - maximumWidth: see above
+        * 
+        * Each image can specify a link which should not point to the image itself, instead it should provide a viewable
+        * page directly related to the image (e.g. photo page). The 'fullURL' parameter is optional and results in the
+        * link "View original image" and should lead to the original exceeding lager than the image specified with 'url'.
+        * 
+        * Expected return value:
+        * array(
+        *      'images' => array(
+        *              array(
+        *                      'image' => array(
+        *                              'height' => 768,
+        *                              [ 'fullURL' => 'http://example.com/path/to/full/image.png', ]
+        *                              [ 'link' => 'http://example.com/index.php/123-MyImage/', ]
+        *                              'title' => 'My first picture',
+        *                              'url' => 'http://example.com/path/to/large/image.png',
+        *                              'width' => 1024
+        *                      ),
+        *                      'thumbnail' => array(
+        *                              'height' => 148,
+        *                              'url' => 'http://example.com/path/to/thumbnail.png',
+        *                              'width' => 148
+        *                      ),
+        *                      [ 'series' => array(
+        *                              'title' => 'My image series,
+        *                              [ 'link' => 'http://example.com/link/to/image/series/ ]
+        *                      ), ]
+        *                      'user' => array(
+        *                              'avatarURL' => 'http://link/to/avatar.png',
+        *                              'link' => 'http://example.com/index.php/123-FooBar/',
+        *                              'username' => 'FooBar'
+        *                      )
+        *              ),
+        *              // ...
+        *      ),
+        *      [ 'items' => 123 ]
+        * )
+        * 
+        * @return      array
+        */
+       public function loadNextImages();
+}
index dcef2b9cc2354ce4a71907985f2dfb876071c88b..5f156ed8c40d8d1a3d1458123b564e586cd2a3f2 100644 (file)
 #lbCaption {
        font-weight: bold;
 }
+
+/* ImageViewer for WCF */
+@wcfImageViewerBorderColor: rgba(51, 51, 51, 1);
+@wcfImageViewerFontColor: rgba(192, 192, 192, 1);
+
+.wcfImageViewer {
+       background-color: rgba(0, 0, 0, 1);
+       bottom: 0;
+       display: none;
+       left: 0;
+       opacity: 0;
+       position: fixed;
+       right: 0;
+       top: 0;
+       z-index: 399;
+       
+       &.open {
+               display: block;
+               opacity: 1;
+       }
+       
+       &.maximized {
+               > header {
+                       top: -100px;
+               }
+               
+               > div {
+                       bottom: 0;
+                       border-color: fade(@wcfImageViewerBorderColor, 0%);
+                       top: 0;
+               }
+               
+               > footer {
+                       bottom: -100px;
+               }
+       }
+       
+       > header,
+       > div,
+       > footer {
+               box-sizing: border-box;
+               left: 0;
+               position: fixed;
+               right: 0;
+               z-index: 400;
+       }
+       
+       > header {
+               height: 100px;
+               padding: 1rem;
+               top: 0;
+               
+               transition: top linear .3s;
+               
+               > div {
+                       > h1,
+                       > h2,
+                       > h3 {
+                               color: @wcfImageViewerFontColor;
+                               margin-left: 80px !important;
+                       }
+                       
+                       > h1 {
+                               font-size: 1.75rem;
+                       }
+                       
+                       > h2 {
+                               font-size: 1.25rem;
+                       }
+                       
+                       > h3 {
+                               color: rgba(153, 153, 153, 1);
+                               font-size: .85rem;
+                               margin-top: .25rem;
+                       }
+               }
+               
+               > .wcfImageViewerButtonClose {
+                       opacity: .6;
+                       position: absolute;
+                       right: 26px;
+                       top: 26px;
+                       
+                       transition: opacity linear .3s;
+                       
+                       &:hover {
+                               opacity: 1;
+                       }
+               }
+       }
+       
+       > div {
+               background-color: rgba(0, 0, 0, 1);
+               border-bottom: 1px solid @wcfImageViewerBorderColor;
+               border-top: 1px solid @wcfImageViewerBorderColor;
+               bottom: 100px;
+               top: 100px;
+               z-index: 401;
+               
+               -webkit-transition: top .3s, bottom .3s, border-color .3s;
+               
+               > img {
+                       opacity: 0;
+                       position: absolute;
+                       top: 50%;
+                       
+                       transition: opacity linear .75s;
+                       
+                       &.animateTransformation {
+                               -webkit-transition: left .3s, margin-top .3s, height .3s, width .3s, opacity .75s;
+                       }
+                       
+                       &.active {
+                               opacity: 1;
+                       }
+               }
+               
+               > ul {
+                       background-color: rgba(0, 0, 0, 1);
+                       border: 1px solid @wcfImageViewerBorderColor;
+                       border-bottom-width: 0;
+                       border-radius: 5px 5px 0 0;
+                       bottom: 0;
+                       display: inline-block;
+                       left: 50%;
+                       margin-left: -122px;
+                       opacity: .4;
+                       position: absolute;
+                       
+                       transition: opacity linear .5s;
+                       
+                       &:hover {
+                               opacity: 1;
+                       }
+                       
+                       > li {
+                               display: inline-block;
+                               opacity: .6;
+                               
+                               transition: opacity linear .5s;
+                               
+                               &.pointer > span.icon {
+                                       cursor: pointer;
+                               }
+                               
+                               &.active,
+                               &.pointer:hover {
+                                       opacity: 1;
+                               }
+                               
+                               &.wcfImageViewerSlideshowButtonToggle > span,
+                               &.wcfImageViewerSlideshowButtonEnlarge > span,
+                               &.wcfImageViewerSlideshowButtonFull > span {
+                                       font-size: 28px;
+                                       
+                                       &:before {
+                                               left: 2px;
+                                               position: relative;
+                                               top: 9px;
+                                       }
+                               }
+                               
+                               .wcfImageViewerSlideshowButtonEnlarge,
+                               .wcfImageViewerSlideshowButtonFull {
+                                       border-left: 1px solid @wcfImageViewerBorderColor;
+                                       box-sizing: border-box;
+                               }
+                               
+                               > span {
+                                       vertical-align: middle;
+                               }
+                       }
+               }
+       }
+       
+       > footer {
+               bottom: 0;
+               height: 100px;
+               padding: 10px;
+               
+               transition: bottom linear .3s;
+               
+               &:hover > div > ul > li > a {
+                       filter: none;
+                       -webkit-filter: none;
+               }
+               
+               > span {
+                       bottom: 0;
+                       font-size: 48px;
+                       padding-top: 26px;
+                       opacity: 0;
+                       position: absolute;
+                       top: 0;
+                       width: 30px;
+                       z-index: 2;
+                       
+                       transition: opacity linear .5s;
+                       
+                       &.pointer {
+                               opacity: .6;
+                               
+                               &:hover {
+                                       opacity: 1;
+                               }
+                       }
+                       
+                       &.wcfImageViewerButtonPrevious  {
+                               left: 5px;
+                       }
+                       
+                       &.wcfImageViewerButtonNext {
+                               right: 5px;
+                       }
+               }
+               
+               > div {
+                       height: 80px;
+                       margin: 0 35px;
+                       overflow: hidden;
+                       white-space: nowrap;
+                       
+                       > ul {
+                               font-size: 0;
+                               height: 80px;
+                               z-index: 1;
+                               
+                               transition: margin-left cubic-bezier(.5, 1.595, .56, .98) .75s;
+                               
+                               > li {
+                                       display: inline-block;
+                                       opacity: .6;
+                                       
+                                       transition: opacity linear .5s;
+                                       
+                                       &.active,
+                                       &.hover {
+                                               opacity: 1;
+                                       }
+                                       
+                                       &:not(:last-child) {
+                                               margin-right: 10px;
+                                       }
+                                       
+                                       &.active > a {
+                                               filter: none;
+                                               -webkit-filter: none;
+                                       }
+                                       
+                                       > a {
+                                               background-position: center;
+                                               background-repeat: no-repeat;
+                                               background-size: contain;
+                                               display: block;
+                                               height: 80px;
+                                               width: 80px;
+                                               
+                                               .grayscale;
+                                               -webkit-filter: grayscale(100%);
+                                               -webkit-transition: filter,-webkit-filter .5s linear;
+                                       }
+                               }
+                       }
+               }
+       }
+}