December 11th, 1:38am 1 comment

Ajax uploads? Image manipulation & drag-and-drop sorting.

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">            &copy; 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                    <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  endend

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"} ) %>    &nbsp;    <%= link_to_remote(image_tag("cross.png", :border => 0), :url => {:action => "remove", :id => @asset.id} ) %>    &nbsp;    <%= 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>&nbsp;    <%= file_field_tag "asset" %>&nbsp;    <%= submit_tag "Upload" %>&nbsp;<%= 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)  endend


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\.\-]/,'_')  endend


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.

Posted