Making TinyMCE work in Ajax With Prototype and Ruby on Rails

Well – it was not nearly as easy as one would expect to make this work – but lots of folks have muddled their way through and I followed in their footsteps and fell into a few holes.

Smart folks should just use this code / plugin – which is pretty and clean

I have not used it – but the demos are gorgeous.

So in this post, I do it the hard way. I won’t go into detail – it is all in the code below. Someday this might make a nice plugin – but it still has a flaw that I compensate for. Here is the basic outline: (1) include TinyMCE in the portal once – also init TinyMCE in portal.rhtml like this:

<%= javascript_include_tag "tiny_mce/tiny_mce.js" %>
mode : "none",
theme : "simple"

(2) In the div that will hold the editor – at the end of the output put in some Javascript to activate TinyMCE on the textarea in question, (2A) While you are there resize the columns so the text area is nice and wide, (2B) – before activating TinyMCE – figure out if TinyMCE has been pithed by too much Ajaxing – if a getInstanceById throws an error (even if not found it should return a null) – throwing an error indicates that MCE can’t even tell if something is *not* there. We declare MCE hosed, don’t even try to make it work – because it actually works half way, (2C) if it turns out getInstanceById works (returning null is OK) – we assume MCE is alive and activate MCE on the text area in question.

The next trick is catching the Save and Cancel (1) First we do a save from TinyMCE back to the div with a catch block so that when we are not even using TinyMCE because is unconscious – we will not confuse Prototype throwing errors, then (2) once the Ajax request is complete (onSuccess) we deactivate the TinyMCE attached to out textarea (again in a try/catch to eat errors if MCE has been knocked silly). Putting this in onSuccess is perfect because by then the server is updated and we are milliseconds away from wiping out the div – so we de-associate the MCE from the text area *just* before it vanishes in a puff of smoke.

So what puts MCE into a sleep-like state? When you are on an exit screen and you overwrite the div containing the editor from an external navigation operation. In Toozday – this is like going into Wiki, Editing a page, and then without pressing Save or Cancel you switch to Profile (oops – MCE left in limbo), then when you come back to Wiki and Edit – MCE is in a state unresponsiveness – waiting forever for its lost textarea to come back to it – which will never happen :(

I detect this in the Javascript and simply don’t use a zoned-out MCE – let users type in a text area I say! I warn the user to press Save or Cancel before continuing – hey if I charge their credit card twice – it is their fault!! Sooner or later users will learn to press Save or Cancel.

This condition is naturally reset when the whole page is refreshed – in Tooz this is on a Refresh, All Sites, or Site to Site Navigation. So MCE will start working again – leaving the user to wonder about the IQ of the system they are dealing with! MCE seems to come and go almost on a whim :)

Someday I will figure this out – but it is now on to more other fun stuff. Golly – everything I do ends up with some sticky wicket. Ah that is the fun of it all!

Below is the sweet code – with a lot of cool helpful urls that saved my arse and led me to the solutions. The URL to source is

<form action="<%= url_for :action => "edit", :id => %>"
onsubmit="try{tinyMCE.triggerSave();}catch(err1){};new Ajax.Updater('<%= portal_get_div %>',
'<%= url_for :action => "edit", :id => %>',
{asynchronous:true, evalScripts:true,
onSuccess: function(transport) {
try {
tinyMCE.execCommand('mceFocus', false, 'umap_content');
tinyMCE.execCommand('mceRemoveControl', false, 'umap_content');
} catch(err2) {}
parameters:Form.serialize(this)});return false;">
<textarea cols="20" id="umap_content"
name="umap[content]" rows="20">
<%= @wiki.content %></textarea> </p>
<input type="submit" name="commit" value="Save"
onclick="try{tinyMCE.triggerSave();}catch(err3){}" /> |
<a href="#" onclick="new Ajax.Updater('<%= portal_get_div %>',
'<%= url_for :action => "view", :id => @wiki.pagename %>',
onSuccess: function(transport) {
try {
tinyMCE.execCommand('mceFocus', false, 'umap_content');
tinyMCE.execCommand('mceRemoveControl', false, 'umap_content');
} catch (err4) {}
evalScripts:true}); return false;">Cancel</a>
<p id="mce-error-message">
Note:You should press "Save" or "Cancel" before navigating away from this screen.
<!-- We need all the try/catch's because if the javascript fails, the Ajax.Updater punts and puts
the output in the main div.   So we eat all the errors in the above code.  -->
<!-- -->
<!-- -->
<!-- -->
<!-- -->
<!-- -->
<!-- -->
<script type="text/javascript">
try {
myWidth = document.getElementById('<%= portal_get_div %>').offsetWidth / 9.0;
} catch (err5) {
myWidth = 20;
if ( myWidth < 20 ) myWidth = 20;
// alert(myWidth);
document.getElementById('umap_content').cols = myWidth;
isOK = false;
try {
if (tinyMCE.getInstanceById('umap_content') != null) {
tinyMCE.execCommand('mceRemoveControl', false, 'umap_content');
// alert('Found old instance - removed');
isOK = true;
} catch (err6) {
document.getElementById("mce-error-message").innerHTML = "The text editor could not be activated.";
// alert("Can't even look up an instance - TinyMCE is hosed - don't bother");
// Only activate if the TinyMCE seems operational
if ( isOK ) {
try {
tinyMCE.execCommand("mceAddControl", false, 'umap_content');
tinyMCE.triggerSave();  // Generate the save error now, rather than later so we put out a message
} catch (err7) {
// A last gasp - can we do this without the control - likely will fail
try {
tinyMCE.execCommand('mceRemoveControl', false, 'umap_content');
} catch (err8) {}
document.getElementById("mce-error-message").innerHTML = "Problems encountered activating the text editor.";