2 * Enhance MediaWiki galleries (from the `<gallery>` parser tag).
4 * - Toggle gallery captions when focused.
5 * - Dynamically resize images to fill horizontal space.
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
18 * Perform the layout justification.
21 * @this HTMLElement A `ul.mw-gallery-*` element
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 ),
33 if ( top !== lastTop ) {
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;
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();
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.
60 const captionWidth = $this.find( 'div.gallerytextwrapper' ).width();
61 const outerWidth = $this.outerWidth();
62 rows[ rows.length - 1 ].push( {
66 // FIXME: Deal with devision by 0.
67 aspect: imgWidth / imgHeight,
68 captionWidth: captionWidth,
72 // Save all boundaries so we can restore them on window resize
77 captionWidth: captionWidth
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 ];
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;
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;
105 combinedAspect += curRow[ j ].aspect;
106 combinedPadding += curRow[ j ].width - curRow[ j ].imgWidth;
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;
128 // Probably a single row gallery
129 preferredHeight = curRowHeight;
132 preferredHeight = 1.5 * curRowHeight;
135 if ( !isFinite( preferredHeight ) ) {
136 // This *definitely* should not happen.
140 if ( preferredHeight < 5 ) {
141 // Well something clearly went wrong...
146 if ( preferredHeight / curRowHeight > 1 ) {
147 totalZoom += preferredHeight / curRowHeight;
149 // If we shrink, still consider that a zoom of 1
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.
176 $gallerybox.width( newWidth + padding );
177 $outerDiv.width( newWidth + padding );
178 $imageDiv.width( newWidth );
179 $caption.width( curRow[ j ].captionWidth + ( newWidth - curRow[ j ].imgWidth ) );
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;
189 $imageDiv.height( preferredHeight );
196 function handleResizeStart() {
197 // Only do anything if window width changed. We don't care about the height.
198 if ( lastWidth === window.innerWidth ) {
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();
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;
228 $imageDiv.height( imgHeight );
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;
239 // Remove temporary min-height
240 .css( 'min-height', '' )
241 // Recalculate layout
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' );
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 );
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.
265 $galleries.each( justify );
267 // Bind here instead of in the top scope as the callbacks use $galleries.
271 .on( 'resize', mw.util.debounce( handleResizeStart, 300, true ) )
272 .on( 'resize', mw.util.debounce( handleResizeEnd, 300 ) );