Writing a tinymce plugin for image uploads using Rails and attachment_fu
By Scott Roth on January 15th, 2008
Tagged with: rails, tinymce, attachment_fu, tutorial
Tinymce is a pretty nice browser based rich text editor (RTE). We are using it successfully in one of our applications to provide a user friendly way for non-techies to have some control over the styling of content they create. However, one glaring deficiency in the included set of plugins is that there is no way to upload an image (or any file) for use in the RTE. This makes sense when thinking about it, as file upload requires a server side component - something that the client-side tinymce package needs to be agnostic about. So, being a good lazy web citizen, I tried to get google to write the plugin I need for me. Searching turns up several options - if you are on a PHP backend. It seems no one has shared their approach to doing this with a Ruby on Rails backend yet. So, I did it myself and am going to outline the process here for posterity.
Getting a basic plugin working (aka: the plumbing)
I started my quest on the tinymce wiki page dedicated to plugin development. I downloaded the dev version of tinymce as instructed, which includes a sample plugin named '_template'. FYI, we are using a 2.x version of tinymce. According to the wiki, due to API architecture changes, the process will be a bit different if you are on a 3.x version. Rename the _template plugin and copy it into the plugins directory of your tinymce installation. You'll see 2 files right away: editor_plugin_src.js and editor_plugin.js. Copy the _src file into the other one for development purposes; this is the file that is actually read. At the end we will copy back into the _src file and compress the editor_plugin.js file, which is the one that is actually used at runtime. Open the editor_plugin.js file and replace all the references to 'template' with your plugin's name.
Now you need to open wherever you have your tinymce.init() method and add your plugin in there. Specifically, add your plugin name on the 'plugins' line to register it and on your theme's button line ('theme_advanced_buttons1' for me) so that the button will show up. You also need an image for your users to click on. Create one (or there is a template.gif in the sample plugin if you want to use that for now) and put it in the theme image directory (themes/advanced/images/[pluginname].gif for me).
Do a hard refresh on a tinymce instance and, viola, you should see a button for your totally useless plugin. Some javascript alerts will probably pop up as the sample plugin is alert happy out of the box.
When you click that button, you are going to want a popup page where the user can specify the file to upload. Let's set that up. In editor_plugin.js there is a function called 'getControlHTML' - change 'mceTemplate' to whatever you want your function name to be - I chose 'doUpload'. Match that name in the 'execCommand' function. In execCommand make sure that the template['file'] line points to a real html file. (There is a sample popup.htm in there.) Ok, now refresh tinymce and click the button - you should get the sample popup.
The front-end
Ok, now all the infrastructure is in place. We should be able to wire everything up now. The plugin that I wrote is actually a bit more complex than the code I'll show here as I created some more advanced features like an option to select a file from the list of files that have previously been uploaded into our system. Because of time and space I'll show some simpler code here though.
We need to take the sample popup.htm and change it to support a file upload. Something like this:
1 <html xmlns="http://www.w3.org/1999/xhtml">
2 <head>
3 <title>Image uploader</title>
4 <script language="javascript" type="text/javascript"
src="../../tiny_mce_popup.js"></script>
5 <script language="javascript" type="text/javascript"
src="../../utils/mctabs.js"></script>
6 <script language="javascript" type="text/javascript"
src="../../utils/form_utils.js"></script>
7 <script language="javascript" type="text/javascript"
src="../../utils/validate.js"></script>
8 </head>
9 <body>
10 <form action="/binaries/upload"
enctype="multipart/form-data" id="binary-edit-form"
method="post">
11 <p><label for="binary">Upload a file:</label><br/>
12 <input id="binary_uploaded_data"
name="binary[uploaded_data]" size="30"
type="file" /></p>
13
14 <p><label for="binary_title">Give it a
title</label><br/>
15 <input id="binary_title" name="binary[title]"
size="30" type="text" value="" /></p>
16
17 <p><label for="binary_contents">Add a
description</label><br/>
18 <textarea cols="40" id="binary_contents"
name="binary[contents]" rows="5"></textarea></p>
19
20 <input type="submit" value="Upload!" />
21 </form>
22 </body>
23 </html>
All we have really done here is create a front-end with a form that posts to your rails app (/binaries/upload) and with field ids that your attachment_fu enabled model class is going to recognize. The javascripts included at the top are some tinymce magic. They are not all needed for this simple example, but they probably would be as you flushed out the functionality you need. For example, if you wanted tabs in your plugin interface or wanted to do some validation.
The back-end
For my example, we will assume that you have a Binary model that is already mixing in attachment_fu and a Binary controller. If you haven't used attachment_fu before, Mike Clark posted a great tutorial on getting it set up. Here is the upload method in the controller (this is who you are posting to):
1 def upload
2 binary = Binary.create(params[:binary])
3 @insertString = "<img src=\"#{binary.public_filename}\" />"
4 render :layout => false
5 end
What a skinny method! Thanks to the magic of attachment_fu, we can upload and save a binary file with just one line. In the second line, we create the html string that will be inserted into the client page's DOM by tinymce. We then render the view. I am keeping this very simple for the example, but you can see some immediate enhancements to this controller. For example, you may want some error checking to make sure that the binary uploaded and saved appropriately. Also, if you are allowing uploads of any files, not just images, you'll need to be smarter about the html string you construct. For example, if a PDF is uploaded, you may want to display your application's PDF icon as a link to the PDF, which would be code something like this:
1 @insertString= "<a href=\"#{binary.public_filename}\"><img
src=\"/images/pdfIcon.gif\" /></a>"
Now we need to return some javascript to the browser to insert our image into the RTE. The view file (upload.rhtml using my code):
1 <script language="javascript" type="text/javascript"
src="/javascripts/tiny_mce/tiny_mce_popup.js"></script>
2 <script language="javascript" type="text/javascript">
3 tinyMCEPopup.execCommand("mceInsertContent", false, '<%=
@insertString%>');
4 tinyMCEPopup.close();
5 </script>
This, also, is pretty simple. Using the built-in tinymce function, mceInsertContent, we take our contructed html string and put it in the DOM and then close the popup window.
Check it out, y'all
Now we have all the pieces in place to try out our new plugin. You should be able to upload an image and have it appear immediately in the tinymce textarea in your application. If you are ready to go to production and want to follow tinymce best practices, don't forget to copy your editor_plugin.js to back to the _src file and to strip the runtime file down for faster loading. (Moxiecode suggests jstrim.)
As I mentioned earlier, there are many more interesting and complex tasks you can accomplish with a plugin like this. The intent here was to get a full round-trip uploading plugin working that could be used as a foundation for future work. Hopefully this helps you get jump-started.

nerb
Wed March 12, 2008 at 22:46
were you aware of this plugin though?
http://www.johnwulff.com/articles/2006/05/31/tinymce-with-ruby-on-rails
in there is a tutorial on how to setup the tinymce plugin, but it uses an older version.
i currently use it, and it works pretty good.
i'm going to read through yours and hopefully mash the two together.
perhaps you can publish yours on agile or something?
Rida Al Barazi
Wed March 19, 2008 at 05:07
Configure tinyMCE to not convert urls by adding the following to tinyMCE.init()
convert_urls: false
Add the following to binaries_controller.rb:
skip_before_filter :verify_authenticity_token
Scott Roth
Mon March 24, 2008 at 00:28
Rida - I double checked our tinymce configuration and convert_urls is true for us. Thanks for pointing out that some people may need to set it differently. Regarding the line you suggest to turn off the CSRF protection - you are absolutely right! I wrote this before we moved this application to Rails 2, but now that the protect_from_forgery protection is on by default, you can either add the line you suggest to turn off the security check for the whole controller or what I did was to add 'protect_from_forgery :except => [:upload]' to just turn off the check for that single action.
Mark
Thu April 10, 2008 at 11:18
Scott Roth
Thu April 10, 2008 at 19:52
Sadly, if we move away from using tinymce, it's not likely that I'll be able to go back and package up the code demonstrated here for release. Of course, I'll post another update if our plan changes. Hopefully the code snippets above are enough to get you started though. Good luck!