FIX: 🐛 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 💥
- 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
This commit is contained in:
Régis Hanol 2015-01-28 19:33:11 +01:00
parent 053d3120f7
commit cd2c9edb46
17 changed files with 341 additions and 179 deletions

View File

@ -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

View File

@ -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')
});

View File

@ -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>&nbsp;{{uploadButtonText}}
</button>
<label class="btn" {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}">
{{fa-icon "picture-o"}}&nbsp;{{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}}

View File

@ -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>

View File

@ -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}}

View File

@ -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

View File

@ -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;

View File

@ -36,4 +36,7 @@
.image-upload-controls {
padding: 10px;
label.btn {
padding: 7px 10px 5px 10px;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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?"

View File

@ -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)

View File

@ -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:

View File

@ -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());
}
}
});

View File

@ -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;
}));