Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.page.gallery.slideshow.js
blobf41292e107f6850d2203604fed56f17ad4af9275
1 ( function () {
2         /**
3          * @class mw.GallerySlideshow
4          * @classdesc Interface controls for the slideshow gallery. To use, first load
5          * the `mediawiki.page.gallery.slideshow` ResourceLoader module.
6          * @uses mw.Title
7          * @uses mw.Api
8          *
9          * @constructor
10          * @description Encapsulates the user interface of the slideshow galleries.
11          * An object is instantiated for each `.mw-gallery-slideshow` element.
12          * @param {jQuery} gallery The `<ul>` element of the gallery.
13          */
14         mw.GallerySlideshow = function ( gallery ) {
15                 // Properties
16                 /**
17                  * The `<ul>` element of the gallery.
18                  *
19                  * @type {jQuery}
20                  */
21                 this.$gallery = $( gallery );
22                 /**
23                  * The `<li>` that has the gallery caption.
24                  *
25                  * @type {jQuery}
26                  */
27                 this.$galleryCaption = this.$gallery.find( '.gallerycaption' );
28                 /**
29                  * Selection of `<li>` elements that have thumbnails.
30                  *
31                  * @type {jQuery}
32                  */
33                 this.$galleryBox = this.$gallery.find( '.gallerybox' );
34                 /**
35                  * The `<li>` element of the current image.
36                  *
37                  * @type {jQuery}
38                  */
39                 this.$currentImage = null;
40                 /**
41                  * A key value pair of thumbnail URLs and image info.
42                  *
43                  * @type {Object.<string,jQuery.Promise>}
44                  */
45                 this.imageInfoCache = {};
47                 /* Properties */
48                 /**
49                  * The `<li>` element that contains the carousel.
50                  *
51                  * @name $carousel
52                  * @memberof mw.GallerySlideshow.prototype
53                  * @type {jQuery|null}
54                  */
56                 /**
57                  * The `<div>` element that contains the interface buttons.
58                  *
59                  * @name $interface
60                  * @memberof mw.GallerySlideshow.prototype
61                  * @type {jQuery}
62                  */
64                 /**
65                  * The `<img>` element that'll display the current image.
66                  *
67                  * @name $img
68                  * @memberof mw.GallerySlideshow.prototype
69                  * @type {jQuery}
70                  */
72                 /**
73                  * The `<p>` element that holds the image caption.
74                  *
75                  * @name $imgCaption
76                  * @memberof mw.GallerySlideshow.prototype
77                  * @type {jQuery}
78                  */
80                 /**
81                  * The `<div>` element that contains the image.
82                  *
83                  * @name $imgContainer
84                  * @memberof mw.GallerySlideshow.prototype
85                  * @type {jQuery}
86                  */
88                 /**
89                  * Width of the image based on viewport size.
90                  *
91                  * @name imageWidth
92                  * @memberof mw.GallerySlideshow.prototype
93                  * @type {number}
94                  */
96                 /**
97                  * Height of the image based on viewport size the URLs in the required size.
98                  *
99                  * @name imageHeight
100                  * @memberof mw.GallerySlideshow.prototype
101                  * @type {number}
102                  */
104                 // Initialize
105                 this.drawCarousel();
106                 this.setSizeRequirement();
107                 this.toggleThumbnails( !!this.$gallery.attr( 'data-showthumbnails' ) );
108                 this.showCurrentImage( true );
110                 // Events
111                 $( window ).on(
112                         'resize',
113                         OO.ui.debounce(
114                                 this.setSizeRequirement.bind( this ),
115                                 100
116                         )
117                 );
119                 // Disable thumbnails' link, instead show the image in the carousel
120                 this.$galleryBox.on( 'click', ( e ) => {
121                         this.$currentImage = $( e.currentTarget );
122                         this.showCurrentImage();
123                         return false;
124                 } );
125         };
127         /* Setup */
128         OO.initClass( mw.GallerySlideshow );
130         /* Methods */
131         /**
132          * Draws the carousel and the interface around it.
133          */
134         mw.GallerySlideshow.prototype.drawCarousel = function () {
135                 this.$carousel = $( '<li>' ).addClass( 'gallerycarousel' );
137                 // Buttons for the interface
138                 const prevButton = new OO.ui.ButtonWidget( {
139                         framed: false,
140                         icon: 'previous'
141                 } ).connect( this, { click: 'prevImage' } );
143                 const nextButton = new OO.ui.ButtonWidget( {
144                         framed: false,
145                         icon: 'next'
146                 } ).connect( this, { click: 'nextImage' } );
148                 const toggleButton = new OO.ui.ButtonWidget( {
149                         framed: false,
150                         icon: 'imageGallery',
151                         title: mw.msg( 'gallery-slideshow-toggle' )
152                 } ).connect( this, { click: 'toggleThumbnails' } );
154                 const interfaceElements = new OO.ui.PanelLayout( {
155                         expanded: false,
156                         classes: [ 'mw-gallery-slideshow-buttons' ],
157                         $content: $( '<div>' ).append(
158                                 prevButton.$element,
159                                 toggleButton.$element,
160                                 nextButton.$element
161                         )
162                 } );
163                 this.$interface = interfaceElements.$element;
165                 // Containers for the current image, caption etc.
166                 this.$imgCaption = $( '<p>' ).attr( 'class', 'mw-gallery-slideshow-caption' );
167                 this.$imgContainer = $( '<div>' )
168                         .attr( 'class', 'mw-gallery-slideshow-img-container' );
170                 const carouselStack = new OO.ui.StackLayout( {
171                         continuous: true,
172                         expanded: false,
173                         items: [
174                                 interfaceElements,
175                                 new OO.ui.PanelLayout( {
176                                         expanded: false,
177                                         $content: this.$imgContainer
178                                 } ),
179                                 new OO.ui.PanelLayout( {
180                                         expanded: false,
181                                         $content: this.$imgCaption
182                                 } )
183                         ]
184                 } );
185                 this.$carousel.append( carouselStack.$element );
187                 // Append below the caption or as the first element in the gallery
188                 if ( this.$galleryCaption.length !== 0 ) {
189                         this.$galleryCaption.after( this.$carousel );
190                 } else {
191                         this.$gallery.prepend( this.$carousel );
192                 }
193         };
195         /**
196          * Sets the {@link mw.GallerySlideshow#imageWidth imageWidth} and
197          * {@link mw.GallerySlideshow#imageHeight imageHeight} properties based on the size of the
198          * window. Also flushes the {@link mw.GallerySlideshow#imageInfoCache imageInfoCache} as we'll
199          * now need URLs for a different size.
200          */
201         mw.GallerySlideshow.prototype.setSizeRequirement = function () {
202                 let w = this.$imgContainer.width(),
203                         h = Math.min( $( window ).height() * ( 3 / 4 ), this.$imgContainer.width() ) - this.getChromeHeight();
205                 // Round values in case the user's browser is returning non-integer values.
206                 w = Math.round( w );
207                 h = Math.round( h );
209                 // Only update and flush the cache if the size changed
210                 if ( w !== this.imageWidth || h !== this.imageHeight ) {
211                         this.imageWidth = w;
212                         this.imageHeight = h;
213                         this.imageInfoCache = {};
214                         this.setImageSize();
215                 }
216         };
218         /**
219          * Gets the height of the interface elements and the
220          * gallery's caption.
221          *
222          * @return {number} Height
223          */
224         mw.GallerySlideshow.prototype.getChromeHeight = function () {
225                 return this.$interface.outerHeight() + ( this.$galleryCaption.outerHeight() || 0 );
226         };
228         /**
229          * Sets the height and width of {@link mw.GallerySlideshow#$img $img} based on the
230          * proportion of the image and the values generated by
231          * {@link mw.GallerySlideshow#setSizeRequirement setSizeRequirement}.
232          */
233         mw.GallerySlideshow.prototype.setImageSize = function () {
234                 if ( this.$img === undefined || this.$thumbnail === undefined ) {
235                         return;
236                 }
238                 // Reset height and width
239                 this.$img
240                         .removeAttr( 'width' )
241                         .removeAttr( 'height' );
243                 // Stretch image to take up the required size
244                 this.$img.attr( 'height', ( this.imageHeight - this.$imgCaption.outerHeight() ) + 'px' );
246                 // Make the image smaller in case the current image
247                 // size is larger than the original file size.
248                 this.getImageInfo( this.$thumbnail ).then( ( info ) => {
249                         // NOTE: There will be a jump when resizing the window
250                         // because the cache is cleared and this a new network request.
251                         if (
252                                 info.thumbwidth < this.$img.width() ||
253                                 info.thumbheight < this.$img.height()
254                         ) {
255                                 this.$img.attr( {
256                                         width: info.thumbwidth + 'px',
257                                         height: info.thumbheight + 'px'
258                                 } );
259                         }
260                 } );
261         };
263         /**
264          * Displays the image set as {@link mw.GallerySlideshow#$currentImage $currentImage} in the
265          * carousel.
266          *
267          * @param {boolean} init Image being shown during gallery init (i.e. first image)
268          */
269         mw.GallerySlideshow.prototype.showCurrentImage = function ( init ) {
270                 const $imageLi = this.getCurrentImage();
271                 const $caption = $imageLi.find( '.gallerytext' );
273                 // The order of the following is important for size calculations
274                 // 1. Highlight current thumbnail
275                 this.$gallery
276                         .find( '.gallerybox.slideshow-current' )
277                         .removeClass( 'slideshow-current' );
278                 $imageLi.addClass( 'slideshow-current' );
280                 this.$thumbnail = $imageLi.find( 'img' );
281                 if ( this.$thumbnail.length ) {
282                         // 2. Create and show thumbnail
283                         this.$img = $( '<img>' ).attr( {
284                                 src: this.$thumbnail.attr( 'src' ),
285                                 alt: this.$thumbnail.attr( 'alt' )
286                         } );
287                         // 'image' class required for detection by MultimediaViewer
288                         const $imgLink = $( '<a>' ).addClass( 'image' )
289                                 .attr( 'href', $imageLi.find( 'a' ).eq( 0 ).attr( 'href' ) )
290                                 .append( this.$img );
292                         this.$imgContainer.empty().append( $imgLink );
293                 } else {
294                         // 2b. No image found (e.g. file doesn't exist)
295                         this.$imgContainer.text( $imageLi.find( '.thumb' ).text() );
296                 }
298                 // 3. Copy caption
299                 this.$imgCaption
300                         .empty()
301                         .append( $caption.clone() );
303                 if ( !this.$thumbnail.length ) {
304                         return;
305                 }
307                 // 4. Stretch thumbnail to correct size
308                 this.setImageSize();
310                 const $thumbnail = this.$thumbnail;
311                 // 5. Load image at the required size
312                 this.loadImage( this.$thumbnail ).done( ( info ) => {
313                         // Show this image to the user only if its still the current one
314                         if ( this.$thumbnail.attr( 'src' ) === $thumbnail.attr( 'src' ) ) {
315                                 this.$img.attr( 'src', info.thumburl );
316                                 this.setImageSize();
317                                 // Don't fire hook twice during init
318                                 if ( !init ) {
319                                         mw.hook( 'wikipage.content' ).fire( this.$imgContainer );
320                                 }
322                                 // Pre-fetch the next image
323                                 this.loadImage( this.getNextImage().find( 'img' ) );
324                         }
325                 } ).fail( () => {
326                         // Image didn't load
327                         const title = mw.Title.newFromImg( this.$img );
328                         this.$imgContainer.text( title ? title.getMainText() : '' );
329                 } );
330         };
332         /**
333          * Loads the full image given the `<img>` element of the thumbnail.
334          *
335          * @param {jQuery} $img
336          * @return {jQuery.Promise} Resolves with the image's URL and original
337          *  element once the image has loaded.
338          */
339         mw.GallerySlideshow.prototype.loadImage = function ( $img ) {
340                 return this.getImageInfo( $img ).then( ( info ) => {
341                         const d = $.Deferred();
342                         const img = new Image();
343                         img.src = info.thumburl;
344                         img.onload = function () {
345                                 d.resolve( info );
346                         };
347                         img.onerror = function () {
348                                 d.reject();
349                         };
350                         return d.promise();
351                 } );
352         };
354         /**
355          * Gets the image's info given an `<img>` element.
356          *
357          * @param {Object} $img
358          * @return {jQuery.Promise} Resolves with the image's info.
359          */
360         mw.GallerySlideshow.prototype.getImageInfo = function ( $img ) {
361                 const imageSrc = $img.attr( 'src' );
363                 // Reject promise if there is no thumbnail image
364                 if ( $img[ 0 ] === undefined ) {
365                         return $.Deferred().reject();
366                 }
368                 if ( this.imageInfoCache[ imageSrc ] === undefined ) {
369                         const api = new mw.Api();
370                         // TODO: This supports only gallery of images
371                         const title = mw.Title.newFromImg( $img );
372                         const params = {
373                                 action: 'query',
374                                 formatversion: 2,
375                                 titles: title.toString(),
376                                 prop: 'imageinfo',
377                                 iiprop: 'url'
378                         };
380                         // Check which dimension we need to request, based on
381                         // image and container proportions.
382                         if ( this.getDimensionToRequest( $img ) === 'height' ) {
383                                 params.iiurlheight = this.imageHeight;
384                         } else {
385                                 params.iiurlwidth = this.imageWidth;
386                         }
388                         this.imageInfoCache[ imageSrc ] = api.get( params ).then( ( data ) => {
389                                 if ( OO.getProp( data, 'query', 'pages', 0, 'imageinfo', 0, 'thumburl' ) !== undefined ) {
390                                         return data.query.pages[ 0 ].imageinfo[ 0 ];
391                                 } else {
392                                         return $.Deferred().reject();
393                                 }
394                         } );
395                 }
397                 return this.imageInfoCache[ imageSrc ];
398         };
400         /**
401          * Given an image, the method checks whether to use the height
402          * or the width to request the larger image.
403          *
404          * @param {jQuery} $img
405          * @return {string}
406          */
407         mw.GallerySlideshow.prototype.getDimensionToRequest = function ( $img ) {
408                 const ratio = $img.width() / $img.height();
410                 if ( this.imageHeight * ratio <= this.imageWidth ) {
411                         return 'height';
412                 } else {
413                         return 'width';
414                 }
415         };
417         /**
418          * Toggles visibility of the thumbnails.
419          *
420          * @param {boolean} show Optional argument to control the state
421          */
422         mw.GallerySlideshow.prototype.toggleThumbnails = function ( show ) {
423                 this.$galleryBox.toggle( show );
424                 this.$carousel.toggleClass( 'mw-gallery-slideshow-thumbnails-toggled', show );
425         };
427         /**
428          * Getter method for {@link mw.GallerySlideshow#$currentImage $currentImage}.
429          *
430          * @return {jQuery}
431          */
432         mw.GallerySlideshow.prototype.getCurrentImage = function () {
433                 this.$currentImage = this.$currentImage || this.$galleryBox.eq( 0 );
434                 return this.$currentImage;
435         };
437         /**
438          * Gets the image after the current one. Returns the first image if
439          * the current one is the last.
440          *
441          * @return {jQuery}
442          */
443         mw.GallerySlideshow.prototype.getNextImage = function () {
444                 // Not the last image in the gallery
445                 if ( this.$currentImage.next( '.gallerybox' )[ 0 ] !== undefined ) {
446                         return this.$currentImage.next( '.gallerybox' );
447                 } else {
448                         return this.$galleryBox.eq( 0 );
449                 }
450         };
452         /**
453          * Gets the image before the current one. Returns the last image if
454          * the current one is the first.
455          *
456          * @return {jQuery}
457          */
458         mw.GallerySlideshow.prototype.getPrevImage = function () {
459                 // Not the first image in the gallery
460                 if ( this.$currentImage.prev( '.gallerybox' )[ 0 ] !== undefined ) {
461                         return this.$currentImage.prev( '.gallerybox' );
462                 } else {
463                         return this.$galleryBox.last();
464                 }
465         };
467         /**
468          * Sets the {@link mw.GallerySlideshow#$currentImage $currentImage} to the next one and shows
469          * it in the carousel.
470          */
471         mw.GallerySlideshow.prototype.nextImage = function () {
472                 this.$currentImage = this.getNextImage();
473                 this.showCurrentImage();
474         };
476         /**
477          * Sets the {@link mw.GallerySlideshow#$currentImage $currentImage} to the previous one and
478          * shows it in the carousel.
479          */
480         mw.GallerySlideshow.prototype.prevImage = function () {
481                 this.$currentImage = this.getPrevImage();
482                 this.showCurrentImage();
483         };
485         // Bootstrap all slideshow galleries
486         mw.hook( 'wikipage.content' ).add( ( $content ) => {
487                 $content.find( '.mw-gallery-slideshow' ).filter( function () {
488                         // This gallery slideshow feature depends on img tags being present in the DOM.
489                         // This might not be true - for example in MobileFrontend - where images are lazy loaded.
490                         // The filter statement can be removed when T194887 is resolved.
491                         return $( this ).find( 'img' ).length > 0;
492                 } ).each( function () {
493                         // eslint-disable-next-line no-new
494                         new mw.GallerySlideshow( this );
495                 } );
496         } );
498 }() );