Followup r79867: re-animate the spinner during Special:Upload's thumbnail init (was...
[mediawiki.git] / resources / mediawiki.special / mediawiki.special.upload.js
blob85b3f3f56b505e917190473970e58302b7d63b4b
1 /*
2  * JavaScript for Special:Upload
3  * Note that additional code still lives in skins/common/upload.js
4  */
6 /**
7  * Add a preview to the upload form
8  */
9 jQuery( function( $ ) {
10         /**
11          * Is the FileAPI available with sufficient functionality?
12          */
13         function hasFileAPI(){
14                 return typeof window.FileReader !== 'undefined';
15         }
17         /**
18          * Check if this is a recognizable image type...
19          * Also excludes files over 10M to avoid going insane on memory usage.
20          *
21          * @todo is there a way we can ask the browser what's supported in <img>s?
22          * @todo put SVG back after working around Firefox 7 bug <https://bugzilla.wikimedia.org/show_bug.cgi?id=31643>
23          *
24          * @param {File} file
25          * @return boolean
26          */
27         function fileIsPreviewable( file ) {
28                 var     known = ['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml'],
29                         tooHuge = 10 * 1024 * 1024;
30                 return ( $.inArray( file.type, known ) !== -1 ) && file.size > 0 && file.size < tooHuge;
31         }
33         /**
34          * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
35          * in browsers supporting HTML5 FileAPI.
36          *
37          * As of this writing, known good:
38          * - Firefox 3.6+
39          * - Chrome 7.something
40          *
41          * @todo check file size limits and warn of likely failures
42          *
43          * @param {File} file
44          */
45         function showPreview( file ) {
46                 var     previewSize = 180,
47                         thumb = $( '<div id="mw-upload-thumbnail" class="thumb tright">' +
48                                                 '<div class="thumbinner">' +
49                                                         '<div class="mw-small-spinner" style="width: 180px; height: 180px"></div>' +
50                                                         '<div class="thumbcaption"><div class="filename"></div><div class="fileinfo"></div></div>' +
51                                                 '</div>' +
52                                         '</div>' );
53                 thumb.find( '.filename' ).text( file.name ).end()
54                         .find( '.fileinfo' ).text( prettySize( file.size ) ).end();
56                 var     $canvas = $('<canvas width="' + previewSize + '" height="' + previewSize + '" ></canvas>'),
57                         ctx = $canvas[0].getContext( '2d' );
58                 $( '#mw-htmlform-source' ).parent().prepend( thumb );
60                 var meta;
61                 fetchPreview( file, function( dataURL ) {
62                         var     img = new Image(),
63                                 rotation = 0;
65                         if ( meta && meta.tiff && meta.tiff.Orientation ) {
66                                 rotation = (360 - function () {
67                                         // See includes/media/Bitmap.php
68                                         switch ( meta.tiff.Orientation.value ) {
69                                                 case 8:
70                                                         return 90;
71                                                 case 3:
72                                                         return 180;
73                                                 case 6:
74                                                         return 270;
75                                                 default:
76                                                         return 0;
77                                         }
78                                 }() ) % 360;
79                         }
81                         img.onload = function() {
82                                 var width, height, x, y, dx, dy, logicalWidth, logicalHeight;
83                                 // Fit the image within the previewSizexpreviewSize box
84                                 if ( img.width > img.height ) {
85                                         width = previewSize;
86                                         height = img.height / img.width * previewSize;
87                                 } else {
88                                         height = previewSize;
89                                         width = img.width / img.height * previewSize;
90                                 }
91                                 // Determine the offset required to center the image
92                                 dx = (180 - width) / 2;
93                                 dy = (180 - height) / 2;
94                                 switch ( rotation ) {
95                                         // If a rotation is applied, the direction of the axis
96                                         // changes as well. You can derive the values below by
97                                         // drawing on paper an axis system, rotate it and see
98                                         // where the positive axis direction is
99                                         case 0:
100                                                 x = dx;
101                                                 y = dy;
102                                                 logicalWidth = img.width;
103                                                 logicalHeight = img.height;
104                                                 break;
105                                         case 90:
107                                                 x = dx;
108                                                 y = dy - previewSize;
109                                                 logicalWidth = img.height;
110                                                 logicalHeight = img.width;
111                                                 break;
112                                         case 180:
113                                                 x = dx - previewSize;
114                                                 y = dy - previewSize;
115                                                 logicalWidth = img.width;
116                                                 logicalHeight = img.height;
117                                                 break;
118                                         case 270:
119                                                 x = dx - previewSize;
120                                                 y = dy;
121                                                 logicalWidth = img.height;
122                                                 logicalHeight = img.width;
123                                                 break;
124                                 }
126                                 ctx.clearRect( 0, 0, 180, 180 );
127                                 ctx.rotate( rotation / 180 * Math.PI );
128                                 ctx.drawImage( img, x, y, width, height );
129                                 thumb.find('.mw-small-spinner').replaceWith($canvas);
131                                 // Image size
132                                 var info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
133                                         ', ' + prettySize( file.size );
134                                 $( '#mw-upload-thumbnail .fileinfo' ).text( info );
135                         };
136                         img.src = dataURL;
137                 }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
138                         try {
139                                 meta = mw.libs.jpegmeta( data, file.fileName );
140                                 meta._binary_data = null;
141                         } catch ( e ) {
142                                 meta = null;
143                         }
144                 } : null );
145         }
147         /**
148          * Start loading a file into memory; when complete, pass it as a
149          * data URL to the callback function. If the callbackBinary is set it will
150          * first be read as binary and afterwards as data URL. Useful if you want
151          * to do preprocessing on the binary data first.
152          *
153          * @param {File} file
154          * @param {function} callback
155          * @param {function} callbackBinary
156          */
157         function fetchPreview( file, callback, callbackBinary ) {
158                 var reader = new FileReader();
159                 if ( callbackBinary ) {
160                         // To fetch JPEG metadata we need a binary string; start there.
161                         // todo: 
162                         reader.onload = function() {
163                                 callbackBinary( reader.result );
165                                 // Now run back through the regular code path.
166                                 fetchPreview(file, callback );
167                         };
168                         reader.readAsBinaryString( file );
169                 } else if ('URL' in window && 'createObjectURL' in window.URL) {
170                         // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en/DOM/window.URL.createObjectURL>
171                         // WebKit has it in a namespace for now but that's ok. ;)
172                         //
173                         // Lifetime of this URL is until document close, which is fine
174                         // for Special:Upload -- if this code gets used on longer-running
175                         // pages, add a revokeObjectURL() when it's no longer needed.
176                         //
177                         // Prefer this over readAsDataURL for Firefox 7 due to bug reading
178                         // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
179                         callback(window.URL.createObjectURL(file));
180                 } else {
181                         // This ends up decoding the file to base-64 and back again, which
182                         // feels horribly inefficient.
183                         reader.onload = function() {
184                                 callback( reader.result );
185                         };
186                         reader.readAsDataURL( file );
187                 }
188         }
190         /**
191          * Format a file size attractively.
192          * @todo match numeric formatting
193          *
194          * @param {number} s
195          * @return string
196          */
197         function prettySize( s ) {
198                 var sizes = ['size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes'];
199                 while ( s >= 1024 && sizes.length > 1 ) {
200                         s /= 1024;
201                         sizes = sizes.slice( 1 );
202                 }
203                 return mw.msg( sizes[0], Math.round( s ) );
204         }
206         /**
207          * Clear the file upload preview area.
208          */
209         function clearPreview() {
210                 $( '#mw-upload-thumbnail' ).remove();
211         }
213         /**
214          * Check if the file does not exceed the maximum size
215          */
216         function checkMaxUploadSize( file ) {
217                 function getMaxUploadSize( type ) {
218                         var sizes = mw.config.get( 'wgMaxUploadSize' );
219                         if ( sizes[type] !== undefined ) {
220                                 return sizes[type];
221                         }
222                         return sizes['*'];
223                 }
224                 $( '.mw-upload-source-error' ).remove();
226                 var maxSize = getMaxUploadSize( 'file' );
227                 if ( file.size > maxSize ) {
228                         var error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
229                                         mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
230                         $( '#wpUploadFile' ).after( error );
231                         return false;
232                 }
233                 return true;
234         }
237         /**
238          * Initialization
239          */
240         if ( hasFileAPI() ) {
241                 // Update thumbnail when the file selection control is updated.
242                 $( '#wpUploadFile' ).change( function() {
243                         clearPreview();
244                         if ( this.files && this.files.length ) {
245                                 // Note: would need to be updated to handle multiple files.
246                                 var file = this.files[0];
248                                 if ( !checkMaxUploadSize( file ) ) {
249                                         return;
250                                 }
252                                 if ( fileIsPreviewable( file ) ) {
253                                         showPreview( file );
254                                 }
255                         }
256                 } );
257         }
258 } );
261  * Disable all upload source fields except the selected one
262  */
263 jQuery( function ( $ ) {
264         var rows = $( '.mw-htmlform-field-UploadSourceField' );
265         for ( var i = rows.length; i; i-- ) {
266                 var row = rows[i - 1];
267                 $( 'input[name="wpSourceType"]', row ).change( function () {
268                         var currentRow = row; // Store current row in our own scope
269                         return function () {
270                                 $( '.mw-upload-source-error' ).remove();
271                                 if ( this.checked ) {
272                                         // Disable all inputs
273                                         $( 'input[name!="wpSourceType"]', rows ).prop( 'disabled', 'disabled' );
274                                         // Re-enable the current one
275                                         $( 'input', currentRow ).prop( 'disabled', false );
276                                 }
277                         };
278                 }() );
279         }
280 } );