3 * @class mw.GallerySlideshow
4 * @classdesc Interface controls for the slideshow gallery. To use, first load
5 * the `mediawiki.page.gallery.slideshow` ResourceLoader module.
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.
14 mw.GallerySlideshow = function ( gallery ) {
17 * The `<ul>` element of the gallery.
21 this.$gallery = $( gallery );
23 * The `<li>` that has the gallery caption.
27 this.$galleryCaption = this.$gallery.find( '.gallerycaption' );
29 * Selection of `<li>` elements that have thumbnails.
33 this.$galleryBox = this.$gallery.find( '.gallerybox' );
35 * The `<li>` element of the current image.
39 this.$currentImage = null;
41 * A key value pair of thumbnail URLs and image info.
43 * @type {Object.<string,jQuery.Promise>}
45 this.imageInfoCache = {};
49 * The `<li>` element that contains the carousel.
52 * @memberof mw.GallerySlideshow.prototype
57 * The `<div>` element that contains the interface buttons.
60 * @memberof mw.GallerySlideshow.prototype
65 * The `<img>` element that'll display the current image.
68 * @memberof mw.GallerySlideshow.prototype
73 * The `<p>` element that holds the image caption.
76 * @memberof mw.GallerySlideshow.prototype
81 * The `<div>` element that contains the image.
84 * @memberof mw.GallerySlideshow.prototype
89 * Width of the image based on viewport size.
92 * @memberof mw.GallerySlideshow.prototype
97 * Height of the image based on viewport size the URLs in the required size.
100 * @memberof mw.GallerySlideshow.prototype
106 this.setSizeRequirement();
107 this.toggleThumbnails( !!this.$gallery.attr( 'data-showthumbnails' ) );
108 this.showCurrentImage( true );
114 this.setSizeRequirement.bind( this ),
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();
128 OO.initClass( mw.GallerySlideshow );
132 * Draws the carousel and the interface around it.
134 mw.GallerySlideshow.prototype.drawCarousel = function () {
135 this.$carousel = $( '<li>' ).addClass( 'gallerycarousel' );
137 // Buttons for the interface
138 const prevButton = new OO.ui.ButtonWidget( {
141 } ).connect( this, { click: 'prevImage' } );
143 const nextButton = new OO.ui.ButtonWidget( {
146 } ).connect( this, { click: 'nextImage' } );
148 const toggleButton = new OO.ui.ButtonWidget( {
150 icon: 'imageGallery',
151 title: mw.msg( 'gallery-slideshow-toggle' )
152 } ).connect( this, { click: 'toggleThumbnails' } );
154 const interfaceElements = new OO.ui.PanelLayout( {
156 classes: [ 'mw-gallery-slideshow-buttons' ],
157 $content: $( '<div>' ).append(
159 toggleButton.$element,
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( {
175 new OO.ui.PanelLayout( {
177 $content: this.$imgContainer
179 new OO.ui.PanelLayout( {
181 $content: this.$imgCaption
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 );
191 this.$gallery.prepend( this.$carousel );
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.
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.
209 // Only update and flush the cache if the size changed
210 if ( w !== this.imageWidth || h !== this.imageHeight ) {
212 this.imageHeight = h;
213 this.imageInfoCache = {};
219 * Gets the height of the interface elements and the
222 * @return {number} Height
224 mw.GallerySlideshow.prototype.getChromeHeight = function () {
225 return this.$interface.outerHeight() + ( this.$galleryCaption.outerHeight() || 0 );
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}.
233 mw.GallerySlideshow.prototype.setImageSize = function () {
234 if ( this.$img === undefined || this.$thumbnail === undefined ) {
238 // Reset height and width
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.
252 info.thumbwidth < this.$img.width() ||
253 info.thumbheight < this.$img.height()
256 width: info.thumbwidth + 'px',
257 height: info.thumbheight + 'px'
264 * Displays the image set as {@link mw.GallerySlideshow#$currentImage $currentImage} in the
267 * @param {boolean} init Image being shown during gallery init (i.e. first image)
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
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' )
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 );
294 // 2b. No image found (e.g. file doesn't exist)
295 this.$imgContainer.text( $imageLi.find( '.thumb' ).text() );
301 .append( $caption.clone() );
303 if ( !this.$thumbnail.length ) {
307 // 4. Stretch thumbnail to correct size
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 );
317 // Don't fire hook twice during init
319 mw.hook( 'wikipage.content' ).fire( this.$imgContainer );
322 // Pre-fetch the next image
323 this.loadImage( this.getNextImage().find( 'img' ) );
327 const title = mw.Title.newFromImg( this.$img );
328 this.$imgContainer.text( title ? title.getMainText() : '' );
333 * Loads the full image given the `<img>` element of the thumbnail.
335 * @param {jQuery} $img
336 * @return {jQuery.Promise} Resolves with the image's URL and original
337 * element once the image has loaded.
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 () {
347 img.onerror = function () {
355 * Gets the image's info given an `<img>` element.
357 * @param {Object} $img
358 * @return {jQuery.Promise} Resolves with the image's info.
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();
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 );
375 titles: title.toString(),
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;
385 params.iiurlwidth = this.imageWidth;
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 ];
392 return $.Deferred().reject();
397 return this.imageInfoCache[ imageSrc ];
401 * Given an image, the method checks whether to use the height
402 * or the width to request the larger image.
404 * @param {jQuery} $img
407 mw.GallerySlideshow.prototype.getDimensionToRequest = function ( $img ) {
408 const ratio = $img.width() / $img.height();
410 if ( this.imageHeight * ratio <= this.imageWidth ) {
418 * Toggles visibility of the thumbnails.
420 * @param {boolean} show Optional argument to control the state
422 mw.GallerySlideshow.prototype.toggleThumbnails = function ( show ) {
423 this.$galleryBox.toggle( show );
424 this.$carousel.toggleClass( 'mw-gallery-slideshow-thumbnails-toggled', show );
428 * Getter method for {@link mw.GallerySlideshow#$currentImage $currentImage}.
432 mw.GallerySlideshow.prototype.getCurrentImage = function () {
433 this.$currentImage = this.$currentImage || this.$galleryBox.eq( 0 );
434 return this.$currentImage;
438 * Gets the image after the current one. Returns the first image if
439 * the current one is the last.
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' );
448 return this.$galleryBox.eq( 0 );
453 * Gets the image before the current one. Returns the last image if
454 * the current one is the first.
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' );
463 return this.$galleryBox.last();
468 * Sets the {@link mw.GallerySlideshow#$currentImage $currentImage} to the next one and shows
469 * it in the carousel.
471 mw.GallerySlideshow.prototype.nextImage = function () {
472 this.$currentImage = this.getNextImage();
473 this.showCurrentImage();
477 * Sets the {@link mw.GallerySlideshow#$currentImage $currentImage} to the previous one and
478 * shows it in the carousel.
480 mw.GallerySlideshow.prototype.prevImage = function () {
481 this.$currentImage = this.getPrevImage();
482 this.showCurrentImage();
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 );