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