diff --git a/.gitignore b/.gitignore index 4040c6c..31be0d4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .bundle Gemfile.lock pkg/* +.ruby-version diff --git a/README.md b/README.md index 6b11d9b..ead9b2f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# jQuery File Upload for Rails 3.1 Asset Pipeline +# jQuery File Upload for Rails [jQuery-File-Plugin](https://github.com/blueimp/jQuery-File-Upload) is a file upload plugin written by [Sebastian Tschan](https://github.com/blueimp). jQuery File Upload features multiple file selection, drag&drop support, progress bars and preview images for jQuery. Supports cross-domain, chunked and resumable file uploads and client-side image resizing. @@ -6,9 +6,7 @@ jquery-fileupload-rails is a library that integrates jQuery File Upload for Rail ## Plugin versions -* jQuery File Upload User Interface Plugin 6.9.4 -* jQuery File Upload Plugin 5.16.3 -* jQuery UI Widget 1.8.23+amd +* jQuery File Upload Plugin v9.12.5 ## Installing Gem @@ -20,36 +18,70 @@ Require jquery-fileupload in your app/assets/application.js file. //= require jquery-fileupload -The snippet above will add the following js files to the mainfest file. - - //=require jquery-fileupload/vendor/jquery.ui.widget - //=require jquery-fileupload/vendor/load-image - //=require jquery-fileupload/vendor/canvas-to-blob - //=require jquery-fileupload/vendor/tmpl - //=require jquery-fileupload/jquery.iframe-transport - //=require jquery-fileupload/jquery.fileupload - //=require jquery-fileupload/jquery.fileupload-fp - //=require jquery-fileupload/jquery.fileupload-ui - //=require jquery-fileupload/locale +The snippet above will add the following js files to the manifest file. + + //= require jquery-fileupload/vendor/jquery.ui.widget + //= require jquery-fileupload/vendor/tmpl + //= require jquery-fileupload/vendor/load-image.all.min + //= require jquery-fileupload/vendor/canvas-to-blob + //= require jquery-fileupload/jquery.iframe-transport + //= require jquery-fileupload/jquery.fileupload + //= require jquery-fileupload/jquery.fileupload-process + //= require jquery-fileupload/jquery.fileupload-image + //= require jquery-fileupload/jquery.fileupload-audio + //= require jquery-fileupload/jquery.fileupload-video + //= require jquery-fileupload/jquery.fileupload-validate + //= require jquery-fileupload/jquery.fileupload-ui + //= require jquery-fileupload/locale + //= require jquery-fileupload/jquery.fileupload-angular + //= require jquery-fileupload/jquery.fileupload-jquery-ui + //= require jquery-fileupload/cors/jquery.postmessage-transport + //= require jquery-fileupload/cors/jquery.xdr-transport If you only need the basic files, just add the code below to your application.js file. [Basic setup guide](https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin) - //=require jquery-fileupload/basic + //= require jquery-fileupload/basic The basic setup only includes the following files: - //=require jquery-fileupload/vendor/jquery.ui.widget - //=require jquery-fileupload/jquery.iframe-transport - //=require jquery-fileupload/jquery.fileupload + //= require jquery-fileupload/vendor/jquery.ui.widget + //= require jquery-fileupload/jquery.iframe-transport + //= require jquery-fileupload/jquery.fileupload + +You can also require the following to get the js from the Basic-Plus, AngularJS and jQuery UI Examples: + + //= require jquery-fileupload/basic-plus + + //= require jquery-fileupload/angularjs + + //= require jquery-fileupload/jquery-ui ## Using the stylesheet Require the stylesheet file to app/assets/stylesheets/application.css + *= require jquery.fileupload *= require jquery.fileupload-ui -## [Example app](https://github.com/tors/jquery-fileupload-rails-paperclip-example) -This app uses paperclip and twitter-bootstrap-rails +There are also noscript styles for Browsers with Javascript disabled, to use them create a noscript.css and add it to your precompile-list and layout inside a noscript tag: + + *= require jquery.fileupload-noscript + *= require jquery.fileupload-ui-noscript + +## Using the middleware + +The `jquery.iframe-transport` fallback transport has some special caveats regarding the response data type, http status, and character encodings. `jquery-fileupload-rails` includes a middleware that handles these inconsistencies seamlessly. If you decide to use it, create an initializer that adds the middleware to your application's middleware stack. + +```ruby +Rails.application.config.middleware.use JQuery::FileUpload::Rails::Middleware +``` + +## Example apps +[jquery-fileupload-rails-paperclip-example](https://github.com/tors/jquery-fileupload-rails-paperclip-example): jQuery File Upload in Rails 3.2 with Paperclip and Bootstrap + +[rails-resumable-jquery-fileupload](https://github.com/vgantchev/rails-resumable-jquery-fileupload): resumable (chunked) uploads with jQuery File Upload in Rails 4.2 using Paperclip + +You can also check out Ryan Bate's RailsCast [jQuery File Upload episode](http://railscasts.com/episodes/381-jquery-file-upload). ## Thanks diff --git a/vendor/assets/images/loading.gif b/app/assets/images/loading.gif similarity index 100% rename from vendor/assets/images/loading.gif rename to app/assets/images/loading.gif diff --git a/vendor/assets/images/progressbar.gif b/app/assets/images/progressbar.gif similarity index 100% rename from vendor/assets/images/progressbar.gif rename to app/assets/images/progressbar.gif diff --git a/app/assets/javascripts/jquery-fileupload/angularjs.js b/app/assets/javascripts/jquery-fileupload/angularjs.js new file mode 100644 index 0000000..c87d11c --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/angularjs.js @@ -0,0 +1,12 @@ +//= require jquery-fileupload/vendor/jquery.ui.widget +//= require jquery-fileupload/vendor/load-image.all.min +//= require jquery-fileupload/vendor/canvas-to-blob +//= require jquery-fileupload/jquery.iframe-transport +//= require jquery-fileupload/jquery.fileupload +//= require jquery-fileupload/jquery.fileupload-process +//= require jquery-fileupload/jquery.fileupload-image +//= require jquery-fileupload/jquery.fileupload-audio +//= require jquery-fileupload/jquery.fileupload-video +//= require jquery-fileupload/jquery.fileupload-validate +//= require jquery-fileupload/jquery.fileupload-angular +//= require jquery-fileupload/locale diff --git a/app/assets/javascripts/jquery-fileupload/basic-plus.js b/app/assets/javascripts/jquery-fileupload/basic-plus.js new file mode 100644 index 0000000..dacdb2e --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/basic-plus.js @@ -0,0 +1,11 @@ +//= require jquery-fileupload/vendor/jquery.ui.widget +//= require jquery-fileupload/vendor/load-image.all.min +//= require jquery-fileupload/vendor/canvas-to-blob +//= require jquery-fileupload/jquery.iframe-transport +//= require jquery-fileupload/jquery.fileupload +//= require jquery-fileupload/jquery.fileupload-process +//= require jquery-fileupload/jquery.fileupload-image +//= require jquery-fileupload/jquery.fileupload-audio +//= require jquery-fileupload/jquery.fileupload-video +//= require jquery-fileupload/jquery.fileupload-validate +//= require jquery-fileupload/locale diff --git a/app/assets/javascripts/jquery-fileupload/basic.js b/app/assets/javascripts/jquery-fileupload/basic.js new file mode 100644 index 0000000..6d07907 --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/basic.js @@ -0,0 +1,3 @@ +//= require jquery-fileupload/vendor/jquery.ui.widget +//= require jquery-fileupload/jquery.iframe-transport +//= require jquery-fileupload/jquery.fileupload diff --git a/vendor/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js b/app/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js similarity index 84% rename from vendor/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js rename to app/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js index 931b635..120c9c0 100644 --- a/vendor/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js +++ b/app/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js @@ -1,5 +1,5 @@ /* - * jQuery postMessage Transport Plugin 1.1 + * jQuery postMessage Transport Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan @@ -9,14 +9,16 @@ * http://www.opensource.org/licenses/MIT */ -/*jslint unparam: true, nomen: true */ -/*global define, window, document */ +/* global define, require, window, document */ -(function (factory) { +;(function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); } else { // Browser globals: factory(window.jQuery); @@ -62,10 +64,17 @@ loc = $('').prop('href', options.postMessage)[0], target = loc.protocol + '//' + loc.host, xhrUpload = options.xhr().upload; + // IE always includes the port for the host property of a link + // element, but not in the location.host or origin property for the + // default http port 80 and https port 443, so we strip it: + if (/^(http:\/\/.+:80)|(https:\/\/.+:443)$/.test(target)) { + target = target.replace(/:(80|443)$/, ''); + } return { send: function (_, completeCallback) { + counter += 1; var message = { - id: 'postmessage-transport-' + (counter += 1) + id: 'postmessage-transport-' + counter }, eventName = 'message.' + message.id; iframe = $( diff --git a/vendor/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js b/app/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js similarity index 81% rename from vendor/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js rename to app/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js index c42c548..d7a9d76 100644 --- a/vendor/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js +++ b/app/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js @@ -1,5 +1,5 @@ /* - * jQuery XDomainRequest Transport Plugin 1.1.2 + * jQuery XDomainRequest Transport Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan @@ -12,14 +12,16 @@ * https://github.com/jaubourg/ajaxHooks/ */ -/*jslint unparam: true */ -/*global define, window, XDomainRequest */ +/* global define, require, window, XDomainRequest */ -(function (factory) { +;(function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); } else { // Browser globals: factory(window.jQuery); @@ -36,6 +38,7 @@ var xdr; return { send: function (headers, completeCallback) { + var addParamChar = /\?/.test(s.url) ? '&' : '?'; function callback(status, statusText, responses, responseHeaders) { xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; xdr = null; @@ -44,12 +47,13 @@ xdr = new XDomainRequest(); // XDomainRequest only supports GET and POST: if (s.type === 'DELETE') { - s.url = s.url + (/\?/.test(s.url) ? '&' : '?') + - '_method=DELETE'; + s.url = s.url + addParamChar + '_method=DELETE'; s.type = 'POST'; } else if (s.type === 'PUT') { - s.url = s.url + (/\?/.test(s.url) ? '&' : '?') + - '_method=PUT'; + s.url = s.url + addParamChar + '_method=PUT'; + s.type = 'POST'; + } else if (s.type === 'PATCH') { + s.url = s.url + addParamChar + '_method=PATCH'; s.type = 'POST'; } xdr.open(s.type, s.url); diff --git a/app/assets/javascripts/jquery-fileupload/index.js b/app/assets/javascripts/jquery-fileupload/index.js new file mode 100644 index 0000000..58d29a3 --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/index.js @@ -0,0 +1,13 @@ +//= require jquery-fileupload/vendor/jquery.ui.widget +//= require jquery-fileupload/vendor/tmpl +//= require jquery-fileupload/vendor/load-image.all.min +//= require jquery-fileupload/vendor/canvas-to-blob +//= require jquery-fileupload/jquery.iframe-transport +//= require jquery-fileupload/jquery.fileupload +//= require jquery-fileupload/jquery.fileupload-process +//= require jquery-fileupload/jquery.fileupload-image +//= require jquery-fileupload/jquery.fileupload-audio +//= require jquery-fileupload/jquery.fileupload-video +//= require jquery-fileupload/jquery.fileupload-validate +//= require jquery-fileupload/jquery.fileupload-ui +//= require jquery-fileupload/locale diff --git a/app/assets/javascripts/jquery-fileupload/jquery-ui.js b/app/assets/javascripts/jquery-fileupload/jquery-ui.js new file mode 100644 index 0000000..256325d --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/jquery-ui.js @@ -0,0 +1,13 @@ +//= require jquery-fileupload/vendor/tmpl +//= require jquery-fileupload/vendor/load-image.all.min +//= require jquery-fileupload/vendor/canvas-to-blob +//= require jquery-fileupload/jquery.iframe-transport +//= require jquery-fileupload/jquery.fileupload +//= require jquery-fileupload/jquery.fileupload-process +//= require jquery-fileupload/jquery.fileupload-image +//= require jquery-fileupload/jquery.fileupload-audio +//= require jquery-fileupload/jquery.fileupload-video +//= require jquery-fileupload/jquery.fileupload-validate +//= require jquery-fileupload/jquery.fileupload-ui +//= require jquery-fileupload/jquery.fileupload-jquery-ui +//= require jquery-fileupload/locale diff --git a/app/assets/javascripts/jquery-fileupload/jquery.fileupload-angular.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-angular.js new file mode 100644 index 0000000..a33f9f4 --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-angular.js @@ -0,0 +1,425 @@ +/* + * jQuery File Upload AngularJS Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, angular */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'angular', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else { + factory(); + } +}(function () { + 'use strict'; + + angular.module('blueimp.fileupload', []) + + // The fileUpload service provides configuration options + // for the fileUpload directive and default handlers for + // File Upload events: + .provider('fileUpload', function () { + var scopeEvalAsync = function (expression) { + var scope = angular.element(this) + .fileupload('option', 'scope'); + // Schedule a new $digest cycle if not already inside of one + // and evaluate the given expression: + scope.$evalAsync(expression); + }, + addFileMethods = function (scope, data) { + var files = data.files, + file = files[0]; + angular.forEach(files, function (file, index) { + file._index = index; + file.$state = function () { + return data.state(); + }; + file.$processing = function () { + return data.processing(); + }; + file.$progress = function () { + return data.progress(); + }; + file.$response = function () { + return data.response(); + }; + }); + file.$submit = function () { + if (!file.error) { + return data.submit(); + } + }; + file.$cancel = function () { + return data.abort(); + }; + }, + $config; + $config = this.defaults = { + handleResponse: function (e, data) { + var files = data.result && data.result.files; + if (files) { + data.scope.replace(data.files, files); + } else if (data.errorThrown || + data.textStatus === 'error') { + data.files[0].error = data.errorThrown || + data.textStatus; + } + }, + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var scope = data.scope, + filesCopy = []; + angular.forEach(data.files, function (file) { + filesCopy.push(file); + }); + scope.$parent.$applyAsync(function () { + addFileMethods(scope, data); + var method = scope.option('prependFiles') ? + 'unshift' : 'push'; + Array.prototype[method].apply(scope.queue, data.files); + }); + data.process(function () { + return scope.process(data); + }).always(function () { + scope.$parent.$applyAsync(function () { + addFileMethods(scope, data); + scope.replace(filesCopy, data.files); + }); + }).then(function () { + if ((scope.option('autoUpload') || + data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }); + }, + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = this; + data.scope.$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = this, + scope = data.scope; + if (data.errorThrown === 'abort') { + scope.clear(data.files); + return; + } + scope.$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + stop: scopeEvalAsync, + processstart: scopeEvalAsync, + processstop: scopeEvalAsync, + getNumberOfFiles: function () { + var scope = this.scope; + return scope.queue.length - scope.processing(); + }, + dataType: 'json', + autoUpload: false + }; + this.$get = [ + function () { + return { + defaults: $config + }; + } + ]; + }) + + // Format byte numbers to readable presentations: + .provider('formatFileSizeFilter', function () { + var $config = { + // Byte units following the IEC format + // http://en.wikipedia.org/wiki/Kilobyte + units: [ + {size: 1000000000, suffix: ' GB'}, + {size: 1000000, suffix: ' MB'}, + {size: 1000, suffix: ' KB'} + ] + }; + this.defaults = $config; + this.$get = function () { + return function (bytes) { + if (!angular.isNumber(bytes)) { + return ''; + } + var unit = true, + i = 0, + prefix, + suffix; + while (unit) { + unit = $config.units[i]; + prefix = unit.prefix || ''; + suffix = unit.suffix || ''; + if (i === $config.units.length - 1 || bytes >= unit.size) { + return prefix + (bytes / unit.size).toFixed(2) + suffix; + } + i += 1; + } + }; + }; + }) + + // The FileUploadController initializes the fileupload widget and + // provides scope methods to control the File Upload functionality: + .controller('FileUploadController', [ + '$scope', '$element', '$attrs', '$window', 'fileUpload', + function ($scope, $element, $attrs, $window, fileUpload) { + var uploadMethods = { + progress: function () { + return $element.fileupload('progress'); + }, + active: function () { + return $element.fileupload('active'); + }, + option: function (option, data) { + if (arguments.length === 1) { + return $element.fileupload('option', option); + } + $element.fileupload('option', option, data); + }, + add: function (data) { + return $element.fileupload('add', data); + }, + send: function (data) { + return $element.fileupload('send', data); + }, + process: function (data) { + return $element.fileupload('process', data); + }, + processing: function (data) { + return $element.fileupload('processing', data); + } + }; + $scope.disabled = !$window.jQuery.support.fileInput; + $scope.queue = $scope.queue || []; + $scope.clear = function (files) { + var queue = this.queue, + i = queue.length, + file = files, + length = 1; + if (angular.isArray(files)) { + file = files[0]; + length = files.length; + } + while (i) { + i -= 1; + if (queue[i] === file) { + return queue.splice(i, length); + } + } + }; + $scope.replace = function (oldFiles, newFiles) { + var queue = this.queue, + file = oldFiles[0], + i, + j; + for (i = 0; i < queue.length; i += 1) { + if (queue[i] === file) { + for (j = 0; j < newFiles.length; j += 1) { + queue[i + j] = newFiles[j]; + } + return; + } + } + }; + $scope.applyOnQueue = function (method) { + var list = this.queue.slice(0), + i, + file; + for (i = 0; i < list.length; i += 1) { + file = list[i]; + if (file[method]) { + file[method](); + } + } + }; + $scope.submit = function () { + this.applyOnQueue('$submit'); + }; + $scope.cancel = function () { + this.applyOnQueue('$cancel'); + }; + // Add upload methods to the scope: + angular.extend($scope, uploadMethods); + // The fileupload widget will initialize with + // the options provided via "data-"-parameters, + // as well as those given via options object: + $element.fileupload(angular.extend( + {scope: $scope}, + fileUpload.defaults + )).on('fileuploadadd', function (e, data) { + data.scope = $scope; + }).on('fileuploadfail', function (e, data) { + if (data.errorThrown === 'abort') { + return; + } + if (data.dataType && + data.dataType.indexOf('json') === data.dataType.length - 4) { + try { + data.result = angular.fromJson(data.jqXHR.responseText); + } catch (ignore) {} + } + }).on([ + 'fileuploadadd', + 'fileuploadsubmit', + 'fileuploadsend', + 'fileuploaddone', + 'fileuploadfail', + 'fileuploadalways', + 'fileuploadprogress', + 'fileuploadprogressall', + 'fileuploadstart', + 'fileuploadstop', + 'fileuploadchange', + 'fileuploadpaste', + 'fileuploaddrop', + 'fileuploaddragover', + 'fileuploadchunksend', + 'fileuploadchunkdone', + 'fileuploadchunkfail', + 'fileuploadchunkalways', + 'fileuploadprocessstart', + 'fileuploadprocess', + 'fileuploadprocessdone', + 'fileuploadprocessfail', + 'fileuploadprocessalways', + 'fileuploadprocessstop' + ].join(' '), function (e, data) { + $scope.$parent.$applyAsync(function () { + if ($scope.$emit(e.type, data).defaultPrevented) { + e.preventDefault(); + } + }); + }).on('remove', function () { + // Remove upload methods from the scope, + // when the widget is removed: + var method; + for (method in uploadMethods) { + if (uploadMethods.hasOwnProperty(method)) { + delete $scope[method]; + } + } + }); + // Observe option changes: + $scope.$watch( + $attrs.fileUpload, + function (newOptions) { + if (newOptions) { + $element.fileupload('option', newOptions); + } + } + ); + } + ]) + + // Provide File Upload progress feedback: + .controller('FileUploadProgressController', [ + '$scope', '$attrs', '$parse', + function ($scope, $attrs, $parse) { + var fn = $parse($attrs.fileUploadProgress), + update = function () { + var progress = fn($scope); + if (!progress || !progress.total) { + return; + } + $scope.num = Math.floor( + progress.loaded / progress.total * 100 + ); + }; + update(); + $scope.$watch( + $attrs.fileUploadProgress + '.loaded', + function (newValue, oldValue) { + if (newValue !== oldValue) { + update(); + } + } + ); + } + ]) + + // Display File Upload previews: + .controller('FileUploadPreviewController', [ + '$scope', '$element', '$attrs', + function ($scope, $element, $attrs) { + $scope.$watch( + $attrs.fileUploadPreview + '.preview', + function (preview) { + $element.empty(); + if (preview) { + $element.append(preview); + } + } + ); + } + ]) + + .directive('fileUpload', function () { + return { + controller: 'FileUploadController', + scope: true + }; + }) + + .directive('fileUploadProgress', function () { + return { + controller: 'FileUploadProgressController', + scope: true + }; + }) + + .directive('fileUploadPreview', function () { + return { + controller: 'FileUploadPreviewController' + }; + }) + + // Enhance the HTML5 download attribute to + // allow drag&drop of files to the desktop: + .directive('download', function () { + return function (scope, elm) { + elm.on('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [ + 'application/octet-stream', + elm.prop('download'), + elm.prop('href') + ].join(':') + ); + } catch (ignore) {} + }); + }; + }); + +})); diff --git a/app/assets/javascripts/jquery-fileupload/jquery.fileupload-audio.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-audio.js new file mode 100644 index 0000000..7b9119d --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-audio.js @@ -0,0 +1,112 @@ +/* + * jQuery File Upload Audio Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('load-image') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadAudio', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableAudioPreview' + }, + { + action: 'setAudio', + name: '@audioPreviewName', + disabled: '@disableAudioPreview' + } + ); + + // The File Upload Audio Preview plugin extends the fileupload widget + // with audio preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of audio files to load, + // matched against the file type: + loadAudioFileTypes: /^audio\/.*$/ + }, + + _audioElement: document.createElement('audio'), + + processActions: { + + // Loads the audio file given via data.files and data.index + // as audio element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadAudio: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + audio; + if (this._audioElement.canPlayType && + this._audioElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + audio = this._audioElement.cloneNode(false); + audio.src = url; + audio.controls = true; + data.audio = audio; + return data; + } + } + return data; + }, + + // Sets the audio element as a property of the file object: + setAudio: function (data, options) { + if (data.audio && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.audio; + } + return data; + } + + } + + }); + +})); diff --git a/app/assets/javascripts/jquery-fileupload/jquery.fileupload-image.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-image.js new file mode 100644 index 0000000..b20b502 --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-image.js @@ -0,0 +1,320 @@ +/* + * jQuery File Upload Image Preview & Resize Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, Blob */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + 'load-image-meta', + 'load-image-exif', + 'canvas-to-blob', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('load-image') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadImageMetaData', + disableImageHead: '@', + disableExif: '@', + disableExifThumbnail: '@', + disableExifSub: '@', + disableExifGps: '@', + disabled: '@disableImageMetaDataLoad' + }, + { + action: 'loadImage', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + noRevoke: '@', + disabled: '@disableImageLoad' + }, + { + action: 'resizeImage', + // Use "image" as prefix for the "@" options: + prefix: 'image', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + forceResize: '@', + disabled: '@disableImageResize' + }, + { + action: 'saveImage', + quality: '@imageQuality', + type: '@imageType', + disabled: '@disableImageResize' + }, + { + action: 'saveImageMetaData', + disabled: '@disableImageMetaDataSave' + }, + { + action: 'resizeImage', + // Use "preview" as prefix for the "@" options: + prefix: 'preview', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + thumbnail: '@', + canvas: '@', + disabled: '@disableImagePreview' + }, + { + action: 'setImage', + name: '@imagePreviewName', + disabled: '@disableImagePreview' + }, + { + action: 'deleteImageReferences', + disabled: '@disableImageReferencesDeletion' + } + ); + + // The File Upload Resize plugin extends the fileupload widget + // with image resize functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of images to load: + // matched against the file type: + loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/, + // The maximum file size of images to load: + loadImageMaxFileSize: 10000000, // 10MB + // The maximum width of resized images: + imageMaxWidth: 1920, + // The maximum height of resized images: + imageMaxHeight: 1080, + // Defines the image orientation (1-8) or takes the orientation + // value from Exif data if set to true: + imageOrientation: false, + // Define if resized images should be cropped or only scaled: + imageCrop: false, + // Disable the resize image functionality by default: + disableImageResize: true, + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // Defines the preview orientation (1-8) or takes the orientation + // value from Exif data if set to true: + previewOrientation: true, + // Create the preview using the Exif data thumbnail: + previewThumbnail: true, + // Define if preview images should be cropped or only scaled: + previewCrop: false, + // Define if preview images should be resized as canvas elements: + previewCanvas: true + }, + + processActions: { + + // Loads the image given via data.files and data.index + // as img element, if the browser supports the File API. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadImage: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (($.type(options.maxFileSize) === 'number' && + file.size > options.maxFileSize) || + (options.fileTypes && + !options.fileTypes.test(file.type)) || + !loadImage( + file, + function (img) { + if (img.src) { + data.img = img; + } + dfd.resolveWith(that, [data]); + }, + options + )) { + return data; + } + return dfd.promise(); + }, + + // Resizes the image given as data.canvas or data.img + // and updates data.canvas or data.img with the resized image. + // Also stores the resized image as preview property. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + if (options.disabled || !(data.canvas || data.img)) { + return data; + } + options = $.extend({canvas: true}, options); + var that = this, + dfd = $.Deferred(), + img = (options.canvas && data.canvas) || data.img, + resolve = function (newImg) { + if (newImg && (newImg.width !== img.width || + newImg.height !== img.height || + options.forceResize)) { + data[newImg.getContext ? 'canvas' : 'img'] = newImg; + } + data.preview = newImg; + dfd.resolveWith(that, [data]); + }, + thumbnail; + if (data.exif) { + if (options.orientation === true) { + options.orientation = data.exif.get('Orientation'); + } + if (options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + if (thumbnail) { + loadImage(thumbnail, resolve, options); + return dfd.promise(); + } + } + // Prevent orienting the same image twice: + if (data.orientation) { + delete options.orientation; + } else { + data.orientation = options.orientation; + } + } + if (img) { + resolve(loadImage.scale(img, options)); + return dfd.promise(); + } + return data; + }, + + // Saves the processed image given as data.canvas + // inplace at data.index of data.files: + saveImage: function (data, options) { + if (!data.canvas || options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (data.canvas.toBlob) { + data.canvas.toBlob( + function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\.\w+$/, + '.' + blob.type.substr(6) + ); + } + } + // Don't restore invalid meta data: + if (file.type !== blob.type) { + delete data.imageHead; + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }, + options.type || file.type, + options.quality + ); + } else { + return data; + } + return dfd.promise(); + }, + + loadImageMetaData: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + dfd = $.Deferred(); + loadImage.parseMetaData(data.files[data.index], function (result) { + $.extend(data, result); + dfd.resolveWith(that, [data]); + }, options); + return dfd.promise(); + }, + + saveImageMetaData: function (data, options) { + if (!(data.imageHead && data.canvas && + data.canvas.toBlob && !options.disabled)) { + return data; + } + var file = data.files[data.index], + blob = new Blob([ + data.imageHead, + // Resized images always have a head size of 20 bytes, + // including the JPEG marker and a minimal JFIF header: + this._blobSlice.call(file, 20) + ], {type: file.type}); + blob.name = file.name; + data.files[data.index] = blob; + return data; + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + if (data.preview && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.preview; + } + return data; + }, + + deleteImageReferences: function (data, options) { + if (!options.disabled) { + delete data.img; + delete data.canvas; + delete data.preview; + delete data.imageHead; + } + return data; + } + + } + + }); + +})); diff --git a/app/assets/javascripts/jquery-fileupload/jquery.fileupload-jquery-ui.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-jquery-ui.js new file mode 100755 index 0000000..ac978b7 --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-jquery-ui.js @@ -0,0 +1,155 @@ +/* + * jQuery File Upload jQuery UI Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery', './jquery.fileupload-ui'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + processdone: function (e, data) { + data.context.find('.start').button('enable'); + }, + progress: function (e, data) { + if (data.context) { + data.context.find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ); + } + }, + progressall: function (e, data) { + var $this = $(this); + $this.find('.fileupload-progress') + .find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ).end() + .find('.progress-extended').each(function () { + $(this).html( + ($this.data('blueimp-fileupload') || + $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + }); + } + }, + + _renderUpload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.progress').empty().progressbar(); + node.find('.start').button({ + icons: {primary: 'ui-icon-circle-arrow-e'}, + text: showIconText + }); + node.find('.cancel').button({ + icons: {primary: 'ui-icon-cancel'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _renderDownload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.delete').button({ + icons: {primary: 'ui-icon-trash'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _startHandler: function (e) { + $(e.currentTarget).button('disable'); + this._super(e); + }, + + _transition: function (node) { + var deferred = $.Deferred(); + if (node.hasClass('fade')) { + node.fadeToggle( + this.options.transitionDuration, + this.options.transitionEasing, + function () { + deferred.resolveWith(node); + } + ); + } else { + deferred.resolveWith(node); + } + return deferred; + }, + + _create: function () { + this._super(); + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button({icons: {primary: 'ui-icon-plusthick'}}) + .append(input); + }) + .end().find('.start') + .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) + .end().find('.cancel') + .button({icons: {primary: 'ui-icon-cancel'}}) + .end().find('.delete') + .button({icons: {primary: 'ui-icon-trash'}}) + .end().find('.progress').progressbar(); + }, + + _destroy: function () { + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button('destroy') + .append(input); + }) + .end().find('.start') + .button('destroy') + .end().find('.cancel') + .button('destroy') + .end().find('.delete') + .button('destroy') + .end().find('.progress').progressbar('destroy'); + this._super(); + } + + }); + +})); diff --git a/app/assets/javascripts/jquery-fileupload/jquery.fileupload-process.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-process.js new file mode 100644 index 0000000..ba09945 --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-process.js @@ -0,0 +1,175 @@ +/* + * jQuery File Upload Processing Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + var originalAdd = $.blueimp.fileupload.prototype.options.add; + + // The File Upload Processing plugin extends the fileupload widget + // with file processing functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The list of processing actions: + processQueue: [ + /* + { + action: 'log', + type: 'debug' + } + */ + ], + add: function (e, data) { + var $this = $(this); + data.process(function () { + return $this.fileupload('process', data); + }); + originalAdd.call(this, e, data); + } + }, + + processActions: { + /* + log: function (data, options) { + console[options.type]( + 'Processing "' + data.files[data.index].name + '"' + ); + } + */ + }, + + _processFile: function (data, originalData) { + var that = this, + dfd = $.Deferred().resolveWith(that, [data]), + chain = dfd.promise(); + this._trigger('process', null, data); + $.each(data.processQueue, function (i, settings) { + var func = function (data) { + if (originalData.errorThrown) { + return $.Deferred() + .rejectWith(that, [originalData]).promise(); + } + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain.then(func, settings.always && func); + }); + chain + .done(function () { + that._trigger('processdone', null, data); + that._trigger('processalways', null, data); + }) + .fail(function () { + that._trigger('processfail', null, data); + that._trigger('processalways', null, data); + }); + return chain; + }, + + // Replaces the settings of each processQueue item that + // are strings starting with an "@", using the remaining + // substring as key for the option map, + // e.g. "@autoUpload" is replaced with options.autoUpload: + _transformProcessQueue: function (options) { + var processQueue = []; + $.each(options.processQueue, function () { + var settings = {}, + action = this.action, + prefix = this.prefix === true ? action : this.prefix; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && + value.charAt(0) === '@') { + settings[key] = options[ + value.slice(1) || (prefix ? prefix + + key.charAt(0).toUpperCase() + key.slice(1) : key) + ]; + } else { + settings[key] = value; + } + + }); + processQueue.push(settings); + }); + options.processQueue = processQueue; + }, + + // Returns the number of files currently in the processsing queue: + processing: function () { + return this._processing; + }, + + // Processes the files given as files property of the data parameter, + // returns a Promise object that allows to bind callbacks: + process: function (data) { + var that = this, + options = $.extend({}, this.options, data); + if (options.processQueue && options.processQueue.length) { + this._transformProcessQueue(options); + if (this._processing === 0) { + this._trigger('processstart'); + } + $.each(data.files, function (index) { + var opts = index ? $.extend({}, options) : options, + func = function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return that._processFile(opts, data); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue.then(func, func) + .always(function () { + that._processing -= 1; + if (that._processing === 0) { + that._trigger('processstop'); + } + }); + }); + } + return this._processingQueue; + }, + + _create: function () { + this._super(); + this._processing = 0; + this._processingQueue = $.Deferred().resolveWith(this) + .promise(); + } + + }); + +})); diff --git a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js similarity index 52% rename from vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js rename to app/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js index 4c36f00..61d0a4a 100644 --- a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js @@ -1,5 +1,5 @@ /* - * jQuery File Upload User Interface Plugin 6.9.4 + * jQuery File Upload User Interface Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan @@ -9,63 +9,52 @@ * http://www.opensource.org/licenses/MIT */ -/*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document, URL, webkitURL, FileReader */ +/* jshint nomen:false */ +/* global define, require, window */ -(function (factory) { +;(function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: define([ 'jquery', 'tmpl', - 'load-image', - './jquery.fileupload-fp' + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('tmpl') + ); } else { // Browser globals: factory( window.jQuery, - window.tmpl, - window.loadImage + window.tmpl ); } -}(function ($, tmpl, loadImage) { +}(function ($, tmpl) { 'use strict'; - // The UI version extends the FP (file processing) version or the basic - // file upload widget and adds complete user interface interaction: - var parentWidget = ($.blueimpFP || $.blueimp).fileupload; - $.widget('blueimpUI.fileupload', parentWidget, { + $.blueimp.fileupload.prototype._specialOptions.push( + 'filesContainer', + 'uploadTemplateId', + 'downloadTemplateId' + ); + + // The UI version extends the file upload widget + // and adds complete user interface interaction: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { options: { // By default, files added to the widget are uploaded as soon // as the user clicks on the start buttons. To enable automatic // uploads, set the following option to true: autoUpload: false, - // The following option limits the number of files that are - // allowed to be uploaded using this widget: - maxNumberOfFiles: undefined, - // The maximum allowed file size: - maxFileSize: undefined, - // The minimum allowed file size: - minFileSize: undefined, - // The regular expression for allowed file types, matches - // against either file type or file name: - acceptFileTypes: /.+$/i, - // The regular expression to define for which files a preview - // image is shown, matched against the file type: - previewSourceFileTypes: /^image\/(gif|jpeg|png)$/, - // The maximum file size of images that are to be displayed as preview: - previewSourceMaxFileSize: 5000000, // 5MB - // The maximum width of the preview images: - previewMaxWidth: 80, - // The maximum height of the preview images: - previewMaxHeight: 80, - // By default, preview images are displayed as canvas elements - // if supported by the browser. Set the following option to false - // to always display preview images as img elements: - previewAsCanvas: true, // The ID of the upload template: uploadTemplateId: 'template-upload', // The ID of the download template: @@ -80,46 +69,79 @@ // option of the $.ajax upload requests: dataType: 'json', + // Error and info messages: + messages: { + unknownError: 'Unknown error' + }, + + // Function returning the current number of files, + // used by the maxNumberOfFiles validation: + getNumberOfFiles: function () { + return this.filesContainer.children() + .not('.processing').length; + }, + + // Callback to retrieve the list of files from the server response: + getFilesFromResponse: function (data) { + if (data.result && $.isArray(data.result.files)) { + return data.result.files; + } + return []; + }, + // The add callback is invoked as soon as files are added to the fileupload // widget (via file input selection, drag & drop or add API call). // See the basic file upload widget for more information: add: function (e, data) { - var that = $(this).data('fileupload'), - options = that.options, - files = data.files; - $(this).fileupload('process', data).done(function () { - that._adjustMaxNumberOfFiles(-files.length); - data.maxNumberOfFilesAdjusted = true; - data.files.valid = data.isValidated = that._validate(files); - data.context = that._renderUpload(files).data('data', data); - options.filesContainer[ - options.prependFiles ? 'prepend' : 'append' - ](data.context); - that._renderPreviews(files, data.context); - that._forceReflow(data.context); - that._transition(data.context).done( - function () { - if ((that._trigger('added', e, data) !== false) && - (options.autoUpload || data.autoUpload) && - data.autoUpload !== false && data.isValidated) { - data.submit(); + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + that = $this.data('blueimp-fileupload') || + $this.data('fileupload'), + options = that.options; + data.context = that._renderUpload(data.files) + .data('data', data) + .addClass('processing'); + options.filesContainer[ + options.prependFiles ? 'prepend' : 'append' + ](data.context); + that._forceReflow(data.context); + that._transition(data.context); + data.process(function () { + return $this.fileupload('process', data); + }).always(function () { + data.context.each(function (index) { + $(this).find('.size').text( + that._formatFileSize(data.files[index].size) + ); + }).removeClass('processing'); + that._renderPreviews(data); + }).done(function () { + data.context.find('.start').prop('disabled', false); + if ((that._trigger('added', e, data) !== false) && + (options.autoUpload || data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }).fail(function () { + if (data.files.error) { + data.context.each(function (index) { + var error = data.files[index].error; + if (error) { + $(this).find('.error').text(error); } - } - ); + }); + } }); }, // Callback for the start of each file upload request: send: function (e, data) { - var that = $(this).data('fileupload'); - if (!data.isValidated) { - if (!data.maxNumberOfFilesAdjusted) { - that._adjustMaxNumberOfFiles(-data.files.length); - data.maxNumberOfFilesAdjusted = true; - } - if (!that._validate(data.files)) { - return false; - } + if (e.isDefaultPrevented()) { + return false; } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); if (data.context && data.dataType && data.dataType.substr(0, 6) === 'iframe') { // Iframe Transport does not support progress events. @@ -130,7 +152,7 @@ !$.support.transition && 'progress-animated' ) .attr('aria-valuenow', 100) - .find('.bar').css( + .children().first().css( 'width', '100%' ); @@ -139,15 +161,21 @@ }, // Callback for successful uploads: done: function (e, data) { - var that = $(this).data('fileupload'), - template; + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + getFilesFromResponse = data.getFilesFromResponse || + that.options.getFilesFromResponse, + files = getFilesFromResponse(data), + template, + deferred; if (data.context) { data.context.each(function (index) { - var file = ($.isArray(data.result) && - data.result[index]) || {error: 'emptyResult'}; - if (file.error) { - that._adjustMaxNumberOfFiles(1); - } + var file = files[index] || + {error: 'Empty file upload result'}; + deferred = that._addFinishedDeferreds(); that._transition($(this)).done( function () { var node = $(this); @@ -158,47 +186,45 @@ function () { data.context = $(this); that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); } ); } ); }); } else { - if ($.isArray(data.result)) { - $.each(data.result, function (index, file) { - if (data.maxNumberOfFilesAdjusted && file.error) { - that._adjustMaxNumberOfFiles(1); - } else if (!data.maxNumberOfFilesAdjusted && - !file.error) { - that._adjustMaxNumberOfFiles(-1); - } - }); - data.maxNumberOfFilesAdjusted = true; - } - template = that._renderDownload(data.result) - .appendTo(that.options.filesContainer); + template = that._renderDownload(files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer); that._forceReflow(template); + deferred = that._addFinishedDeferreds(); that._transition(template).done( function () { data.context = $(this); that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); } ); } }, // Callback for failed (abort or error) uploads: fail: function (e, data) { - var that = $(this).data('fileupload'), - template; - if (data.maxNumberOfFilesAdjusted) { - that._adjustMaxNumberOfFiles(data.files.length); + if (e.isDefaultPrevented()) { + return false; } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + template, + deferred; if (data.context) { data.context.each(function (index) { if (data.errorThrown !== 'abort') { var file = data.files[index]; file.error = file.error || data.errorThrown || - true; + data.i18n('unknownError'); + deferred = that._addFinishedDeferreds(); that._transition($(this)).done( function () { var node = $(this); @@ -209,69 +235,94 @@ function () { data.context = $(this); that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); } ); } ); } else { + deferred = that._addFinishedDeferreds(); that._transition($(this)).done( function () { $(this).remove(); that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); } ); } }); } else if (data.errorThrown !== 'abort') { - data.context = that._renderUpload(data.files) - .appendTo(that.options.filesContainer) + data.context = that._renderUpload(data.files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer) .data('data', data); that._forceReflow(data.context); + deferred = that._addFinishedDeferreds(); that._transition(data.context).done( function () { data.context = $(this); that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); } ); } else { that._trigger('failed', e, data); + that._trigger('finished', e, data); + that._addFinishedDeferreds().resolve(); } }, // Callback for upload progress events: progress: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var progress = Math.floor(data.loaded / data.total * 100); if (data.context) { - var progress = parseInt(data.loaded / data.total * 100, 10); - data.context.find('.progress') - .attr('aria-valuenow', progress) - .find('.bar').css( - 'width', - progress + '%' - ); + data.context.each(function () { + $(this).find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }); } }, // Callback for global upload progress events: progressall: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } var $this = $(this), - progress = parseInt(data.loaded / data.total * 100, 10), + progress = Math.floor(data.loaded / data.total * 100), globalProgressNode = $this.find('.fileupload-progress'), extendedProgressNode = globalProgressNode .find('.progress-extended'); if (extendedProgressNode.length) { extendedProgressNode.html( - $this.data('fileupload')._renderExtendedProgress(data) + ($this.data('blueimp-fileupload') || $this.data('fileupload')) + ._renderExtendedProgress(data) ); } globalProgressNode .find('.progress') .attr('aria-valuenow', progress) - .find('.bar').css( + .children().first().css( 'width', progress + '%' ); }, // Callback for uploads start, equivalent to the global ajaxStart event: start: function (e) { - var that = $(this).data('fileupload'); + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + that._resetFinishedDeferreds(); that._transition($(this).find('.fileupload-progress')).done( function () { that._trigger('started', e); @@ -280,33 +331,80 @@ }, // Callback for uploads stop, equivalent to the global ajaxStop event: stop: function (e) { - var that = $(this).data('fileupload'); + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + deferred = that._addFinishedDeferreds(); + $.when.apply($, that._getFinishedDeferreds()) + .done(function () { + that._trigger('stopped', e); + }); that._transition($(this).find('.fileupload-progress')).done( function () { $(this).find('.progress') .attr('aria-valuenow', '0') - .find('.bar').css('width', '0%'); + .children().first().css('width', '0%'); $(this).find('.progress-extended').html(' '); - that._trigger('stopped', e); + deferred.resolve(); } ); }, + processstart: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).addClass('fileupload-processing'); + }, + processstop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).removeClass('fileupload-processing'); + }, // Callback for file deletion: destroy: function (e, data) { - var that = $(this).data('fileupload'); + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + removeNode = function () { + that._transition(data.context).done( + function () { + $(this).remove(); + that._trigger('destroyed', e, data); + } + ); + }; if (data.url) { - $.ajax(data); - that._adjustMaxNumberOfFiles(1); + data.dataType = data.dataType || that.options.dataType; + $.ajax(data).done(removeNode).fail(function () { + that._trigger('destroyfailed', e, data); + }); + } else { + removeNode(); } - that._transition(data.context).done( - function () { - $(this).remove(); - that._trigger('destroyed', e, data); - } - ); } }, + _resetFinishedDeferreds: function () { + this._finishedUploads = []; + }, + + _addFinishedDeferreds: function (deferred) { + if (!deferred) { + deferred = $.Deferred(); + } + this._finishedUploads.push(deferred); + return deferred; + }, + + _getFinishedDeferreds: function () { + return this._finishedUploads; + }, + // Link handler, that allows to download files // by drag & drop of the links to the desktop: _enableDragToDesktop: function () { @@ -320,21 +418,10 @@ 'DownloadURL', [type, name, url].join(':') ); - } catch (err) {} + } catch (ignore) {} }); }, - _adjustMaxNumberOfFiles: function (operand) { - if (typeof this.options.maxNumberOfFiles === 'number') { - this.options.maxNumberOfFiles += operand; - if (this.options.maxNumberOfFiles < 1) { - this._disableFileInputButton(); - } else { - this._enableFileInputButton(); - } - } - }, - _formatFileSize: function (bytes) { if (typeof bytes !== 'number') { return ''; @@ -361,12 +448,12 @@ if (bits >= 1000) { return (bits / 1000).toFixed(2) + ' kbit/s'; } - return bits + ' bit/s'; + return bits.toFixed(2) + ' bit/s'; }, _formatTime: function (seconds) { var date = new Date(seconds * 1000), - days = parseInt(seconds / 86400, 10); + days = Math.floor(seconds / 86400); days = days ? days + 'd ' : ''; return days + ('0' + date.getUTCHours()).slice(-2) + ':' + @@ -390,46 +477,6 @@ this._formatFileSize(data.total); }, - _hasError: function (file) { - if (file.error) { - return file.error; - } - // The number of added files is subtracted from - // maxNumberOfFiles before validation, so we check if - // maxNumberOfFiles is below 0 (instead of below 1): - if (this.options.maxNumberOfFiles < 0) { - return 'maxNumberOfFiles'; - } - // Files are accepted if either the file type or the file name - // matches against the acceptFileTypes regular expression, as - // only browsers with support for the File API report the type: - if (!(this.options.acceptFileTypes.test(file.type) || - this.options.acceptFileTypes.test(file.name))) { - return 'acceptFileTypes'; - } - if (this.options.maxFileSize && - file.size > this.options.maxFileSize) { - return 'maxFileSize'; - } - if (typeof file.size === 'number' && - file.size < this.options.minFileSize) { - return 'minFileSize'; - } - return null; - }, - - _validate: function (files) { - var that = this, - valid = !!files.length; - $.each(files, function (index, file) { - file.error = that._hasError(file); - if (file.error) { - valid = false; - } - }); - return valid; - }, - _renderTemplate: function (func, files) { if (!func) { return $(); @@ -445,53 +492,10 @@ return $(this.options.templatesContainer).html(result).children(); }, - _renderPreview: function (file, node) { - var that = this, - options = this.options, - dfd = $.Deferred(); - return ((loadImage && loadImage( - file, - function (img) { - node.append(img); - that._forceReflow(node); - that._transition(node).done(function () { - dfd.resolveWith(node); - }); - if (!$.contains(document.body, node[0])) { - // If the element is not part of the DOM, - // transition events are not triggered, - // so we have to resolve manually: - dfd.resolveWith(node); - } - }, - { - maxWidth: options.previewMaxWidth, - maxHeight: options.previewMaxHeight, - canvas: options.previewAsCanvas - } - )) || dfd.resolveWith(node)) && dfd; - }, - - _renderPreviews: function (files, nodes) { - var that = this, - options = this.options; - nodes.find('.preview span').each(function (index, element) { - var file = files[index]; - if (options.previewSourceFileTypes.test(file.type) && - ($.type(options.previewSourceMaxFileSize) !== 'number' || - file.size < options.previewSourceMaxFileSize)) { - that._processingQueue = that._processingQueue.pipe(function () { - var dfd = $.Deferred(); - that._renderPreview(file, $(element)).done( - function () { - dfd.resolveWith(that); - } - ); - return dfd.promise(); - }); - } + _renderPreviews: function (data) { + data.context.find('.preview').each(function (index, elm) { + $(elm).append(data.files[index].preview); }); - return this._processingQueue; }, _renderUpload: function (files) { @@ -510,35 +514,36 @@ _startHandler: function (e) { e.preventDefault(); - var button = $(this), + var button = $(e.currentTarget), template = button.closest('.template-upload'), data = template.data('data'); - if (data && data.submit && !data.jqXHR && data.submit()) { - button.prop('disabled', true); + button.prop('disabled', true); + if (data && data.submit) { + data.submit(); } }, _cancelHandler: function (e) { e.preventDefault(); - var template = $(this).closest('.template-upload'), + var template = $(e.currentTarget) + .closest('.template-upload,.template-download'), data = template.data('data') || {}; - if (!data.jqXHR) { - data.errorThrown = 'abort'; - e.data.fileupload._trigger('fail', e, data); + data.context = data.context || template; + if (data.abort) { + data.abort(); } else { - data.jqXHR.abort(); + data.errorThrown = 'abort'; + this._trigger('fail', e, data); } }, _deleteHandler: function (e) { e.preventDefault(); - var button = $(this); - e.data.fileupload._trigger('destroy', e, { + var button = $(e.currentTarget); + this._trigger('destroy', e, $.extend({ context: button.closest('.template-download'), - url: button.attr('data-url'), - type: button.attr('data-type') || 'DELETE', - dataType: e.data.fileupload.options.dataType - }); + type: 'DELETE' + }, button.data())); }, _forceReflow: function (node) { @@ -548,7 +553,7 @@ _transition: function (node) { var dfd = $.Deferred(); - if ($.support.transition && node.hasClass('fade')) { + if ($.support.transition && node.hasClass('fade') && node.is(':visible')) { node.bind( $.support.transition.end, function (e) { @@ -569,75 +574,65 @@ _initButtonBarEventHandlers: function () { var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), - filesList = this.options.filesContainer, - ns = this.options.namespace; - fileUploadButtonBar.find('.start') - .bind('click.' + ns, function (e) { + filesList = this.options.filesContainer; + this._on(fileUploadButtonBar.find('.start'), { + click: function (e) { e.preventDefault(); - filesList.find('.start button').click(); - }); - fileUploadButtonBar.find('.cancel') - .bind('click.' + ns, function (e) { + filesList.find('.start').click(); + } + }); + this._on(fileUploadButtonBar.find('.cancel'), { + click: function (e) { e.preventDefault(); - filesList.find('.cancel button').click(); - }); - fileUploadButtonBar.find('.delete') - .bind('click.' + ns, function (e) { + filesList.find('.cancel').click(); + } + }); + this._on(fileUploadButtonBar.find('.delete'), { + click: function (e) { e.preventDefault(); - filesList.find('.delete input:checked') - .siblings('button').click(); + filesList.find('.toggle:checked') + .closest('.template-download') + .find('.delete').click(); fileUploadButtonBar.find('.toggle') .prop('checked', false); - }); - fileUploadButtonBar.find('.toggle') - .bind('change.' + ns, function (e) { - filesList.find('.delete input').prop( + } + }); + this._on(fileUploadButtonBar.find('.toggle'), { + change: function (e) { + filesList.find('.toggle').prop( 'checked', - $(this).is(':checked') + $(e.currentTarget).is(':checked') ); - }); + } + }); }, _destroyButtonBarEventHandlers: function () { - this.element.find('.fileupload-buttonbar button') - .unbind('click.' + this.options.namespace); - this.element.find('.fileupload-buttonbar .toggle') - .unbind('change.' + this.options.namespace); + this._off( + this.element.find('.fileupload-buttonbar') + .find('.start, .cancel, .delete'), + 'click' + ); + this._off( + this.element.find('.fileupload-buttonbar .toggle'), + 'change.' + ); }, _initEventHandlers: function () { - parentWidget.prototype._initEventHandlers.call(this); - var eventData = {fileupload: this}; - this.options.filesContainer - .delegate( - '.start button', - 'click.' + this.options.namespace, - eventData, - this._startHandler - ) - .delegate( - '.cancel button', - 'click.' + this.options.namespace, - eventData, - this._cancelHandler - ) - .delegate( - '.delete button', - 'click.' + this.options.namespace, - eventData, - this._deleteHandler - ); + this._super(); + this._on(this.options.filesContainer, { + 'click .start': this._startHandler, + 'click .cancel': this._cancelHandler, + 'click .delete': this._deleteHandler + }); this._initButtonBarEventHandlers(); }, _destroyEventHandlers: function () { - var options = this.options; this._destroyButtonBarEventHandlers(); - options.filesContainer - .undelegate('.start button', 'click.' + options.namespace) - .undelegate('.cancel button', 'click.' + options.namespace) - .undelegate('.delete button', 'click.' + options.namespace); - parentWidget.prototype._destroyEventHandlers.call(this); + this._off(this.options.filesContainer, 'click'); + this._super(); }, _enableFileInputButton: function () { @@ -654,7 +649,7 @@ _initTemplates: function () { var options = this.options; - options.templatesContainer = document.createElement( + options.templatesContainer = this.document[0].createElement( options.filesContainer.prop('nodeName') ); if (tmpl) { @@ -676,59 +671,38 @@ } }, - _stringToRegExp: function (str) { - var parts = str.split('/'), - modifiers = parts.pop(); - parts.shift(); - return new RegExp(parts.join('/'), modifiers); - }, - - _initRegExpOptions: function () { - var options = this.options; - if ($.type(options.acceptFileTypes) === 'string') { - options.acceptFileTypes = this._stringToRegExp( - options.acceptFileTypes - ); - } - if ($.type(options.previewSourceFileTypes) === 'string') { - options.previewSourceFileTypes = this._stringToRegExp( - options.previewSourceFileTypes - ); - } - }, - _initSpecialOptions: function () { - parentWidget.prototype._initSpecialOptions.call(this); + this._super(); this._initFilesContainer(); this._initTemplates(); - this._initRegExpOptions(); }, _create: function () { - parentWidget.prototype._create.call(this); - this._refreshOptionsList.push( - 'filesContainer', - 'uploadTemplateId', - 'downloadTemplateId' - ); - if (!$.blueimpFP) { - this._processingQueue = $.Deferred().resolveWith(this).promise(); - this.process = function () { - return this._processingQueue; - }; + this._super(); + this._resetFinishedDeferreds(); + if (!$.support.fileInput) { + this._disableFileInputButton(); } }, enable: function () { - parentWidget.prototype.enable.call(this); - this.element.find('input, button').prop('disabled', false); - this._enableFileInputButton(); + var wasDisabled = false; + if (this.options.disabled) { + wasDisabled = true; + } + this._super(); + if (wasDisabled) { + this.element.find('input, button').prop('disabled', false); + this._enableFileInputButton(); + } }, disable: function () { - this.element.find('input, button').prop('disabled', true); - this._disableFileInputButton(); - parentWidget.prototype.disable.call(this); + if (!this.options.disabled) { + this.element.find('input, button').prop('disabled', true); + this._disableFileInputButton(); + } + this._super(); } }); diff --git a/app/assets/javascripts/jquery-fileupload/jquery.fileupload-validate.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-validate.js new file mode 100644 index 0000000..312256d --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-validate.js @@ -0,0 +1,122 @@ +/* + * jQuery File Upload Validation Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + // Append to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.push( + { + action: 'validate', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Options taken from the global options map: + acceptFileTypes: '@', + maxFileSize: '@', + minFileSize: '@', + maxNumberOfFiles: '@', + disabled: '@disableValidation' + } + ); + + // The File Upload Validation plugin extends the fileupload widget + // with file validation functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + /* + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + // The maximum allowed file size in bytes: + maxFileSize: 10000000, // 10 MB + // The minimum allowed file size in bytes: + minFileSize: undefined, // No minimal file size + // The limit of files to be uploaded: + maxNumberOfFiles: 10, + */ + + // Function returning the current number of files, + // has to be overriden for maxNumberOfFiles validation: + getNumberOfFiles: $.noop, + + // Error and info messages: + messages: { + maxNumberOfFiles: 'Maximum number of files exceeded', + acceptFileTypes: 'File type not allowed', + maxFileSize: 'File is too large', + minFileSize: 'File is too small' + } + }, + + processActions: { + + validate: function (data, options) { + if (options.disabled) { + return data; + } + var dfd = $.Deferred(), + settings = this.options, + file = data.files[data.index], + fileSize; + if (options.minFileSize || options.maxFileSize) { + fileSize = file.size; + } + if ($.type(options.maxNumberOfFiles) === 'number' && + (settings.getNumberOfFiles() || 0) + data.files.length > + options.maxNumberOfFiles) { + file.error = settings.i18n('maxNumberOfFiles'); + } else if (options.acceptFileTypes && + !(options.acceptFileTypes.test(file.type) || + options.acceptFileTypes.test(file.name))) { + file.error = settings.i18n('acceptFileTypes'); + } else if (fileSize > options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ($.type(fileSize) === 'number' && + fileSize < options.minFileSize) { + file.error = settings.i18n('minFileSize'); + } else { + delete file.error; + } + if (file.error || data.files.error) { + data.files.error = true; + dfd.rejectWith(this, [data]); + } else { + dfd.resolveWith(this, [data]); + } + return dfd.promise(); + } + + } + + }); + +})); diff --git a/app/assets/javascripts/jquery-fileupload/jquery.fileupload-video.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-video.js new file mode 100644 index 0000000..cfd008c --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload-video.js @@ -0,0 +1,112 @@ +/* + * jQuery File Upload Video Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('load-image') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadVideo', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableVideoPreview' + }, + { + action: 'setVideo', + name: '@videoPreviewName', + disabled: '@disableVideoPreview' + } + ); + + // The File Upload Video Preview plugin extends the fileupload widget + // with video preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of video files to load, + // matched against the file type: + loadVideoFileTypes: /^video\/.*$/ + }, + + _videoElement: document.createElement('video'), + + processActions: { + + // Loads the video file given via data.files and data.index + // as video element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadVideo: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + video; + if (this._videoElement.canPlayType && + this._videoElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + video = this._videoElement.cloneNode(false); + video.src = url; + video.controls = true; + data.video = video; + return data; + } + } + return data; + }, + + // Sets the video element as a property of the file object: + setVideo: function (data, options) { + if (data.video && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.video; + } + return data; + } + + } + + }); + +})); diff --git a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload.js b/app/assets/javascripts/jquery-fileupload/jquery.fileupload.js similarity index 51% rename from vendor/assets/javascripts/jquery-fileupload/jquery.fileupload.js rename to app/assets/javascripts/jquery-fileupload/jquery.fileupload.js index ba67a3a..a524778 100644 --- a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload.js +++ b/app/assets/javascripts/jquery-fileupload/jquery.fileupload.js @@ -1,5 +1,5 @@ /* - * jQuery File Upload Plugin 5.16.3 + * jQuery File Upload Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan @@ -9,10 +9,10 @@ * http://www.opensource.org/licenses/MIT */ -/*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document, Blob, FormData, location */ +/* jshint nomen:false */ +/* global define, require, window, document, location, Blob, FormData */ -(function (factory) { +;(function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: @@ -20,6 +20,12 @@ 'jquery', 'jquery.ui.widget' ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./vendor/jquery.ui.widget') + ); } else { // Browser globals: factory(window.jQuery); @@ -27,12 +33,49 @@ }(function ($) { 'use strict'; + // Detect file input support, based on + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ + $.support.fileInput = !(new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('').prop('disabled')); + // The FileReader API is not actually used, but works as feature detection, - // as e.g. Safari supports XHR file uploads via the FormData API, - // but not non-multipart XHR file uploads: - $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader); + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); $.support.xhrFormDataFileUpload = !!window.FormData; + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // Helper function to create drag handlers for dragover/dragenter/dragleave: + function getDragHandler(type) { + var isDragOver = type === 'dragover'; + return function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger( + type, + $.Event(type, {delegatedEvent: e}) + ) !== false) { + e.preventDefault(); + if (isDragOver) { + dataTransfer.dropEffect = 'copy'; + } + } + }; + } + // The fileupload widget listens for change events on file input fields defined // via fileInput setting and paste or drop events of the given dropZone. // In addition to the default jQuery Widget methods, the fileupload widget @@ -44,17 +87,16 @@ $.widget('blueimp.fileupload', { options: { - // The namespace used for event handler binding on the dropZone and - // fileInput collections. - // If not set, the name of the widget ("fileupload") is used. - namespace: undefined, - // The drop target collection, by the default the complete document. - // Set to null or an empty collection to disable drag & drop support: + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: dropZone: $(document), - // The file input field collection, that is listened for change events. + // The paste target element(s), by the default undefined. + // Set to a DOM node or jQuery object to enable file pasting: + pasteZone: undefined, + // The file input field(s), that are listened to for change events. // If undefined, it is set to the file input fields inside // of the widget element on plugin initialization. - // Set to null or an empty collection to disable the change listener. + // Set to null to disable the change listener. fileInput: undefined, // By default, the file input field is replaced with a clone after // each input field change event. This is required for iframe transport @@ -73,6 +115,14 @@ // To limit the number of files uploaded with one XHR request, // set the following option to an integer greater than 0: limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, // Set the following option to true to issue all file upload requests // in a sequential order: sequentialUploads: false, @@ -113,6 +163,25 @@ progressInterval: 100, // Interval in milliseconds to calculate progress bitrate: bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, // Additional form data to be sent along with the file uploads can be set // using this option, which accepts an array of objects with name and @@ -126,66 +195,109 @@ // The add callback is invoked as soon as files are added to the fileupload // widget (via file input selection, drag & drop, paste or add API call). // If the singleFileUploads option is enabled, this callback will be - // called once for each file in the selection for XHR file uplaods, else + // called once for each file in the selection for XHR file uploads, else // once for each file selection. + // // The upload starts when the submit method is invoked on the data parameter. // The data object contains a files property holding the added files - // and allows to override plugin options as well as define ajax settings. + // and allows you to override plugin options as well as define ajax settings. + // // Listeners for this callback can also be bound the following way: // .bind('fileuploadadd', func); + // // data.submit() returns a Promise object and allows to attach additional // handlers using jQuery's Deferred callbacks: // data.submit().done(func).fail(func).always(func); add: function (e, data) { - data.submit(); + if (e.isDefaultPrevented()) { + return false; + } + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } }, // Other callbacks: + // Callback for the submit event of each file upload: // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + // Callback for the start of each file upload request: // send: function (e, data) {}, // .bind('fileuploadsend', func); + // Callback for successful uploads: // done: function (e, data) {}, // .bind('fileuploaddone', func); + // Callback for failed (abort or error) uploads: // fail: function (e, data) {}, // .bind('fileuploadfail', func); + // Callback for completed (success, abort or error) requests: // always: function (e, data) {}, // .bind('fileuploadalways', func); + // Callback for upload progress events: // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + // Callback for global upload progress events: // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + // Callback for uploads start, equivalent to the global ajaxStart event: // start: function (e) {}, // .bind('fileuploadstart', func); + // Callback for uploads stop, equivalent to the global ajaxStop event: // stop: function (e) {}, // .bind('fileuploadstop', func); - // Callback for change events of the fileInput collection: + + // Callback for change events of the fileInput(s): // change: function (e, data) {}, // .bind('fileuploadchange', func); - // Callback for paste events to the dropZone collection: + + // Callback for paste events to the pasteZone(s): // paste: function (e, data) {}, // .bind('fileuploadpaste', func); - // Callback for drop events of the dropZone collection: + + // Callback for drop events of the dropZone(s): // drop: function (e, data) {}, // .bind('fileuploaddrop', func); - // Callback for dragover events of the dropZone collection: + + // Callback for dragover events of the dropZone(s): // dragover: function (e) {}, // .bind('fileuploaddragover', func); + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + // The plugin options are used as settings object for the ajax calls. // The following are jQuery ajax settings required for the file uploads: processData: false, contentType: false, - cache: false + cache: false, + timeout: 0 }, - // A list of options that require a refresh after assigning a new value: - _refreshOptionsList: [ - 'namespace', - 'dropZone', + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ 'fileInput', + 'dropZone', + 'pasteZone', 'multipart', 'forceIframeTransport' ], + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + _BitrateTimer: function () { - this.timestamp = +(new Date()); + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); this.loaded = 0; this.bitrate = 0; this.getBitrate = function (now, loaded, interval) { @@ -207,13 +319,13 @@ _getFormData: function (options) { var formData; - if (typeof options.formData === 'function') { + if ($.type(options.formData) === 'function') { return options.formData(options.form); } - if ($.isArray(options.formData)) { + if ($.isArray(options.formData)) { return options.formData; } - if (options.formData) { + if ($.type(options.formData) === 'object') { formData = []; $.each(options.formData, function (name, value) { formData.push({name: name, value: value}); @@ -231,10 +343,35 @@ return total; }, + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + _onProgress: function (e, data) { if (e.lengthComputable) { - var now = +(new Date()), - total, + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), loaded; if (data._time && data.progressInterval && (now - data._time < data.progressInterval) && @@ -242,16 +379,19 @@ return; } data._time = now; - total = data.total || this._getTotal(data.files); - loaded = parseInt( - e.loaded / e.total * (data.chunkSize || total), - 10 + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) ) + (data.uploadedBytes || 0); - this._loaded += loaded - (data.loaded || data.uploadedBytes || 0); - data.lengthComputable = true; - data.loaded = loaded; - data.total = total; - data.bitrate = data._bitrateTimer.getBitrate( + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( now, loaded, data.bitrateInterval @@ -259,19 +399,18 @@ // Trigger a custom progress event with a total data property set // to the file size(s) of the current upload and a loaded data // property calculated accordingly: - this._trigger('progress', e, data); + this._trigger( + 'progress', + $.Event('progress', {delegatedEvent: e}), + data + ); // Trigger a global progress event for all current file uploads, // including ajax calls queued for sequential file uploads: - this._trigger('progressall', e, { - lengthComputable: true, - loaded: this._loaded, - total: this._total, - bitrate: this._bitrateTimer.getBitrate( - now, - this._loaded, - data.bitrateInterval - ) - }); + this._trigger( + 'progressall', + $.Event('progressall', {delegatedEvent: e}), + this._progress + ); } }, @@ -295,35 +434,31 @@ } }, + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + _initXHRData: function (options) { - var formData, + var that = this, + formData, file = options.files[0], // Ignore non-multipart setting if not supported: multipart = options.multipart || !$.support.xhrFileUpload, - paramName = options.paramName[0]; - if (!multipart || options.blob) { - // For non-multipart uploads and chunked uploads, - // file meta data is not part of the request body, - // so we transmit this data as part of the HTTP headers. - // For cross domain requests, these headers must be allowed - // via Access-Control-Allow-Headers or removed using - // the beforeSend callback: - options.headers = $.extend(options.headers, { - 'X-File-Name': file.name, - 'X-File-Type': file.type, - 'X-File-Size': file.size - }); - if (!options.blob) { - // Non-chunked non-multipart upload: - options.contentType = file.type; - options.data = file; - } else if (!multipart) { - // Chunked non-multipart upload: - options.contentType = 'application/octet-stream'; - options.data = options.blob; - } + paramName = $.type(options.paramName) === 'array' ? + options.paramName[0] : options.paramName; + options.headers = $.extend({}, options.headers); + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; } - if (multipart && $.support.xhrFormDataFileUpload) { + if (!multipart) { + options.contentType = file.type || 'application/octet-stream'; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { if (options.postMessage) { // window.postMessage does not allow sending FormData // objects, so we just add the File/Blob objects to @@ -338,13 +473,14 @@ } else { $.each(options.files, function (index, file) { formData.push({ - name: options.paramName[index] || paramName, + name: ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, value: file }); }); } } else { - if (options.formData instanceof FormData) { + if (that._isInstanceOf('FormData', options.formData)) { formData = options.formData; } else { formData = new FormData(); @@ -356,14 +492,15 @@ formData.append(paramName, options.blob, file.name); } else { $.each(options.files, function (index, file) { - // File objects are also Blob instances. // This check allows the tests to run with // dummy objects: - if (file instanceof Blob) { + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { formData.append( - options.paramName[index] || paramName, + ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, file, - file.name + file.uploadName || file.name ); } }); @@ -376,13 +513,13 @@ }, _initIframeSettings: function (options) { + var targetHost = $('').prop('href', options.url).prop('host'); // Setting the dataType to iframe enables the iframe transport: options.dataType = 'iframe ' + (options.dataType || ''); // The iframe transport accepts a serialized array as form data: options.formData = this._getFormData(options); // Add redirect url to form data on cross-domain uploads: - if (options.redirect && $('').prop('href', options.url) - .prop('host') !== location.host) { + if (options.redirect && targetHost && targetHost !== location.host) { options.formData.push({ name: options.redirectParamName || 'redirect', value: options.redirect @@ -404,7 +541,7 @@ options.dataType = 'postmessage ' + (options.dataType || ''); } } else { - this._initIframeSettings(options, 'iframe'); + this._initIframeSettings(options); } }, @@ -436,15 +573,23 @@ // associated form, if available: if (!options.form || !options.form.length) { options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } } options.paramName = this._getParamName(options); if (!options.url) { options.url = options.form.prop('action') || location.href; } // The HTTP request method must be "POST" or "PUT": - options.type = (options.type || options.form.prop('method') || '') - .toUpperCase(); - if (options.type !== 'POST' && options.type !== 'PUT') { + options.type = (options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || '' + ).toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { options.type = 'POST'; } if (!options.formAcceptCharset) { @@ -459,6 +604,21 @@ return options; }, + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + // Maps jqXHR callbacks to the equivalent // methods of the given Promise object: _enhancePromise: function (promise) { @@ -483,24 +643,93 @@ return this._enhancePromise(promise); }, + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise([this])).then( + function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + } + ).then(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise([this]); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger( + 'submit', + $.Event('submit', {delegatedEvent: e}), + this + ) !== false) && that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.processing = function () { + return !this.jqXHR && this._processQueue && that + ._getDeferredState(this._processQueue) === 'pending'; + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + // Uploads a file in multiple, sequential requests // by splitting the file up in multiple blob chunks. // If the second parameter is true, only tests if the file // should be uploaded in chunks, but does not invoke any // upload requests: _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; var that = this, file = options.files[0], fs = file.size, - ub = options.uploadedBytes = options.uploadedBytes || 0, + ub = options.uploadedBytes, mcs = options.maxChunkSize || fs, - // Use the Blob methods with the slice implementation - // according to the W3C Blob API specification: - slice = file.webkitSlice || file.mozSlice || file.slice, - upload, - n, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), jqXHR, - pipe; + upload; if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || options.data) { return false; @@ -509,66 +738,84 @@ return true; } if (ub >= fs) { - file.error = 'uploadedBytes'; + file.error = options.i18n('uploadedBytes'); return this._getXHRPromise( false, options.context, [null, 'error', file.error] ); } - // n is the number of blobs to upload, - // calculated via filesize, uploaded bytes and max chunk size: - n = Math.ceil((fs - ub) / mcs); - // The chunk upload method accepting the chunk number as parameter: - upload = function (i) { - if (!i) { - return that._getXHRPromise(true, options.context); - } - // Upload the blobs in sequential order: - return upload(i -= 1).pipe(function () { - // Clone the options object for each chunk upload: - var o = $.extend({}, options); - o.blob = slice.call( - file, - ub + i * mcs, - ub + (i + 1) * mcs - ); - // Expose the chunk index: - o.chunkIndex = i; - // Expose the number of chunks: - o.chunksNumber = n; - // Store the current chunk size, as the blob itself - // will be dereferenced after data processing: - o.chunkSize = o.blob.size; - // Process the upload data (the blob and potential form data): - that._initXHRData(o); - // Add progress listeners for this chunk upload: - that._initProgressListener(o); - jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)) - .done(function () { - // Create a progress event if upload is done and - // no progress event has been invoked for this chunk: - if (!o.loaded) { - that._onProgress($.Event('progress', { - lengthComputable: true, - loaded: o.chunkSize, - total: o.chunkSize - }), o); - } - options.uploadedBytes = o.uploadedBytes += - o.chunkSize; - }); - return jqXHR; - }); + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); }; - // Return the piped Promise object, enhanced with an abort method, - // which is delegated to the jqXHR object of the current upload, - // and jqXHR callbacks mapped to the equivalent Promise methods: - pipe = upload(n); - pipe.abort = function () { + this._enhancePromise(promise); + promise.abort = function () { return jqXHR.abort(); }; - return this._enhancePromise(pipe); + upload(); + return promise; }, _beforeSend: function (e, data) { @@ -579,108 +826,115 @@ this._trigger('start'); // Set timer for global bitrate progress calculation: this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; this._active += 1; // Initialize the global progress values: - this._loaded += data.uploadedBytes || 0; - this._total += this._getTotal(data.files); + this._progress.loaded += data.loaded; + this._progress.total += data.total; }, _onDone: function (result, textStatus, jqXHR, options) { - if (!this._isXHRUpload(options)) { - // Create a progress event for each iframe load: + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: this._onProgress($.Event('progress', { lengthComputable: true, - loaded: 1, - total: 1 + loaded: total, + total: total }), options); } - options.result = result; - options.textStatus = textStatus; - options.jqXHR = jqXHR; + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; this._trigger('done', null, options); }, _onFail: function (jqXHR, textStatus, errorThrown, options) { - options.jqXHR = jqXHR; - options.textStatus = textStatus; - options.errorThrown = errorThrown; - this._trigger('fail', null, options); + var response = options._response; if (options.recalculateProgress) { // Remove the failed (error or abort) file upload from // the global progress calculation: - this._loaded -= options.loaded || options.uploadedBytes || 0; - this._total -= options.total || this._getTotal(options.files); + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); }, _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { - this._active -= 1; - options.textStatus = textStatus; - if (jqXHRorError && jqXHRorError.always) { - options.jqXHR = jqXHRorError; - options.result = jqXHRorResult; - } else { - options.jqXHR = jqXHRorResult; - options.errorThrown = jqXHRorError; - } + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks this._trigger('always', null, options); - if (this._active === 0) { - // The stop callback is triggered when all uploads have - // been completed, equivalent to the global ajaxStop event: - this._trigger('stop'); - // Reset the global progress values: - this._loaded = this._total = 0; - this._bitrateTimer = null; - } }, _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } var that = this, jqXHR, + aborted, slot, pipe, options = that._getAJAXSettings(data), - send = function (resolve, args) { + send = function () { that._sending += 1; // Set timer for bitrate progress calculation: options._bitrateTimer = new that._BitrateTimer(); jqXHR = jqXHR || ( - (resolve !== false && - that._trigger('send', e, options) !== false && - (that._chunkedUpload(options) || $.ajax(options))) || - that._getXHRPromise(false, options.context, args) + ((aborted || that._trigger( + 'send', + $.Event('send', {delegatedEvent: e}), + options + ) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) ).done(function (result, textStatus, jqXHR) { that._onDone(result, textStatus, jqXHR, options); }).fail(function (jqXHR, textStatus, errorThrown) { that._onFail(jqXHR, textStatus, errorThrown, options); }).always(function (jqXHRorResult, textStatus, jqXHRorError) { - that._sending -= 1; that._onAlways( jqXHRorResult, textStatus, jqXHRorError, options ); + that._sending -= 1; + that._active -= 1; if (options.limitConcurrentUploads && options.limitConcurrentUploads > that._sending) { // Start the next queued upload, // that has not been aborted: - var nextSlot = that._slots.shift(), - isPending; + var nextSlot = that._slots.shift(); while (nextSlot) { - // jQuery 1.6 doesn't provide .state(), - // while jQuery 1.8+ removed .isRejected(): - isPending = nextSlot.state ? - nextSlot.state() === 'pending' : - !nextSlot.isRejected(); - if (isPending) { + if (that._getDeferredState(nextSlot) === 'pending') { nextSlot.resolve(); break; } nextSlot = that._slots.shift(); } } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } }); return jqXHR; }; @@ -691,20 +945,21 @@ if (this.options.limitConcurrentUploads > 1) { slot = $.Deferred(); this._slots.push(slot); - pipe = slot.pipe(send); + pipe = slot.then(send); } else { - pipe = (this._sequence = this._sequence.pipe(send, send)); + this._sequence = this._sequence.then(send, send); + pipe = this._sequence; } // Return the piped Promise object, enhanced with an abort method, // which is delegated to the jqXHR object of the current upload, // and jqXHR callbacks mapped to the equivalent Promise methods: pipe.abort = function () { - var args = [undefined, 'abort', 'abort']; + aborted = [undefined, 'abort', 'abort']; if (!jqXHR) { if (slot) { - slot.rejectWith(pipe, args); + slot.rejectWith(options.context, aborted); } - return send(false, args); + return send(); } return jqXHR.abort(); }; @@ -717,56 +972,97 @@ var that = this, result = true, options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, paramName = this._getParamName(options), paramNameSet, paramNameSlice, fileSet, - i; - if (!(options.singleFileUploads || limit) || + i, + j = 0; + if (!filesLength) { + return false; + } + if (limitSize && files[0].size === undefined) { + limitSize = undefined; + } + if (!(options.singleFileUploads || limit || limitSize) || !this._isXHRUpload(options)) { - fileSet = [data.files]; + fileSet = [files]; paramNameSet = [paramName]; - } else if (!options.singleFileUploads && limit) { + } else if (!(options.singleFileUploads || limitSize) && limit) { fileSet = []; paramNameSet = []; - for (i = 0; i < data.files.length; i += limit) { - fileSet.push(data.files.slice(i, i + limit)); + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); paramNameSlice = paramName.slice(i, i + limit); if (!paramNameSlice.length) { paramNameSlice = paramName; } paramNameSet.push(paramNameSlice); } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if (i + 1 === filesLength || + ((batchSize + files[i + 1].size + overhead) > limitSize) || + (limit && i + 1 - j >= limit)) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } } else { paramNameSet = paramName; } - data.originalFiles = data.files; - $.each(fileSet || data.files, function (index, element) { + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { var newData = $.extend({}, data); newData.files = fileSet ? element : [element]; newData.paramName = paramNameSet[index]; - newData.submit = function () { - newData.jqXHR = this.jqXHR = - (that._trigger('submit', e, this) !== false) && - that._onSend(e, this); - return this.jqXHR; - }; - return (result = that._trigger('add', e, newData)); + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger( + 'add', + $.Event('add', {delegatedEvent: e}), + newData + ); + return result; }); return result; }, - _replaceFileInput: function (input) { - var inputClone = input.clone(true); + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true), + restoreFocus = input.is(document.activeElement); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; $('
').append(inputClone)[0].reset(); // Detaching allows to insert the fileInput on another form // without loosing the file input value: input.after(inputClone).detach(); + // If the fileInput had focus before it was detached, + // restore focus to the inputClone. + if (restoreFocus) { + inputClone.focus(); + } // Avoid memory leaks with the detached file input: $.cleanData(input.unbind('remove')); // Replace the original file input element in the fileInput - // collection with the clone, which has been copied including + // elements set with the clone, which has been copied including // event handlers: this.options.fileInput = this.options.fileInput.map(function (i, el) { if (el === input[0]) { @@ -784,28 +1080,54 @@ _handleFileTreeEntry: function (entry, path) { var that = this, dfd = $.Deferred(), - errorHandler = function () { - dfd.reject(); + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); }, - dirReader; - path = path || ''; - if (entry.isFile) { - entry.file(function (file) { - file.relativePath = path; - dfd.resolve(file); - }, errorHandler); - } else if (entry.isDirectory) { - dirReader = entry.createReader(); - dirReader.readEntries(function (entries) { + successHandler = function (entries) { that._handleFileTreeEntries( entries, path + entry.name + '/' ).done(function (files) { dfd.resolve(files); }).fail(errorHandler); - }, errorHandler); + }, + readEntries = function () { + dirReader.readEntries(function (results) { + if (!results.length) { + successHandler(entries); + } else { + entries = entries.concat(results); + readEntries(); + } + }, errorHandler); + }, + dirReader, entries = []; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + readEntries(); } else { - errorHandler(); + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); } return dfd.promise(); }, @@ -817,7 +1139,7 @@ $.map(entries, function (entry) { return that._handleFileTreeEntry(entry, path); }) - ).pipe(function () { + ).then(function () { return Array.prototype.concat.apply( [], arguments @@ -832,8 +1154,14 @@ items[0].getAsEntry)) { return this._handleFileTreeEntries( $.map(items, function (item) { + var entry; if (item.webkitGetAsEntry) { - return item.webkitGetAsEntry(); + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; } return item.getAsEntry(); }) @@ -844,7 +1172,7 @@ ).promise(); }, - _getFileInputFiles: function (fileInput) { + _getSingleFileInputFiles: function (fileInput) { fileInput = $(fileInput); var entries = fileInput.prop('webkitEntries') || fileInput.prop('entries'), @@ -857,104 +1185,139 @@ if (!files.length) { value = fileInput.prop('value'); if (!value) { - return $.Deferred().reject([]).promise(); + return $.Deferred().resolve([]).promise(); } // If the files property is not available, the browser does not // support the File API and we add a pseudo File object with // the input value as name with path information removed: files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); } return $.Deferred().resolve(files).promise(); }, + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).then(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + _onChange: function (e) { - var that = e.data.fileupload, + var that = this, data = { fileInput: $(e.target), form: $(e.target.form) }; - that._getFileInputFiles(data.fileInput).always(function (files) { + this._getFileInputFiles(data.fileInput).always(function (files) { data.files = files; if (that.options.replaceFileInput) { - that._replaceFileInput(data.fileInput); + that._replaceFileInput(data); } - if (that._trigger('change', e, data) !== false) { + if (that._trigger( + 'change', + $.Event('change', {delegatedEvent: e}), + data + ) !== false) { that._onAdd(e, data); } }); }, _onPaste: function (e) { - var that = e.data.fileupload, - cbd = e.originalEvent.clipboardData, - items = (cbd && cbd.items) || [], + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, data = {files: []}; - $.each(items, function (index, item) { - var file = item.getAsFile && item.getAsFile(); - if (file) { - data.files.push(file); + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger( + 'paste', + $.Event('paste', {delegatedEvent: e}), + data + ) !== false) { + this._onAdd(e, data); } - }); - if (that._trigger('paste', e, data) === false || - that._onAdd(e, data) === false) { - return false; } }, _onDrop: function (e) { - e.preventDefault(); - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, data = {}; - that._getDroppedFiles(dataTransfer).always(function (files) { - data.files = files; - if (that._trigger('drop', e, data) !== false) { - that._onAdd(e, data); - } - }); - }, - - _onDragOver: function (e) { - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; - if (that._trigger('dragover', e) === false) { - return false; - } - if (dataTransfer) { - dataTransfer.dropEffect = 'copy'; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger( + 'drop', + $.Event('drop', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); } - e.preventDefault(); }, + _onDragOver: getDragHandler('dragover'), + + _onDragEnter: getDragHandler('dragenter'), + + _onDragLeave: getDragHandler('dragleave'), + _initEventHandlers: function () { - var ns = this.options.namespace; if (this._isXHRUpload(this.options)) { - this.options.dropZone - .bind('dragover.' + ns, {fileupload: this}, this._onDragOver) - .bind('drop.' + ns, {fileupload: this}, this._onDrop) - .bind('paste.' + ns, {fileupload: this}, this._onPaste); + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop, + // event.preventDefault() on dragenter is required for IE10+: + dragenter: this._onDragEnter, + // dragleave is not required, but added for completeness: + dragleave: this._onDragLeave + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); } - this.options.fileInput - .bind('change.' + ns, {fileupload: this}, this._onChange); }, _destroyEventHandlers: function () { - var ns = this.options.namespace; - this.options.dropZone - .unbind('dragover.' + ns, this._onDragOver) - .unbind('drop.' + ns, this._onDrop) - .unbind('paste.' + ns, this._onPaste); - this.options.fileInput - .unbind('change.' + ns, this._onChange); + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); }, _setOption: function (key, value) { - var refresh = $.inArray(key, this._refreshOptionsList) !== -1; - if (refresh) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { this._destroyEventHandlers(); } - $.Widget.prototype._setOption.call(this, key, value); - if (refresh) { + this._super(key, value); + if (reinit) { this._initSpecialOptions(); this._initEventHandlers(); } @@ -971,33 +1334,70 @@ if (!(options.dropZone instanceof $)) { options.dropZone = $(options.dropZone); } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } }, - _create: function () { - var options = this.options; + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options, + data = this.element.data(); // Initialize options set via HTML5 data-attributes: - $.extend(options, $(this.element[0].cloneNode(false)).data()); - options.namespace = options.namespace || this.widgetName; + $.each( + this.element[0].attributes, + function (index, attr) { + var key = attr.name.toLowerCase(), + value; + if (/^data-/.test(key)) { + // Convert hyphen-ated key to camelCase: + key = key.slice(5).replace(/-[a-z]/g, function (str) { + return str.charAt(1).toUpperCase(); + }); + value = data[key]; + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + } + ); + }, + + _create: function () { + this._initDataAttributes(); this._initSpecialOptions(); this._slots = []; this._sequence = this._getXHRPromise(true); - this._sending = this._active = this._loaded = this._total = 0; + this._sending = this._active = 0; + this._initProgressObject(this); this._initEventHandlers(); }, - destroy: function () { - this._destroyEventHandlers(); - $.Widget.prototype.destroy.call(this); - }, - - enable: function () { - $.Widget.prototype.enable.call(this); - this._initEventHandlers(); + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; }, - disable: function () { - this._destroyEventHandlers(); - $.Widget.prototype.disable.call(this); + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; }, // This method is exposed to the widget API and allows adding files @@ -1046,8 +1446,13 @@ if (aborted) { return; } + if (!files.length) { + dfd.reject(); + return; + } data.files = files; - jqXHR = that._onSend(null, data).then( + jqXHR = that._onSend(null, data); + jqXHR.then( function (result, textStatus, jqXHR) { dfd.resolve(result, textStatus, jqXHR); }, diff --git a/vendor/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js b/app/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js similarity index 65% rename from vendor/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js rename to app/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js index 4749f46..b7922e6 100644 --- a/vendor/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js +++ b/app/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js @@ -1,5 +1,5 @@ /* - * jQuery Iframe Transport Plugin 1.5 + * jQuery Iframe Transport Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan @@ -9,14 +9,16 @@ * http://www.opensource.org/licenses/MIT */ -/*jslint unparam: true, nomen: true */ -/*global define, window, document */ +/* global define, require, window, document */ -(function (factory) { +;(function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); } else { // Browser globals: factory(window.jQuery); @@ -27,7 +29,7 @@ // Helper variable to create unique names for the transport iframes: var counter = 0; - // The iframe transport accepts three additional options: + // The iframe transport accepts four additional options: // options.fileInput: a jQuery collection of file input fields // options.paramName: the parameter name for the file form data, // overrides the name property of the file input field(s), @@ -35,22 +37,41 @@ // options.formData: an array of objects with name and value properties, // equivalent to the return data of .serializeArray(), e.g.: // [{name: 'a', value: 1}, {name: 'b', value: 2}] + // options.initialIframeSrc: the URL of the initial iframe src, + // by default set to "javascript:false;" $.ajaxTransport('iframe', function (options) { - if (options.async && (options.type === 'POST' || options.type === 'GET')) { - var form, - iframe; + if (options.async) { + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6: + /*jshint scripturl: true */ + var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', + /*jshint scripturl: false */ + form, + iframe, + addParamChar; return { send: function (_, completeCallback) { form = $(''); form.attr('accept-charset', options.formAcceptCharset); - // javascript:false as initial iframe src - // prevents warning popups on HTTPS in IE6. + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } // IE versions below IE8 cannot set the name property of // elements that have already been added to the DOM, // so we set the name along with the iframe HTML markup: + counter += 1; iframe = $( - '' + '' ).bind('load', function () { var fileInputClones, paramNames = $.isArray(options.paramName) ? @@ -81,9 +102,14 @@ ); // Fix for IE endless progress bar activity bug // (happens on form submits to iframe targets): - $('') + $('') .appendTo(form); - form.remove(); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); }); form .prop('target', iframe.prop('name')) @@ -119,6 +145,8 @@ .prop('enctype', 'multipart/form-data') // enctype must be set as encoding for IE: .prop('encoding', 'multipart/form-data'); + // Remove the HTML5 form attribute from the input(s): + options.fileInput.removeAttr('form'); } form.submit(); // Insert the file input fields at their original location @@ -126,7 +154,10 @@ if (fileInputClones && fileInputClones.length) { options.fileInput.each(function (index, input) { var clone = $(fileInputClones[index]); - $(input).prop('name', clone.prop('name')); + // Restore the original name and form properties: + $(input) + .prop('name', clone.prop('name')) + .attr('form', clone.attr('form')); clone.replaceWith(input); }); } @@ -140,7 +171,7 @@ // concat is used to avoid the "Script URL" JSLint error: iframe .unbind('load') - .prop('src', 'javascript'.concat(':false;')); + .prop('src', initialIframeSrc); } if (form) { form.remove(); @@ -151,20 +182,34 @@ }); // The iframe transport returns the iframe content document as response. - // The following adds converters from iframe to text, json, html, and script: + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation $.ajaxSetup({ converters: { 'iframe text': function (iframe) { - return $(iframe[0].body).text(); + return iframe && $(iframe[0].body).text(); }, 'iframe json': function (iframe) { - return $.parseJSON($(iframe[0].body).text()); + return iframe && $.parseJSON($(iframe[0].body).text()); }, 'iframe html': function (iframe) { - return $(iframe[0].body).html(); + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); }, 'iframe script': function (iframe) { - return $.globalEval($(iframe[0].body).text()); + return iframe && $.globalEval($(iframe[0].body).text()); } } }); diff --git a/vendor/assets/javascripts/jquery-fileupload/locale.js b/app/assets/javascripts/jquery-fileupload/locale.js similarity index 100% rename from vendor/assets/javascripts/jquery-fileupload/locale.js rename to app/assets/javascripts/jquery-fileupload/locale.js diff --git a/vendor/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js b/app/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js similarity index 86% rename from vendor/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js rename to app/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js index 72edfdb..d35ce0a 100644 --- a/vendor/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js +++ b/app/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js @@ -1,5 +1,5 @@ /* - * JavaScript Canvas to Blob 2.0.3 + * JavaScript Canvas to Blob 2.0.5 * https://github.com/blueimp/JavaScript-Canvas-to-Blob * * Copyright 2012, Sebastian Tschan @@ -72,12 +72,16 @@ }; if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) { if (CanvasPrototype.mozGetAsFile) { - CanvasPrototype.toBlob = function (callback, type) { - callback(this.mozGetAsFile('blob', type)); + CanvasPrototype.toBlob = function (callback, type, quality) { + if (quality && CanvasPrototype.toDataURL && dataURLtoBlob) { + callback(dataURLtoBlob(this.toDataURL(type, quality))); + } else { + callback(this.mozGetAsFile('blob', type)); + } }; } else if (CanvasPrototype.toDataURL && dataURLtoBlob) { - CanvasPrototype.toBlob = function (callback, type) { - callback(dataURLtoBlob(this.toDataURL(type))); + CanvasPrototype.toBlob = function (callback, type, quality) { + callback(dataURLtoBlob(this.toDataURL(type, quality))); }; } } @@ -88,4 +92,4 @@ } else { window.dataURLtoBlob = dataURLtoBlob; } -}(this)); +}(window)); diff --git a/app/assets/javascripts/jquery-fileupload/vendor/jquery.ui.widget.js b/app/assets/javascripts/jquery-fileupload/vendor/jquery.ui.widget.js new file mode 100644 index 0000000..e08df3f --- /dev/null +++ b/app/assets/javascripts/jquery-fileupload/vendor/jquery.ui.widget.js @@ -0,0 +1,572 @@ +/*! jQuery UI - v1.11.4+CommonJS - 2015-08-28 +* http://jqueryui.com +* Includes: widget.js +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + + } else if ( typeof exports === "object" ) { + + // Node/CommonJS + factory( require( "jquery" ) ); + + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { +/*! + * jQuery UI Widget 1.11.4 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + + +var widget_uuid = 0, + widget_slice = Array.prototype.slice; + +$.cleanData = (function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; (elem = elems[i]) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; +})( $.cleanData ); + +$.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; +}; + +$.widget.extend = function( target ) { + var input = widget_slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = widget_slice.call( arguments, 1 ), + returnValue = this; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( options === "instance" ) { + returnValue = instance; + return false; + } + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat(args) ); + } + + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "