diff --git a/README.md b/README.md index 8ccc9a3..4207f35 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -# 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. -jquery-fileupload-rails is a library that integrates jQuery File Upload for Rails 3.1 Asset Pipeline. +jquery-fileupload-rails is a library that integrates jQuery File Upload for Rails 3.1 Asset Pipeline (Rails 3.2 supported). -jquery-fileupload-rails is currently using jQuery-File-Plugin version 6.7.0. +## Plugin versions + +* jQuery File Upload User Interface Plugin 6.11 +* jQuery File Upload Plugin 5.19.3 +* jQuery UI Widget 1.9.1+amd ## Installing Gem @@ -18,25 +22,25 @@ Require jquery-fileupload in your app/assets/application.js file. 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 + //= 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 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 ## Using the stylesheet @@ -44,6 +48,19 @@ Require the stylesheet file to app/assets/stylesheets/application.css *= require jquery.fileupload-ui +## 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. + + Rails.application.config.middleware.use JQuery::FileUpload::Rails::Middleware + +## [Example app](https://github.com/tors/jquery-fileupload-rails-paperclip-example) +This app uses paperclip and twitter-bootstrap-rails + +You can also check out Ryan Bate's RailsCast [jQuery File Upload episode](http://railscasts.com/episodes/381-jquery-file-upload). You will +need a pro account to watch it though. + + ## Thanks Thanks to [Sebastian Tschan](https://github.com/blueimp) for writing an awesome file upload plugin. diff --git a/lib/jquery/fileupload/rails/middleware.rb b/lib/jquery/fileupload/rails/middleware.rb new file mode 100644 index 0000000..334ea67 --- /dev/null +++ b/lib/jquery/fileupload/rails/middleware.rb @@ -0,0 +1,59 @@ +module JQuery + module FileUpload + module Rails + class Middleware + def initialize(app) + @app = app + end + + def call(env) + dup._call(env) + end + + def _call(env) + @status, @headers, @response = @app.call(env) + @request = Rack::Request.new(env) + + if iframe_transport? + @headers['Content-Type'] = 'text/html' + [@status, @headers, self] + else + [@status, @headers, @response] + end + end + + def each(&block) + block.call(html_document_left) if iframe_transport? + @response.each(&block) + block.call(html_document_right) if iframe_transport? + end + + def iframe_transport? + @request.params['X-Requested-With'] == 'IFrame' + end + + def html_document_left + "
" + end + + def metadata + meta = {} + meta['data-status'] = @response.status if @response.respond_to? :status + meta['data-statusText'] = @response.status_message if @response.respond_to? :status_message + meta['data-type'] = @headers['Content-Type'] if @headers.has_key?('Content-Type') + meta.map {|key,value| "#{key}='#{value}'" }.join(' ') + end + + private + + def method_missing(method, *args) + @response.send(method.intern, *args) + end + end + end + end +end diff --git a/lib/jquery/fileupload/rails/upload.rb b/lib/jquery/fileupload/rails/upload.rb index bd8adcb..f02d383 100644 --- a/lib/jquery/fileupload/rails/upload.rb +++ b/lib/jquery/fileupload/rails/upload.rb @@ -1,2 +1,3 @@ require "jquery/fileupload/rails/engine" require "jquery/fileupload/rails/version" +require "jquery/fileupload/rails/middleware" diff --git a/lib/jquery/fileupload/rails/version.rb b/lib/jquery/fileupload/rails/version.rb index f057966..10fbdc9 100644 --- a/lib/jquery/fileupload/rails/version.rb +++ b/lib/jquery/fileupload/rails/version.rb @@ -1,7 +1,7 @@ module JQuery module FileUpload module Rails - VERSION = "0.3.0" + VERSION = "0.4.1" end end end diff --git a/vendor/assets/images/loading.gif b/vendor/assets/images/loading.gif old mode 100644 new mode 100755 diff --git a/vendor/assets/images/progressbar.gif b/vendor/assets/images/progressbar.gif old mode 100644 new mode 100755 diff --git a/vendor/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js b/vendor/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js old mode 100644 new mode 100755 diff --git a/vendor/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js b/vendor/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js old mode 100644 new mode 100755 index c42c548..d769f45 --- a/vendor/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js +++ b/vendor/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js @@ -1,5 +1,5 @@ /* - * jQuery XDomainRequest Transport Plugin 1.1.2 + * jQuery XDomainRequest Transport Plugin 1.1.3 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan @@ -36,6 +36,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 +45,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/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-angular.js b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-angular.js new file mode 100755 index 0000000..e7ba784 --- /dev/null +++ b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-angular.js @@ -0,0 +1,348 @@ +/* + * jQuery File Upload AngularJS Plugin 1.0.1 + * 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 + */ + +/*jslint nomen: true, unparam: true */ +/*global angular */ + +(function () { + 'use strict'; + + angular.module('blueimp.fileupload', []) + + .provider('fileUpload', function () { + var scopeApply = function () { + var scope = angular.element(this) + .fileupload('option', 'scope')(); + if (!scope.$$phase) { + scope.$apply(); + } + }, + $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) { + var scope = data.scope(); + data.process(function () { + return scope.process(data); + }).always( + function () { + var file = data.files[0], + submit = function () { + return data.submit(); + }; + file.$cancel = function () { + scope.clear(data.files); + return data.abort(); + }; + file.$state = function () { + return data.state(); + }; + file.$progress = function () { + return data.progress(); + }; + file.$response = function () { + return data.response(); + }; + if (file.$state() === 'rejected') { + file._$submit = submit; + } else { + file.$submit = submit; + } + scope.$apply(function () { + var method = scope.option('prependFiles') ? + 'unshift' : 'push'; + Array.prototype[method].apply( + scope.queue, + data.files + ); + if (file.$submit && + (scope.option('autoUpload') || + data.autoUpload) && + data.autoUpload !== false) { + file.$submit(); + } + }); + } + ); + }, + progress: function (e, data) { + data.scope().$apply(); + }, + done: function (e, data) { + var that = this; + data.scope().$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + fail: function (e, data) { + var that = this; + if (data.errorThrown === 'abort') { + return; + } + if (data.dataType.indexOf('json') === data.dataType.length - 4) { + try { + data.result = angular.fromJson(data.jqXHR.responseText); + } catch (err) {} + } + data.scope().$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + stop: scopeApply, + processstart: scopeApply, + processstop: scopeApply, + getNumberOfFiles: function () { + return this.scope().queue.length; + }, + dataType: 'json', + prependFiles: true, + autoUpload: false + }; + this.$get = [ + function () { + return { + defaults: $config + }; + } + ]; + }) + + .provider('formatFileSizeFilter', function () { + var $config = this.defaults = { + // 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.$get = function () { + return function (bytes) { + if (!angular.isNumber(bytes)) { + return ''; + } + var unit = true, + i = -1; + while (unit) { + unit = $config.units[i += 1]; + if (i === $config.units.length - 1 || bytes >= unit.size) { + return (bytes / unit.size).toFixed(2) + unit.suffix; + } + } + }; + }; + }) + + .controller('FileUploadController', [ + '$scope', '$element', '$attrs', 'fileUpload', + function ($scope, $element, $attrs, fileUpload) { + $scope.disabled = angular.element('') + .prop('disabled'); + $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) { + if (queue[i -= 1] === 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.progress = function () { + return $element.fileupload('progress'); + }; + $scope.active = function () { + return $element.fileupload('active'); + }; + $scope.option = function (option, data) { + return $element.fileupload('option', option, data); + }; + $scope.add = function (data) { + return $element.fileupload('add', data); + }; + $scope.send = function (data) { + return $element.fileupload('send', data); + }; + $scope.process = function (data) { + return $element.fileupload('process', data); + }; + $scope.processing = function (data) { + return $element.fileupload('processing', data); + }; + $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'); + }; + // 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: function () { + return $scope; + }}, + fileUpload.defaults + )).on('fileuploadadd', function (e, data) { + data.scope = $scope.option('scope'); + }).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.$emit(e.type, data); + }); + // Observe option changes: + $scope.$watch( + $attrs.fileupload, + function (newOptions, oldOptions) { + if (newOptions) { + $element.fileupload('option', newOptions); + } + } + ); + } + ]) + + .controller('FileUploadProgressController', [ + '$scope', '$attrs', '$parse', + function ($scope, $attrs, $parse) { + var fn = $parse($attrs.progress), + update = function () { + var progress = fn($scope); + if (!progress || !progress.total) { + return; + } + $scope.num = Math.floor( + progress.loaded / progress.total * 100 + ); + }; + update(); + $scope.$watch( + $attrs.progress + '.loaded', + function (newValue, oldValue) { + if (newValue !== oldValue) { + update(); + } + } + ); + } + ]) + + .controller('FileUploadPreviewController', [ + '$scope', '$element', '$attrs', '$parse', + function ($scope, $element, $attrs, $parse) { + var fn = $parse($attrs.preview), + file = fn($scope); + if (file.preview) { + $element.append(file.preview); + } + } + ]) + + .directive('fileupload', function () { + return { + controller: 'FileUploadController' + }; + }) + + .directive('progress', function () { + return { + controller: 'FileUploadProgressController' + }; + }) + + .directive('preview', function () { + return { + controller: 'FileUploadPreviewController' + }; + }) + + .directive('download', function () { + return function (scope, elm, attrs) { + elm.on('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [ + 'application/octet-stream', + elm.prop('download'), + elm.prop('href') + ].join(':') + ); + } catch (err) {} + }); + }; + }); + +}()); diff --git a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-fp.js b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-fp.js index 634fb5e..c782f1e 100644 --- a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-fp.js +++ b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-fp.js @@ -1,5 +1,5 @@ /* - * jQuery File Upload File Processing Plugin 1.0 + * jQuery File Upload File Processing Plugin 1.2.3 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2012, Sebastian Tschan @@ -32,9 +32,9 @@ }(function ($, loadImage) { 'use strict'; - // The File Upload IP version extends the basic fileupload widget + // The File Upload FP version extends the fileupload widget // with file processing functionality: - $.widget('blueimpFP.fileupload', $.blueimp.fileupload, { + $.widget('blueimp.fileupload', $.blueimp.fileupload, { options: { // The list of file processing actions: @@ -62,15 +62,19 @@ // 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) { - $(this).fileupload('process', data).done(function () { - data.submit(); - }); + if (data.autoUpload || (data.autoUpload !== false && + ($(this).data('blueimp-fileupload') || + $(this).data('fileupload')).options.autoUpload)) { + $(this).fileupload('process', data).done(function () { + data.submit(); + }); + } } }, processActions: { // Loads the image given via data.files and data.index - // as canvas element. + // as img element if the browser supports canvas. // Accepts the options fileTypes (regular expression) // and maxFileSize (integer) to limit the files to load: load: function (data, options) { @@ -85,28 +89,32 @@ options.fileTypes.test(file.type))) { loadImage( file, - function (canvas) { - data.canvas = canvas; + function (img) { + if (!img.src) { + return dfd.rejectWith(that, [data]); + } + data.img = img; dfd.resolveWith(that, [data]); - }, - {canvas: true} + } ); } else { dfd.rejectWith(that, [data]); } return dfd.promise(); }, - // Resizes the image given as data.canvas and updates - // data.canvas with the resized image. + // Resizes the image given as data.img and updates + // data.canvas with the resized image as canvas element. // Accepts the options maxWidth, maxHeight, minWidth and // minHeight to scale the given image: resize: function (data, options) { - if (data.canvas) { - var canvas = loadImage.scale(data.canvas, options); - if (canvas.width !== data.canvas.width || - canvas.height !== data.canvas.height) { + var img = data.img, + canvas; + options = $.extend({canvas: true}, options); + if (img) { + canvas = loadImage.scale(img, options); + if (canvas.width !== img.width || + canvas.height !== img.height) { data.canvas = canvas; - data.processed = true; } } return data; @@ -115,7 +123,7 @@ // inplace at data.index of data.files: save: function (data, options) { // Do nothing if no processing has happened: - if (!data.canvas || !data.processed) { + if (!data.canvas) { return data; } var that = this, @@ -208,7 +216,7 @@ }, _create: function () { - $.blueimp.fileupload.prototype._create.call(this); + this._super(); this._processing = 0; this._processingQueue = $.Deferred().resolveWith(this) .promise(); diff --git a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-process.js b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-process.js new file mode 100755 index 0000000..2f9eeed --- /dev/null +++ b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-process.js @@ -0,0 +1,158 @@ +/* + * jQuery File Upload Processing Plugin 1.1 + * 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 + */ + +/*jslint nomen: true, unparam: true */ +/*global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload' + ], factory); + } 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) { + 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) { + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain.pipe(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 = {}; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && + value.charAt(0) === '@') { + settings[key] = options[value.slice(1)]; + } 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, file) { + var opts = index ? $.extend({}, options) : options, + func = function () { + return that._processFile(opts); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue.pipe(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-resize.js b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-resize.js new file mode 100755 index 0000000..ae5c5be --- /dev/null +++ b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-resize.js @@ -0,0 +1,212 @@ +/* + * jQuery File Upload Image Resize Plugin 1.1.2 + * 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 + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + 'canvas-to-blob', + './jquery.fileupload-process' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadImage', + fileTypes: '@loadImageFileTypes', + maxFileSize: '@loadImageMaxFileSize', + noRevoke: '@loadImageNoRevoke', + disabled: '@disableImageLoad' + }, + { + action: 'resizeImage', + maxWidth: '@imageMaxWidth', + maxHeight: '@imageMaxHeight', + minWidth: '@imageMinWidth', + minHeight: '@imageMinHeight', + crop: '@imageCrop', + disabled: '@disableImageResize' + }, + { + action: 'saveImage', + disabled: '@disableImageResize' + }, + { + action: 'resizeImage', + maxWidth: '@previewMaxWidth', + maxHeight: '@previewMaxHeight', + minWidth: '@previewMinWidth', + minHeight: '@previewMinHeight', + crop: '@previewCrop', + canvas: '@previewAsCanvas', + disabled: '@disableImagePreview' + }, + { + action: 'setImage', + // The name of the property the resized image + // is saved as on the associated file object: + name: 'preview', + disabled: '@disableImagePreview' + } + ); + + // 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)$/, + // The maximum file size of images to load: + loadImageMaxFileSize: 5000000, // 5MB + // The maximum width of resized images: + imageMaxWidth: 1920, + // The maximum height of resized images: + imageMaxHeight: 1080, + // 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, + // Define if preview images should be cropped or only scaled: + previewCrop: false, + // Define if preview images should be resized as canvas elements: + previewAsCanvas: true + }, + + processActions: { + + // Loads the image given via data.files and data.index + // as img element if the browser supports canvas. + // 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) { + return dfd.rejectWith(that, [data]); + } + data.img = img; + dfd.resolveWith(that, [data]); + }, + options + )) { + dfd.rejectWith(that, [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. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + options = $.extend({canvas: true}, options); + var img = (options.canvas && data.canvas) || data.img, + canvas; + if (img && !options.disabled) { + canvas = loadImage.scale(img, options); + if (canvas && (canvas.width !== img.width || + canvas.height !== img.height)) { + data[canvas.getContext ? 'canvas' : 'img'] = canvas; + } + } + 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], + name = file.name, + dfd = $.Deferred(), + callback = function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\..+$/, + '.' + blob.type.substr(6) + ); + } + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }; + // Use canvas.mozGetAsFile directly, to retain the filename, as + // Gecko doesn't support the filename option for FormData.append: + if (data.canvas.mozGetAsFile) { + callback(data.canvas.mozGetAsFile( + (/^image\/(jpeg|png)$/.test(file.type) && name) || + ((name && name.replace(/\..+$/, '')) || + 'blob') + '.png', + file.type + )); + } else if (data.canvas.toBlob) { + data.canvas.toBlob(callback, file.type); + } else { + return data; + } + return dfd.promise(); + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + var img = data.canvas || data.img; + if (img && !options.disabled) { + data.files[data.index][options.name] = img; + } + return data; + } + + } + + }); + +})); diff --git a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js old mode 100644 new mode 100755 diff --git a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-validate.js b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-validate.js new file mode 100755 index 0000000..2599da8 --- /dev/null +++ b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-validate.js @@ -0,0 +1,116 @@ +/* + * jQuery File Upload Validation Plugin 1.0.2 + * 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 + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-process' + ], factory); + } 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: '@acceptFileTypes', + maxFileSize: '@maxFileSize', + minFileSize: '@minFileSize', + maxNumberOfFiles: '@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], + numberOfFiles = settings.getNumberOfFiles(); + if (numberOfFiles && $.type(options.maxNumberOfFiles) === 'number' && + numberOfFiles + 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 (options.maxFileSize && file.size > options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ($.type(file.size) === 'number' && + file.size < 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/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload.js b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload.js old mode 100644 new mode 100755 index 40661c1..03678f3 --- a/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload.js +++ b/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload.js @@ -1,5 +1,5 @@ /* - * jQuery File Upload Plugin 5.11.2 + * jQuery File Upload Plugin 5.31.1 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan @@ -10,7 +10,7 @@ */ /*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document, Blob, FormData, location */ +/*global define, window, document, File, Blob, FormData, location */ (function (factory) { 'use strict'; @@ -44,17 +44,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 the complete document. + // Set to null to disable paste support: + pasteZone: $(document), + // 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 @@ -113,6 +112,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 @@ -137,37 +155,67 @@ // handlers using jQuery's Deferred callbacks: // data.submit().done(func).fail(func).always(func); add: function (e, data) { - data.submit(); + 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, @@ -175,17 +223,18 @@ cache: false }, - // 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' ], _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) { @@ -213,7 +262,7 @@ 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 +280,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 +316,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 @@ -262,16 +339,7 @@ this._trigger('progress', 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', e, this._progress); } }, @@ -295,35 +363,28 @@ } }, + _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; - } + options.headers = options.headers || {}; + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; } - if (multipart && $.support.xhrFormDataFileUpload) { + if (!multipart) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; + options.contentType = file.type; + 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 @@ -344,7 +405,7 @@ }); } } else { - if (options.formData instanceof FormData) { + if (that._isInstanceOf('FormData', options.formData)) { formData = options.formData; } else { formData = new FormData(); @@ -353,13 +414,15 @@ }); } if (options.blob) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; 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, file, @@ -404,7 +467,7 @@ options.dataType = 'postmessage ' + (options.dataType || ''); } } else { - this._initIframeSettings(options, 'iframe'); + this._initIframeSettings(options); } }, @@ -436,6 +499,11 @@ // 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) { @@ -444,9 +512,13 @@ // 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') { + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { options.type = 'POST'; } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } }, _getAJAXSettings: function (data) { @@ -456,6 +528,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) { @@ -480,6 +567,60 @@ return this._enhancePromise(promise); }, + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (data) { + return $.Deferred().resolveWith(that, [data]).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise(this)) + .pipe(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise(this); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger('submit', e, this) !== false) && + that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + return that._getXHRPromise(); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + 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 @@ -491,13 +632,11 @@ fs = file.size, ub = options.uploadedBytes = options.uploadedBytes || 0, 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 = file.slice || file.webkitSlice || file.mozSlice, + dfd = $.Deferred(), + promise = dfd.promise(), jqXHR, - pipe; + upload; if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || options.data) { return false; @@ -506,62 +645,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 - ); - // 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) { @@ -572,102 +733,111 @@ 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', 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(); while (nextSlot) { - if (!nextSlot.isRejected()) { + 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; }; @@ -686,12 +856,12 @@ // 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(args); + slot.rejectWith(options.context, aborted); } - return send(false, args); + return send(); } return jqXHR.abort(); }; @@ -733,25 +903,15 @@ 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', e, newData); + return result; }); return result; }, - // File Normalization for Gecko 1.9.1 (Firefox 3.5) support: - _normalizeFile: function (index, file) { - if (file.name === undefined && file.size === undefined) { - file.name = file.fileName; - file.size = file.fileSize; - } - }, - _replaceFileInput: function (input) { var inputClone = input.clone(true); $('').append(inputClone)[0].reset(); @@ -761,7 +921,7 @@ // 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]) { @@ -776,102 +936,227 @@ } }, - _onChange: function (e) { - var that = e.data.fileupload, - data = { - files: $.each($.makeArray(e.target.files), that._normalizeFile), - fileInput: $(e.target), - form: $(e.target.form) - }; - if (!data.files.length) { + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + 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) { + 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(); + dirReader.readEntries(function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, errorHandler); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + 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: - data.files = [{name: e.target.value.replace(/^.*\\/, '')}]; - } - if (that.options.replaceFileInput) { - that._replaceFileInput(data.fileInput); + 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; + }); } - if (that._trigger('change', e, data) === false || - that._onAdd(e, data) === false) { - return false; + 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) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data.fileInput); + } + if (that._trigger('change', 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', e, data) === false || + this._onAdd(e, data) === false) { + return false; } - }); - if (that._trigger('paste', e, data) === false || - that._onAdd(e, data) === false) { - return false; } }, _onDrop: function (e) { - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, - data = { - files: $.each( - $.makeArray(dataTransfer && dataTransfer.files), - that._normalizeFile - ) - }; - if (that._trigger('drop', e, data) === false || - that._onAdd(e, data) === false) { - return false; + var that = this, + dataTransfer = e.dataTransfer = e.originalEvent && + e.originalEvent.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger('drop', e, data) !== false) { + that._onAdd(e, data); + } + }); } - e.preventDefault(); }, _onDragOver: function (e) { - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; - if (that._trigger('dragover', e) === false) { - return false; - } + var dataTransfer = e.dataTransfer = e.originalEvent && + e.originalEvent.dataTransfer; if (dataTransfer) { - dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy'; + if (this._trigger('dragover', e) === false) { + return false; + } + if ($.inArray('Files', dataTransfer.types) !== -1) { + dataTransfer.dropEffect = 'copy'; + e.preventDefault(); + } } - e.preventDefault(); }, _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 + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); } - this.options.fileInput - .bind('change.' + ns, {fileupload: this}, this._onChange); + this._on(this.options.fileInput, { + change: 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, '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(); } @@ -880,41 +1165,68 @@ _initSpecialOptions: function () { var options = this.options; if (options.fileInput === undefined) { - options.fileInput = this.element.is('input:file') ? - this.element : this.element.find('input:file'); + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); } else if (!(options.fileInput instanceof $)) { options.fileInput = $(options.fileInput); } 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; // 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].cloneNode(false)).data(), + function (key, value) { + 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); + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; }, - enable: function () { - $.Widget.prototype.enable.call(this); - this._initEventHandlers(); - }, - - 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 @@ -922,21 +1234,61 @@ // must have a files property and can contain additional options: // .fileupload('add', {files: filesList}); add: function (data) { + var that = this; if (!data || this.options.disabled) { return; } - data.files = $.each($.makeArray(data.files), this._normalizeFile); - this._onAdd(null, data); + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } }, // This method is exposed to the widget API and allows sending files // using the fileupload API. The data parameter accepts an object which - // must have a files property and can contain additional options: + // must have a files or fileInput property and can contain additional options: // .fileupload('send', {files: filesList}); // The method returns a Promise object for the file upload call. send: function (data) { if (data && !this.options.disabled) { - data.files = $.each($.makeArray(data.files), this._normalizeFile); + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + data.files = files; + jqXHR = that._onSend(null, data).then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); if (data.files.length) { return this._onSend(null, data); } diff --git a/vendor/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js b/vendor/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js old mode 100644 new mode 100755 index 04a5662..e04e7a0 --- a/vendor/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js +++ b/vendor/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js @@ -1,5 +1,5 @@ /* - * jQuery Iframe Transport Plugin 1.4 + * jQuery Iframe Transport Plugin 1.6.2 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan @@ -36,20 +36,35 @@ // equivalent to the return data of .serializeArray(), e.g.: // [{name: 'a', value: 1}, {name: 'b', value: 2}] $.ajaxTransport('iframe', function (options) { - if (options.async && (options.type === 'POST' || options.type === 'GET')) { + if (options.async) { var form, - iframe; + iframe, + addParamChar; return { send: function (_, completeCallback) { form = $(''); + form.attr('accept-charset', options.formAcceptCharset); + 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'; + } // javascript:false as initial iframe src // prevents warning popups on HTTPS in IE6. // 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 = $( '' + counter + '">' ).bind('load', function () { var fileInputClones, paramNames = $.isArray(options.paramName) ? @@ -82,7 +97,12 @@ // (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')) @@ -154,16 +174,16 @@ $.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 script': function (iframe) { - return $.globalEval($(iframe[0].body).text()); + return iframe && $.globalEval($(iframe[0].body).text()); } } }); diff --git a/vendor/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js b/vendor/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js index 7078fc9..fcef339 100644 --- a/vendor/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js +++ b/vendor/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js @@ -1,5 +1,5 @@ /* - * JavaScript Canvas to Blob 1.0.1 + * JavaScript Canvas to Blob 2.0.5 * https://github.com/blueimp/JavaScript-Canvas-to-Blob * * Copyright 2012, Sebastian Tschan @@ -13,73 +13,83 @@ */ /*jslint nomen: true, regexp: true */ -/*global window, atob, ArrayBuffer, Uint8Array, define */ +/*global window, atob, Blob, ArrayBuffer, Uint8Array, define */ -(function ($) { +(function (window) { 'use strict'; - - var BlobBuilder = window.MozBlobBuilder || - window.WebKitBlobBuilder || window.BlobBuilder, - blobTypes = /^image\/(jpeg|png)$/, - - // Converts a canvas element to a Blob or File object: - canvasToBlob = function (canvas, callback, options) { - options = options || {}; - if (canvas.toBlob) { - canvas.toBlob(callback, options.type); - return true; - } else if (canvas.mozGetAsFile) { - var name = options.name; - callback(canvas.mozGetAsFile( - (blobTypes.test(options.type) && name) || - ((name && name.replace(/\..+$/, '')) || 'blob') + '.png', - options.type - )); - return true; - } else if (canvas.toDataURL && BlobBuilder && window.atob && - window.ArrayBuffer && window.Uint8Array) { - callback(canvasToBlob.dataURItoBlob( - canvas.toDataURL(options.type) - )); - return true; + var CanvasPrototype = window.HTMLCanvasElement && + window.HTMLCanvasElement.prototype, + hasBlobConstructor = window.Blob && (function () { + try { + return Boolean(new Blob()); + } catch (e) { + return false; } - return false; - }; - - // Converts a dataURI to a Blob: - canvasToBlob.dataURItoBlob = function (dataURI) { - var byteString, - arrayBuffer, - intArray, - i, - bb, - mimeString; - if (dataURI.split(',')[0].indexOf('base64') >= 0) { - // Convert base64 to raw binary data held in a string: - byteString = atob(dataURI.split(',')[1]); - } else { - // Convert base64/URLEncoded data component to raw binary data: - byteString = decodeURIComponent(dataURI.split(',')[1]); + }()), + hasArrayBufferViewSupport = hasBlobConstructor && window.Uint8Array && + (function () { + try { + return new Blob([new Uint8Array(100)]).size === 100; + } catch (e) { + return false; + } + }()), + BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || + window.MozBlobBuilder || window.MSBlobBuilder, + dataURLtoBlob = (hasBlobConstructor || BlobBuilder) && window.atob && + window.ArrayBuffer && window.Uint8Array && function (dataURI) { + var byteString, + arrayBuffer, + intArray, + i, + mimeString, + bb; + if (dataURI.split(',')[0].indexOf('base64') >= 0) { + // Convert base64 to raw binary data held in a string: + byteString = atob(dataURI.split(',')[1]); + } else { + // Convert base64/URLEncoded data component to raw binary data: + byteString = decodeURIComponent(dataURI.split(',')[1]); + } + // Write the bytes of the string to an ArrayBuffer: + arrayBuffer = new ArrayBuffer(byteString.length); + intArray = new Uint8Array(arrayBuffer); + for (i = 0; i < byteString.length; i += 1) { + intArray[i] = byteString.charCodeAt(i); + } + // Separate out the mime component: + mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; + // Write the ArrayBuffer (or ArrayBufferView) to a blob: + if (hasBlobConstructor) { + return new Blob( + [hasArrayBufferViewSupport ? intArray : arrayBuffer], + {type: mimeString} + ); + } + bb = new BlobBuilder(); + bb.append(arrayBuffer); + return bb.getBlob(mimeString); + }; + if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) { + if (CanvasPrototype.mozGetAsFile) { + 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, quality) { + callback(dataURLtoBlob(this.toDataURL(type, quality))); + }; } - // Write the bytes of the string to an ArrayBuffer: - arrayBuffer = new ArrayBuffer(byteString.length); - intArray = new Uint8Array(arrayBuffer); - for (i = 0; i < byteString.length; i += 1) { - intArray[i] = byteString.charCodeAt(i); - } - // Write the ArrayBuffer to a blob: - bb = new BlobBuilder(); - bb.append(arrayBuffer); - // Separate out the mime component: - mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; - return bb.getBlob(mimeString); - }; - - if (typeof define !== 'undefined' && define.amd) { + } + if (typeof define === 'function' && define.amd) { define(function () { - return canvasToBlob; + return dataURLtoBlob; }); } else { - $.canvasToBlob = canvasToBlob; + window.dataURLtoBlob = dataURLtoBlob; } }(this)); diff --git a/vendor/assets/javascripts/jquery-fileupload/vendor/jquery.ui.widget.js b/vendor/assets/javascripts/jquery-fileupload/vendor/jquery.ui.widget.js index 9da8673..fd2948f 100644 --- a/vendor/assets/javascripts/jquery-fileupload/vendor/jquery.ui.widget.js +++ b/vendor/assets/javascripts/jquery-fileupload/vendor/jquery.ui.widget.js @@ -1,12 +1,12 @@ /* - * jQuery UI Widget 1.8.18+amd + * jQuery UI Widget 1.10.1+amd * https://github.com/blueimp/jQuery-File-Upload * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. * http://jquery.org/license * - * http://docs.jquery.com/UI/Widget + * http://api.jqueryui.com/jQuery.widget/ */ (function (factory) { @@ -19,40 +19,26 @@ } }(function( $, undefined ) { -// jQuery 1.4+ -if ( $.cleanData ) { - var _cleanData = $.cleanData; - $.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { - try { - $( elem ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - } - _cleanData( elems ); - }; -} else { - var _remove = $.fn.remove; - $.fn.remove = function( selector, keepData ) { - return this.each(function() { - if ( !keepData ) { - if ( !selector || $.filter( selector, [ this ] ).length ) { - $( "*", this ).add( [ this ] ).each(function() { - try { - $( this ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - }); - } - } - return _remove.call( $(this), selector, keepData ); - }); - }; -} +var uuid = 0, + slice = Array.prototype.slice, + _cleanData = $.cleanData; +$.cleanData = function( elems ) { + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + try { + $( elem ).triggerHandler( "remove" ); + // http://bugs.jquery.com/ticket/8235 + } catch( e ) {} + } + _cleanData( elems ); +}; $.widget = function( name, base, prototype ) { - var namespace = name.split( "." )[ 0 ], - fullName; + 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; @@ -62,81 +48,167 @@ $.widget = function( name, base, prototype ) { } // create selector for plugin - $.expr[ ":" ][ fullName ] = function( elem ) { - return !!$.data( elem, name ); + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); }; $[ namespace ] = $[ namespace ] || {}; - $[ namespace ][ name ] = function( options, element ) { + 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 ); } }; - - var basePrototype = new base(); + // 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 -// $.each( basePrototype, function( key, val ) { -// if ( $.isPlainObject(val) ) { -// basePrototype[ key ] = $.extend( {}, val ); -// } -// }); - basePrototype.options = $.extend( true, {}, basePrototype.options ); - $[ namespace ][ name ].prototype = $.extend( true, basePrototype, { + 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 + }, proxiedPrototype, { + constructor: constructor, namespace: namespace, widgetName: name, - widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name, - widgetBaseClass: fullName - }, prototype ); + 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, $[ namespace ][ name ] ); + $.widget.bridge( name, constructor ); +}; + +$.widget.extend = function( target ) { + var input = 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 = Array.prototype.slice.call( arguments, 1 ), + args = slice.call( arguments, 1 ), returnValue = this; // allow multiple hashes to be passed on init options = !isMethodCall && args.length ? - $.extend.apply( null, [ true, options ].concat(args) ) : + $.widget.extend.apply( null, [ options ].concat(args) ) : options; - // prevent calls to internal methods - if ( isMethodCall && options.charAt( 0 ) === "_" ) { - return returnValue; - } - if ( isMethodCall ) { this.each(function() { - var instance = $.data( this, name ), - methodValue = instance && $.isFunction( instance[options] ) ? - instance[ options ].apply( instance, args ) : - instance; - // TODO: add this back in 1.9 and use $.error() (see #5972) -// if ( !instance ) { -// throw "cannot call methods on " + name + " prior to initialization; " + -// "attempted to call method '" + options + "'"; -// } -// if ( !$.isFunction( instance[options] ) ) { -// throw "no such method '" + options + "' for " + name + " widget instance"; -// } -// var methodValue = instance[ options ].apply( instance, args ); + var methodValue, + instance = $.data( this, fullName ); + 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; + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; return false; } }); } else { this.each(function() { - var instance = $.data( this, name ); + var instance = $.data( this, fullName ); if ( instance ) { instance.option( options || {} )._init(); } else { - $.data( this, name, new object( options, this ) ); + $.data( this, fullName, new object( options, this ) ); } }); } @@ -145,74 +217,123 @@ $.widget.bridge = function( name, object ) { }; }; -$.Widget = function( options, element ) { - // allow instantiation without initializing for simple inheritance - if ( arguments.length ) { - this._createWidget( options, element ); - } -}; +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; $.Widget.prototype = { widgetName: "widget", widgetEventPrefix: "", + defaultElement: "