Merge branch '3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WCF.ImageViewer.js
CommitLineData
e368b357
AE
1"use strict";
2
e471f286 3/**
9a2a5bfc 4 * Enhanced image viewer for WCF.
e471f286 5 *
e471f286 6 * @author Alexander Ebert
c839bd49 7 * @copyright 2001-2018 WoltLab GmbH
e471f286
AE
8 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
9 */
10WCF.ImageViewer = Class.extend({
11 /**
9a2a5bfc
AE
12 * trigger element to mimic a slideshow button
13 * @var jQuery
14 */
15 _triggerElement: null,
16
17 /**
18 * Initializes the WCF.ImageViewer class.
e471f286
AE
19 */
20 init: function() {
9a2a5bfc
AE
21 this._triggerElement = $('<span class="wcfImageViewerTriggerElement" />').data('disableSlideshow', true).hide().appendTo(document.body);
22 this._triggerElement.wcfImageViewer({
23 enableSlideshow: 0,
24 imageSelector: '.jsImageViewerEnabled',
25 staticViewer: true
26 });
42d7d2cc
AE
27
28 WCF.DOMNodeInsertedHandler.addCallback('WCF.ImageViewer', $.proxy(this._domNodeInserted, this));
29 WCF.DOMNodeInsertedHandler.execute();
cd000ad8
AE
30 },
31
32 /**
33 * Executes actions upon DOMNodeInserted events.
34 */
35 _domNodeInserted: function() {
36 this._initImageSizeCheck();
9a2a5bfc 37 this._rebuildImageViewer();
cd000ad8
AE
38 },
39
40 /**
9a2a5bfc 41 * Rebuilds the image viewer.
cd000ad8 42 */
9a2a5bfc 43 _rebuildImageViewer: function() {
f9520da6
AE
44 var $links = $('a.jsImageViewer');
45 if ($links.length) {
9a2a5bfc 46 $links.removeClass('jsImageViewer').addClass('jsImageViewerEnabled').click($.proxy(this._click, this));
f9520da6 47 }
e471f286
AE
48 },
49
50 /**
9a2a5bfc
AE
51 * Handles click on an image with image viewer support.
52 *
53 * @param object event
e471f286 54 */
9a2a5bfc 55 _click: function(event) {
823532b0
AE
56 // ignore clicks while ctrl key is being pressed
57 if (event.ctrlKey) {
58 return;
59 }
60
9a2a5bfc
AE
61 event.preventDefault();
62 event.stopPropagation();
b391396f
MW
63 // skip if element is in a popover
64 if ($(event.currentTarget).closest('.popover').length) return;
9a2a5bfc
AE
65
66 this._triggerElement.wcfImageViewer('open', null, $(event.currentTarget).wcfIdentify());
0d69ce6e
MW
67 },
68
69 /**
70 * Initializes the image size check.
71 */
72 _initImageSizeCheck: function() {
7961df36
MW
73 $('.jsResizeImage').each($.proxy(function(index, image) {
74 if (image.complete) this._checkImageSize({ currentTarget: image });
75 }, this));
76
0d69ce6e
MW
77 $('.jsResizeImage').on('load', $.proxy(this._checkImageSize, this));
78 },
79
80 /**
81 * Checks the image size.
82 */
83 _checkImageSize: function(event) {
84 var $image = $(event.currentTarget);
38b3a9d7
AE
85 if (!$image.is(':visible')) {
86 $image.off('load');
87
88 return;
89 }
90
0d69ce6e 91 $image.removeClass('jsResizeImage');
0d69ce6e 92
309c94cf
AE
93 // check if image falls within the signature, in that case ignore it
94 if ($image.closest('.messageSignature').length) {
95 return;
96 }
97
719046d7
AE
98 // setting img { max-width: 100% } causes the image to fit within boundaries, but does not reveal the original dimenions
99 var $imageObject = new Image();
100 $imageObject.src = $image.attr('src');
101
510cccdd 102 var $maxWidth = $image.closest('div.messageText, div.messageTextPreview').width();
719046d7 103 if ($maxWidth < $imageObject.width) {
0d69ce6e 104 if (!$image.parents('a').length) {
d35f16da 105 $image.wrap('<a href="' + $image.attr('src') + '" class="jsImageViewerEnabled embeddedImageLink" />');
efdaa080 106 $image.parent().click($.proxy(this._click, this));
76b7aa02
MW
107
108 if ($image.css('float') == 'right') {
109 $image.parent().addClass('messageFloatObjectRight');
110 }
111 else if ($image.css('float') == 'left') {
112 $image.parent().addClass('messageFloatObjectLeft');
113 }
114 $image[0].style.removeProperty('float');
115 $image[0].style.removeProperty('margin');
0d69ce6e
MW
116 }
117 }
29657d14
MW
118 else {
119 $image.removeClass('embeddedAttachmentLink');
120 }
e471f286 121 }
dd5df48e
AE
122});
123
124/**
125 * Provides a focused image viewer for WCF.
126 *
127 * Usage:
128 * $('.triggerElement').wcfImageViewer({
129 * shiftBy: 5,
130 *
131 * enableSlideshow: 1,
132 * speed: 5,
133 *
134 * className: 'wcf\\data\\foo\\FooAction'
135 * });
136 */
137$.widget('ui.wcfImageViewer', {
138 /**
139 * active image index
140 * @var integer
141 */
142 _active: -1,
143
144 /**
145 * active image object id
146 * @var integer
147 */
148 _activeImage: null,
149
150 /**
151 * image viewer container object
152 * @var jQuery
153 */
154 _container: null,
155
156 /**
157 * initialization state
158 * @var boolean
159 */
160 _didInit: false,
161
d3d05fd9
AE
162 /**
163 * overrides slideshow settings unless explicitly enabled by user
164 * @var boolean
165 */
166 _disableSlideshow: false,
167
77404ea9
AE
168 /**
169 * event namespace used to distinguish event handlers using $.proxy
170 * @var string
171 */
172 _eventNamespace: '',
173
dd5df48e
AE
174 /**
175 * list of available images
176 * @var array<object>
177 */
178 _images: [ ],
179
fa7549e1
AE
180 /**
181 * true if image viewer uses the mobile-optimized UI
182 * @var boolean
183 */
184 _isMobile: false,
185
dd5df48e
AE
186 /**
187 * true if image viewer is open
188 * @var boolean
189 */
190 _isOpen: false,
191
192 /**
193 * number of total images
194 * @var integer
195 */
196 _items: -1,
197
198 /**
199 * maximum dimensions for enlarged view
200 * @var object<integer>
201 */
202 _maxDimensions: {
203 height: 0,
204 width: 0
205 },
206
207 /**
208 * action proxy object
209 * @var WCF.Action.Proxy
210 */
211 _proxy: null,
212
213 /**
214 * true if slideshow is currently running
215 * @var boolean
216 */
217 _slideshowEnabled: false,
218
219 /**
220 * visible width of thumbnail container
221 * @var integer
222 */
223 _thumbnailContainerWidth: 0,
224
225 /**
226 * right margin of a thumbnail
227 * @var integer
228 */
229 _thumbnailMarginRight: 0,
230
231 /**
232 * left offset of thumbnail list
233 * @var integer
234 */
235 _thumbnailOffset: 0,
236
237 /**
238 * outer width of a thumbnail (includes margin)
239 * @var integer
240 */
241 _thumbnailWidth: 0,
242
243 /**
244 * slideshow timer object
245 * @var WCF.PeriodicalExecuter
246 */
247 _timer: null,
248
249 /**
250 * list of interface elements
251 * @var object<jQuery>
252 */
253 _ui: {
254 buttonNext: null,
255 buttonPrevious: null,
256 header: null,
257 image: null,
258 imageContainer: null,
259 imageList: null,
260 slideshow: {
261 container: null,
262 enlarge: null,
263 next: null,
264 previous: null,
265 toggle: null
266 }
267 },
268
269 /**
270 * list of options parsed during init
271 * @var object<mixed>
272 */
273 options: {
274 // navigation
275 shiftBy: 5, // thumbnail slider control
276
277 // slideshow
278 enableSlideshow: 1,
279 speed: 5, // time in seconds
280
281 // ajax
fa7549e1
AE
282 className: '', // must be an instance of \wcf\data\IImageViewerAction
283
284 // alternative mode - static view
285 imageSelector: '',
286 staticViewer: false
dd5df48e
AE
287 },
288
289 /**
290 * Creates a new wcfImageViewer instance.
291 */
292 _create: function() {
293 this._active = -1;
294 this._activeImage = null;
295 this._container = null;
296 this._didInit = false;
d3d05fd9 297 this._disableSlideshow = (this.element.data('disableSlideshow'));
77404ea9 298 this._eventNamespace = this.element.wcfIdentify();
dd5df48e 299 this._images = [ ];
fa7549e1 300 this._isMobile = false;
dd5df48e
AE
301 this._isOpen = false;
302 this._items = -1;
303 this._maxDimensions = {
304 height: document.documentElement.clientHeight,
305 width: document.documentElement.clientWidth
306 };
307 this._proxy = new WCF.Action.Proxy({
308 success: $.proxy(this._success, this)
309 });
310 this._slideshowEnabled = false;
311 this._thumbnailContainerWidth = 0;
312 this._thumbnailMarginRight = 0;
313 this._thumbnailOffset = 0;
314 this._thumbnaiLWidth = 0;
315 this._timer = null;
316 this._ui = { };
317
318 this.element.click($.proxy(this.open, this));
ead50260
TD
319
320 window.addEventListener('popstate', (function(event) {
67bcb900 321 if (event.state != null && event.state.name === 'imageViewer') {
ead50260
TD
322 if (event.state.container === this._eventNamespace) {
323 this.open(event);
324 this.showImage(event.state.image);
325
326 return;
327 }
328 }
329
330 this.close(event);
331 }).bind(this));
dd5df48e
AE
332 },
333
334 /**
335 * Opens the image viewer.
336 *
c96906ac 337 * @param object event
fa7549e1 338 * @param string targetImageElementID
dd5df48e
AE
339 * @return boolean
340 */
fa7549e1 341 open: function(event, targetImageElementID) {
c96906ac
AE
342 if (event) event.preventDefault();
343
dd5df48e
AE
344 if (this._isOpen) {
345 return false;
346 }
347
ead50260
TD
348 // add history item for the image viewer
349 if (!event || event.type !== 'popstate') {
350 window.history.pushState({
351 name: 'imageViewer'
352 }, '', '');
353 }
354
fa7549e1
AE
355 if (this.options.staticViewer) {
356 var $images = this._getStaticImages();
357 this._initUI();
358 this._createThumbnails($images, true);
359 this._render(true, undefined, targetImageElementID);
360
361 this._isOpen = true;
362
363 WCF.System.DisableScrolling.disable();
9c31391d 364 WCF.System.DisableZoom.disable();
fa7549e1
AE
365
366 // switch to fullscreen mode on smartphones
367 if ($.browser.touch) {
368 setTimeout($.proxy(function() {
369 if (this._isMobile && !this._container.hasClass('maximized')) {
370 this._toggleView();
371 }
372 }, this), 500);
373 }
dd5df48e
AE
374 }
375 else {
fa7549e1
AE
376 if (this._images.length === 0) {
377 this._loadNextImages(true);
dd5df48e 378 }
fa7549e1
AE
379 else {
380 this._render(false, this.element.data('targetImageID'));
381
382 if (this._items > 1 && this._slideshowEnabled) {
383 this.startSlideshow();
384 }
385
386 this._isOpen = true;
1a6e8c52 387
fa7549e1 388 WCF.System.DisableScrolling.disable();
9c31391d 389 WCF.System.DisableZoom.disable();
fa7549e1 390 }
dd5df48e
AE
391 }
392
2bb74999 393 this._bindListener();
77404ea9 394
0c35c3e7
AE
395 document.documentElement.classList.add('pageOverlayActive');
396
dd5df48e
AE
397 return true;
398 },
399
400 /**
401 * Closes the image viewer.
402 *
403 * @return boolean
404 */
63c497a3 405 close: function(event) {
c96906ac
AE
406 if (event) event.preventDefault();
407
ead50260
TD
408 // clear history item of the image viewer
409 if (!event || event.type !== 'popstate') {
410 window.history.back();
411 return;
412 }
413
dd5df48e
AE
414 if (!this._isOpen) {
415 return false;
416 }
417
418 this._container.removeClass('open');
419 if (this._timer !== null) {
420 this._timer.stop();
421 }
422
2bb74999 423 this._unbindListener();
77404ea9 424
dd5df48e
AE
425 this._isOpen = false;
426
ff34ee1f 427 WCF.System.DisableScrolling.enable();
9c31391d 428 WCF.System.DisableZoom.enable();
ff34ee1f 429
0c35c3e7
AE
430 document.documentElement.classList.remove('pageOverlayActive');
431
dd5df48e
AE
432 return true;
433 },
434
435 /**
436 * Enables the slideshow.
437 *
438 * @return boolean
439 */
440 startSlideshow: function() {
d3d05fd9 441 if (this._disableSlideshow || this._slideshowEnabled) {
dd5df48e
AE
442 return false;
443 }
444
ff34ee1f
AE
445 if (this._timer === null) {
446 this._timer = new WCF.PeriodicalExecuter($.proxy(function() {
447 var $index = this._active + 1;
448 if ($index == this._items) {
449 $index = 0;
450 }
451
452 this.showImage($index);
453 }, this), this.options.speed * 1000);
454 }
455 else {
456 this._timer.resume();
457 }
dd5df48e
AE
458
459 this._slideshowEnabled = true;
460
ca8bfa53 461 this._ui.slideshow.toggle.children('span').removeClass('fa-play').addClass('fa-pause');
dd5df48e
AE
462
463 return true;
464 },
465
466 /**
467 * Disables the slideshow.
468 *
ff34ee1f 469 * @param boolean disableSlideshow
dd5df48e
AE
470 * @return boolean
471 */
ff34ee1f 472 stopSlideshow: function(disableSlideshow) {
dd5df48e
AE
473 if (!this._slideshowEnabled) {
474 return false;
475 }
476
477 this._timer.stop();
4c6b5841 478 if (disableSlideshow) {
ca8bfa53 479 this._ui.slideshow.toggle.children('span').removeClass('fa-pause').addClass('fa-play');
4c6b5841 480 }
dd5df48e
AE
481
482 this._slideshowEnabled = false;
483
484 return true;
485 },
486
2bb74999
AE
487 /**
488 * Binds event listeners.
489 */
490 _bindListener: function() {
491 $(document).on('keydown.' + this._eventNamespace, $.proxy(this._keyDown, this));
492 $(window).on('resize.' + this._eventNamespace, $.proxy(this._renderImage, this));
493 },
494
495 /**
496 * Unbinds event listeners.
497 */
498 _unbindListener: function() {
499 $(document).off('keydown.' + this._eventNamespace);
500 $(window).off('resize.' + this._eventNamespace);
501 },
502
77404ea9
AE
503 /**
504 * Closes the slideshow on escape.
505 *
506 * @param object event
507 * @return boolean
508 */
509 _keyDown: function(event) {
510 switch (event.which) {
511 // close slideshow
512 case $.ui.keyCode.ESCAPE:
513 this.close();
514 break;
515
516 // show previous image
517 case $.ui.keyCode.LEFT:
518 this._previousImage();
519 break;
520
521 // show next image
522 case $.ui.keyCode.RIGHT:
523 this._nextImage();
524 break;
525
526 // enable fullscreen mode
527 case $.ui.keyCode.UP:
528 if (!this._container.hasClass('maximized')) {
529 this._toggleView();
530 }
531 break;
532
533 // disable fullscreen mode
534 case $.ui.keyCode.DOWN:
535 if (this._container.hasClass('maximized')) {
536 this._toggleView();
537 }
538 break;
539
540 // jump to image page or full version
541 case $.ui.keyCode.ENTER:
3166e052 542 var $link = this._ui.header.find('h1 > a');
77404ea9
AE
543 if ($link.length == 1) {
544 // forward to image page
545 window.location = $link.prop('href');
546 }
547 else {
548 // forward to full version
549 this._ui.slideshow.full.trigger('click');
550 }
551 break;
552
553 // toggle play/pause (80 = [p])
554 case 80:
555 this._ui.slideshow.toggle.trigger('click');
556 break;
c16cb4ba
AE
557
558 default:
559 return true;
560 break;
77404ea9
AE
561 }
562
563 return false;
564 },
565
dd5df48e
AE
566 /**
567 * Renders the image viewer UI.
568 *
569 * @param boolean initialized
d3d05fd9 570 * @param integer targetImageID
fa7549e1 571 * @param string targetImageElementID
dd5df48e 572 */
fa7549e1 573 _render: function(initialized, targetImageID, targetImageElementID) {
dd5df48e 574 this._container.addClass('open');
04e1c42a 575
c96906ac 576 var $thumbnail = null;
dd5df48e 577 if (initialized) {
c96906ac 578 $thumbnail = this._ui.imageList.children('li:eq(0)');
dd5df48e
AE
579 this._thumbnailMarginRight = parseInt($thumbnail.css('marginRight').replace(/px$/, '')) || 0;
580 this._thumbnailWidth = $thumbnail.outerWidth(true);
581 this._thumbnailContainerWidth = this._ui.imageList.parent().innerWidth();
582
fa7549e1 583 if (this._items > 1 && this.options.enableSlideshow && !targetImageID && !targetImageElementID) {
dd5df48e
AE
584 this.startSlideshow();
585 }
586 }
587
c96906ac
AE
588 if (targetImageID) {
589 this._ui.imageList.children('li').each($.proxy(function(index, item) {
590 var $item = $(item);
591 if ($item.data('objectID') == targetImageID) {
592 $item.trigger('click');
593 this.moveToImage($item.data('index'));
594
595 return false;
596 }
597 }, this));
598 }
e9b1b686 599 else if (targetImageElementID) {
fa7549e1
AE
600 var $i = 0;
601 $(this.options.imageSelector).each(function(index, element) {
602 if ($(element).wcfIdentify() == targetImageElementID) {
603 $i = index;
604
605 return false;
606 }
607 });
608
609 var $item = this._ui.imageList.children('li:eq(' + $i + ')');
7c76da5d
AE
610
611 // check if currently active image does not exist anymore
612 if (this._active !== -1) {
613 var $clear = false;
614 if (this._active != $item.data('index')) {
615 $clear = true;
616 }
617
618 if (this._ui.images[this._activeImage].prop('src') != this._images[this._active].image.url) {
619 $clear = true;
620 }
621
622 if ($clear) {
623 // reset active state
624 this._active = -1;
625 }
626 }
627
fa7549e1
AE
628 $item.trigger('click');
629 this.moveToImage($item.data('index'));
630 }
c96906ac
AE
631 else if ($thumbnail !== null) {
632 $thumbnail.trigger('click');
633 }
634
dd5df48e
AE
635 this._toggleButtons();
636
637 // check if there is enough space to load more thumbnails
638 this._preload();
639 },
640
641 /**
642 * Attempts to load the next images.
643 */
644 _preload: function() {
645 if (this._images.length < this._items) {
646 var $thumbnailsWidth = this._images.length * this._thumbnailWidth;
647 if ($thumbnailsWidth - this._thumbnailOffset < this._thumbnailContainerWidth) {
d3d05fd9 648 this._loadNextImages(false);
dd5df48e
AE
649 }
650 }
651 },
652
653 /**
654 * Displays image on thumbnail click.
655 *
656 * @param object event
657 */
658 _showImage: function(event) {
ff34ee1f 659 this.showImage($(event.currentTarget).data('index'), true);
dd5df48e
AE
660 },
661
662 /**
663 * Displays an image by index.
664 *
665 * @param integer index
ff34ee1f 666 * @param boolean disableSlideshow
dd5df48e
AE
667 * @return boolean
668 */
ff34ee1f 669 showImage: function(index, disableSlideshow) {
dd5df48e
AE
670 if (this._active == index) {
671 return false;
672 }
673
ff34ee1f
AE
674 this.stopSlideshow(disableSlideshow || false);
675
dd5df48e
AE
676 // reset active marking
677 if (this._active != -1) {
678 this._images[this._active].listItem.removeClass('active');
679 }
680
681 this._active = index;
ead50260
TD
682
683 // store latest image in history entry
684 window.history.replaceState({
685 name: 'imageViewer',
686 container: this._eventNamespace,
687 image: this._active
688 }, '', '');
689
dd5df48e
AE
690 var $image = this._images[index];
691
692 this._ui.imageList.children('li').removeClass('active');
693 $image.listItem.addClass('active');
694
695 var $dimensions = this._ui.imageContainer.getDimensions('inner');
ff34ee1f 696 var $newImageIndex = (this._activeImage ? 0 : 1);
dd5df48e 697
ff34ee1f
AE
698 if (this._activeImage !== null) {
699 this._ui.images[this._activeImage].removeClass('active');
700 }
701
702 this._activeImage = $newImageIndex;
ff34ee1f 703 var $currentActiveImage = this._active;
56d3c2de 704 this._ui.imageContainer.addClass('loading');
719046d7
AE
705 this._ui.images[$newImageIndex].off('load').prop('src', 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='); // 1x1 pixel transparent gif
706 this._ui.images[$newImageIndex].on('load', $.proxy(function() {
ff34ee1f 707 this._imageOnLoad($currentActiveImage, $newImageIndex);
56d3c2de
AE
708 }, this));
709
710 this._renderImage($newImageIndex, $image, $dimensions);
dd5df48e
AE
711
712 // user
fa7549e1
AE
713 if (!this.options.staticViewer) {
714 var $link = this._ui.header.find('> div > a').prop('href', $image.user.link).prop('title', $image.user.username);
715 $link.children('img').prop('src', $image.user.avatarURL);
716 }
dd5df48e
AE
717
718 // meta data
719 var $title = WCF.String.escapeHTML($image.image.title);
1689207b 720 if ($image.image.link) $title = '<a href="' + $image.image.link + '">' + $title + '</a>';
3166e052 721 this._ui.header.find('h1').html($title);
dd5df48e 722
fa7549e1
AE
723 if (!this.options.staticViewer) {
724 var $seriesTitle = ($image.series && $image.series.title ? WCF.String.escapeHTML($image.series.title) : '');
725 if ($image.series.link) $seriesTitle = '<a href="' + $image.series.link + '">' + $seriesTitle + '</a>';
3166e052 726 this._ui.header.find('h2').html($seriesTitle);
fa7549e1 727 }
dd5df48e 728
3166e052 729 this._ui.header.find('h3').text(WCF.Language.get('wcf.imageViewer.seriesIndex').replace(/{x}/, $image.listItem.data('index') + 1).replace(/{y}/, this._items));
dd5df48e
AE
730
731 this._ui.slideshow.full.data('link', ($image.image.fullURL ? $image.image.fullURL : $image.image.url));
732
733 this.moveToImage($image.listItem.data('index'));
734
735 this._toggleButtons();
736
737 return true;
738 },
739
785dac42
AE
740 /**
741 * Callback function for the image 'load' event.
742 *
743 * @param integer currentActiveImage
744 * @param integer activeImageIndex
745 */
ff34ee1f
AE
746 _imageOnLoad: function(currentActiveImage, activeImageIndex) {
747 // image did not load in time, ignore
748 if (currentActiveImage != this._active) {
749 return;
750 }
751
752 this._ui.imageContainer.removeClass('loading');
753 this._ui.images[activeImageIndex].addClass('active');
4c6b5841 754
fa7549e1
AE
755 if (this.options.staticViewer) {
756 this._renderImage(activeImageIndex, null);
757 }
758
4c6b5841 759 this.startSlideshow();
ff34ee1f
AE
760 },
761
dd5df48e
AE
762 /**
763 * Renders target image, leaving 'imageData' undefined will invoke the rendering process for the currently active image.
764 *
765 * @param integer targetIndex
766 * @param object imageData
767 * @param object containerDimensions
768 */
769 _renderImage: function(targetIndex, imageData, containerDimensions) {
c3bae4f8 770 var $checkForComplete = true;
dd5df48e
AE
771 if (!imageData) {
772 targetIndex = this._activeImage;
773 imageData = this._images[this._active];
774
775 containerDimensions = {
fa7549e1 776 height: $(window).height() - (this._container.hasClass('maximized') || this._container.hasClass('wcfImageViewerMobile') ? 0 : 200),
dd5df48e
AE
777 width: this._ui.imageContainer.innerWidth()
778 };
c3bae4f8
AE
779
780 $checkForComplete = false;
dd5df48e
AE
781 }
782
783 // simulate padding
784 containerDimensions.height -= 22;
785 containerDimensions.width -= 20;
786
ce49a649
AE
787 var $image = this._ui.images[targetIndex];
788 if ($image.prop('src') !== imageData.image.url) {
789 // assigning the same exact source again breaks Internet Explorer 10
790 $image.prop('src', imageData.image.url);
791 }
792
c3bae4f8
AE
793 if ($checkForComplete && $image[0].complete) {
794 $image.trigger('load');
795 }
fa7549e1 796
edf4ac39 797 if (this.options.staticViewer && !imageData.image.height && $image[0].complete) {
92fbf517
F
798 // Firefox and Safari returns bogus values if attempting to read the real dimensions
799 if ($.browser.mozilla || $.browser.safari) {
bcc5af2b
AE
800 var $img = new Image();
801 $img.src = imageData.image.url;
802
2f638c83
AE
803 imageData.image.height = $img.height || $image[0].naturalHeight;
804 imageData.image.width = $img.width || $image[0].naturalWidth;
bcc5af2b
AE
805 }
806 else {
807 $image.css({
808 height: 'auto',
809 width: 'auto'
810 });
811
812 imageData.image.height = $image[0].height;
813 imageData.image.width = $image[0].width;
814 }
fa7549e1 815 }
dd5df48e
AE
816
817 var $height = imageData.image.height;
818 var $width = imageData.image.width;
819 var $ratio = 0.0;
820
821 // check if image exceeds dimensions on the Y axis
822 if ($height > containerDimensions.height) {
823 $ratio = containerDimensions.height / $height;
824 $height = containerDimensions.height;
825 $width = Math.floor($width * $ratio);
826 }
827
828 // check if image exceeds dimensions on the X axis
829 if ($width > containerDimensions.width) {
830 $ratio = containerDimensions.width / $width;
831 $width = containerDimensions.width;
832 $height = Math.floor($height * $ratio);
833 }
834
835 var $left = Math.floor((containerDimensions.width - $width) / 2);
dd5df48e
AE
836 this._ui.images[targetIndex].css({
837 height: $height + 'px',
785dac42 838 left: ($left + 10) + 'px',
dd5df48e
AE
839 marginTop: (Math.round($height / 2) * -1) + 'px',
840 width: $width + 'px'
841 });
842 },
843
844 /**
1615fc2e 845 * Initializes the user interface.
dd5df48e
AE
846 *
847 * @return boolean
848 */
849 _initUI: function() {
850 if (this._didInit) {
851 return false;
852 }
853
854 this._didInit = true;
855
fa7549e1 856 this._container = $('<div class="wcfImageViewer' + (this.options.staticViewer ? ' wcfImageViewerStatic' : '') + '" />').appendTo(document.body);
98df8a61 857 var $imageContainer = $('<div><img /><img /></div>').appendTo(this._container);
ca8bfa53 858 var $imageList = $('<footer><span class="wcfImageViewerButtonPrevious icon fa-angle-double-left" /><div><ul /></div><span class="wcfImageViewerButtonNext icon fa-angle-double-right" /></footer>').appendTo(this._container);
dd5df48e 859 var $slideshowContainer = $('<ul />').appendTo($imageContainer);
ca8bfa53
MS
860 var $slideshowButtonPrevious = $('<li class="wcfImageViewerSlideshowButtonPrevious"><span class="icon icon48 fa-angle-left" /></li>').appendTo($slideshowContainer);
861 var $slideshowButtonToggle = $('<li class="wcfImageViewerSlideshowButtonToggle pointer"><span class="icon icon48 fa-play" /></li>').appendTo($slideshowContainer);
862 var $slideshowButtonNext = $('<li class="wcfImageViewerSlideshowButtonNext"><span class="icon icon48 fa-angle-right" /></li>').appendTo($slideshowContainer);
863 var $slideshowButtonEnlarge = $('<li class="wcfImageViewerSlideshowButtonEnlarge pointer jsTooltip" title="' + WCF.Language.get('wcf.imageViewer.button.enlarge') + '"><span class="icon icon48 fa-expand" /></li>').appendTo($slideshowContainer);
864 var $slideshowButtonFull = $('<li class="wcfImageViewerSlideshowButtonFull pointer jsTooltip" title="' + WCF.Language.get('wcf.imageViewer.button.full') + '"><span class="icon icon48 fa-external-link" /></li>').appendTo($slideshowContainer);
dd5df48e
AE
865
866 this._ui = {
867 buttonNext: $imageList.children('span.wcfImageViewerButtonNext'),
868 buttonPrevious: $imageList.children('span.wcfImageViewerButtonPrevious'),
3166e052 869 header: $('<header><div' + (this.options.staticViewer ? '>' : ' class="box64"><a class="jsTooltip"><img /></a>' ) + '<div><h1 /><h2 /><h3 /></div></div></header>').appendTo(this._container),
dd5df48e
AE
870 imageContainer: $imageContainer,
871 images: [
872 $imageContainer.children('img:eq(0)').on('webkitTransitionEnd transitionend msTransitionEnd oTransitionEnd', function() { $(this).removeClass('animateTransformation'); }),
873 $imageContainer.children('img:eq(1)').on('webkitTransitionEnd transitionend msTransitionEnd oTransitionEnd', function() { $(this).removeClass('animateTransformation'); })
874 ],
875 imageList: $imageList.find('> div > ul'),
876 slideshow: {
877 container: $slideshowContainer,
878 enlarge: $slideshowButtonEnlarge,
879 full: $slideshowButtonFull,
880 next: $slideshowButtonNext,
881 previous: $slideshowButtonPrevious,
882 toggle: $slideshowButtonToggle
883 }
884 };
885
886 this._ui.buttonNext.click($.proxy(this._next, this));
887 this._ui.buttonPrevious.click($.proxy(this._previous, this));
888
889 $slideshowButtonNext.click($.proxy(this._nextImage, this));
890 $slideshowButtonPrevious.click($.proxy(this._previousImage, this));
891 $slideshowButtonEnlarge.click($.proxy(this._toggleView, this));
892 $slideshowButtonToggle.click($.proxy(function() {
dfa4a65b
AE
893 if (this._items < 2) {
894 return;
895 }
896
dd5df48e 897 if (this._slideshowEnabled) {
ff34ee1f 898 this.stopSlideshow(true);
dd5df48e
AE
899 }
900 else {
d3d05fd9 901 this._disableSlideshow = false;
dd5df48e
AE
902 this.startSlideshow();
903 }
904 }, this));
905 $slideshowButtonFull.click(function(event) { window.location = $(event.currentTarget).data('link'); });
906
907 // close button
ca8bfa53 908 $('<span class="wcfImageViewerButtonClose icon icon48 fa-times pointer jsTooltip" title="' + WCF.Language.get('wcf.global.button.close') + '" />').appendTo(this._ui.header).click($.proxy(this.close, this));
dd5df48e 909
3aa68c56
AE
910 if (!$.browser.mobile) {
911 // clicking on the inner container should close the dialog, but it should not be available on mobile due to
912 // the lack of precision causing accidental closing, the close button is big enough and easily reachable
913 $imageContainer.click((function(event) {
914 if (event.target === $imageContainer[0]) {
915 this.close();
916 }
917 }).bind(this));
918 }
919
fa7549e1
AE
920 WCF.DOMNodeInsertedHandler.execute();
921
431e4cb4 922 enquire.register('(max-width: 767px)', {
fa7549e1
AE
923 match: $.proxy(this._enableMobileView, this),
924 unmatch: $.proxy(this._disableMobileView, this)
925 });
926
dd5df48e
AE
927 return true;
928 },
929
fa7549e1
AE
930 /**
931 * Enables the mobile-optimized UI.
932 */
933 _enableMobileView: function() {
934 this._container.addClass('wcfImageViewerMobile');
935
936 var self = this;
937 this._ui.imageContainer.swipe({
938 swipeLeft: function(event) {
939 if (self._container.hasClass('maximized')) {
940 self._nextImage(event);
941 }
942 },
943 swipeRight: function(event) {
944 if (self._container.hasClass('maximized')) {
945 self._previousImage(event);
946 }
947 },
948 tap: function(event, element) {
949 // tap fires before click, prevent conflicts
950 switch (element.tagName) {
951 case 'DIV':
952 case 'IMG':
953 self._toggleView();
954 break;
955 }
956 }
957 });
958
959 this._isMobile = true;
960 },
961
962 /**
963 * Disables the mobile-optimized UI.
964 */
965 _disableMobileView: function() {
966 this._container.removeClass('wcfImageViewerMobile');
2a7e7ab9 967 this._ui.imageContainer.swipe('destroy');
fa7549e1
AE
968
969 this._isMobile = false;
970 },
971
dd5df48e
AE
972 /**
973 * Toggles between normal and fullscreen view.
974 */
975 _toggleView: function() {
976 this._ui.images[this._activeImage].addClass('animateTransformation');
977 this._container.toggleClass('maximized');
ca8bfa53 978 this._ui.slideshow.enlarge.toggleClass('active').children('span').toggleClass('fa-expand').toggleClass('fa-compress');
dd5df48e
AE
979
980 this._renderImage(null, undefined, null);
981 },
982
983 /**
984 * Shifts the thumbnail list.
985 *
986 * @param object event
987 * @param integer shiftBy
988 */
989 _next: function(event, shiftBy) {
990 if (this._ui.buttonNext.hasClass('pointer')) {
991 if (shiftBy == undefined) {
ff34ee1f 992 this.stopSlideshow(true);
dd5df48e
AE
993 }
994
995 var $maximumOffset = Math.max((this._items * this._thumbnailWidth) - this._thumbnailContainerWidth - this._thumbnailMarginRight, 0);
996 this._thumbnailOffset = Math.min(this._thumbnailOffset + (this._thumbnailWidth * (shiftBy ? shiftBy : this.options.shiftBy)), $maximumOffset);
997 this._ui.imageList.css('marginLeft', (this._thumbnailOffset * -1));
998 }
999
1000 this._preload();
1001
1002 this._toggleButtons();
1003 },
1004
1005 /**
1006 * Unshifts the thumbnail list.
1007 *
1008 * @param object event
1009 * @param integer shiftBy
1010 */
1011 _previous: function(event, unshiftBy) {
1012 if (this._ui.buttonPrevious.hasClass('pointer')) {
1013 if (unshiftBy == undefined) {
ff34ee1f 1014 this.stopSlideshow(true);
dd5df48e
AE
1015 }
1016
1017 this._thumbnailOffset = Math.max(this._thumbnailOffset - (this._thumbnailWidth * (unshiftBy ? unshiftBy : this.options.shiftBy)), 0);
1018 this._ui.imageList.css('marginLeft', (this._thumbnailOffset * -1));
1019 }
1020
1021 this._toggleButtons();
1022 },
1023
1024 /**
1025 * Displays the next image.
1026 *
1027 * @param object event
1028 */
1029 _nextImage: function(event) {
1030 if (this._ui.slideshow.next.hasClass('pointer')) {
77404ea9
AE
1031 this._disableSlideshow = true;
1032
ff34ee1f 1033 this.stopSlideshow(true);
dd5df48e 1034 this.showImage(this._active + 1);
fa7549e1
AE
1035
1036 if (event) {
1037 event.preventDefault();
1038 event.stopPropagation();
1039 }
dd5df48e
AE
1040 }
1041 },
1042
1043 /**
1044 * Displays the previous image.
1045 *
1046 * @param object event
1047 */
1048 _previousImage: function(event) {
1049 if (this._ui.slideshow.previous.hasClass('pointer')) {
77404ea9
AE
1050 this._disableSlideshow = true;
1051
ff34ee1f 1052 this.stopSlideshow(true);
dd5df48e 1053 this.showImage(this._active - 1);
fa7549e1
AE
1054
1055 if (event) {
1056 event.preventDefault();
1057 event.stopPropagation();
1058 }
dd5df48e
AE
1059 }
1060 },
1061
1062 /**
1063 * Moves thumbnail list to target thumbnail.
1064 *
1065 * @param integer seriesIndex
1066 */
1067 moveToImage: function(seriesIndex) {
1068 // calculate start and end of thumbnail
1069 var $start = (seriesIndex - 3) * this._thumbnailWidth;
1070 var $end = $start + (this._thumbnailWidth * 5);
1071
1072 // calculate visible offsets
1073 var $left = this._thumbnailOffset;
1074 var $right = this._thumbnailOffset + this._thumbnailContainerWidth;
1075
1076 // check if thumbnail is within boundaries
1077 var $shouldMove = false;
1078 if ($start < $left || $end > $right) {
1079 $shouldMove = true;
1080 }
1081
1082 // try to shift until the thumbnail itself and the next/previous 2 thumbnails are visible
1083 if ($shouldMove) {
1084 var $shiftBy = 0;
1085
1086 // unshift
1087 if ($start < $left) {
1088 while ($start < $left) {
1089 $shiftBy++;
1090 $left -= this._thumbnailWidth;
1091 }
1092
1093 this._previous(null, $shiftBy);
1094 }
1095 else {
1096 // shift
1097 while ($end > $right) {
1098 $shiftBy++;
1099 $right += this._thumbnailWidth;
1100 }
1101
1102 this._next(null, $shiftBy);
1103 }
1104 }
1105 },
1106
1107 /**
1108 * Toggles control buttons.
1109 */
1110 _toggleButtons: function() {
1111 // button 'previous'
1112 if (this._thumbnailOffset > 0) {
1113 this._ui.buttonPrevious.addClass('pointer');
1114 }
1115 else {
1116 this._ui.buttonPrevious.removeClass('pointer');
1117 }
1118
1119 // button 'next'
1120 var $maximumOffset = (this._images.length * this._thumbnailWidth) - this._thumbnailContainerWidth - this._thumbnailMarginRight;
1121 if (this._thumbnailOffset >= $maximumOffset) {
1122 this._ui.buttonNext.removeClass('pointer');
1123 }
1124 else {
1125 this._ui.buttonNext.addClass('pointer');
1126 }
1127
1128 // slideshow controls
1129 if (this._active > 0) {
1130 this._ui.slideshow.previous.addClass('pointer');
1131 }
1132 else {
1133 this._ui.slideshow.previous.removeClass('pointer');
1134 }
1135
1136 if (this._active + 1 < this._images.length) {
1137 this._ui.slideshow.next.addClass('pointer');
1138 }
1139 else {
1140 this._ui.slideshow.next.removeClass('pointer');
1141 }
dfa4a65b
AE
1142
1143 if (this._items < 2) {
1144 this._ui.slideshow.toggle.removeClass('pointer');
1145 }
c3bae4f8
AE
1146 else {
1147 this._ui.slideshow.toggle.addClass('pointer');
1148 }
dd5df48e
AE
1149 },
1150
1151 /**
1152 * Inserts thumbnails.
1153 *
1154 * @param array<object> images
1155 */
1156 _createThumbnails: function(images) {
fa7549e1
AE
1157 if (this.options.staticViewer) {
1158 this._images = [ ];
1159 this._ui.imageList.empty();
1160 }
1161
dd5df48e
AE
1162 for (var $i = 0, $length = images.length; $i < $length; $i++) {
1163 var $image = images[$i];
1164
ff34ee1f 1165 var $listItem = $('<li class="loading pointer"><img src="' + $image.thumbnail.url + '" /></li>').appendTo(this._ui.imageList);
d3d05fd9 1166 $listItem.data('index', this._images.length).data('objectID', $image.objectID).click($.proxy(this._showImage, this));
ff34ee1f
AE
1167 var $img = $listItem.children('img');
1168 if ($img.get(0).complete) {
1169 // thumbnail is read from cache
1170 $listItem.removeClass('loading');
fa7549e1
AE
1171
1172 // fix dimensions
1173 if (this.options.staticViewer) {
1174 this._fixThumbnailDimensions($img);
1175 }
ff34ee1f
AE
1176 }
1177 else {
fa7549e1
AE
1178 var self = this;
1179 $img.on('load', function() {
1180 var $img = $(this);
1181 $img.parent().removeClass('loading');
1182
1183 if (self.options.staticViewer) {
1184 self._fixThumbnailDimensions($img);
1185 }
1186 });
ff34ee1f 1187 }
dd5df48e
AE
1188
1189 $image.listItem = $listItem;
1190 this._images.push($image);
1191 }
1192 },
1193
fa7549e1
AE
1194 /**
1195 * Fixes thumbnail dimensions within static mode.
1196 *
1197 * @param jQuery image
1198 */
1199 _fixThumbnailDimensions: function(image) {
1200 var $image = new Image();
1201 $image.src = image.prop('src');
1202
1203 var $height = $image.height;
1204 var $width = $image.width;
1205
1206 // quadratic, scale to 80x80
1207 if ($height == $width) {
1208 $height = $width = 80;
1209 }
1210 else if ($height < $width) {
1211 // landscape, use width as reference
1212 var $scale = 80 / $width;
1213 $width = 80;
1214 $height *= $scale;
1215 }
1216 else {
1217 // portrait, use height as reference
1218 var $scale = 80 / $height;
1219 $height = 80;
1220 $width *= $scale;
1221 }
1222
1223 image.css({
1224 height: $height + 'px',
1225 width: $width + 'px'
1226 });
1227 },
1228
dd5df48e
AE
1229 /**
1230 * Loads the next images via AJAX.
d3d05fd9
AE
1231 *
1232 * @param boolean init
dd5df48e 1233 */
d3d05fd9 1234 _loadNextImages: function(init) {
dd5df48e
AE
1235 this._proxy.setOption('data', {
1236 actionName: 'loadNextImages',
1237 className: this.options.className,
1238 interfaceName: 'wcf\\data\\IImageViewerAction',
1239 objectIDs: [ this.element.data('objectID') ],
1240 parameters: {
1241 maximumHeight: this._maxDimensions.height,
1242 maximumWidth: this._maxDimensions.width,
d3d05fd9
AE
1243 offset: this._images.length,
1244 targetImageID: (init && this.element.data('targetImageID') ? this.element.data('targetImageID') : 0)
dd5df48e
AE
1245 }
1246 });
ff34ee1f 1247 this._proxy.setOption('showLoadingOverlay', false);
dd5df48e
AE
1248 this._proxy.sendRequest();
1249 },
1250
fa7549e1
AE
1251 /**
1252 * Builds the list of static images and returns it.
1253 *
1254 * @return array<object>
1255 */
1256 _getStaticImages: function() {
1257 var $images = [ ];
1258
1259 $(this.options.imageSelector).each(function(index, link) {
1260 var $link = $(link);
80d3787b 1261 var $thumbnail = $link.find('> img, .attachmentThumbnailImage > img').first();
bc8a4ed8
AE
1262 if (!$thumbnail.length) {
1263 $thumbnail = $link.parentsUntil('.formAttachmentList').last().find('.attachmentTinyThumbnail');
1264 }
fa7549e1
AE
1265
1266 $images.push({
1267 image: {
e934d809 1268 fullURL: $thumbnail.data('source') ? $thumbnail.data('source').replace(/\\\//g, '/') : $link.prop('href'),
fa7549e1
AE
1269 link: '',
1270 title: $link.prop('title'),
7eff88e3 1271 url: $link.prop('href')
fa7549e1
AE
1272 },
1273 series: null,
1274 thumbnail: {
bc8a4ed8 1275 url: $thumbnail.prop('src')
fa7549e1
AE
1276 },
1277 user: null
1278 });
1279 });
1280
1281 this._items = $images.length;
1282
1283 return $images;
1284 },
1285
dd5df48e
AE
1286 /**
1287 * Handles successful AJAX requests.
1288 *
1289 * @param object data
1290 * @param string textStatus
1291 * @param jQuery jqXHR
1292 */
1293 _success: function(data, textStatus, jqXHR) {
1294 if (data.returnValues.items) {
1295 this._items = data.returnValues.items;
1296 }
1297
1298 var $initialized = this._initUI();
1299
1300 this._createThumbnails(data.returnValues.images);
1301
d3d05fd9
AE
1302 var $targetImageID = (data.returnValues.targetImageID ? data.returnValues.targetImageID : 0);
1303 this._render($initialized, $targetImageID);
7cf89d4c
MW
1304
1305 if (!this._isOpen) {
1306 this._isOpen = true;
1a6e8c52 1307
7cf89d4c 1308 WCF.System.DisableScrolling.disable();
9c31391d 1309 WCF.System.DisableZoom.disable();
7cf89d4c 1310 }
dd5df48e
AE
1311 }
1312});