├── .gitignore
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── assets
│ ├── images
│ │ └── rails.png
│ ├── javascripts
│ │ ├── application.js
│ │ └── screenshots.js.coffee
│ └── stylesheets
│ │ ├── application.css
│ │ ├── scaffolds.css.scss
│ │ └── screenshots.css.scss
├── controllers
│ ├── application_controller.rb
│ └── screenshots_controller.rb
├── helpers
│ ├── application_helper.rb
│ └── screenshots_helper.rb
├── models
│ └── screenshot.rb
├── uploaders
│ └── screenshot_uploader.rb
└── views
│ ├── layouts
│ └── application.html.erb
│ └── screenshots
│ ├── _list.js.erb
│ ├── destroy.js.erb
│ ├── index.html.erb
│ └── index.js.erb
├── config.ru
├── config
├── application.rb
├── boot.rb
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── backtrace_silencers.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ ├── secret_token.rb
│ ├── session_store.rb
│ └── wrap_parameters.rb
├── locales
│ └── en.yml
├── mongoid.yml
└── routes.rb
├── db
└── seeds.rb
├── doc
└── README_FOR_APP
├── public
├── 404.html
├── 422.html
├── 500.html
├── favicon.ico
├── robots.txt
└── uploads
│ └── placeholder
├── script
└── rails
├── test
├── fixtures
│ └── screenshots.yml
├── functional
│ └── screenshots_controller_test.rb
├── performance
│ └── browsing_test.rb
├── test_helper.rb
└── unit
│ ├── helpers
│ └── screenshots_helper_test.rb
│ └── screenshot_test.rb
└── vendor
└── assets
├── images
└── loading.gif
├── javascripts
└── fileuploader.js
└── stylesheets
└── fileuploader.css
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile ~/.gitignore_global
6 |
7 | # Ignore bundler config
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 |
13 | # Ignore all logfiles and tempfiles.
14 | /log/*.log
15 | /tmp
16 | .gitkeep
17 | /public/tmp
18 | /public/uploads/screenshot
19 | /public/uploads/tmp
20 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://ruby.taobao.org'
2 |
3 | gem 'rails', '3.2.8'
4 | gem 'mongoid', '3.0.4'
5 | gem 'carrierwave', '0.6.2'
6 | gem 'carrierwave-mongoid', :github => 'jnicklas/carrierwave-mongoid', :branch => 'mongoid-3.0'
7 | gem 'mini_magick', '3.4'
8 | gem 'rack-raw-upload', '1.1.0'
9 |
10 | # gem 'rails', :git => 'git://github.com/rails/rails.git'
11 |
12 |
13 |
14 | # Gems used only for assets and not required
15 | # in production environments by default.
16 | group :assets do
17 | gem 'sass-rails', '~> 3.2.3'
18 | gem 'coffee-rails', '~> 3.2.1'
19 |
20 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes
21 | # gem 'therubyracer', :platforms => :ruby
22 |
23 | gem 'uglifier', '>= 1.0.3'
24 | end
25 |
26 | gem 'jquery-rails', '2.0.2'
27 |
28 | # To use ActiveModel has_secure_password
29 | # gem 'bcrypt-ruby', '~> 3.0.0'
30 |
31 | # To use Jbuilder templates for JSON
32 | # gem 'jbuilder'
33 |
34 | # Use unicorn as the app server
35 | # gem 'unicorn'
36 |
37 | # Deploy with Capistrano
38 | # gem 'capistrano'
39 |
40 | # To use debugger
41 | # gem 'debugger'
42 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: git://github.com/jnicklas/carrierwave-mongoid.git
3 | revision: 28a9b718d42b8aba47d2eb951f73bd9d872b6eb0
4 | branch: mongoid-3.0
5 | specs:
6 | carrierwave-mongoid (0.3.0.alpha)
7 | carrierwave (~> 0.6.1)
8 | mongoid (~> 3.0.0)
9 | mongoid-grid_fs (~> 1.3.1)
10 |
11 | GEM
12 | remote: http://ruby.taobao.org/
13 | specs:
14 | actionmailer (3.2.8)
15 | actionpack (= 3.2.8)
16 | mail (~> 2.4.4)
17 | actionpack (3.2.8)
18 | activemodel (= 3.2.8)
19 | activesupport (= 3.2.8)
20 | builder (~> 3.0.0)
21 | erubis (~> 2.7.0)
22 | journey (~> 1.0.4)
23 | rack (~> 1.4.0)
24 | rack-cache (~> 1.2)
25 | rack-test (~> 0.6.1)
26 | sprockets (~> 2.1.3)
27 | activemodel (3.2.8)
28 | activesupport (= 3.2.8)
29 | builder (~> 3.0.0)
30 | activerecord (3.2.8)
31 | activemodel (= 3.2.8)
32 | activesupport (= 3.2.8)
33 | arel (~> 3.0.2)
34 | tzinfo (~> 0.3.29)
35 | activeresource (3.2.8)
36 | activemodel (= 3.2.8)
37 | activesupport (= 3.2.8)
38 | activesupport (3.2.8)
39 | i18n (~> 0.6)
40 | multi_json (~> 1.0)
41 | arel (3.0.2)
42 | builder (3.0.0)
43 | carrierwave (0.6.2)
44 | activemodel (>= 3.2.0)
45 | activesupport (>= 3.2.0)
46 | coffee-rails (3.2.2)
47 | coffee-script (>= 2.2.0)
48 | railties (~> 3.2.0)
49 | coffee-script (2.2.0)
50 | coffee-script-source
51 | execjs
52 | coffee-script-source (1.3.3)
53 | erubis (2.7.0)
54 | execjs (1.4.0)
55 | multi_json (~> 1.0)
56 | hike (1.2.1)
57 | i18n (0.6.0)
58 | journey (1.0.4)
59 | jquery-rails (2.0.2)
60 | railties (>= 3.2.0, < 5.0)
61 | thor (~> 0.14)
62 | json (1.7.4)
63 | mail (2.4.4)
64 | i18n (>= 0.4.0)
65 | mime-types (~> 1.16)
66 | treetop (~> 1.4.8)
67 | mime-types (1.19)
68 | mini_magick (3.4)
69 | subexec (~> 0.2.1)
70 | mongoid (3.0.4)
71 | activemodel (~> 3.1)
72 | moped (~> 1.1)
73 | origin (~> 1.0)
74 | tzinfo (~> 0.3.22)
75 | mongoid-grid_fs (1.3.2)
76 | mime-types (~> 1.19)
77 | mongoid (~> 3.0)
78 | moped (1.2.0)
79 | multi_json (1.3.6)
80 | origin (1.0.6)
81 | polyglot (0.3.3)
82 | rack (1.4.1)
83 | rack-cache (1.2)
84 | rack (>= 0.4)
85 | rack-raw-upload (1.1.0)
86 | json
87 | rack-ssl (1.3.2)
88 | rack
89 | rack-test (0.6.1)
90 | rack (>= 1.0)
91 | rails (3.2.8)
92 | actionmailer (= 3.2.8)
93 | actionpack (= 3.2.8)
94 | activerecord (= 3.2.8)
95 | activeresource (= 3.2.8)
96 | activesupport (= 3.2.8)
97 | bundler (~> 1.0)
98 | railties (= 3.2.8)
99 | railties (3.2.8)
100 | actionpack (= 3.2.8)
101 | activesupport (= 3.2.8)
102 | rack-ssl (~> 1.3.2)
103 | rake (>= 0.8.7)
104 | rdoc (~> 3.4)
105 | thor (>= 0.14.6, < 2.0)
106 | rake (0.9.2.2)
107 | rdoc (3.12)
108 | json (~> 1.4)
109 | sass (3.2.0)
110 | sass-rails (3.2.5)
111 | railties (~> 3.2.0)
112 | sass (>= 3.1.10)
113 | tilt (~> 1.3)
114 | sprockets (2.1.3)
115 | hike (~> 1.2)
116 | rack (~> 1.0)
117 | tilt (~> 1.1, != 1.3.0)
118 | subexec (0.2.2)
119 | thor (0.15.4)
120 | tilt (1.3.3)
121 | treetop (1.4.10)
122 | polyglot
123 | polyglot (>= 0.3.1)
124 | tzinfo (0.3.33)
125 | uglifier (1.2.7)
126 | execjs (>= 0.3.0)
127 | multi_json (~> 1.3)
128 |
129 | PLATFORMS
130 | ruby
131 |
132 | DEPENDENCIES
133 | carrierwave (= 0.6.2)
134 | carrierwave-mongoid!
135 | coffee-rails (~> 3.2.1)
136 | jquery-rails (= 2.0.2)
137 | mini_magick (= 3.4)
138 | mongoid (= 3.0.4)
139 | rack-raw-upload (= 1.1.0)
140 | rails (= 3.2.8)
141 | sass-rails (~> 3.2.3)
142 | uglifier (>= 1.0.3)
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ajax Upload With Carrierwave and Mongoid
2 | [file-uploader](https://github.com/valums/file-uploader) 是一个用 Javascrit 编写的文件上传 Libary,在这里我使用 [CarrierWave](https://github.com/jnicklas/carrierwave) 和 [Mongoid](https://github.com/mongoid/mongoid) 来完成多文件的无刷新上传功能
3 |
4 | # RubyGems
5 |
6 | ```ruby
7 | gem 'mongoid', '3.0.4'
8 | gem 'carrierwave', '0.6.2'
9 | gem 'carrierwave-mongoid', :github => 'jnicklas/carrierwave-mongoid', :branch => 'mongoid-3.0'
10 | gem 'mini_magick', '3.4'
11 | gem 'rack-raw-upload', '1.1.0'
12 | ```
13 |
14 | # application.rb
15 | config.middleware.use 'Rack::RawUpload'
16 |
17 | # Model
18 |
19 | ```ruby
20 | class Screenshot
21 | include Mongoid::Document
22 | include Mongoid::Timestamps::Created
23 | include Rails.application.routes.url_helpers
24 |
25 | attr_accessible :image
26 | field :image
27 |
28 | mount_uploader :image, ScreenshotUploader
29 |
30 | end
31 | ```
32 |
33 | # Controller
34 |
35 | ```ruby
36 | class ScreenshotsController < ApplicationController
37 |
38 | def index
39 | @screenshots = Screenshot.all.desc(:created_at)
40 |
41 | respond_to do |format|
42 | format.html # index.html.erb
43 | format.js{ render :layout => false}
44 | end
45 | end
46 |
47 | def create
48 | file = params[:qqfile].is_a?(ActionDispatch::Http::UploadedFile) ? params[:qqfile] : params[:file]
49 | @screenshot = Screenshot.new
50 | @screenshot.image = file
51 | if @screenshot.save
52 | render json: { success: true, src: @screenshot.to_json }
53 | else
54 | render json: @screenshot.errors.to_json
55 | end
56 | end
57 |
58 | def destroy
59 | @screenshot = Screenshot.find(params[:id])
60 | @screenshot.destroy
61 |
62 | respond_to do |format|
63 | format.html { redirect_to screenshots_path }
64 | format.js{
65 | @screenshots = Screenshot.all.desc(:created_at)
66 | render :layout => false
67 | }
68 | end
69 |
70 | end
71 |
72 | end
73 | ```
74 |
75 | # Uploader
76 |
77 | ```ruby
78 | # encoding: utf-8
79 | require "digest/md5"
80 | require 'carrierwave/processing/mini_magick'
81 |
82 | class ScreenshotUploader < CarrierWave::Uploader::Base
83 | include CarrierWave::MiniMagick
84 |
85 | storage :file
86 |
87 | # Override the directory where uploaded files will be stored.
88 | def store_dir
89 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
90 | end
91 |
92 |
93 | process :convert => 'png'
94 |
95 | # Create different versions of your uploaded files:
96 | version :thumb do
97 | process :resize_to_fill => [120, 120]
98 | end
99 |
100 | # Add a white list of extensions which are allowed to be uploaded.
101 | def extension_white_list
102 | %w(jpg jpeg gif png)
103 | end
104 |
105 | # Override the filename of the uploaded files:
106 | # see http://huacnlee.com/blog/carrierwave-upload-store-file-name-config/
107 | def filename
108 | if super.present?
109 | # current_path 是 Carrierwave 上传过程临时创建的一个文件,有时间标记,所以它将是唯一的
110 | @name ||= Digest::MD5.hexdigest(File.dirname(current_path))
111 | "#{@name}.#{file.extension.downcase}"
112 | end
113 | end
114 | end
115 | ```
116 |
117 | # Javascript
118 |
119 | ```Javascript
120 | $(document).ready(function(){
121 |
122 | var uploader = new qq.FileUploader({
123 | debug: true,
124 | allowedExtensions: ['jpg', 'jpeg', 'png', 'gif'],
125 | sizeLimit: 1048576, // max size: 1MB
126 | minSizeLimit: 0, // min size
127 | multiple: true,
128 | element: document.getElementById('file-uploader'),
129 | action: '<%= screenshots_path %>',
130 | onComplete: function(id, fileName, responseJSON){
131 | $.getScript("<%= screenshots_path %>");
132 | },
133 | onSubmit: function(id, fileName) {
134 | uploader.setParams({
135 | xx: "xx",
136 | yy: 'yy',
137 | zz: 'zz',
138 | authenticity_token: "<%= form_authenticity_token.to_s %>"
139 | });
140 | }
141 | });
142 |
143 | });
144 | ```
145 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | AjaxUploadWithCarrierwaveMongoid::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/app/assets/images/rails.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huobazi/ajax-upload-with-carrierwave-mongoid/b220a8324018b79803b589ebe98622399acc7f06/app/assets/images/rails.png
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // the compiled file.
9 | //
10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11 | // GO AFTER THE REQUIRES BELOW.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require_tree .
16 | //= require screenshots
17 | //= require fileuploader
--------------------------------------------------------------------------------
/app/assets/javascripts/screenshots.js.coffee:
--------------------------------------------------------------------------------
1 | # Place all the behaviors and hooks related to the matching controller here.
2 | # All this logic will automatically be available in application.js.
3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
4 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require_self
12 | *= require_tree .
13 | */
--------------------------------------------------------------------------------
/app/assets/stylesheets/scaffolds.css.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #fff;
3 | color: #333;
4 | font-family: verdana, arial, helvetica, sans-serif;
5 | font-size: 13px;
6 | line-height: 18px;
7 | }
8 |
9 | p, ol, ul, td {
10 | font-family: verdana, arial, helvetica, sans-serif;
11 | font-size: 13px;
12 | line-height: 18px;
13 | }
14 |
15 | pre {
16 | background-color: #eee;
17 | padding: 10px;
18 | font-size: 11px;
19 | }
20 |
21 | a {
22 | color: #000;
23 | &:visited {
24 | color: #666;
25 | }
26 | &:hover {
27 | color: #fff;
28 | }
29 | }
30 |
31 | div {
32 | &.field, &.actions {
33 | margin-bottom: 10px;
34 | }
35 | }
36 |
37 | #notice {
38 | color: green;
39 | }
40 |
41 | .field_with_errors {
42 | padding: 2px;
43 | background-color: red;
44 | display: table;
45 | }
46 |
47 | #error_explanation {
48 | width: 450px;
49 | border: 2px solid red;
50 | padding: 7px;
51 | padding-bottom: 0;
52 | margin-bottom: 20px;
53 | background-color: #f0f0f0;
54 | h2 {
55 | text-align: left;
56 | font-weight: bold;
57 | padding: 5px 5px 5px 15px;
58 | font-size: 12px;
59 | margin: -7px;
60 | margin-bottom: 0px;
61 | background-color: #c00;
62 | color: #fff;
63 | }
64 | ul li {
65 | font-size: 12px;
66 | list-style: square;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/screenshots.css.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the screenshots controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 | body{padding: 20px;}
5 | h1{margin: 20px; text-align: center}
6 | #file-uploader{margin: 20px;}
7 | #screenshots_list{
8 | width: 100%;
9 | .screenshot{float: left;margin:20px;
10 | img{clear:both;}
11 | .remove{clear:both;}
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/app/controllers/screenshots_controller.rb:
--------------------------------------------------------------------------------
1 | class ScreenshotsController < ApplicationController
2 |
3 | def index
4 | @screenshots = Screenshot.all.desc(:created_at)
5 |
6 | respond_to do |format|
7 | format.html # index.html.erb
8 | format.js{ render :layout => false}
9 | end
10 | end
11 |
12 | def create
13 | file = params[:qqfile].is_a?(ActionDispatch::Http::UploadedFile) ? params[:qqfile] : params[:file]
14 | @screenshot = Screenshot.new
15 | @screenshot.image = file
16 | if @screenshot.save
17 | render json: { success: true, src: @screenshot.to_json }
18 | else
19 | render json: @screenshot.errors.to_json
20 | end
21 | end
22 |
23 | def destroy
24 | @screenshot = Screenshot.find(params[:id])
25 | @screenshot.destroy
26 |
27 | respond_to do |format|
28 | format.html { redirect_to screenshots_path }
29 | format.js{
30 | @screenshots = Screenshot.all.desc(:created_at)
31 | render :layout => false
32 | }
33 | end
34 |
35 | end
36 |
37 | end
38 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/screenshots_helper.rb:
--------------------------------------------------------------------------------
1 | module ScreenshotsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/screenshot.rb:
--------------------------------------------------------------------------------
1 | class Screenshot
2 | include Mongoid::Document
3 | include Mongoid::Timestamps::Created
4 | include Rails.application.routes.url_helpers
5 |
6 | attr_accessible :image
7 | field :image
8 |
9 | mount_uploader :image, ScreenshotUploader
10 |
11 | end
12 |
--------------------------------------------------------------------------------
/app/uploaders/screenshot_uploader.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require "digest/md5"
3 | require 'carrierwave/processing/mini_magick'
4 |
5 | class ScreenshotUploader < CarrierWave::Uploader::Base
6 | include CarrierWave::MiniMagick
7 |
8 | storage :file
9 |
10 | # Override the directory where uploaded files will be stored.
11 | def store_dir
12 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
13 | end
14 |
15 |
16 | process :convert => 'png'
17 |
18 | # Create different versions of your uploaded files:
19 | version :thumb do
20 | process :resize_to_fill => [120, 120]
21 | end
22 |
23 | # Add a white list of extensions which are allowed to be uploaded.
24 | def extension_white_list
25 | %w(jpg jpeg gif png)
26 | end
27 |
28 | # Override the filename of the uploaded files:
29 | # see http://huacnlee.com/blog/carrierwave-upload-store-file-name-config/
30 | def filename
31 | if super.present?
32 | # current_path 是 Carrierwave 上传过程临时创建的一个文件,有时间标记,所以它将是唯一的
33 | @name ||= Digest::MD5.hexdigest(File.dirname(current_path))
34 | "#{@name}.#{file.extension.downcase}"
35 | end
36 | end
37 | end
38 |
39 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ajax Upload With Carrierwave Mongoid
5 | <%= stylesheet_link_tag "application", "fileuploader", :media => "all" %>
6 | <%= javascript_include_tag "application" %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/views/screenshots/_list.js.erb:
--------------------------------------------------------------------------------
1 | <% @screenshots.each do |screenshot| %>
2 |
3 |
4 |
5 | <%= link_to "Remove", screenshot_path(screenshot.id), :method => :delete, 'data-confirm' => '确定要删除吗?', :remote => true %>
6 |
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/screenshots/destroy.js.erb:
--------------------------------------------------------------------------------
1 | $('#screenshots_list').html("<%= escape_javascript(render :partial => 'list') %>")
--------------------------------------------------------------------------------
/app/views/screenshots/index.html.erb:
--------------------------------------------------------------------------------
1 | Ajax Upload With Carrierwave and Mongoid
2 |
3 |
4 | Please enable JavaScript to use file uploader.
5 |
6 |
7 |
8 |
9 |
10 |
37 |
38 |
39 | <% @screenshots.each do |screenshot| %>
40 |
41 |
42 |
43 |
44 | <%= link_to "Remove", screenshot_path(screenshot.id), :method => :delete, 'data-confirm' => '确定要删除吗?', :remote => true, :class => "remove" %>
45 |
46 | <% end %>
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/views/screenshots/index.js.erb:
--------------------------------------------------------------------------------
1 | $('#screenshots_list').html("<%= escape_javascript(render :partial => 'list') %>")
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run AjaxUploadWithCarrierwaveMongoid::Application
5 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | # Pick the frameworks you want:
4 | # require "active_record/railtie"
5 | require "action_controller/railtie"
6 | require "action_mailer/railtie"
7 | require "active_resource/railtie"
8 | require "sprockets/railtie"
9 | require "rails/test_unit/railtie"
10 |
11 | if defined?(Bundler)
12 | # If you precompile assets before deploying to production, use this line
13 | Bundler.require(*Rails.groups(:assets => %w(development test)))
14 | # If you want your assets lazily compiled in production, use this line
15 | # Bundler.require(:default, :assets, Rails.env)
16 | end
17 |
18 | module AjaxUploadWithCarrierwaveMongoid
19 | class Application < Rails::Application
20 | # Settings in config/environments/* take precedence over those specified here.
21 | # Application configuration should go into files in config/initializers
22 | # -- all .rb files in that directory are automatically loaded.
23 |
24 | # Custom directories with classes and modules you want to be autoloadable.
25 | config.autoload_paths += %W(#{config.root}/uploaders)
26 |
27 | # Only load the plugins named here, in the order given (default is alphabetical).
28 | # :all can be used as a placeholder for all plugins not explicitly named.
29 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
30 |
31 | # Activate observers that should always be running.
32 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
33 |
34 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
35 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
36 | # config.time_zone = 'Central Time (US & Canada)'
37 |
38 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
39 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
40 | # config.i18n.default_locale = :de
41 |
42 | # Configure the default encoding used in templates for Ruby 1.9.
43 | config.encoding = "utf-8"
44 |
45 | # Configure sensitive parameters which will be filtered from the log file.
46 | config.filter_parameters += [:password]
47 |
48 | # Enable escaping HTML in JSON.
49 | config.active_support.escape_html_entities_in_json = true
50 |
51 | # Use SQL instead of Active Record's schema dumper when creating the database.
52 | # This is necessary if your schema can't be completely dumped by the schema dumper,
53 | # like if you have constraints or database-specific column types
54 | # config.active_record.schema_format = :sql
55 |
56 | # Enforce whitelist mode for mass assignment.
57 | # This will create an empty whitelist of attributes available for mass-assignment for all models
58 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
59 | # parameters by using an attr_accessible or attr_protected declaration.
60 | # config.active_record.whitelist_attributes = true
61 |
62 | # Enable the asset pipeline
63 | config.assets.enabled = true
64 |
65 | # Version of your assets, change this if you want to expire all your assets
66 | config.assets.version = '1.0'
67 |
68 | config.middleware.use 'Rack::RawUpload'
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5 |
6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | AjaxUploadWithCarrierwaveMongoid::Application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | AjaxUploadWithCarrierwaveMongoid::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger
20 | config.active_support.deprecation = :log
21 |
22 | # Only use best-standards-support built into browsers
23 | config.action_dispatch.best_standards_support = :builtin
24 |
25 |
26 | # Do not compress assets
27 | config.assets.compress = false
28 |
29 | # Expands the lines which load the assets
30 | config.assets.debug = true
31 | end
32 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | AjaxUploadWithCarrierwaveMongoid::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = false
13 |
14 | # Compress JavaScripts and CSS
15 | config.assets.compress = true
16 |
17 | # Don't fallback to assets pipeline if a precompiled asset is missed
18 | config.assets.compile = false
19 |
20 | # Generate digests for assets URLs
21 | config.assets.digest = true
22 |
23 | # Defaults to nil and saved in location specified by config.assets.prefix
24 | # config.assets.manifest = YOUR_PATH
25 |
26 | # Specifies the header that your server uses for sending files
27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | # config.force_ssl = true
32 |
33 | # See everything in the log (default is :info)
34 | # config.log_level = :debug
35 |
36 | # Prepend all log lines with the following tags
37 | # config.log_tags = [ :subdomain, :uuid ]
38 |
39 | # Use a different logger for distributed setups
40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
41 |
42 | # Use a different cache store in production
43 | # config.cache_store = :mem_cache_store
44 |
45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
46 | # config.action_controller.asset_host = "http://assets.example.com"
47 |
48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
49 | # config.assets.precompile += %w( search.js )
50 |
51 | # Disable delivery errors, bad email addresses will be ignored
52 | # config.action_mailer.raise_delivery_errors = false
53 |
54 | # Enable threaded mode
55 | # config.threadsafe!
56 |
57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
58 | # the I18n.default_locale when a translation can not be found)
59 | config.i18n.fallbacks = true
60 |
61 | # Send deprecation notices to registered listeners
62 | config.active_support.deprecation = :notify
63 |
64 | end
65 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | AjaxUploadWithCarrierwaveMongoid::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Configure static asset server for tests with Cache-Control for performance
11 | config.serve_static_assets = true
12 | config.static_cache_control = "public, max-age=3600"
13 |
14 | # Log error messages when you accidentally call methods on nil
15 | config.whiny_nils = true
16 |
17 | # Show full error reports and disable caching
18 | config.consider_all_requests_local = true
19 | config.action_controller.perform_caching = false
20 |
21 | # Raise exceptions instead of rendering exception templates
22 | config.action_dispatch.show_exceptions = false
23 |
24 | # Disable request forgery protection in test environment
25 | config.action_controller.allow_forgery_protection = false
26 |
27 | # Tell Action Mailer not to deliver emails to the real world.
28 | # The :test delivery method accumulates sent emails in the
29 | # ActionMailer::Base.deliveries array.
30 | config.action_mailer.delivery_method = :test
31 |
32 |
33 | # Print deprecation notices to the stderr
34 | config.active_support.deprecation = :stderr
35 | end
36 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 | #
12 | # These inflection rules are supported but not enabled by default:
13 | # ActiveSupport::Inflector.inflections do |inflect|
14 | # inflect.acronym 'RESTful'
15 | # end
16 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | AjaxUploadWithCarrierwaveMongoid::Application.config.secret_token = '4a2b2ee1abda31158cce3ae333d6c67c80fceac238a33d6fb525ed5551a2d2c0afcc95fd2da6fbfaae8f6aa77bf9464c9d95df73286e16e6e1f28aeaf5cc0fdb'
8 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | AjaxUploadWithCarrierwaveMongoid::Application.config.session_store :cookie_store, key: '_ajax-upload-with-carrierwave-mongoid_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # AjaxUploadWithCarrierwaveMongoid::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/config/mongoid.yml:
--------------------------------------------------------------------------------
1 | development:
2 | # Configure available database sessions. (required)
3 | sessions:
4 | # Defines the default session. (required)
5 | default:
6 | # Defines the name of the default database that Mongoid can connect to.
7 | # (required).
8 | database: ajax_upload_with_carrierwave_mongoid_development
9 | # Provides the hosts the default session can connect to. Must be an array
10 | # of host:port pairs. (required)
11 | hosts:
12 | - localhost:27017
13 | options:
14 | # Change whether the session persists in safe mode by default.
15 | # (default: false)
16 | # safe: false
17 |
18 | # Change the default consistency model to :eventual or :strong.
19 | # :eventual will send reads to secondaries, :strong sends everything
20 | # to master. (default: :eventual)
21 | consistency: :strong
22 | # Configure Mongoid specific options. (optional)
23 | options:
24 | # Configuration for whether or not to allow access to fields that do
25 | # not have a field definition on the model. (default: true)
26 | # allow_dynamic_fields: true
27 |
28 | # Enable the identity map, needed for eager loading. (default: false)
29 | # identity_map_enabled: false
30 |
31 | # Includes the root model name in json serialization. (default: false)
32 | # include_root_in_json: false
33 |
34 | # Include the _type field in serializaion. (default: false)
35 | # include_type_for_serialization: false
36 |
37 | # Preload all models in development, needed when models use
38 | # inheritance. (default: false)
39 | # preload_models: false
40 |
41 | # Protect id and type from mass assignment. (default: true)
42 | # protect_sensitive_fields: true
43 |
44 | # Raise an error when performing a #find and the document is not found.
45 | # (default: true)
46 | # raise_not_found_error: true
47 |
48 | # Raise an error when defining a scope with the same name as an
49 | # existing method. (default: false)
50 | # scope_overwrite_exception: false
51 |
52 | # Skip the database version check, used when connecting to a db without
53 | # admin access. (default: false)
54 | # skip_version_check: false
55 |
56 | # User Active Support's time zone in conversions. (default: true)
57 | # use_activesupport_time_zone: true
58 |
59 | # Ensure all times are UTC in the app side. (default: false)
60 | # use_utc: false
61 | test:
62 | sessions:
63 | default:
64 | database: ajax_upload_with_carrierwave_mongoid_test
65 | hosts:
66 | - localhost:27017
67 | options:
68 | consistency: :strong
69 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | AjaxUploadWithCarrierwaveMongoid::Application.routes.draw do
2 | root :to => 'screenshots#index'
3 | resources :screenshots, :only => [:index, :create, :destroy]
4 | # The priority is based upon order of creation:
5 | # first created -> highest priority.
6 |
7 | # Sample of regular route:
8 | # match 'products/:id' => 'catalog#view'
9 | # Keep in mind you can assign values other than :controller and :action
10 |
11 | # Sample of named route:
12 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
13 | # This route can be invoked with purchase_url(:id => product.id)
14 |
15 | # Sample resource route (maps HTTP verbs to controller actions automatically):
16 | # resources :products
17 |
18 | # Sample resource route with options:
19 | # resources :products do
20 | # member do
21 | # get 'short'
22 | # post 'toggle'
23 | # end
24 | #
25 | # collection do
26 | # get 'sold'
27 | # end
28 | # end
29 |
30 | # Sample resource route with sub-resources:
31 | # resources :products do
32 | # resources :comments, :sales
33 | # resource :seller
34 | # end
35 |
36 | # Sample resource route with more complex sub-resources
37 | # resources :products do
38 | # resources :comments
39 | # resources :sales do
40 | # get 'recent', :on => :collection
41 | # end
42 | # end
43 |
44 | # Sample resource route within a namespace:
45 | # namespace :admin do
46 | # # Directs /admin/products/* to Admin::ProductsController
47 | # # (app/controllers/admin/products_controller.rb)
48 | # resources :products
49 | # end
50 |
51 | # You can have the root of your site routed with "root"
52 | # just remember to delete public/index.html.
53 | # root :to => 'welcome#index'
54 |
55 | # See how all your routes lay out with "rake routes"
56 |
57 | # This is a legacy wild controller route that's not recommended for RESTful applications.
58 | # Note: This route will make all actions in every controller accessible via GET requests.
59 | # match ':controller(/:action(/:id))(.:format)'
60 | end
61 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/doc/README_FOR_APP:
--------------------------------------------------------------------------------
1 | Use this README file to introduce your application and point to useful places in the API for learning more.
2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
3 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huobazi/ajax-upload-with-carrierwave-mongoid/b220a8324018b79803b589ebe98622399acc7f06/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-Agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/public/uploads/placeholder:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huobazi/ajax-upload-with-carrierwave-mongoid/b220a8324018b79803b589ebe98622399acc7f06/public/uploads/placeholder
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/test/fixtures/screenshots.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
2 |
3 | # This model initially had no columns defined. If you add columns to the
4 | # model remove the '{}' from the fixture names and add the columns immediately
5 | # below each fixture, per the syntax in the comments below
6 | #
7 | one: {}
8 | # column: value
9 | #
10 | two: {}
11 | # column: value
12 |
--------------------------------------------------------------------------------
/test/functional/screenshots_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ScreenshotsControllerTest < ActionController::TestCase
4 | setup do
5 | @screenshot = screenshots(:one)
6 | end
7 |
8 | test "should get index" do
9 | get :index
10 | assert_response :success
11 | assert_not_nil assigns(:screenshots)
12 | end
13 |
14 | test "should get new" do
15 | get :new
16 | assert_response :success
17 | end
18 |
19 | test "should create screenshot" do
20 | assert_difference('Screenshot.count') do
21 | post :create, screenshot: { }
22 | end
23 |
24 | assert_redirected_to screenshot_path(assigns(:screenshot))
25 | end
26 |
27 | test "should show screenshot" do
28 | get :show, id: @screenshot
29 | assert_response :success
30 | end
31 |
32 | test "should get edit" do
33 | get :edit, id: @screenshot
34 | assert_response :success
35 | end
36 |
37 | test "should update screenshot" do
38 | put :update, id: @screenshot, screenshot: { }
39 | assert_redirected_to screenshot_path(assigns(:screenshot))
40 | end
41 |
42 | test "should destroy screenshot" do
43 | assert_difference('Screenshot.count', -1) do
44 | delete :destroy, id: @screenshot
45 | end
46 |
47 | assert_redirected_to screenshots_path
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/test/performance/browsing_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'rails/performance_test_help'
3 |
4 | class BrowsingTest < ActionDispatch::PerformanceTest
5 | # Refer to the documentation for all available options
6 | # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
7 | # :output => 'tmp/performance', :formats => [:flat] }
8 |
9 | def test_homepage
10 | get '/'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] = "test"
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Add more helper methods to be used by all tests here...
7 | end
8 |
--------------------------------------------------------------------------------
/test/unit/helpers/screenshots_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ScreenshotsHelperTest < ActionView::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/test/unit/screenshot_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ScreenshotTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/vendor/assets/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huobazi/ajax-upload-with-carrierwave-mongoid/b220a8324018b79803b589ebe98622399acc7f06/vendor/assets/images/loading.gif
--------------------------------------------------------------------------------
/vendor/assets/javascripts/fileuploader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * http://github.com/Valums-File-Uploader/file-uploader
3 | *
4 | * Multiple file upload component with progress-bar, drag-and-drop.
5 | *
6 | * Have ideas for improving this JS for the general community?
7 | * Submit your changes at: https://github.com/Valums-File-Uploader/file-uploader
8 | *
9 | * VERSION 2.0 beta
10 | * Original version 1.0 © 2010 Andrew Valums ( andrew(at)valums.com )
11 | *
12 | * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
13 | */
14 |
15 | //
16 | // Helper functions
17 | //
18 |
19 | var qq = qq || {};
20 |
21 | /**
22 | * Adds all missing properties from second obj to first obj
23 | */
24 | qq.extend = function(first, second){
25 | for (var prop in second){
26 | first[prop] = second[prop];
27 | }
28 | };
29 |
30 | /**
31 | * Searches for a given element in the array, returns -1 if it is not present.
32 | * @param {Number} [from] The index at which to begin the search
33 | */
34 | qq.indexOf = function(arr, elt, from){
35 | if (arr.indexOf) return arr.indexOf(elt, from);
36 |
37 | from = from || 0;
38 | var len = arr.length;
39 |
40 | if (from < 0) from += len;
41 |
42 | for (; from < len; from++){
43 | if (from in arr && arr[from] === elt){
44 | return from;
45 | }
46 | }
47 | return -1;
48 | };
49 |
50 | qq.getUniqueId = (function(){
51 | var id = 0;
52 | return function(){ return id++; };
53 | })();
54 |
55 | //
56 | // Browsers and platforms detection
57 |
58 | qq.ie = function(){ return navigator.userAgent.indexOf('MSIE') != -1; }
59 | qq.safari = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf("Apple") != -1; }
60 | qq.chrome = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf('Google') != -1; }
61 | qq.firefox = function(){ return (navigator.userAgent.indexOf('Mozilla') != -1 && navigator.vendor != undefined && navigator.vendor == ''); }
62 | qq.windows = function(){ return navigator.platform == "Win32"; }
63 |
64 | //
65 | // Events
66 |
67 | /** Returns the function which detaches attached event */
68 | qq.attach = function(element, type, fn){
69 | if (element.addEventListener){
70 | element.addEventListener(type, fn, false);
71 | } else if (element.attachEvent){
72 | element.attachEvent('on' + type, fn);
73 | }
74 | return function() {
75 | qq.detach(element, type, fn)
76 | }
77 | };
78 | qq.detach = function(element, type, fn){
79 | if (element.removeEventListener){
80 | element.removeEventListener(type, fn, false);
81 | } else if (element.attachEvent){
82 | element.detachEvent('on' + type, fn);
83 | }
84 | };
85 |
86 | qq.preventDefault = function(e){
87 | if (e.preventDefault){
88 | e.preventDefault();
89 | } else{
90 | e.returnValue = false;
91 | }
92 | };
93 |
94 | //
95 | // Node manipulations
96 |
97 | /**
98 | * Insert node a before node b.
99 | */
100 | qq.insertBefore = function(a, b){
101 | b.parentNode.insertBefore(a, b);
102 | };
103 | qq.remove = function(element){
104 | element.parentNode.removeChild(element);
105 | };
106 |
107 | qq.contains = function(parent, descendant){
108 | // compareposition returns false in this case
109 | if (parent == descendant) return true;
110 |
111 | if (parent.contains){
112 | return parent.contains(descendant);
113 | } else {
114 | return !!(descendant.compareDocumentPosition(parent) & 8);
115 | }
116 | };
117 |
118 | /**
119 | * Creates and returns element from html string
120 | * Uses innerHTML to create an element
121 | */
122 | qq.toElement = (function(){
123 | var div = document.createElement('div');
124 | return function(html){
125 | div.innerHTML = html;
126 | var element = div.firstChild;
127 | div.removeChild(element);
128 | return element;
129 | };
130 | })();
131 |
132 | //
133 | // Node properties and attributes
134 |
135 | /**
136 | * Sets styles for an element.
137 | * Fixes opacity in IE6-8.
138 | */
139 | qq.css = function(element, styles){
140 | if (styles.opacity != null){
141 | if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
142 | styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
143 | }
144 | }
145 | qq.extend(element.style, styles);
146 | };
147 | qq.hasClass = function(element, name){
148 | var re = new RegExp('(^| )' + name + '( |$)');
149 | return re.test(element.className);
150 | };
151 | qq.addClass = function(element, name){
152 | if (!qq.hasClass(element, name)){
153 | element.className += ' ' + name;
154 | }
155 | };
156 | qq.removeClass = function(element, name){
157 | var re = new RegExp('(^| )' + name + '( |$)');
158 | element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
159 | };
160 | qq.setText = function(element, text){
161 | element.innerText = text;
162 | element.textContent = text;
163 | };
164 |
165 | //
166 | // Selecting elements
167 |
168 | qq.children = function(element){
169 | var children = [],
170 | child = element.firstChild;
171 |
172 | while (child){
173 | if (child.nodeType == 1){
174 | children.push(child);
175 | }
176 | child = child.nextSibling;
177 | }
178 |
179 | return children;
180 | };
181 |
182 | qq.getByClass = function(element, className){
183 | if (element.querySelectorAll){
184 | return element.querySelectorAll('.' + className);
185 | }
186 |
187 | var result = [];
188 | var candidates = element.getElementsByTagName("*");
189 | var len = candidates.length;
190 |
191 | for (var i = 0; i < len; i++){
192 | if (qq.hasClass(candidates[i], className)){
193 | result.push(candidates[i]);
194 | }
195 | }
196 | return result;
197 | };
198 |
199 | /**
200 | * obj2url() takes a json-object as argument and generates
201 | * a querystring. pretty much like jQuery.param()
202 | *
203 | * how to use:
204 | *
205 | * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
206 | *
207 | * will result in:
208 | *
209 | * `http://any.url/upload?otherParam=value&a=b&c=d`
210 | *
211 | * @param Object JSON-Object
212 | * @param String current querystring-part
213 | * @return String encoded querystring
214 | */
215 | qq.obj2url = function(obj, temp, prefixDone){
216 | var uristrings = [],
217 | prefix = '&',
218 | add = function(nextObj, i){
219 | var nextTemp = temp
220 | ? (/\[\]$/.test(temp)) // prevent double-encoding
221 | ? temp
222 | : temp+'['+i+']'
223 | : i;
224 | if ((nextTemp != 'undefined') && (i != 'undefined')) {
225 | uristrings.push(
226 | (typeof nextObj === 'object')
227 | ? qq.obj2url(nextObj, nextTemp, true)
228 | : (Object.prototype.toString.call(nextObj) === '[object Function]')
229 | ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
230 | : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
231 | );
232 | }
233 | };
234 |
235 | if (!prefixDone && temp) {
236 | prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
237 | uristrings.push(temp);
238 | uristrings.push(qq.obj2url(obj));
239 | } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
240 | // we wont use a for-in-loop on an array (performance)
241 | for (var i = 0, len = obj.length; i < len; ++i){
242 | add(obj[i], i);
243 | }
244 | } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
245 | // for anything else but a scalar, we will use for-in-loop
246 | for (var i in obj){
247 | add(obj[i], i);
248 | }
249 | } else {
250 | uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
251 | }
252 |
253 | return uristrings.join(prefix)
254 | .replace(/^&/, '')
255 | .replace(/%20/g, '+');
256 | };
257 |
258 | //
259 | //
260 | // Uploader Classes
261 | //
262 | //
263 |
264 | var qq = qq || {};
265 |
266 | /**
267 | * Creates upload button, validates upload, but doesn't create file list or dd.
268 | */
269 | qq.FileUploaderBasic = function(o){
270 | this._options = {
271 | // set to true to see the server response
272 | debug: false,
273 | action: '/server/upload',
274 | params: {},
275 | customHeaders: {},
276 | button: null,
277 | multiple: true,
278 | maxConnections: 3,
279 | // validation
280 | allowedExtensions: [],
281 | acceptFiles: null, // comma separated string of mime-types for browser to display in browse dialog
282 | sizeLimit: 0,
283 | minSizeLimit: 0,
284 | // events
285 | // return false to cancel submit
286 | onSubmit: function(id, fileName){},
287 | onProgress: function(id, fileName, loaded, total){},
288 | onComplete: function(id, fileName, responseJSON){},
289 | onCancel: function(id, fileName){},
290 | onUpload: function(id, fileName, xhr){},
291 | onError: function(id, fileName, xhr) {},
292 | // messages
293 | messages: {
294 | typeError: "Unfortunately the file(s) you selected weren't the type we were expecting. Only {extensions} files are allowed.",
295 | sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
296 | minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
297 | emptyError: "{file} is empty, please select files again without it.",
298 | onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
299 | },
300 | showMessage: function(message){
301 | alert(message);
302 | },
303 | inputName: 'qqfile',
304 | extraDropzones : []
305 | };
306 | qq.extend(this._options, o);
307 | qq.extend(this, qq.DisposeSupport);
308 |
309 | // number of files being uploaded
310 | this._filesInProgress = 0;
311 | this._handler = this._createUploadHandler();
312 |
313 | if (this._options.button){
314 | this._button = this._createUploadButton(this._options.button);
315 | }
316 |
317 | this._preventLeaveInProgress();
318 | };
319 |
320 | qq.FileUploaderBasic.prototype = {
321 | setParams: function(params){
322 | this._options.params = params;
323 | },
324 | getInProgress: function(){
325 | return this._filesInProgress;
326 | },
327 | _createUploadButton: function(element){
328 | var self = this;
329 |
330 | var button = new qq.UploadButton({
331 | element: element,
332 | multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
333 | acceptFiles: this._options.acceptFiles,
334 | onChange: function(input){
335 | self._onInputChange(input);
336 | }
337 | });
338 |
339 | this.addDisposer(function() { button.dispose(); });
340 | return button;
341 | },
342 | _createUploadHandler: function(){
343 | var self = this,
344 | handlerClass;
345 |
346 | if(qq.UploadHandlerXhr.isSupported()){
347 | handlerClass = 'UploadHandlerXhr';
348 | } else {
349 | handlerClass = 'UploadHandlerForm';
350 | }
351 |
352 | var handler = new qq[handlerClass]({
353 | debug: this._options.debug,
354 | action: this._options.action,
355 | encoding: this._options.encoding,
356 | maxConnections: this._options.maxConnections,
357 | customHeaders: this._options.customHeaders,
358 | inputName: this._options.inputName,
359 | extraDropzones: this._options.extraDropzones,
360 | onProgress: function(id, fileName, loaded, total){
361 | self._onProgress(id, fileName, loaded, total);
362 | self._options.onProgress(id, fileName, loaded, total);
363 | },
364 | onComplete: function(id, fileName, result){
365 | self._onComplete(id, fileName, result);
366 | self._options.onComplete(id, fileName, result);
367 | },
368 | onCancel: function(id, fileName){
369 | self._onCancel(id, fileName);
370 | self._options.onCancel(id, fileName);
371 | },
372 | onError: self._options.onError,
373 | onUpload: function(id, fileName, xhr){
374 | self._onUpload(id, fileName, xhr);
375 | self._options.onUpload(id, fileName, xhr);
376 | }
377 | });
378 |
379 | return handler;
380 | },
381 | _preventLeaveInProgress: function(){
382 | var self = this;
383 |
384 | this._attach(window, 'beforeunload', function(e){
385 | if (!self._filesInProgress){return;}
386 |
387 | var e = e || window.event;
388 | // for ie, ff
389 | e.returnValue = self._options.messages.onLeave;
390 | // for webkit
391 | return self._options.messages.onLeave;
392 | });
393 | },
394 | _onSubmit: function(id, fileName){
395 | this._filesInProgress++;
396 | },
397 | _onProgress: function(id, fileName, loaded, total){
398 | },
399 | _onComplete: function(id, fileName, result){
400 | this._filesInProgress--;
401 | if (result.error){
402 | this._options.showMessage(result.error);
403 | }
404 | },
405 | _onCancel: function(id, fileName){
406 | this._filesInProgress--;
407 | },
408 | _onUpload: function(id, fileName, xhr){
409 | },
410 | _onInputChange: function(input){
411 | if (this._handler instanceof qq.UploadHandlerXhr){
412 | this._uploadFileList(input.files);
413 | } else {
414 | if (this._validateFile(input)){
415 | this._uploadFile(input);
416 | }
417 | }
418 | this._button.reset();
419 | },
420 | _uploadFileList: function(files){
421 | for (var i=0; i this._options.sizeLimit){
462 | this._error('sizeError', name);
463 | return false;
464 |
465 | } else if (size && size < this._options.minSizeLimit){
466 | this._error('minSizeError', name);
467 | return false;
468 | }
469 |
470 | return true;
471 | },
472 | _error: function(code, fileName){
473 | var message = this._options.messages[code];
474 | function r(name, replacement){ message = message.replace(name, replacement); }
475 |
476 | r('{file}', this._formatFileName(fileName));
477 | r('{extensions}', this._options.allowedExtensions.join(', '));
478 | r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
479 | r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
480 |
481 | this._options.showMessage(message);
482 | },
483 | _formatFileName: function(name){
484 | if (name.length > 33){
485 | name = name.slice(0, 19) + '...' + name.slice(-13);
486 | }
487 | return name;
488 | },
489 | _isAllowedExtension: function(fileName){
490 | var ext = (-1 !== fileName.indexOf('.'))
491 | ? fileName.replace(/.*[.]/, '').toLowerCase()
492 | : '';
493 | var allowed = this._options.allowedExtensions;
494 |
495 | if (!allowed.length){return true;}
496 |
497 | for (var i=0; i 99);
509 |
510 | return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
511 | }
512 | };
513 |
514 |
515 | /**
516 | * Class that creates upload widget with drag-and-drop and file list
517 | * @inherits qq.FileUploaderBasic
518 | */
519 | qq.FileUploader = function(o){
520 | // call parent constructor
521 | qq.FileUploaderBasic.apply(this, arguments);
522 |
523 | // additional options
524 | qq.extend(this._options, {
525 | element: null,
526 | // if set, will be used instead of qq-upload-list in template
527 | listElement: null,
528 | dragText: 'Drop files here to upload',
529 | uploadButtonText: 'Upload a file',
530 | cancelButtonText: 'Cancel',
531 | failUploadText: 'Upload failed',
532 |
533 | template: '' +
534 | '
{dragText}
' +
535 | '
{uploadButtonText}
' +
536 | '
' +
537 | '
',
538 |
539 | // template for one item in file list
540 | fileTemplate: '' +
541 | ' ' +
542 | ' ' +
543 | ' ' +
544 | ' ' +
545 | '{cancelButtonText} ' +
546 | '{failUploadtext} ' +
547 | ' ',
548 |
549 | classes: {
550 | // used to get elements from templates
551 | button: 'qq-upload-button',
552 | drop: 'qq-upload-drop-area',
553 | dropActive: 'qq-upload-drop-area-active',
554 | dropDisabled: 'qq-upload-drop-area-disabled',
555 | list: 'qq-upload-list',
556 | progressBar: 'qq-progress-bar',
557 | file: 'qq-upload-file',
558 | spinner: 'qq-upload-spinner',
559 | size: 'qq-upload-size',
560 | cancel: 'qq-upload-cancel',
561 |
562 | // added to list item when upload completes
563 | // used in css to hide progress spinner
564 | success: 'qq-upload-success',
565 | fail: 'qq-upload-fail'
566 | }
567 | });
568 | // overwrite options with user supplied
569 | qq.extend(this._options, o);
570 |
571 | // overwrite the upload button text if any
572 | // same for the Cancel button and Fail message text
573 | this._options.template = this._options.template.replace(/\{dragText\}/g, this._options.dragText);
574 | this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.uploadButtonText);
575 | this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.cancelButtonText);
576 | this._options.fileTemplate = this._options.fileTemplate.replace(/\{failUploadtext\}/g, this._options.failUploadText);
577 |
578 | this._element = this._options.element;
579 | this._element.innerHTML = this._options.template;
580 | this._listElement = this._options.listElement || this._find(this._element, 'list');
581 |
582 | this._classes = this._options.classes;
583 |
584 | this._button = this._createUploadButton(this._find(this._element, 'button'));
585 |
586 | this._bindCancelEvent();
587 | this._setupDragDrop();
588 | };
589 |
590 | // inherit from Basic Uploader
591 | qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);
592 |
593 | qq.extend(qq.FileUploader.prototype, {
594 | addExtraDropzone: function(element){
595 | this._setupExtraDropzone(element);
596 | },
597 | removeExtraDropzone: function(element){
598 | var dzs = this._options.extraDropzones;
599 | for(var i in dzs) if (dzs[i] === element) return this._options.extraDropzones.splice(i,1);
600 | },
601 | _leaving_document_out: function(e){
602 | return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
603 | || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
604 | },
605 | /**
606 | * Gets one of the elements listed in this._options.classes
607 | **/
608 | _find: function(parent, type){
609 | var element = qq.getByClass(parent, this._options.classes[type])[0];
610 | if (!element){
611 | throw new Error('element not found ' + type);
612 | }
613 |
614 | return element;
615 | },
616 | _setupExtraDropzone: function(element){
617 | this._options.extraDropzones.push(element);
618 | this._setupDropzone(element);
619 | },
620 | _setupDropzone: function(dropArea){
621 | var self = this;
622 |
623 | var dz = new qq.UploadDropZone({
624 | element: dropArea,
625 | onEnter: function(e){
626 | qq.addClass(dropArea, self._classes.dropActive);
627 | e.stopPropagation();
628 | },
629 | onLeave: function(e){
630 | //e.stopPropagation();
631 | },
632 | onLeaveNotDescendants: function(e){
633 | qq.removeClass(dropArea, self._classes.dropActive);
634 | },
635 | onDrop: function(e){
636 | dropArea.style.display = 'none';
637 | qq.removeClass(dropArea, self._classes.dropActive);
638 | self._uploadFileList(e.dataTransfer.files);
639 | }
640 | });
641 |
642 | this.addDisposer(function() { dz.dispose(); });
643 |
644 | dropArea.style.display = 'none';
645 | },
646 | _setupDragDrop: function(){
647 | var dropArea = this._find(this._element, 'drop');
648 | var self = this;
649 | this._options.extraDropzones.push(dropArea);
650 |
651 | var dropzones = this._options.extraDropzones;
652 | var i;
653 | for (i=0; i < dropzones.length; i++){
654 | this._setupDropzone(dropzones[i]);
655 | }
656 |
657 | // IE <= 9 does not support the File API used for drag+drop uploads
658 | // Any volunteers to enable & test this for IE10?
659 | if (!qq.ie()) {
660 | this._attach(document, 'dragenter', function(e){
661 | if (qq.hasClass(dropArea, self._classes.dropDisabled)) return;
662 |
663 | dropArea.style.display = 'block';
664 | for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; }
665 |
666 | });
667 | }
668 | this._attach(document, 'dragleave', function(e){
669 | var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
670 | // only fire when leaving document out
671 | if (qq.FileUploader.prototype._leaving_document_out(e)) {
672 | for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'none'; }
673 | }
674 | });
675 | qq.attach(document, 'drop', function(e){
676 | for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'none'; }
677 | e.preventDefault();
678 | });
679 | },
680 | _onSubmit: function(id, fileName){
681 | qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
682 | this._addToList(id, fileName);
683 | },
684 | // Update the progress bar & percentage as the file is uploaded
685 | _onProgress: function(id, fileName, loaded, total){
686 | qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments);
687 |
688 | var item = this._getItemByFileId(id);
689 | var size = this._find(item, 'size');
690 | size.style.display = 'inline';
691 |
692 | var text;
693 | var percent = Math.round(loaded / total * 100);
694 |
695 | if (loaded != total) {
696 | // If still uploading, display percentage
697 | text = percent + '% from ' + this._formatSize(total);
698 | } else {
699 | // If complete, just display final size
700 | text = this._formatSize(total);
701 | }
702 |
703 | // Update progress bar tag
704 | this._find(item, 'progressBar').style.width = percent + '%';
705 |
706 | qq.setText(size, text);
707 | },
708 | _onComplete: function(id, fileName, result){
709 | qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments);
710 |
711 | // mark completed
712 | var item = this._getItemByFileId(id);
713 | qq.remove(this._find(item, 'cancel'));
714 | qq.remove(this._find(item, 'spinner'));
715 |
716 | if (result.success){
717 | qq.addClass(item, this._classes.success);
718 | } else {
719 | qq.addClass(item, this._classes.fail);
720 | }
721 | },
722 | _addToList: function(id, fileName){
723 | var item = qq.toElement(this._options.fileTemplate);
724 | item.qqFileId = id;
725 |
726 | var fileElement = this._find(item, 'file');
727 | qq.setText(fileElement, this._formatFileName(fileName));
728 | this._find(item, 'size').style.display = 'none';
729 | if (!this._options.multiple) this._clearList();
730 | this._listElement.appendChild(item);
731 | },
732 | _clearList: function(){
733 | this._listElement.innerHTML = '';
734 | },
735 | _getItemByFileId: function(id){
736 | var item = this._listElement.firstChild;
737 |
738 | // there can't be txt nodes in dynamically created list
739 | // and we can use nextSibling
740 | while (item){
741 | if (item.qqFileId == id) return item;
742 | item = item.nextSibling;
743 | }
744 | },
745 | /**
746 | * delegate click event for cancel link
747 | **/
748 | _bindCancelEvent: function(){
749 | var self = this,
750 | list = this._listElement;
751 |
752 | this._attach(list, 'click', function(e){
753 | e = e || window.event;
754 | var target = e.target || e.srcElement;
755 |
756 | if (qq.hasClass(target, self._classes.cancel)){
757 | qq.preventDefault(e);
758 |
759 | var item = target.parentNode;
760 | self._handler.cancel(item.qqFileId);
761 | qq.remove(item);
762 | }
763 | });
764 | }
765 | });
766 |
767 | qq.UploadDropZone = function(o){
768 | this._options = {
769 | element: null,
770 | onEnter: function(e){},
771 | onLeave: function(e){},
772 | // is not fired when leaving element by hovering descendants
773 | onLeaveNotDescendants: function(e){},
774 | onDrop: function(e){}
775 | };
776 | qq.extend(this._options, o);
777 | qq.extend(this, qq.DisposeSupport);
778 |
779 | this._element = this._options.element;
780 |
781 | this._disableDropOutside();
782 | this._attachEvents();
783 | };
784 |
785 | qq.UploadDropZone.prototype = {
786 | _dragover_should_be_canceled: function(){
787 | return qq.safari() || (qq.firefox() && qq.windows());
788 | },
789 | _disableDropOutside: function(e){
790 | // run only once for all instances
791 | if (!qq.UploadDropZone.dropOutsideDisabled ){
792 |
793 | // for these cases we need to catch onDrop to reset dropArea
794 | if (this._dragover_should_be_canceled){
795 | qq.attach(document, 'dragover', function(e){
796 | e.preventDefault();
797 | });
798 | } else {
799 | qq.attach(document, 'dragover', function(e){
800 | if (e.dataTransfer){
801 | e.dataTransfer.dropEffect = 'none';
802 | e.preventDefault();
803 | }
804 | });
805 | }
806 |
807 | qq.UploadDropZone.dropOutsideDisabled = true;
808 | }
809 | },
810 | _attachEvents: function(){
811 | var self = this;
812 |
813 | self._attach(self._element, 'dragover', function(e){
814 | if (!self._isValidFileDrag(e)) return;
815 |
816 | var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
817 | if (effect == 'move' || effect == 'linkMove'){
818 | e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
819 | } else {
820 | e.dataTransfer.dropEffect = 'copy'; // for Chrome
821 | }
822 |
823 | e.stopPropagation();
824 | e.preventDefault();
825 | });
826 |
827 | self._attach(self._element, 'dragenter', function(e){
828 | if (!self._isValidFileDrag(e)) return;
829 |
830 | self._options.onEnter(e);
831 | });
832 |
833 | self._attach(self._element, 'dragleave', function(e){
834 | if (!self._isValidFileDrag(e)) return;
835 |
836 | self._options.onLeave(e);
837 |
838 | var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
839 | // do not fire when moving a mouse over a descendant
840 | if (qq.contains(this, relatedTarget)) return;
841 |
842 | self._options.onLeaveNotDescendants(e);
843 | });
844 |
845 | self._attach(self._element, 'drop', function(e){
846 | if (!self._isValidFileDrag(e)) return;
847 |
848 | e.preventDefault();
849 | self._options.onDrop(e);
850 | });
851 | },
852 | _isValidFileDrag: function(e){
853 | // e.dataTransfer currently causing IE errors
854 | // IE9 does NOT support file API, so drag-and-drop is not possible
855 | // IE10 should work, but currently has not been tested - any volunteers?
856 | if (qq.ie()) return false;
857 |
858 | var dt = e.dataTransfer,
859 | // do not check dt.types.contains in webkit, because it crashes safari 4
860 | isSafari = qq.safari();
861 |
862 | // dt.effectAllowed is none in Safari 5
863 | // dt.types.contains check is for firefox
864 | return dt && dt.effectAllowed != 'none' &&
865 | (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
866 |
867 | }
868 | };
869 |
870 | qq.UploadButton = function(o){
871 | this._options = {
872 | element: null,
873 | // if set to true adds multiple attribute to file input
874 | multiple: false,
875 | acceptFiles: null,
876 | // name attribute of file input
877 | name: 'file',
878 | onChange: function(input){},
879 | hoverClass: 'qq-upload-button-hover',
880 | focusClass: 'qq-upload-button-focus'
881 | };
882 |
883 | qq.extend(this._options, o);
884 | qq.extend(this, qq.DisposeSupport);
885 |
886 | this._element = this._options.element;
887 |
888 | // make button suitable container for input
889 | qq.css(this._element, {
890 | position: 'relative',
891 | overflow: 'hidden',
892 | // Make sure browse button is in the right side
893 | // in Internet Explorer
894 | direction: 'ltr'
895 | });
896 |
897 | this._input = this._createInput();
898 | };
899 |
900 | qq.UploadButton.prototype = {
901 | /* returns file input element */
902 | getInput: function(){
903 | return this._input;
904 | },
905 | /* cleans/recreates the file input */
906 | reset: function(){
907 | if (this._input.parentNode){
908 | qq.remove(this._input);
909 | }
910 |
911 | qq.removeClass(this._element, this._options.focusClass);
912 | this._input = this._createInput();
913 | },
914 | _createInput: function(){
915 | var input = document.createElement("input");
916 |
917 | if (this._options.multiple){
918 | input.setAttribute("multiple", "multiple");
919 | }
920 |
921 | if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles);
922 |
923 | input.setAttribute("type", "file");
924 | input.setAttribute("name", this._options.name);
925 |
926 | qq.css(input, {
927 | position: 'absolute',
928 | // in Opera only 'browse' button
929 | // is clickable and it is located at
930 | // the right side of the input
931 | right: 0,
932 | top: 0,
933 | fontFamily: 'Arial',
934 | // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
935 | fontSize: '118px',
936 | margin: 0,
937 | padding: 0,
938 | cursor: 'pointer',
939 | opacity: 0
940 | });
941 |
942 | this._element.appendChild(input);
943 |
944 | var self = this;
945 | this._attach(input, 'change', function(){
946 | self._options.onChange(input);
947 | });
948 |
949 | this._attach(input, 'mouseover', function(){
950 | qq.addClass(self._element, self._options.hoverClass);
951 | });
952 | this._attach(input, 'mouseout', function(){
953 | qq.removeClass(self._element, self._options.hoverClass);
954 | });
955 | this._attach(input, 'focus', function(){
956 | qq.addClass(self._element, self._options.focusClass);
957 | });
958 | this._attach(input, 'blur', function(){
959 | qq.removeClass(self._element, self._options.focusClass);
960 | });
961 |
962 | // IE and Opera, unfortunately have 2 tab stops on file input
963 | // which is unacceptable in our case, disable keyboard access
964 | if (window.attachEvent){
965 | // it is IE or Opera
966 | input.setAttribute('tabIndex', "-1");
967 | }
968 |
969 | return input;
970 | }
971 | };
972 |
973 | /**
974 | * Class for uploading files, uploading itself is handled by child classes
975 | */
976 | qq.UploadHandlerAbstract = function(o){
977 | // Default options, can be overridden by the user
978 | this._options = {
979 | debug: false,
980 | action: '/upload.php',
981 | // maximum number of concurrent uploads
982 | maxConnections: 999,
983 | onProgress: function(id, fileName, loaded, total){},
984 | onComplete: function(id, fileName, response){},
985 | onCancel: function(id, fileName){},
986 | onUpload: function(id, fileName, xhr){}
987 | };
988 | qq.extend(this._options, o);
989 |
990 | this._queue = [];
991 | // params for files in queue
992 | this._params = [];
993 | };
994 | qq.UploadHandlerAbstract.prototype = {
995 | log: function(str){
996 | if (this._options.debug && window.console) console.log('[uploader] ' + str);
997 | },
998 | /**
999 | * Adds file or file input to the queue
1000 | * @returns id
1001 | **/
1002 | add: function(file){},
1003 | /**
1004 | * Sends the file identified by id and additional query params to the server
1005 | */
1006 | upload: function(id, params){
1007 | var len = this._queue.push(id);
1008 |
1009 | var copy = {};
1010 | qq.extend(copy, params);
1011 | this._params[id] = copy;
1012 |
1013 | // if too many active uploads, wait...
1014 | if (len <= this._options.maxConnections){
1015 | this._upload(id, this._params[id]);
1016 | }
1017 | },
1018 | /**
1019 | * Cancels file upload by id
1020 | */
1021 | cancel: function(id){
1022 | this._cancel(id);
1023 | this._dequeue(id);
1024 | },
1025 | /**
1026 | * Cancells all uploads
1027 | */
1028 | cancelAll: function(){
1029 | for (var i=0; i= max && i < max){
1067 | var nextId = this._queue[max-1];
1068 | this._upload(nextId, this._params[nextId]);
1069 | }
1070 | }
1071 | };
1072 |
1073 | /**
1074 | * Class for uploading files using form and iframe
1075 | * @inherits qq.UploadHandlerAbstract
1076 | */
1077 | qq.UploadHandlerForm = function(o){
1078 | qq.UploadHandlerAbstract.apply(this, arguments);
1079 |
1080 | this._inputs = {};
1081 | };
1082 | // @inherits qq.UploadHandlerAbstract
1083 | qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
1084 |
1085 | qq.extend(qq.UploadHandlerForm.prototype, {
1086 | add: function(fileInput){
1087 | fileInput.setAttribute('name', this._options.inputName);
1088 | var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
1089 |
1090 | this._inputs[id] = fileInput;
1091 |
1092 | // remove file input from DOM
1093 | if (fileInput.parentNode){
1094 | qq.remove(fileInput);
1095 | }
1096 |
1097 | return id;
1098 | },
1099 | getName: function(id){
1100 | // get input value and remove path to normalize
1101 | return this._inputs[id].value.replace(/.*(\/|\\)/, "");
1102 | },
1103 | _cancel: function(id){
1104 | this._options.onCancel(id, this.getName(id));
1105 |
1106 | delete this._inputs[id];
1107 |
1108 | var iframe = document.getElementById(id);
1109 | if (iframe){
1110 | // to cancel request set src to something else
1111 | // we use src="javascript:false;" because it doesn't
1112 | // trigger ie6 prompt on https
1113 | iframe.setAttribute('src', 'javascript:false;');
1114 |
1115 | qq.remove(iframe);
1116 | }
1117 | },
1118 | _upload: function(id, params){
1119 | this._options.onUpload(id, this.getName(id), false);
1120 | var input = this._inputs[id];
1121 |
1122 | if (!input){
1123 | throw new Error('file with passed id was not added, or already uploaded or cancelled');
1124 | }
1125 |
1126 | var fileName = this.getName(id);
1127 |
1128 | var iframe = this._createIframe(id);
1129 | var form = this._createForm(iframe, params);
1130 | form.appendChild(input);
1131 |
1132 | var self = this;
1133 | this._attachLoadEvent(iframe, function(){
1134 | self.log('iframe loaded');
1135 |
1136 | var response = self._getIframeContentJSON(iframe);
1137 |
1138 | self._options.onComplete(id, fileName, response);
1139 | self._dequeue(id);
1140 |
1141 | delete self._inputs[id];
1142 | // timeout added to fix busy state in FF3.6
1143 | setTimeout(function(){
1144 | self._detach_event();
1145 | qq.remove(iframe);
1146 | }, 1);
1147 | });
1148 |
1149 | form.submit();
1150 | qq.remove(form);
1151 |
1152 | return id;
1153 | },
1154 | _attachLoadEvent: function(iframe, callback){
1155 | this._detach_event = qq.attach(iframe, 'load', function(){
1156 | // when we remove iframe from dom
1157 | // the request stops, but in IE load
1158 | // event fires
1159 | if (!iframe.parentNode){
1160 | return;
1161 | }
1162 |
1163 | // fixing Opera 10.53
1164 | if (iframe.contentDocument &&
1165 | iframe.contentDocument.body &&
1166 | iframe.contentDocument.body.innerHTML == "false"){
1167 | // In Opera event is fired second time
1168 | // when body.innerHTML changed from false
1169 | // to server response approx. after 1 sec
1170 | // when we upload file with iframe
1171 | return;
1172 | }
1173 |
1174 | callback();
1175 | });
1176 | },
1177 | /**
1178 | * Returns json object received by iframe from server.
1179 | */
1180 | _getIframeContentJSON: function(iframe){
1181 | // iframe.contentWindow.document - for IE<7
1182 | var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
1183 | response;
1184 |
1185 | var innerHTML = doc.body.innerHTML;
1186 | this.log("converting iframe's innerHTML to JSON");
1187 | this.log("innerHTML = " + innerHTML);
1188 | //plain text response may be wrapped in tag
1189 | if (innerHTML.slice(0, 5).toLowerCase() == '' && innerHTML.slice(-6).toLowerCase() == ' ') {
1190 | innerHTML = doc.body.firstChild.firstChild.nodeValue;
1191 | }
1192 |
1193 | try {
1194 | response = eval("(" + innerHTML + ")");
1195 | } catch(err){
1196 | response = {};
1197 | }
1198 |
1199 | return response;
1200 | },
1201 | /**
1202 | * Creates iframe with unique name
1203 | */
1204 | _createIframe: function(id){
1205 | // We can't use following code as the name attribute
1206 | // won't be properly registered in IE6, and new window
1207 | // on form submit will open
1208 | // var iframe = document.createElement('iframe');
1209 | // iframe.setAttribute('name', id);
1210 |
1211 | var iframe = qq.toElement('');
1212 | // src="javascript:false;" removes ie6 prompt on https
1213 |
1214 | iframe.setAttribute('id', id);
1215 |
1216 | iframe.style.display = 'none';
1217 | document.body.appendChild(iframe);
1218 |
1219 | return iframe;
1220 | },
1221 | /**
1222 | * Creates form, that will be submitted to iframe
1223 | */
1224 | _createForm: function(iframe, params){
1225 | // We can't use the following code in IE6
1226 | // var form = document.createElement('form');
1227 | // form.setAttribute('method', 'post');
1228 | // form.setAttribute('enctype', 'multipart/form-data');
1229 | // Because in this case file won't be attached to request
1230 | var form = qq.toElement(' ');
1231 |
1232 | var queryString = qq.obj2url(params, this._options.action);
1233 |
1234 | form.setAttribute('action', queryString);
1235 | form.setAttribute('target', iframe.name);
1236 | form.style.display = 'none';
1237 | document.body.appendChild(form);
1238 |
1239 | return form;
1240 | }
1241 | });
1242 |
1243 | /**
1244 | * Class for uploading files using xhr
1245 | * @inherits qq.UploadHandlerAbstract
1246 | */
1247 | qq.UploadHandlerXhr = function(o){
1248 | qq.UploadHandlerAbstract.apply(this, arguments);
1249 |
1250 | this._files = [];
1251 | this._xhrs = [];
1252 |
1253 | // current loaded size in bytes for each file
1254 | this._loaded = [];
1255 | };
1256 |
1257 | // static method
1258 | qq.UploadHandlerXhr.isSupported = function(){
1259 | var input = document.createElement('input');
1260 | input.type = 'file';
1261 |
1262 | return (
1263 | 'multiple' in input &&
1264 | typeof File != "undefined" &&
1265 | typeof FormData != "undefined" &&
1266 | typeof (new XMLHttpRequest()).upload != "undefined" );
1267 | };
1268 |
1269 | // @inherits qq.UploadHandlerAbstract
1270 | qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
1271 |
1272 | qq.extend(qq.UploadHandlerXhr.prototype, {
1273 | /**
1274 | * Adds file to the queue
1275 | * Returns id to use with upload, cancel
1276 | **/
1277 | add: function(file){
1278 | if (!(file instanceof File)){
1279 | throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
1280 | }
1281 |
1282 | return this._files.push(file) - 1;
1283 | },
1284 | getName: function(id){
1285 | var file = this._files[id];
1286 | // fix missing name in Safari 4
1287 | //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
1288 | return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
1289 | },
1290 | getSize: function(id){
1291 | var file = this._files[id];
1292 | return file.fileSize != null ? file.fileSize : file.size;
1293 | },
1294 | /**
1295 | * Returns uploaded bytes for file identified by id
1296 | */
1297 | getLoaded: function(id){
1298 | return this._loaded[id] || 0;
1299 | },
1300 | /**
1301 | * Sends the file identified by id and additional query params to the server
1302 | * @param {Object} params name-value string pairs
1303 | */
1304 | _upload: function(id, params){
1305 | this._options.onUpload(id, this.getName(id), true);
1306 |
1307 | var file = this._files[id],
1308 | name = this.getName(id),
1309 | size = this.getSize(id);
1310 |
1311 | this._loaded[id] = 0;
1312 |
1313 | var xhr = this._xhrs[id] = new XMLHttpRequest();
1314 | var self = this;
1315 |
1316 | xhr.upload.onprogress = function(e){
1317 | if (e.lengthComputable){
1318 | self._loaded[id] = e.loaded;
1319 | self._options.onProgress(id, name, e.loaded, e.total);
1320 | }
1321 | };
1322 |
1323 | xhr.onreadystatechange = function(){
1324 | if (xhr.readyState == 4){
1325 | self._onComplete(id, xhr);
1326 | }
1327 | };
1328 |
1329 | // build query string
1330 | params = params || {};
1331 | params[this._options.inputName] = name;
1332 | var queryString = qq.obj2url(params, this._options.action);
1333 |
1334 | xhr.open("POST", queryString, true);
1335 | xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1336 | xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
1337 | if (this._options.encoding == 'multipart') {
1338 | var formData = new FormData();
1339 | formData.append(name, file);
1340 | file = formData;
1341 | } else {
1342 | xhr.setRequestHeader("Content-Type", "application/octet-stream");
1343 | //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
1344 | xhr.setRequestHeader("X-Mime-Type",file.type );
1345 | }
1346 | for (key in this._options.customHeaders){
1347 | xhr.setRequestHeader(key, this._options.customHeaders[key]);
1348 | };
1349 | xhr.send(file);
1350 | },
1351 | _onComplete: function(id, xhr){
1352 | // the request was aborted/cancelled
1353 | if (!this._files[id]) return;
1354 |
1355 | var name = this.getName(id);
1356 | var size = this.getSize(id);
1357 |
1358 | this._options.onProgress(id, name, size, size);
1359 |
1360 | if (xhr.status == 200){
1361 | this.log("xhr - server response received");
1362 | this.log("responseText = " + xhr.responseText);
1363 |
1364 | var response;
1365 |
1366 | try {
1367 | response = eval("(" + xhr.responseText + ")");
1368 | } catch(err){
1369 | response = {};
1370 | }
1371 |
1372 | this._options.onComplete(id, name, response);
1373 |
1374 | } else {
1375 | this._options.onError(id, name, xhr);
1376 | this._options.onComplete(id, name, {});
1377 | }
1378 |
1379 | this._files[id] = null;
1380 | this._xhrs[id] = null;
1381 | this._dequeue(id);
1382 | },
1383 | _cancel: function(id){
1384 | this._options.onCancel(id, this.getName(id));
1385 |
1386 | this._files[id] = null;
1387 |
1388 | if (this._xhrs[id]){
1389 | this._xhrs[id].abort();
1390 | this._xhrs[id] = null;
1391 | }
1392 | }
1393 | });
1394 |
1395 | /**
1396 | * A generic module which supports object disposing in dispose() method.
1397 | * */
1398 | qq.DisposeSupport = {
1399 | _disposers: [],
1400 |
1401 | /** Run all registered disposers */
1402 | dispose: function() {
1403 | var disposer;
1404 | while (disposer = this._disposers.shift()) {
1405 | disposer();
1406 | }
1407 | },
1408 |
1409 | /** Add disposer to the collection */
1410 | addDisposer: function(disposeFunction) {
1411 | this._disposers.push(disposeFunction);
1412 | },
1413 |
1414 | /** Attach event handler and register de-attacher as a disposer */
1415 | _attach: function() {
1416 | this.addDisposer(qq.attach.apply(this, arguments));
1417 | }
1418 | };
1419 |
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/fileuploader.css:
--------------------------------------------------------------------------------
1 | /* Have ideas for improving this CSS for the general community? Submit your changes at: https://github.com/Valums-File-Uploader/file-uploader */
2 | .qq-uploader {
3 | position: relative;
4 | width: 100%;
5 | }
6 | .qq-upload-button {
7 | display: block;
8 | /*or inline-block*/
9 | width: 105px;
10 | padding: 7px 0;
11 | text-align: center;
12 | background: #880000;
13 | border-bottom: 1px solid #DDD;
14 | color: #FFF;
15 | }
16 | .qq-upload-button-hover {
17 | background: #CC0000;
18 | }
19 | .qq-upload-button-focus {
20 | outline: 1px dotted #000000;
21 | }
22 | .qq-upload-drop-area, .qq-upload-extra-drop-area {
23 | position: absolute;
24 | top: 0;
25 | left: 0;
26 | width: 100%;
27 | height: 100%;
28 | min-height: 30px;
29 | z-index: 2;
30 | background: #FF9797;
31 | text-align: center;
32 | }
33 | .qq-upload-drop-area span {
34 | display: block;
35 | position: absolute;
36 | top: 50%;
37 | width: 100%;
38 | margin-top: -8px;
39 | font-size: 16px;
40 | }
41 | .qq-upload-extra-drop-area {
42 | position: relative;
43 | margin-top: 50px;
44 | font-size: 16px;
45 | padding-top: 30px;
46 | height: 20px;
47 | min-height: 40px;
48 | }
49 | .qq-upload-drop-area-active {
50 | background: #FF7171;
51 | }
52 | .qq-upload-list {
53 | margin: 0;
54 | padding: 0;
55 | list-style: none;
56 | }
57 | .qq-upload-list li {
58 | margin: 0;
59 | padding: 9px;
60 | line-height: 15px;
61 | font-size: 16px;
62 | background-color: #FFF0BD;
63 | }
64 | .qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-failed-text {
65 | margin-right: 12px;
66 | }
67 | .qq-upload-file {
68 | }
69 | .qq-upload-spinner {
70 | display: inline-block;
71 | background: url("loading.gif");
72 | width: 15px;
73 | height: 15px;
74 | vertical-align: text-bottom;
75 | }
76 | .qq-upload-size, .qq-upload-cancel {
77 | font-size: 12px;
78 | font-weight: normal;
79 | }
80 | .qq-upload-failed-text {
81 | display: none;
82 | }
83 | .qq-upload-fail .qq-upload-failed-text {
84 | display: inline;
85 | }
86 | .qq-upload-list li.qq-upload-success {
87 | background-color: #5DA30C;
88 | color: #FFFFFF;
89 | }
90 | .qq-upload-list li.qq-upload-fail {
91 | background-color: #D60000;
92 | color: #FFFFFF;
93 | }
94 |
--------------------------------------------------------------------------------