hjg
2024-10-30 8cf23534166c07e711aac2a25911ada317ba01f0
提交 | 用户 | 时间
58d006 1 /**
A 2  <b>Ace file input element</b>. Custom, simple file input element to style browser's default file input.
3 */
4 (function($ , undefined) {
5     var multiplible = 'multiple' in document.createElement('INPUT');
6     var hasFileList = 'FileList' in window;//file list enabled in modern browsers
7     var hasFileReader = 'FileReader' in window;
8     var hasFile = 'File' in window;
9
10     var Ace_File_Input = function(element , settings) {
11         var self = this;
12         
13         var attrib_values = ace.helper.getAttrSettings(element, $.fn.ace_file_input.defaults);
14         this.settings = $.extend({}, $.fn.ace_file_input.defaults, settings, attrib_values);
15
16         this.$element = $(element);
17         this.element = element;
18         this.disabled = false;
19         this.can_reset = true;
20         
21
22         this.$element
23         .off('change.ace_inner_call')
24         .on('change.ace_inner_call', function(e , ace_inner_call){
25             if(self.disabled) return;
26         
27             if(ace_inner_call === true) return;//this change event is called from above drop event and extra checkings are taken care of there
28             return handle_on_change.call(self);
29             //if(ret === false) e.preventDefault();
30         });
31
32         var parent_label = this.$element.closest('label').css({'display':'block'})
33         var tagName = parent_label.length == 0 ? 'label' : 'span';//if not inside a "LABEL" tag, use "LABEL" tag, otherwise use "SPAN"
34         this.$element.wrap('<'+tagName+' class="ace-file-input" />');
35
36         this.apply_settings();
37         this.reset_input_field();//for firefox as it keeps selected file after refresh
38     }
39     Ace_File_Input.error = {
40         'FILE_LOAD_FAILED' : 1,
41         'IMAGE_LOAD_FAILED' : 2,
42         'THUMBNAIL_FAILED' : 3
43     };
44
45
46     Ace_File_Input.prototype.apply_settings = function() {
47         var self = this;
48
49         this.multi = this.$element.attr('multiple') && multiplible;
50         this.well_style = this.settings.style == 'well';
51
52         if(this.well_style) {
53             if( !this.settings.thumbnail ) this.settings.thumbnail = 'small';
54             this.$element.parent().addClass('ace-file-multiple');
55         }
56         else {
57             this.$element.parent().removeClass('ace-file-multiple');
58         }
59
60
61         this.$element.parent().find(':not(input[type=file])').remove();//remove all except our input, good for when changing settings
62         this.$element.after('<span class="ace-file-container" data-title="'+this.settings.btn_choose+'"><span class="ace-file-name" data-title="'+this.settings.no_file+'">'+(this.settings.no_icon ? '<i class="'+ ace.vars['icon'] + this.settings.no_icon+'"></i>' : '')+'</span></span>');
63         this.$label = this.$element.next();
64         this.$container = this.$element.closest('.ace-file-input');
65
66         var remove_btn = !!this.settings.icon_remove;
67         if(remove_btn) {
68             var btn = 
69             $('<a class="remove" href="#"><i class="'+ ace.vars['icon'] + this.settings.icon_remove+'"></i></a>')
70             .appendTo(this.$element.parent());
71
72             btn.on(ace.click_event, function(e){
73                 e.preventDefault();
74                 if( !self.can_reset ) return false;
75                 
76                 var ret = true;
77                 if(self.settings.before_remove) ret = self.settings.before_remove.call(self.element);
78                 if(!ret) return false;
79
80                 var r = self.reset_input();
81                 return false;
82             });
83         }
84
85
86         if(this.settings.droppable && hasFileList) {
87             enable_drop_functionality.call(this);
88         }
89     }
90
91     Ace_File_Input.prototype.show_file_list = function($files , inner_call) {
92         var files = typeof $files === "undefined" ? this.$element.data('ace_input_files') : $files;
93         if(!files || files.length == 0) return;
94         
95         //////////////////////////////////////////////////////////////////
96         
97         if(this.well_style) {
98             this.$label.find('.ace-file-name').remove();
99             if(!this.settings.btn_change) this.$label.addClass('hide-placeholder');
100         }
101         this.$label.attr('data-title', this.settings.btn_change).addClass('selected');
102         
103         for (var i = 0; i < files.length; i++) {
104             var filename = '', format = false;
105             if(typeof files[i] === "string") filename = files[i];
106             else if(hasFile && files[i] instanceof File) filename = $.trim( files[i].name );
107             else if(files[i] instanceof Object && files[i].hasOwnProperty('name')) {
108                 //format & name specified by user (pre-displaying name, etc)
109                 filename = files[i].name;
110                 if(files[i].hasOwnProperty('type')) format = files[i].type;
111                 if(!files[i].hasOwnProperty('path')) files[i].path = files[i].name;
112             }
113             else continue;
114             
115             var index = filename.lastIndexOf("\\") + 1;
116             if(index == 0)index = filename.lastIndexOf("/") + 1;
117             filename = filename.substr(index);
118             
119             if(format == false) {
120                 if((/\.(jpe?g|png|gif|svg|bmp|tiff?)$/i).test(filename)) {                
121                     format = 'image';
122                 }
123                 else if((/\.(mpe?g|flv|mov|avi|swf|mp4|mkv|webm|wmv|3gp)$/i).test(filename)) {
124                     format = 'video';
125                 }
126                 else if((/\.(mp3|ogg|wav|wma|amr|aac)$/i).test(filename)) {
127                     format = 'audio';
128                 }
129                 else format = 'file';
130             }
131             
132             var fileIcons = {
133                 'file' : 'fa fa-file',
134                 'image' : 'fa fa-picture-o file-image',
135                 'video' : 'fa fa-film file-video',
136                 'audio' : 'fa fa-music file-audio'
137             };
138             var fileIcon = fileIcons[format];
139             
140             
141             if(!this.well_style) this.$label.find('.ace-file-name').attr({'data-title':filename}).find(ace.vars['.icon']).attr('class', ace.vars['icon'] + fileIcon);
142             else {
143                 this.$label.append('<span class="ace-file-name" data-title="'+filename+'"><i class="'+ ace.vars['icon'] + fileIcon+'"></i></span>');
144                 var type = (inner_call === true && hasFile && files[i] instanceof File) ? $.trim(files[i].type) : '';
145                 var can_preview = hasFileReader && this.settings.thumbnail 
146                         &&
147                         ( (type.length > 0 && type.match('image')) || (type.length == 0 && format == 'image') )//the second one is for older Android's default browser which gives an empty text for file.type
148                 if(can_preview) {
149                     var self = this;
150                     $.when(preview_image.call(this, files[i])).fail(function(result){
151                         //called on failure to load preview
152                         if(self.settings.preview_error) self.settings.preview_error.call(self, filename, result.code);
153                     })
154                 }
155             }
156         }
157         
158         return true;
159     }
160     
161     Ace_File_Input.prototype.reset_input = function() {
162         this.reset_input_ui();
163         this.reset_input_field();
164     }
165     
166     Ace_File_Input.prototype.reset_input_ui = function() {
167          this.$label.attr({'data-title':this.settings.btn_choose, 'class':'ace-file-container'})
168             .find('.ace-file-name:first').attr({'data-title':this.settings.no_file , 'class':'ace-file-name'})
169             .find(ace.vars['.icon']).attr('class', ace.vars['icon'] + this.settings.no_icon)
170             .prev('img').remove();
171             if(!this.settings.no_icon) this.$label.find(ace.vars['.icon']).remove();
172         
173         this.$label.find('.ace-file-name').not(':first').remove();
174         
175         this.reset_input_data();
176         
177         //if(ace.vars['old_ie']) ace.helper.redraw(this.$container[0]);
178     }
179     Ace_File_Input.prototype.reset_input_field = function() {
180         //http://stackoverflow.com/questions/1043957/clearing-input-type-file-using-jquery/13351234#13351234
181         this.$element.wrap('<form>').parent().get(0).reset();
182         this.$element.unwrap();
183         
184         //strangely when reset is called on this temporary inner form
185         //only **IE9/10** trigger 'reset' on the outer form as well
186         //and as we have mentioned to reset input on outer form reset
187         //it causes infinite recusrsion by coming back to reset_input_field
188         //thus calling reset again and again and again
189         //so because when "reset" button of outer form is hit, file input is automatically reset
190         //we just reset_input_ui to avoid recursion
191     }
192     Ace_File_Input.prototype.reset_input_data = function() {
193         if(this.$element.data('ace_input_files')) {
194             this.$element.removeData('ace_input_files');
195             this.$element.removeData('ace_input_method');
196         }
197     }
198
199     Ace_File_Input.prototype.enable_reset = function(can_reset) {
200         this.can_reset = can_reset;
201     }
202
203     Ace_File_Input.prototype.disable = function() {
204         this.disabled = true;
205         this.$element.attr('disabled', 'disabled').addClass('disabled');
206     }
207     Ace_File_Input.prototype.enable = function() {
208         this.disabled = false;
209         this.$element.removeAttr('disabled').removeClass('disabled');
210     }
211
212     Ace_File_Input.prototype.files = function() {
213         return $(this).data('ace_input_files') || null;
214     }
215     Ace_File_Input.prototype.method = function() {
216         return $(this).data('ace_input_method') || '';
217     }
218     
219     Ace_File_Input.prototype.update_settings = function(new_settings) {
220         this.settings = $.extend({}, this.settings, new_settings);
221         this.apply_settings();
222     }
223     
224     Ace_File_Input.prototype.loading = function(is_loading) {
225         if(is_loading === false) {
226             this.$container.find('.ace-file-overlay').remove();
227             this.element.removeAttribute('readonly');
228         }
229         else {
230             var inside = typeof is_loading === 'string' ? is_loading : '<i class="overlay-content fa fa-spin fa-spinner orange2 fa-2x"></i>';
231             var loader = this.$container.find('.ace-file-overlay');
232             if(loader.length == 0) {
233                 loader = $('<div class="ace-file-overlay"></div>').appendTo(this.$container);
234                 loader.on('click tap', function(e) {
235                     e.stopImmediatePropagation();
236                     e.preventDefault();
237                     return false;
238                 });
239                 
240                 this.element.setAttribute('readonly' , 'true');//for IE
241             }
242             loader.empty().append(inside);
243         }
244     }
245
246
247
248     var enable_drop_functionality = function() {
249         var self = this;
250         
251         var dropbox = this.$element.parent();
252         dropbox
253         .off('dragenter')
254         .on('dragenter', function(e){
255             e.preventDefault();
256             e.stopPropagation();
257         })
258         .off('dragover')
259         .on('dragover', function(e){
260             e.preventDefault();
261             e.stopPropagation();
262         })
263         .off('drop')
264         .on('drop', function(e){
265             e.preventDefault();
266             e.stopPropagation();
267
268             if(self.disabled) return;
269         
270             var dt = e.originalEvent.dataTransfer;
271             var file_list = dt.files;
272             if(!self.multi && file_list.length > 1) {//single file upload, but dragged multiple files
273                 var tmpfiles = [];
274                 tmpfiles.push(file_list[0]);
275                 file_list = tmpfiles;//keep only first file
276             }
277             
278             
279             file_list = processFiles.call(self, file_list, true);//true means files have been selected, not dropped
280             if(file_list === false) return false;
281
282             self.$element.data('ace_input_method', 'drop');
283             self.$element.data('ace_input_files', file_list);//save files data to be used later by user
284
285             self.show_file_list(file_list , true);
286             
287             self.$element.triggerHandler('change' , [true]);//true means ace_inner_call
288             return true;
289         });
290     }
291     
292     
293     var handle_on_change = function() {
294         var file_list = this.element.files || [this.element.value];/** make it an array */
295         
296         file_list = processFiles.call(this, file_list, false);//false means files have been selected, not dropped
297         if(file_list === false) return false;
298         
299         this.$element.data('ace_input_method', 'select');
300         this.$element.data('ace_input_files', file_list);
301         
302         this.show_file_list(file_list , true);
303         
304         return true;
305     }
306
307
308
309     var preview_image = function(file) {
310         var self = this;
311         var $span = self.$label.find('.ace-file-name:last');//it should be out of onload, otherwise all onloads may target the same span because of delays
312         
313         var deferred = new $.Deferred;
314         
315         var getImage = function(src, $file) {
316             $span.prepend("<img class='middle' style='display:none;' />");
317             var img = $span.find('img:last').get(0);
318         
319             $(img).one('load', function() {
320                 imgLoaded.call(null, img, $file);
321             }).one('error', function() {
322                 imgFailed.call(null, img);
323             });
324
325             img.src = src;
326         }
327         var imgLoaded = function(img, $file) {
328             //if image loaded successfully
329             
330             var size = self.settings['previewSize'];
331             
332             if(!size) {
333                 if(self.settings['previewWidth'] || self.settings['previewHeight']) {
334                     size = {previewWidth: self.settings['previewWidth'], previewHeight: self.settings['previewHeight']}
335                 }
336                 else {
337                     size = 50;
338                     if(self.settings.thumbnail == 'large') size = 150;
339                 }
340             }
341             if(self.settings.thumbnail == 'fit') size = $span.width();
342             else if(typeof size == 'number') size = parseInt(Math.min(size, $span.width()));
343
344
345             var thumb = get_thumbnail(img, size/**, file.type*/);
346             if(thumb == null) {
347                 //if making thumbnail fails
348                 $(this).remove();
349                 deferred.reject({code:Ace_File_Input.error['THUMBNAIL_FAILED']});
350                 return;
351             }
352             
353             
354             var showPreview = true;
355             //add width/height info to "file" and trigger preview finished event for each image!
356             if($file && $file instanceof File) {
357                 $file.width = thumb.width;
358                 $file.height = thumb.height;
359                 self.$element.trigger('file.preview.ace', {'file': $file});
360                 
361                 var event
362                 self.$element.trigger( event = new $.Event('file.preview.ace'), {'file': $file} );
363                 if ( event.isDefaultPrevented() ) showPreview = false;
364             }
365
366
367             if(showPreview) {
368                 var w = thumb.previewWidth, h = thumb.previewHeight;
369                 if(self.settings.thumbnail == 'small') {w=h=parseInt(Math.max(w,h))}
370                 else $span.addClass('large');
371
372                 $(img).css({'background-image':'url('+thumb.src+')' , width:w, height:h})
373                         .data('thumb', thumb.src)
374                         .attr({src:'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=='})
375                         .show()
376             }
377
378             ///////////////////
379             deferred.resolve();
380         }
381         var imgFailed = function(img) {
382             //for example when a file has image extenstion, but format is something else
383             $span.find('img').remove();
384             deferred.reject({code:Ace_File_Input.error['IMAGE_LOAD_FAILED']});
385         }
386         
387         if(hasFile && file instanceof File) {
388             var reader = new FileReader();
389             reader.onload = function (e) {
390                 getImage(e.target.result, file);
391             }
392             reader.onerror = function (e) {
393                 deferred.reject({code:Ace_File_Input.error['FILE_LOAD_FAILED']});
394             }
395             reader.readAsDataURL(file);
396         }
397         else {
398             if(file instanceof Object && file.hasOwnProperty('path')) {
399                 getImage(file.path, null);//file is a file name (path) --- this is used to pre-show user-selected image
400             }
401         }
402         
403         return deferred.promise();
404     }
405
406     var get_thumbnail = function(img, size, type) {
407         var imgWidth = img.width, imgHeight = img.height;
408         
409         //**IE10** is not giving correct width using img.width so we use $(img).width()
410         imgWidth = imgWidth > 0 ? imgWidth : $(img).width()
411         imgHeight = imgHeight > 0 ? imgHeight : $(img).height()
412
413         var previewSize = false, previewHeight = false, previewWidth = false;
414         if(typeof size == 'number') previewSize = size;
415         else if(size instanceof Object) {
416             if(size['previewWidth'] && !size['previewHeight']) previewWidth = size['previewWidth'];
417             else if(size['previewHeight'] && !size['previewWidth']) previewHeight = size['previewHeight'];
418             else if(size['previewWidth'] && size['previewHeight']) {
419                 previewWidth = size['previewWidth'];
420                 previewHeight = size['previewHeight'];
421             }
422         }
423         
424         if(previewSize) {
425             if(imgWidth > imgHeight) {
426                 previewWidth = previewSize;
427                 previewHeight = parseInt(imgHeight/imgWidth * previewWidth);
428             } else {
429                 previewHeight = previewSize;
430                 previewWidth = parseInt(imgWidth/imgHeight * previewHeight);
431             }
432         }
433         else {
434             if(!previewHeight && previewWidth) {
435                 previewHeight = parseInt(imgHeight/imgWidth * previewWidth);
436             }
437             else if(previewHeight && !previewWidth) {
438                 previewWidth = parseInt(imgWidth/imgHeight * previewHeight);
439             }
440         }
441         
442         
443     
444         var dataURL
445         try {
446             var canvas = document.createElement('canvas');
447             canvas.width = previewWidth; canvas.height = previewHeight;
448             var context = canvas.getContext('2d');
449             context.drawImage(img, 0, 0, imgWidth, imgHeight, 0, 0, previewWidth, previewHeight);
450             dataURL = canvas.toDataURL(/*type == 'image/jpeg' ? type : 'image/png', 10*/)
451         } catch(e) {
452             dataURL = null;
453         }
454         if(! dataURL) return null;
455         
456
457         //there was only one image that failed in firefox completely randomly! so let's double check things
458         if( !( /^data\:image\/(png|jpe?g|gif);base64,[0-9A-Za-z\+\/\=]+$/.test(dataURL)) ) dataURL = null;
459         if(! dataURL) return null;
460         
461
462         return {src: dataURL, previewWidth: previewWidth, previewHeight: previewHeight, width: imgWidth, height: imgHeight};
463     }
464     
465
466     
467     var processFiles = function(file_list, dropped) {
468         var ret = checkFileList.call(this, file_list, dropped);
469         if(ret === -1) {
470             this.reset_input();
471             return false;
472         }
473         if( !ret || ret.length == 0 ) {
474             if( !this.$element.data('ace_input_files') ) this.reset_input();
475             //if nothing selected before, reset because of the newly unacceptable (ret=false||length=0) selection
476             //otherwise leave the previous selection intact?!!!
477             return false;
478         }
479         if (ret instanceof Array || (hasFileList && ret instanceof FileList)) file_list = ret;
480         
481         
482         ret = true;
483         if(this.settings.before_change) ret = this.settings.before_change.call(this.element, file_list, dropped);
484         if(ret === -1) {
485             this.reset_input();
486             return false;
487         }
488         if(!ret || ret.length == 0) {
489             if( !this.$element.data('ace_input_files') ) this.reset_input();
490             return false;
491         }
492         
493         //inside before_change you can return a modified File Array as result
494         if (ret instanceof Array || (hasFileList && ret instanceof FileList)) file_list = ret;
495         
496         return file_list;
497     }
498     
499     
500     var getExtRegex = function(ext) {
501         if(!ext) return null;
502         if(typeof ext === 'string') ext = [ext];
503         if(ext.length == 0) return null;
504         return new RegExp("\.(?:"+ext.join('|')+")$", "i");
505     }
506     var getMimeRegex = function(mime) {
507         if(!mime) return null;
508         if(typeof mime === 'string') mime = [mime];
509         if(mime.length == 0) return null;
510         return new RegExp("^(?:"+mime.join('|').replace(/\//g, "\\/")+")$", "i");
511     }
512     var checkFileList = function(files, dropped) {
513         var allowExt   = getExtRegex(this.settings.allowExt);
514
515         var denyExt    = getExtRegex(this.settings.denyExt);
516         
517         var allowMime  = getMimeRegex(this.settings.allowMime);
518
519         var denyMime   = getMimeRegex(this.settings.denyMime);
520
521         var maxSize    = this.settings.maxSize || false;
522         
523         if( !(allowExt || denyExt || allowMime || denyMime || maxSize) ) return true;//no checking required
524
525
526         var safe_files = [];
527         var error_list = {}
528         for(var f = 0; f < files.length; f++) {
529             var file = files[f];
530             
531             //file is either a string(file name) or a File object
532             var filename = !hasFile ? file : file.name;
533             if( allowExt && !allowExt.test(filename) ) {
534                 //extension not matching whitelist, so drop it
535                 if(!('ext' in error_list)) error_list['ext'] = [];
536                  error_list['ext'].push(filename);
537                 
538                 continue;
539             } else if( denyExt && denyExt.test(filename) ) {
540                 //extension is matching blacklist, so drop it
541                 if(!('ext' in error_list)) error_list['ext'] = [];
542                  error_list['ext'].push(filename);
543                 
544                 continue;
545             }
546
547             var type;
548             if( !hasFile ) {
549                 //in browsers that don't support FileReader API
550                 safe_files.push(file);
551                 continue;
552             }
553             else if((type = $.trim(file.type)).length > 0) {
554                 //there is a mimetype for file so let's check against are rules
555                 if( allowMime && !allowMime.test(type) ) {
556                     //mimeType is not matching whitelist, so drop it
557                     if(!('mime' in error_list)) error_list['mime'] = [];
558                      error_list['mime'].push(filename);
559                     continue;
560                 }
561                 else if( denyMime && denyMime.test(type) ) {
562                     //mimeType is matching blacklist, so drop it
563                     if(!('mime' in error_list)) error_list['mime'] = [];
564                      error_list['mime'].push(filename);
565                     continue;
566                 }
567             }
568
569             if( maxSize && file.size > maxSize ) {
570                 //file size is not acceptable
571                 if(!('size' in error_list)) error_list['size'] = [];
572                  error_list['size'].push(filename);
573                 continue;
574             }
575
576             safe_files.push(file)
577         }
578         
579     
580         
581         if(safe_files.length == files.length) return files;//return original file list if all are valid
582
583         /////////
584         var error_count = {'ext': 0, 'mime': 0, 'size': 0}
585         if( 'ext' in error_list ) error_count['ext'] = error_list['ext'].length;
586         if( 'mime' in error_list ) error_count['mime'] = error_list['mime'].length;
587         if( 'size' in error_list ) error_count['size'] = error_list['size'].length;
588         
589         var event
590         this.$element.trigger(
591             event = new $.Event('file.error.ace'), 
592             {
593                 'file_count': files.length,
594                 'invalid_count' : files.length - safe_files.length,
595                 'error_list' : error_list,
596                 'error_count' : error_count,
597                 'dropped': dropped
598             }
599         );
600         if ( event.isDefaultPrevented() ) return -1;//it will reset input
601         //////////
602
603         return safe_files;//return safe_files
604     }
605
606
607
608     ///////////////////////////////////////////
609     $.fn.aceFileInput = $.fn.ace_file_input = function (option,value) {
610         var retval;
611
612         var $set = this.each(function () {
613             var $this = $(this);
614             var data = $this.data('ace_file_input');
615             var options = typeof option === 'object' && option;
616
617             if (!data) $this.data('ace_file_input', (data = new Ace_File_Input(this, options)));
618             if (typeof option === 'string') retval = data[option](value);
619         });
620
621         return (retval === undefined) ? $set : retval;
622     };
623
624
625     $.fn.ace_file_input.defaults = {
626         style: false,
627         no_file: 'No File ...',
628         no_icon: 'fa fa-upload',
629         btn_choose: 'Choose',
630         btn_change: 'Change',
631         icon_remove: 'fa fa-times',
632         droppable: false,
633         thumbnail: false,//large, fit, small
634         
635         allowExt: null,
636         denyExt: null,
637         allowMime: null,
638         denyMime: null,
639         maxSize: false,
640         
641         previewSize: false,
642         previewWidth: false,
643         previewHeight: false,
644         
645         //callbacks
646         before_change: null,
647         before_remove: null,
648         preview_error: null
649      }
650
651
652 })(window.jQuery);