Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.page.gallery.js
blob68aa459e990db7172b991bb32d6e386e570074c4
1 /*!
2  * Enhance MediaWiki galleries (from the `<gallery>` parser tag).
3  *
4  * - Toggle gallery captions when focused.
5  * - Dynamically resize images to fill horizontal space.
6  */
7 ( function () {
8         let $galleries,
9                 bound = false,
10                 lastWidth = window.innerWidth,
11                 justifyNeeded = false;
12                 // Is there a better way to detect a touchscreen? Current check taken from stack overflow.
13         const isTouchScreen = !!( window.ontouchstart !== undefined ||
14                 window.DocumentTouch !== undefined && document instanceof window.DocumentTouch
15         );
17         /**
18          * Perform the layout justification.
19          *
20          * @ignore
21          * @this HTMLElement A `ul.mw-gallery-*` element
22          */
23         function justify() {
24                 let lastTop;
25                 const rows = [],
26                         $gallery = $( this );
28                 $gallery.children( 'li.gallerybox' ).each( function () {
29                         // Math.floor, to be paranoid if things are off by 0.00000000001
30                         const top = Math.floor( $( this ).position().top ),
31                                 $this = $( this );
33                         if ( top !== lastTop ) {
34                                 rows.push( [] );
35                                 lastTop = top;
36                         }
38                         const $imageDiv = $this.find( 'div.thumb' ).first();
39                         const $img = $imageDiv.find( 'img, video' ).first();
40                         let imgWidth, imgHeight;
41                         if ( $img.length && $img[ 0 ].height ) {
42                                 imgHeight = $img[ 0 ].height;
43                                 imgWidth = $img[ 0 ].width;
44                         } else {
45                                 // If we don't have a real image, get the containing divs width/height.
46                                 // Note that if we do have a real image, using this method will generally
47                                 // give the same answer, but can be different in the case of a very
48                                 // narrow image where extra padding is added.
49                                 imgHeight = $imageDiv.height();
50                                 imgWidth = $imageDiv.width();
51                         }
53                         // Hack to make an edge case work ok
54                         // (This happens for very small images, and for audio files)
55                         if ( imgHeight < 40 ) {
56                                 // Don't try and resize this item.
57                                 imgHeight = 0;
58                         }
60                         const captionWidth = $this.find( 'div.gallerytextwrapper' ).width();
61                         const outerWidth = $this.outerWidth();
62                         rows[ rows.length - 1 ].push( {
63                                 $elm: $this,
64                                 width: outerWidth,
65                                 imgWidth: imgWidth,
66                                 // FIXME: Deal with devision by 0.
67                                 aspect: imgWidth / imgHeight,
68                                 captionWidth: captionWidth,
69                                 height: imgHeight
70                         } );
72                         // Save all boundaries so we can restore them on window resize
73                         $this.data( {
74                                 imgWidth: imgWidth,
75                                 imgHeight: imgHeight,
76                                 width: outerWidth,
77                                 captionWidth: captionWidth
78                         } );
79                 } );
81                 ( function () {
82                         let totalZoom = 0;
84                         for ( let i = 0; i < rows.length; i++ ) {
85                                 const maxWidth = $gallery.width();
86                                 let combinedAspect = 0;
87                                 let combinedPadding = 0;
88                                 const curRow = rows[ i ];
89                                 let curRowHeight = 0;
91                                 for ( let j = 0; j < curRow.length; j++ ) {
92                                         if ( curRowHeight === 0 ) {
93                                                 if ( isFinite( curRow[ j ].height ) ) {
94                                                         // Get the height of this row, by taking the first
95                                                         // non-out of bounds height
96                                                         curRowHeight = curRow[ j ].height;
97                                                 }
98                                         }
100                                         if ( curRow[ j ].aspect === 0 || !isFinite( curRow[ j ].aspect ) ) {
101                                                 // One of the dimensions are 0. Probably should
102                                                 // not try to resize.
103                                                 combinedPadding += curRow[ j ].width;
104                                         } else {
105                                                 combinedAspect += curRow[ j ].aspect;
106                                                 combinedPadding += curRow[ j ].width - curRow[ j ].imgWidth;
107                                         }
108                                 }
110                                 // Add some padding for inter-element spacing.
111                                 combinedPadding += 5 * curRow.length;
112                                 const wantedWidth = maxWidth - combinedPadding;
113                                 let preferredHeight = wantedWidth / combinedAspect;
115                                 if ( preferredHeight > curRowHeight * 1.5 ) {
116                                         // Only expand at most 1.5 times current size
117                                         // As that's as high a resolution as we have.
118                                         // Also on the off chance there is a bug in this
119                                         // code, would prevent accidentally expanding to
120                                         // be 10 billion pixels wide.
121                                         if ( i === rows.length - 1 ) {
122                                                 // If its the last row, and we can't fit it,
123                                                 // don't make the entire row huge.
124                                                 const avgZoom = totalZoom / ( rows.length - 1 );
125                                                 if ( isFinite( avgZoom ) && avgZoom >= 1 && avgZoom <= 1.5 ) {
126                                                         preferredHeight = avgZoom * curRowHeight;
127                                                 } else {
128                                                         // Probably a single row gallery
129                                                         preferredHeight = curRowHeight;
130                                                 }
131                                         } else {
132                                                 preferredHeight = 1.5 * curRowHeight;
133                                         }
134                                 }
135                                 if ( !isFinite( preferredHeight ) ) {
136                                         // This *definitely* should not happen.
137                                         // Skip this row.
138                                         continue;
139                                 }
140                                 if ( preferredHeight < 5 ) {
141                                         // Well something clearly went wrong...
142                                         // Skip this row.
143                                         continue;
144                                 }
146                                 if ( preferredHeight / curRowHeight > 1 ) {
147                                         totalZoom += preferredHeight / curRowHeight;
148                                 } else {
149                                         // If we shrink, still consider that a zoom of 1
150                                         totalZoom += 1;
151                                 }
153                                 for ( let j = 0; j < curRow.length; j++ ) {
154                                         const newWidth = preferredHeight * curRow[ j ].aspect;
155                                         const padding = curRow[ j ].width - curRow[ j ].imgWidth;
156                                         const $gallerybox = curRow[ j ].$elm;
157                                         // This wrapper is only present if ParserEnableLegacyMediaDOM is true
158                                         const $outerDiv = $gallerybox.children( 'div:not( [class] )' ).first();
159                                         const $imageDiv = $gallerybox.find( 'div.thumb' ).first();
160                                         const $imageElm = $imageDiv.find( 'img, video' ).first();
161                                         const $caption = $gallerybox.find( 'div.gallerytextwrapper' );
163                                         // Since we are going to re-adjust the height, the vertical
164                                         // centering margins need to be reset.
165                                         $imageDiv.children( 'div' ).css( 'margin', '0px auto' );
167                                         if ( newWidth < 60 || !isFinite( newWidth ) ) {
168                                                 // Making something skinnier than this will mess up captions,
169                                                 if ( newWidth < 1 || !isFinite( newWidth ) ) {
170                                                         $outerDiv.height( preferredHeight );
171                                                         // Don't even try and touch the image size if it could mean
172                                                         // making it disappear.
173                                                         continue;
174                                                 }
175                                         } else {
176                                                 $gallerybox.width( newWidth + padding );
177                                                 $outerDiv.width( newWidth + padding );
178                                                 $imageDiv.width( newWidth );
179                                                 $caption.width( curRow[ j ].captionWidth + ( newWidth - curRow[ j ].imgWidth ) );
180                                         }
182                                         // We don't always have an img, e.g. in the case of an invalid file.
183                                         if ( $imageElm[ 0 ] ) {
184                                                 const imageElm = $imageElm[ 0 ];
185                                                 imageElm.width = newWidth;
186                                                 imageElm.height = preferredHeight;
187                                         } else {
188                                                 // Not a file box.
189                                                 $imageDiv.height( preferredHeight );
190                                         }
191                                 }
192                         }
193                 }() );
194         }
196         function handleResizeStart() {
197                 // Only do anything if window width changed. We don't care about the height.
198                 if ( lastWidth === window.innerWidth ) {
199                         return;
200                 }
202                 justifyNeeded = true;
203                 // Temporarily set min-height, so that content following the gallery is not reflowed twice
204                 $galleries.css( 'min-height', function () {
205                         return $( this ).height();
206                 } );
207                 $galleries.children( 'li.gallerybox' ).each( function () {
208                         const imgWidth = $( this ).data( 'imgWidth' ),
209                                 imgHeight = $( this ).data( 'imgHeight' ),
210                                 width = $( this ).data( 'width' ),
211                                 captionWidth = $( this ).data( 'captionWidth' ),
212                                 // This wrapper is only present if ParserEnableLegacyMediaDOM is true
213                                 $outerDiv = $( this ).children( 'div:not( [class] )' ).first(),
214                                 $imageDiv = $( this ).find( 'div.thumb' ).first();
216                         // Restore original sizes so we can arrange the elements as on freshly loaded page
217                         $( this ).width( width );
218                         $outerDiv.width( width );
219                         $imageDiv.width( imgWidth );
220                         $( this ).find( 'div.gallerytextwrapper' ).width( captionWidth );
222                         const $imageElm = $imageDiv.find( 'img, video' ).first();
223                         if ( $imageElm[ 0 ] ) {
224                                 const imageElm = $imageElm[ 0 ];
225                                 imageElm.width = imgWidth;
226                                 imageElm.height = imgHeight;
227                         } else {
228                                 $imageDiv.height( imgHeight );
229                         }
230                 } );
231         }
233         function handleResizeEnd() {
234                 // If window width never changed during the resize, don't do anything.
235                 if ( justifyNeeded ) {
236                         justifyNeeded = false;
237                         lastWidth = window.innerWidth;
238                         $galleries
239                                 // Remove temporary min-height
240                                 .css( 'min-height', '' )
241                                 // Recalculate layout
242                                 .each( justify );
243                 }
244         }
246         mw.hook( 'wikipage.content' ).add( ( $content ) => {
247                 if ( isTouchScreen ) {
248                         // Always show the caption for a touch screen.
249                         $content.find( 'ul.mw-gallery-packed-hover' )
250                                 .addClass( 'mw-gallery-packed-overlay' )
251                                 .removeClass( 'mw-gallery-packed-hover' );
252                 } else {
253                         // Note use of just `a`, not `a.image`, since we also want this to trigger if a link
254                         // within the caption text receives focus.
255                         $content.find( 'ul.mw-gallery-packed-hover li.gallerybox' ).on( 'focus blur', 'a', function ( e ) {
256                                 // Confusingly jQuery leaves e.type as focusout for delegated blur events
257                                 const gettingFocus = e.type !== 'blur' && e.type !== 'focusout';
258                                 $( this ).closest( 'li.gallerybox' ).toggleClass( 'mw-gallery-focused', gettingFocus );
259                         } );
260                 }
262                 $galleries = $content.find( 'ul.mw-gallery-packed-overlay, ul.mw-gallery-packed-hover, ul.mw-gallery-packed' );
263                 // Call the justification asynchronous because live preview fires the hook with detached $content.
264                 setTimeout( () => {
265                         $galleries.each( justify );
267                         // Bind here instead of in the top scope as the callbacks use $galleries.
268                         if ( !bound ) {
269                                 bound = true;
270                                 $( window )
271                                         .on( 'resize', mw.util.debounce( handleResizeStart, 300, true ) )
272                                         .on( 'resize', mw.util.debounce( handleResizeEnd, 300 ) );
273                         }
274                 } );
275         } );
276 }() );