Using TinyMCE with CakePHP and AJAX

By Tom OReilly (TommyO)
jtreglos wrote an excellent article for integrating TinyMCE into your applications. This takes it one step further, making it possible to have powerful wysiwyg features almost anywhere.

This tutorial assumes you\'ve already read Using TinyMCE with CakePHP (http://bakery.cakephp.org/articles/view/60). Here I will address some of the questions from the comments of that tutorial.

How does TinyMCE work?

I won\'t go into the gory details, but knowing some of the "How" might help to understand a lot of the "Why".

  1. TinyMCE doesn\'t play nicely with Script.aculo.us. It\'s not really explained anywhere, except maybe on a back page of some forum about javascript effects. I don\'t understand it, I just followed one simple rule and everything got better: load TinyMCE code before loading Script.aculo.us code.
  2. TinyMCE instantiates itself at page load. It immediately attaches itself to all textareas that exist or that you specify. If the textarea does not yet exist, as is often the case with AJAX forms, then the form element remains untouched.
  3. TinyMCE isn\'t really your textarea. Actually, it temporarily replaces your textarea with a table and an iframe. It also places a javascript event trigger on the submit button which copies all of the data from the editor\'s iframe back to your textaea before saving.

Configuration

In response to point 1

You need to load the code before you load Script.aculo.us. Change the line in your layout(s) to read:

PHP Snippet:

Download code <?php 
if(isset($javascript)):
    ...
    echo 
$javascript->link('prototype');
    echo 
$javascript->link('tiny_mce/tiny_mce');
    echo 
$javascript->link('scriptaculous');
    ...
endif;
?>

Note: Script.aculo.us is not required and not used in this tutorial, but knowing this will save you hours of time debugging.

In response to point 2

We will turn off TinyMCE\'s automatic attachment to textareas on load, and instead trigger it when necessary. Change your init code to read:

HTML:

Download code
<script type="text/javascript">
    tinyMCE.init({
        theme : "simple",
        mode : "none",
        convert_urls : false
    });
</script>

The mode setting is what we need changed here.

Note: the block that initiates TinyMCE must be part of the base page. It may very well be best suited right in your template file. DRY design is best, and here it\'s almost mandatory.

Now in the view that generates the form, after the textarea is rendered, we need to initiate TinyMCE. It would be best to script this, especially if you want to bind TinyMCE to all textarea\'s, but I include it here for clarity.

PHP Snippet:

Download code <?php 
echo $html->textarea('Model/field', array('cols' => '60''rows' => '2'));
echo 
$javascript->codeBlock("tinyMCE.addMCEControl($('ModelField'),
'ModelField');"
);
?>

In response to point 3

Now that TinyMCE is bound and loaded on your form field, we need to make sure that the data in the editor makes it back into the form before the save is triggered. We also want to unbind TinyMCE from the field to keep future instances of the editor from tripping over itself. Again, it would be best to scipt this, but I provide it here for clarity. Add a 'before' entry to your AJAX submit options, like so:

PHP Snippet:

Download code <?php 
echo $ajax->submit'Save', array(
                ...
                
'before'=>"tinyMCE.triggerSave();
                    tinyMCE.execCommand(
                        'mceRemoveControl', true, 'ModelField');"
,
                ...));
?>

There\'s a TinyMCEHelper just waiting to be written

With so many more features to be utilized, and integration with Script.aculo.us yet to be done, there would be great use for a helper. The gauntlet has been thrown down - who will stand up to the challenge?

 

Comments 140

CakePHP Team Comments Author Comments
 

Comment

1 Nicely done

Good job on finding a way to use TinyMCE in an Ajax context. I haven't tried it yet, but I have an upcoming project where this will be needed, so I'll tell you how it does !
Posted Nov 9, 2006 by Jean-Baptiste Tréglos
 

Question

2 How to..

How 'before' statement works? I got problem in redirecting the page.

Thank you
Posted Nov 21, 2006 by Fendy Limanto
 

Comment

3 Easy.

Quick note about point 3. You should add the mceRemoveControl in the callback if you are doing any type of validation on the server side where you might need a user to maintain the tinyMCE instance after the ajax 'save'.
Cheers,
Jeff
Posted Nov 25, 2006 by Jeff Smirnoff
 

Comment

4 The reason behind the mceRemoveControl

You should add the mceRemoveControl in the callback if you are doing any type of validation on the server side where you might need a user to maintain the tinyMCE instance after the ajax 'save'.
In this example, the entire form is replaced with each call. If the form values fail the validation, the form is sent again and a new TinyMCE instance is attached to the new form field with the same name as the previous form field. That is why it is important to destroy the previous instance.

It should be possible to leave the form up and pass back only the value, thereby removing the need to destroy and reinstantiate Tiny, but I haven't tried this as it wasn't a requirement of mine. If anyone succeeds in doing this, I'd love to see how it works out.
Posted Nov 27, 2006 by Tom OReilly
 

Comment

5 tinyMCE.init

For those of you have not used tinyMCE before tinyMCE.init is declared in the of your page (in your layout).
Posted Oct 11, 2007 by Chris Arthur
 

Comment

6 Not required

For those of you have not used tinyMCE before tinyMCE.init is declared in the of your page (in your layout). Yes, although that makes for cleaner html, it is not necessary. As jtreglos' original article points out, this can be done in the view.

1.2 now makes it possible to put the load in the view, but have the code rendered in the page head tags by passing false in as a second parameter to JavascriptHelper::link() To do this, replace:
<?php 
if(isset($javascript)):
    ...
    echo 
$javascript->link('prototype');
    echo 
$javascript->link('tiny_mce/tiny_mce');
    echo 
$javascript->link('scriptaculous');
    ...
endif;
?>
in your layout with:
<?php echo $scripts_for_layout ?> Then, in the view that needs Tiny and Ajax loaded, do:
<?php echo $javascript->link('prototype'false); ?>
<?php 
echo $javascript->link('tiny_mce/tiny_mce' false); ?>
<?php 
echo $javascript->link('scriptaculous'false); ?>
As you can see, this is much cleaner, and your scripts only end up in the pages that need it. If you really care about having the tinyMCE.init in the head, you should be able to make a webroot/js/tiny_mce/tiny_init.js file, and add this after the above:
<?php echo $javascript->link('tiny_mce/tiny_init'false); ?> Maybe once 1.2 is released, we can rewrite these two articles using the new, cleaner, 1.2 methods. But unitl then, enjoy!
Posted Oct 11, 2007 by Tom OReilly
 

Comment

7 Wrapper for prototype Effects

If you want to use tinyMCE inside a
which is toggled with prototypes Effec.toggle() function you can use this wrapper to add and remove tinyMCE after and before toggle the
content:

1. create the file toggleTinyMCEWrapper.js and paste this code into it:


function toggleTinyMCEEffect(element, effect, refTextareas){
    var element = $(element);
    var effect = (effect || 'appear').toLowerCase();
    var textareas = $(refTextareas);

    if (element.visible()) {
        for (i=0;i<textareas.length;i++){
            tinyMCE.execCommand('mceFocus', false, textareas);    
            tinyMCE.execCommand('mceRemoveControl', false, textareas);
        }
        Effect.toggle(element,effect);
    }else{
        Effect.toggle(element,effect);
        for (i=0;i<textareas.length;i++){
            tinyMCE.execCommand('mceAddControl', false, textareas);    
        }     
    }   
};

Note: this function takes
element -> the id of the div to toggle
effect -> the effect to be used by prototype [appear|blind|slide] textareas -> the id's of the textareas inside this div

if the div is in a closed state it opens it and adds the tinyMCE editor to the textareas
if it is in an opened state it puts the focus on each tinyMCE editor and removes it befor closing the div

2. load the wrapper toggleTinyMCEWrapper.js (see code below) and other libraries


<?= $javascript->link('scriptaculous/prototype.js')?>
<?= $javascript
->link('tiny_mce/tiny_mce.js')?>
<?= $javascript
->link('toggleTinyMCEWrapper.js')?>
<?= $javascript
->link('scriptaculous/scriptaculous.js')?>

Note: Load the tiny_mce.js before scriptaculous.js


3. in your view:

<a href onclick="toggleTinyMCEEffect('divToToggle','blind',['firstEditor','secondEditor']); return false">+/- Toggle me</a> 
<div id="divToToggle"  style="display:none">        
    <?= $form->textarea('firstEditor',array('rows' => '20''cols' => '10')) ?>
    <?= $form->textarea('secondEditor',array('rows' => '20''cols' => '10')) ?>
</div>
Note: replace 'firstEditor' with your Model/field
Posted Mar 21, 2008 by Marc Cathomen