From cd2c9edb46ce870c050ab0fc17730f8c3eb3c55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Wed, 28 Jan 2015 19:33:11 +0100 Subject: [PATCH] FIX: :bug: upload on IE9 wasn't working :'( - FIX: make sure we set a default name to a pasted image only on Chrome (the only browser that supports it) - FIX: use ".json" extension to uploads endpoints since IE9 doesn't pass the correct header - FIX: pass the CSRF token in a query parameter since IE9 doesn't pass it in the headers - FIX: display error messages comming from the server when there is one over the default error message - FIX: HACK around IE9 security issue when clicking a file input via JavaScript (use a label and set `visibility:hidden` on the input) - FIX: hide the "cancel" upload on IE9 since it's not supported - FIX: return "text/plain" content-type when uploading a file for IE9 in order to prevent it from displaying the save dialog - FIX: check the maximum file size on the server :boom: - update jQuery File Upload Plugin to v. 5.42.2 - update JQuery IFram Transport Plugin to v. 1.8.5 - update jQuery UI Widget to v. 1.11.1 --- .../javascripts/discourse/lib/utilities.js | 6 +- .../discourse/mixins/upload.js.es6 | 45 ++++---- .../templates/components/avatar-uploader.hbs | 8 +- .../templates/components/emoji-uploader.hbs | 6 +- .../templates/components/image-uploader.hbs | 6 +- .../discourse/views/composer.js.es6 | 67 ++++++----- app/assets/stylesheets/desktop/compose.scss | 3 + app/assets/stylesheets/desktop/upload.scss | 3 + app/controllers/uploads_controller.rb | 10 +- app/controllers/users_controller.rb | 3 + app/models/upload.rb | 74 +++++++----- app/views/layouts/application.html.erb | 3 +- config/locales/server.en.yml | 4 +- spec/models/upload_spec.rb | 12 ++ .../assets/javascripts/jquery.fileupload.js | 76 ++++++++---- .../javascripts/jquery.iframe-transport.js | 85 ++++++++++---- vendor/assets/javascripts/jquery.ui.widget.js | 109 ++++++++++++------ 17 files changed, 341 insertions(+), 179 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index 81afac78a6c..243910e019b 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -164,7 +164,9 @@ Discourse.Utilities = { var upload = files[0]; // CHROME ONLY: if the image was pasted, sets its name to a default one - if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; } + if (typeof Blob !== "undefined" && typeof File !== "undefined") { + if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; } + } var type = Discourse.Utilities.isAnImage(upload.name) ? 'image' : 'attachment'; @@ -287,7 +289,7 @@ Discourse.Utilities = { // deal with meaningful errors first if (data.jqXHR) { switch (data.jqXHR.status) { - // cancel from the user + // cancelled by the user case 0: return; // entity too large, usually returned from the web server diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6 index f9b98024f4e..dceab04b34b 100644 --- a/app/assets/javascripts/discourse/mixins/upload.js.es6 +++ b/app/assets/javascripts/discourse/mixins/upload.js.es6 @@ -11,17 +11,15 @@ export default Em.Mixin.create({ }, _initializeUploader: function() { - // NOTE: we can't cache this as fileupload replaces the input after upload - // cf. https://github.com/blueimp/jQuery-File-Upload/wiki/Frequently-Asked-Questions#why-is-the-file-input-field-cloned-and-replaced-after-each-selection - var $upload = this.$('input[type=file]'), - self = this; + var $upload = this.$(), + self = this, + csrf = Discourse.Session.currentProp("csrfToken"); $upload.fileupload({ - url: this.get('uploadUrl'), + url: this.get('uploadUrl') + ".json?authenticity_token=" + encodeURIComponent(csrf), dataType: "json", - fileInput: $upload, - dropZone: this.$(), - pasteZone: this.$() + dropZone: $upload, + pasteZone: $upload }); $upload.on('fileuploadsubmit', function (e, data) { @@ -39,14 +37,20 @@ export default Em.Mixin.create({ }); $upload.on("fileuploaddone", function(e, data) { - if(data.result.url) { - self.uploadDone(data); - } else { - if (data.result.message) { - bootbox.alert(data.result.message); + if (data.result) { + if (data.result.url) { + self.uploadDone(data); } else { - bootbox.alert(I18n.t('post.errors.upload')); + if (data.result.message) { + bootbox.alert(data.result.message); + } else if (data.result.length > 0) { + bootbox.alert(data.result.join("\n")); + } else { + bootbox.alert(I18n.t('post.errors.upload')); + } } + } else { + bootbox.alert(I18n.t('post.errors.upload')); } }); @@ -60,12 +64,9 @@ export default Em.Mixin.create({ }.on('didInsertElement'), _destroyUploader: function() { - this.$('input[type=file]').fileupload('destroy'); - }.on('willDestroyElement'), - - actions: { - selectFile: function() { - this.$('input[type=file]').click(); - } - } + var $upload = this.$(); + try { $upload.fileupload('destroy'); } + catch (e) { /* wasn't initialized yet */ } + $upload.off(); + }.on('willDestroyElement') }); diff --git a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs index 028b30eab56..21761c67bd7 100644 --- a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs @@ -1,7 +1,7 @@ -<input type="file" accept="image/*" style="display:none" /> -<button class="btn" {{action "selectFile"}} {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}"> - <i class="fa fa-picture-o"></i> {{uploadButtonText}} -</button> +<label class="btn" {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}"> + {{fa-icon "picture-o"}} {{uploadButtonText}} + <input {{bind-attr disabled="uploading"}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" /> +</label> {{#if uploading}} <span>{{i18n 'upload_selector.uploading'}} {{view.uploadProgress}}%</span> {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs b/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs index 8d0166e7362..8d9b297cf92 100644 --- a/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs @@ -1,6 +1,6 @@ {{text-field name="name" placeholderKey="admin.emoji.name" value=name}} -<input type="file" accept=".png,.gif" style="display:none" /> -<button {{bind-attr disabled="addDisabled"}} {{action "selectFile"}} class='btn btn-primary'> +<label class="btn btn-primary" {{bind-attr disabled="addDisabled"}}> {{fa-icon "plus"}} {{i18n 'admin.emoji.add'}} -</button> + <input {{bind-attr disabled="addDisabled"}} type="file" accept=".png,.gif" style="visibility: hidden; position: absolute;" /> +</label> diff --git a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs index ccb6a03ae2f..32c0dce1299 100644 --- a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs @@ -1,7 +1,9 @@ -<input type="file" accept="image/*" style="display:none" /> <div class="uploaded-image-preview" class="input-xxlarge" {{bind-attr style="backgroundStyle"}}> <div class="image-upload-controls"> - <button {{action "selectFile"}} class="btn pad-left no-text">{{fa-icon "picture-o"}}</button> + <label class="btn pad-left no-text" {{bind-attr disabled="uploading"}}> + {{fa-icon "picture-o"}} + <input {{bind-attr disabled="uploading"}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" /> + </label> {{#if backgroundStyle}} <button {{action "trash"}} class="btn btn-danger pad-left no-text">{{fa-icon "trash-o"}}</button> {{/if}} diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index be2a8c14cc9..7fe4ec8efde 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -307,11 +307,14 @@ var ComposerView = Discourse.View.extend(Ember.Evented, { // in case it's still bound somehow this._unbindUploadTarget(); - var $uploadTarget = $('#reply-control'); + var $uploadTarget = $('#reply-control'), + csrf = Discourse.Session.currentProp('csrfToken'), + cancelledByTheUser; + // NOTE: we need both the .json extension and the CSRF token as a query parameter for IE9 $uploadTarget.fileupload({ - url: Discourse.getURL('/uploads'), - dataType: 'json', + url: Discourse.getURL('/uploads.json?authenticity_token=' + encodeURIComponent(csrf)), + dataType: 'json' }); // submit - this event is triggered for each upload @@ -324,22 +327,27 @@ var ComposerView = Discourse.View.extend(Ember.Evented, { // send - this event is triggered when the upload request is about to start $uploadTarget.on('fileuploadsend', function (e, data) { + cancelledByTheUser = false; // hide the "file selector" modal self.get('controller').send('closeModal'); - // cf. https://github.com/blueimp/jQuery-File-Upload/wiki/API#how-to-cancel-an-upload - var jqXHR = data.xhr(); - // need to wait for the link to show up in the DOM - Em.run.schedule('afterRender', function() { - // bind on the click event on the cancel link - $('#cancel-file-upload').on('click', function() { - // cancel the upload - self.set('isUploading', false); - // NOTE: this might trigger a 'fileuploadfail' event with status = 0 - if (jqXHR) jqXHR.abort(); - // unbind - $(this).off('click'); - }); - }); + // NOTE: IE9 doesn't support XHR + if (data["xhr"]) { + var jqHXR = data.xhr(); + if (jqHXR) { + // need to wait for the link to show up in the DOM + Em.run.schedule('afterRender', function() { + // bind on the click event on the cancel link + $('#cancel-file-upload').on('click', function() { + // cancel the upload + self.set('isUploading', false); + // NOTE: this might trigger a 'fileuploadfail' event with status = 0 + if (jqHXR) { cancelledByTheUser = true; jqHXR.abort(); } + // unbind + $(this).off('click'); + }); + }); + } + } }); // progress all @@ -350,14 +358,17 @@ var ComposerView = Discourse.View.extend(Ember.Evented, { // done $uploadTarget.on('fileuploaddone', function (e, data) { - // make sure we have a url - if (data.result.url) { - var markdown = Discourse.Utilities.getUploadMarkdown(data.result); - // appends a space at the end of the inserted markdown - self.addMarkdown(markdown + " "); - self.set('isUploading', false); - } else { - bootbox.alert(I18n.t('post.errors.upload')); + if (!cancelledByTheUser) { + // make sure we have a url + if (data.result.url) { + var markdown = Discourse.Utilities.getUploadMarkdown(data.result); + // appends a space at the end of the inserted markdown + self.addMarkdown(markdown + " "); + self.set('isUploading', false); + } else { + // display the error message sent by the server + bootbox.alert(data.result.join("\n")); + } } }); @@ -365,8 +376,10 @@ var ComposerView = Discourse.View.extend(Ember.Evented, { $uploadTarget.on('fileuploadfail', function (e, data) { // hide upload status self.set('isUploading', false); - // display an error message - Discourse.Utilities.displayErrorForUpload(data); + if (!cancelledByTheUser) { + // display an error message + Discourse.Utilities.displayErrorForUpload(data); + } }); // contenteditable div hack for getting image paste to upload working in diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 5c9032b7423..ca0af4639d0 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -81,6 +81,9 @@ margin-left: 10px; } +// hide cancel upload link on IE9 (not supported) +.ie9 #cancel-file-upload { display: none; } + #reply-control { .toggle-preview, #draft-status, #file-uploading { position: absolute; diff --git a/app/assets/stylesheets/desktop/upload.scss b/app/assets/stylesheets/desktop/upload.scss index 80d0667cacf..431130be68d 100644 --- a/app/assets/stylesheets/desktop/upload.scss +++ b/app/assets/stylesheets/desktop/upload.scss @@ -36,4 +36,7 @@ .image-upload-controls { padding: 10px; + label.btn { + padding: 7px 10px 5px 10px; + } } diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index a8f1dad6f6e..4ceb9810952 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -4,17 +4,17 @@ class UploadsController < ApplicationController def create file = params[:file] || params[:files].first - filesize = File.size(file.tempfile) upload = Upload.create_for(current_user.id, file.tempfile, file.original_filename, filesize, { content_type: file.content_type }) - if current_user.admin? + if upload.errors.empty? && current_user.admin? retain_hours = params[:retain_hours].to_i - if retain_hours > 0 - upload.update_columns(retain_hours: retain_hours) - end + upload.update_columns(retain_hours: retain_hours) if retain_hours > 0 end + # HACK FOR IE9 to prevent the "download dialog" + response.headers["Content-Type"] = "text/plain" if request.user_agent =~ /MSIE 9/ + if upload.errors.empty? render_serialized(upload, UploadSerializer, root: false) else diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index b2ff46cecda..192791818f8 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -441,6 +441,9 @@ class UsersController < ApplicationController file = params[:file] || params[:files].first + # HACK FOR IE9 to prevent the "download dialog" + response.headers["Content-Type"] = "text/plain" if request.user_agent =~ /MSIE 9/ + begin image = build_user_image_from(file) rescue Discourse::InvalidParameters diff --git a/app/models/upload.rb b/app/models/upload.rb index 33432a28a4b..81ab4e5fe81 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -72,39 +72,23 @@ class Upload < ActiveRecord::Base # trim the origin if any upload.origin = options[:origin][0...1000] if options[:origin] - # deal with width & height for images + # check the size of the upload if FileHelper.is_image?(filename) - begin - if filename =~ /\.svg$/i - svg = Nokogiri::XML(file).at_css("svg") - width, height = svg["width"].to_i, svg["height"].to_i - if width == 0 || height == 0 - upload.errors.add(:base, I18n.t("upload.images.size_not_found")) - else - upload.width, upload.height = ImageSizer.resize(width, height) - end - else - # fix orientation first - Upload.fix_image_orientation(file.path) - # retrieve image info - image_info = FastImage.new(file, raise_on_failure: true) - # compute image aspect ratio - upload.width, upload.height = ImageSizer.resize(*image_info.size) - end - # make sure we're at the beginning of the file - # (FastImage and Nokogiri move the pointer) - file.rewind - rescue FastImage::ImageFetchFailure - upload.errors.add(:base, I18n.t("upload.images.fetch_failure")) - rescue FastImage::UnknownImageType - upload.errors.add(:base, I18n.t("upload.images.unknown_image_type")) - rescue FastImage::SizeNotFound - upload.errors.add(:base, I18n.t("upload.images.size_not_found")) + if SiteSetting.max_image_size_kb > 0 && filesize >= SiteSetting.max_image_size_kb.kilobytes + upload.errors.add(:base, I18n.t("upload.images.too_large", max_size_kb: SiteSetting.max_image_size_kb)) + else + # deal with width & height for images + upload = Upload.resize_image(filename, file, upload) + end + else + if SiteSetting.max_attachment_size_kb > 0 && filesize >= SiteSetting.max_attachment_size_kb.kilobytes + upload.errors.add(:base, I18n.t("upload.attachments.too_large", max_size_kb: SiteSetting.max_attachment_size_kb)) end - - return upload unless upload.errors.empty? end + # make sure there is no error + return upload unless upload.errors.empty? + # create a db record (so we can use the id) return upload unless upload.save @@ -122,6 +106,38 @@ class Upload < ActiveRecord::Base upload end + def self.resize_image(filename, file, upload) + begin + if filename =~ /\.svg$/i + svg = Nokogiri::XML(file).at_css("svg") + width, height = svg["width"].to_i, svg["height"].to_i + if width == 0 || height == 0 + upload.errors.add(:base, I18n.t("upload.images.size_not_found")) + else + upload.width, upload.height = ImageSizer.resize(width, height) + end + else + # fix orientation first + Upload.fix_image_orientation(file.path) + # retrieve image info + image_info = FastImage.new(file, raise_on_failure: true) + # compute image aspect ratio + upload.width, upload.height = ImageSizer.resize(*image_info.size) + end + # make sure we're at the beginning of the file + # (FastImage and Nokogiri move the pointer) + file.rewind + rescue FastImage::ImageFetchFailure + upload.errors.add(:base, I18n.t("upload.images.fetch_failure")) + rescue FastImage::UnknownImageType + upload.errors.add(:base, I18n.t("upload.images.unknown_image_type")) + rescue FastImage::SizeNotFound + upload.errors.add(:base, I18n.t("upload.images.size_not_found")) + end + + upload + end + def self.get_from_url(url) return if url.blank? # we store relative urls, so we need to remove any host/cdn diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 533587e3542..f4c61ef136e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,6 @@ <!DOCTYPE html> -<html lang="<%= SiteSetting.default_locale %>" class="<%= html_classes %>"> +<!--[if IE 9]><html lang="<%= SiteSetting.default_locale %>" class="ie9 <%= html_classes %>"><![endif]--> +<!--[if (!IE 9) | (!IE)]><!--><html lang="<%= SiteSetting.default_locale %>" class="<%= html_classes %>"><!--<![endif]--> <head> <meta charset="utf-8"> <title><%= content_for?(:title) ? yield(:title) + ' - ' + SiteSetting.title : SiteSetting.title %></title> diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 22ad4af979d..e4166e10d0b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1828,9 +1828,9 @@ en: pasted_image_filename: "Pasted image" store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}." attachments: - too_large: "Sorry, the file you are trying to upload is too big (maximum size is %{max_size_kb}%kb)." + too_large: "Sorry, the file you are trying to upload is too big (maximum size is %{max_size_kb}KB)." images: - too_large: "Sorry, the image you are trying to upload is too big (maximum size is %{max_size_kb}%kb), please resize it and try again." + too_large: "Sorry, the image you are trying to upload is too big (maximum size is %{max_size_kb}KB), please resize it and try again." fetch_failure: "Sorry, there has been an error while fetching the image." unknown_image_type: "Sorry, but the file you tried to upload doesn't appear to be an image." size_not_found: "Sorry, but we couldn't determine the size of the image. Maybe your image is corrupted?" diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index fce64613898..092006b38c1 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -84,6 +84,18 @@ describe Upload do expect(upload.errors.size).to be > 0 end + it "generates an error when the image is too large" do + SiteSetting.stubs(:max_image_size_kb).returns(1) + upload = Upload.create_for(user_id, image, image_filename, image_filesize) + expect(upload.errors.size).to be > 0 + end + + it "generates an error when the attachment is too large" do + SiteSetting.stubs(:max_attachment_size_kb).returns(1) + upload = Upload.create_for(user_id, attachment, attachment_filename, attachment_filesize) + expect(upload.errors.size).to be > 0 + end + it "saves proper information" do store = {} Discourse.expects(:store).returns(store) diff --git a/vendor/assets/javascripts/jquery.fileupload.js b/vendor/assets/javascripts/jquery.fileupload.js index 833d3532328..e99f3091e6f 100644 --- a/vendor/assets/javascripts/jquery.fileupload.js +++ b/vendor/assets/javascripts/jquery.fileupload.js @@ -1,5 +1,5 @@ /* - * jQuery File Upload Plugin 5.40.3 + * jQuery File Upload Plugin 5.42.2 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan @@ -10,7 +10,7 @@ */ /* jshint nomen:false */ -/* global define, window, document, location, Blob, FormData */ +/* global define, require, window, document, location, Blob, FormData */ (function (factory) { 'use strict'; @@ -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); @@ -51,6 +57,25 @@ $.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 @@ -65,9 +90,9 @@ // The drop target element(s), by the default the complete document. // Set to null to disable drag & drop support: dropZone: $(document), - // The paste target element(s), by the default the complete document. - // Set to null to disable paste support: - pasteZone: $(document), + // 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. @@ -1015,8 +1040,11 @@ return result; }, - _replaceFileInput: function (input) { - var inputClone = input.clone(true); + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; $('<form></form>').append(inputClone)[0].reset(); // Detaching allows to insert the fileInput on another form // without loosing the file input value: @@ -1187,7 +1215,7 @@ 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', @@ -1240,24 +1268,21 @@ } }, - _onDragOver: function (e) { - e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; - var dataTransfer = e.dataTransfer; - if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && - this._trigger( - 'dragover', - $.Event('dragover', {delegatedEvent: e}) - ) !== false) { - e.preventDefault(); - dataTransfer.dropEffect = 'copy'; - } - }, + _onDragOver: getDragHandler('dragover'), + + _onDragEnter: getDragHandler('dragenter'), + + _onDragLeave: getDragHandler('dragleave'), _initEventHandlers: function () { if (this._isXHRUpload(this.options)) { this._on(this.options.dropZone, { dragover: this._onDragOver, - drop: this._onDrop + 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 @@ -1271,7 +1296,7 @@ }, _destroyEventHandlers: function () { - this._off(this.options.dropZone, 'dragover drop'); + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); this._off(this.options.pasteZone, 'paste'); this._off(this.options.fileInput, 'change'); }, @@ -1319,10 +1344,13 @@ _initDataAttributes: function () { var that = this, options = this.options, - clone = $(this.element[0].cloneNode(false)); + clone = $(this.element[0].cloneNode(false)), + data = clone.data(); + // Avoid memory leaks: + clone.remove(); // Initialize options set via HTML5 data-attributes: $.each( - clone.data(), + data, function (key, value) { var dataAttributeName = 'data-' + // Convert camelCase to hyphen-ated key: diff --git a/vendor/assets/javascripts/jquery.iframe-transport.js b/vendor/assets/javascripts/jquery.iframe-transport.js index 4749f469936..b7581f23f43 100644 --- a/vendor/assets/javascripts/jquery.iframe-transport.js +++ b/vendor/assets/javascripts/jquery.iframe-transport.js @@ -1,5 +1,5 @@ /* - * jQuery Iframe Transport Plugin 1.5 + * jQuery Iframe Transport Plugin 1.8.3 * 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) { '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 style="display:none;"></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 = $( - '<iframe src="javascript:false;" name="iframe-transport-' + - (counter += 1) + '"></iframe>' + '<iframe src="' + initialIframeSrc + + '" name="iframe-transport-' + counter + '"></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): - $('<iframe src="javascript:false;"></iframe>') + $('<iframe src="' + initialIframeSrc + '"></iframe>') .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.ui.widget.js b/vendor/assets/javascripts/jquery.ui.widget.js index c430419971e..5ac2ed5a572 100644 --- a/vendor/assets/javascripts/jquery.ui.widget.js +++ b/vendor/assets/javascripts/jquery.ui.widget.js @@ -1,6 +1,27 @@ +/*! jQuery UI - v1.11.1+CommonJS - 2014-09-17 +* http://jqueryui.com +* Includes: widget.js +* Copyright 2014 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.10.4+amd - * https://github.com/blueimp/jQuery-File-Upload + * jQuery UI Widget 1.11.1 + * http://jqueryui.com * * Copyright 2014 jQuery Foundation and other contributors * Released under the MIT license. @@ -9,28 +30,28 @@ * http://api.jqueryui.com/jQuery.widget/ */ -(function (factory) { - if (typeof define === "function" && define.amd) { - // Register as an anonymous AMD module: - define(["jquery"], factory); - } else { - // Browser globals: - factory(jQuery); - } -}(function( $, undefined ) { -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 ); -}; +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, @@ -143,10 +164,12 @@ $.widget = function( name, base, prototype ) { } $.widget.bridge( name, constructor ); + + return constructor; }; $.widget.extend = function( target ) { - var input = slice.call( arguments, 1 ), + var input = widget_slice.call( arguments, 1 ), inputIndex = 0, inputLength = input.length, key, @@ -175,7 +198,7 @@ $.widget.bridge = function( name, object ) { var fullName = object.prototype.widgetFullName || name; $.fn[ name ] = function( options ) { var isMethodCall = typeof options === "string", - args = slice.call( arguments, 1 ), + args = widget_slice.call( arguments, 1 ), returnValue = this; // allow multiple hashes to be passed on init @@ -187,6 +210,10 @@ $.widget.bridge = function( name, object ) { 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 + "'" ); @@ -206,7 +233,10 @@ $.widget.bridge = function( name, object ) { this.each(function() { var instance = $.data( this, fullName ); if ( instance ) { - instance.option( options || {} )._init(); + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } } else { $.data( this, fullName, new object( options, this ) ); } @@ -233,7 +263,7 @@ $.Widget.prototype = { _createWidget: function( options, element ) { element = $( element || this.defaultElement || this )[ 0 ]; this.element = $( element ); - this.uuid = uuid++; + this.uuid = widget_uuid++; this.eventNamespace = "." + this.widgetName + this.uuid; this.options = $.widget.extend( {}, this.options, @@ -276,9 +306,6 @@ $.Widget.prototype = { // all event bindings should go through this._on() this.element .unbind( this.eventNamespace ) - // 1.9 BC for #7810 - // TODO remove dual storage - .removeData( this.widgetName ) .removeData( this.widgetFullName ) // support: jquery <1.6.3 // http://bugs.jquery.com/ticket/9413 @@ -354,20 +381,23 @@ $.Widget.prototype = { if ( key === "disabled" ) { this.widget() - .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) - .attr( "aria-disabled", value ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); + .toggleClass( this.widgetFullName + "-disabled", !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } } return this; }, enable: function() { - return this._setOption( "disabled", false ); + return this._setOptions({ disabled: false }); }, disable: function() { - return this._setOption( "disabled", true ); + return this._setOptions({ disabled: true }); }, _on: function( suppressDisabledCheck, element, handlers ) { @@ -387,7 +417,6 @@ $.Widget.prototype = { element = this.element; delegateElement = this.widget(); } else { - // accept selectors, DOM elements element = delegateElement = $( element ); this.bindings = this.bindings.add( element ); } @@ -412,7 +441,7 @@ $.Widget.prototype = { handler.guid || handlerProxy.guid || $.guid++; } - var match = event.match( /^(\w+)\s*(.*)$/ ), + var match = event.match( /^([\w:-]*)\s*(.*)$/ ), eventName = match[1] + instance.eventNamespace, selector = match[2]; if ( selector ) { @@ -527,4 +556,8 @@ $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { }; }); +var widget = $.widget; + + + }));