fileapi 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ window.FileAPI =
2
+ debug: true
3
+ staticPath: '/js/jquery.fileapi/FileAPI/'
@@ -0,0 +1,4 @@
1
+ #= require ./config
2
+ #= require ./FileAPI
3
+ #= require ./FileAPI.exif.js
4
+ #= require ./jquery.fileapi.js
@@ -0,0 +1,1377 @@
1
+ /**
2
+ * jQuery plugin for FileAPI v2+
3
+ * @auhtor RubaXa <trash@rubaxa.org>
4
+ */
5
+
6
+ /*jslint evil: true */
7
+ /*global jQuery, FileAPI*/
8
+ (function ($, api){
9
+ "use strict";
10
+
11
+ var
12
+ noop = $.noop
13
+ , oldJQ = !$.fn.prop
14
+ , propFn = oldJQ ? 'attr' : 'prop'
15
+
16
+ , _dataAttr = 'data-fileapi'
17
+ , _dataFileId = 'data-fileapi-id'
18
+
19
+ , _slice = [].slice
20
+ , _each = api.each
21
+ , _extend = api.extend
22
+
23
+ , _bind = function (ctx, fn) {
24
+ var args = _slice.call(arguments, 2);
25
+ return fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function (){
26
+ return fn.apply(ctx, args.concat(_slice.call(arguments)));
27
+ };
28
+ }
29
+
30
+ , _optDataAttr = function (name){
31
+ return '['+_dataAttr+'="'+name+'"]';
32
+ }
33
+
34
+ , _isID = function (selector){
35
+ return selector.indexOf('#') === 0;
36
+ }
37
+ ;
38
+
39
+
40
+
41
+ var Plugin = function (el, options){
42
+ this.$el = el = $(el).on('change.fileapi', 'input[type="file"]', _bind(this, this._onSelect));
43
+ this.el = el[0];
44
+
45
+ this._options = {}; // previous options
46
+ this.options = { // current options
47
+ url: 0,
48
+ data: {}, // additional POST-data
49
+ accept: 0, // accept mime types, "*" — unlimited
50
+ multiple: false, // single or multiple mode upload mode
51
+ paramName: 0, // POST-parameter name
52
+ dataType: 'json',
53
+ duplicate: false, // ignore duplicate
54
+
55
+ uploadRetry: 0,
56
+ networkDownRetryTimeout: 5000,
57
+
58
+ chunkSize: 0, // or chunk size in bytes, eg: .5 * FileAPI.MB
59
+ chunkUploadRetry: 3, // number of retries during upload chunks (html5)
60
+ chunkNetworkDownRetryTimeout: 2000,
61
+
62
+ maxSize: 0, // max file size, 0 — unlimited
63
+ maxFiles: 0, // 0 unlimited
64
+ imageSize: 0, // { minWidth: 320, minHeight: 240, maxWidth: 3840, maxHeight: 2160 }
65
+
66
+ sortFn: 0,
67
+ filterFn: 0,
68
+ autoUpload: false,
69
+
70
+ clearOnSelect: void 0,
71
+ clearOnComplete: void 0,
72
+
73
+ lang: {
74
+ B: 'bytes'
75
+ , KB: 'KB'
76
+ , MB: 'MB'
77
+ , GB: 'GB'
78
+ , TB: 'TB'
79
+ },
80
+ sizeFormat: '0.00',
81
+
82
+ imageOriginal: true,
83
+ imageTransform: 0,
84
+ imageAutoOrientation: !!FileAPI.support.exif,
85
+
86
+ elements: {
87
+ ctrl: {
88
+ upload: _optDataAttr('ctrl.upload'),
89
+ reset: _optDataAttr('ctrl.reset'),
90
+ abort: _optDataAttr('ctrl.abort')
91
+ },
92
+ empty: {
93
+ show: _optDataAttr('empty.show'),
94
+ hide: _optDataAttr('empty.hide')
95
+ },
96
+ emptyQueue: {
97
+ show: _optDataAttr('emptyQueue.show'),
98
+ hide: _optDataAttr('emptyQueue.hide')
99
+ },
100
+ active: {
101
+ show: _optDataAttr('active.show'),
102
+ hide: _optDataAttr('active.hide')
103
+ },
104
+ size: _optDataAttr('size'),
105
+ name: _optDataAttr('name'),
106
+ progress: _optDataAttr('progress'),
107
+ list: _optDataAttr('list'),
108
+ file: {
109
+ tpl: _optDataAttr('file.tpl'),
110
+ progress: _optDataAttr('file.progress'),
111
+ active: {
112
+ show: _optDataAttr('file.active.show'),
113
+ hide: _optDataAttr('file.active.hide')
114
+ },
115
+ preview: {
116
+ el: 0,
117
+ get: 0,
118
+ width: 0,
119
+ height: 0,
120
+ processing: 0
121
+ },
122
+ abort: _optDataAttr('file.abort'),
123
+ remove: _optDataAttr('file.remove'),
124
+ rotate: _optDataAttr('file.rotate')
125
+ },
126
+ dnd: {
127
+ el: _optDataAttr('dnd'),
128
+ hover: 'dnd_hover',
129
+ fallback: _optDataAttr('dnd.fallback')
130
+ }
131
+ },
132
+
133
+ onDrop: noop,
134
+ onDropHover: noop,
135
+
136
+ onSelect: noop,
137
+
138
+ onBeforeUpload: noop,
139
+ onUpload: noop,
140
+ onProgress: noop,
141
+ onComplete: noop,
142
+
143
+ onFilePrepare: noop,
144
+ onFileUpload: noop,
145
+ onFileProgress: noop,
146
+ onFileComplete: noop,
147
+
148
+ onFileRemove: null,
149
+ onFileRemoveCompleted: null
150
+ };
151
+
152
+ $.extend(true, this.options, options); // deep extend
153
+ options = this.options;
154
+
155
+ this.option('elements.file.preview.rotate', options.imageAutoOrientation);
156
+
157
+ if( !options.url ){
158
+ var url = this.$el.attr('action') || this.$el.find('form').attr('action');
159
+ if( url ){
160
+ options.url = url;
161
+ } else {
162
+ this._throw('url — is not defined');
163
+ }
164
+ }
165
+
166
+
167
+ this.$files = this.$elem('list');
168
+
169
+ this.$fileTpl = this.$elem('file.tpl');
170
+ this.itemTplFn = $.fn.fileapi.tpl( $('<div/>').append( this.$elem('file.tpl') ).html() );
171
+
172
+
173
+ _each(options, function (value, option){
174
+ this._setOption(option, value);
175
+ }, this);
176
+
177
+
178
+ this.$el
179
+ .on('reset.fileapi', _bind(this, this._onReset))
180
+ .on('submit.fileapi', _bind(this, this._onSubmit))
181
+ .on('upload.fileapi progress.fileapi complete.fileapi', _bind(this, this._onUploadEvent))
182
+ .on('fileupload.fileapi fileprogress.fileapi filecomplete.fileapi', _bind(this, this._onFileUploadEvent))
183
+ .on('click', '['+_dataAttr+']', _bind(this, this._onActionClick))
184
+ ;
185
+
186
+
187
+ // Controls
188
+ var ctrl = options.elements.ctrl;
189
+ if( ctrl ){
190
+ this._listen('click', ctrl.reset, _bind(this, this._onReset));
191
+ this._listen('click', ctrl.upload, _bind(this, this._onSubmit));
192
+ this._listen('click', ctrl.abort, _bind(this, this._onAbort));
193
+ }
194
+
195
+ // Drag'n'Drop
196
+ var dnd = FileAPI.support.dnd;
197
+ this.$elem('dnd.el', true).toggle(dnd);
198
+ this.$elem('dnd.fallback').toggle(!dnd);
199
+
200
+ if( dnd ){
201
+ this.$elem('dnd.el', true).dnd(_bind(this, this._onDropHover), _bind(this, this._onDrop));
202
+ }
203
+
204
+
205
+ this.$progress = this.$elem('progress');
206
+
207
+ if( options.clearOnSelect === void 0 ){
208
+ options.clearOnSelect = !options.multiple;
209
+ }
210
+
211
+ this.clear();
212
+
213
+ if( $.isArray(options.files) ){
214
+ this.files = $.map(options.files, function (file){
215
+ if( $.type(file) === 'string' ){
216
+ file = { src: file, size: 0 };
217
+ }
218
+
219
+ file.name = file.name || file.src.split('/').pop();
220
+ file.type = file.type || /\.(jpe?g|png|bmp|gif|tiff?)/i.test(file.src) && 'image/' + file.src.split('.').pop(); // @todo: use FileAPI.getMimeType (v2.1+)
221
+ file.complete = true;
222
+
223
+ return file;
224
+ });
225
+
226
+ this._redraw();
227
+ }
228
+ };
229
+
230
+
231
+ Plugin.prototype = {
232
+ constructor: Plugin,
233
+
234
+ _throw: function (msg){
235
+ throw "jquery.fileapi: " + msg;
236
+ },
237
+
238
+ _getFiles: function (evt, fn){
239
+ var
240
+ opts = this.options
241
+ , maxSize = opts.maxSize
242
+ , maxFiles = opts.maxFiles
243
+ , filterFn = opts.filterFn
244
+ , countFiles = this.files.length
245
+ , files = api.getFiles(evt)
246
+ , data = {
247
+ all: files
248
+ , files: []
249
+ , other: []
250
+ , duplicate: opts.duplicate ? [] : this._extractDuplicateFiles(files)
251
+ }
252
+ , imageSize = opts.imageSize
253
+ , _this = this
254
+ ;
255
+
256
+ if( imageSize || filterFn ){
257
+ api.filterFiles(files, function (file, info){
258
+ if( info && imageSize ){
259
+ _checkFileByCriteria(file, 'minWidth', imageSize, info);
260
+ _checkFileByCriteria(file, 'minHeight', imageSize, info);
261
+ _checkFileByCriteria(file, 'maxWidth', imageSize, info);
262
+ _checkFileByCriteria(file, 'maxHeight', imageSize, info);
263
+ }
264
+
265
+ _checkFileByCriteria(file, 'maxSize', maxSize, file.size);
266
+
267
+ return !file.errors && (!filterFn || filterFn(file, info));
268
+ }, function (success, rejected){
269
+ _extractFilesOverLimit(maxFiles, countFiles, success, rejected);
270
+
271
+ data.other = rejected;
272
+ data.files = success;
273
+
274
+ fn.call(_this, data);
275
+ });
276
+ } else {
277
+ _each(files, function (file){
278
+ _checkFileByCriteria(file, 'maxSize', maxSize, file.size);
279
+ data[file.errors ? 'other' : 'files'].push(file);
280
+ });
281
+
282
+ _extractFilesOverLimit(maxFiles, countFiles, data.files, data.other);
283
+ fn.call(_this, data);
284
+ }
285
+ },
286
+
287
+ _extractDuplicateFiles: function (list/**Array*/){
288
+ var duplicates = [], i = list.length, files = this.files, j;
289
+
290
+ while( i-- ){
291
+ j = files.length;
292
+ while( j-- ){
293
+ if( this._fileCompare(list[i], files[j]) ){
294
+ duplicates.push( list.splice(i, 1) );
295
+ break;
296
+ }
297
+ }
298
+ }
299
+
300
+ return duplicates;
301
+ },
302
+
303
+ _fileCompare: function (A/**File*/, B/**File*/){
304
+ return (A.size == B.size) && (A.name == B.name);
305
+ },
306
+
307
+ _getFormatedSize: function (size){
308
+ var opts = this.options, postfix = 'B';
309
+
310
+ if( size >= api.TB ){
311
+ size /= api[postfix = 'TB'];
312
+ }
313
+ else if( size >= api.GB ){
314
+ size /= api[postfix = 'GB'];
315
+ }
316
+ else if( size >= api.MB ){
317
+ size /= api[postfix = 'MB'];
318
+ }
319
+ else if( size >= api.KB ){
320
+ size /= api[postfix = 'KB'];
321
+ }
322
+
323
+ return opts.sizeFormat.replace(/^\d+([^\d]+)(\d*)/, function (_, separator, fix){
324
+ size = (parseFloat(size) || 0).toFixed(fix.length);
325
+ return (size + '').replace('.', separator) +' '+ opts.lang[postfix];
326
+ });
327
+ },
328
+
329
+ _onSelect: function (evt){
330
+ if( this.options.clearOnSelect ){
331
+ this.queue = [];
332
+ this.files = [];
333
+ }
334
+
335
+ this._getFiles(evt, _bind(this, function (data){
336
+ if( data.all.length && this.emit('select', data) !== false ){
337
+ this.add(data.files);
338
+ }
339
+
340
+ // Reset input
341
+ FileAPI.reset(evt.target);
342
+ }));
343
+ },
344
+
345
+ _onActionClick: function (evt){
346
+ var
347
+ el = evt.currentTarget
348
+ , act = $.attr(el, _dataAttr)
349
+ , uid = $(el).closest('['+_dataFileId+']', this.$el).attr(_dataFileId)
350
+ , file = this._getFile(uid)
351
+ , prevent = true
352
+ ;
353
+
354
+ if( !this.$file(uid).attr('disabled') ){
355
+ if( 'file.remove' == act ){
356
+ if( file && this.emit('fileRemove' + (file.complete ? 'Completed' : ''), file) ){
357
+ this.remove(uid);
358
+ }
359
+ }
360
+ else if( /^file\.rotate/.test(act) ){
361
+ this.rotate(uid, (/ccw/.test(act) ? '-=90' : '+=90'));
362
+ }
363
+ else {
364
+ prevent = false;
365
+ }
366
+ }
367
+
368
+ if( prevent ){
369
+ evt.preventDefault();
370
+ }
371
+ },
372
+
373
+ _listen: function (name, selector, fn){
374
+ selector && _each($.trim(selector).split(','), function (selector){
375
+ selector = $.trim(selector);
376
+
377
+ if( _isID(selector) ){
378
+ $(selector).on(name+'.fileapi', fn);
379
+ } else {
380
+ this.$el.on(name+'.fileapi', selector, fn);
381
+ }
382
+ }, this);
383
+ },
384
+
385
+ _onSubmit: function (evt){
386
+ evt.preventDefault();
387
+ this.upload();
388
+ },
389
+
390
+ _onReset: function (evt){
391
+ evt.preventDefault();
392
+ this.clear(true);
393
+ },
394
+
395
+ _onAbort: function (evt){
396
+ evt.preventDefault();
397
+ this.abort();
398
+ },
399
+
400
+ _onDrop: function (files){
401
+ this._getFiles(files, function (data){
402
+ if( this.emit('drop', data) !== false ){
403
+ this.add(data.files);
404
+ }
405
+ });
406
+ },
407
+
408
+ _onDropHover: function (state, evt){
409
+ if( this.emit('dropHover', { state: state, event: evt }) !== false ){
410
+ var hover = this.option('elements.dnd.hover');
411
+ if( hover ){
412
+ $(evt.currentTarget).toggleClass(hover, state);
413
+ }
414
+ }
415
+ },
416
+
417
+ _getFile: function (uid){
418
+ return api.filter(this.files, function (file){ return api.uid(file) == uid; })[0];
419
+ },
420
+
421
+ _getUploadEvent: function (xhr, extra){
422
+ var evt = {
423
+ xhr: xhr
424
+ , file: this.xhr.currentFile
425
+ , files: this.xhr.files
426
+ , widget: this
427
+ };
428
+ return _extend(evt, extra);
429
+ },
430
+
431
+ _emitUploadEvent: function (prefix, file, xhr){
432
+ var evt = this._getUploadEvent(xhr);
433
+ this.emit(prefix+'Upload', evt);
434
+ },
435
+
436
+ _emitProgressEvent: function (prefix, event, file, xhr){
437
+ var evt = this._getUploadEvent(xhr, event);
438
+ this.emit(prefix+'Progress', evt);
439
+ },
440
+
441
+ _emitCompleteEvent: function (prefix, err, xhr, file){
442
+ var evt = this._getUploadEvent(xhr, {
443
+ error: err
444
+ , status: xhr.status
445
+ , statusText: xhr.statusText
446
+ , result: xhr.responseText
447
+ });
448
+
449
+ if( prefix == 'file' ){
450
+ file.complete = true;
451
+ }
452
+
453
+ if( this.options.dataType == 'json' ){
454
+ try {
455
+ evt.result = $.parseJSON(evt.result);
456
+ } catch (err){
457
+ evt.error = err;
458
+ }
459
+ }
460
+
461
+ this.emit(prefix+'Complete', evt);
462
+ },
463
+
464
+ _onUploadEvent: function (evt, ui){
465
+ var _this = this, $progress = _this.$progress, type = evt.type;
466
+
467
+ if( type == 'progress' ){
468
+ $progress.stop().animate({ width: ui.loaded/ui.total*100 + '%' }, 300);
469
+ }
470
+ else if( type == 'upload' ){
471
+ // Начало загрузки
472
+ $progress.width(0);
473
+ }
474
+ else {
475
+ // Завершение загрузки
476
+ var fn = function (){
477
+ $progress.dequeue();
478
+ _this[_this.options.clearOnComplete ? 'clear' : 'dequeue']();
479
+ };
480
+
481
+ this.xhr = null;
482
+ this.active = false;
483
+
484
+ if( $progress.length ){
485
+ $progress.queue(fn);
486
+ } else {
487
+ fn();
488
+ }
489
+ }
490
+ },
491
+
492
+ _onFileUploadPrepare: function (file, opts){
493
+ var
494
+ uid = api.uid(file)
495
+ , deg = this._rotate[uid]
496
+ , crop = this._crop[uid]
497
+ , resize = this._resize[uid]
498
+ , evt = this._getUploadEvent(this.xhr)
499
+ ;
500
+
501
+ if( deg || crop || resize ){
502
+ var trans = $.extend(true, {}, opts.imageTransform || {});
503
+ deg = (deg != null) ? deg : (this.options.imageAutoOrientation ? 'auto' : void 0);
504
+
505
+ if( $.isEmptyObject(trans) || _isOriginTransform(trans) ){
506
+ _extend(trans, resize);
507
+
508
+ trans.crop = crop;
509
+ trans.rotate = deg;
510
+ }
511
+ else {
512
+ _each(trans, function (opts){
513
+ _extend(opts, resize);
514
+
515
+ opts.crop = crop;
516
+ opts.rotate = deg;
517
+ });
518
+ }
519
+
520
+ opts.imageTransform = trans;
521
+ }
522
+
523
+ evt.file = file;
524
+ evt.options = opts;
525
+
526
+ this.emit('filePrepare', evt);
527
+ },
528
+
529
+ _onFileUploadEvent: function (evt, ui){
530
+ var
531
+ _this = this
532
+ , type = evt.type.substr(4)
533
+ , uid = api.uid(ui.file)
534
+ , $file = this.$file(uid)
535
+ , $progress = this._$fileprogress
536
+ , opts = this.options
537
+ ;
538
+
539
+ if( this.__fileId !== uid ){
540
+ this.__fileId = uid;
541
+ this._$fileprogress = $progress = this.$elem('file.progress', $file);
542
+ }
543
+
544
+ if( type == 'progress' ){
545
+ $progress.stop().animate({ width: ui.loaded/ui.total*100 + '%' }, 300);
546
+ }
547
+ else if( type == 'upload' || type == 'complete' ){
548
+ var fn = function (){
549
+ var
550
+ elem = 'file.'+ type
551
+ , $remove = _this.$elem('file.remove', $file)
552
+ ;
553
+
554
+ if( type == 'upload' ){
555
+ $remove.hide();
556
+ $progress.width(0);
557
+ } else if( opts.onRemoveCompleted ){
558
+ $remove.show();
559
+ }
560
+
561
+ $progress.dequeue();
562
+
563
+ _this.$elem(elem + '.show', $file).show();
564
+ _this.$elem(elem + '.hide', $file).hide();
565
+ };
566
+
567
+ if( $progress.length ){
568
+ $progress.queue(fn);
569
+ } else {
570
+ fn();
571
+ }
572
+
573
+ if( type == 'complete' ){
574
+ this.uploaded.push(ui.file);
575
+ delete this._rotate[uid];
576
+ }
577
+ }
578
+ },
579
+
580
+ _redraw: function (clearFiles/**Boolean*/, clearPreview/**Boolean*/){
581
+ var
582
+ files = this.files
583
+ , active = !!this.active
584
+ , empty = !files.length && !active
585
+ , emptyQueue = !this.queue.length && !active
586
+ , name = []
587
+ , size = 0
588
+ , $files = this.$files
589
+ , offset = $files.children().length
590
+ , preview = this.option('elements.preview')
591
+ , filePreview = this.option('elements.file.preview')
592
+ ;
593
+
594
+ if( clearFiles ){
595
+ this.$files.empty();
596
+ }
597
+
598
+ if( clearPreview && preview && preview.el && !this.queue.length ) {
599
+ this.$(preview.el).empty();
600
+ }
601
+
602
+ _each(files, function (file, i){
603
+ var uid = api.uid(file);
604
+
605
+ name.push(file.name);
606
+ size += file.complete ? 0 : file.size;
607
+
608
+ if( preview && preview.el ){
609
+ this._makeFilePreview(uid, file, preview, true);
610
+ }
611
+ else if( $files.length && !this.$file(uid).length ){
612
+ var
613
+ html = this.itemTplFn({
614
+ $idx: offset + i
615
+ , uid: uid
616
+ , name: file.name
617
+ , type: file.type
618
+ , size: file.size
619
+ , complete: !!file.complete
620
+ , sizeText: this._getFormatedSize(file.size)
621
+ })
622
+
623
+ , $file = $(html).attr(_dataFileId, uid)
624
+ ;
625
+
626
+ file.$el = $file;
627
+ $files.append( $file );
628
+
629
+ if( file.complete ){
630
+ this.$elem('file.upload.hide', $file).hide();
631
+ this.$elem('file.complete.hide', $file).hide();
632
+ }
633
+
634
+ if( filePreview.el ){
635
+ this._makeFilePreview(uid, file, filePreview);
636
+ }
637
+ }
638
+ }, this);
639
+
640
+
641
+ this.$elem('name').text( name.join(', ') );
642
+ this.$elem('size').text( !emptyQueue ? this._getFormatedSize(size) : '' );
643
+
644
+
645
+ this.$elem('empty.show').toggle( empty );
646
+ this.$elem('empty.hide').toggle( !empty );
647
+
648
+
649
+ this.$elem('emptyQueue.show').toggle( emptyQueue );
650
+ this.$elem('emptyQueue.hide').toggle( !emptyQueue );
651
+
652
+
653
+ this.$elem('active.show').toggle( active );
654
+ this.$elem('active.hide').toggle( !active );
655
+
656
+
657
+ this.$('.js-fileapi-wrapper,:file')
658
+ [active ? 'attr' : 'removeAttr']('aria-disabled', active)
659
+ [propFn]('disabled', active)
660
+ ;
661
+
662
+ // Upload control
663
+ this._disableElem('ctrl.upload', emptyQueue || active);
664
+
665
+ // Reset control
666
+ this._disableElem('ctrl.reset', emptyQueue || active);
667
+
668
+ // Abort control
669
+ this._disableElem('ctrl.abort', !active);
670
+ },
671
+
672
+ _disableElem: function (name, state){
673
+ this.$elem(name)
674
+ [state ? 'attr' : 'removeAttr']('aria-disabled', 'disabled')
675
+ [propFn]('disabled', state)
676
+ ;
677
+ },
678
+
679
+ _makeFilePreview: function (uid, file, opts, global){
680
+ var
681
+ _this = this
682
+ , $el = global ? _this.$(opts.el) : _this.$file(uid).find(opts.el)
683
+ ;
684
+
685
+ if( !_this._crop[uid] ){
686
+ if( /^image/.test(file.type) ){
687
+ var
688
+ image = api.Image(file.src || file)
689
+ , doneFn = function (){
690
+ image.get(function (err, img){
691
+ if( !_this._crop[uid] ){
692
+ if( err ){
693
+ opts.get && opts.get($el, file);
694
+ _this.emit('previewError', [err, file]);
695
+ } else {
696
+ $el.html(img);
697
+ }
698
+ }
699
+ });
700
+ }
701
+ ;
702
+
703
+ if( opts.width ){
704
+ image.preview(opts.width, opts.height);
705
+ }
706
+
707
+ if( opts.rotate ){
708
+ image.rotate('auto');
709
+ }
710
+
711
+ if( opts.processing ){
712
+ opts.processing(file, image, doneFn);
713
+ } else {
714
+ doneFn();
715
+ }
716
+ }
717
+ else {
718
+ opts.get && opts.get($el, file);
719
+ }
720
+ }
721
+ },
722
+
723
+ emit: function (name, arg){
724
+ var opts = this.options, evt = $.Event(name.toLowerCase()), res;
725
+ evt.widget = this;
726
+ name = $.camelCase('on-'+name);
727
+ if( $.isFunction(opts[name]) ){
728
+ res = opts[name].call(this.el, evt, arg);
729
+ }
730
+ this.$el.triggerHandler(evt, arg);
731
+ return (res !== false) && !evt.isDefaultPrevented();
732
+ },
733
+
734
+ /**
735
+ * Add files to queue
736
+ * @param {Array} files
737
+ * @param {Boolean} [clear]
738
+ */
739
+ add: function (files, clear){
740
+ files = [].concat(files);
741
+
742
+ if( files.length ){
743
+ var
744
+ opts = this.options
745
+ , sortFn = opts.sortFn
746
+ ;
747
+
748
+ if( sortFn ){
749
+ files.sort(sortFn);
750
+ }
751
+
752
+ if( this.xhr ){
753
+ this.xhr.append(files);
754
+ }
755
+
756
+ this.queue = clear ? files : this.queue.concat(files);
757
+ this.files = clear ? files : this.files.concat(files);
758
+
759
+ if( this.active ){
760
+ this.xhr.append(files);
761
+ this._redraw(clear);
762
+ }
763
+ else {
764
+ this._redraw(clear);
765
+ if( this.options.autoUpload ){
766
+ this.upload();
767
+ }
768
+ }
769
+ }
770
+ },
771
+
772
+ /**
773
+ * Find element
774
+ * @param {String} sel
775
+ * @param {jQuery} [ctx]
776
+ * @return {jQuery}
777
+ */
778
+ $: function (sel, ctx){
779
+ if( typeof sel === 'string' ){
780
+ sel = /^#/.test(sel) ? sel : (ctx ? $(ctx) : this.$el).find(sel);
781
+ }
782
+ return $(sel);
783
+ },
784
+
785
+ /**
786
+ * @param {String} name
787
+ * @param {Boolean|jQuery} [up]
788
+ * @param {jQuery} [ctx]
789
+ * @return {jQuery}
790
+ */
791
+ $elem: function (name, up, ctx){
792
+ if( up && up.jquery ){
793
+ ctx = up;
794
+ up = false;
795
+ }
796
+
797
+ var sel = this.option('elements.'+name);
798
+ if( sel === void 0 && up ){
799
+ sel = this.option('elements.'+name.substr(0, name.lastIndexOf('.')));
800
+ }
801
+
802
+ return this.$($.type(sel) != 'string' && $.isEmptyObject(sel) ? [] : sel, ctx);
803
+ },
804
+
805
+ /**
806
+ * @param {String} uid
807
+ * @return {jQuery}
808
+ */
809
+ $file: function (uid){
810
+ return this.$('['+_dataFileId+'="'+uid+'"]');
811
+ },
812
+
813
+ /**
814
+ * Get/set options
815
+ * @param {String} name
816
+ * @param {*} [value]
817
+ * @return {*}
818
+ */
819
+ option: function (name, value){
820
+ if( value !== void 0 && $.isPlainObject(value) ){
821
+ _each(value, function (val, key){
822
+ this.option(name+'.'+key, val);
823
+ }, this);
824
+
825
+ return this;
826
+ }
827
+
828
+ var opts = this.options, val = opts[name], i = 0, len, part;
829
+
830
+ if( name.indexOf('.') != -1 ){
831
+ val = opts;
832
+ name = name.split('.');
833
+ len = name.length;
834
+
835
+ for( ; i < len; i++ ){
836
+ part = name[i];
837
+
838
+ if( (value !== void 0) && (len - i === 1) ){
839
+ val[part] = value;
840
+ break;
841
+ }
842
+ else if( !val[part] ){
843
+ val[part] = {};
844
+ }
845
+
846
+ val = val[part];
847
+ }
848
+ }
849
+ else if( value !== void 0 ){
850
+ opts[name] = value;
851
+ }
852
+
853
+ if( value !== void 0 ){
854
+ this._setOption(name, value, this._options[name]);
855
+ this._options[name] = value;
856
+ }
857
+
858
+ return value !== void 0 ? value : val;
859
+ },
860
+
861
+ _setOption: function (name, nVal){
862
+ switch( name ){
863
+ case 'accept':
864
+ case 'multiple':
865
+ case 'paramName':
866
+ if( name == 'paramName' ){ name = 'name'; }
867
+ if( nVal ){
868
+ this.$(':file')[propFn](name, nVal);
869
+ }
870
+ break;
871
+ }
872
+ },
873
+
874
+ serialize: function (){
875
+ var obj = {}, val;
876
+
877
+ this.$el.find(':input').each(function(name, node){
878
+ if(
879
+ (name = node.name) && !node.disabled
880
+ && (node.checked || /select|textarea|input/i.test(node.nodeName) && !/checkbox|radio|file/i.test(node.type))
881
+ ){
882
+ val = $(node).val();
883
+ if( obj[name] !== void 0 ){
884
+ if( !obj[name].push ){
885
+ obj[name] = [obj[name]];
886
+ }
887
+
888
+ obj[name].push(val);
889
+ } else {
890
+ obj[name] = val;
891
+ }
892
+ }
893
+ });
894
+
895
+ return obj;
896
+ },
897
+
898
+ upload: function (){
899
+ if( !this.active && this.emit('beforeUpload', { widget: this, files: this.queue }) ){
900
+ this.active = true;
901
+
902
+ var
903
+ $el = this.$el
904
+ , opts = this.options
905
+ , files = {}
906
+ , uploadOpts = {
907
+ url: opts.url
908
+ , data: _extend({}, this.serialize(), opts.data)
909
+ , headers: opts.headers
910
+ , files: files
911
+
912
+ , uploadRetry: opts.uploadRetry
913
+ , networkDownRetryTimeout: opts.networkDownRetryTimeout
914
+ , chunkSize: opts.chunkSize
915
+ , chunkUploadRetry: opts.chunkUploadRetry
916
+ , chunkNetworkDownRetryTimeout: opts.chunkNetworkDownRetryTimeout
917
+
918
+ , prepare: _bind(this, this._onFileUploadPrepare)
919
+ , imageOriginal: opts.imageOriginal
920
+ , imageTransform: opts.imageTransform
921
+ , imageAutoOrientation: opts.imageAutoOrientation
922
+ }
923
+ ;
924
+
925
+ // Set files
926
+ files[$el.find(':file').attr('name') || 'files[]'] = this.queue;
927
+
928
+ // Add event listeners
929
+ _each(['Upload', 'Progress', 'Complete'], function (name){
930
+ var lowerName = name.toLowerCase();
931
+ uploadOpts[lowerName] = _bind(this, this['_emit'+name+'Event'], '');
932
+ uploadOpts['file'+lowerName] = _bind(this, this['_emit'+name+'Event'], 'file');
933
+ }, this);
934
+
935
+ // Start uploading
936
+ this.xhr = api.upload(uploadOpts);
937
+ this._redraw();
938
+ }
939
+ },
940
+
941
+ abort: function (text){
942
+ if( this.active && this.xhr ){
943
+ this.xhr.abort(text);
944
+ }
945
+ },
946
+
947
+ crop: function (file, coords){
948
+ var
949
+ uid = api.uid(file)
950
+ , opts = this.options
951
+ , preview = opts.multiple ? this.option('elements.file.preview') : opts.elements.preview
952
+ , $el = (opts.multiple ? this.$file(uid) : this.$el).find(preview && preview.el)
953
+ ;
954
+
955
+ if( $el.length ){
956
+ api.getInfo(file, _bind(this, function (err, info){
957
+ if( err ){
958
+ this.emit('previewError', [err, file]);
959
+ } else {
960
+ if( !$el.find('div>div').length ){
961
+ $el.html(
962
+ $('<div><div></div></div>')
963
+ .css(preview)
964
+ .css('overflow', 'hidden')
965
+ );
966
+ }
967
+
968
+ if( this.__cropFile !== file ){
969
+ this.__cropFile = file;
970
+ api.Image(file).rotate(opts.imageAutoOrientation ? 'auto' : 0).get(function (err, img){
971
+ $el.find('>div>div').html($(img).width('100%').height('100%'));
972
+ }, 'exactFit');
973
+ }
974
+
975
+
976
+ var
977
+ pw = preview.width, ph = preview.height
978
+ , mx = pw, my = ph
979
+ , rx = pw/coords.rw, ry = ph/coords.rh
980
+ ;
981
+
982
+ if( preview.keepAspectRatio ){
983
+ if (rx > 1 && ry > 1){ // image is smaller than preview (no scale)
984
+ rx = ry = 1;
985
+ my = coords.h;
986
+ mx = coords.w;
987
+
988
+ } else { // image is bigger than preview (scale)
989
+ if( rx < ry ){
990
+ ry = rx;
991
+ my = pw * coords.rh / coords.rw;
992
+ } else {
993
+ rx = ry;
994
+ mx = ph * coords.rw / coords.rh;
995
+ }
996
+ }
997
+ }
998
+
999
+ $el.find('>div>div').css({
1000
+ width: Math.round(rx * info[coords.flip ? 'height' : 'width'])
1001
+ , height: Math.round(ry * info[coords.flip ? 'width' : 'height'])
1002
+ , marginLeft: -Math.round(rx * coords.rx)
1003
+ , marginTop: -Math.round(ry * coords.ry)
1004
+ });
1005
+
1006
+ if( preview.keepAspectRatio ){ // create side gaps
1007
+ $el.find('>div').css({
1008
+ width: Math.round(mx)
1009
+ , height: Math.round(my)
1010
+ , marginLeft: mx < pw ? Math.round((pw - mx) / 2) : 0
1011
+ , marginTop: my < ph ? Math.round((ph - my) / 2) : 0
1012
+ });
1013
+ }
1014
+ }
1015
+ }));
1016
+ }
1017
+
1018
+ this._crop[uid] = coords;
1019
+ },
1020
+
1021
+ resize: function (file, width, height, type){
1022
+ this._resize[api.uid(file)] = {
1023
+ type: type
1024
+ , width: width
1025
+ , height: height
1026
+ };
1027
+ },
1028
+
1029
+ rotate: function (file, deg){
1030
+ var
1031
+ uid = $.type(file) == 'string' ? file : api.uid(file)
1032
+ , opts = this.options
1033
+ , preview = opts.multiple ? this.option('elements.file.preview') : opts.elements.preview
1034
+ , $el = (opts.multiple ? this.$file(uid) : this.$el).find(preview && preview.el)
1035
+ , _rotate = this._rotate
1036
+ ;
1037
+
1038
+ file = this._getFile(uid);
1039
+
1040
+ api.getInfo(file, function (err, info) {
1041
+ var orientation = info && info.exif && info.exif.Orientation,
1042
+ startDeg = opts.imageAutoOrientation && api.Image.exifOrientation[orientation] || 0;
1043
+
1044
+ if (_rotate[uid] == null) {
1045
+ _rotate[uid] = startDeg || 0;
1046
+ }
1047
+
1048
+ if( /([+-])=/.test(deg) ){
1049
+ _rotate[uid] = deg = (_rotate[uid] + (RegExp.$1 == '+' ? 1 : -1) * deg.substr(2));
1050
+ } else {
1051
+ _rotate[uid] = deg;
1052
+ }
1053
+
1054
+ // Store deg
1055
+ file.rotate = deg;
1056
+
1057
+ // Fix exif.rotate.auto
1058
+ deg -= startDeg;
1059
+ $el.css({
1060
+ '-webkit-transform': 'rotate('+deg+'deg)'
1061
+ , '-moz-transform': 'rotate('+deg+'deg)'
1062
+ , 'transform': 'rotate('+deg+'deg)'
1063
+ });
1064
+ });
1065
+ },
1066
+
1067
+ remove: function (file){
1068
+ var uid = typeof file == 'object' ? api.uid(file) : file;
1069
+
1070
+ this.$file(uid).remove();
1071
+
1072
+ this.queue = api.filter(this.queue, function (file){ return api.uid(file) != uid; });
1073
+ this.files = api.filter(this.files, function (file){ return api.uid(file) != uid; });
1074
+
1075
+ this._redraw();
1076
+ },
1077
+
1078
+ clear: function (all) {
1079
+ this._crop = {};
1080
+ this._resize = {};
1081
+ this._rotate = {}; // rotate deg
1082
+
1083
+ this.queue = [];
1084
+ this.files = []; // all files
1085
+ this.uploaded = []; // uploaded files
1086
+
1087
+ all = all === void 0 ? true : all;
1088
+ this._redraw(all, all);
1089
+ },
1090
+
1091
+ dequeue: function (){
1092
+ this.queue = [];
1093
+ this._redraw();
1094
+ },
1095
+
1096
+ widget: function (){
1097
+ return this;
1098
+ },
1099
+
1100
+ toString: function (){
1101
+ return '[jQuery.FileAPI object]';
1102
+ },
1103
+
1104
+ destroy: function (){
1105
+ this.$files.empty().append(this.$fileTpl);
1106
+
1107
+ this.$el
1108
+ .off('.fileapi')
1109
+ .removeData('fileapi')
1110
+ ;
1111
+
1112
+ _each(this.options.elements.ctrl, function (selector){
1113
+ _isID(selector) && $(selector).off('click.fileapi');
1114
+ });
1115
+ }
1116
+ };
1117
+
1118
+
1119
+ function _isOriginTransform(trans){
1120
+ var key;
1121
+ for( key in trans ){
1122
+ if( trans.hasOwnProperty(key) ){
1123
+ if( !(trans[key] instanceof Object || key === 'overlay') ){
1124
+ return true;
1125
+ }
1126
+ }
1127
+ }
1128
+ return false;
1129
+ }
1130
+
1131
+
1132
+ function _checkFileByCriteria(file, name, imageSize, info){
1133
+ if( imageSize && info ){
1134
+ var excepted = imageSize > 0 ? imageSize : imageSize[name],
1135
+ actual = info > 0 ? info : info[name.substr(3).toLowerCase()],
1136
+ val = (excepted - actual),
1137
+ isMax = /max/.test(name)
1138
+ ;
1139
+
1140
+ if( (isMax && val < 0) || (!isMax && val > 0) ){
1141
+ if( !file.errors ){
1142
+ file.errors = {};
1143
+ }
1144
+ file.errors[name] = Math.abs(val);
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
+
1150
+ function _extractFilesOverLimit(limit, countFiles, files, other){
1151
+ if( limit ){
1152
+ var delta = files.length - (limit - countFiles);
1153
+ if( delta > 0 ){
1154
+ _each(files.splice(0, delta), function (file, i){
1155
+ _checkFileByCriteria(file, 'maxFiles', -1, i);
1156
+ other.push(file);
1157
+ });
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+
1163
+
1164
+
1165
+
1166
+ /**
1167
+ * @export
1168
+ * @param {Object} options
1169
+ * @param {String} [value]
1170
+ */
1171
+ $.fn.fileapi = function (options, value){
1172
+ var plugin = this.data('fileapi');
1173
+
1174
+ if( plugin ){
1175
+ if( options === 'widget' ){
1176
+ return plugin;
1177
+ }
1178
+
1179
+ if( typeof options == 'string' ){
1180
+ var fn = plugin[options], res;
1181
+
1182
+ if( $.isFunction(fn) ){
1183
+ res = fn.apply(plugin, _slice.call(arguments, 1));
1184
+ }
1185
+ else if( fn === void 0 ){
1186
+ res = plugin.option(options, value);
1187
+ }
1188
+ else if( options === 'files' ){
1189
+ res = fn;
1190
+ }
1191
+
1192
+ return res === void 0 ? this : res;
1193
+ }
1194
+ } else if( options == null || typeof options == 'object' ){
1195
+ this.data('fileapi', new Plugin(this, options));
1196
+ }
1197
+
1198
+ return this;
1199
+ };
1200
+
1201
+
1202
+ $.fn.fileapi.version = '0.4.5';
1203
+ $.fn.fileapi.tpl = function (text){
1204
+ var index = 0;
1205
+ var source = "__b+='";
1206
+
1207
+ text.replace(/(?:&lt;|<)%([-=])?([\s\S]+?)%(?:&gt;|>)|$/g, function (match, mode, expr, offset){
1208
+ source += text.slice(index, offset).replace(/[\r\n"']/g, function (match){ return '\\'+match; });
1209
+
1210
+ if( expr ){
1211
+ if( mode ){
1212
+ source += "'+\n((__x=("+ expr +"))==null?'':" + (mode == "-" ? "__esc(__x)" : "__x")+")\n+'";
1213
+ } else {
1214
+ source += "';\n"+ expr +"\n__b+='";
1215
+ }
1216
+ }
1217
+
1218
+ index = offset + match.length;
1219
+ return match;
1220
+ });
1221
+
1222
+ return new Function("ctx", "var __x,__b=''," +
1223
+ "__esc=function(val){return typeof val=='string'?val.replace(/</g,'&lt;').replace(/\"/g,'&quot;'):val;};" +
1224
+ "with(ctx||{}){\n"+ source +"';\n}return __b;");
1225
+ };
1226
+
1227
+
1228
+ /**
1229
+ * FileAPI.Camera wrapper
1230
+ * @param {Object|String} options
1231
+ * @returns {jQuery}
1232
+ */
1233
+ $.fn.webcam = function (options){
1234
+ var el = this, ret = el, $el = $(el), key = 'fileapi-camera', inst = $el.data(key);
1235
+
1236
+ if( inst === true ){
1237
+ api.log("[webcam.warn] not ready.");
1238
+ ret = null;
1239
+ }
1240
+ else if( options === 'widget' ){
1241
+ ret = inst;
1242
+ }
1243
+ else if( options === 'destroy' ){
1244
+ inst.stop();
1245
+ $el.empty();
1246
+ }
1247
+ else if( inst ){
1248
+ ret = inst[options]();
1249
+ }
1250
+ else if( inst === false ){
1251
+ api.log("[webcam.error] does not work.");
1252
+ ret = null;
1253
+ }
1254
+ else {
1255
+ $el.data(key, true);
1256
+ options = _extend({ success: noop, error: noop }, options);
1257
+
1258
+ FileAPI.Camera.publish($el, options, function (err, cam){
1259
+ $el.data(key, err ? false : cam);
1260
+ options[err ? 'error' : 'success'].call(el, err || cam);
1261
+ });
1262
+ }
1263
+
1264
+ return ret;
1265
+ };
1266
+
1267
+
1268
+ /**
1269
+ * Wrapper for JCrop
1270
+ */
1271
+ $.fn.cropper = function (opts){
1272
+ var $el = this, file = opts.file;
1273
+
1274
+ if( typeof opts === 'string' ){
1275
+ $el.first().Jcrop.apply($el, arguments);
1276
+ }
1277
+ else {
1278
+ var
1279
+ minSize = opts.minSize || [0, 0],
1280
+ ratio = (opts.aspectRatio || minSize[0]/minSize[1])
1281
+ ;
1282
+
1283
+ if( $.isArray(opts.minSize) && opts.aspectRatio === void 0 && ratio > 0 ){
1284
+ opts.aspectRatio = ratio;
1285
+ }
1286
+
1287
+ api.getInfo(file, function (err, info){
1288
+ var
1289
+ Image = api.Image(file)
1290
+ , maxSize = opts.maxSize
1291
+ , deg = file.rotate
1292
+ ;
1293
+
1294
+ if( maxSize ){
1295
+ Image.resize(
1296
+ Math.max(maxSize[0], minSize[0])
1297
+ , Math.max(maxSize[1], minSize[1])
1298
+ , 'max'
1299
+ );
1300
+ }
1301
+
1302
+ Image.rotate(deg === void 0 ? 'auto' : deg).get(function (err, img){
1303
+ var
1304
+ selection = opts.selection
1305
+ , minSide = Math.min(img.width, img.height)
1306
+
1307
+ , selWidth = minSide
1308
+ , selHeight = minSide / ratio
1309
+
1310
+ , deg = FileAPI.Image.exifOrientation[info.exif && info.exif.Orientation] || 0
1311
+ ;
1312
+
1313
+ if( selection ){
1314
+ if( /%/.test(selection) || (selection > 0 && selection < 1) ){
1315
+ selection = parseFloat(selection, 10) / (selection > 0 ? 1 : 100);
1316
+ selWidth *= selection;
1317
+ selHeight *= selection;
1318
+ }
1319
+
1320
+ var
1321
+ selLeft = (img.width - selWidth)/2
1322
+ , selTop = (img.height - selHeight)/2
1323
+ ;
1324
+
1325
+ opts.setSelect = [selLeft|0, selTop|0, (selLeft + selWidth)|0, (selTop + selHeight)|0];
1326
+ }
1327
+
1328
+ _each(['onSelect', 'onChange'], function (name, fn){
1329
+ if( fn = opts[name] ){
1330
+ opts[name] = function (coords){
1331
+ var
1332
+ flip = deg % 180
1333
+ , ow = info.width
1334
+ , oh = info.height
1335
+ , fx = coords.x/img.width
1336
+ , fy = coords.y/img.height
1337
+ , fw = coords.w/img.width
1338
+ , fh = coords.h/img.height
1339
+ , x = ow * (flip ? fy : fx)
1340
+ , y = oh * (flip ? 1 - (coords.x + coords.w)/img.width : fy)
1341
+ , w = ow * (flip ? fh : fw)
1342
+ , h = oh * (flip ? fw : fh)
1343
+ ;
1344
+
1345
+
1346
+ fn({
1347
+ x: x
1348
+ , y: y
1349
+ , w: w
1350
+ , h: h
1351
+ , rx: fx * (flip ? oh : ow)
1352
+ , ry: fy * (flip ? ow : oh)
1353
+ , rw: fw * (flip ? oh : ow)
1354
+ , rh: fh * (flip ? ow : oh)
1355
+ , lx: coords.x // local coords
1356
+ , ly: coords.y
1357
+ , lw: coords.w
1358
+ , lh: coords.h
1359
+ , lx2: coords.x2
1360
+ , ly2: coords.y2
1361
+ , deg: deg
1362
+ , flip: flip
1363
+ });
1364
+ };
1365
+ }
1366
+ });
1367
+
1368
+ var $inner = $('<div/>').css('lineHeight', 0).append( $(img).css('margin', 0) );
1369
+ $el.html($inner);
1370
+ $inner.Jcrop(opts).trigger('resize');
1371
+ });
1372
+ });
1373
+ }
1374
+
1375
+ return $el;
1376
+ };
1377
+ })(jQuery, FileAPI);