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]; var upload = files[0];
// CHROME ONLY: if the image was pasted, sets its name to a default one // CHROME ONLY: if the image was pasted, sets its name to a default one
if (typeof Blob !== "undefined" && typeof File !== "undefined") {
if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; } if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; }
}
var type = Discourse.Utilities.isAnImage(upload.name) ? 'image' : 'attachment'; var type = Discourse.Utilities.isAnImage(upload.name) ? 'image' : 'attachment';
@ -287,7 +289,7 @@ Discourse.Utilities = {
// deal with meaningful errors first // deal with meaningful errors first
if (data.jqXHR) { if (data.jqXHR) {
switch (data.jqXHR.status) { switch (data.jqXHR.status) {
// cancel from the user // cancelled by the user
case 0: return; case 0: return;
// entity too large, usually returned from the web server // entity too large, usually returned from the web server

View file

@ -11,17 +11,15 @@ export default Em.Mixin.create({
}, },
_initializeUploader: function() { _initializeUploader: function() {
// NOTE: we can't cache this as fileupload replaces the input after upload var $upload = this.$(),
// cf. https://github.com/blueimp/jQuery-File-Upload/wiki/Frequently-Asked-Questions#why-is-the-file-input-field-cloned-and-replaced-after-each-selection self = this,
var $upload = this.$('input[type=file]'), csrf = Discourse.Session.currentProp("csrfToken");
self = this;
$upload.fileupload({ $upload.fileupload({
url: this.get('uploadUrl'), url: this.get('uploadUrl') + ".json?authenticity_token=" + encodeURIComponent(csrf),
dataType: "json", dataType: "json",
fileInput: $upload, dropZone: $upload,
dropZone: this.$(), pasteZone: $upload
pasteZone: this.$()
}); });
$upload.on('fileuploadsubmit', function (e, data) { $upload.on('fileuploadsubmit', function (e, data) {
@ -39,15 +37,21 @@ export default Em.Mixin.create({
}); });
$upload.on("fileuploaddone", function(e, data) { $upload.on("fileuploaddone", function(e, data) {
if (data.result) {
if (data.result.url) { if (data.result.url) {
self.uploadDone(data); self.uploadDone(data);
} else { } else {
if (data.result.message) { if (data.result.message) {
bootbox.alert(data.result.message); bootbox.alert(data.result.message);
} else if (data.result.length > 0) {
bootbox.alert(data.result.join("\n"));
} else { } else {
bootbox.alert(I18n.t('post.errors.upload')); bootbox.alert(I18n.t('post.errors.upload'));
} }
} }
} else {
bootbox.alert(I18n.t('post.errors.upload'));
}
}); });
$upload.on("fileuploadfail", function(e, data) { $upload.on("fileuploadfail", function(e, data) {
@ -60,12 +64,9 @@ export default Em.Mixin.create({
}.on('didInsertElement'), }.on('didInsertElement'),
_destroyUploader: function() { _destroyUploader: function() {
this.$('input[type=file]').fileupload('destroy'); var $upload = this.$();
}.on('willDestroyElement'), try { $upload.fileupload('destroy'); }
catch (e) { /* wasn't initialized yet */ }
actions: { $upload.off();
selectFile: function() { }.on('willDestroyElement')
this.$('input[type=file]').click();
}
}
}); });

View file

@ -1,7 +1,7 @@
<input type="file" accept="image/*" style="display:none" /> <label class="btn" {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}">
<button class="btn" {{action "selectFile"}} {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}"> {{fa-icon "picture-o"}}&nbsp;{{uploadButtonText}}
<i class="fa fa-picture-o"></i>&nbsp;{{uploadButtonText}} <input {{bind-attr disabled="uploading"}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" />
</button> </label>
{{#if uploading}} {{#if uploading}}
<span>{{i18n 'upload_selector.uploading'}} {{view.uploadProgress}}%</span> <span>{{i18n 'upload_selector.uploading'}} {{view.uploadProgress}}%</span>
{{/if}} {{/if}}

View file

@ -1,6 +1,6 @@
{{text-field name="name" placeholderKey="admin.emoji.name" value=name}} {{text-field name="name" placeholderKey="admin.emoji.name" value=name}}
<input type="file" accept=".png,.gif" style="display:none" /> <label class="btn btn-primary" {{bind-attr disabled="addDisabled"}}>
<button {{bind-attr disabled="addDisabled"}} {{action "selectFile"}} class='btn btn-primary'>
{{fa-icon "plus"}} {{fa-icon "plus"}}
{{i18n 'admin.emoji.add'}} {{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="uploaded-image-preview" class="input-xxlarge" {{bind-attr style="backgroundStyle"}}>
<div class="image-upload-controls"> <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}} {{#if backgroundStyle}}
<button {{action "trash"}} class="btn btn-danger pad-left no-text">{{fa-icon "trash-o"}}</button> <button {{action "trash"}} class="btn btn-danger pad-left no-text">{{fa-icon "trash-o"}}</button>
{{/if}} {{/if}}

View file

@ -307,11 +307,14 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// in case it's still bound somehow // in case it's still bound somehow
this._unbindUploadTarget(); 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({ $uploadTarget.fileupload({
url: Discourse.getURL('/uploads'), url: Discourse.getURL('/uploads.json?authenticity_token=' + encodeURIComponent(csrf)),
dataType: 'json', dataType: 'json'
}); });
// submit - this event is triggered for each upload // submit - this event is triggered for each upload
@ -324,10 +327,13 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// send - this event is triggered when the upload request is about to start // send - this event is triggered when the upload request is about to start
$uploadTarget.on('fileuploadsend', function (e, data) { $uploadTarget.on('fileuploadsend', function (e, data) {
cancelledByTheUser = false;
// hide the "file selector" modal // hide the "file selector" modal
self.get('controller').send('closeModal'); self.get('controller').send('closeModal');
// cf. https://github.com/blueimp/jQuery-File-Upload/wiki/API#how-to-cancel-an-upload // NOTE: IE9 doesn't support XHR
var jqXHR = data.xhr(); if (data["xhr"]) {
var jqHXR = data.xhr();
if (jqHXR) {
// need to wait for the link to show up in the DOM // need to wait for the link to show up in the DOM
Em.run.schedule('afterRender', function() { Em.run.schedule('afterRender', function() {
// bind on the click event on the cancel link // bind on the click event on the cancel link
@ -335,11 +341,13 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// cancel the upload // cancel the upload
self.set('isUploading', false); self.set('isUploading', false);
// NOTE: this might trigger a 'fileuploadfail' event with status = 0 // NOTE: this might trigger a 'fileuploadfail' event with status = 0
if (jqXHR) jqXHR.abort(); if (jqHXR) { cancelledByTheUser = true; jqHXR.abort(); }
// unbind // unbind
$(this).off('click'); $(this).off('click');
}); });
}); });
}
}
}); });
// progress all // progress all
@ -350,6 +358,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// done // done
$uploadTarget.on('fileuploaddone', function (e, data) { $uploadTarget.on('fileuploaddone', function (e, data) {
if (!cancelledByTheUser) {
// make sure we have a url // make sure we have a url
if (data.result.url) { if (data.result.url) {
var markdown = Discourse.Utilities.getUploadMarkdown(data.result); var markdown = Discourse.Utilities.getUploadMarkdown(data.result);
@ -357,7 +366,9 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
self.addMarkdown(markdown + " "); self.addMarkdown(markdown + " ");
self.set('isUploading', false); self.set('isUploading', false);
} else { } else {
bootbox.alert(I18n.t('post.errors.upload')); // 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) { $uploadTarget.on('fileuploadfail', function (e, data) {
// hide upload status // hide upload status
self.set('isUploading', false); self.set('isUploading', false);
if (!cancelledByTheUser) {
// display an error message // display an error message
Discourse.Utilities.displayErrorForUpload(data); Discourse.Utilities.displayErrorForUpload(data);
}
}); });
// contenteditable div hack for getting image paste to upload working in // contenteditable div hack for getting image paste to upload working in

View file

@ -81,6 +81,9 @@
margin-left: 10px; margin-left: 10px;
} }
// hide cancel upload link on IE9 (not supported)
.ie9 #cancel-file-upload { display: none; }
#reply-control { #reply-control {
.toggle-preview, #draft-status, #file-uploading { .toggle-preview, #draft-status, #file-uploading {
position: absolute; position: absolute;

View file

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

View file

@ -4,17 +4,17 @@ class UploadsController < ApplicationController
def create def create
file = params[:file] || params[:files].first file = params[:file] || params[:files].first
filesize = File.size(file.tempfile) filesize = File.size(file.tempfile)
upload = Upload.create_for(current_user.id, file.tempfile, file.original_filename, filesize, { content_type: file.content_type }) 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 retain_hours = params[:retain_hours].to_i
if retain_hours > 0 upload.update_columns(retain_hours: retain_hours) if retain_hours > 0
upload.update_columns(retain_hours: retain_hours)
end
end 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? if upload.errors.empty?
render_serialized(upload, UploadSerializer, root: false) render_serialized(upload, UploadSerializer, root: false)
else else

View file

@ -441,6 +441,9 @@ class UsersController < ApplicationController
file = params[:file] || params[:files].first 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 begin
image = build_user_image_from(file) image = build_user_image_from(file)
rescue Discourse::InvalidParameters rescue Discourse::InvalidParameters

View file

@ -72,8 +72,41 @@ class Upload < ActiveRecord::Base
# trim the origin if any # trim the origin if any
upload.origin = options[:origin][0...1000] if options[:origin] 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) if FileHelper.is_image?(filename)
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
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
# store the file and update its url
url = Discourse.store.store_upload(file, upload, options[:content_type])
if url.present?
upload.url = url
upload.save
else
upload.errors.add(:url, I18n.t("upload.store_failure", { upload_id: upload.id, user_id: user_id }))
end
end
# return the uploaded file
upload
end
def self.resize_image(filename, file, upload)
begin begin
if filename =~ /\.svg$/i if filename =~ /\.svg$/i
svg = Nokogiri::XML(file).at_css("svg") svg = Nokogiri::XML(file).at_css("svg")
@ -102,23 +135,6 @@ class Upload < ActiveRecord::Base
upload.errors.add(:base, I18n.t("upload.images.size_not_found")) upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
end end
return upload unless upload.errors.empty?
end
# create a db record (so we can use the id)
return upload unless upload.save
# store the file and update its url
url = Discourse.store.store_upload(file, upload, options[:content_type])
if url.present?
upload.url = url
upload.save
else
upload.errors.add(:url, I18n.t("upload.store_failure", { upload_id: upload.id, user_id: user_id }))
end
end
# return the uploaded file
upload upload
end end

View file

@ -1,5 +1,6 @@
<!DOCTYPE html> <!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> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title><%= content_for?(:title) ? yield(:title) + ' - ' + SiteSetting.title : SiteSetting.title %></title> <title><%= content_for?(:title) ? yield(:title) + ' - ' + SiteSetting.title : SiteSetting.title %></title>

View file

@ -1828,9 +1828,9 @@ en:
pasted_image_filename: "Pasted image" pasted_image_filename: "Pasted image"
store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}." store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}."
attachments: 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: 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." 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." 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?" 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 expect(upload.errors.size).to be > 0
end 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 it "saves proper information" do
store = {} store = {}
Discourse.expects(:store).returns(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 * https://github.com/blueimp/jQuery-File-Upload
* *
* Copyright 2010, Sebastian Tschan * Copyright 2010, Sebastian Tschan
@ -10,7 +10,7 @@
*/ */
/* jshint nomen:false */ /* jshint nomen:false */
/* global define, window, document, location, Blob, FormData */ /* global define, require, window, document, location, Blob, FormData */
(function (factory) { (function (factory) {
'use strict'; 'use strict';
@ -20,6 +20,12 @@
'jquery', 'jquery',
'jquery.ui.widget' 'jquery.ui.widget'
], factory); ], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS:
factory(
require('jquery'),
require('./vendor/jquery.ui.widget')
);
} else { } else {
// Browser globals: // Browser globals:
factory(window.jQuery); factory(window.jQuery);
@ -51,6 +57,25 @@
$.support.blobSlice = window.Blob && (Blob.prototype.slice || $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
Blob.prototype.webkitSlice || Blob.prototype.mozSlice); 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 // The fileupload widget listens for change events on file input fields defined
// via fileInput setting and paste or drop events of the given dropZone. // via fileInput setting and paste or drop events of the given dropZone.
// In addition to the default jQuery Widget methods, the fileupload widget // 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. // The drop target element(s), by the default the complete document.
// Set to null to disable drag & drop support: // Set to null to disable drag & drop support:
dropZone: $(document), dropZone: $(document),
// The paste target element(s), by the default the complete document. // The paste target element(s), by the default undefined.
// Set to null to disable paste support: // Set to a DOM node or jQuery object to enable file pasting:
pasteZone: $(document), pasteZone: undefined,
// The file input field(s), that are listened to for change events. // The file input field(s), that are listened to for change events.
// If undefined, it is set to the file input fields inside // If undefined, it is set to the file input fields inside
// of the widget element on plugin initialization. // of the widget element on plugin initialization.
@ -1015,8 +1040,11 @@
return result; return result;
}, },
_replaceFileInput: function (input) { _replaceFileInput: function (data) {
var inputClone = input.clone(true); 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(); $('<form></form>').append(inputClone)[0].reset();
// Detaching allows to insert the fileInput on another form // Detaching allows to insert the fileInput on another form
// without loosing the file input value: // without loosing the file input value:
@ -1187,7 +1215,7 @@
this._getFileInputFiles(data.fileInput).always(function (files) { this._getFileInputFiles(data.fileInput).always(function (files) {
data.files = files; data.files = files;
if (that.options.replaceFileInput) { if (that.options.replaceFileInput) {
that._replaceFileInput(data.fileInput); that._replaceFileInput(data);
} }
if (that._trigger( if (that._trigger(
'change', 'change',
@ -1240,24 +1268,21 @@
} }
}, },
_onDragOver: function (e) { _onDragOver: getDragHandler('dragover'),
e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
var dataTransfer = e.dataTransfer; _onDragEnter: getDragHandler('dragenter'),
if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
this._trigger( _onDragLeave: getDragHandler('dragleave'),
'dragover',
$.Event('dragover', {delegatedEvent: e})
) !== false) {
e.preventDefault();
dataTransfer.dropEffect = 'copy';
}
},
_initEventHandlers: function () { _initEventHandlers: function () {
if (this._isXHRUpload(this.options)) { if (this._isXHRUpload(this.options)) {
this._on(this.options.dropZone, { this._on(this.options.dropZone, {
dragover: this._onDragOver, 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, { this._on(this.options.pasteZone, {
paste: this._onPaste paste: this._onPaste
@ -1271,7 +1296,7 @@
}, },
_destroyEventHandlers: function () { _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.pasteZone, 'paste');
this._off(this.options.fileInput, 'change'); this._off(this.options.fileInput, 'change');
}, },
@ -1319,10 +1344,13 @@
_initDataAttributes: function () { _initDataAttributes: function () {
var that = this, var that = this,
options = this.options, 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: // Initialize options set via HTML5 data-attributes:
$.each( $.each(
clone.data(), data,
function (key, value) { function (key, value) {
var dataAttributeName = 'data-' + var dataAttributeName = 'data-' +
// Convert camelCase to hyphen-ated key: // 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 * https://github.com/blueimp/jQuery-File-Upload
* *
* Copyright 2011, Sebastian Tschan * Copyright 2011, Sebastian Tschan
@ -9,14 +9,16 @@
* http://www.opensource.org/licenses/MIT * http://www.opensource.org/licenses/MIT
*/ */
/*jslint unparam: true, nomen: true */ /* global define, require, window, document */
/*global define, window, document */
(function (factory) { (function (factory) {
'use strict'; 'use strict';
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module: // Register as an anonymous AMD module:
define(['jquery'], factory); define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS:
factory(require('jquery'));
} else { } else {
// Browser globals: // Browser globals:
factory(window.jQuery); factory(window.jQuery);
@ -27,7 +29,7 @@
// Helper variable to create unique names for the transport iframes: // Helper variable to create unique names for the transport iframes:
var counter = 0; 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.fileInput: a jQuery collection of file input fields
// options.paramName: the parameter name for the file form data, // options.paramName: the parameter name for the file form data,
// overrides the name property of the file input field(s), // 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, // options.formData: an array of objects with name and value properties,
// equivalent to the return data of .serializeArray(), e.g.: // equivalent to the return data of .serializeArray(), e.g.:
// [{name: 'a', value: 1}, {name: 'b', value: 2}] // [{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) { $.ajaxTransport('iframe', function (options) {
if (options.async && (options.type === 'POST' || options.type === 'GET')) { if (options.async) {
var form, // javascript:false as initial iframe src
iframe; // prevents warning popups on HTTPS in IE6:
/*jshint scripturl: true */
var initialIframeSrc = options.initialIframeSrc || 'javascript:false;',
/*jshint scripturl: false */
form,
iframe,
addParamChar;
return { return {
send: function (_, completeCallback) { send: function (_, completeCallback) {
form = $('<form style="display:none;"></form>'); form = $('<form style="display:none;"></form>');
form.attr('accept-charset', options.formAcceptCharset); form.attr('accept-charset', options.formAcceptCharset);
// javascript:false as initial iframe src addParamChar = /\?/.test(options.url) ? '&' : '?';
// prevents warning popups on HTTPS in IE6. // 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 // IE versions below IE8 cannot set the name property of
// elements that have already been added to the DOM, // elements that have already been added to the DOM,
// so we set the name along with the iframe HTML markup: // so we set the name along with the iframe HTML markup:
counter += 1;
iframe = $( iframe = $(
'<iframe src="javascript:false;" name="iframe-transport-' + '<iframe src="' + initialIframeSrc +
(counter += 1) + '"></iframe>' '" name="iframe-transport-' + counter + '"></iframe>'
).bind('load', function () { ).bind('load', function () {
var fileInputClones, var fileInputClones,
paramNames = $.isArray(options.paramName) ? paramNames = $.isArray(options.paramName) ?
@ -81,9 +102,14 @@
); );
// Fix for IE endless progress bar activity bug // Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets): // (happens on form submits to iframe targets):
$('<iframe src="javascript:false;"></iframe>') $('<iframe src="' + initialIframeSrc + '"></iframe>')
.appendTo(form); .appendTo(form);
window.setTimeout(function () {
// Removing the form in a setTimeout call
// allows Chrome's developer tools to display
// the response result
form.remove(); form.remove();
}, 0);
}); });
form form
.prop('target', iframe.prop('name')) .prop('target', iframe.prop('name'))
@ -119,6 +145,8 @@
.prop('enctype', 'multipart/form-data') .prop('enctype', 'multipart/form-data')
// enctype must be set as encoding for IE: // enctype must be set as encoding for IE:
.prop('encoding', 'multipart/form-data'); .prop('encoding', 'multipart/form-data');
// Remove the HTML5 form attribute from the input(s):
options.fileInput.removeAttr('form');
} }
form.submit(); form.submit();
// Insert the file input fields at their original location // Insert the file input fields at their original location
@ -126,7 +154,10 @@
if (fileInputClones && fileInputClones.length) { if (fileInputClones && fileInputClones.length) {
options.fileInput.each(function (index, input) { options.fileInput.each(function (index, input) {
var clone = $(fileInputClones[index]); 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); clone.replaceWith(input);
}); });
} }
@ -140,7 +171,7 @@
// concat is used to avoid the "Script URL" JSLint error: // concat is used to avoid the "Script URL" JSLint error:
iframe iframe
.unbind('load') .unbind('load')
.prop('src', 'javascript'.concat(':false;')); .prop('src', initialIframeSrc);
} }
if (form) { if (form) {
form.remove(); form.remove();
@ -151,20 +182,34 @@
}); });
// The iframe transport returns the iframe content document as response. // 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({ $.ajaxSetup({
converters: { converters: {
'iframe text': function (iframe) { 'iframe text': function (iframe) {
return $(iframe[0].body).text(); return iframe && $(iframe[0].body).text();
}, },
'iframe json': function (iframe) { 'iframe json': function (iframe) {
return $.parseJSON($(iframe[0].body).text()); return iframe && $.parseJSON($(iframe[0].body).text());
}, },
'iframe html': function (iframe) { '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) { '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 * jQuery UI Widget 1.11.1
* https://github.com/blueimp/jQuery-File-Upload * http://jqueryui.com
* *
* Copyright 2014 jQuery Foundation and other contributors * Copyright 2014 jQuery Foundation and other contributors
* Released under the MIT license. * Released under the MIT license.
@ -9,28 +30,28 @@
* http://api.jqueryui.com/jQuery.widget/ * 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, var widget_uuid = 0,
slice = Array.prototype.slice, widget_slice = Array.prototype.slice;
_cleanData = $.cleanData;
$.cleanData = function( elems ) { $.cleanData = (function( orig ) {
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { return function( elems ) {
var events, elem, i;
for ( i = 0; (elem = elems[i]) != null; i++ ) {
try { try {
// Only trigger remove when necessary to save time
events = $._data( elem, "events" );
if ( events && events.remove ) {
$( elem ).triggerHandler( "remove" ); $( elem ).triggerHandler( "remove" );
}
// http://bugs.jquery.com/ticket/8235 // http://bugs.jquery.com/ticket/8235
} catch( e ) {} } catch( e ) {}
} }
_cleanData( elems ); orig( elems );
}; };
})( $.cleanData );
$.widget = function( name, base, prototype ) { $.widget = function( name, base, prototype ) {
var fullName, existingConstructor, constructor, basePrototype, var fullName, existingConstructor, constructor, basePrototype,
@ -143,10 +164,12 @@ $.widget = function( name, base, prototype ) {
} }
$.widget.bridge( name, constructor ); $.widget.bridge( name, constructor );
return constructor;
}; };
$.widget.extend = function( target ) { $.widget.extend = function( target ) {
var input = slice.call( arguments, 1 ), var input = widget_slice.call( arguments, 1 ),
inputIndex = 0, inputIndex = 0,
inputLength = input.length, inputLength = input.length,
key, key,
@ -175,7 +198,7 @@ $.widget.bridge = function( name, object ) {
var fullName = object.prototype.widgetFullName || name; var fullName = object.prototype.widgetFullName || name;
$.fn[ name ] = function( options ) { $.fn[ name ] = function( options ) {
var isMethodCall = typeof options === "string", var isMethodCall = typeof options === "string",
args = slice.call( arguments, 1 ), args = widget_slice.call( arguments, 1 ),
returnValue = this; returnValue = this;
// allow multiple hashes to be passed on init // allow multiple hashes to be passed on init
@ -187,6 +210,10 @@ $.widget.bridge = function( name, object ) {
this.each(function() { this.each(function() {
var methodValue, var methodValue,
instance = $.data( this, fullName ); instance = $.data( this, fullName );
if ( options === "instance" ) {
returnValue = instance;
return false;
}
if ( !instance ) { if ( !instance ) {
return $.error( "cannot call methods on " + name + " prior to initialization; " + return $.error( "cannot call methods on " + name + " prior to initialization; " +
"attempted to call method '" + options + "'" ); "attempted to call method '" + options + "'" );
@ -206,7 +233,10 @@ $.widget.bridge = function( name, object ) {
this.each(function() { this.each(function() {
var instance = $.data( this, fullName ); var instance = $.data( this, fullName );
if ( instance ) { if ( instance ) {
instance.option( options || {} )._init(); instance.option( options || {} );
if ( instance._init ) {
instance._init();
}
} else { } else {
$.data( this, fullName, new object( options, this ) ); $.data( this, fullName, new object( options, this ) );
} }
@ -233,7 +263,7 @@ $.Widget.prototype = {
_createWidget: function( options, element ) { _createWidget: function( options, element ) {
element = $( element || this.defaultElement || this )[ 0 ]; element = $( element || this.defaultElement || this )[ 0 ];
this.element = $( element ); this.element = $( element );
this.uuid = uuid++; this.uuid = widget_uuid++;
this.eventNamespace = "." + this.widgetName + this.uuid; this.eventNamespace = "." + this.widgetName + this.uuid;
this.options = $.widget.extend( {}, this.options = $.widget.extend( {},
this.options, this.options,
@ -276,9 +306,6 @@ $.Widget.prototype = {
// all event bindings should go through this._on() // all event bindings should go through this._on()
this.element this.element
.unbind( this.eventNamespace ) .unbind( this.eventNamespace )
// 1.9 BC for #7810
// TODO remove dual storage
.removeData( this.widgetName )
.removeData( this.widgetFullName ) .removeData( this.widgetFullName )
// support: jquery <1.6.3 // support: jquery <1.6.3
// http://bugs.jquery.com/ticket/9413 // http://bugs.jquery.com/ticket/9413
@ -354,20 +381,23 @@ $.Widget.prototype = {
if ( key === "disabled" ) { if ( key === "disabled" ) {
this.widget() this.widget()
.toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) .toggleClass( this.widgetFullName + "-disabled", !!value );
.attr( "aria-disabled", value );
// If the widget is becoming disabled, then nothing is interactive
if ( value ) {
this.hoverable.removeClass( "ui-state-hover" ); this.hoverable.removeClass( "ui-state-hover" );
this.focusable.removeClass( "ui-state-focus" ); this.focusable.removeClass( "ui-state-focus" );
} }
}
return this; return this;
}, },
enable: function() { enable: function() {
return this._setOption( "disabled", false ); return this._setOptions({ disabled: false });
}, },
disable: function() { disable: function() {
return this._setOption( "disabled", true ); return this._setOptions({ disabled: true });
}, },
_on: function( suppressDisabledCheck, element, handlers ) { _on: function( suppressDisabledCheck, element, handlers ) {
@ -387,7 +417,6 @@ $.Widget.prototype = {
element = this.element; element = this.element;
delegateElement = this.widget(); delegateElement = this.widget();
} else { } else {
// accept selectors, DOM elements
element = delegateElement = $( element ); element = delegateElement = $( element );
this.bindings = this.bindings.add( element ); this.bindings = this.bindings.add( element );
} }
@ -412,7 +441,7 @@ $.Widget.prototype = {
handler.guid || handlerProxy.guid || $.guid++; handler.guid || handlerProxy.guid || $.guid++;
} }
var match = event.match( /^(\w+)\s*(.*)$/ ), var match = event.match( /^([\w:-]*)\s*(.*)$/ ),
eventName = match[1] + instance.eventNamespace, eventName = match[1] + instance.eventNamespace,
selector = match[2]; selector = match[2];
if ( selector ) { if ( selector ) {
@ -527,4 +556,8 @@ $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
}; };
}); });
var widget = $.widget;
})); }));