├── .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 | 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('