支持图片上传, 增加rss订阅
This commit is contained in:
parent
edc538062b
commit
de17536628
|
@ -15,3 +15,5 @@
|
|||
/tmp
|
||||
|
||||
*.swp
|
||||
|
||||
/public/uploads/*
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -40,3 +40,5 @@ gem 'jquery-rails'
|
|||
gem "redcarpet"
|
||||
gem "simple_form"
|
||||
gem 'database_cleaner'
|
||||
gem "mini_magick"
|
||||
gem 'carrierwave-mongoid'
|
||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -33,6 +33,12 @@ GEM
|
|||
bson_ext (1.6.4)
|
||||
bson (~> 1.6.4)
|
||||
builder (3.0.0)
|
||||
carrierwave (0.6.2)
|
||||
activemodel (>= 3.2.0)
|
||||
activesupport (>= 3.2.0)
|
||||
carrierwave-mongoid (0.2.1)
|
||||
carrierwave (~> 0.6.1)
|
||||
mongoid (~> 2.1)
|
||||
coffee-rails (3.2.2)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (~> 3.2.0)
|
||||
|
@ -57,6 +63,8 @@ GEM
|
|||
mime-types (~> 1.16)
|
||||
treetop (~> 1.4.8)
|
||||
mime-types (1.19)
|
||||
mini_magick (3.4)
|
||||
subexec (~> 0.2.1)
|
||||
mongo (1.6.2)
|
||||
bson (~> 1.6.2)
|
||||
mongoid (2.4.11)
|
||||
|
@ -116,6 +124,7 @@ GEM
|
|||
hike (~> 1.2)
|
||||
rack (~> 1.0)
|
||||
tilt (~> 1.1, != 1.3.0)
|
||||
subexec (0.2.2)
|
||||
thor (0.15.3)
|
||||
tilt (1.3.3)
|
||||
treetop (1.4.10)
|
||||
|
@ -131,9 +140,11 @@ PLATFORMS
|
|||
|
||||
DEPENDENCIES
|
||||
bson_ext
|
||||
carrierwave-mongoid
|
||||
coffee-rails (~> 3.2.1)
|
||||
database_cleaner
|
||||
jquery-rails
|
||||
mini_magick
|
||||
mongoid
|
||||
rails (= 3.2.6)
|
||||
redcarpet
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
|
@ -23,3 +23,14 @@ $(document).ready ->
|
|||
$.post $(this).attr('url'), text: content.val(), (data)->
|
||||
preview.html(data)
|
||||
|
||||
$('a#upload_photo').click ->
|
||||
$('input[type=file]').show().focus().click().hide()
|
||||
|
||||
opt =
|
||||
type: 'POST'
|
||||
url: "/photos"
|
||||
success: (data,status,xhr)->
|
||||
insertAtCaret('post_content', data)
|
||||
|
||||
|
||||
$('input[type=file]').fileUpload opt
|
||||
|
|
|
@ -12,4 +12,5 @@
|
|||
//
|
||||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
//= require 'jquery.html5-fileupload'
|
||||
//= require_tree .
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
|
|
@ -0,0 +1,34 @@
|
|||
function insertAtCaret(areaId,text) {
|
||||
var txtarea = document.getElementById(areaId);
|
||||
var scrollPos = txtarea.scrollTop;
|
||||
var strPos = 0;
|
||||
var br = ((txtarea.selectionStart || txtarea.selectionStart == '0') ?
|
||||
"ff" : (document.selection ? "ie" : false ) );
|
||||
if (br == "ie") {
|
||||
txtarea.focus();
|
||||
var range = document.selection.createRange();
|
||||
range.moveStart ('character', -txtarea.value.length);
|
||||
strPos = range.text.length;
|
||||
}
|
||||
else if (br == "ff") strPos = txtarea.selectionStart;
|
||||
|
||||
var front = (txtarea.value).substring(0,strPos);
|
||||
var back = (txtarea.value).substring(strPos,txtarea.value.length);
|
||||
txtarea.value=front+text+back;
|
||||
strPos = strPos + text.length;
|
||||
if (br == "ie") {
|
||||
txtarea.focus();
|
||||
var range = document.selection.createRange();
|
||||
range.moveStart ('character', -txtarea.value.length);
|
||||
range.moveStart ('character', strPos);
|
||||
range.moveEnd ('character', 0);
|
||||
range.select();
|
||||
}
|
||||
else if (br == "ff") {
|
||||
txtarea.selectionStart = strPos;
|
||||
txtarea.selectionEnd = strPos;
|
||||
txtarea.focus();
|
||||
}
|
||||
txtarea.scrollTop = scrollPos;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
|
|
@ -59,3 +59,6 @@ div.preview {
|
|||
display: none;
|
||||
}
|
||||
|
||||
#upload_photo {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
|
||||
div.blogs {
|
||||
width: 700px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.blog {
|
||||
background: url('bg_fn_blog_corner.png') no-repeat;
|
||||
padding: 2em 19px;
|
||||
|
@ -20,7 +25,6 @@ div.blog {
|
|||
}
|
||||
strong {
|
||||
font-size: 10px;
|
||||
line-height: 40px;
|
||||
margin-left: 1em;
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// Place all the styles related to the home controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
|
@ -0,0 +1,3 @@
|
|||
// Place all the styles related to the photos controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
|
@ -0,0 +1,25 @@
|
|||
div.slide {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 400px;
|
||||
.subscribe {
|
||||
margin-bottom: 19px;
|
||||
padding: 9px 0 16px 9px;
|
||||
border: 1px solid #CCC;
|
||||
border-radius: 3px;
|
||||
background-color: #E9F2F5;
|
||||
clear: both;
|
||||
.subscribe_descrip {
|
||||
width: 290px;
|
||||
display: inline-block;
|
||||
line-height: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
a {
|
||||
img {
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,7 +28,8 @@ class Admin::PostsController < ApplicationController
|
|||
|
||||
def preview
|
||||
text = params[:text] || ""
|
||||
md = Redcarpet::Markdown.new(Redcarpet::Render::HTML, :autolink=>true)
|
||||
rd = Redcarpet::Render::HTML.new(:hard_wrap=>true)
|
||||
md = Redcarpet::Markdown.new(rd, :autolink=>true)
|
||||
render :text => md.render(text)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,12 @@ class BlogsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def rss
|
||||
@posts = Post.all.limit(10)
|
||||
render :layout=>false
|
||||
response.headers["Content-Type"] = "application/xml; charset=utf-8"
|
||||
end
|
||||
|
||||
def show
|
||||
@post = Post.find(params[:id])
|
||||
end
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
class HomeController < ApplicationController
|
||||
def index
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
class PhotosController < ApplicationController
|
||||
def create
|
||||
@photo = Photo.new
|
||||
@photo.image = params["Filedata"]
|
||||
@photo.save!
|
||||
render :text=> md_url(@photo.image.url)
|
||||
end
|
||||
|
||||
def md_url(url)
|
||||
"![](#{url})"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
module HomeHelper
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
module PhotosHelper
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
class Comment
|
||||
include Mongoid::Document
|
||||
include Mongoid::Timestamps
|
||||
field :content, :type => String
|
||||
validates :content, presence: true
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
class Photo
|
||||
include Mongoid::Document
|
||||
include Mongoid::Timestamps
|
||||
field :image
|
||||
|
||||
attr_accessible :image
|
||||
|
||||
mount_uploader :image, PhotoUploader
|
||||
end
|
|
@ -14,4 +14,10 @@ class Post
|
|||
validates :title, :presence=>true, :uniqueness=> true
|
||||
validates :content, :presence=>true, :length => { :minimum=> 30 }
|
||||
validates :type, :presence=>true, :inclusion => { :in => [ TECH, LIFE, CREATOR ] }
|
||||
|
||||
def content_html
|
||||
rd = Redcarpet::Render::HTML.new(:hard_wrap=>true)
|
||||
md = Redcarpet::Markdown.new(rd, :autolink=>true)
|
||||
md.render(self.content)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# encoding: utf-8
|
||||
|
||||
class PhotoUploader < CarrierWave::Uploader::Base
|
||||
|
||||
# Include RMagick or MiniMagick support:
|
||||
# include CarrierWave::RMagick
|
||||
include CarrierWave::MiniMagick
|
||||
|
||||
# Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility:
|
||||
# include Sprockets::Helpers::RailsHelper
|
||||
# include Sprockets::Helpers::IsolatedHelper
|
||||
|
||||
# Choose what kind of storage to use for this uploader:
|
||||
storage :file
|
||||
# storage :fog
|
||||
|
||||
# Override the directory where uploaded files will be stored.
|
||||
# This is a sensible default for uploaders that are meant to be mounted:
|
||||
def store_dir
|
||||
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
|
||||
end
|
||||
|
||||
# Provide a default URL as a default if there hasn't been a file uploaded:
|
||||
# def default_url
|
||||
# # For Rails 3.1+ asset pipeline compatibility:
|
||||
# # asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
|
||||
#
|
||||
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
|
||||
# end
|
||||
|
||||
# Process files as they are uploaded:
|
||||
process :resize_to_limit => [680,nil]
|
||||
#
|
||||
# def scale(width, height)
|
||||
# # do something
|
||||
# end
|
||||
|
||||
# Create different versions of your uploaded files:
|
||||
# version :thumb do
|
||||
# process :scale => [50, 50]
|
||||
# end
|
||||
|
||||
# Add a white list of extensions which are allowed to be uploaded.
|
||||
# For images you might use something like this:
|
||||
def extension_white_list
|
||||
%w(jpg jpeg gif png)
|
||||
end
|
||||
|
||||
# Override the filename of the uploaded files:
|
||||
# Avoid using model.id or version_name here, see uploader/store.rb for details.
|
||||
# def filename
|
||||
# "something.jpg" if original_filename
|
||||
# end
|
||||
|
||||
end
|
|
@ -4,6 +4,8 @@
|
|||
<li class="active" id="content">content</li>
|
||||
<li id="preview" url='<%= preview_admin_posts_path %>'>preview</li>
|
||||
</ul>
|
||||
<%= link_to t(:upload_photo), "#", :id=>'upload_photo' %>
|
||||
<input type="file" style="display: none;" />
|
||||
<%= f.input :content, :as=> :text, :label=>false %>
|
||||
<div class="preview"></div>
|
||||
<%= f.input :type, :as=>:select, :collection=> [ Post::TECH, Post::LIFE, Post::CREATOR ], :include_blank=>false %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="blog">
|
||||
<h2><%= link_to (post.title + "<strong> >></strong>").html_safe, blog_path(post) %></h2>
|
||||
<div class="content"><%= post.content %></div>
|
||||
<div class="content"><%= post.content_html.html_safe %></div>
|
||||
</div>
|
||||
<div class="bottom"></div>
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
<%= render :partial=> "post", :collection=> @posts %>
|
||||
<div class="blogs">
|
||||
<%= render :partial=> "post", :collection=> @posts %>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
xml.instruct!
|
||||
|
||||
xml.rss "version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/" do
|
||||
xml.channel do
|
||||
|
||||
xml.title "windy's blog"
|
||||
xml.link url_for :only_path => false, :controller => 'blogs'
|
||||
xml.description "windy's blogs here"
|
||||
|
||||
@posts.each do |post|
|
||||
xml.item do
|
||||
xml.title post.title
|
||||
xml.link blog_url(post)
|
||||
xml.description post.content
|
||||
xml.guid blog_url(post)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,4 +1 @@
|
|||
<div class="blog">
|
||||
<h2><%= @post.title %></h2>
|
||||
<div class="content"><%= @post.content %></div>
|
||||
</div>
|
||||
<%= render :partial=> "post", :locals=> { :post=> @post } %>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<div class="slide">
|
||||
<div class="subscribe">
|
||||
<div class="subscribe_descrip">
|
||||
如果你想关注我的每一篇文章,推荐进行订阅, 将右面的链接复制到你的阅读器里吧.
|
||||
</div>
|
||||
<%= link_to(image_tag('rss.png'), rss_blogs_path) %>
|
||||
</div>
|
||||
<div class="broadcast"></div>
|
||||
<div class="comment"></div>
|
||||
<div class="recommend"></div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<h1>关于我</h1>
|
||||
<p>
|
||||
为什么会有这里?
|
||||
</p>
|
|
@ -19,10 +19,12 @@
|
|||
<li> <a id="creator" href="creator">创业<strong>Creator</strong></a> </li>
|
||||
</ul>
|
||||
<strong class="descrip">WinDy的个人中心 - 记录人生经历 - 技术 生活 And 创业</strong>
|
||||
<a id="about" href="/about">关于我</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<%= yield %>
|
||||
<%= render "common/slide" %>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<h1>Photos#create</h1>
|
||||
<p>Find me in app/views/photos/create.html.erb</p>
|
|
@ -0,0 +1,42 @@
|
|||
require 'rexml/parsers/pullparser'
|
||||
|
||||
class String
|
||||
def truncate_html(len = 30, at_end = nil)
|
||||
p = REXML::Parsers::PullParser.new(self)
|
||||
tags = []
|
||||
new_len = len
|
||||
results = ''
|
||||
while p.has_next? && new_len > 0
|
||||
p_e = p.pull
|
||||
case p_e.event_type
|
||||
when :start_element
|
||||
tags.push p_e[0]
|
||||
results << "<#{tags.last}#{attrs_to_s(p_e[1])}>"
|
||||
when :end_element
|
||||
results << "</#{tags.pop}>"
|
||||
when :text
|
||||
results << p_e[0][0..new_len]
|
||||
new_len -= p_e[0].length
|
||||
else
|
||||
results << "<!-- #{p_e.inspect} -->"
|
||||
end
|
||||
end
|
||||
if at_end
|
||||
results << "..."
|
||||
end
|
||||
tags.reverse.each do |tag|
|
||||
results << "</#{tag}>"
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attrs_to_s(attrs)
|
||||
if attrs.empty?
|
||||
''
|
||||
else
|
||||
' ' + attrs.to_a.map { |attr| %{#{attr[0]}="#{attr[1]}"} }.join(' ')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,3 +3,4 @@
|
|||
|
||||
en:
|
||||
hello: "Hello world"
|
||||
upload_photo: "上传图片"
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
WBlog::Application.routes.draw do
|
||||
root :to => 'blogs#index'
|
||||
resources :blogs, :only=>[:index, :show]
|
||||
resources :blogs, :only=>[:index, :show] do
|
||||
collection do
|
||||
get :rss
|
||||
end
|
||||
end
|
||||
|
||||
# photos
|
||||
resources :photos, :only=>[:create]
|
||||
|
||||
namespace :admin do
|
||||
resources :posts do
|
||||
collection do
|
||||
|
@ -8,6 +16,7 @@ WBlog::Application.routes.draw do
|
|||
end
|
||||
end
|
||||
end
|
||||
match '/about' => 'home#index'
|
||||
match '/admin' => 'admin/posts#new'
|
||||
match '/:type' => 'blogs#index'
|
||||
end
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe HomeController do
|
||||
|
||||
describe "GET 'index'" do
|
||||
it "returns http success" do
|
||||
get 'index'
|
||||
response.should be_success
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe PhotosController do
|
||||
|
||||
describe "GET 'create'" do
|
||||
it "returns http success" do
|
||||
get 'create'
|
||||
response.should be_success
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
require 'spec_helper'
|
||||
|
||||
# Specs in this file have access to a helper object that includes
|
||||
# the HomeHelper. For example:
|
||||
#
|
||||
# describe HomeHelper do
|
||||
# describe "string concat" do
|
||||
# it "concats two strings with spaces" do
|
||||
# helper.concat_strings("this","that").should == "this that"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
describe HomeHelper do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
require 'spec_helper'
|
||||
|
||||
# Specs in this file have access to a helper object that includes
|
||||
# the PhotosHelper. For example:
|
||||
#
|
||||
# describe PhotosHelper do
|
||||
# describe "string concat" do
|
||||
# it "concats two strings with spaces" do
|
||||
# helper.concat_strings("this","that").should == "this that"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
describe PhotosHelper do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Comment do
|
||||
it "comment should not blank" do
|
||||
a = Comment.new
|
||||
a.save.should == false
|
||||
a = Comment.new(content: '11')
|
||||
a.save.should == true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Photo do
|
||||
it "photo with picture will be ok" do
|
||||
a = Photo.new
|
||||
a.save
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe "home/index.html.erb" do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe "photos/create.html.erb" do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
|
@ -0,0 +1,513 @@
|
|||
/*
|
||||
* jQuery HTML5 File Upload
|
||||
*
|
||||
* Author: timdream at gmail.com
|
||||
* Web: http://timc.idv.tw/html5-file-upload/
|
||||
*
|
||||
* Ajax File Upload that use real xhr,
|
||||
* built with getAsBinary, sendAsBinary, FormData, FileReader, ArrayBuffer, BlobBuilder and etc.
|
||||
* works in Firefox 3, Chrome 5, Safari 5 and higher
|
||||
*
|
||||
* Image resizing and uploading currently works in Fx 3 and up, and Chrome 9 (dev) and up only.
|
||||
* Extra settings will allow current Webkit users to upload the original image
|
||||
* or send the resized image in base64 form.
|
||||
*
|
||||
* Usage:
|
||||
* $.fileUploadSupported // a boolean value indicates if the browser is supported.
|
||||
* $.imageUploadSupported // a boolean value indicates if the browser could resize image and upload in binary form.
|
||||
* $.fileUploadAsBase64Supported // a boolean value indicate if the browser upload files in based64.
|
||||
* $.imageUploadAsBase64Supported // a boolean value indicate if the browser could resize image and upload in based64.
|
||||
* $('input[type=file]').fileUpload(ajaxSettings); //Make a input[type=file] select-and-send file upload widget
|
||||
* $('#any-element').fileUpload(ajaxSettings); //Make a element receive dropped file
|
||||
* //TBD $('form#fileupload').fileUpload(ajaxSettings); //Send a ajax form with file
|
||||
* //TBD $('canvas').fileUpload(ajaxSettings); //Upload given canvas as if it's an png image.
|
||||
*
|
||||
* ajaxSettings is the object contains $.ajax settings that will be passed to.
|
||||
* Available extended settings are:
|
||||
* fileType:
|
||||
* regexp check against filename extension; You should always checked it again on server-side.
|
||||
* e.g. /^(gif|jpe?g|png|tiff?)$/i for images
|
||||
* fileMaxSize:
|
||||
* Maxium file size allowed in bytes. Use scientific notation for converience.
|
||||
* e.g. 1E4 for 1KB, 1E8 for 1MB, 1E9 for 10MB.
|
||||
* If you really care the difference between 1024 and 1000, use Math.pow(2, 10)
|
||||
* fileError(info, textStatus, textDescription):
|
||||
* callback function when there is any error preventing file upload to start,
|
||||
* $.ajax and ajax events won't be called when error.
|
||||
* Use $.noop to overwrite default alert function.
|
||||
* imageMaxWidth, imageMaxHeight:
|
||||
* Use any of the two settings to enable client-size image resizing.
|
||||
* Image will be resized to fit into given rectangle.
|
||||
* File size and type limit checking will be ignored.
|
||||
* allowUploadOriginalImage:
|
||||
* Set to true if you accept original image to be uploaded as a fallback
|
||||
* when image resizing functionality is not availible (such as Webkit browsers).
|
||||
* File size and type limit will be enforced.
|
||||
* allowDataInBase64:
|
||||
* Alternatively, you may wish to resize the image anyway and send the data
|
||||
* in base64. The data will be 133% larger and you will need to process it further with
|
||||
* server-side script.
|
||||
* This setting might work with browsers which could read file but cannot send it in original
|
||||
* binary (no known browser are designed this way though)
|
||||
* forceResize:
|
||||
* Set to true will cause the image being re-sampled even if the resized image
|
||||
* has the same demension as the original one.
|
||||
* imageType:
|
||||
* Acceptable values are: 'jpeg', 'png', or 'auto'.
|
||||
*
|
||||
* TBD:
|
||||
* ability to change settings after binding (you can unbind and bind again as a workaround)
|
||||
* multipole file handling
|
||||
* form intergation
|
||||
*
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
// Don't do logging if window.log function does not exist.
|
||||
var log = window.log || $.noop;
|
||||
|
||||
// jQuery.ajax config
|
||||
var config = {
|
||||
fileError: function (info, textStatus, textDescription) {
|
||||
window.alert(textDescription);
|
||||
}
|
||||
};
|
||||
|
||||
// Feature detection
|
||||
|
||||
// Read as binary string: FileReader API || Gecko-specific function (Fx3)
|
||||
var canReadAsBinaryString = (window.FileReader || window.File.prototype.getAsBinary);
|
||||
// Read file using FormData interface
|
||||
var canReadFormData = !!(window.FormData);
|
||||
// Read file into data: URL: FileReader API || Gecko-specific function (Fx3)
|
||||
var canReadAsBase64 = (window.FileReader || window.File.prototype.getAsDataURL);
|
||||
|
||||
var canResizeImageToBase64 = !!(document.createElement('canvas').toDataURL);
|
||||
var canResizeImageToBinaryString = canResizeImageToBase64 && window.atob;
|
||||
var canResizeImageToFile = !!(document.createElement('canvas').mozGetAsFile);
|
||||
|
||||
// Send file in multipart/form-data with binary xhr (Gecko-specific function)
|
||||
// || xhr.send(blob) that sends blob made with ArrayBuffer.
|
||||
var canSendBinaryString = (
|
||||
(window.XMLHttpRequest && window.XMLHttpRequest.prototype.sendAsBinary)
|
||||
|| (window.ArrayBuffer && window.BlobBuilder)
|
||||
);
|
||||
// Send file as in FormData object
|
||||
var canSendFormData = !!(window.FormData);
|
||||
// Send image base64 data by extracting data: URL
|
||||
var canSendImageInBase64 = !!(document.createElement('canvas').toDataURL);
|
||||
|
||||
var isSupported = (
|
||||
(canReadAsBinaryString && canSendBinaryString)
|
||||
|| (canReadFormData && canSendFormData)
|
||||
);
|
||||
var isImageSupported = (
|
||||
canReadAsBase64 && (
|
||||
(canResizeImageToBinaryString && canSendBinaryString)
|
||||
|| (canResizeImageToFile && canSendFormData)
|
||||
)
|
||||
);
|
||||
var isSupportedInBase64 = canReadAsBase64;
|
||||
var isImageSupportedInBase64 = canReadAsBase64 && canResizeImageToBase64;
|
||||
|
||||
var dataURLtoBase64 = function (dataurl) {
|
||||
return dataurl.substring(dataurl.indexOf(',')+1, dataurl.length);
|
||||
}
|
||||
|
||||
// Step 1: check file info and attempt to read the file
|
||||
// paramaters: Ajax settings, File object
|
||||
var handleFile = function (settings, file) {
|
||||
var info = {
|
||||
// properties of standard File object || Gecko 1.9 properties
|
||||
type: file.type || '', // MIME type
|
||||
size: file.size || file.fileSize,
|
||||
name: file.name || file.fileName
|
||||
};
|
||||
|
||||
settings.resizeImage = !!(settings.imageMaxWidth || settings.imageMaxHeight);
|
||||
|
||||
if (settings.resizeImage && !isImageSupported && settings.allowUploadOriginalImage) {
|
||||
log('WARN: Fall back to upload original un-resized image.');
|
||||
settings.resizeImage = false;
|
||||
}
|
||||
|
||||
if (settings.resizeImage) {
|
||||
settings.imageMaxWidth = settings.imageMaxWidth || Infinity;
|
||||
settings.imageMaxHeight = settings.imageMaxHeight || Infinity;
|
||||
}
|
||||
|
||||
if (!settings.resizeImage) {
|
||||
if (settings.fileType && settings.fileType.test) {
|
||||
// Not using MIME types
|
||||
if (!settings.fileType.test(info.name.substr(info.name.lastIndexOf('.')+1))) {
|
||||
log('ERROR: Invalid Filetype.');
|
||||
settings.fileError.call(this, info, 'INVALID_FILETYPE', 'Invalid filetype.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.fileMaxSize && file.size > settings.fileMaxSize) {
|
||||
log('ERROR: File exceeds size limit.');
|
||||
settings.fileError.call(this, info, 'FILE_EXCEEDS_SIZE_LIMIT', 'File exceeds size limit.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!settings.resizeImage && canReadFormData) {
|
||||
log('INFO: Bypass file reading, insert file object into FormData object directly.');
|
||||
handleForm(settings, 'file', file, info);
|
||||
} else if (window.FileReader) {
|
||||
log('INFO: Using FileReader to do asynchronously file reading.');
|
||||
var reader = new FileReader();
|
||||
reader.onerror = function (ev) {
|
||||
if (ev.target.error) {
|
||||
switch (ev.target.error) {
|
||||
case 8:
|
||||
log('ERROR: File not found.');
|
||||
settings.fileError.call(this, info, 'FILE_NOT_FOUND', 'File not found.');
|
||||
break;
|
||||
case 24:
|
||||
log('ERROR: File not readable.');
|
||||
settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.');
|
||||
break;
|
||||
case 18:
|
||||
log('ERROR: File cannot be access due to security constrant.');
|
||||
settings.fileError.call(this, info, 'SECURITY_ERROR', 'File cannot be access due to security constrant.');
|
||||
break;
|
||||
case 20: //User Abort
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!settings.resizeImage) {
|
||||
if (canSendBinaryString) {
|
||||
reader.onloadend = function (ev) {
|
||||
var bin = ev.target.result;
|
||||
handleForm(settings, 'bin', bin, info);
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
} else if (settings.allowDataInBase64) {
|
||||
reader.onloadend = function (ev) {
|
||||
handleForm(
|
||||
settings,
|
||||
'base64',
|
||||
dataURLtoBase64(ev.target.result),
|
||||
info
|
||||
);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
log('ERROR: No available method to extract file; allowDataInBase64 not set.');
|
||||
settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.');
|
||||
}
|
||||
} else {
|
||||
reader.onloadend = function (ev) {
|
||||
var dataurl = ev.target.result;
|
||||
handleImage(settings, dataurl, info);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
} else if (window.File.prototype.getAsBinary) {
|
||||
log('WARN: FileReader does not exist, UI will be blocked when reading big file.');
|
||||
if (!settings.resizeImage) {
|
||||
try {
|
||||
var bin = file.getAsBinary();
|
||||
} catch (e) {
|
||||
log('ERROR: File not readable.');
|
||||
settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.');
|
||||
return;
|
||||
}
|
||||
handleForm(settings, 'bin', bin, info);
|
||||
} else {
|
||||
try {
|
||||
var bin = file.getAsDataURL();
|
||||
} catch (e) {
|
||||
log('ERROR: File not readable.');
|
||||
settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.');
|
||||
return;
|
||||
}
|
||||
handleImage(settings, dataurl, info);
|
||||
}
|
||||
} else {
|
||||
log('ERROR: No available method to extract file; this browser is not supported.');
|
||||
settings.fileError.call(this, info, 'NOT_SUPPORT', 'ERROR: No available method to extract file; this browser is not supported.');
|
||||
}
|
||||
};
|
||||
|
||||
// step 1.5: inject file into <img>, paste the pixels into <canvas>,
|
||||
// read the final image
|
||||
var handleImage = function (settings, dataurl, info) {
|
||||
var img = new Image();
|
||||
img.onerror = function () {
|
||||
log('ERROR: <img> failed to load, file is not a supported image format.');
|
||||
settings.fileError.call(this, info, 'FILE_NOT_IMAGE', 'File is not a supported image format.');
|
||||
};
|
||||
img.onload = function () {
|
||||
var ratio = Math.max(
|
||||
img.width/settings.imageMaxWidth,
|
||||
img.height/settings.imageMaxHeight,
|
||||
1
|
||||
);
|
||||
var d = {
|
||||
w: Math.floor(Math.max(img.width/ratio, 1)),
|
||||
h: Math.floor(Math.max(img.height/ratio, 1))
|
||||
}
|
||||
log(
|
||||
'INFO: Original image size: ' + img.width.toString(10) + 'x' + img.height.toString(10)
|
||||
+ ', resized image size: ' + d.w + 'x' + d.h + '.'
|
||||
);
|
||||
if (!settings.forceResize && img.width === d.w && img.height === d.h) {
|
||||
log('INFO: Image demension is the same, send the original file.');
|
||||
if (canResizeImageToBinaryString) {
|
||||
handleForm(
|
||||
settings,
|
||||
'bin',
|
||||
window.atob(dataURLtoBase64(dataurl)),
|
||||
info
|
||||
);
|
||||
} else if (settings.allowDataInBase64) {
|
||||
handleForm(
|
||||
settings,
|
||||
'base64',
|
||||
dataURLtoBase64(dataurl),
|
||||
info
|
||||
);
|
||||
} else {
|
||||
log('ERROR: No available method to send the original file; allowDataInBase64 not set.');
|
||||
settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('width', d.w);
|
||||
canvas.setAttribute('height', d.h);
|
||||
canvas.getContext('2d').drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
img.width,
|
||||
img.height,
|
||||
0,
|
||||
0,
|
||||
d.w,
|
||||
d.h
|
||||
);
|
||||
if (!settings.imageType || settings.imageType === 'auto') {
|
||||
if (info.type === 'image/jpeg') settings.imageType = 'jpeg';
|
||||
else settings.imageType = 'png';
|
||||
}
|
||||
|
||||
var ninfo = {
|
||||
type: 'image/' + settings.imageType,
|
||||
name: info.name.substr(0, info.name.indexOf('.')) + '.resized.' + settings.imageType
|
||||
};
|
||||
|
||||
if (canResizeImageToFile && canSendFormData) {
|
||||
// Gecko 2 (Fx4) non-standard function
|
||||
var nfile = canvas.mozGetAsFile(
|
||||
ninfo.name,
|
||||
'image/' + settings.imageType
|
||||
);
|
||||
ninfo.size = file.size || file.fileSize;
|
||||
handleForm(
|
||||
settings,
|
||||
'file',
|
||||
nfile,
|
||||
ninfo
|
||||
);
|
||||
} else if (canResizeImageToBinaryString && canSendBinaryString) {
|
||||
// Read the image as DataURL, convert it back to binary string.
|
||||
var bin = window.atob(dataURLtoBase64(canvas.toDataURL('image/' + settings.imageType)));
|
||||
ninfo.size = bin.length;
|
||||
handleForm(
|
||||
settings,
|
||||
'bin',
|
||||
bin,
|
||||
ninfo
|
||||
);
|
||||
} else if (settings.allowDataInBase64 && canResizeImageToBase64 && canSendImageInBase64) {
|
||||
handleForm(
|
||||
settings,
|
||||
'base64',
|
||||
dataURLtoBase64(canvas.toDataURL('image/' + settings.imageType)),
|
||||
ninfo
|
||||
);
|
||||
} else {
|
||||
log('ERROR: No available method to extract image; allowDataInBase64 not set.');
|
||||
settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.');
|
||||
}
|
||||
}
|
||||
img.src = dataurl;
|
||||
}
|
||||
// Step 2: construct form data and send the file
|
||||
// paramaters: Ajax settings, File object, binary string of file || null, file info assoc array
|
||||
var handleForm = function (settings, type, data, info) {
|
||||
if (canSendFormData && type === 'file') {
|
||||
// FormData API saves the day
|
||||
log('INFO: Using FormData to construct form.');
|
||||
var formdata = new FormData();
|
||||
formdata.append('Filedata', data);
|
||||
// Prevent jQuery form convert FormData object into string.
|
||||
settings.processData = false;
|
||||
// Prevent jQuery from overwrite automatically generated xhr content-Type header
|
||||
// by unsetting the default contentType and inject data only right before xhr.send()
|
||||
settings.contentType = null;
|
||||
settings.__beforeSend = settings.beforeSend;
|
||||
settings.beforeSend = function (xhr, s) {
|
||||
s.data = formdata;
|
||||
if (s.__beforeSend) return s.__beforeSend.call(this, xhr, s);
|
||||
}
|
||||
//settings.data = formdata;
|
||||
} else if (canSendBinaryString && type === 'bin') {
|
||||
log('INFO: Concat our own multipart/form-data data string.');
|
||||
|
||||
// A placeholder MIME type
|
||||
if (!info.type) info.type = 'application/octet-stream';
|
||||
|
||||
if (/[^\x20-\x7E]/.test(info.name)) {
|
||||
log('INFO: Filename contains non-ASCII code, do UTF8-binary string conversion.');
|
||||
info.name_bin = unescape(encodeURIComponent(info.name));
|
||||
}
|
||||
|
||||
//filtered out non-ASCII chars in filenames
|
||||
// info.name = info.name.replace(/[^\x20-\x7E]/g, '_');
|
||||
|
||||
// multipart/form-data boundary
|
||||
var bd = 'xhrupload-' + parseInt(Math.random()*(2 << 16));
|
||||
settings.contentType = 'multipart/form-data; boundary=' + bd;
|
||||
var formdata = '--' + bd + '\n' // RFC 1867 Format, simulate form file upload
|
||||
+ 'content-disposition: form-data; name="Filedata";'
|
||||
+ ' filename="' + (info.name_bin || info.name) + '"\n'
|
||||
+ 'Content-Type: ' + info.type + '\n\n'
|
||||
+ data + '\n\n'
|
||||
+ '--' + bd + '--';
|
||||
|
||||
if (window.XMLHttpRequest.prototype.sendAsBinary) {
|
||||
// Use xhr.sendAsBinary that takes binary string
|
||||
log('INFO: Pass binary string to xhr.');
|
||||
settings.data = formdata;
|
||||
} else {
|
||||
// make a blob
|
||||
log('INFO: Convert binary string into Blob.');
|
||||
var buf = new ArrayBuffer(formdata.length);
|
||||
var view = new Uint8Array(buf);
|
||||
$.each(
|
||||
formdata,
|
||||
function (i, o) {
|
||||
view[i] = o.charCodeAt(0);
|
||||
}
|
||||
);
|
||||
var bb = new BlobBuilder();
|
||||
bb.append(buf);
|
||||
var blob = bb.getBlob();
|
||||
|
||||
settings.processData = false;
|
||||
settings.__beforeSend = settings.beforeSend;
|
||||
settings.beforeSend = function (xhr, s) {
|
||||
s.data = blob;
|
||||
if (s.__beforeSend) return s.__beforeSend.call(this, xhr, s);
|
||||
};
|
||||
}
|
||||
|
||||
} else if (settings.allowDataInBase64 && type === 'base64') {
|
||||
log('INFO: Concat our own multipart/form-data data string; send the file in base64 because binary xhr is not supported.');
|
||||
|
||||
// A placeholder MIME type
|
||||
if (!info.type) info.type = 'application/octet-stream';
|
||||
|
||||
// multipart/form-data boundary
|
||||
var bd = 'xhrupload-' + parseInt(Math.random()*(2 << 16));
|
||||
settings.contentType = 'multipart/form-data; boundary=' + bd;
|
||||
settings.data = '--' + bd + '\n' // RFC 1867 Format, simulate form file upload
|
||||
+ 'content-disposition: form-data; name="Filedata";'
|
||||
+ ' filename="' + encodeURIComponent(info.name) + '.base64"\n'
|
||||
+ 'Content-Transfer-Encoding: base64\n' // Vaild MIME header, but won't work with PHP file upload handling.
|
||||
+ 'Content-Type: ' + info.type + '\n\n'
|
||||
+ data + '\n\n'
|
||||
+ '--' + bd + '--';
|
||||
} else {
|
||||
log('ERROR: Data is not given in processable form.');
|
||||
settings.fileError.call(this, info, 'INTERNAL_ERROR', 'Data is not given in processable form.');
|
||||
return;
|
||||
}
|
||||
xhrupload(settings);
|
||||
};
|
||||
|
||||
// Step 3: start sending out file
|
||||
var xhrupload = function (settings) {
|
||||
log('INFO: Sending file.');
|
||||
if (typeof settings.data === 'string' && canSendBinaryString) {
|
||||
log('INFO: Using xhr.sendAsBinary.');
|
||||
settings.___beforeSend = settings.beforeSend;
|
||||
settings.beforeSend = function (xhr, s) {
|
||||
xhr.send = xhr.sendAsBinary;
|
||||
if (s.___beforeSend) return s.___beforeSend.call(this, xhr, s);
|
||||
}
|
||||
}
|
||||
$.ajax(settings);
|
||||
};
|
||||
|
||||
$.fn.fileUpload = function(settings) {
|
||||
this.each(function(i, el) {
|
||||
if ($(el).is('input[type=file]')) {
|
||||
log('INFO: binding onchange event to a input[type=file].');
|
||||
$(el).bind(
|
||||
'change',
|
||||
function () {
|
||||
if (!this.files.length) {
|
||||
log('ERROR: no file selected.');
|
||||
return;
|
||||
} else if (this.files.length > 1) {
|
||||
log('WARN: Multiple file upload not implemented yet, only first file will be uploaded.');
|
||||
}
|
||||
handleFile($.extend({}, config, settings), this.files[0]);
|
||||
|
||||
if (this.form.length === 1) {
|
||||
this.form.reset();
|
||||
} else {
|
||||
log('WARN: Unable to reset file selection, upload won\'t be triggered again if user selects the same file.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($(el).is('form')) {
|
||||
log('ERROR: <form> not implemented yet.');
|
||||
} else {
|
||||
log('INFO: binding ondrop event.');
|
||||
$(el).bind(
|
||||
'dragover', // dragover behavior should be blocked for drop to invoke.
|
||||
function(ev) {
|
||||
return false;
|
||||
}
|
||||
).bind(
|
||||
'drop',
|
||||
function (ev) {
|
||||
if (!ev.originalEvent.dataTransfer.files) {
|
||||
log('ERROR: No FileList object present; user might had dropped text.');
|
||||
return false;
|
||||
}
|
||||
if (!ev.originalEvent.dataTransfer.files.length) {
|
||||
log('ERROR: User had dropped a virual file (e.g. "My Computer")');
|
||||
return false;
|
||||
}
|
||||
if (!ev.originalEvent.dataTransfer.files.length > 1) {
|
||||
log('WARN: Multiple file upload not implemented yet, only first file will be uploaded.');
|
||||
}
|
||||
handleFile($.extend({}, config, settings), ev.originalEvent.dataTransfer.files[0]);
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
$.fileUploadSupported = isSupported;
|
||||
$.imageUploadSupported = isImageSupported;
|
||||
$.fileUploadAsBase64Supported = isSupportedInBase64;
|
||||
$.imageUploadAsBase64Supported = isImageSupportedInBase64;
|
||||
|
||||
})(jQuery);
|
Loading…
Reference in New Issue