Ajax uploads? Image manipulation & drag-and-drop sorting.
Posted 12/11/2006 by dnaffis 14 CommentsAdd Your Comment
Wouldn’t it be nice to allow uploads in a cool Ajaxy way? Well, because of security restrictions it’s just not possible. There are however ways to create the same effect.
Here’s a quick demo of an ajax-ish image upload as well as some image manipulation functionality, and drag and drop sorting. I’m not sure this will work on all browsers but it’s been tested successfully with most. This was created about 4 months ago and I never had time to polish any of it up so take what you can from it.
http://www.naffis.com/demos/image_demo
First our layout (layouts/image_demo.rhtml):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/strict.dtd">
<html>
<head>
<title>Image Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<%= javascript_include_tag :defaults %>
<%= stylesheet_link_tag 'image_demo' %>
</head>
<body>
<div id="titlebar">Image Demo</div>
<%= render :partial => "upload_form" %>
<div id="centercontent">
<%= yield %>
</div>
<div id="next">
<%= link_to "Create Animated Gif", :action => "animate" %>
</div>
<div id="bottom">
© naffis.com 2006
</div>
</body>
</html>
We’re going to extend the form_remote_tag to handle file uploads.
Drop this in your lib directory (lib/remote_uploads.rb):
module ActionView
module Helpers
module PrototypeHelper
alias_method :form_remote_tag_old, :form_remote_tag
def form_remote_tag(options = {})
if options[:html] && options[:html][:multipart]
uid = "a#{Time.now.to_f.hash}"
<<-STR
<iframe name="#{uid}" id="#{uid}" src="about:blank" style="position:absolute;left:-100px;width:0px;height:0px;border:0px"></iframe>
<form method="post" action="#{url_for options[:url].update({:iframe_remote => true})}" enctype="multipart/form-data" target="#{uid}" #{%(onsubmit="#{options[:loading]}") if options[:loading]}>
STR
else
form_remote_tag_old(options)
end
end
end
end
end
Add the require in your environment.rb:
require 'remote_uploads.rb'
This will create a custom form for file uploads (multipart => true) that submits to a hidden iframe. If it’s not a file upload then it will revert to the standard form_remote_tag of PrototypeHelper.
Some boring half baked styles for our demo:
body {
background-color: #FFFFFF;
background-image: url(/maps/images/gradient.jpg);
background-repeat: no-repeat;
color: #666666;
font-family: arial, sans;
font-size: 100%;
line-height: 1.7em;
margin: 1em 2em;
}
#titlebar {
font-size: 1.2em;
border-bottom: 2px solid #333333;
margin-bottom: 1em;
padding-bottom: 1em;
}
h2 {
font-size: 1.2em;
}
ul.navigation {
background-color: #333333;
padding: 0em 0.5em;
list-style-type: none;
}
ul.navigation li {
border-right: 1px solid #666666;
display: inline;
}
.navigation a {
color: #FFFFFF;
padding: 0.5em;
}
.description {
font-size: 1.2em;
}
.upload {
font-size: 1.2em;
}
strong {
background-color: #FFFF99;
}
#centercontent {
width: 100%;
text-align: center;
margin-bottom: 1em;
padding-bottom: 1em;
margin-top: 1em;
padding-top: 1em;
}
#bottom {
width: 100%;
float: left;
text-align: center;
border-top: 2px solid #333333;
margin-top: 1em;
padding-top: 1em;
}
div.float {
width: 120px;
padding: 10px;
float: left;
}
div.spacer {
clear: both;
}
div.float img {
margin-left: 5px;
}
div.float p {
font-size: 9px;
text-align: center;
}
#image-list ul {
list-style: none;
}
#image-list ul li {
list-style: none;
display: inline;
float: left;
width: 120px;
height: 120px;
padding: 10px;
border: 1px solid #000;
}
We’re using Sean Treadway’s responds_to_parent plugin (http://sean.treadway.info/svn/plugins/responds_to_parent/) to execute our RJS generated javascript in the parent window instead of the iframe which the file upload is submitted to. There are other ways of doing this that use less code but the plugin is simple so why not use it?
Everything from this point on is pretty self explanitory. I can expand on it later but here’s the rest of the code.
Our index:
<div id="image-list">
<ul id="sortable_list">
<% for @asset in @assets %>
<%= render :partial => "image_container", :locals => { :asset => @asset } %>
<% end %>
</ul>
</div>
<%= sortable_element('sortable_list', :constraint => false, :url => {:action => :update_positions}) %>
Some partials used above:
_image_container.rhtml
<li id="item_<%= @asset.id %>" class="float">
<%= render :partial => "image_thumb", :locals => { :asset => @asset } %>
</li>
_image_thumb.rhtml
<%= image_tag @asset.thumbnail, :border => 2 %>
<br>
<%= link_to_remote(image_tag("arrow_rotate_anticlockwise.png", :border => 0), :url => {:action => "rotate", :id => @asset.id, :direction => "left"} ) %>
<%= link_to_remote(image_tag("cross.png", :border => 0), :url => {:action => "remove", :id => @asset.id} ) %>
<%= link_to_remote(image_tag("arrow_rotate_clockwise.png", :border => 0), :url => {:action => "rotate", :id => @asset.id, :direction => "right"} ) %>
_upload_form.rhtml
<%= form_remote_tag(:url => {
:controller => "image_demo",
:action => "create" },
:html => {:multipart => true}) %>
<b>Picture:</b>
<%= file_field_tag "asset" %>
<%= submit_tag "Upload" %>
<%= end_form_tag %>
Our RJS to handle the create, remove, and rotate.
create.rjs
if @asset.new_record?
page.alert "There was a problem uploading your file:\n" +
@asset.errors.full_messages.join("\n")
else
page.insert_html :top, 'sortable_list', :partial => 'image_container', :locals => { :asset => @asset }
page.visual_effect :highlight, "item_#{@asset.id}"
page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }
end
remove.rjs
page.remove "item_#{@asset_id}"
page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }
rotate.rjs
page.replace_html "item_#{@asset.id}", :partial => 'image_thumb', :locals => { :asset => @asset }
page.visual_effect :highlight, "item_#{@asset.id}"
page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }
Our controller:
class ImageDemoController < ApplicationController
layout 'image_demo'
def index
session[:uid] = Time.now.to_i unless session[:uid]
@assets = Asset.find(:all,
:conditions => ["user_id = ?", session[:uid].to_i],
:order => "position")
end
def create
@asset = Asset.new()
@asset.uploaded_file = params['asset']
@asset.position = 0
@asset.user_id = session[:uid].to_i
@asset.save
responds_to_parent do
render :action => 'create.rjs'
end
return
end
def list
@assets = Asset.find(:all,
:conditions => ["user_id = ?", session[:uid].to_i],
:order => "position")
end
def update_positions
params[:sortable_list].each_with_index do |id, position|
Asset.update(id, :position => position)
end
render :nothing => true
end
def rotate
@asset = Asset.find(params[:id])
degrees = params[:direction] == "left" ? -90 : 90
@asset.rotate(degrees)
end
def remove
@asset_id = params[:id]
Asset.delete(@asset_id)
end
end
Our asset model:
require 'RMagick'
class Asset < ActiveRecord::Base
def uploaded_file=(incoming_file)
content_type = incoming_file.content_type.chomp
if content_type.rindex(/image\/[(jpe?g)||(gif)]/)
self.name = base_part_of(incoming_file.original_filename)
base_dir = "/some/path/you/like"
# save original file
self.original = "image_demo_assets/o_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name
File.open(base_dir+self.original,File::CREAT|File::TRUNC|File::WRONLY,0666){ |f|
f.write(incoming_file.read)
}
self.resized = "image_demo_assets/r_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name
resized = Magick::Image.read(base_dir+self.original).first
resized.change_geometry!('500x500') { |cols, rows, img|
img.resize!(cols, rows)
}
resized.write(base_dir+self.resized)
self.thumbnail = "image_demo_assets/t_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name
thumb = Magick::Image.read(base_dir+self.original).first
thumb.change_geometry!('100x100') { |cols, rows, img|
img.resize!(cols, rows)
}
thumb.write(base_dir+self.thumbnail)
self.save
end
end
def rotate(degrees)
base_dir = "/some/path/you/like"
#main photo
image = Magick::ImageList.new(base_dir+self.original)
image = image.rotate(degrees)
image.write(base_dir+self.original)
# resized
resized = Magick::ImageList.new(base_dir+self.resized)
resized = resized.rotate(degrees)
resized.write(base_dir+self.resized)
# thumb
thumb = Magick::ImageList.new(base_dir+self.thumbnail)
thumb = thumb.rotate(degrees)
thumb.write(base_dir+self.thumbnail)
end
private
def base_part_of(file_name)
name = File.basename(file_name)
name.gsub(/[^W._-]/, '')
sanitize_filename(name)
end
# Fixes a 'feature' of IE where it passes the entire path instead of just the filename
def sanitize_filename(value)
#get only the filename (not the whole path)
just_filename = value.gsub(/^.*(\\|\/)/, '')
just_filename.gsub(/[^\w\.\-]/,'_')
end
end
Some suggestions:
- Use form_for and get rid of some ugliness in the controller by using Asset.new(params[:asset]) instead of setting each value individually.
- Use simply_helpful for generiting your DOM id’s.
- Use acts_as_attachment for handing the storing of files.
- Better validations (aaa will handle that too).
- Rewrite the whole thing.
Again, this is a VERY quick-and-dirty demo written in about 20 minutes with so much room for improvement. If I had the time I would, but alas I hope it helps.
July 15th, 2007 at 06:56 AM
An amazing enlightenment. The formremotetag is very useful to me. I just want to know how can we include the other options like oncomplete in the code. I badly require it.
Thanks and Regards, Jazzy Buddy.
July 20th, 2007 at 01:17 AM
Jazzy, the callback options for formremotetag are the same as link_to_remote. Take a look at http://api.rubyonrails.org/classes/ActionView/Helpers/PrototypeHelper.html#M000529
August 28th, 2007 at 12:37 AM
I using this technique, but it doesn’t work, apparently because the IFRAME tag doesn’t include the default Javascript links. Any idea what I’m missing?
August 28th, 2007 at 01:01 AM
Doh! I didn’t wrap my render call in responds_to_parent do … end.
Now I have another problem though. I’m trying to render the response using the :update option to the formremotetag. Apparently that doesn’t work, because I’m getting a “missing ; before statement” Javascript error.
Does this only work with a render :update => … response and an rjs view?
October 17th, 2007 at 01:30 AM
what are the fields of migration and rmagic
October 17th, 2007 at 02:03 AM
what are the fields to migrate and which plugins to install
November 10th, 2007 at 12:53 PM
Fantastic, thank you.
November 12th, 2007 at 04:45 AM
hi,
November 12th, 2007 at 04:45 AM
hi,
December 7th, 2007 at 03:32 PM
Dave - great code - this saved me a bunch of time. Here are a few things that I think were missing…
The migration:
class CreateAssets < ActiveRecord::Migration def self.up create_table :assets do |t| t.column :user_id, :integer t.column :name, :string t.column :original, :string t.column :thumbnail, :string t.column :resized, :string t.column :position, :integer t.timestamps end end
def self.down drop_table :assets end end
You also need to install the responseto_parent plugin ( http://agilewebdevelopment.com/plugins/respondsto_parent )
And, of course RMagick, which is always a nightmare but different on all systems (I guess users should just google it).
And, finally - they need to grab the rotate/delete graphics off your demo (upload file, grab graphics).
I’ll be making this into a plugin (that also supports S3) when I get a chance.
June 24th, 2008 at 06:08 AM
Nice Site! http://google.com
July 25th, 2008 at 02:26 AM
Hi, this is a fairly old article will this still work with the latest ruby on rails? or is there a better example out there? cheers
November 5th, 2008 at 05:35 AM
sdfsdf
March 20th, 2009 at 07:44 PM
33201 , http://www.simteach.com/wiki/index.php?title=User:Buyaciphex&oldid=9219#1 aciphex generic, ooo6%% , http://www.simteach.com/wiki/index.php?title=User:Buyacomplia&oldid=9221#1 buy acomplia, lk , http://www.simteach.com/wiki/index.php?title=User:Buyallegra&oldid=9223#1 cheap allegra, 68458 , http://www.simteach.com/wiki/index.php?title=User:Buyallopurinol&oldid=9225#1 allopurinol, wsxsw , http://www.simteach.com/wiki/index.php?title=User:Buyavapro&oldid=9227#1 avapro medication, 6354654 , http://www.simteach.com/wiki/index.php?title=User:Buyadalat&oldid=9229#1 adalat cc, vcjjd^%$#@ , http://www.simteach.com/wiki/index.php?title=User:Buyamoxil&oldid=9231#1 amoxil 500mg, wvsxoki , http://www.simteach.com/wiki/index.php?title=User:Buyamoxicillin&oldid=9233#1 amoxicillin trihydrate