Commit 231ad8a0b70db5bdaf4f1232976cc67f8309eab5

Authored by Rodrigo Souto
1 parent 8bf75114

[media-panel-improvements] Progress bar

app/controllers/my_profile/cms_controller.rb
... ... @@ -318,14 +318,12 @@ class CmsController < MyProfileController
318 318 end
319 319  
320 320 def media_upload
321   - files_uploaded = []
322 321 parent = check_parent(params[:parent_id])
323 322 if request.post?
324   - params[:images].each do |file|
325   - files_uploaded << UploadedFile.create(:uploaded_data => file, :profile => profile, :parent => parent) unless file == ''
326   - end
  323 + @file = UploadedFile.create(:uploaded_data => params[:file], :profile => profile, :parent => parent) unless params[:file] == ''
327 324 end
328   - render :text => article_list_to_json(files_uploaded), :content_type => 'text/plain'
  325 + #render :text => article_list_to_json([file]), :content_type => 'text/plain'
  326 + render :text => ''
329 327 end
330 328  
331 329 protected
... ...
app/views/cms/_text_editor_sidebar.html.erb
... ... @@ -16,10 +16,7 @@
16 16 "type='Folder' or type='Gallery'"
17 17 ) %>
18 18 </div>
19   - <p><%= file_field_tag('images[]', :multiple => true) %></p>
20   - <% button_bar do %>
21   - <%= submit_button(:save, _('Upload')) %>
22   - <% end %>
  19 + <p><%= file_field_tag('file', :multiple => true) %></p>
23 20 <% end %>
24 21 </div>
25 22 <div id='media-upload-results' style='display: none'>
... ... @@ -47,4 +44,11 @@
47 44 </div>
48 45 </div>
49 46  
  47 +<script id="template-upload" type="text/x-tmpl">
  48 + <div class="upload">
  49 + {%=o.name%}<span class="percentage"></span>
  50 + <div class="progress"><div class="bar" style="width: 0%;"></div></div>
  51 + </div>
  52 +</script>
50 53  
  54 +<%= javascript_include_tag 'jquery.fileupload.js', 'tmpl.js', 'media-upload.js' %>
... ...
public/javascripts/jquery.fileupload.js 0 → 100644
... ... @@ -0,0 +1,1438 @@
  1 +/*
  2 + * jQuery File Upload Plugin 5.40.3
  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 +/* jshint nomen:false */
  13 +/* global define, window, document, location, Blob, FormData */
  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 + // Detect file input support, based on
  31 + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
  32 + $.support.fileInput = !(new RegExp(
  33 + // Handle devices which give false positives for the feature detection:
  34 + '(Android (1\\.[0156]|2\\.[01]))' +
  35 + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
  36 + '|(w(eb)?OSBrowser)|(webOS)' +
  37 + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
  38 + ).test(window.navigator.userAgent) ||
  39 + // Feature detection for all other devices:
  40 + $('<input type="file">').prop('disabled'));
  41 +
  42 + // The FileReader API is not actually used, but works as feature detection,
  43 + // as some Safari versions (5?) support XHR file uploads via the FormData API,
  44 + // but not non-multipart XHR file uploads.
  45 + // window.XMLHttpRequestUpload is not available on IE10, so we check for
  46 + // window.ProgressEvent instead to detect XHR2 file upload capability:
  47 + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
  48 + $.support.xhrFormDataFileUpload = !!window.FormData;
  49 +
  50 + // Detect support for Blob slicing (required for chunked uploads):
  51 + $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
  52 + Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
  53 +
  54 + // The fileupload widget listens for change events on file input fields defined
  55 + // via fileInput setting and paste or drop events of the given dropZone.
  56 + // In addition to the default jQuery Widget methods, the fileupload widget
  57 + // exposes the "add" and "send" methods, to add or directly send files using
  58 + // the fileupload API.
  59 + // By default, files added via file input selection, paste, drag & drop or
  60 + // "add" method are uploaded immediately, but it is possible to override
  61 + // the "add" callback option to queue file uploads.
  62 + $.widget('blueimp.fileupload', {
  63 +
  64 + options: {
  65 + // The drop target element(s), by the default the complete document.
  66 + // Set to null to disable drag & drop support:
  67 + dropZone: $(document),
  68 + // The paste target element(s), by the default the complete document.
  69 + // Set to null to disable paste support:
  70 + pasteZone: $(document),
  71 + // The file input field(s), that are listened to for change events.
  72 + // If undefined, it is set to the file input fields inside
  73 + // of the widget element on plugin initialization.
  74 + // Set to null to disable the change listener.
  75 + fileInput: undefined,
  76 + // By default, the file input field is replaced with a clone after
  77 + // each input field change event. This is required for iframe transport
  78 + // queues and allows change events to be fired for the same file
  79 + // selection, but can be disabled by setting the following option to false:
  80 + replaceFileInput: true,
  81 + // The parameter name for the file form data (the request argument name).
  82 + // If undefined or empty, the name property of the file input field is
  83 + // used, or "files[]" if the file input name property is also empty,
  84 + // can be a string or an array of strings:
  85 + paramName: undefined,
  86 + // By default, each file of a selection is uploaded using an individual
  87 + // request for XHR type uploads. Set to false to upload file
  88 + // selections in one request each:
  89 + singleFileUploads: true,
  90 + // To limit the number of files uploaded with one XHR request,
  91 + // set the following option to an integer greater than 0:
  92 + limitMultiFileUploads: undefined,
  93 + // The following option limits the number of files uploaded with one
  94 + // XHR request to keep the request size under or equal to the defined
  95 + // limit in bytes:
  96 + limitMultiFileUploadSize: undefined,
  97 + // Multipart file uploads add a number of bytes to each uploaded file,
  98 + // therefore the following option adds an overhead for each file used
  99 + // in the limitMultiFileUploadSize configuration:
  100 + limitMultiFileUploadSizeOverhead: 512,
  101 + // Set the following option to true to issue all file upload requests
  102 + // in a sequential order:
  103 + sequentialUploads: false,
  104 + // To limit the number of concurrent uploads,
  105 + // set the following option to an integer greater than 0:
  106 + limitConcurrentUploads: undefined,
  107 + // Set the following option to true to force iframe transport uploads:
  108 + forceIframeTransport: false,
  109 + // Set the following option to the location of a redirect url on the
  110 + // origin server, for cross-domain iframe transport uploads:
  111 + redirect: undefined,
  112 + // The parameter name for the redirect url, sent as part of the form
  113 + // data and set to 'redirect' if this option is empty:
  114 + redirectParamName: undefined,
  115 + // Set the following option to the location of a postMessage window,
  116 + // to enable postMessage transport uploads:
  117 + postMessage: undefined,
  118 + // By default, XHR file uploads are sent as multipart/form-data.
  119 + // The iframe transport is always using multipart/form-data.
  120 + // Set to false to enable non-multipart XHR uploads:
  121 + multipart: true,
  122 + // To upload large files in smaller chunks, set the following option
  123 + // to a preferred maximum chunk size. If set to 0, null or undefined,
  124 + // or the browser does not support the required Blob API, files will
  125 + // be uploaded as a whole.
  126 + maxChunkSize: undefined,
  127 + // When a non-multipart upload or a chunked multipart upload has been
  128 + // aborted, this option can be used to resume the upload by setting
  129 + // it to the size of the already uploaded bytes. This option is most
  130 + // useful when modifying the options object inside of the "add" or
  131 + // "send" callbacks, as the options are cloned for each file upload.
  132 + uploadedBytes: undefined,
  133 + // By default, failed (abort or error) file uploads are removed from the
  134 + // global progress calculation. Set the following option to false to
  135 + // prevent recalculating the global progress data:
  136 + recalculateProgress: true,
  137 + // Interval in milliseconds to calculate and trigger progress events:
  138 + progressInterval: 100,
  139 + // Interval in milliseconds to calculate progress bitrate:
  140 + bitrateInterval: 500,
  141 + // By default, uploads are started automatically when adding files:
  142 + autoUpload: true,
  143 +
  144 + // Error and info messages:
  145 + messages: {
  146 + uploadedBytes: 'Uploaded bytes exceed file size'
  147 + },
  148 +
  149 + // Translation function, gets the message key to be translated
  150 + // and an object with context specific data as arguments:
  151 + i18n: function (message, context) {
  152 + message = this.messages[message] || message.toString();
  153 + if (context) {
  154 + $.each(context, function (key, value) {
  155 + message = message.replace('{' + key + '}', value);
  156 + });
  157 + }
  158 + return message;
  159 + },
  160 +
  161 + // Additional form data to be sent along with the file uploads can be set
  162 + // using this option, which accepts an array of objects with name and
  163 + // value properties, a function returning such an array, a FormData
  164 + // object (for XHR file uploads), or a simple object.
  165 + // The form of the first fileInput is given as parameter to the function:
  166 + formData: function (form) {
  167 + return form.serializeArray();
  168 + },
  169 +
  170 + // The add callback is invoked as soon as files are added to the fileupload
  171 + // widget (via file input selection, drag & drop, paste or add API call).
  172 + // If the singleFileUploads option is enabled, this callback will be
  173 + // called once for each file in the selection for XHR file uploads, else
  174 + // once for each file selection.
  175 + //
  176 + // The upload starts when the submit method is invoked on the data parameter.
  177 + // The data object contains a files property holding the added files
  178 + // and allows you to override plugin options as well as define ajax settings.
  179 + //
  180 + // Listeners for this callback can also be bound the following way:
  181 + // .bind('fileuploadadd', func);
  182 + //
  183 + // data.submit() returns a Promise object and allows to attach additional
  184 + // handlers using jQuery's Deferred callbacks:
  185 + // data.submit().done(func).fail(func).always(func);
  186 + add: function (e, data) {
  187 + if (e.isDefaultPrevented()) {
  188 + return false;
  189 + }
  190 + if (data.autoUpload || (data.autoUpload !== false &&
  191 + $(this).fileupload('option', 'autoUpload'))) {
  192 + data.process().done(function () {
  193 + data.submit();
  194 + });
  195 + }
  196 + },
  197 +
  198 + // Other callbacks:
  199 +
  200 + // Callback for the submit event of each file upload:
  201 + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
  202 +
  203 + // Callback for the start of each file upload request:
  204 + // send: function (e, data) {}, // .bind('fileuploadsend', func);
  205 +
  206 + // Callback for successful uploads:
  207 + // done: function (e, data) {}, // .bind('fileuploaddone', func);
  208 +
  209 + // Callback for failed (abort or error) uploads:
  210 + // fail: function (e, data) {}, // .bind('fileuploadfail', func);
  211 +
  212 + // Callback for completed (success, abort or error) requests:
  213 + // always: function (e, data) {}, // .bind('fileuploadalways', func);
  214 +
  215 + // Callback for upload progress events:
  216 + // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
  217 +
  218 + // Callback for global upload progress events:
  219 + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
  220 +
  221 + // Callback for uploads start, equivalent to the global ajaxStart event:
  222 + // start: function (e) {}, // .bind('fileuploadstart', func);
  223 +
  224 + // Callback for uploads stop, equivalent to the global ajaxStop event:
  225 + // stop: function (e) {}, // .bind('fileuploadstop', func);
  226 +
  227 + // Callback for change events of the fileInput(s):
  228 + // change: function (e, data) {}, // .bind('fileuploadchange', func);
  229 +
  230 + // Callback for paste events to the pasteZone(s):
  231 + // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
  232 +
  233 + // Callback for drop events of the dropZone(s):
  234 + // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
  235 +
  236 + // Callback for dragover events of the dropZone(s):
  237 + // dragover: function (e) {}, // .bind('fileuploaddragover', func);
  238 +
  239 + // Callback for the start of each chunk upload request:
  240 + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
  241 +
  242 + // Callback for successful chunk uploads:
  243 + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
  244 +
  245 + // Callback for failed (abort or error) chunk uploads:
  246 + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
  247 +
  248 + // Callback for completed (success, abort or error) chunk upload requests:
  249 + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
  250 +
  251 + // The plugin options are used as settings object for the ajax calls.
  252 + // The following are jQuery ajax settings required for the file uploads:
  253 + processData: false,
  254 + contentType: false,
  255 + cache: false
  256 + },
  257 +
  258 + // A list of options that require reinitializing event listeners and/or
  259 + // special initialization code:
  260 + _specialOptions: [
  261 + 'fileInput',
  262 + 'dropZone',
  263 + 'pasteZone',
  264 + 'multipart',
  265 + 'forceIframeTransport'
  266 + ],
  267 +
  268 + _blobSlice: $.support.blobSlice && function () {
  269 + var slice = this.slice || this.webkitSlice || this.mozSlice;
  270 + return slice.apply(this, arguments);
  271 + },
  272 +
  273 + _BitrateTimer: function () {
  274 + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
  275 + this.loaded = 0;
  276 + this.bitrate = 0;
  277 + this.getBitrate = function (now, loaded, interval) {
  278 + var timeDiff = now - this.timestamp;
  279 + if (!this.bitrate || !interval || timeDiff > interval) {
  280 + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
  281 + this.loaded = loaded;
  282 + this.timestamp = now;
  283 + }
  284 + return this.bitrate;
  285 + };
  286 + },
  287 +
  288 + _isXHRUpload: function (options) {
  289 + return !options.forceIframeTransport &&
  290 + ((!options.multipart && $.support.xhrFileUpload) ||
  291 + $.support.xhrFormDataFileUpload);
  292 + },
  293 +
  294 + _getFormData: function (options) {
  295 + var formData;
  296 + if ($.type(options.formData) === 'function') {
  297 + return options.formData(options.form);
  298 + }
  299 + if ($.isArray(options.formData)) {
  300 + return options.formData;
  301 + }
  302 + if ($.type(options.formData) === 'object') {
  303 + formData = [];
  304 + $.each(options.formData, function (name, value) {
  305 + formData.push({name: name, value: value});
  306 + });
  307 + return formData;
  308 + }
  309 + return [];
  310 + },
  311 +
  312 + _getTotal: function (files) {
  313 + var total = 0;
  314 + $.each(files, function (index, file) {
  315 + total += file.size || 1;
  316 + });
  317 + return total;
  318 + },
  319 +
  320 + _initProgressObject: function (obj) {
  321 + var progress = {
  322 + loaded: 0,
  323 + total: 0,
  324 + bitrate: 0
  325 + };
  326 + if (obj._progress) {
  327 + $.extend(obj._progress, progress);
  328 + } else {
  329 + obj._progress = progress;
  330 + }
  331 + },
  332 +
  333 + _initResponseObject: function (obj) {
  334 + var prop;
  335 + if (obj._response) {
  336 + for (prop in obj._response) {
  337 + if (obj._response.hasOwnProperty(prop)) {
  338 + delete obj._response[prop];
  339 + }
  340 + }
  341 + } else {
  342 + obj._response = {};
  343 + }
  344 + },
  345 +
  346 + _onProgress: function (e, data) {
  347 + if (e.lengthComputable) {
  348 + var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
  349 + loaded;
  350 + if (data._time && data.progressInterval &&
  351 + (now - data._time < data.progressInterval) &&
  352 + e.loaded !== e.total) {
  353 + return;
  354 + }
  355 + data._time = now;
  356 + loaded = Math.floor(
  357 + e.loaded / e.total * (data.chunkSize || data._progress.total)
  358 + ) + (data.uploadedBytes || 0);
  359 + // Add the difference from the previously loaded state
  360 + // to the global loaded counter:
  361 + this._progress.loaded += (loaded - data._progress.loaded);
  362 + this._progress.bitrate = this._bitrateTimer.getBitrate(
  363 + now,
  364 + this._progress.loaded,
  365 + data.bitrateInterval
  366 + );
  367 + data._progress.loaded = data.loaded = loaded;
  368 + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
  369 + now,
  370 + loaded,
  371 + data.bitrateInterval
  372 + );
  373 + // Trigger a custom progress event with a total data property set
  374 + // to the file size(s) of the current upload and a loaded data
  375 + // property calculated accordingly:
  376 + this._trigger(
  377 + 'progress',
  378 + $.Event('progress', {delegatedEvent: e}),
  379 + data
  380 + );
  381 + // Trigger a global progress event for all current file uploads,
  382 + // including ajax calls queued for sequential file uploads:
  383 + this._trigger(
  384 + 'progressall',
  385 + $.Event('progressall', {delegatedEvent: e}),
  386 + this._progress
  387 + );
  388 + }
  389 + },
  390 +
  391 + _initProgressListener: function (options) {
  392 + var that = this,
  393 + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
  394 + // Accesss to the native XHR object is required to add event listeners
  395 + // for the upload progress event:
  396 + if (xhr.upload) {
  397 + $(xhr.upload).bind('progress', function (e) {
  398 + var oe = e.originalEvent;
  399 + // Make sure the progress event properties get copied over:
  400 + e.lengthComputable = oe.lengthComputable;
  401 + e.loaded = oe.loaded;
  402 + e.total = oe.total;
  403 + that._onProgress(e, options);
  404 + });
  405 + options.xhr = function () {
  406 + return xhr;
  407 + };
  408 + }
  409 + },
  410 +
  411 + _isInstanceOf: function (type, obj) {
  412 + // Cross-frame instanceof check
  413 + return Object.prototype.toString.call(obj) === '[object ' + type + ']';
  414 + },
  415 +
  416 + _initXHRData: function (options) {
  417 + var that = this,
  418 + formData,
  419 + file = options.files[0],
  420 + // Ignore non-multipart setting if not supported:
  421 + multipart = options.multipart || !$.support.xhrFileUpload,
  422 + paramName = $.type(options.paramName) === 'array' ?
  423 + options.paramName[0] : options.paramName;
  424 + options.headers = $.extend({}, options.headers);
  425 + if (options.contentRange) {
  426 + options.headers['Content-Range'] = options.contentRange;
  427 + }
  428 + if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
  429 + options.headers['Content-Disposition'] = 'attachment; filename="' +
  430 + encodeURI(file.name) + '"';
  431 + }
  432 + if (!multipart) {
  433 + options.contentType = file.type || 'application/octet-stream';
  434 + options.data = options.blob || file;
  435 + } else if ($.support.xhrFormDataFileUpload) {
  436 + if (options.postMessage) {
  437 + // window.postMessage does not allow sending FormData
  438 + // objects, so we just add the File/Blob objects to
  439 + // the formData array and let the postMessage window
  440 + // create the FormData object out of this array:
  441 + formData = this._getFormData(options);
  442 + if (options.blob) {
  443 + formData.push({
  444 + name: paramName,
  445 + value: options.blob
  446 + });
  447 + } else {
  448 + $.each(options.files, function (index, file) {
  449 + formData.push({
  450 + name: ($.type(options.paramName) === 'array' &&
  451 + options.paramName[index]) || paramName,
  452 + value: file
  453 + });
  454 + });
  455 + }
  456 + } else {
  457 + if (that._isInstanceOf('FormData', options.formData)) {
  458 + formData = options.formData;
  459 + } else {
  460 + formData = new FormData();
  461 + $.each(this._getFormData(options), function (index, field) {
  462 + formData.append(field.name, field.value);
  463 + });
  464 + }
  465 + if (options.blob) {
  466 + formData.append(paramName, options.blob, file.name);
  467 + } else {
  468 + $.each(options.files, function (index, file) {
  469 + // This check allows the tests to run with
  470 + // dummy objects:
  471 + if (that._isInstanceOf('File', file) ||
  472 + that._isInstanceOf('Blob', file)) {
  473 + formData.append(
  474 + ($.type(options.paramName) === 'array' &&
  475 + options.paramName[index]) || paramName,
  476 + file,
  477 + file.uploadName || file.name
  478 + );
  479 + }
  480 + });
  481 + }
  482 + }
  483 + options.data = formData;
  484 + }
  485 + // Blob reference is not needed anymore, free memory:
  486 + options.blob = null;
  487 + },
  488 +
  489 + _initIframeSettings: function (options) {
  490 + var targetHost = $('<a></a>').prop('href', options.url).prop('host');
  491 + // Setting the dataType to iframe enables the iframe transport:
  492 + options.dataType = 'iframe ' + (options.dataType || '');
  493 + // The iframe transport accepts a serialized array as form data:
  494 + options.formData = this._getFormData(options);
  495 + // Add redirect url to form data on cross-domain uploads:
  496 + if (options.redirect && targetHost && targetHost !== location.host) {
  497 + options.formData.push({
  498 + name: options.redirectParamName || 'redirect',
  499 + value: options.redirect
  500 + });
  501 + }
  502 + },
  503 +
  504 + _initDataSettings: function (options) {
  505 + if (this._isXHRUpload(options)) {
  506 + if (!this._chunkedUpload(options, true)) {
  507 + if (!options.data) {
  508 + this._initXHRData(options);
  509 + }
  510 + this._initProgressListener(options);
  511 + }
  512 + if (options.postMessage) {
  513 + // Setting the dataType to postmessage enables the
  514 + // postMessage transport:
  515 + options.dataType = 'postmessage ' + (options.dataType || '');
  516 + }
  517 + } else {
  518 + this._initIframeSettings(options);
  519 + }
  520 + },
  521 +
  522 + _getParamName: function (options) {
  523 + var fileInput = $(options.fileInput),
  524 + paramName = options.paramName;
  525 + if (!paramName) {
  526 + paramName = [];
  527 + fileInput.each(function () {
  528 + var input = $(this),
  529 + name = input.prop('name') || 'files[]',
  530 + i = (input.prop('files') || [1]).length;
  531 + while (i) {
  532 + paramName.push(name);
  533 + i -= 1;
  534 + }
  535 + });
  536 + if (!paramName.length) {
  537 + paramName = [fileInput.prop('name') || 'files[]'];
  538 + }
  539 + } else if (!$.isArray(paramName)) {
  540 + paramName = [paramName];
  541 + }
  542 + return paramName;
  543 + },
  544 +
  545 + _initFormSettings: function (options) {
  546 + // Retrieve missing options from the input field and the
  547 + // associated form, if available:
  548 + if (!options.form || !options.form.length) {
  549 + options.form = $(options.fileInput.prop('form'));
  550 + // If the given file input doesn't have an associated form,
  551 + // use the default widget file input's form:
  552 + if (!options.form.length) {
  553 + options.form = $(this.options.fileInput.prop('form'));
  554 + }
  555 + }
  556 + options.paramName = this._getParamName(options);
  557 + if (!options.url) {
  558 + options.url = options.form.prop('action') || location.href;
  559 + }
  560 + // The HTTP request method must be "POST" or "PUT":
  561 + options.type = (options.type ||
  562 + ($.type(options.form.prop('method')) === 'string' &&
  563 + options.form.prop('method')) || ''
  564 + ).toUpperCase();
  565 + if (options.type !== 'POST' && options.type !== 'PUT' &&
  566 + options.type !== 'PATCH') {
  567 + options.type = 'POST';
  568 + }
  569 + if (!options.formAcceptCharset) {
  570 + options.formAcceptCharset = options.form.attr('accept-charset');
  571 + }
  572 + },
  573 +
  574 + _getAJAXSettings: function (data) {
  575 + var options = $.extend({}, this.options, data);
  576 + this._initFormSettings(options);
  577 + this._initDataSettings(options);
  578 + return options;
  579 + },
  580 +
  581 + // jQuery 1.6 doesn't provide .state(),
  582 + // while jQuery 1.8+ removed .isRejected() and .isResolved():
  583 + _getDeferredState: function (deferred) {
  584 + if (deferred.state) {
  585 + return deferred.state();
  586 + }
  587 + if (deferred.isResolved()) {
  588 + return 'resolved';
  589 + }
  590 + if (deferred.isRejected()) {
  591 + return 'rejected';
  592 + }
  593 + return 'pending';
  594 + },
  595 +
  596 + // Maps jqXHR callbacks to the equivalent
  597 + // methods of the given Promise object:
  598 + _enhancePromise: function (promise) {
  599 + promise.success = promise.done;
  600 + promise.error = promise.fail;
  601 + promise.complete = promise.always;
  602 + return promise;
  603 + },
  604 +
  605 + // Creates and returns a Promise object enhanced with
  606 + // the jqXHR methods abort, success, error and complete:
  607 + _getXHRPromise: function (resolveOrReject, context, args) {
  608 + var dfd = $.Deferred(),
  609 + promise = dfd.promise();
  610 + context = context || this.options.context || promise;
  611 + if (resolveOrReject === true) {
  612 + dfd.resolveWith(context, args);
  613 + } else if (resolveOrReject === false) {
  614 + dfd.rejectWith(context, args);
  615 + }
  616 + promise.abort = dfd.promise;
  617 + return this._enhancePromise(promise);
  618 + },
  619 +
  620 + // Adds convenience methods to the data callback argument:
  621 + _addConvenienceMethods: function (e, data) {
  622 + var that = this,
  623 + getPromise = function (args) {
  624 + return $.Deferred().resolveWith(that, args).promise();
  625 + };
  626 + data.process = function (resolveFunc, rejectFunc) {
  627 + if (resolveFunc || rejectFunc) {
  628 + data._processQueue = this._processQueue =
  629 + (this._processQueue || getPromise([this])).pipe(
  630 + function () {
  631 + if (data.errorThrown) {
  632 + return $.Deferred()
  633 + .rejectWith(that, [data]).promise();
  634 + }
  635 + return getPromise(arguments);
  636 + }
  637 + ).pipe(resolveFunc, rejectFunc);
  638 + }
  639 + return this._processQueue || getPromise([this]);
  640 + };
  641 + data.submit = function () {
  642 + if (this.state() !== 'pending') {
  643 + data.jqXHR = this.jqXHR =
  644 + (that._trigger(
  645 + 'submit',
  646 + $.Event('submit', {delegatedEvent: e}),
  647 + this
  648 + ) !== false) && that._onSend(e, this);
  649 + }
  650 + return this.jqXHR || that._getXHRPromise();
  651 + };
  652 + data.abort = function () {
  653 + if (this.jqXHR) {
  654 + return this.jqXHR.abort();
  655 + }
  656 + this.errorThrown = 'abort';
  657 + that._trigger('fail', null, this);
  658 + return that._getXHRPromise(false);
  659 + };
  660 + data.state = function () {
  661 + if (this.jqXHR) {
  662 + return that._getDeferredState(this.jqXHR);
  663 + }
  664 + if (this._processQueue) {
  665 + return that._getDeferredState(this._processQueue);
  666 + }
  667 + };
  668 + data.processing = function () {
  669 + return !this.jqXHR && this._processQueue && that
  670 + ._getDeferredState(this._processQueue) === 'pending';
  671 + };
  672 + data.progress = function () {
  673 + return this._progress;
  674 + };
  675 + data.response = function () {
  676 + return this._response;
  677 + };
  678 + },
  679 +
  680 + // Parses the Range header from the server response
  681 + // and returns the uploaded bytes:
  682 + _getUploadedBytes: function (jqXHR) {
  683 + var range = jqXHR.getResponseHeader('Range'),
  684 + parts = range && range.split('-'),
  685 + upperBytesPos = parts && parts.length > 1 &&
  686 + parseInt(parts[1], 10);
  687 + return upperBytesPos && upperBytesPos + 1;
  688 + },
  689 +
  690 + // Uploads a file in multiple, sequential requests
  691 + // by splitting the file up in multiple blob chunks.
  692 + // If the second parameter is true, only tests if the file
  693 + // should be uploaded in chunks, but does not invoke any
  694 + // upload requests:
  695 + _chunkedUpload: function (options, testOnly) {
  696 + options.uploadedBytes = options.uploadedBytes || 0;
  697 + var that = this,
  698 + file = options.files[0],
  699 + fs = file.size,
  700 + ub = options.uploadedBytes,
  701 + mcs = options.maxChunkSize || fs,
  702 + slice = this._blobSlice,
  703 + dfd = $.Deferred(),
  704 + promise = dfd.promise(),
  705 + jqXHR,
  706 + upload;
  707 + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
  708 + options.data) {
  709 + return false;
  710 + }
  711 + if (testOnly) {
  712 + return true;
  713 + }
  714 + if (ub >= fs) {
  715 + file.error = options.i18n('uploadedBytes');
  716 + return this._getXHRPromise(
  717 + false,
  718 + options.context,
  719 + [null, 'error', file.error]
  720 + );
  721 + }
  722 + // The chunk upload method:
  723 + upload = function () {
  724 + // Clone the options object for each chunk upload:
  725 + var o = $.extend({}, options),
  726 + currentLoaded = o._progress.loaded;
  727 + o.blob = slice.call(
  728 + file,
  729 + ub,
  730 + ub + mcs,
  731 + file.type
  732 + );
  733 + // Store the current chunk size, as the blob itself
  734 + // will be dereferenced after data processing:
  735 + o.chunkSize = o.blob.size;
  736 + // Expose the chunk bytes position range:
  737 + o.contentRange = 'bytes ' + ub + '-' +
  738 + (ub + o.chunkSize - 1) + '/' + fs;
  739 + // Process the upload data (the blob and potential form data):
  740 + that._initXHRData(o);
  741 + // Add progress listeners for this chunk upload:
  742 + that._initProgressListener(o);
  743 + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
  744 + that._getXHRPromise(false, o.context))
  745 + .done(function (result, textStatus, jqXHR) {
  746 + ub = that._getUploadedBytes(jqXHR) ||
  747 + (ub + o.chunkSize);
  748 + // Create a progress event if no final progress event
  749 + // with loaded equaling total has been triggered
  750 + // for this chunk:
  751 + if (currentLoaded + o.chunkSize - o._progress.loaded) {
  752 + that._onProgress($.Event('progress', {
  753 + lengthComputable: true,
  754 + loaded: ub - o.uploadedBytes,
  755 + total: ub - o.uploadedBytes
  756 + }), o);
  757 + }
  758 + options.uploadedBytes = o.uploadedBytes = ub;
  759 + o.result = result;
  760 + o.textStatus = textStatus;
  761 + o.jqXHR = jqXHR;
  762 + that._trigger('chunkdone', null, o);
  763 + that._trigger('chunkalways', null, o);
  764 + if (ub < fs) {
  765 + // File upload not yet complete,
  766 + // continue with the next chunk:
  767 + upload();
  768 + } else {
  769 + dfd.resolveWith(
  770 + o.context,
  771 + [result, textStatus, jqXHR]
  772 + );
  773 + }
  774 + })
  775 + .fail(function (jqXHR, textStatus, errorThrown) {
  776 + o.jqXHR = jqXHR;
  777 + o.textStatus = textStatus;
  778 + o.errorThrown = errorThrown;
  779 + that._trigger('chunkfail', null, o);
  780 + that._trigger('chunkalways', null, o);
  781 + dfd.rejectWith(
  782 + o.context,
  783 + [jqXHR, textStatus, errorThrown]
  784 + );
  785 + });
  786 + };
  787 + this._enhancePromise(promise);
  788 + promise.abort = function () {
  789 + return jqXHR.abort();
  790 + };
  791 + upload();
  792 + return promise;
  793 + },
  794 +
  795 + _beforeSend: function (e, data) {
  796 + if (this._active === 0) {
  797 + // the start callback is triggered when an upload starts
  798 + // and no other uploads are currently running,
  799 + // equivalent to the global ajaxStart event:
  800 + this._trigger('start');
  801 + // Set timer for global bitrate progress calculation:
  802 + this._bitrateTimer = new this._BitrateTimer();
  803 + // Reset the global progress values:
  804 + this._progress.loaded = this._progress.total = 0;
  805 + this._progress.bitrate = 0;
  806 + }
  807 + // Make sure the container objects for the .response() and
  808 + // .progress() methods on the data object are available
  809 + // and reset to their initial state:
  810 + this._initResponseObject(data);
  811 + this._initProgressObject(data);
  812 + data._progress.loaded = data.loaded = data.uploadedBytes || 0;
  813 + data._progress.total = data.total = this._getTotal(data.files) || 1;
  814 + data._progress.bitrate = data.bitrate = 0;
  815 + this._active += 1;
  816 + // Initialize the global progress values:
  817 + this._progress.loaded += data.loaded;
  818 + this._progress.total += data.total;
  819 + },
  820 +
  821 + _onDone: function (result, textStatus, jqXHR, options) {
  822 + var total = options._progress.total,
  823 + response = options._response;
  824 + if (options._progress.loaded < total) {
  825 + // Create a progress event if no final progress event
  826 + // with loaded equaling total has been triggered:
  827 + this._onProgress($.Event('progress', {
  828 + lengthComputable: true,
  829 + loaded: total,
  830 + total: total
  831 + }), options);
  832 + }
  833 + response.result = options.result = result;
  834 + response.textStatus = options.textStatus = textStatus;
  835 + response.jqXHR = options.jqXHR = jqXHR;
  836 + this._trigger('done', null, options);
  837 + },
  838 +
  839 + _onFail: function (jqXHR, textStatus, errorThrown, options) {
  840 + var response = options._response;
  841 + if (options.recalculateProgress) {
  842 + // Remove the failed (error or abort) file upload from
  843 + // the global progress calculation:
  844 + this._progress.loaded -= options._progress.loaded;
  845 + this._progress.total -= options._progress.total;
  846 + }
  847 + response.jqXHR = options.jqXHR = jqXHR;
  848 + response.textStatus = options.textStatus = textStatus;
  849 + response.errorThrown = options.errorThrown = errorThrown;
  850 + this._trigger('fail', null, options);
  851 + },
  852 +
  853 + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
  854 + // jqXHRorResult, textStatus and jqXHRorError are added to the
  855 + // options object via done and fail callbacks
  856 + this._trigger('always', null, options);
  857 + },
  858 +
  859 + _onSend: function (e, data) {
  860 + if (!data.submit) {
  861 + this._addConvenienceMethods(e, data);
  862 + }
  863 + var that = this,
  864 + jqXHR,
  865 + aborted,
  866 + slot,
  867 + pipe,
  868 + options = that._getAJAXSettings(data),
  869 + send = function () {
  870 + that._sending += 1;
  871 + // Set timer for bitrate progress calculation:
  872 + options._bitrateTimer = new that._BitrateTimer();
  873 + jqXHR = jqXHR || (
  874 + ((aborted || that._trigger(
  875 + 'send',
  876 + $.Event('send', {delegatedEvent: e}),
  877 + options
  878 + ) === false) &&
  879 + that._getXHRPromise(false, options.context, aborted)) ||
  880 + that._chunkedUpload(options) || $.ajax(options)
  881 + ).done(function (result, textStatus, jqXHR) {
  882 + that._onDone(result, textStatus, jqXHR, options);
  883 + }).fail(function (jqXHR, textStatus, errorThrown) {
  884 + that._onFail(jqXHR, textStatus, errorThrown, options);
  885 + }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
  886 + that._onAlways(
  887 + jqXHRorResult,
  888 + textStatus,
  889 + jqXHRorError,
  890 + options
  891 + );
  892 + that._sending -= 1;
  893 + that._active -= 1;
  894 + if (options.limitConcurrentUploads &&
  895 + options.limitConcurrentUploads > that._sending) {
  896 + // Start the next queued upload,
  897 + // that has not been aborted:
  898 + var nextSlot = that._slots.shift();
  899 + while (nextSlot) {
  900 + if (that._getDeferredState(nextSlot) === 'pending') {
  901 + nextSlot.resolve();
  902 + break;
  903 + }
  904 + nextSlot = that._slots.shift();
  905 + }
  906 + }
  907 + if (that._active === 0) {
  908 + // The stop callback is triggered when all uploads have
  909 + // been completed, equivalent to the global ajaxStop event:
  910 + that._trigger('stop');
  911 + }
  912 + });
  913 + return jqXHR;
  914 + };
  915 + this._beforeSend(e, options);
  916 + if (this.options.sequentialUploads ||
  917 + (this.options.limitConcurrentUploads &&
  918 + this.options.limitConcurrentUploads <= this._sending)) {
  919 + if (this.options.limitConcurrentUploads > 1) {
  920 + slot = $.Deferred();
  921 + this._slots.push(slot);
  922 + pipe = slot.pipe(send);
  923 + } else {
  924 + this._sequence = this._sequence.pipe(send, send);
  925 + pipe = this._sequence;
  926 + }
  927 + // Return the piped Promise object, enhanced with an abort method,
  928 + // which is delegated to the jqXHR object of the current upload,
  929 + // and jqXHR callbacks mapped to the equivalent Promise methods:
  930 + pipe.abort = function () {
  931 + aborted = [undefined, 'abort', 'abort'];
  932 + if (!jqXHR) {
  933 + if (slot) {
  934 + slot.rejectWith(options.context, aborted);
  935 + }
  936 + return send();
  937 + }
  938 + return jqXHR.abort();
  939 + };
  940 + return this._enhancePromise(pipe);
  941 + }
  942 + return send();
  943 + },
  944 +
  945 + _onAdd: function (e, data) {
  946 + var that = this,
  947 + result = true,
  948 + options = $.extend({}, this.options, data),
  949 + files = data.files,
  950 + filesLength = files.length,
  951 + limit = options.limitMultiFileUploads,
  952 + limitSize = options.limitMultiFileUploadSize,
  953 + overhead = options.limitMultiFileUploadSizeOverhead,
  954 + batchSize = 0,
  955 + paramName = this._getParamName(options),
  956 + paramNameSet,
  957 + paramNameSlice,
  958 + fileSet,
  959 + i,
  960 + j = 0;
  961 + if (limitSize && (!filesLength || files[0].size === undefined)) {
  962 + limitSize = undefined;
  963 + }
  964 + if (!(options.singleFileUploads || limit || limitSize) ||
  965 + !this._isXHRUpload(options)) {
  966 + fileSet = [files];
  967 + paramNameSet = [paramName];
  968 + } else if (!(options.singleFileUploads || limitSize) && limit) {
  969 + fileSet = [];
  970 + paramNameSet = [];
  971 + for (i = 0; i < filesLength; i += limit) {
  972 + fileSet.push(files.slice(i, i + limit));
  973 + paramNameSlice = paramName.slice(i, i + limit);
  974 + if (!paramNameSlice.length) {
  975 + paramNameSlice = paramName;
  976 + }
  977 + paramNameSet.push(paramNameSlice);
  978 + }
  979 + } else if (!options.singleFileUploads && limitSize) {
  980 + fileSet = [];
  981 + paramNameSet = [];
  982 + for (i = 0; i < filesLength; i = i + 1) {
  983 + batchSize += files[i].size + overhead;
  984 + if (i + 1 === filesLength ||
  985 + ((batchSize + files[i + 1].size + overhead) > limitSize) ||
  986 + (limit && i + 1 - j >= limit)) {
  987 + fileSet.push(files.slice(j, i + 1));
  988 + paramNameSlice = paramName.slice(j, i + 1);
  989 + if (!paramNameSlice.length) {
  990 + paramNameSlice = paramName;
  991 + }
  992 + paramNameSet.push(paramNameSlice);
  993 + j = i + 1;
  994 + batchSize = 0;
  995 + }
  996 + }
  997 + } else {
  998 + paramNameSet = paramName;
  999 + }
  1000 + data.originalFiles = files;
  1001 + $.each(fileSet || files, function (index, element) {
  1002 + var newData = $.extend({}, data);
  1003 + newData.files = fileSet ? element : [element];
  1004 + newData.paramName = paramNameSet[index];
  1005 + that._initResponseObject(newData);
  1006 + that._initProgressObject(newData);
  1007 + that._addConvenienceMethods(e, newData);
  1008 + result = that._trigger(
  1009 + 'add',
  1010 + $.Event('add', {delegatedEvent: e}),
  1011 + newData
  1012 + );
  1013 + return result;
  1014 + });
  1015 + return result;
  1016 + },
  1017 +
  1018 + _replaceFileInput: function (input) {
  1019 + var inputClone = input.clone(true);
  1020 + $('<form></form>').append(inputClone)[0].reset();
  1021 + // Detaching allows to insert the fileInput on another form
  1022 + // without loosing the file input value:
  1023 + input.after(inputClone).detach();
  1024 + // Avoid memory leaks with the detached file input:
  1025 + $.cleanData(input.unbind('remove'));
  1026 + // Replace the original file input element in the fileInput
  1027 + // elements set with the clone, which has been copied including
  1028 + // event handlers:
  1029 + this.options.fileInput = this.options.fileInput.map(function (i, el) {
  1030 + if (el === input[0]) {
  1031 + return inputClone[0];
  1032 + }
  1033 + return el;
  1034 + });
  1035 + // If the widget has been initialized on the file input itself,
  1036 + // override this.element with the file input clone:
  1037 + if (input[0] === this.element[0]) {
  1038 + this.element = inputClone;
  1039 + }
  1040 + },
  1041 +
  1042 + _handleFileTreeEntry: function (entry, path) {
  1043 + var that = this,
  1044 + dfd = $.Deferred(),
  1045 + errorHandler = function (e) {
  1046 + if (e && !e.entry) {
  1047 + e.entry = entry;
  1048 + }
  1049 + // Since $.when returns immediately if one
  1050 + // Deferred is rejected, we use resolve instead.
  1051 + // This allows valid files and invalid items
  1052 + // to be returned together in one set:
  1053 + dfd.resolve([e]);
  1054 + },
  1055 + successHandler = function (entries) {
  1056 + that._handleFileTreeEntries(
  1057 + entries,
  1058 + path + entry.name + '/'
  1059 + ).done(function (files) {
  1060 + dfd.resolve(files);
  1061 + }).fail(errorHandler);
  1062 + },
  1063 + readEntries = function () {
  1064 + dirReader.readEntries(function (results) {
  1065 + if (!results.length) {
  1066 + successHandler(entries);
  1067 + } else {
  1068 + entries = entries.concat(results);
  1069 + readEntries();
  1070 + }
  1071 + }, errorHandler);
  1072 + },
  1073 + dirReader, entries = [];
  1074 + path = path || '';
  1075 + if (entry.isFile) {
  1076 + if (entry._file) {
  1077 + // Workaround for Chrome bug #149735
  1078 + entry._file.relativePath = path;
  1079 + dfd.resolve(entry._file);
  1080 + } else {
  1081 + entry.file(function (file) {
  1082 + file.relativePath = path;
  1083 + dfd.resolve(file);
  1084 + }, errorHandler);
  1085 + }
  1086 + } else if (entry.isDirectory) {
  1087 + dirReader = entry.createReader();
  1088 + readEntries();
  1089 + } else {
  1090 + // Return an empy list for file system items
  1091 + // other than files or directories:
  1092 + dfd.resolve([]);
  1093 + }
  1094 + return dfd.promise();
  1095 + },
  1096 +
  1097 + _handleFileTreeEntries: function (entries, path) {
  1098 + var that = this;
  1099 + return $.when.apply(
  1100 + $,
  1101 + $.map(entries, function (entry) {
  1102 + return that._handleFileTreeEntry(entry, path);
  1103 + })
  1104 + ).pipe(function () {
  1105 + return Array.prototype.concat.apply(
  1106 + [],
  1107 + arguments
  1108 + );
  1109 + });
  1110 + },
  1111 +
  1112 + _getDroppedFiles: function (dataTransfer) {
  1113 + dataTransfer = dataTransfer || {};
  1114 + var items = dataTransfer.items;
  1115 + if (items && items.length && (items[0].webkitGetAsEntry ||
  1116 + items[0].getAsEntry)) {
  1117 + return this._handleFileTreeEntries(
  1118 + $.map(items, function (item) {
  1119 + var entry;
  1120 + if (item.webkitGetAsEntry) {
  1121 + entry = item.webkitGetAsEntry();
  1122 + if (entry) {
  1123 + // Workaround for Chrome bug #149735:
  1124 + entry._file = item.getAsFile();
  1125 + }
  1126 + return entry;
  1127 + }
  1128 + return item.getAsEntry();
  1129 + })
  1130 + );
  1131 + }
  1132 + return $.Deferred().resolve(
  1133 + $.makeArray(dataTransfer.files)
  1134 + ).promise();
  1135 + },
  1136 +
  1137 + _getSingleFileInputFiles: function (fileInput) {
  1138 + fileInput = $(fileInput);
  1139 + var entries = fileInput.prop('webkitEntries') ||
  1140 + fileInput.prop('entries'),
  1141 + files,
  1142 + value;
  1143 + if (entries && entries.length) {
  1144 + return this._handleFileTreeEntries(entries);
  1145 + }
  1146 + files = $.makeArray(fileInput.prop('files'));
  1147 + if (!files.length) {
  1148 + value = fileInput.prop('value');
  1149 + if (!value) {
  1150 + return $.Deferred().resolve([]).promise();
  1151 + }
  1152 + // If the files property is not available, the browser does not
  1153 + // support the File API and we add a pseudo File object with
  1154 + // the input value as name with path information removed:
  1155 + files = [{name: value.replace(/^.*\\/, '')}];
  1156 + } else if (files[0].name === undefined && files[0].fileName) {
  1157 + // File normalization for Safari 4 and Firefox 3:
  1158 + $.each(files, function (index, file) {
  1159 + file.name = file.fileName;
  1160 + file.size = file.fileSize;
  1161 + });
  1162 + }
  1163 + return $.Deferred().resolve(files).promise();
  1164 + },
  1165 +
  1166 + _getFileInputFiles: function (fileInput) {
  1167 + if (!(fileInput instanceof $) || fileInput.length === 1) {
  1168 + return this._getSingleFileInputFiles(fileInput);
  1169 + }
  1170 + return $.when.apply(
  1171 + $,
  1172 + $.map(fileInput, this._getSingleFileInputFiles)
  1173 + ).pipe(function () {
  1174 + return Array.prototype.concat.apply(
  1175 + [],
  1176 + arguments
  1177 + );
  1178 + });
  1179 + },
  1180 +
  1181 + _onChange: function (e) {
  1182 + var that = this,
  1183 + data = {
  1184 + fileInput: $(e.target),
  1185 + form: $(e.target.form)
  1186 + };
  1187 + this._getFileInputFiles(data.fileInput).always(function (files) {
  1188 + data.files = files;
  1189 + if (that.options.replaceFileInput) {
  1190 + that._replaceFileInput(data.fileInput);
  1191 + }
  1192 + if (that._trigger(
  1193 + 'change',
  1194 + $.Event('change', {delegatedEvent: e}),
  1195 + data
  1196 + ) !== false) {
  1197 + that._onAdd(e, data);
  1198 + }
  1199 + });
  1200 + },
  1201 +
  1202 + _onPaste: function (e) {
  1203 + var items = e.originalEvent && e.originalEvent.clipboardData &&
  1204 + e.originalEvent.clipboardData.items,
  1205 + data = {files: []};
  1206 + if (items && items.length) {
  1207 + $.each(items, function (index, item) {
  1208 + var file = item.getAsFile && item.getAsFile();
  1209 + if (file) {
  1210 + data.files.push(file);
  1211 + }
  1212 + });
  1213 + if (this._trigger(
  1214 + 'paste',
  1215 + $.Event('paste', {delegatedEvent: e}),
  1216 + data
  1217 + ) !== false) {
  1218 + this._onAdd(e, data);
  1219 + }
  1220 + }
  1221 + },
  1222 +
  1223 + _onDrop: function (e) {
  1224 + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
  1225 + var that = this,
  1226 + dataTransfer = e.dataTransfer,
  1227 + data = {};
  1228 + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
  1229 + e.preventDefault();
  1230 + this._getDroppedFiles(dataTransfer).always(function (files) {
  1231 + data.files = files;
  1232 + if (that._trigger(
  1233 + 'drop',
  1234 + $.Event('drop', {delegatedEvent: e}),
  1235 + data
  1236 + ) !== false) {
  1237 + that._onAdd(e, data);
  1238 + }
  1239 + });
  1240 + }
  1241 + },
  1242 +
  1243 + _onDragOver: function (e) {
  1244 + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
  1245 + var dataTransfer = e.dataTransfer;
  1246 + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
  1247 + this._trigger(
  1248 + 'dragover',
  1249 + $.Event('dragover', {delegatedEvent: e})
  1250 + ) !== false) {
  1251 + e.preventDefault();
  1252 + dataTransfer.dropEffect = 'copy';
  1253 + }
  1254 + },
  1255 +
  1256 + _initEventHandlers: function () {
  1257 + if (this._isXHRUpload(this.options)) {
  1258 + this._on(this.options.dropZone, {
  1259 + dragover: this._onDragOver,
  1260 + drop: this._onDrop
  1261 + });
  1262 + this._on(this.options.pasteZone, {
  1263 + paste: this._onPaste
  1264 + });
  1265 + }
  1266 + if ($.support.fileInput) {
  1267 + this._on(this.options.fileInput, {
  1268 + change: this._onChange
  1269 + });
  1270 + }
  1271 + },
  1272 +
  1273 + _destroyEventHandlers: function () {
  1274 + this._off(this.options.dropZone, 'dragover drop');
  1275 + this._off(this.options.pasteZone, 'paste');
  1276 + this._off(this.options.fileInput, 'change');
  1277 + },
  1278 +
  1279 + _setOption: function (key, value) {
  1280 + var reinit = $.inArray(key, this._specialOptions) !== -1;
  1281 + if (reinit) {
  1282 + this._destroyEventHandlers();
  1283 + }
  1284 + this._super(key, value);
  1285 + if (reinit) {
  1286 + this._initSpecialOptions();
  1287 + this._initEventHandlers();
  1288 + }
  1289 + },
  1290 +
  1291 + _initSpecialOptions: function () {
  1292 + var options = this.options;
  1293 + if (options.fileInput === undefined) {
  1294 + options.fileInput = this.element.is('input[type="file"]') ?
  1295 + this.element : this.element.find('input[type="file"]');
  1296 + } else if (!(options.fileInput instanceof $)) {
  1297 + options.fileInput = $(options.fileInput);
  1298 + }
  1299 + if (!(options.dropZone instanceof $)) {
  1300 + options.dropZone = $(options.dropZone);
  1301 + }
  1302 + if (!(options.pasteZone instanceof $)) {
  1303 + options.pasteZone = $(options.pasteZone);
  1304 + }
  1305 + },
  1306 +
  1307 + _getRegExp: function (str) {
  1308 + var parts = str.split('/'),
  1309 + modifiers = parts.pop();
  1310 + parts.shift();
  1311 + return new RegExp(parts.join('/'), modifiers);
  1312 + },
  1313 +
  1314 + _isRegExpOption: function (key, value) {
  1315 + return key !== 'url' && $.type(value) === 'string' &&
  1316 + /^\/.*\/[igm]{0,3}$/.test(value);
  1317 + },
  1318 +
  1319 + _initDataAttributes: function () {
  1320 + var that = this,
  1321 + options = this.options,
  1322 + clone = $(this.element[0].cloneNode(false));
  1323 + // Initialize options set via HTML5 data-attributes:
  1324 + $.each(
  1325 + clone.data(),
  1326 + function (key, value) {
  1327 + var dataAttributeName = 'data-' +
  1328 + // Convert camelCase to hyphen-ated key:
  1329 + key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  1330 + if (clone.attr(dataAttributeName)) {
  1331 + if (that._isRegExpOption(key, value)) {
  1332 + value = that._getRegExp(value);
  1333 + }
  1334 + options[key] = value;
  1335 + }
  1336 + }
  1337 + );
  1338 + },
  1339 +
  1340 + _create: function () {
  1341 + this._initDataAttributes();
  1342 + this._initSpecialOptions();
  1343 + this._slots = [];
  1344 + this._sequence = this._getXHRPromise(true);
  1345 + this._sending = this._active = 0;
  1346 + this._initProgressObject(this);
  1347 + this._initEventHandlers();
  1348 + },
  1349 +
  1350 + // This method is exposed to the widget API and allows to query
  1351 + // the number of active uploads:
  1352 + active: function () {
  1353 + return this._active;
  1354 + },
  1355 +
  1356 + // This method is exposed to the widget API and allows to query
  1357 + // the widget upload progress.
  1358 + // It returns an object with loaded, total and bitrate properties
  1359 + // for the running uploads:
  1360 + progress: function () {
  1361 + return this._progress;
  1362 + },
  1363 +
  1364 + // This method is exposed to the widget API and allows adding files
  1365 + // using the fileupload API. The data parameter accepts an object which
  1366 + // must have a files property and can contain additional options:
  1367 + // .fileupload('add', {files: filesList});
  1368 + add: function (data) {
  1369 + var that = this;
  1370 + if (!data || this.options.disabled) {
  1371 + return;
  1372 + }
  1373 + if (data.fileInput && !data.files) {
  1374 + this._getFileInputFiles(data.fileInput).always(function (files) {
  1375 + data.files = files;
  1376 + that._onAdd(null, data);
  1377 + });
  1378 + } else {
  1379 + data.files = $.makeArray(data.files);
  1380 + this._onAdd(null, data);
  1381 + }
  1382 + },
  1383 +
  1384 + // This method is exposed to the widget API and allows sending files
  1385 + // using the fileupload API. The data parameter accepts an object which
  1386 + // must have a files or fileInput property and can contain additional options:
  1387 + // .fileupload('send', {files: filesList});
  1388 + // The method returns a Promise object for the file upload call.
  1389 + send: function (data) {
  1390 + if (data && !this.options.disabled) {
  1391 + if (data.fileInput && !data.files) {
  1392 + var that = this,
  1393 + dfd = $.Deferred(),
  1394 + promise = dfd.promise(),
  1395 + jqXHR,
  1396 + aborted;
  1397 + promise.abort = function () {
  1398 + aborted = true;
  1399 + if (jqXHR) {
  1400 + return jqXHR.abort();
  1401 + }
  1402 + dfd.reject(null, 'abort', 'abort');
  1403 + return promise;
  1404 + };
  1405 + this._getFileInputFiles(data.fileInput).always(
  1406 + function (files) {
  1407 + if (aborted) {
  1408 + return;
  1409 + }
  1410 + if (!files.length) {
  1411 + dfd.reject();
  1412 + return;
  1413 + }
  1414 + data.files = files;
  1415 + jqXHR = that._onSend(null, data);
  1416 + jqXHR.then(
  1417 + function (result, textStatus, jqXHR) {
  1418 + dfd.resolve(result, textStatus, jqXHR);
  1419 + },
  1420 + function (jqXHR, textStatus, errorThrown) {
  1421 + dfd.reject(jqXHR, textStatus, errorThrown);
  1422 + }
  1423 + );
  1424 + }
  1425 + );
  1426 + return this._enhancePromise(promise);
  1427 + }
  1428 + data.files = $.makeArray(data.files);
  1429 + if (data.files.length) {
  1430 + return this._onSend(null, data);
  1431 + }
  1432 + }
  1433 + return this._getXHRPromise(false, data && data.context);
  1434 + }
  1435 +
  1436 + });
  1437 +
  1438 +}));
... ...
public/javascripts/media-upload.js 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +jQuery('#file').fileupload({
  2 + add: function(e, data){
  3 + data.context = jQuery(tmpl("template-upload", data.files[0]));
  4 + jQuery('#media-upload-form').append(data.context);
  5 + data.submit();
  6 + },
  7 + progress: function (e, data) {
  8 + if (data.context) {
  9 + progress = parseInt(data.loaded / data.total * 100, 10);
  10 + data.context.find('.bar').css('width', progress + '%');
  11 + data.context.find('.percentage').text(progress + '%');
  12 + }
  13 + }
  14 +});
... ...
public/javascripts/tmpl.js 0 → 100644
... ... @@ -0,0 +1,87 @@
  1 +/*
  2 + * JavaScript Templates 2.4.1
  3 + * https://github.com/blueimp/JavaScript-Templates
  4 + *
  5 + * Copyright 2011, Sebastian Tschan
  6 + * https://blueimp.net
  7 + *
  8 + * Licensed under the MIT license:
  9 + * http://www.opensource.org/licenses/MIT
  10 + *
  11 + * Inspired by John Resig's JavaScript Micro-Templating:
  12 + * http://ejohn.org/blog/javascript-micro-templating/
  13 + */
  14 +
  15 +/*jslint evil: true, regexp: true, unparam: true */
  16 +/*global document, define */
  17 +
  18 +(function ($) {
  19 + "use strict";
  20 + var tmpl = function (str, data) {
  21 + var f = !/[^\w\-\.:]/.test(str) ? tmpl.cache[str] = tmpl.cache[str] ||
  22 + tmpl(tmpl.load(str)) :
  23 + new Function(
  24 + tmpl.arg + ',tmpl',
  25 + "var _e=tmpl.encode" + tmpl.helper + ",_s='" +
  26 + str.replace(tmpl.regexp, tmpl.func) +
  27 + "';return _s;"
  28 + );
  29 + return data ? f(data, tmpl) : function (data) {
  30 + return f(data, tmpl);
  31 + };
  32 + };
  33 + tmpl.cache = {};
  34 + tmpl.load = function (id) {
  35 + return document.getElementById(id).innerHTML;
  36 + };
  37 + tmpl.regexp = /([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g;
  38 + tmpl.func = function (s, p1, p2, p3, p4, p5) {
  39 + if (p1) { // whitespace, quote and backspace in HTML context
  40 + return {
  41 + "\n": "\\n",
  42 + "\r": "\\r",
  43 + "\t": "\\t",
  44 + " " : " "
  45 + }[p1] || "\\" + p1;
  46 + }
  47 + if (p2) { // interpolation: {%=prop%}, or unescaped: {%#prop%}
  48 + if (p2 === "=") {
  49 + return "'+_e(" + p3 + ")+'";
  50 + }
  51 + return "'+(" + p3 + "==null?'':" + p3 + ")+'";
  52 + }
  53 + if (p4) { // evaluation start tag: {%
  54 + return "';";
  55 + }
  56 + if (p5) { // evaluation end tag: %}
  57 + return "_s+='";
  58 + }
  59 + };
  60 + tmpl.encReg = /[<>&"'\x00]/g;
  61 + tmpl.encMap = {
  62 + "<" : "&lt;",
  63 + ">" : "&gt;",
  64 + "&" : "&amp;",
  65 + "\"" : "&quot;",
  66 + "'" : "&#39;"
  67 + };
  68 + tmpl.encode = function (s) {
  69 + /*jshint eqnull:true */
  70 + return (s == null ? "" : "" + s).replace(
  71 + tmpl.encReg,
  72 + function (c) {
  73 + return tmpl.encMap[c] || "";
  74 + }
  75 + );
  76 + };
  77 + tmpl.arg = "o";
  78 + tmpl.helper = ",print=function(s,e){_s+=e?(s==null?'':s):_e(s);}" +
  79 + ",include=function(s,d){_s+=tmpl(s,d);}";
  80 + if (typeof define === "function" && define.amd) {
  81 + define(function () {
  82 + return tmpl;
  83 + });
  84 + } else {
  85 + $.tmpl = tmpl;
  86 + }
  87 +}(this));
... ...
public/stylesheets/application.css
... ... @@ -3380,6 +3380,27 @@ div.with_media_panel .formfield input[type=&quot;checkbox&quot;] {
3380 3380 width: auto;
3381 3381 }
3382 3382  
  3383 +#media-upload-form {
  3384 + padding-bottom: 5px;
  3385 +}
  3386 +
  3387 +.upload {
  3388 + border-top: solid 1px #CCC;
  3389 + width: 100%;
  3390 + padding-top: 5px;
  3391 + margin-top: 5px;
  3392 +}
  3393 +
  3394 +.percentage {
  3395 + float: right;
  3396 +}
  3397 +
  3398 +.bar {
  3399 + height: 18px;
  3400 + background: #729fcf;
  3401 + border-radius: 4px;
  3402 +}
  3403 +
3383 3404 .text-editor-sidebar {
3384 3405 position: absolute;
3385 3406 width: 280px;
... ...