Part Two: How to Create an Image Selection & Upload Plugin for tinyMCE with Ruby on Rails

Update: A sample project is now available

In the first part of our how-to we created a tinyMCE plugin with a dynamic image selector panel based on the advimage plugin. Now we will add functionality to upload images or select an existing image.

Our plugin needed to satisfy the following requirements:

  • Use the existing inline popup (IFRAME) adding a form for uploading an image. We did not want the user to be directed to another page or create a pop up window.
  • In javascript determine which controller to call in the form because we can not pass a variable from the parent to the image selection IFRAME.
  • The image selection IFRAME should not reload when the form is submitted. Reloading the IFRAME would trigger another fetch of uploaded images and that should be avoided.
  • After an image is successfully uploaded the source and alternative text fields should be set as if the user had clicked on an existing image and the IFRAME should then disappear using the plugins existing functionality.

picture-1.png

To follow along with this tutorial you will need:

With what we want our updated plugin to do, let’s get started!

  1. The original image.htm (tiny_mce/plugins/ts_advimage/image.htm) has a form tag right after the body tag, we need to relocate this form in order for our image upload to function properly:
    <body id="advimage" onload="tinyMCEPopup.executeOnLoad('init();');" style="display: none">
    
        <form onsubmit="insertAction();return false;" action="#">
    
    

    Relocate the form just before the tabs div:

    <form onsubmit="insertAction();return false;" action="#">
    
     <div class="tabs">
    
    
  2. Add a form for the image upload image.htm. Create a new div called “image-upload” to house the form. The form needs to have multipart/form-data encoding and will call a javascript function ts_onload (which we will write shortly) on submit. The target for our form is html_editor_image_upload_frame which we will create dynamically to avoid javascript errors when the plugin removes the IFRAME containing the inline popup window.
    <div id="image-upload">
    
      <fieldset>
    
        <legend>Upload New Image</legend>
        <form id='image_upload_form' enctype="multipart/form-data" method="post" onsubmit="ts_onload(); return true;" target="html_editor_image_upload_frame">
    
          <input class="input-file" id="image_uploaded_data" name="image[uploaded_data]" size="30" type="file" />
    
          <div class="submit">
    
            <input class="input-submit" name="commit" type="submit" value="Upload & Insert" />
    
          </div>
    
        </form>
    
      </fieldset>
    
    </div>
    
    
  3. Adding the form to the panel breaks the plugin because the plugin was originally written assuming that there would only be one form. We need to change instances of document.forms[0] to document.forms[1]. We could simple do a find and replace but if we ever add a form in the future we would have to do that again. Instead let’s create a function called formElement that returns the proper form:
    function formElement() {
    
     return document.forms[1];
    
    }
    

    Now, search for instances of document.forms[0] and replace them with calls to formElement(); (there should be sixteen).

  4. Create javascript functions to create the IFRAME and determine the path for the form action in tiny_mce/plugins/ts_advimage/jscripts/functions.jsts_onload: Create a hidden one pixel by one pixel IFRAME with an id of html_editor_image_upload_frame and add it to the end of the image-upload div we created in step two. The function also modifies our form action with the result from ts_upload_image_path(). The code highlighted below was generated [2] by DOMTool v1.1
    function ts_onload(){
    
     var iframe1=ts_ce('iframe','html_editor_image_upload_frame');
    
     iframe1.setAttribute('src','about:blank');
    
     iframe1.style.border="0px none";
    
     iframe1.style.position="absolute";
    
     iframe1.style.width="1px";
    
     iframe1.style.height="1px";
    
     iframe1.style.visibility="hidden";
    
     iframe1.setAttribute('id','html_editor_image_upload_frame');
    
     $('image-upload').appendChild(iframe1);
    
     $('image_upload_form').setAttribute("action", ts_upload_image_path());
    
    }
    

    ts_ce: A helper for creating the iframe

    function ts_ce(tag,name){
    
      if (name && window.ActiveXObject){
    
        element = document.createElement('<'+tag+' name="'+name+'">');
    
      }else{
    
        element = document.createElement(tag);
    
        element.setAttribute('name',name);
    
      }
    
      return element;
    
    }
    
    
    
    

    ts_upload_image_path: Determine the action for the image upload form based on the URI of the parent. Divide the path into segments delimited by a forward slash. Our path_prefix is always the token after the domain name. The result is a string used to call the controller with the action for uploading images.

    function ts_upload_image_path() {
    
      path_prefix = window.parent.location.pathname.split("/")[1];
    
      to_path = "/" + path_prefix + "/manage_images.js";
    
      return to_path;
    
    }
    
        
  5. With the HTML and javascript completed, let’s revisit our manage images controller. In part one our controllers respond_to block looked like this:
        respond_to do |format|
    
          format.html
    
          format.js
    
        end
    
    

    We need to modify our create action so it notifies the parent IFRAME that the image was uploaded. We will use a responds_to_parent block to call the javascript ts_insert_image function we wrote in part one:

    def create
    
      @image = handle_uploaded_image params[:image]
    
      respond_to do |format|
    
        if @image.save
    
          format.js do
    
            responds_to_parent do
    
              render :update do |page|
    
                page << "ts_insert_image('#{@image.public_filename()}', '#{@image.name}');"
    
              end
    
            end
    
          end
    
        else
    
          format.js do
    
            responds_to_parent do
    
              render :update do |page|
    
                page.alert('sorry, error uploading image')
    
              end
    
            end
    
          end
    
        end
    
      end
    
    end
    
  6. One final step. We need to modify the responds_to_parent plugin (vendor/plugins/responds_to_parent/lib/responds_to_parent.rb) to fix a javascript error caused when the IFRAME is removed from the DOM by the tinyMCE plugin. Change the rendor block near the end of the file from:
    render :text => "<html><body><script type='text/javascript' charset='utf-8'>
    
            var loc = document.location;
    
            with(window.parent) { setTimeout(function() { window.eval('#{script}'); loc.replace('about:blank'); }, 1) }
    
          </script></body></html>"
    
    

    To:

    render :text => "<html><body><script type='text/javascript' charset='utf-8'>
    
      var loc = document.location;
    
      with(window.parent) { setTimeout(function() { window.eval('#{script}'); try { loc.replace('about:blank'); } catch(ignored){} }, 1) }
    
    </script></body></html>"
    

You now have a new tinyMCE image plugin that allows a user to select an existing uploaded image or use a form to upload a new image. Once the image is uploaded the IFRAME is removed from the DOM and the image is inserted into the tinyMCE text box.

Notes:

  1. The responds_to_parent plugin allows “AJAX” style file uploads by posting a form to a hidden IFRAME.
  2. Dynamic IFRAME javascript code was generated by http://muffinresearch.co.uk/code/javascript/DOMTool/ – “The compatibility mode for attributes in Internet Explorer is only required should the user need to use innerHTML or outerHTML to get hold of the markup for a form. Using either name properties or setAttribute results in the name attributes being present in the document but not available to innerHTML or outerHTML. When disabled DOMTool uses setAttribute to generate name attributes”
Advertisements
Explore posts in the same categories: User Interface

28 Comments on “Part Two: How to Create an Image Selection & Upload Plugin for tinyMCE with Ruby on Rails”


  1. […] hope you enjoyed this post. Part two is out as well! will be out soon so stay tuned! Filed under: User Interface […]

  2. Juan Says:

    Hi, I followed your tutorial but I couldn’t make it work. Would you share the source files? I had a hard time copying and pasting into Eclipse because of the quotes characters used in this tutorial.

    Thanks,
    Juan.

  3. Dan Says:

    Hi Juan, I have updated the tutorial with a link to a sample project.

  4. Matthew Says:

    Fantastic work! I’m encountering a slight problem, though. When an image is inserted, it has a relative path, and displays fine when in edit mode. The edit url is /post/edit, and the inserted image path is something like ../images/0000/0001/foo.png. However, when I go to reedit or to display the post, the url is something like this /post/edit/3, resulting in a broken image. Is there a way to insert the absolute, as opposed to the relative, path?

    Thanks!

  5. Dan Says:

    @Matthew, Paths are handled by the attachment_fu plugin. If you would like to use an absolute path you could create your own public_filename method in the image model, you would probably want to alias the original public_filename method from the plugin.

    Something along the lines of:

    alias public_filename_orig public_filename
    def public_filename(url)
    “#{url}#{public_filename_orig}”
    end

    Should do the trick.

    – D

  6. Rimu Says:

    also add this to your routes.rb

    map.resources :stories
    map.resources :my_images, :controller => ‘images’


  7. […] Pictures are a big part of the Sprout! experience. With Attachment Fu, a good bit of Javascript a new tinyMCE plugin and a splash of CSS we were off to the races. Attachment fu managed the the uploaded images, […]

  8. Umberto Says:

    Thak you for this tutorial!

  9. chucki Says:

    Hello,

    thank you for your wonderfull work. I have same problem with relative paths and I don’t understand how you mean to create method. Have you same example please,

    Thanks a lot. chucki

  10. Dan Says:

    @chucki. Sorry my comment wasn’t clear. The attachment_fu plugin has a method public_filename. Default behavior for this method returns a relative path.

    Have you tried the sample code? It uses relative paths.

  11. DrMark Says:

    Hi,

    Thanks for creating a great tutorial. I am examining the sample project and I noticed an issue. If you go to the /stories/new page, you will notice that the add image button is not visible in the editor (I am on OSX Leopard). This makes it rather difficult to investigate your solution 🙂

    In the console I am receiving this error:

    Processing ApplicationController#index (for 127.0.0.1 at 2007-11-15 08:18:26) [GET]
    Session ID: dd5c1ac7676324209b53ccd93dfb70d4
    Parameters: {}

    ActionController::RoutingError (no route found to match “/images/shd-ul-800-blu.png” with {:method=>:get}):

    I searched Google and your site to no avail. Can you please explain the missing image button and/or provide the missing “/images/shd-ul-800-blu.png” image?

    Thanks!

  12. DrMark Says:

    By the way, in the application_helper.rb file in the use_tinymce configuration you have:

    plugins : “inlinepopups,ts_advimage,advlink,emotions,iespell,zoom,searchreplace,fullscreen,visualchars,youtube”,
    theme_advanced_buttons1 : “undo,redo,removeformat,|,formatselect,|,bold,italic,underline,strikethrough,sub,sup,|,outdent,indent,bullist,numlist,hr,backcolor,|,link,unlink,charmap,emotions,ts_image,youtube,iespell,|,search,replace,|,cleanup,code,fullscreen,help”,

    You have the button ts_image next to the Youtube button. Should this be ts_advimage to match the advanced image plugin? I tried changing it but the button still doesn’t load.

    Any ideas on the missing button?

    Thanks!

  13. Dan Says:

    @DrMark,
    The shd-ul-800-blu.png image is a design element left over from the project I pulled this code out of. It can be safely removed from scaffold.css line 75.

    I just tried the sample code on Leopard in Safari and FireFox the icon appears in the editor toolbar for /stories/new.

    The button name is ts_image set in editor_plugin.js.

    One thing I did forget to mention in the post is that the sample code uses attachment_fu and the RMagick image processor. (attachment_fu comes with the sample code as a plugin) RMagick would need to be downloaded and compiled (or if you are using locomotive the project needs framework: Rmagick Rails Mar 2007.

  14. Ian Lotinsky Says:

    responds_to_parent doesn’t appear to be hosted anymore. Any idea where I can get a copy?

    Thanks!

  15. Ian Lotinsky Says:

    Never mind. I got it from

    http://www.koders.com/ruby/fidBB6E8DB515F40AABB9E81D6E7670D27618031781.aspx?s=responds_to_parent

    Again, this works as advertised. Thanks!

    If you’re ever in the DC/MD/VA area, I owe you a drink of your choosing (beer, wine, coffee, whatever).

  16. DrMark Says:

    LOL…I discovered the problem with the missing image upload button. When you said that it worked in Safari, I tried it…and it worked.

    Adblock Plus in Firefox was the culprit. It has a rule that blocks URLs with Ad in the string. Once I disabled ABP, all is well.

    Thanks!!

  17. Eric Says:

    Hi, I’m going to try your code. Hopefully it will fix the problems i have! tnx!

  18. Richard Says:

    It would be nice if someone figured out a way to include the upload form within the corresponding tab panel. I see in your example your upload form is outside the tabs scope and ‘wrapper form’ needed by mctabs..i’ve been scratching my head for a while now on a solution to keep the upload form within the upload panel…i do not understand why a wrapper form is needed for mctabs.. In any case, i think the only solution would be heavy dom manipulation..arg

  19. code-idiot Says:

    sorry but i seam to be a little idiot. please can anybody send me the final code ore tell me where to download it? there are many things i don´t understand here …


  20. Looks good! Do you have any advice on how to incorporate flickr sliders? I imagine extending your code to add images to the gallery from flickr wouldn’t be hard.

    I am not sure how to communicate with tinymce about inserting a slider, and handling selection, deletes, resize etc. — Stephan

  21. Gary Says:

    I want to confirm that the plug-in runs on Rails 2.0.

    I’m not sure if the Rails version is the problem…

    I grabbed the code from the sample project. I have a problem uploading and inserting. No errors – just doesn’t create the image object in the model.

    Any tips?

    Thanks.


  22. […] you’ll want to be able to add images inline style. Here’s a useful tutorial Part 1 and Part 2 for […]

  23. Florian Says:

    Hi,

    I have this error :

    RJS error:
    TypeError: $(“#dynamic_images_list”) is null

    Rails 2.2.2
    jRails

    Thank you,
    Florian

  24. KRISS Says:

    Great work! Saved my life! 😛

  25. Alexander Says:

    Thanks a lot. I’ve implemented image uploading using your posts.

  26. Tamer Salama Says:

    Just wanted to say thanks for posting this step-by-step. Had to work around few issues, but, it is a good guide.

  27. sandip Says:

    Hi,

    I am also getting same error as @Florian.
    I have rails 2.1.2 version and ruby 1.8.7 version

    RJS error:
    TypeError: $(”#dynamic_images_list”) is null

    I will really appreciate the way your tutorial has been written.

    Thanks,

    Sandip

  28. hrhrhr Says:

    Hey, I have followed your tutorail, but I always get:
    Permission denied for to get property Window.setTimeout from

    Anybody know that’s why?


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: