We want our editor to do a number of things over and above the default.  For example, we want to integrate a source highlighter so that we get highlighted text in our text blocks, and we also want to add pictures.  We also want to bring up an edit box when someone clicks on a source text box, so that it becomes impossible to accidentally mess with the syntax highlighting.  All of this is going to need a couple more libraries, and some javascript programming of our own.

TinyMCE is a pluggable editor.  To extend tinyMCE, you write some Javascript, add your plugin and resources to a specific place in the tinyMCE folder hierarchy, and tell the initialisation script about your plugin.  This lets one add menu items or buttons to the editor navigation, and alter the editor's behaviour.

Assuming the tinyMCE distribution is unpacked at static/lib/tinyMCE, the initialisation script will generally be found in that directory too, and it is called init.js.  In our case, init.js looks like this:

tinymce.init({
    selector: "textarea",
    content_css: "/fanstatic/mygrok.grok4noobs/mceStyle.css",
    plugins: ["lists", "charmap", "paste", "gfncode", "textcolor", "link", 
              "visualblocks", "table"],
    toolbar: 'undo redo | gfncode | link | styleselect | bold italic underline | ' +
    		 'alignleft aligncenter alignright alignjustify | ' +
    		 'bullist numlist outdent indent | forecolor backcolor',
    extended_valid_elements: "div[class|id|title|lang|style|align|onclick|name|src]",
    style_formats_merge: true,
    style_formats: [
        {  title: "Images", selector: 'img',
           items: [
              { title: 'Normal',      styles: {'float': 'none', 'margin': '10px', }},
              { title: 'Float Left',  styles: {'float': 'left', 'margin': '0 10px 0 10px'}},
              { title: 'Float Right', styles: {'float': 'right','margin': '0 0 10px 10px'}},
            ]
        },
    ],
});

Without going into too much detail, this initialises the editor with a bunch of options, and attaches the editor to instances of textarea.

The two interesting options, are

  1. The gfncode plugin is one we wrote ourselves to handle attachments, and
  2. Our css for the editor is in /fanstatic/mygrok.grok4noobs/mceStyle.css, which according to mygrok/setup.py, translates to grok4noobs/static/mceStyle.css.  Using fanstatic in this way bypasses the automatic versioning, so browsers will cache such css files until told not to.
    Note: this path may need to be manually adapted to point to the correct place.

Our plugin displays an attachment edit form inside a dialog, lets one add an attachment either by uploading a file, or by typing in some text.  The attachment submits the edit form using ajax, and if the response validates the request, will close the dialog.  If the response was an error, the dialog remains visible.

For consistency, the dialog uses JQuery and JQuery-ui. To submit the form, the plugin uses JQuery-form, which properly handles file uploads over AJAX.  So the dependency list for our tinyMCE grows quite radically:

from fanstatic import Library, Resource
library    = Library('mygrok.grok4noobs', 'static')
...
jquery     = Resource(library, 'lib/jquery-1.11.1.min.js')
jqueryform = Resource(library, 'lib/jquery.form.min.js', depends=[jquery])
jqueryuicss= Resource(library, 'lib/jquery-ui-1.11.1/jquery-ui.min.css')
jqueryui   = Resource(library, 'lib/jquery-ui-1.11.1/jquery-ui.min.js', depends=[jquery, jqueryuicss])
tinymcelib = Resource(library, 'lib/tinymce/js/tinymce/tinymce.min.js', depends=[jqueryform, jqueryui])
...
tinymcegfn = Resource(library, 'tinymce.gfn.plugin.js', depends=[tinymcelib, jquery])
tinymce    = Resource(library, 'lib/tinymce/init.js', depends=[tinymcelib, tinymcegfn])

 Our plugin is called tinymce.gfn.plugin.js.  It starts with an instruction to add itself into the plugin manager for tinyMCE:

tinymce.PluginManager.add('gfncode', function(editor, url) {...}

Event handlers are defined by the plugin to handle all sorts of things, but in particular, the init() method is trapped to perform all initialisation and override the click event. Calls to the addMenuItem() and addButton() methods add instructions to the menu and buttonbar to launch our dialog.

Here is the full code for our plugin:

tinymce.PluginManager.add('gfncode', function(editor, url) {

    function addOrEdit(url) {
        var url = url || 'attach';
        var title = 'Modify Attachment';

        if (url=='attach') title = 'Insert Attachment';

        $('<div />').load(url, function(r, a , b) {

            var dlg = $(this).dialog({
                title: title,
                modal: true,
                width: 'auto',
                resizable: false,
            });

            function responseHandler(responseText, textStatus, xhr, $form){
                if (textStatus=='error') { // retry- comms or other error in submission
                    alert('Could not reach the server; Please try again.');
                } else if (responseText.length) {
                    if ($('div.form-status', $(responseText)).length) {  
                        // form had errors in submission
                        $(form).html($(responseText));
                    } else {  // Everything is great
                        tinymce.activeEditor.selection.setContent(responseText);
                        dlg.remove();
                    }
               } else {   // Cancelled
                    dlg.remove();
               }
            }

	    var form = $('form', this);
	    $(form).ajaxForm({
	        success: responseHandler,
	    });
        });
    }

    editor.addCommand('gfncodeCmd', addOrEdit);

    editor.addButton( 'gfncode', {
        title: 'Add Attachment',
        image: url+'/highlight.jpg',
        cmd: 'gfncodeCmd',
    });

    editor.addMenuItem('gfncode', {
        text: 'Add Attachment',
        image: url+'/highlight.jpg',
        cmd: 'gfncodeCmd',
        context: 'insert',
    });

    editor.on('init', function(initEv){
        $(editor.getBody()).on("click", "div.attachment", function(clickEv){
            clickEv.preventDefault();
            var src = $(this).attr("src") + '/modify';
            editor.selection.select(this);
            addOrEdit(src);
        });
    });
});

 editor.addCommand() is a very useful shorthand for defining editor commands.  The same command is then referenced by name when defining the menu and toolbar additions. editor.on('init', ...) is an event handler which is called when the initialisation is complete. This means that the editor window exists, and lets us trap the click event on highlighted text to edit our attachment.

The Python source code to handle attachments is one of the longest modules we have in terms of lines of code, and we discuss the module here.

 

Grok 4 Noobs

A less trivial integration for tinyMCE