helios 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +50 -21
  3. data/README.md +27 -2
  4. data/helios.gemspec +10 -6
  5. data/lib/helios.rb +10 -4
  6. data/lib/helios/backend.rb +29 -22
  7. data/lib/helios/backend/data.rb +10 -5
  8. data/lib/helios/backend/in-app-purchase.rb +4 -2
  9. data/lib/helios/backend/newsstand.rb +97 -0
  10. data/lib/helios/backend/passbook.rb +8 -2
  11. data/lib/helios/backend/push-notification.rb +25 -12
  12. data/lib/helios/commands/console.rb +4 -10
  13. data/lib/helios/commands/new.rb +0 -1
  14. data/lib/helios/commands/server.rb +4 -1
  15. data/lib/helios/frontend.rb +6 -14
  16. data/lib/helios/frontend/images/helios.svg +33 -0
  17. data/lib/helios/frontend/javascripts/helios.coffee +39 -3
  18. data/lib/helios/frontend/javascripts/helios/collections.coffee +10 -2
  19. data/lib/helios/frontend/javascripts/helios/models.coffee +4 -1
  20. data/lib/helios/frontend/javascripts/helios/router.coffee +13 -1
  21. data/lib/helios/frontend/javascripts/helios/views.coffee +82 -8
  22. data/lib/helios/frontend/javascripts/vendor/jquery/jquery.fileupload-ui.js +807 -0
  23. data/lib/helios/frontend/javascripts/vendor/jquery/jquery.fileupload.js +1201 -0
  24. data/lib/helios/frontend/javascripts/vendor/jquery/jquery.ui.widget.js +530 -0
  25. data/lib/helios/frontend/javascripts/vendor/linkheaders.js +117 -0
  26. data/lib/helios/frontend/stylesheets/_iphone.sass +1 -1
  27. data/lib/helios/frontend/stylesheets/screen.sass +2 -6
  28. data/lib/helios/frontend/templates/{entities.jst.tpl → data/entities.jst.tpl} +0 -0
  29. data/lib/helios/frontend/templates/{receipts.jst.tpl → in-app-purchase/receipts.jst.tpl} +0 -0
  30. data/lib/helios/frontend/templates/navigation.jst.tpl +31 -0
  31. data/lib/helios/frontend/templates/newsstand/issues.jst.tpl +16 -0
  32. data/lib/helios/frontend/templates/newsstand/new.jst.tpl +28 -0
  33. data/lib/helios/frontend/templates/{passes.jst.tpl → passbook/passes.jst.tpl} +0 -0
  34. data/lib/helios/frontend/templates/{compose.jst.tpl → push-notification/compose.jst.tpl} +0 -0
  35. data/lib/helios/frontend/templates/{devices.jst.tpl → push-notification/devices.jst.tpl} +0 -0
  36. data/lib/helios/frontend/views/index.haml +4 -28
  37. data/lib/helios/templates/.env.erb +1 -0
  38. data/lib/helios/templates/.gitignore +3 -0
  39. data/lib/helios/version.rb +1 -1
  40. metadata +86 -20
  41. data/lib/helios/frontend/stylesheets/_bariol.scss +0 -41
@@ -0,0 +1,1201 @@
1
+ /*
2
+ * jQuery File Upload Plugin 5.26
3
+ * https://github.com/blueimp/jQuery-File-Upload
4
+ *
5
+ * Copyright 2010, Sebastian Tschan
6
+ * https://blueimp.net
7
+ *
8
+ * Licensed under the MIT license:
9
+ * http://www.opensource.org/licenses/MIT
10
+ */
11
+
12
+ /*jslint nomen: true, unparam: true, regexp: true */
13
+ /*global define, window, document, File, Blob, FormData, location */
14
+
15
+ (function (factory) {
16
+ 'use strict';
17
+ if (typeof define === 'function' && define.amd) {
18
+ // Register as an anonymous AMD module:
19
+ define([
20
+ 'jquery',
21
+ 'jquery.ui.widget'
22
+ ], factory);
23
+ } else {
24
+ // Browser globals:
25
+ factory(window.jQuery);
26
+ }
27
+ }(function ($) {
28
+ 'use strict';
29
+
30
+ // The FileReader API is not actually used, but works as feature detection,
31
+ // as e.g. Safari supports XHR file uploads via the FormData API,
32
+ // but not non-multipart XHR file uploads:
33
+ $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
34
+ $.support.xhrFormDataFileUpload = !!window.FormData;
35
+
36
+ // The fileupload widget listens for change events on file input fields defined
37
+ // via fileInput setting and paste or drop events of the given dropZone.
38
+ // In addition to the default jQuery Widget methods, the fileupload widget
39
+ // exposes the "add" and "send" methods, to add or directly send files using
40
+ // the fileupload API.
41
+ // By default, files added via file input selection, paste, drag & drop or
42
+ // "add" method are uploaded immediately, but it is possible to override
43
+ // the "add" callback option to queue file uploads.
44
+ $.widget('blueimp.fileupload', {
45
+
46
+ options: {
47
+ // The drop target element(s), by the default the complete document.
48
+ // Set to null to disable drag & drop support:
49
+ dropZone: $(document),
50
+ // The paste target element(s), by the default the complete document.
51
+ // Set to null to disable paste support:
52
+ pasteZone: $(document),
53
+ // The file input field(s), that are listened to for change events.
54
+ // If undefined, it is set to the file input fields inside
55
+ // of the widget element on plugin initialization.
56
+ // Set to null to disable the change listener.
57
+ fileInput: undefined,
58
+ // By default, the file input field is replaced with a clone after
59
+ // each input field change event. This is required for iframe transport
60
+ // queues and allows change events to be fired for the same file
61
+ // selection, but can be disabled by setting the following option to false:
62
+ replaceFileInput: true,
63
+ // The parameter name for the file form data (the request argument name).
64
+ // If undefined or empty, the name property of the file input field is
65
+ // used, or "files[]" if the file input name property is also empty,
66
+ // can be a string or an array of strings:
67
+ paramName: undefined,
68
+ // By default, each file of a selection is uploaded using an individual
69
+ // request for XHR type uploads. Set to false to upload file
70
+ // selections in one request each:
71
+ singleFileUploads: true,
72
+ // To limit the number of files uploaded with one XHR request,
73
+ // set the following option to an integer greater than 0:
74
+ limitMultiFileUploads: undefined,
75
+ // Set the following option to true to issue all file upload requests
76
+ // in a sequential order:
77
+ sequentialUploads: false,
78
+ // To limit the number of concurrent uploads,
79
+ // set the following option to an integer greater than 0:
80
+ limitConcurrentUploads: undefined,
81
+ // Set the following option to true to force iframe transport uploads:
82
+ forceIframeTransport: false,
83
+ // Set the following option to the location of a redirect url on the
84
+ // origin server, for cross-domain iframe transport uploads:
85
+ redirect: undefined,
86
+ // The parameter name for the redirect url, sent as part of the form
87
+ // data and set to 'redirect' if this option is empty:
88
+ redirectParamName: undefined,
89
+ // Set the following option to the location of a postMessage window,
90
+ // to enable postMessage transport uploads:
91
+ postMessage: undefined,
92
+ // By default, XHR file uploads are sent as multipart/form-data.
93
+ // The iframe transport is always using multipart/form-data.
94
+ // Set to false to enable non-multipart XHR uploads:
95
+ multipart: true,
96
+ // To upload large files in smaller chunks, set the following option
97
+ // to a preferred maximum chunk size. If set to 0, null or undefined,
98
+ // or the browser does not support the required Blob API, files will
99
+ // be uploaded as a whole.
100
+ maxChunkSize: undefined,
101
+ // When a non-multipart upload or a chunked multipart upload has been
102
+ // aborted, this option can be used to resume the upload by setting
103
+ // it to the size of the already uploaded bytes. This option is most
104
+ // useful when modifying the options object inside of the "add" or
105
+ // "send" callbacks, as the options are cloned for each file upload.
106
+ uploadedBytes: undefined,
107
+ // By default, failed (abort or error) file uploads are removed from the
108
+ // global progress calculation. Set the following option to false to
109
+ // prevent recalculating the global progress data:
110
+ recalculateProgress: true,
111
+ // Interval in milliseconds to calculate and trigger progress events:
112
+ progressInterval: 100,
113
+ // Interval in milliseconds to calculate progress bitrate:
114
+ bitrateInterval: 500,
115
+ // By default, uploads are started automatically when adding files:
116
+ autoUpload: true,
117
+
118
+ // Additional form data to be sent along with the file uploads can be set
119
+ // using this option, which accepts an array of objects with name and
120
+ // value properties, a function returning such an array, a FormData
121
+ // object (for XHR file uploads), or a simple object.
122
+ // The form of the first fileInput is given as parameter to the function:
123
+ formData: function (form) {
124
+ return form.serializeArray();
125
+ },
126
+
127
+ // The add callback is invoked as soon as files are added to the fileupload
128
+ // widget (via file input selection, drag & drop, paste or add API call).
129
+ // If the singleFileUploads option is enabled, this callback will be
130
+ // called once for each file in the selection for XHR file uplaods, else
131
+ // once for each file selection.
132
+ // The upload starts when the submit method is invoked on the data parameter.
133
+ // The data object contains a files property holding the added files
134
+ // and allows to override plugin options as well as define ajax settings.
135
+ // Listeners for this callback can also be bound the following way:
136
+ // .bind('fileuploadadd', func);
137
+ // data.submit() returns a Promise object and allows to attach additional
138
+ // handlers using jQuery's Deferred callbacks:
139
+ // data.submit().done(func).fail(func).always(func);
140
+ add: function (e, data) {
141
+ if (data.autoUpload || (data.autoUpload !== false &&
142
+ ($(this).data('blueimp-fileupload') ||
143
+ $(this).data('fileupload')).options.autoUpload)) {
144
+ data.submit();
145
+ }
146
+ },
147
+
148
+ // Other callbacks:
149
+
150
+ // Callback for the submit event of each file upload:
151
+ // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
152
+
153
+ // Callback for the start of each file upload request:
154
+ // send: function (e, data) {}, // .bind('fileuploadsend', func);
155
+
156
+ // Callback for successful uploads:
157
+ // done: function (e, data) {}, // .bind('fileuploaddone', func);
158
+
159
+ // Callback for failed (abort or error) uploads:
160
+ // fail: function (e, data) {}, // .bind('fileuploadfail', func);
161
+
162
+ // Callback for completed (success, abort or error) requests:
163
+ // always: function (e, data) {}, // .bind('fileuploadalways', func);
164
+
165
+ // Callback for upload progress events:
166
+ // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
167
+
168
+ // Callback for global upload progress events:
169
+ // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
170
+
171
+ // Callback for uploads start, equivalent to the global ajaxStart event:
172
+ // start: function (e) {}, // .bind('fileuploadstart', func);
173
+
174
+ // Callback for uploads stop, equivalent to the global ajaxStop event:
175
+ // stop: function (e) {}, // .bind('fileuploadstop', func);
176
+
177
+ // Callback for change events of the fileInput(s):
178
+ // change: function (e, data) {}, // .bind('fileuploadchange', func);
179
+
180
+ // Callback for paste events to the pasteZone(s):
181
+ // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
182
+
183
+ // Callback for drop events of the dropZone(s):
184
+ // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
185
+
186
+ // Callback for dragover events of the dropZone(s):
187
+ // dragover: function (e) {}, // .bind('fileuploaddragover', func);
188
+
189
+ // Callback for the start of each chunk upload request:
190
+ // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
191
+
192
+ // Callback for successful chunk uploads:
193
+ // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
194
+
195
+ // Callback for failed (abort or error) chunk uploads:
196
+ // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
197
+
198
+ // Callback for completed (success, abort or error) chunk upload requests:
199
+ // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
200
+
201
+ // The plugin options are used as settings object for the ajax calls.
202
+ // The following are jQuery ajax settings required for the file uploads:
203
+ processData: false,
204
+ contentType: false,
205
+ cache: false
206
+ },
207
+
208
+ // A list of options that require a refresh after assigning a new value:
209
+ _refreshOptionsList: [
210
+ 'fileInput',
211
+ 'dropZone',
212
+ 'pasteZone',
213
+ 'multipart',
214
+ 'forceIframeTransport'
215
+ ],
216
+
217
+ _BitrateTimer: function () {
218
+ this.timestamp = +(new Date());
219
+ this.loaded = 0;
220
+ this.bitrate = 0;
221
+ this.getBitrate = function (now, loaded, interval) {
222
+ var timeDiff = now - this.timestamp;
223
+ if (!this.bitrate || !interval || timeDiff > interval) {
224
+ this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
225
+ this.loaded = loaded;
226
+ this.timestamp = now;
227
+ }
228
+ return this.bitrate;
229
+ };
230
+ },
231
+
232
+ _isXHRUpload: function (options) {
233
+ return !options.forceIframeTransport &&
234
+ ((!options.multipart && $.support.xhrFileUpload) ||
235
+ $.support.xhrFormDataFileUpload);
236
+ },
237
+
238
+ _getFormData: function (options) {
239
+ var formData;
240
+ if (typeof options.formData === 'function') {
241
+ return options.formData(options.form);
242
+ }
243
+ if ($.isArray(options.formData)) {
244
+ return options.formData;
245
+ }
246
+ if (options.formData) {
247
+ formData = [];
248
+ $.each(options.formData, function (name, value) {
249
+ formData.push({name: name, value: value});
250
+ });
251
+ return formData;
252
+ }
253
+ return [];
254
+ },
255
+
256
+ _getTotal: function (files) {
257
+ var total = 0;
258
+ $.each(files, function (index, file) {
259
+ total += file.size || 1;
260
+ });
261
+ return total;
262
+ },
263
+
264
+ _initProgressObject: function (obj) {
265
+ obj._progress = {
266
+ loaded: 0,
267
+ total: 0,
268
+ bitrate: 0
269
+ };
270
+ },
271
+
272
+ _onProgress: function (e, data) {
273
+ if (e.lengthComputable) {
274
+ var now = +(new Date()),
275
+ loaded;
276
+ if (data._time && data.progressInterval &&
277
+ (now - data._time < data.progressInterval) &&
278
+ e.loaded !== e.total) {
279
+ return;
280
+ }
281
+ data._time = now;
282
+ loaded = Math.floor(
283
+ e.loaded / e.total * (data.chunkSize || data._progress.total)
284
+ ) + (data.uploadedBytes || 0);
285
+ // Add the difference from the previously loaded state
286
+ // to the global loaded counter:
287
+ this._progress.loaded += (loaded - data._progress.loaded);
288
+ this._progress.bitrate = this._bitrateTimer.getBitrate(
289
+ now,
290
+ this._progress.loaded,
291
+ data.bitrateInterval
292
+ );
293
+ data._progress.loaded = data.loaded = loaded;
294
+ data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
295
+ now,
296
+ loaded,
297
+ data.bitrateInterval
298
+ );
299
+ // Trigger a custom progress event with a total data property set
300
+ // to the file size(s) of the current upload and a loaded data
301
+ // property calculated accordingly:
302
+ this._trigger('progress', e, data);
303
+ // Trigger a global progress event for all current file uploads,
304
+ // including ajax calls queued for sequential file uploads:
305
+ this._trigger('progressall', e, this._progress);
306
+ }
307
+ },
308
+
309
+ _initProgressListener: function (options) {
310
+ var that = this,
311
+ xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
312
+ // Accesss to the native XHR object is required to add event listeners
313
+ // for the upload progress event:
314
+ if (xhr.upload) {
315
+ $(xhr.upload).bind('progress', function (e) {
316
+ var oe = e.originalEvent;
317
+ // Make sure the progress event properties get copied over:
318
+ e.lengthComputable = oe.lengthComputable;
319
+ e.loaded = oe.loaded;
320
+ e.total = oe.total;
321
+ that._onProgress(e, options);
322
+ });
323
+ options.xhr = function () {
324
+ return xhr;
325
+ };
326
+ }
327
+ },
328
+
329
+ _initXHRData: function (options) {
330
+ var formData,
331
+ file = options.files[0],
332
+ // Ignore non-multipart setting if not supported:
333
+ multipart = options.multipart || !$.support.xhrFileUpload,
334
+ paramName = options.paramName[0];
335
+ options.headers = options.headers || {};
336
+ if (options.contentRange) {
337
+ options.headers['Content-Range'] = options.contentRange;
338
+ }
339
+ if (!multipart) {
340
+ options.headers['Content-Disposition'] = 'attachment; filename="' +
341
+ encodeURI(file.name) + '"';
342
+ options.contentType = file.type;
343
+ options.data = options.blob || file;
344
+ } else if ($.support.xhrFormDataFileUpload) {
345
+ if (options.postMessage) {
346
+ // window.postMessage does not allow sending FormData
347
+ // objects, so we just add the File/Blob objects to
348
+ // the formData array and let the postMessage window
349
+ // create the FormData object out of this array:
350
+ formData = this._getFormData(options);
351
+ if (options.blob) {
352
+ formData.push({
353
+ name: paramName,
354
+ value: options.blob
355
+ });
356
+ } else {
357
+ $.each(options.files, function (index, file) {
358
+ formData.push({
359
+ name: options.paramName[index] || paramName,
360
+ value: file
361
+ });
362
+ });
363
+ }
364
+ } else {
365
+ if (options.formData instanceof FormData) {
366
+ formData = options.formData;
367
+ } else {
368
+ formData = new FormData();
369
+ $.each(this._getFormData(options), function (index, field) {
370
+ formData.append(field.name, field.value);
371
+ });
372
+ }
373
+ if (options.blob) {
374
+ options.headers['Content-Disposition'] = 'attachment; filename="' +
375
+ encodeURI(file.name) + '"';
376
+ formData.append(paramName, options.blob, file.name);
377
+ } else {
378
+ $.each(options.files, function (index, file) {
379
+ // Files are also Blob instances, but some browsers
380
+ // (Firefox 3.6) support the File API but not Blobs.
381
+ // This check allows the tests to run with
382
+ // dummy objects:
383
+ if ((window.Blob && file instanceof Blob) ||
384
+ (window.File && file instanceof File)) {
385
+ formData.append(
386
+ options.paramName[index] || paramName,
387
+ file,
388
+ file.name
389
+ );
390
+ }
391
+ });
392
+ }
393
+ }
394
+ options.data = formData;
395
+ }
396
+ // Blob reference is not needed anymore, free memory:
397
+ options.blob = null;
398
+ },
399
+
400
+ _initIframeSettings: function (options) {
401
+ // Setting the dataType to iframe enables the iframe transport:
402
+ options.dataType = 'iframe ' + (options.dataType || '');
403
+ // The iframe transport accepts a serialized array as form data:
404
+ options.formData = this._getFormData(options);
405
+ // Add redirect url to form data on cross-domain uploads:
406
+ if (options.redirect && $('<a></a>').prop('href', options.url)
407
+ .prop('host') !== location.host) {
408
+ options.formData.push({
409
+ name: options.redirectParamName || 'redirect',
410
+ value: options.redirect
411
+ });
412
+ }
413
+ },
414
+
415
+ _initDataSettings: function (options) {
416
+ if (this._isXHRUpload(options)) {
417
+ if (!this._chunkedUpload(options, true)) {
418
+ if (!options.data) {
419
+ this._initXHRData(options);
420
+ }
421
+ this._initProgressListener(options);
422
+ }
423
+ if (options.postMessage) {
424
+ // Setting the dataType to postmessage enables the
425
+ // postMessage transport:
426
+ options.dataType = 'postmessage ' + (options.dataType || '');
427
+ }
428
+ } else {
429
+ this._initIframeSettings(options, 'iframe');
430
+ }
431
+ },
432
+
433
+ _getParamName: function (options) {
434
+ var fileInput = $(options.fileInput),
435
+ paramName = options.paramName;
436
+ if (!paramName) {
437
+ paramName = [];
438
+ fileInput.each(function () {
439
+ var input = $(this),
440
+ name = input.prop('name') || 'files[]',
441
+ i = (input.prop('files') || [1]).length;
442
+ while (i) {
443
+ paramName.push(name);
444
+ i -= 1;
445
+ }
446
+ });
447
+ if (!paramName.length) {
448
+ paramName = [fileInput.prop('name') || 'files[]'];
449
+ }
450
+ } else if (!$.isArray(paramName)) {
451
+ paramName = [paramName];
452
+ }
453
+ return paramName;
454
+ },
455
+
456
+ _initFormSettings: function (options) {
457
+ // Retrieve missing options from the input field and the
458
+ // associated form, if available:
459
+ if (!options.form || !options.form.length) {
460
+ options.form = $(options.fileInput.prop('form'));
461
+ // If the given file input doesn't have an associated form,
462
+ // use the default widget file input's form:
463
+ if (!options.form.length) {
464
+ options.form = $(this.options.fileInput.prop('form'));
465
+ }
466
+ }
467
+ options.paramName = this._getParamName(options);
468
+ if (!options.url) {
469
+ options.url = options.form.prop('action') || location.href;
470
+ }
471
+ // The HTTP request method must be "POST" or "PUT":
472
+ options.type = (options.type || options.form.prop('method') || '')
473
+ .toUpperCase();
474
+ if (options.type !== 'POST' && options.type !== 'PUT' &&
475
+ options.type !== 'PATCH') {
476
+ options.type = 'POST';
477
+ }
478
+ if (!options.formAcceptCharset) {
479
+ options.formAcceptCharset = options.form.attr('accept-charset');
480
+ }
481
+ },
482
+
483
+ _getAJAXSettings: function (data) {
484
+ var options = $.extend({}, this.options, data);
485
+ this._initFormSettings(options);
486
+ this._initDataSettings(options);
487
+ return options;
488
+ },
489
+
490
+ // jQuery 1.6 doesn't provide .state(),
491
+ // while jQuery 1.8+ removed .isRejected() and .isResolved():
492
+ _getDeferredState: function (deferred) {
493
+ if (deferred.state) {
494
+ return deferred.state();
495
+ }
496
+ if (deferred.isResolved()) {
497
+ return 'resolved';
498
+ }
499
+ if (deferred.isRejected()) {
500
+ return 'rejected';
501
+ }
502
+ return 'pending';
503
+ },
504
+
505
+ // Maps jqXHR callbacks to the equivalent
506
+ // methods of the given Promise object:
507
+ _enhancePromise: function (promise) {
508
+ promise.success = promise.done;
509
+ promise.error = promise.fail;
510
+ promise.complete = promise.always;
511
+ return promise;
512
+ },
513
+
514
+ // Creates and returns a Promise object enhanced with
515
+ // the jqXHR methods abort, success, error and complete:
516
+ _getXHRPromise: function (resolveOrReject, context, args) {
517
+ var dfd = $.Deferred(),
518
+ promise = dfd.promise();
519
+ context = context || this.options.context || promise;
520
+ if (resolveOrReject === true) {
521
+ dfd.resolveWith(context, args);
522
+ } else if (resolveOrReject === false) {
523
+ dfd.rejectWith(context, args);
524
+ }
525
+ promise.abort = dfd.promise;
526
+ return this._enhancePromise(promise);
527
+ },
528
+
529
+ // Adds convenience methods to the callback arguments:
530
+ _addConvenienceMethods: function (e, data) {
531
+ var that = this;
532
+ data.submit = function () {
533
+ if (this.state() !== 'pending') {
534
+ data.jqXHR = this.jqXHR =
535
+ (that._trigger('submit', e, this) !== false) &&
536
+ that._onSend(e, this);
537
+ }
538
+ return this.jqXHR || that._getXHRPromise();
539
+ };
540
+ data.abort = function () {
541
+ if (this.jqXHR) {
542
+ return this.jqXHR.abort();
543
+ }
544
+ return this._getXHRPromise();
545
+ };
546
+ data.state = function () {
547
+ if (this.jqXHR) {
548
+ return that._getDeferredState(this.jqXHR);
549
+ }
550
+ };
551
+ data.progress = function () {
552
+ return this._progress;
553
+ };
554
+ },
555
+
556
+ // Parses the Range header from the server response
557
+ // and returns the uploaded bytes:
558
+ _getUploadedBytes: function (jqXHR) {
559
+ var range = jqXHR.getResponseHeader('Range'),
560
+ parts = range && range.split('-'),
561
+ upperBytesPos = parts && parts.length > 1 &&
562
+ parseInt(parts[1], 10);
563
+ return upperBytesPos && upperBytesPos + 1;
564
+ },
565
+
566
+ // Uploads a file in multiple, sequential requests
567
+ // by splitting the file up in multiple blob chunks.
568
+ // If the second parameter is true, only tests if the file
569
+ // should be uploaded in chunks, but does not invoke any
570
+ // upload requests:
571
+ _chunkedUpload: function (options, testOnly) {
572
+ var that = this,
573
+ file = options.files[0],
574
+ fs = file.size,
575
+ ub = options.uploadedBytes = options.uploadedBytes || 0,
576
+ mcs = options.maxChunkSize || fs,
577
+ slice = file.slice || file.webkitSlice || file.mozSlice,
578
+ dfd = $.Deferred(),
579
+ promise = dfd.promise(),
580
+ jqXHR,
581
+ upload;
582
+ if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
583
+ options.data) {
584
+ return false;
585
+ }
586
+ if (testOnly) {
587
+ return true;
588
+ }
589
+ if (ub >= fs) {
590
+ file.error = 'Uploaded bytes exceed file size';
591
+ return this._getXHRPromise(
592
+ false,
593
+ options.context,
594
+ [null, 'error', file.error]
595
+ );
596
+ }
597
+ // The chunk upload method:
598
+ upload = function () {
599
+ // Clone the options object for each chunk upload:
600
+ var o = $.extend({}, options),
601
+ currentLoaded = o._progress.loaded;
602
+ o.blob = slice.call(
603
+ file,
604
+ ub,
605
+ ub + mcs,
606
+ file.type
607
+ );
608
+ // Store the current chunk size, as the blob itself
609
+ // will be dereferenced after data processing:
610
+ o.chunkSize = o.blob.size;
611
+ // Expose the chunk bytes position range:
612
+ o.contentRange = 'bytes ' + ub + '-' +
613
+ (ub + o.chunkSize - 1) + '/' + fs;
614
+ // Process the upload data (the blob and potential form data):
615
+ that._initXHRData(o);
616
+ // Add progress listeners for this chunk upload:
617
+ that._initProgressListener(o);
618
+ jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
619
+ that._getXHRPromise(false, o.context))
620
+ .done(function (result, textStatus, jqXHR) {
621
+ ub = that._getUploadedBytes(jqXHR) ||
622
+ (ub + o.chunkSize);
623
+ // Create a progress event if no final progress event
624
+ // with loaded equaling total has been triggered
625
+ // for this chunk:
626
+ if (o._progress.loaded === currentLoaded) {
627
+ that._onProgress($.Event('progress', {
628
+ lengthComputable: true,
629
+ loaded: ub - o.uploadedBytes,
630
+ total: ub - o.uploadedBytes
631
+ }), o);
632
+ }
633
+ options.uploadedBytes = o.uploadedBytes = ub;
634
+ o.result = result;
635
+ o.textStatus = textStatus;
636
+ o.jqXHR = jqXHR;
637
+ that._trigger('chunkdone', null, o);
638
+ that._trigger('chunkalways', null, o);
639
+ if (ub < fs) {
640
+ // File upload not yet complete,
641
+ // continue with the next chunk:
642
+ upload();
643
+ } else {
644
+ dfd.resolveWith(
645
+ o.context,
646
+ [result, textStatus, jqXHR]
647
+ );
648
+ }
649
+ })
650
+ .fail(function (jqXHR, textStatus, errorThrown) {
651
+ o.jqXHR = jqXHR;
652
+ o.textStatus = textStatus;
653
+ o.errorThrown = errorThrown;
654
+ that._trigger('chunkfail', null, o);
655
+ that._trigger('chunkalways', null, o);
656
+ dfd.rejectWith(
657
+ o.context,
658
+ [jqXHR, textStatus, errorThrown]
659
+ );
660
+ });
661
+ };
662
+ this._enhancePromise(promise);
663
+ promise.abort = function () {
664
+ return jqXHR.abort();
665
+ };
666
+ upload();
667
+ return promise;
668
+ },
669
+
670
+ _beforeSend: function (e, data) {
671
+ if (this._active === 0) {
672
+ // the start callback is triggered when an upload starts
673
+ // and no other uploads are currently running,
674
+ // equivalent to the global ajaxStart event:
675
+ this._trigger('start');
676
+ // Set timer for global bitrate progress calculation:
677
+ this._bitrateTimer = new this._BitrateTimer();
678
+ // Reset the global progress values:
679
+ this._progress.loaded = this._progress.total = 0;
680
+ this._progress.bitrate = 0;
681
+ }
682
+ if (!data._progress) {
683
+ data._progress = {};
684
+ }
685
+ data._progress.loaded = data.loaded = data.uploadedBytes || 0;
686
+ data._progress.total = data.total = this._getTotal(data.files) || 1;
687
+ data._progress.bitrate = data.bitrate = 0;
688
+ this._active += 1;
689
+ // Initialize the global progress values:
690
+ this._progress.loaded += data.loaded;
691
+ this._progress.total += data.total;
692
+ },
693
+
694
+ _onDone: function (result, textStatus, jqXHR, options) {
695
+ var total = options._progress.total;
696
+ if (options._progress.loaded < total) {
697
+ // Create a progress event if no final progress event
698
+ // with loaded equaling total has been triggered:
699
+ this._onProgress($.Event('progress', {
700
+ lengthComputable: true,
701
+ loaded: total,
702
+ total: total
703
+ }), options);
704
+ }
705
+ options.result = result;
706
+ options.textStatus = textStatus;
707
+ options.jqXHR = jqXHR;
708
+ this._trigger('done', null, options);
709
+ },
710
+
711
+ _onFail: function (jqXHR, textStatus, errorThrown, options) {
712
+ options.jqXHR = jqXHR;
713
+ options.textStatus = textStatus;
714
+ options.errorThrown = errorThrown;
715
+ this._trigger('fail', null, options);
716
+ if (options.recalculateProgress) {
717
+ // Remove the failed (error or abort) file upload from
718
+ // the global progress calculation:
719
+ this._progress.loaded -= options._progress.loaded;
720
+ this._progress.total -= options._progress.total;
721
+ }
722
+ },
723
+
724
+ _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
725
+ // jqXHRorResult, textStatus and jqXHRorError are added to the
726
+ // options object via done and fail callbacks
727
+ this._active -= 1;
728
+ this._trigger('always', null, options);
729
+ if (this._active === 0) {
730
+ // The stop callback is triggered when all uploads have
731
+ // been completed, equivalent to the global ajaxStop event:
732
+ this._trigger('stop');
733
+ }
734
+ },
735
+
736
+ _onSend: function (e, data) {
737
+ if (!data.submit) {
738
+ this._addConvenienceMethods(e, data);
739
+ }
740
+ var that = this,
741
+ jqXHR,
742
+ aborted,
743
+ slot,
744
+ pipe,
745
+ options = that._getAJAXSettings(data),
746
+ send = function () {
747
+ that._sending += 1;
748
+ // Set timer for bitrate progress calculation:
749
+ options._bitrateTimer = new that._BitrateTimer();
750
+ jqXHR = jqXHR || (
751
+ ((aborted || that._trigger('send', e, options) === false) &&
752
+ that._getXHRPromise(false, options.context, aborted)) ||
753
+ that._chunkedUpload(options) || $.ajax(options)
754
+ ).done(function (result, textStatus, jqXHR) {
755
+ that._onDone(result, textStatus, jqXHR, options);
756
+ }).fail(function (jqXHR, textStatus, errorThrown) {
757
+ that._onFail(jqXHR, textStatus, errorThrown, options);
758
+ }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
759
+ that._sending -= 1;
760
+ that._onAlways(
761
+ jqXHRorResult,
762
+ textStatus,
763
+ jqXHRorError,
764
+ options
765
+ );
766
+ if (options.limitConcurrentUploads &&
767
+ options.limitConcurrentUploads > that._sending) {
768
+ // Start the next queued upload,
769
+ // that has not been aborted:
770
+ var nextSlot = that._slots.shift();
771
+ while (nextSlot) {
772
+ if (that._getDeferredState(nextSlot) === 'pending') {
773
+ nextSlot.resolve();
774
+ break;
775
+ }
776
+ nextSlot = that._slots.shift();
777
+ }
778
+ }
779
+ });
780
+ return jqXHR;
781
+ };
782
+ this._beforeSend(e, options);
783
+ if (this.options.sequentialUploads ||
784
+ (this.options.limitConcurrentUploads &&
785
+ this.options.limitConcurrentUploads <= this._sending)) {
786
+ if (this.options.limitConcurrentUploads > 1) {
787
+ slot = $.Deferred();
788
+ this._slots.push(slot);
789
+ pipe = slot.pipe(send);
790
+ } else {
791
+ pipe = (this._sequence = this._sequence.pipe(send, send));
792
+ }
793
+ // Return the piped Promise object, enhanced with an abort method,
794
+ // which is delegated to the jqXHR object of the current upload,
795
+ // and jqXHR callbacks mapped to the equivalent Promise methods:
796
+ pipe.abort = function () {
797
+ aborted = [undefined, 'abort', 'abort'];
798
+ if (!jqXHR) {
799
+ if (slot) {
800
+ slot.rejectWith(options.context, aborted);
801
+ }
802
+ return send();
803
+ }
804
+ return jqXHR.abort();
805
+ };
806
+ return this._enhancePromise(pipe);
807
+ }
808
+ return send();
809
+ },
810
+
811
+ _onAdd: function (e, data) {
812
+ var that = this,
813
+ result = true,
814
+ options = $.extend({}, this.options, data),
815
+ limit = options.limitMultiFileUploads,
816
+ paramName = this._getParamName(options),
817
+ paramNameSet,
818
+ paramNameSlice,
819
+ fileSet,
820
+ i;
821
+ if (!(options.singleFileUploads || limit) ||
822
+ !this._isXHRUpload(options)) {
823
+ fileSet = [data.files];
824
+ paramNameSet = [paramName];
825
+ } else if (!options.singleFileUploads && limit) {
826
+ fileSet = [];
827
+ paramNameSet = [];
828
+ for (i = 0; i < data.files.length; i += limit) {
829
+ fileSet.push(data.files.slice(i, i + limit));
830
+ paramNameSlice = paramName.slice(i, i + limit);
831
+ if (!paramNameSlice.length) {
832
+ paramNameSlice = paramName;
833
+ }
834
+ paramNameSet.push(paramNameSlice);
835
+ }
836
+ } else {
837
+ paramNameSet = paramName;
838
+ }
839
+ data.originalFiles = data.files;
840
+ $.each(fileSet || data.files, function (index, element) {
841
+ var newData = $.extend({}, data);
842
+ newData.files = fileSet ? element : [element];
843
+ newData.paramName = paramNameSet[index];
844
+ that._initProgressObject(newData);
845
+ that._addConvenienceMethods(e, newData);
846
+ result = that._trigger('add', e, newData);
847
+ return result;
848
+ });
849
+ return result;
850
+ },
851
+
852
+ _replaceFileInput: function (input) {
853
+ var inputClone = input.clone(true);
854
+ $('<form></form>').append(inputClone)[0].reset();
855
+ // Detaching allows to insert the fileInput on another form
856
+ // without loosing the file input value:
857
+ input.after(inputClone).detach();
858
+ // Avoid memory leaks with the detached file input:
859
+ $.cleanData(input.unbind('remove'));
860
+ // Replace the original file input element in the fileInput
861
+ // elements set with the clone, which has been copied including
862
+ // event handlers:
863
+ this.options.fileInput = this.options.fileInput.map(function (i, el) {
864
+ if (el === input[0]) {
865
+ return inputClone[0];
866
+ }
867
+ return el;
868
+ });
869
+ // If the widget has been initialized on the file input itself,
870
+ // override this.element with the file input clone:
871
+ if (input[0] === this.element[0]) {
872
+ this.element = inputClone;
873
+ }
874
+ },
875
+
876
+ _handleFileTreeEntry: function (entry, path) {
877
+ var that = this,
878
+ dfd = $.Deferred(),
879
+ errorHandler = function (e) {
880
+ if (e && !e.entry) {
881
+ e.entry = entry;
882
+ }
883
+ // Since $.when returns immediately if one
884
+ // Deferred is rejected, we use resolve instead.
885
+ // This allows valid files and invalid items
886
+ // to be returned together in one set:
887
+ dfd.resolve([e]);
888
+ },
889
+ dirReader;
890
+ path = path || '';
891
+ if (entry.isFile) {
892
+ if (entry._file) {
893
+ // Workaround for Chrome bug #149735
894
+ entry._file.relativePath = path;
895
+ dfd.resolve(entry._file);
896
+ } else {
897
+ entry.file(function (file) {
898
+ file.relativePath = path;
899
+ dfd.resolve(file);
900
+ }, errorHandler);
901
+ }
902
+ } else if (entry.isDirectory) {
903
+ dirReader = entry.createReader();
904
+ dirReader.readEntries(function (entries) {
905
+ that._handleFileTreeEntries(
906
+ entries,
907
+ path + entry.name + '/'
908
+ ).done(function (files) {
909
+ dfd.resolve(files);
910
+ }).fail(errorHandler);
911
+ }, errorHandler);
912
+ } else {
913
+ // Return an empy list for file system items
914
+ // other than files or directories:
915
+ dfd.resolve([]);
916
+ }
917
+ return dfd.promise();
918
+ },
919
+
920
+ _handleFileTreeEntries: function (entries, path) {
921
+ var that = this;
922
+ return $.when.apply(
923
+ $,
924
+ $.map(entries, function (entry) {
925
+ return that._handleFileTreeEntry(entry, path);
926
+ })
927
+ ).pipe(function () {
928
+ return Array.prototype.concat.apply(
929
+ [],
930
+ arguments
931
+ );
932
+ });
933
+ },
934
+
935
+ _getDroppedFiles: function (dataTransfer) {
936
+ dataTransfer = dataTransfer || {};
937
+ var items = dataTransfer.items;
938
+ if (items && items.length && (items[0].webkitGetAsEntry ||
939
+ items[0].getAsEntry)) {
940
+ return this._handleFileTreeEntries(
941
+ $.map(items, function (item) {
942
+ var entry;
943
+ if (item.webkitGetAsEntry) {
944
+ entry = item.webkitGetAsEntry();
945
+ if (entry) {
946
+ // Workaround for Chrome bug #149735:
947
+ entry._file = item.getAsFile();
948
+ }
949
+ return entry;
950
+ }
951
+ return item.getAsEntry();
952
+ })
953
+ );
954
+ }
955
+ return $.Deferred().resolve(
956
+ $.makeArray(dataTransfer.files)
957
+ ).promise();
958
+ },
959
+
960
+ _getSingleFileInputFiles: function (fileInput) {
961
+ fileInput = $(fileInput);
962
+ var entries = fileInput.prop('webkitEntries') ||
963
+ fileInput.prop('entries'),
964
+ files,
965
+ value;
966
+ if (entries && entries.length) {
967
+ return this._handleFileTreeEntries(entries);
968
+ }
969
+ files = $.makeArray(fileInput.prop('files'));
970
+ if (!files.length) {
971
+ value = fileInput.prop('value');
972
+ if (!value) {
973
+ return $.Deferred().resolve([]).promise();
974
+ }
975
+ // If the files property is not available, the browser does not
976
+ // support the File API and we add a pseudo File object with
977
+ // the input value as name with path information removed:
978
+ files = [{name: value.replace(/^.*\\/, '')}];
979
+ } else if (files[0].name === undefined && files[0].fileName) {
980
+ // File normalization for Safari 4 and Firefox 3:
981
+ $.each(files, function (index, file) {
982
+ file.name = file.fileName;
983
+ file.size = file.fileSize;
984
+ });
985
+ }
986
+ return $.Deferred().resolve(files).promise();
987
+ },
988
+
989
+ _getFileInputFiles: function (fileInput) {
990
+ if (!(fileInput instanceof $) || fileInput.length === 1) {
991
+ return this._getSingleFileInputFiles(fileInput);
992
+ }
993
+ return $.when.apply(
994
+ $,
995
+ $.map(fileInput, this._getSingleFileInputFiles)
996
+ ).pipe(function () {
997
+ return Array.prototype.concat.apply(
998
+ [],
999
+ arguments
1000
+ );
1001
+ });
1002
+ },
1003
+
1004
+ _onChange: function (e) {
1005
+ var that = this,
1006
+ data = {
1007
+ fileInput: $(e.target),
1008
+ form: $(e.target.form)
1009
+ };
1010
+ this._getFileInputFiles(data.fileInput).always(function (files) {
1011
+ data.files = files;
1012
+ if (that.options.replaceFileInput) {
1013
+ that._replaceFileInput(data.fileInput);
1014
+ }
1015
+ if (that._trigger('change', e, data) !== false) {
1016
+ that._onAdd(e, data);
1017
+ }
1018
+ });
1019
+ },
1020
+
1021
+ _onPaste: function (e) {
1022
+ var cbd = e.originalEvent.clipboardData,
1023
+ items = (cbd && cbd.items) || [],
1024
+ data = {files: []};
1025
+ $.each(items, function (index, item) {
1026
+ var file = item.getAsFile && item.getAsFile();
1027
+ if (file) {
1028
+ data.files.push(file);
1029
+ }
1030
+ });
1031
+ if (this._trigger('paste', e, data) === false ||
1032
+ this._onAdd(e, data) === false) {
1033
+ return false;
1034
+ }
1035
+ },
1036
+
1037
+ _onDrop: function (e) {
1038
+ var that = this,
1039
+ dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
1040
+ data = {};
1041
+ if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1042
+ e.preventDefault();
1043
+ }
1044
+ this._getDroppedFiles(dataTransfer).always(function (files) {
1045
+ data.files = files;
1046
+ if (that._trigger('drop', e, data) !== false) {
1047
+ that._onAdd(e, data);
1048
+ }
1049
+ });
1050
+ },
1051
+
1052
+ _onDragOver: function (e) {
1053
+ var dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
1054
+ if (this._trigger('dragover', e) === false) {
1055
+ return false;
1056
+ }
1057
+ if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1) {
1058
+ dataTransfer.dropEffect = 'copy';
1059
+ e.preventDefault();
1060
+ }
1061
+ },
1062
+
1063
+ _initEventHandlers: function () {
1064
+ if (this._isXHRUpload(this.options)) {
1065
+ this._on(this.options.dropZone, {
1066
+ dragover: this._onDragOver,
1067
+ drop: this._onDrop
1068
+ });
1069
+ this._on(this.options.pasteZone, {
1070
+ paste: this._onPaste
1071
+ });
1072
+ }
1073
+ this._on(this.options.fileInput, {
1074
+ change: this._onChange
1075
+ });
1076
+ },
1077
+
1078
+ _destroyEventHandlers: function () {
1079
+ this._off(this.options.dropZone, 'dragover drop');
1080
+ this._off(this.options.pasteZone, 'paste');
1081
+ this._off(this.options.fileInput, 'change');
1082
+ },
1083
+
1084
+ _setOption: function (key, value) {
1085
+ var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
1086
+ if (refresh) {
1087
+ this._destroyEventHandlers();
1088
+ }
1089
+ this._super(key, value);
1090
+ if (refresh) {
1091
+ this._initSpecialOptions();
1092
+ this._initEventHandlers();
1093
+ }
1094
+ },
1095
+
1096
+ _initSpecialOptions: function () {
1097
+ var options = this.options;
1098
+ if (options.fileInput === undefined) {
1099
+ options.fileInput = this.element.is('input[type="file"]') ?
1100
+ this.element : this.element.find('input[type="file"]');
1101
+ } else if (!(options.fileInput instanceof $)) {
1102
+ options.fileInput = $(options.fileInput);
1103
+ }
1104
+ if (!(options.dropZone instanceof $)) {
1105
+ options.dropZone = $(options.dropZone);
1106
+ }
1107
+ if (!(options.pasteZone instanceof $)) {
1108
+ options.pasteZone = $(options.pasteZone);
1109
+ }
1110
+ },
1111
+
1112
+ _create: function () {
1113
+ var options = this.options;
1114
+ // Initialize options set via HTML5 data-attributes:
1115
+ $.extend(options, $(this.element[0].cloneNode(false)).data());
1116
+ this._initSpecialOptions();
1117
+ this._slots = [];
1118
+ this._sequence = this._getXHRPromise(true);
1119
+ this._sending = this._active = 0;
1120
+ this._initProgressObject(this);
1121
+ this._initEventHandlers();
1122
+ },
1123
+
1124
+ // This method is exposed to the widget API and allows to query
1125
+ // the widget upload progress.
1126
+ // It returns an object with loaded, total and bitrate properties
1127
+ // for the running uploads:
1128
+ progress: function () {
1129
+ return this._progress;
1130
+ },
1131
+
1132
+ // This method is exposed to the widget API and allows adding files
1133
+ // using the fileupload API. The data parameter accepts an object which
1134
+ // must have a files property and can contain additional options:
1135
+ // .fileupload('add', {files: filesList});
1136
+ add: function (data) {
1137
+ var that = this;
1138
+ if (!data || this.options.disabled) {
1139
+ return;
1140
+ }
1141
+ if (data.fileInput && !data.files) {
1142
+ this._getFileInputFiles(data.fileInput).always(function (files) {
1143
+ data.files = files;
1144
+ that._onAdd(null, data);
1145
+ });
1146
+ } else {
1147
+ data.files = $.makeArray(data.files);
1148
+ this._onAdd(null, data);
1149
+ }
1150
+ },
1151
+
1152
+ // This method is exposed to the widget API and allows sending files
1153
+ // using the fileupload API. The data parameter accepts an object which
1154
+ // must have a files or fileInput property and can contain additional options:
1155
+ // .fileupload('send', {files: filesList});
1156
+ // The method returns a Promise object for the file upload call.
1157
+ send: function (data) {
1158
+ if (data && !this.options.disabled) {
1159
+ if (data.fileInput && !data.files) {
1160
+ var that = this,
1161
+ dfd = $.Deferred(),
1162
+ promise = dfd.promise(),
1163
+ jqXHR,
1164
+ aborted;
1165
+ promise.abort = function () {
1166
+ aborted = true;
1167
+ if (jqXHR) {
1168
+ return jqXHR.abort();
1169
+ }
1170
+ dfd.reject(null, 'abort', 'abort');
1171
+ return promise;
1172
+ };
1173
+ this._getFileInputFiles(data.fileInput).always(
1174
+ function (files) {
1175
+ if (aborted) {
1176
+ return;
1177
+ }
1178
+ data.files = files;
1179
+ jqXHR = that._onSend(null, data).then(
1180
+ function (result, textStatus, jqXHR) {
1181
+ dfd.resolve(result, textStatus, jqXHR);
1182
+ },
1183
+ function (jqXHR, textStatus, errorThrown) {
1184
+ dfd.reject(jqXHR, textStatus, errorThrown);
1185
+ }
1186
+ );
1187
+ }
1188
+ );
1189
+ return this._enhancePromise(promise);
1190
+ }
1191
+ data.files = $.makeArray(data.files);
1192
+ if (data.files.length) {
1193
+ return this._onSend(null, data);
1194
+ }
1195
+ }
1196
+ return this._getXHRPromise(false, data && data.context);
1197
+ }
1198
+
1199
+ });
1200
+
1201
+ }));