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