Merge branch 'tova'

master
Joey Hess 2011-06-16 13:05:05 -04:00
commit 6d80bdda7c
22 changed files with 1612 additions and 147 deletions

View File

@ -6,11 +6,13 @@ use strict;
use IkiWiki 3.00;
sub import {
add_underlay("attachment");
add_underlay("javascript");
add_underlay("jquery");
hook(type => "getsetup", id => "attachment", call => \&getsetup);
hook(type => "checkconfig", id => "attachment", call => \&checkconfig);
hook(type => "formbuilder_setup", id => "attachment", call => \&formbuilder_setup);
hook(type => "formbuilder", id => "attachment", call => \&formbuilder);
hook(type => "formbuilder", id => "attachment", call => \&formbuilder, last => 1);
IkiWiki::loadplugin("filecheck");
}
@ -89,10 +91,20 @@ sub formbuilder_setup (@) {
$form->tmpl_param("field-upload" => '<input name="_submit" type="submit" value="Upload Attachment" />');
$form->tmpl_param("field-link" => '<input name="_submit" type="submit" value="Insert Links" />');
# Add the toggle javascript; the attachments interface uses
# it to toggle visibility.
# Add all the javascript used by the attachments interface.
require IkiWiki::Plugin::toggle;
$form->tmpl_param("javascript" => IkiWiki::Plugin::toggle::include_javascript($params{page}));
my $js=IkiWiki::Plugin::toggle::include_javascript($params{page});
$js.='<link rel="stylesheet" href="'.urlto("ikiwiki/jquery-ui.min.css", $params{page}).'" id="theme">'."\n";
my @jsfiles=qw{jquery.min jquery-ui.min
jquery.tmpl.min jquery.iframe-transport
jquery.fileupload jquery.fileupload-ui
};
foreach my $file (@jsfiles) {
$js.='<script src="'.urlto("ikiwiki/$file.js", $params{page}).
'" type="text/javascript" charset="utf-8"></script>'."\n";
}
$form->tmpl_param("javascript" => $js);
# Start with the attachments interface toggled invisible,
# but if it was used, keep it open.
if ($form->submitted ne "Upload Attachment" &&
@ -103,6 +115,12 @@ sub formbuilder_setup (@) {
else {
$form->tmpl_param("attachments-class" => "toggleable-open");
}
# Save attachments in holding area before previewing so
# they can be seen in the preview.
if ($form->submitted eq "Preview") {
attachments_save($form, $params{session});
}
}
}
@ -114,88 +132,15 @@ sub formbuilder (@) {
return if ! defined $form->field("do") || ($form->field("do") ne "edit" && $form->field("do") ne "create") ;
my $filename=Encode::decode_utf8($q->param('attachment'));
if (defined $filename && length $filename &&
($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) {
my $session=$params{session};
# This is an (apparently undocumented) way to get the name
# of the temp file that CGI writes the upload to.
my $tempfile=$q->tmpFileName($filename);
if (! defined $tempfile || ! length $tempfile) {
# perl 5.8 needs an alternative, awful method
if ($q =~ /HASH/ && exists $q->{'.tmpfiles'}) {
foreach my $key (keys(%{$q->{'.tmpfiles'}})) {
$tempfile=$q->tmpFileName(\$key);
last if defined $tempfile && length $tempfile;
}
}
if (! defined $tempfile || ! length $tempfile) {
error("CGI::tmpFileName failed to return the uploaded file name");
}
}
$filename=IkiWiki::basename($filename);
$filename=~s/.*\\+(.+)/$1/; # hello, windows
$filename=linkpage(IkiWiki::possibly_foolish_untaint(
attachment_location($form->field('page')).
$filename));
if (IkiWiki::file_pruned($filename)) {
error(gettext("bad attachment filename"));
}
# Check that the user is allowed to edit a page with the
# name of the attachment.
IkiWiki::check_canedit($filename, $q, $session);
# And that the attachment itself is acceptable.
check_canattach($session, $filename, $tempfile);
# Needed for fast_file_copy and for rendering below.
require IkiWiki::Render;
# Move the attachment into place.
# Try to use a fast rename; fall back to copying.
IkiWiki::prep_writefile($filename, $config{srcdir});
unlink($config{srcdir}."/".$filename);
if (rename($tempfile, $config{srcdir}."/".$filename)) {
# The temp file has tight permissions; loosen up.
chmod(0666 & ~umask, $config{srcdir}."/".$filename);
}
else {
my $fh=$q->upload('attachment');
if (! defined $fh || ! ref $fh) {
# needed by old CGI versions
$fh=$q->param('attachment');
if (! defined $fh || ! ref $fh) {
# even that doesn't always work,
# fall back to opening the tempfile
$fh=undef;
open($fh, "<", $tempfile) || error("failed to open \"$tempfile\": $!");
}
}
binmode($fh);
writefile($filename, $config{srcdir}, undef, 1, sub {
IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_);
});
}
# Check the attachment in and trigger a wiki refresh.
if ($config{rcs}) {
IkiWiki::rcs_add($filename);
IkiWiki::disable_commit_hook();
IkiWiki::rcs_commit(
file => $filename,
message => gettext("attachment upload"),
token => IkiWiki::rcs_prepedit($filename),
session => $session,
);
IkiWiki::enable_commit_hook();
IkiWiki::rcs_update();
}
IkiWiki::refresh();
IkiWiki::saveindex();
if (defined $filename && length $filename) {
attachment_store($filename, $form, $q, $params{session});
}
elsif ($form->submitted eq "Insert Links") {
if ($form->submitted eq "Save Page") {
attachments_save($form, $params{session});
}
if ($form->submitted eq "Insert Links") {
my $page=quotemeta(Encode::decode_utf8($q->param("page")));
my $add="";
foreach my $f ($q->param("attachment_select")) {
@ -220,6 +165,140 @@ sub formbuilder (@) {
$form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]);
}
sub attachment_holding_location {
my $page=attachment_location(shift);
my $dir=$config{wikistatedir}."/attachments/".
IkiWiki::possibly_foolish_untaint(linkpage($page));
$dir=~s/\/$//;
return $dir;
}
sub is_held_attachment {
my $attachment=shift;
my $f=attachment_holding_location($attachment);
if (-f $f) {
return $f
}
else {
return undef;
}
}
# Stores the attachment in a holding area, not yet in the wiki proper.
sub attachment_store {
my $filename=shift;
my $form=shift;
my $q=shift;
my $session=shift;
# This is an (apparently undocumented) way to get the name
# of the temp file that CGI writes the upload to.
my $tempfile=$q->tmpFileName($filename);
if (! defined $tempfile || ! length $tempfile) {
# perl 5.8 needs an alternative, awful method
if ($q =~ /HASH/ && exists $q->{'.tmpfiles'}) {
foreach my $key (keys(%{$q->{'.tmpfiles'}})) {
$tempfile=$q->tmpFileName(\$key);
last if defined $tempfile && length $tempfile;
}
}
if (! defined $tempfile || ! length $tempfile) {
error("CGI::tmpFileName failed to return the uploaded file name");
}
}
$filename=IkiWiki::basename($filename);
$filename=~s/.*\\+(.+)/$1/; # hello, windows
$filename=IkiWiki::possibly_foolish_untaint(linkpage($filename));
my $dest=attachment_holding_location($form->field('page'));
# Check that the user is allowed to edit the attachment.
my $final_filename=
linkpage(IkiWiki::possibly_foolish_untaint(
attachment_location($form->field('page')))).
$filename;
eval {
if (IkiWiki::file_pruned($final_filename)) {
error(gettext("bad attachment filename"));
}
IkiWiki::check_canedit($final_filename, $q, $session);
# And that the attachment itself is acceptable.
check_canattach($session, $final_filename, $tempfile);
};
if ($@) {
json_response($q, $dest."/".$filename, $@);
error $@;
}
# Move the attachment into holding directory.
# Try to use a fast rename; fall back to copying.
IkiWiki::prep_writefile($filename, $dest);
unlink($dest."/".$filename);
if (rename($tempfile, $dest."/".$filename)) {
# The temp file has tight permissions; loosen up.
chmod(0666 & ~umask, $dest."/".$filename);
}
else {
my $fh=$q->upload('attachment');
if (! defined $fh || ! ref $fh) {
# needed by old CGI versions
$fh=$q->param('attachment');
if (! defined $fh || ! ref $fh) {
# even that doesn't always work,
# fall back to opening the tempfile
$fh=undef;
open($fh, "<", $tempfile) || error("failed to open \"$tempfile\": $!");
}
}
binmode($fh);
require IkiWiki::Render;
writefile($filename, $dest, undef, 1, sub {
IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_);
});
}
json_response($q, $dest."/".$filename, stored_msg());
}
# Save all stored attachments for a page.
sub attachments_save {
my $form=shift;
my $session=shift;
# Move attachments out of holding directory.
my @attachments;
my $dir=attachment_holding_location($form->field('page'));
foreach my $filename (glob("$dir/*")) {
next unless -f $filename;
my $dest=$config{srcdir}."/".
linkpage(IkiWiki::possibly_foolish_untaint(
attachment_location($form->field('page')))).
IkiWiki::basename($filename);
unlink($dest);
rename($filename, $dest);
push @attachments, $dest;
}
return unless @attachments;
require IkiWiki::Render;
IkiWiki::prune($dir);
# Check the attachments in and trigger a wiki refresh.
if ($config{rcs}) {
IkiWiki::rcs_add($_) foreach @attachments;
IkiWiki::disable_commit_hook();
IkiWiki::rcs_commit_staged(
message => gettext("attachment upload"),
session => $session,
);
IkiWiki::enable_commit_hook();
IkiWiki::rcs_update();
}
IkiWiki::refresh();
IkiWiki::saveindex();
}
sub attachment_location ($) {
my $page=shift;
@ -235,23 +314,75 @@ sub attachment_list ($) {
my $page=shift;
my $loc=attachment_location($page);
my @ret;
my $std=sub {
my $file=shift;
my $mtime=shift;
my $date=shift;
my $size=shift;
name => $file,
size => IkiWiki::Plugin::filecheck::humansize($size),
mtime => $date,
mtime_raw => $mtime,
};
# attachments already in the wiki
my %attachments;
foreach my $f (values %pagesources) {
if (! defined pagetype($f) &&
$f=~m/^\Q$loc\E[^\/]+$/) {
push @ret, {
"field-select" => '<input type="checkbox" name="attachment_select" value="'.$f.'" />',
$attachments{$f}={
$std->($f, $IkiWiki::pagemtime{$f}, displaytime($IkiWiki::pagemtime{$f}), (stat($f))[7]),
link => htmllink($page, $page, $f, noimageinline => 1),
size => IkiWiki::Plugin::filecheck::humansize((stat($f))[7]),
mtime => displaytime($IkiWiki::pagemtime{$f}),
mtime_raw => $IkiWiki::pagemtime{$f},
};
}
}
# attachments in holding directory
my $dir=attachment_holding_location($page);
my $heldmsg=gettext("this attachment is not yet saved");
foreach my $file (glob("$dir/*")) {
next unless -f $file;
my $base=IkiWiki::basename($file);
my $f=$loc.$base;
$attachments{$f}={
$std->($f, (stat($file))[9]*2, stored_msg(), (stat(_))[7]),
link => $base,
}
}
# Sort newer attachments to the top of the list, so a newly-added
# attachment appears just before the form used to add it.
return sort { $b->{mtime_raw} <=> $a->{mtime_raw} || $a->{link} cmp $b->{link} } @ret;
# Sort newer attachments to the end of the list.
return sort { $a->{mtime_raw} <=> $b->{mtime_raw} || $a->{link} cmp $b->{link} }
values %attachments;
}
sub stored_msg {
gettext("just uploaded");
}
sub json_response ($$$) {
my $q=shift;
my $filename=shift;
my $stored_msg=shift;
# for the jquery file upload widget
if ($q->Accept("application/json") >= 1.0 &&
grep { /application\/json/i } $q->Accept) {
eval q{use JSON};
error $@ if $@;
print "Content-type: application/json\n\n";
my $size=-s $filename;
print to_json([
{
name => IkiWiki::basename($filename),
size => $size,
humansize => IkiWiki::Plugin::filecheck::humansize($size),
stored_msg => $stored_msg,
}
]);
exit 0;
}
}
1

View File

@ -8,6 +8,7 @@ use IkiWiki 3.00;
sub import {
add_underlay("openid-selector");
add_underlay("jquery");
hook(type => "checkconfig", id => "openid", call => \&checkconfig);
hook(type => "getsetup", id => "openid", call => \&getsetup);
hook(type => "auth", id => "openid", call => \&auth);

View File

@ -117,13 +117,27 @@ sub removal_confirm ($$@) {
my $session=shift;
my $attachment=shift;
my @pages=@_;
# Special case for unsaved attachments.
foreach my $page (@pages) {
if (IkiWiki::Plugin::attachment->can("is_held_attachment")) {
my $f=IkiWiki::Plugin::attachment::is_held_attachment($page);
if (defined $f) {
print STDERR "!! remove $f\n";
require IkiWiki::Render;
IkiWiki::prune($f);
}
}
}
@pages=grep { exists $pagesources{$_} } @pages;
return unless @pages;
foreach my $page (@pages) {
IkiWiki::check_canedit($page, $q, $session);
check_canremove($page, $q, $session);
}
# Save current form state to allow returning to it later
# Save current form state to allow returning to it later
# without losing any edits.
# (But don't save what button was submitted, to avoid
# looping back to here.)
@ -178,10 +192,10 @@ sub formbuilder (@) {
}
sub sessioncgi ($$) {
my $q=shift;
my $q=shift;
if ($q->param("do") eq 'remove') {
my $session=shift;
my $session=shift;
my ($form, $buttons)=confirmation_form($q, $session);
IkiWiki::decode_form_utf8($form);
@ -192,7 +206,7 @@ sub sessioncgi ($$) {
IkiWiki::checksessionexpiry($q, $session, $q->param('sid'));
my @pages=$form->field("page");
# Validate removal by checking that the page exists,
# and that the user is allowed to edit(/remove) it.
my @files;

View File

@ -179,8 +179,15 @@ sub rename_start ($$$$) {
my $attachment=shift;
my $page=shift;
check_canrename($page, $pagesources{$page}, undef, undef,
$q, $session);
# Special case for renaming held attachments; normal checks
# don't apply.
my $held=$attachment &&
IkiWiki::Plugin::attachment->can("is_held_attachment") &&
IkiWiki::Plugin::attachment::is_held_attachment($page);
if (! defined $held) {
check_canrename($page, $pagesources{$page}, undef, undef,
$q, $session);
}
# Save current form state to allow returning to it later
# without losing any edits.
@ -291,13 +298,11 @@ sub sessioncgi ($$) {
elsif ($form->submitted eq 'Rename' && $form->validate) {
IkiWiki::checksessionexpiry($q, $session, $q->param('sid'));
# Queue of rename actions to perfom.
my @torename;
# These untaints are safe because of the checks
# performed in check_canrename later.
my $src=$form->field("page");
my $srcfile=IkiWiki::possibly_foolish_untaint($pagesources{$src});
my $srcfile=IkiWiki::possibly_foolish_untaint($pagesources{$src})
if exists $pagesources{$src};
my $dest=IkiWiki::possibly_foolish_untaint(titlepage($form->field("new_name")));
my $destfile=$dest;
if (! $q->param("attachment")) {
@ -312,6 +317,19 @@ sub sessioncgi ($$) {
$destfile=newpagefile($dest, $type);
}
# Special case for renaming held attachments.
my $held=$q->param("attachment") &&
IkiWiki::Plugin::attachment->can("is_held_attachment") &&
IkiWiki::Plugin::attachment::is_held_attachment($src);
if (defined $held) {
rename($held, IkiWiki::Plugin::attachment::attachment_holding_location($dest));
postrename($session, $src, $dest, $q->param("attachment"))
unless defined $srcfile;
}
# Queue of rename actions to perfom.
my @torename;
push @torename, {
src => $src,
srcfile => $srcfile,

View File

@ -49,7 +49,7 @@ sub gen_wrapper () {
push @envsave, qw{REMOTE_ADDR QUERY_STRING REQUEST_METHOD REQUEST_URI
CONTENT_TYPE CONTENT_LENGTH GATEWAY_INTERFACE
HTTP_COOKIE REMOTE_USER HTTPS REDIRECT_STATUS
HTTP_HOST SERVER_PORT HTTPS
HTTP_HOST SERVER_PORT HTTPS HTTP_ACCEPT
REDIRECT_URL} if $config{cgi};
my $envsave="";
foreach my $var (@envsave) {

10
debian/changelog vendored
View File

@ -2,6 +2,16 @@ ikiwiki (3.20110609) UNRELEASED; urgency=low
* userlist: New plugin, lets admins see a list of users and their info.
* aggregate: Improve checking for too long aggregated filenames.
* Updated to jQuery 1.6.1.
* attachment: Speed up multiple file uploads by storing uploaded files
in a staging area until the page is saved/previewed, rather than
refreshing the site after each upload.
(Sponsored by The TOVA Company.)
* attachment: Files can be dragged into the edit page to upload them.
Multiple file batch upload support. Upload progress bars.
AJAX special effects. Impemented using the jQuery-File-Upload widget.
(If you don't have javascript don't worry, I kept that working too.)
(Sponsored by The TOVA Company.)
-- Joey Hess <joeyh@debian.org> Thu, 09 Jun 2011 10:06:44 -0400

21
debian/copyright vendored
View File

@ -203,10 +203,27 @@ Comment:
From http://code.google.com/p/openid-selector/
License: BSD-2-clause
Files: underlays/openid-selector/ikiwiki/openid/jquery.js
Copyright: © 2005-2008 by John Resig, Branden Aaron & Jörn Zaefferer
Files: underlays/jquery/*
Copyright: © 2005-2011 by John Resig, Branden Aaron & Jörn Zaefferer
© 2011 The Dojo Foundation
License: GPL-2
Files: underlays/attachments/ikiwiki/jquery-ui*
Copyright: © 2008 Paul Bakaus
© 2011 the jQuery UI Authors (http://jqueryui.com/about)
License: GPL-2
Files: underlays/attachments/ikiwiki/jquery.tmpl.min.js
Copyright: © Boris Moore
License: GPL-2
Files: underlays/attachments/ikiwiki/
Copyright: 2010, 2011 Sebastian Tschan
Comment:
blueimp / jQuery-File-Upload widget,
from https://github.com/blueimp/jQuery-File-Upload
License: Expat
Files: underlays/themes/blueview/style.css
Copyright: © 2009,2010 Bernd Zeimetz
© 2008 Yahoo! Inc.

View File

@ -493,3 +493,11 @@ a.openid_large_btn:focus {
.openid_selected {
border: 4px solid #DDD;
}
.fileupload-content .ui-progressbar {
width: 200px;
height: 20px;
}
.fileupload-content .ui-progressbar-value {
background: url(ikiwiki/images/pbar-ani.gif);
}

View File

@ -26,20 +26,50 @@
<TMPL_IF NAME="FIELD-ATTACHMENT">
<a class="toggle" href="#attachments">Attachments</a>
<div class="<TMPL_VAR ATTACHMENTS-CLASS>" id="attachments">
<table>
<tr><td colspan="5"><TMPL_VAR FIELD-ATTACHMENT><TMPL_VAR FIELD-UPLOAD></td></tr>
<div id="fileupload">
<script>
$(function () { $('#fileupload').fileupload(); }); // initialize upload widget
</script>
<script id="template-upload" type="text/x-jquery-tmpl">
<tr class="template-upload{{if error}} ui-state-error{{/if}}">
<td><input type="checkbox" name="attachment_select" value="${name}" />${name}</td>
{{if error}}
<td class="error" colspan="2">failed!</td>
{{else}}
<td class="progress" colspan="2"><div></div></td>
<td class="start"><button>Start</button></td>
{{/if}}
<td class="cancel"><button>Cancel</button></td>
</tr>
</script>
<script id="template-download" type="text/x-jquery-tmpl">
<tr class="template-download{{if error}} ui-state-error{{/if}}">
<td><input type="checkbox" name="attachment_select" value="${name}" />${name}</td>
<td>${humansize}</td>
{{if error}}
<td class="error" colspan="2">failed!</td>
{{else}}
<td>${stored_msg}</td>
{{/if}}
</tr>
</script>
<div class="fileupload-content">
<table class="files">
<TMPL_LOOP NAME="ATTACHMENT_LIST">
<tr><td><TMPL_VAR FIELD-SELECT><TMPL_VAR LINK></td><td><TMPL_VAR SIZE></td><td><TMPL_VAR MTIME></td></tr>
<tr><td><input type="checkbox" name="attachment_select" value="<TMPL_VAR NAME ESCAPE="HTML">" /><TMPL_VAR LINK></td><td><TMPL_VAR SIZE></td><td><TMPL_VAR MTIME></td></tr>
</TMPL_LOOP>
<TMPL_IF NAME="ATTACHMENT_LIST">
<tr><td colspan="2"><TMPL_VAR FIELD-LINK><TMPL_VAR FIELD-RENAME><TMPL_VAR FIELD-REMOVE></td></tr>
</TMPL_IF>
</table>
</div>
<TMPL_VAR FIELD-ATTACHMENT>
<noscript><TMPL_VAR FIELD-UPLOAD></noscript>
<TMPL_IF NAME="ATTACHMENT_LIST">
<TMPL_VAR FIELD-LINK><TMPL_VAR FIELD-RENAME><TMPL_VAR FIELD-REMOVE>
</TMPL_IF>
</div>
</div>
</TMPL_IF>
<TMPL_VAR FORM-END>
<TMPL_VAR WMD_PREVIEW>
<TMPL_IF NAME="PAGE_PREVIEW">
<hr />
<div class="header">

View File

@ -1,4 +1,4 @@
<script type="text/javascript" src="ikiwiki/openid/jquery.js"></script>
<script type="text/javascript" src="ikiwiki/jquery.min.js"></script>
<script type="text/javascript" src="ikiwiki/openid/openid-jquery.js"></script>
<script type="text/javascript">
$(document).ready(function() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,357 @@
/*
* jQuery File Upload User Interface Plugin 5.0.13
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2010, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://creativecommons.org/licenses/MIT/
*/
/*jslint nomen: true, unparam: true, regexp: true */
/*global window, document, URL, webkitURL, FileReader, jQuery */
(function ($) {
'use strict';
// The UI version extends the basic fileupload widget and adds
// a complete user interface based on the given upload/download
// templates.
$.widget('blueimpUI.fileupload', $.blueimp.fileupload, {
options: {
// By default, files added to the widget are uploaded as soon
// as the user clicks on the start buttons. To enable automatic
// uploads, set the following option to true:
autoUpload: true,
// The file upload template that is given as first argument to the
// jQuery.tmpl method to render the file uploads:
uploadTemplate: $('#template-upload'),
// The file download template, that is given as first argument to the
// jQuery.tmpl method to render the file downloads:
downloadTemplate: $('#template-download'),
// The expected data type of the upload response, sets the dataType
// option of the $.ajax upload requests:
dataType: 'json',
// The add callback is invoked as soon as files are added to the fileupload
// widget (via file input selection, drag & drop or add API call).
// See the basic file upload widget for more information:
add: function (e, data) {
var that = $(this).data('fileupload');
data.isAdjusted = true;
data.isValidated = that._validate(data.files);
data.context = that._renderUpload(data.files)
.appendTo($(this).find('.files')).fadeIn(function () {
// Fix for IE7 and lower:
$(this).show();
}).data('data', data);
if ((that.options.autoUpload || data.autoUpload) &&
data.isValidated) {
data.jqXHR = data.submit();
}
},
// Callback for the start of each file upload request:
send: function (e, data) {
if (!data.isValidated) {
var that = $(this).data('fileupload');
if (!that._validate(data.files)) {
return false;
}
}
if (data.context && data.dataType &&
data.dataType.substr(0, 6) === 'iframe') {
// Iframe Transport does not support progress events.
// In lack of an indeterminate progress bar, we set
// the progress to 100%, showing the full animated bar:
data.context.find('.ui-progressbar').progressbar(
'value',
parseInt(100, 10)
);
}
},
// Callback for successful uploads:
done: function (e, data) {
var that = $(this).data('fileupload');
if (data.context) {
data.context.each(function (index) {
var file = ($.isArray(data.result) &&
data.result[index]) || {error: 'emptyResult'};
$(this).fadeOut(function () {
that._renderDownload([file])
.css('display', 'none')
.replaceAll(this)
.fadeIn(function () {
// Fix for IE7 and lower:
$(this).show();
});
});
});
} else {
that._renderDownload(data.result)
.css('display', 'none')
.appendTo($(this).find('.files'))
.fadeIn(function () {
// Fix for IE7 and lower:
$(this).show();
});
}
},
// Callback for failed (abort or error) uploads:
fail: function (e, data) {
var that = $(this).data('fileupload');
if (data.context) {
data.context.each(function (index) {
$(this).fadeOut(function () {
if (data.errorThrown !== 'abort') {
var file = data.files[index];
file.error = file.error || data.errorThrown
|| true;
that._renderDownload([file])
.css('display', 'none')
.replaceAll(this)
.fadeIn(function () {
// Fix for IE7 and lower:
$(this).show();
});
} else {
data.context.remove();
}
});
});
} else if (data.errorThrown !== 'abort') {
data.context = that._renderUpload(data.files)
.css('display', 'none')
.appendTo($(this).find('.files'))
.fadeIn(function () {
// Fix for IE7 and lower:
$(this).show();
}).data('data', data);
}
},
// Callback for upload progress events:
progress: function (e, data) {
if (data.context) {
data.context.find('.ui-progressbar').progressbar(
'value',
parseInt(data.loaded / data.total * 100, 10)
);
}
},
// Callback for global upload progress events:
progressall: function (e, data) {
$(this).find('.fileupload-progressbar').progressbar(
'value',
parseInt(data.loaded / data.total * 100, 10)
);
},
// Callback for uploads start, equivalent to the global ajaxStart event:
start: function () {
$(this).find('.fileupload-progressbar')
.progressbar('value', 0).fadeIn();
},
// Callback for uploads stop, equivalent to the global ajaxStop event:
stop: function () {
$(this).find('.fileupload-progressbar').fadeOut();
},
},
_createObjectURL: function (file) {
var undef = 'undefined',
urlAPI = (typeof window.createObjectURL !== undef && window) ||
(typeof URL !== undef && URL) ||
(typeof webkitURL !== undef && webkitURL);
return urlAPI ? urlAPI.createObjectURL(file) : false;
},
_revokeObjectURL: function (url) {
var undef = 'undefined',
urlAPI = (typeof window.revokeObjectURL !== undef && window) ||
(typeof URL !== undef && URL) ||
(typeof webkitURL !== undef && webkitURL);
return urlAPI ? urlAPI.revokeObjectURL(url) : false;
},
// Link handler, that allows to download files
// by drag & drop of the links to the desktop:
_enableDragToDesktop: function () {
var link = $(this),
url = link.prop('href'),
name = decodeURIComponent(url.split('/').pop())
.replace(/:/g, '-'),
type = 'application/octet-stream';
link.bind('dragstart', function (e) {
try {
e.originalEvent.dataTransfer.setData(
'DownloadURL',
[type, name, url].join(':')
);
} catch (err) {}
});
},
_hasError: function (file) {
if (file.error) {
return file.error;
}
return null;
},
_validate: function (files) {
var that = this,
valid;
$.each(files, function (index, file) {
file.error = that._hasError(file);
valid = !file.error;
});
return valid;
},
_uploadTemplateHelper: function (file) {
return file;
},
_renderUploadTemplate: function (files) {
var that = this;
return $.tmpl(
this.options.uploadTemplate,
$.map(files, function (file) {
return that._uploadTemplateHelper(file);
})
);
},
_renderUpload: function (files) {
var that = this,
options = this.options,
tmpl = this._renderUploadTemplate(files);
if (!(tmpl instanceof $)) {
return $();
}
tmpl.css('display', 'none');
// .slice(1).remove().end().first() removes all but the first
// element and selects only the first for the jQuery collection:
tmpl.find('.progress div').slice(1).remove().end().first()
.progressbar();
tmpl.find('.start button').slice(
this.options.autoUpload ? 0 : 1
).remove().end().first()
.button({
text: false,
icons: {primary: 'ui-icon-circle-arrow-e'}
});
tmpl.find('.cancel button').slice(1).remove().end().first()
.button({
text: false,
icons: {primary: 'ui-icon-cancel'}
});
return tmpl;
},
_downloadTemplateHelper: function (file) {
return file;
},
_renderDownloadTemplate: function (files) {
var that = this;
return $.tmpl(
this.options.downloadTemplate,
$.map(files, function (file) {
return that._downloadTemplateHelper(file);
})
);
},
_renderDownload: function (files) {
var tmpl = this._renderDownloadTemplate(files);
if (!(tmpl instanceof $)) {
return $();
}
tmpl.css('display', 'none');
tmpl.find('a').each(this._enableDragToDesktop);
return tmpl;
},
_startHandler: function (e) {
e.preventDefault();
var tmpl = $(this).closest('.template-upload'),
data = tmpl.data('data');
if (data && data.submit && !data.jqXHR) {
data.jqXHR = data.submit();
$(this).fadeOut();
}
},
_cancelHandler: function (e) {
e.preventDefault();
var tmpl = $(this).closest('.template-upload'),
data = tmpl.data('data') || {};
if (!data.jqXHR) {
data.errorThrown = 'abort';
e.data.fileupload._trigger('fail', e, data);
} else {
data.jqXHR.abort();
}
},
_initEventHandlers: function () {
$.blueimp.fileupload.prototype._initEventHandlers.call(this);
var filesList = this.element.find('.files'),
eventData = {fileupload: this};
filesList.find('.start button')
.live(
'click.' + this.options.namespace,
eventData,
this._startHandler
);
filesList.find('.cancel button')
.live(
'click.' + this.options.namespace,
eventData,
this._cancelHandler
);
},
_destroyEventHandlers: function () {
var filesList = this.element.find('.files');
filesList.find('.start button')
.die('click.' + this.options.namespace);
filesList.find('.cancel button')
.die('click.' + this.options.namespace);
$.blueimp.fileupload.prototype._destroyEventHandlers.call(this);
},
_initTemplates: function () {
// Handle cases where the templates are defined
// after the widget library has been included:
if (this.options.uploadTemplate instanceof $ &&
!this.options.uploadTemplate.length) {
this.options.uploadTemplate = $(
this.options.uploadTemplate.selector
);
}
if (this.options.downloadTemplate instanceof $ &&
!this.options.downloadTemplate.length) {
this.options.downloadTemplate = $(
this.options.downloadTemplate.selector
);
}
},
_create: function () {
$.blueimp.fileupload.prototype._create.call(this);
this._initTemplates();
},
enable: function () {
$.blueimp.fileupload.prototype.enable.call(this);
},
disable: function () {
$.blueimp.fileupload.prototype.disable.call(this);
}
});
}(jQuery));

View File

@ -0,0 +1,720 @@
/*
* jQuery File Upload Plugin 5.0.2
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2010, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://creativecommons.org/licenses/MIT/
*/
/*jslint nomen: true, unparam: true, regexp: true */
/*global document, XMLHttpRequestUpload, Blob, File, FormData, location, jQuery */
(function ($) {
'use strict';
// The fileupload widget listens for change events on file input fields
// defined via fileInput setting and drop events of the given dropZone.
// In addition to the default jQuery Widget methods, the fileupload widget
// exposes the "add" and "send" methods, to add or directly send files
// using the fileupload API.
// By default, files added via file input selection, drag & drop or
// "add" method are uploaded immediately, but it is possible to override
// the "add" callback option to queue file uploads.
$.widget('blueimp.fileupload', {
options: {
// The namespace used for event handler binding on the dropZone and
// fileInput collections.
// If not set, the name of the widget ("fileupload") is used.
namespace: undefined,
// The drop target collection, by the default the complete document.
// Set to null or an empty collection to disable drag & drop support:
dropZone: $(document),
// The file input field collection, that is listened for change events.
// If undefined, it is set to the file input fields inside
// of the widget element on plugin initialization.
// Set to null or an empty collection to disable the change listener.
fileInput: undefined,
// By default, the file input field is replaced with a clone after
// each input field change event. This is required for iframe transport
// queues and allows change events to be fired for the same file
// selection, but can be disabled by setting the following option to false:
replaceFileInput: true,
// The parameter name for the file form data (the request argument name).
// If undefined or empty, the name property of the file input field is
// used, or "files[]" if the file input name property is also empty:
paramName: undefined,
// By default, each file of a selection is uploaded using an individual
// request for XHR type uploads. Set to false to upload file
// selections in one request each:
singleFileUploads: true,
// Set the following option to true to issue all file upload requests
// in a sequential order:
sequentialUploads: false,
// Set the following option to true to force iframe transport uploads:
forceIframeTransport: false,
// By default, XHR file uploads are sent as multipart/form-data.
// The iframe transport is always using multipart/form-data.
// Set to false to enable non-multipart XHR uploads:
multipart: true,
// To upload large files in smaller chunks, set the following option
// to a preferred maximum chunk size. If set to 0, null or undefined,
// or the browser does not support the required Blob API, files will
// be uploaded as a whole.
maxChunkSize: undefined,
// When a non-multipart upload or a chunked multipart upload has been
// aborted, this option can be used to resume the upload by setting
// it to the size of the already uploaded bytes. This option is most
// useful when modifying the options object inside of the "add" or
// "send" callbacks, as the options are cloned for each file upload.
uploadedBytes: undefined,
// By default, failed (abort or error) file uploads are removed from the
// global progress calculation. Set the following option to false to
// prevent recalculating the global progress data:
recalculateProgress: true,
// Additional form data to be sent along with the file uploads can be set
// using this option, which accepts an array of objects with name and
// value properties, a function returning such an array, a FormData
// object (for XHR file uploads), or a simple object.
// The form of the first fileInput is given as parameter to the function:
formData: function (form) {
return form.serializeArray();
},
// The add callback is invoked as soon as files are added to the fileupload
// widget (via file input selection, drag & drop or add API call).
// If the singleFileUploads option is enabled, this callback will be
// called once for each file in the selection for XHR file uplaods, else
// once for each file selection.
// The upload starts when the submit method is invoked on the data parameter.
// The data object contains a files property holding the added files
// and allows to override plugin options as well as define ajax settings.
// Listeners for this callback can also be bound the following way:
// .bind('fileuploadadd', func);
// data.submit() returns a Promise object and allows to attach additional
// handlers using jQuery's Deferred callbacks:
// data.submit().done(func).fail(func).always(func);
add: function (e, data) {
data.submit();
},
// Other callbacks:
// Callback for the start of each file upload request:
// send: function (e, data) {}, // .bind('fileuploadsend', func);
// Callback for successful uploads:
// done: function (e, data) {}, // .bind('fileuploaddone', func);
// Callback for failed (abort or error) uploads:
// fail: function (e, data) {}, // .bind('fileuploadfail', func);
// Callback for completed (success, abort or error) requests:
// always: function (e, data) {}, // .bind('fileuploadalways', func);
// Callback for upload progress events:
// progress: function (e, data) {}, // .bind('fileuploadprogress', func);
// Callback for global upload progress events:
// progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
// Callback for uploads start, equivalent to the global ajaxStart event:
// start: function (e) {}, // .bind('fileuploadstart', func);
// Callback for uploads stop, equivalent to the global ajaxStop event:
// stop: function (e) {}, // .bind('fileuploadstop', func);
// Callback for change events of the fileInput collection:
// change: function (e, data) {}, // .bind('fileuploadchange', func);
// Callback for drop events of the dropZone collection:
// drop: function (e, data) {}, // .bind('fileuploaddrop', func);
// Callback for dragover events of the dropZone collection:
// dragover: function (e) {}, // .bind('fileuploaddragover', func);
// The plugin options are used as settings object for the ajax calls.
// The following are jQuery ajax settings required for the file uploads:
processData: false,
contentType: false,
cache: false
},
// A list of options that require a refresh after assigning a new value:
_refreshOptionsList: ['namespace', 'dropZone', 'fileInput'],
_isXHRUpload: function (options) {
var undef = 'undefined';
return !options.forceIframeTransport &&
typeof XMLHttpRequestUpload !== undef && typeof File !== undef &&
(!options.multipart || typeof FormData !== undef);
},
_getFormData: function (options) {
var formData;
if (typeof options.formData === 'function') {
return options.formData(options.form);
} else if ($.isArray(options.formData)) {
return options.formData;
} else if (options.formData) {
formData = [];
$.each(options.formData, function (name, value) {
formData.push({name: name, value: value});
});
return formData;
}
return [];
},
_getTotal: function (files) {
var total = 0;
$.each(files, function (index, file) {
total += file.size || 1;
});
return total;
},
_onProgress: function (e, data) {
if (e.lengthComputable) {
var total = data.total || this._getTotal(data.files),
loaded = parseInt(
e.loaded / e.total * (data.chunkSize || total),
10
) + (data.uploadedBytes || 0);
this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
data.lengthComputable = true;
data.loaded = loaded;
data.total = total;
// Trigger a custom progress event with a total data property set
// to the file size(s) of the current upload and a loaded data
// property calculated accordingly:
this._trigger('progress', e, data);
// Trigger a global progress event for all current file uploads,
// including ajax calls queued for sequential file uploads:
this._trigger('progressall', e, {
lengthComputable: true,
loaded: this._loaded,
total: this._total
});
}
},
_initProgressListener: function (options) {
var that = this,
xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
// Accesss to the native XHR object is required to add event listeners
// for the upload progress event:
if (xhr.upload && xhr.upload.addEventListener) {
xhr.upload.addEventListener('progress', function (e) {
that._onProgress(e, options);
}, false);
options.xhr = function () {
return xhr;
};
}
},
_initXHRData: function (options) {
var formData,
file = options.files[0];
if (!options.multipart || options.blob) {
// For non-multipart uploads and chunked uploads,
// file meta data is not part of the request body,
// so we transmit this data as part of the HTTP headers.
// For cross domain requests, these headers must be allowed
// via Access-Control-Allow-Headers or removed using
// the beforeSend callback:
options.headers = $.extend(options.headers, {
'X-File-Name': file.name,
'X-File-Type': file.type,
'X-File-Size': file.size
});
if (!options.blob) {
// Non-chunked non-multipart upload:
options.contentType = file.type;
options.data = file;
} else if (!options.multipart) {
// Chunked non-multipart upload:
options.contentType = 'application/octet-stream';
options.data = options.blob;
}
}
if (options.multipart && typeof FormData !== 'undefined') {
if (options.formData instanceof FormData) {
formData = options.formData;
} else {
formData = new FormData();
$.each(this._getFormData(options), function (index, field) {
formData.append(field.name, field.value);
});
}
if (options.blob) {
formData.append(options.paramName, options.blob);
} else {
$.each(options.files, function (index, file) {
// File objects are also Blob instances.
// This check allows the tests to run with
// dummy objects:
if (file instanceof Blob) {
formData.append(options.paramName, file);
}
});
}
options.data = formData;
}
// Blob reference is not needed anymore, free memory:
options.blob = null;
},
_initIframeSettings: function (options) {
// Setting the dataType to iframe enables the iframe transport:
options.dataType = 'iframe ' + (options.dataType || '');
// The iframe transport accepts a serialized array as form data:
options.formData = this._getFormData(options);
},
_initDataSettings: function (options) {
if (this._isXHRUpload(options)) {
if (!this._chunkedUpload(options, true)) {
if (!options.data) {
this._initXHRData(options);
}
this._initProgressListener(options);
}
} else {
this._initIframeSettings(options);
}
},
_initFormSettings: function (options) {
// Retrieve missing options from the input field and the
// associated form, if available:
if (!options.form || !options.form.length) {
options.form = $(options.fileInput.prop('form'));
}
if (!options.paramName) {
options.paramName = options.fileInput.prop('name') ||
'files[]';
}
if (!options.url) {
options.url = options.form.prop('action') || location.href;
}
// The HTTP request method must be "POST" or "PUT":
options.type = (options.type || options.form.prop('method') || '')
.toUpperCase();
if (options.type !== 'POST' && options.type !== 'PUT') {
options.type = 'POST';
}
},
_getAJAXSettings: function (data) {
var options = $.extend({}, this.options, data);
this._initFormSettings(options);
this._initDataSettings(options);
return options;
},
// Maps jqXHR callbacks to the equivalent
// methods of the given Promise object:
_enhancePromise: function (promise) {
promise.success = promise.done;
promise.error = promise.fail;
promise.complete = promise.always;
return promise;
},
// Creates and returns a Promise object enhanced with
// the jqXHR methods abort, success, error and complete:
_getXHRPromise: function (resolveOrReject, context, args) {
var dfd = $.Deferred(),
promise = dfd.promise();
context = context || this.options.context || promise;
if (resolveOrReject === true) {
dfd.resolveWith(context, args);
} else if (resolveOrReject === false) {
dfd.rejectWith(context, args);
}
promise.abort = dfd.promise;
return this._enhancePromise(promise);
},
// Uploads a file in multiple, sequential requests
// by splitting the file up in multiple blob chunks.
// If the second parameter is true, only tests if the file
// should be uploaded in chunks, but does not invoke any
// upload requests:
_chunkedUpload: function (options, testOnly) {
var that = this,
file = options.files[0],
fs = file.size,
ub = options.uploadedBytes = options.uploadedBytes || 0,
mcs = options.maxChunkSize || fs,
// Use the Blob methods with the slice implementation
// according to the W3C Blob API specification:
slice = file.webkitSlice || file.mozSlice || file.slice,
upload,
n,
jqXHR,
pipe;
if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
options.data) {
return false;
}
if (testOnly) {
return true;
}
if (ub >= fs) {
file.error = 'uploadedBytes';
return this._getXHRPromise(false);
}
// n is the number of blobs to upload,
// calculated via filesize, uploaded bytes and max chunk size:
n = Math.ceil((fs - ub) / mcs);
// The chunk upload method accepting the chunk number as parameter:
upload = function (i) {
if (!i) {
return that._getXHRPromise(true);
}
// Upload the blobs in sequential order:
return upload(i -= 1).pipe(function () {
// Clone the options object for each chunk upload:
var o = $.extend({}, options);
o.blob = slice.call(
file,
ub + i * mcs,
ub + (i + 1) * mcs
);
// Store the current chunk size, as the blob itself
// will be dereferenced after data processing:
o.chunkSize = o.blob.size;
// Process the upload data (the blob and potential form data):
that._initXHRData(o);
// Add progress listeners for this chunk upload:
that._initProgressListener(o);
jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context))
.done(function () {
// Create a progress event if upload is done and
// no progress event has been invoked for this chunk:
if (!o.loaded) {
that._onProgress($.Event('progress', {
lengthComputable: true,
loaded: o.chunkSize,
total: o.chunkSize
}), o);
}
options.uploadedBytes = o.uploadedBytes
+= o.chunkSize;
});
return jqXHR;
});
};
// Return the piped Promise object, enhanced with an abort method,
// which is delegated to the jqXHR object of the current upload,
// and jqXHR callbacks mapped to the equivalent Promise methods:
pipe = upload(n);
pipe.abort = function () {
return jqXHR.abort();
};
return this._enhancePromise(pipe);
},
_beforeSend: function (e, data) {
if (this._active === 0) {
// the start callback is triggered when an upload starts
// and no other uploads are currently running,
// equivalent to the global ajaxStart event:
this._trigger('start');
}
this._active += 1;
// Initialize the global progress values:
this._loaded += data.uploadedBytes || 0;
this._total += this._getTotal(data.files);
},
_onDone: function (result, textStatus, jqXHR, options) {
if (!this._isXHRUpload(options)) {
// Create a progress event for each iframe load:
this._onProgress($.Event('progress', {
lengthComputable: true,
loaded: 1,
total: 1
}), options);
}
options.result = result;
options.textStatus = textStatus;
options.jqXHR = jqXHR;
this._trigger('done', null, options);
},
_onFail: function (jqXHR, textStatus, errorThrown, options) {
options.jqXHR = jqXHR;
options.textStatus = textStatus;
options.errorThrown = errorThrown;
this._trigger('fail', null, options);
if (options.recalculateProgress) {
// Remove the failed (error or abort) file upload from
// the global progress calculation:
this._loaded -= options.loaded || options.uploadedBytes || 0;
this._total -= options.total || this._getTotal(options.files);
}
},
_onAlways: function (result, textStatus, jqXHR, errorThrown, options) {
this._active -= 1;
options.result = result;
options.textStatus = textStatus;
options.jqXHR = jqXHR;
options.errorThrown = errorThrown;
this._trigger('always', null, options);
if (this._active === 0) {
// The stop callback is triggered when all uploads have
// been completed, equivalent to the global ajaxStop event:
this._trigger('stop');
// Reset the global progress values:
this._loaded = this._total = 0;
}
},
_onSend: function (e, data) {
var that = this,
jqXHR,
pipe,
options = that._getAJAXSettings(data),
send = function (resolve, args) {
jqXHR = jqXHR || (
(resolve !== false &&
that._trigger('send', e, options) !== false &&
(that._chunkedUpload(options) || $.ajax(options))) ||
that._getXHRPromise(false, options.context, args)
).done(function (result, textStatus, jqXHR) {
that._onDone(result, textStatus, jqXHR, options);
}).fail(function (jqXHR, textStatus, errorThrown) {
that._onFail(jqXHR, textStatus, errorThrown, options);
}).always(function (a1, a2, a3) {
if (!a3 || typeof a3 === 'string') {
that._onAlways(undefined, a2, a1, a3, options);
} else {
that._onAlways(a1, a2, a3, undefined, options);
}
});
return jqXHR;
};
this._beforeSend(e, options);
if (this.options.sequentialUploads) {
// Return the piped Promise object, enhanced with an abort method,
// which is delegated to the jqXHR object of the current upload,
// and jqXHR callbacks mapped to the equivalent Promise methods:
pipe = (this._sequence = this._sequence.pipe(send, send));
pipe.abort = function () {
if (!jqXHR) {
return send(false, [undefined, 'abort', 'abort']);
}
return jqXHR.abort();
};
return this._enhancePromise(pipe);
}
return send();
},
_onAdd: function (e, data) {
var that = this,
result = true,
options = $.extend({}, this.options, data);
if (options.singleFileUploads && this._isXHRUpload(options)) {
$.each(data.files, function (index, file) {
var newData = $.extend({}, data, {files: [file]});
newData.submit = function () {
return that._onSend(e, newData);
};
return (result = that._trigger('add', e, newData));
});
return result;
} else if (data.files.length) {
data = $.extend({}, data);
data.submit = function () {
return that._onSend(e, data);
};
return this._trigger('add', e, data);
}
},
// File Normalization for Gecko 1.9.1 (Firefox 3.5) support:
_normalizeFile: function (index, file) {
if (file.name === undefined && file.size === undefined) {
file.name = file.fileName;
file.size = file.fileSize;
}
},
_replaceFileInput: function (input) {
var inputClone = input.clone(true);
$('<form></form>').append(inputClone)[0].reset();
// Detaching allows to insert the fileInput on another form
// without loosing the file input value:
input.after(inputClone).detach();
// Replace the original file input element in the fileInput
// collection with the clone, which has been copied including
// event handlers:
this.options.fileInput = this.options.fileInput.map(function (i, el) {
if (el === input[0]) {
return inputClone[0];
}
return el;
});
},
_onChange: function (e) {
var that = e.data.fileupload,
data = {
files: $.each($.makeArray(e.target.files), that._normalizeFile),
fileInput: $(e.target),
form: $(e.target.form)
};
if (!data.files.length) {
// If the files property is not available, the browser does not
// support the File API and we add a pseudo File object with
// the input value as name with path information removed:
data.files = [{name: e.target.value.replace(/^.*\\/, '')}];
}
// Store the form reference as jQuery data for other event handlers,
// as the form property is not available after replacing the file input:
if (data.form.length) {
data.fileInput.data('blueimp.fileupload.form', data.form);
} else {
data.form = data.fileInput.data('blueimp.fileupload.form');
}
if (that.options.replaceFileInput) {
that._replaceFileInput(data.fileInput);
}
if (that._trigger('change', e, data) === false ||
that._onAdd(e, data) === false) {
return false;
}
},
_onDrop: function (e) {
var that = e.data.fileupload,
dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
data = {
files: $.each(
$.makeArray(dataTransfer && dataTransfer.files),
that._normalizeFile
)
};
if (that._trigger('drop', e, data) === false ||
that._onAdd(e, data) === false) {
return false;
}
e.preventDefault();
},
_onDragOver: function (e) {
var that = e.data.fileupload,
dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
if (that._trigger('dragover', e) === false) {
return false;
}
if (dataTransfer) {
dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy';
}
e.preventDefault();
},
_initEventHandlers: function () {
var ns = this.options.namespace || this.name;
this.options.dropZone
.bind('dragover.' + ns, {fileupload: this}, this._onDragOver)
.bind('drop.' + ns, {fileupload: this}, this._onDrop);
this.options.fileInput
.bind('change.' + ns, {fileupload: this}, this._onChange);
},
_destroyEventHandlers: function () {
var ns = this.options.namespace || this.name;
this.options.dropZone
.unbind('dragover.' + ns, this._onDragOver)
.unbind('drop.' + ns, this._onDrop);
this.options.fileInput
.unbind('change.' + ns, this._onChange);
},
_beforeSetOption: function (key, value) {
this._destroyEventHandlers();
},
_afterSetOption: function (key, value) {
var options = this.options;
if (!options.fileInput) {
options.fileInput = $();
}
if (!options.dropZone) {
options.dropZone = $();
}
this._initEventHandlers();
},
_setOption: function (key, value) {
var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
if (refresh) {
this._beforeSetOption(key, value);
}
$.Widget.prototype._setOption.call(this, key, value);
if (refresh) {
this._afterSetOption(key, value);
}
},
_create: function () {
var options = this.options;
if (options.fileInput === undefined) {
options.fileInput = this.element.is('input:file') ?
this.element : this.element.find('input:file');
} else if (!options.fileInput) {
options.fileInput = $();
}
if (!options.dropZone) {
options.dropZone = $();
}
this._sequence = this._getXHRPromise(true);
this._active = this._loaded = this._total = 0;
this._initEventHandlers();
},
destroy: function () {
this._destroyEventHandlers();
$.Widget.prototype.destroy.call(this);
},
enable: function () {
$.Widget.prototype.enable.call(this);
this._initEventHandlers();
},
disable: function () {
this._destroyEventHandlers();
$.Widget.prototype.disable.call(this);
},
// This method is exposed to the widget API and allows adding files
// using the fileupload API. The data parameter accepts an object which
// must have a files property and can contain additional options:
// .fileupload('add', {files: filesList});
add: function (data) {
if (!data || this.options.disabled) {
return;
}
data.files = $.each($.makeArray(data.files), this._normalizeFile);
this._onAdd(null, data);
},
// This method is exposed to the widget API and allows sending files
// using the fileupload API. The data parameter accepts an object which
// must have a files property and can contain additional options:
// .fileupload('send', {files: filesList});
// The method returns a Promise object for the file upload call.
send: function (data) {
if (data && !this.options.disabled) {
data.files = $.each($.makeArray(data.files), this._normalizeFile);
if (data.files.length) {
return this._onSend(null, data);
}
}
return this._getXHRPromise(false, data && data.context);
}
});
}(jQuery));

View File

@ -0,0 +1,133 @@
/*
* jQuery Iframe Transport Plugin 1.1
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://creativecommons.org/licenses/MIT/
*/
/*jslint unparam: true */
/*global jQuery */
(function ($) {
'use strict';
// Helper variable to create unique names for the transport iframes:
var counter = 0;
// The iframe transport accepts two additional options:
// options.fileInput: a jQuery collection of file input fields
// 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}]
$.ajaxTransport('iframe', function (options, originalOptions, jqXHR) {
if (options.type === 'POST' || options.type === 'GET') {
var form,
iframe;
return {
send: function (headers, completeCallback) {
form = $('<form style="display:none;"></form>');
// javascript:false as initial iframe src
// prevents warning popups on HTTPS in IE6.
// IE versions below IE8 cannot set the name property of
// elements that have already been added to the DOM,
// so we set the name along with the iframe HTML markup:
iframe = $(
'<iframe src="javascript:false;" name="iframe-transport-' +
(counter += 1) + '"></iframe>'
).bind('load', function () {
var fileInputClones;
iframe
.unbind('load')
.bind('load', function () {
// The complete callback returns the
// iframe content document as response object:
completeCallback(
200,
'success',
{'iframe': iframe.contents()}
);
// Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets):
$('<iframe src="javascript:false;"></iframe>')
.appendTo(form);
form.remove();
});
form
.prop('target', iframe.prop('name'))
.prop('action', options.url)
.prop('method', options.type);
if (options.formData) {
$.each(options.formData, function (index, field) {
$('<input type="hidden"/>')
.prop('name', field.name)
.val(field.value)
.appendTo(form);
});
}
if (options.fileInput && options.fileInput.length &&
options.type === 'POST') {
fileInputClones = options.fileInput.clone();
// Insert a clone for each file input field:
options.fileInput.after(function (index) {
return fileInputClones[index];
});
// Appending the file input fields to the hidden form
// removes them from their original location:
form
.append(options.fileInput)
.prop('enctype', 'multipart/form-data')
// enctype must be set as encoding for IE:
.prop('encoding', 'multipart/form-data');
}
form.submit();
// Insert the file input fields at their original location
// by replacing the clones with the originals:
if (fileInputClones && fileInputClones.length) {
options.fileInput.each(function (index, input) {
$(fileInputClones[index]).replaceWith(input);
});
}
});
form.append(iframe).appendTo('body');
},
abort: function () {
if (iframe) {
// javascript:false as iframe src aborts the request
// and prevents warning popups on HTTPS in IE6.
// concat is used to avoid the "Script URL" JSLint error:
iframe
.unbind('load')
.prop('src', 'javascript'.concat(':false;'));
}
if (form) {
form.remove();
}
}
};
}
});
// The iframe transport returns the iframe content document as response.
// The following adds converters from iframe to text, json, html, and script:
$.ajaxSetup({
converters: {
'iframe text': function (iframe) {
return iframe.text();
},
'iframe json': function (iframe) {
return $.parseJSON(iframe.text());
},
'iframe html': function (iframe) {
return iframe.find('body').html();
},
'iframe script': function (iframe) {
return $.globalEval(iframe.text());
}
}
});
}(jQuery));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long