12/06/2009

Custom Tags in TinyMCE

One of the applications that I work with uses custom HTML-like tags to implement some of its functions. For example:

<review>Some text here</review>

The task was to create a plugin form TinyMCE that with a single click of a button would allow the user to wrap of portion of text ot HTML in such <review> tag. The task seems fairly simple and I was able to make it work pretty fast in all browsers, except IE. The problem is that TinyMCE uses DOM for processing HTML, however IE does not recognize any custom tags in DOM, thus it would simply ignore the custom tag that I put in. However, after a couple of days I've been able to solve that task as well. So let me start shwoing how the custom tag plugin is created and works.

To create new plugin in the plugins folder of the TinyMCE I create a new folder called KSReviewTag (this will be the name of the plugin). In this folder I have created a file called plugin_src.js: this will be the code for my plugin. TinyMCE comes with a bunch of plugins, so it is easy to see what the code of the plugin should be and what the interfaces are like. 

To allow the user wrapping HTML into the custom tag, a new TinyMCE command will need to be created. Since there will be only one button, the same command will be used to wrap the HTML and unwrap it, if it was already wrapped in the tag. Also, since TinyMCE and the browser do not know how to display this tag in the TinyMCE I am using SPAN with a class, so that user will be able to distinguish the text wrapped in the <review> tag. 

Adding a command looks like this:




ed.addCommand('mceKSReviewTag', function() {
var elm = ed.dom.getParent(ed.selection.getNode(), "SPAN.ksreview");
if (elm && elm.nodeName.toUpperCase() == "SPAN") {
tinyMCE.execCommand('mceRemoveNode', false, elm);
}
else {
ed.selection.setNode(ed.dom.create('span', {title: 'Review link', 'class': "ksreview"}, ed.selection.getContent()));
}
});

If the parent node of the current selection is <SPAN class="ksreview"> I remove it, otherwise I add it to the selection.

Assigning a command to a button is also really simple:



ed.addButton('KSReview', {
title : 'Review link',
cmd : 'mceKSReviewTag',
label: 'Review Link'
});


Now, when the user switches to the View Soucre mode of TinyMCE, or when the user saves the edited content I want him to see the custom tag, not the <SPAN> one. To achieve this I add a cusom handler to the PreProcess event:



ed.onPreProcess.add(function(ed, o) {
tinymce.each(ed.dom.select('span.ksreview', o.node), function(n) {
ed.dom.replace(ed.dom.create('review', null, n.innerHTML), n);
});
});


This code replaces all <SPAN class="ksreview"> tags with the custom < review> tag

Similarly, I want the custom tag to be replaced with SPAN tag when the user saves HTML content or when the editor first renders what has been previously saved in the TextArea.And this is where the Internet Explorer problem comes into play. Since IE does not understand custom tags, the TinyMCE would just drop it. However, it is not quite true to say that IE cannot work with custom tags: it can, but only in case id the custom tags have their own namespace. In other words the tag described here, if defined under the custom KS namespace would look like <ks:review>. But in order for IE to know that the KS namespace is valid it has to be mentioned at the definition of the HTML document. This is where I decided to modify the source code in the tinymce_src.js a little to make it able to support custom namespaces. In the tinymce_src.js I found where this HTML tag is defined for the editor's IFRAME and replaced it with the following:



var nsString = '';
if (s.customNS && s.customNS.length != 0) nsString = ' xmlns:' + s.customNS;
t.iframeHTML = s.doctype + '<html'+nsString+'><head xmlns="http://www.w3.org/1999/xhtml">';

So now I can add a custom namespace as a customNS configuration setting for the TinyMCE. In this case, I need to add custonNS: "ks" to the configuration settings of the editor.

So for internet explorer, bofre processing any content we need to replace <review> with <ks:review>. This is done with the following processing on the BeforeSetContent event:



ed.onBeforeSetContent.add( function(ed, o) {
if (tinymce.isIE) {
o.content = o.content.replace(new RegExp("<review>", "gi"),"<ks:review>");
o.content = o.content.replace(new RegExp("</review>", "gi"),"</ks:review>");
}
});


So the last thing left is to replace the custon tags with SPAN to display properly in the editor:



ed.onSetContent.add(function(ed, o) {
tinymce.each(ed.getDoc().getElementsByTagName("review"), function(n) {
ed.dom.replace(ed.dom.create('span', {title: 'Review link', 'class': "ksreview"}, n.innerHTML), n);
});
});


After all processing is done, all that's left to do is add some cosmetics: highlight the plugin button if the current selection is within the custom tag and display the custom tag in the DOM path properly:



ed.onNodeChange.add(function(ed, cm, n, co) {
n = ed.dom.getParent(n, 'SPAN.ksreview');

cm.setDisabled('KSReview', co);
cm.setActive('KSReview', 0);

// Activate all
if (n) {
do {
cm.setDisabled('KSReview', 0);
cm.setActive('KSReview', 1);
} while (n = n.parentNode);
}
});

if (ed.theme.onResolveName) {
ed.theme.onResolveName.add(function(th, o) {
var n = ed.dom.getParent(o.node, 'SPAN.ksreview');
if (n && n.nodeName.toUpperCase() == 'SPAN' && n.className.toUpperCase() == 'KSREVIEW') {
o.name = 'Review';
o.title = 'Review';
return false;
}
});
});

2 comments:

Webnet said... [Reply]

Thanks, just what I was looking for!!

Unknown said... [Reply]

@Webnet
You are very welcome. Glad that it worked for you.