Oct 29 2006

Using TinyMCE with Rails/AJAX

admin @ 11:56 am

Our current project is basically a very structured (i.e. niche/focused/custom) content management system. There are a few places where we want to give the client the ability to edit some HTML page content. The client is tech-savvy, but not in the “I enjoy slinging HTML” way. Ergo, we needed WYSIWYG HTML editing capability. After some research we decided that TinyMCE was the way to go. We just had to make it work the way we wanted it to.

Here are my notes on getting TinyMCE working nicely in a Rails/AJAX environment.

Start by grabbing the rails plugin (and read the material) from here and install it as per the instructions.

Allowable options to uses_tiny_mce are documented here… there are lots of them

I figured that having followed the directions in the above, my work was over. In fact it was just beginning. The above will work fine if you have a page, with a textarea that you want to be WYSIWYG. Our requirements were a bit more involved. The textarea in question was in a partial that was rendered via a remote updater call (via a link_to_remote in a list on the page). The main issue here is that the textarea didn’t exist when the page was rendered… so TinyMCE had to be hooked up to it later… when it was injected into the DOM tree. Some digging through support forums and I found what I needed. This required a bit of java script in the partial… after the textarea:

<%= form_remote_tag :url => {:action => 'edit_page', :id => @page},
                    :before => "tinyMCE.triggerSave(true,true)" %>
  <b>Page Content:</b><br />
  <%= text_area :page, :content, :rows => 15, :cols => 150 %>
  <br /><br />
  <%= submit_tag "Update" %>
  <script type="text/javascript">
  //<![CDATA[
    tinyMCE.execCommand('mceAddControl', true, 'page_content');
  //]]>
  </script>
<%= end_form_tag %>

The next issue was getting the contents out of TinyMCE and accessible to the parameter construction for the remote call. After a bit of research I ended up with the following:

<%= form_remote_tag :url => {:action => 'edit_page', :id => @page},
                    :before => "tinyMCE.triggerSave(true,true)" %>

Normally the save is trigger by a page unload, I needed to force it to happen before the remote call happened. Putting the call to force (aka trigger) the save in the :before script of the form submission remote call worked great. So now I had a bigger problem. TinyMCE was getting hooked up to the textarea. If the user picked another page item from the list, the div would be refilled with a different rendering of the partial… with a different textarea node. The old textarea would be gone… out from under TinyMCE. I needed a way to reconnect to the new textarea. More digging turned up this example. It gave me the final bit of the puzzle. My final solution includes the following in application.js:

bTextareaWasTinyfied = false; //this should be global, could be stored in a cookie...

function setTextareaToTinyMCE(sEditorID) {
	var oEditor = document.getElementById(sEditorID);
	if(oEditor && !bTextareaWasTinyfied) {
		tinyMCE.execCommand('mceAddControl', true, sEditorID);
		bTextareaWasTinyfied = true;
	}
	return;
}

function unsetTextareaToTinyMCE(sEditorID) {
	var oEditor = document.getElementById(sEditorID);
	if(oEditor && bTextareaWasTinyfied) {
		tinyMCE.execCommand('mceRemoveControl', true, sEditorID);
		bTextareaWasTinyfied = false;
	}
	return;
}

These two functions are used to disconnect from an existing textarea and reconnect to the newly rendered one. In the list item that causes the rendering:

<%= link_to_remote "<span class=\"listTitle\">#{page.title}</span>",
                   {:update => "editPage",
                    :url => {:action => :get_page, :id => page},
                    :before => "Effect.Fade('editPage',
                                            {duration: 0.25,
                                             queue: 'end',
                                             afterFinish: function(effect) {
                                               unsetTextareaToTinyMCE('page_content')}})",
                    :complete => "Effect.Appear('editPage', {duration: 0.5, queue: 'end'})"},
                    :title => "Edit #{page.title}"  %>

To avoid visual weirdness the disconnect is delayed until the fade has completed. Once the new version of the partial has been loaded, it’s faded back in. The relavant bit of the partial is here:

<%= form_remote_tag :url => {:action => 'edit_page', :id => @page},
                    :before => "tinyMCE.triggerSave(true,true)" %>
  <b>Page Content:</b><br />
  <%= text_area :page, :content, :rows => 15, :cols => 150 %>
  <br /><br />
  <%= submit_tag "Update" %>
  <script type="text/javascript">
  //<![CDATA[
    setTextareaToTinyMCE('page_content');
  //]]>
  </script>
<%= end_form_tag %>

The last thing was to add a Done/Cancel button to the form:

<input type="button"
       value="Done"
       onclick="Effect.Fade('editPage',
                            {duration: 0.25,
                             queue: 'end',
                             afterFinish: function(effect) {
                               unsetTextareaToTinyMCE('page_content')
                             }})" />

Now I have a WYSIWYG textarea in a partial that’s rendered via a remote call.. and it all works smoothly and exactly as required.

Tags: 

Aug 30 2006

BDD-style JavaScript testing

admin @ 3:53 pm

BDD-style JavaScript testing:

“Borrowing from Behaviour Driven Development techniques, especially the RSpec framework I’ve added some new features to script.aculo.us™ testing library.”

(Via mir.aculo.us.)

Check it out!

Tags: