Pixelastic

You can cut our wings but we will always remember what it was like to fly.

Posts tagged with "tinyMCE"

Detecting fullscreen in tinyMCE

I wanted one of my tinyMCE plugins to fire a certain set of actions whenever the full screen mode was activated.

I search for an onFullScren event or somethin similar but to no avail. I finally discovered that the full screen actually creates a whole new tinymce editor instance.

So the only thing I had to do was writing my little piece of code in the init method of my plugin and check to see if the fullscreen was enabled.

The initial fullscreen plugin exposes a fullscreen_is_enabled param that can be checked.

All I had to do was a simple condition like : if (editor.getParam('fullscreen_is_enabled')) {}. And once in the condition I tried to execute my custom code. I needed the editor.execCommand method but all I got was an error because t.selection was not set.

It appears that the execCommand can only be executed when the full editor instance is loaded (and thus a t.selection is created). So I wrap my little piece of code into a setInterval checking for editor.selection and finally executed my code when the .selection was set.

Here is my final snippet, to be included in my plugin init method :

if (editor.getParam('fullscreen_is_enabled')) {
var fullScreenInterval = setInterval(function() {
if (!editor.selection) return false;
clearInterval(fullScreenInterval);
editor.execCommand('anyTinyMCECommand');
}, 1000);
}

 

Adding a <hr> in a tinyMCE instance

When using the default tinyMCE implementation to add an <hr> element to the editor content, the <hr> is added inside its parent <p> element (when it should be an element on its own, without such a parent).

I've added my own plugin to resolve this small issue, here is the code :

(function() {
tinymce.create('tinymce.plugins.pixelastic_hrPlugin', {
init : function(editor, url) {
// Register the command
editor.addCommand('mcepixelastic_hr', function() {
// We get the parent node
var parentNode = editor.selection.getNode(),
uniqueId = editor.dom.uniqueId();
// We insert the hr (with a unique id to select it later)
editor.execCommand('mceInsertContent', false, '<hr id="'+uniqueId+'" />');
var hr = editor.dom.select('#'+uniqueId)[0];
// We split the parent element around the hr
editor.dom.split(parentNode, hr);
// We remove the temporary id
$(hr).attr('id', null);
});
// Adding a button
editor.addButton(pluginName, { title : 'pixelastic_hr.desc', cmd : 'mcepixelastic_hr' });
}
}
});
// Register plugin
tinymce.PluginManager.add('pixelastic_hr', tinymce.plugins['pixelastic_hrPlugin']);
})();

The trick to use the editor.dom.split method to split the parent element around the <hr> element.

The dirty hacks is that there is no way to get a direct reference to a DOM element added through mceInsertContent, so we need to set a temporary unique id and then select it through this id.

I use jQuery in my example and I strongly suggest you to do the same, the selectors it provides are much sexier and helps writing these kind of plugins pretty fast.

Preventing tinyMCE from wrapping <img /> in <p>

If you just want to add a presentational image in a tinyMCE editor, you'll find out very quickly that it will be wrapped in <p></p> without you asking.

The question has been asked several times on the tinyMCE forums, but the answers never quite satisfied me. It ranges from the classical "Why do you want to do this ? You should better use <insert semantic element and css here>" to "Just do a regexp before displaying your content to remove the bad <p></p>".

This clearly did not satisfy me.

The solution was to define the forced_root_block setting value to false, allowing us to create any element on the top level, and not having it automatically wrapped in <p>.

Fixing the side effect

Allowing for elements to be input directly in the root level has the nasty side effect of creating tinyMCE editor instances with a default text of, well, nothing, instead of the really usefull <p> tag. Also, if you do a Ctrl+A and delete all content, you'll end up with an empty editor without the initial <p> tags

To fix this part, I just added an onNodeChange event to fire every time the content is changed. I test the current content and if empty add the <p> tag. There is a little subtelty though, to correctly place the caret where needed.

In your tinyMCE.init call, just add the following setup key :

tinyMCE.init({
[...]
setup: function(editor) {
editor.onNodeChange.add(function(editor, cm, e, c, o) {
var editorContent = editor.getContent();
if (editorContent==="") {
// We set content as a <p> containing a placeholder, then we delete the placeholder to place the caret
editor.setContent('<p><span id="__CaretPlacholder">Placeholder</span></p>');
editor.selection.select(editor.dom.select('#__CaretPlacholder')[0]);
editor.dom.remove(editor.dom.select('#__CaretPlacholder')[0]);
}
});
}),
[...]
});

Before finding this solution, I tried the onBeforeSetContent callback, but due to a bug in the tinyMCE source, it couldn't handle well the case where the editor is empty. So I had to resort to a more generic callback.

Copy/Paste text from Word into tinyMCE

What good is building a really nice CMS with top-notch WYSIWYG editor if you handed it to clients that will blindly copy/paste Word documents into it ?

Well, it will just render a big ugly mess of proprietary and contradictory pseudo-css rules. You could even be blamed for it.

The solution

The latest versions of tinyMCE came bundled with an improved paste plugin. It will automatically attempt to clean bad pasted text by operating some dark voodoo magic on it.

It does it quite well to be honest, removing almost all messy formatting. From the tests I tried, I was still getting useless crap in the resulting text (CSS comments, <span> with overly long style of color and background-color definitions, etc).

I finally decided to take a more brutal approach. The paste plugin contained a _insertPlainText method that was supposed to be used in conjunction with a toolbar icon.

This method removes all formatting. Period.

As I didn't want to have to click on a toolbar icon before pasting my text (and none of my actual clients would ever think of doing that either), I came up with a very simple plugin to do the boring stuff for me.

The plugin

The only goal of this plugin is setting to true the pasteAsPlainText property of every editor. This property is defaulted to false but can be swapped using the toolbar icon.

I also forced the paste_text_sticky setting to true, preventing the previous property to revert to false after the first pasting.

Here's my plugin full code :

Be sure to include the paste plugin in your plugin list, and insert my plugin after the paste plugin.

/**
 *    Will automatically convert pasted text so no junk code will be included.
 *    This plugin depends on the core paste plugin.
 **/
(function() {
tinymce.create('tinymce.plugins.pasteAsPlainTextPlugin', {
init : function(editor, url) {
// We force the pasting to occur as plain text
editor.pasteAsPlainText = true;
// We also put it as sticky to allow for multiple pastings
editor.settings.paste_text_sticky = true;

// Adding some special post process
editor.settings.paste_postprocess = function(a, o) {
var text = o.content,
split = String.fromCharCode(13)+String.fromCharCode(10)
;

// If content is long text without HTML, We'll break it into <p>ieces
if (text.charAt(0)!='<' && text.indexOf(split)!=-1) {
// Adding <p> around each line
var node = document.createElement('div'),
sentences = text.split(split)
;
for(var i=0,max=sentences.length;i!=max;i++) {
node.innerHTML+='<p>'+sentences[i]+'</p>';
}

// Saving back in original content/node
o.node = node;
o.content = node.innerHTML;
}
return o;
}
}
});

// Register plugin
tinymce.PluginManager.add('pasteAsPlainText', tinymce.plugins[pasteAsPlainTextPlugin]);
})();

Update

I added a postprocess calback after seing that the pasted text was a little too plain in Webkit. All my text was displayed on the same line, without the nice breaking into paragraph that Firefox showed.

It occured because tinyMCE used the event.clipboardData property that Webkit browsers provides and allow for easy retrieving of clipboard data. Unfortunatly it returned a really plain text, and I had to apply a little loop to replace each new line with a paragraph.

Tabindexing and inserting tabs with tinyMCE

You may have noticed that you can't press tab to jump from field to field in a form that uses tinyMCE. There is a plugin named tabfocus in the core that is here to allow just that.

But it does not seem to work correctly if you already have tabindex values defined on your inputs. So I decided to code my own.

I choose the easiest way, letting the browser do most of the job. I only copied the textarea tabindex value to the create iframe and it did the trick.

I created a tinyMCE for that, here the code of the init() method. By the way, I'm using the jQuery version of tinyMCE.

init : function(editor, url) {
// We set a tabindex value to the iframe instead of the initial textarea
editor.onInit.add(function() {
var editorId = editor.editorId;
var textarea = $('#'+editorId);
$('#'+editorId+'_ifr').attr('tabindex', textarea.attr('tabindex'));
textarea.attr('tabindex', null);
});
}

What it does is grabbing the initial tabindex value of your textarea and setting it to the tinyMCE iframe. You have to wrap this in onInit.add because at the time the plugin init method is called, the iframe is not yet created. I also removed the tabindex value from the original textarea, two elements aren't supposed to have the same tabindex value.

This method does not work in Chrome. Chrome always add a tab character when you press the tab key in a tinyMCE editor, it does not jump to the next tabindexed element. Is that a good behavior ? I don't know, it surely is useful to be able to insert tab characters, but it also is useful to tab through the whole form.

Listening to the tab key

Anyway, I decided to hook on the keyDown event and listen to the tab key being pressed. This way I could manually jump focus to the next field when tab is pressed, or insert a tab character when Shift+Tab is pressed (for example).

I used the tinyMCE event helper methods and wrote this (just add it right after the previous editor.onInit.add code) :

// We hook on the tab key. One press will jump to the next focusable field. Maj+tab will insert a tab
editor.onKeyDown.add(function(editor, event) {
// We only listen for the tab key
if (event.keyCode!=9) return;
       
// Shift + tab will insert a tab
if (event.shiftKey) {
editor.execCommand('mceInsertContent', false, "\t");
tinymce.dom.Event.cancel(event);
return;
}
// Just pressing tab will jump to the next element
var tabindex = $('#'+editor.editorId+'_ifr').attr('tabindex');
// We get all the tabindexed elements of the page
var inputs = [];
$(':input[tabindex]').each(function() {
inputs[$(this).attr('tabindex')] = this;
});
// We find the next after our element and focus it
for (var position in inputs) {
if (position<=tabindex) continue;
inputs[position].focus();
break;
}

tinymce.dom.Event.cancel(event);
return;
});

First, we discard any key press that is not a tab key.

Then we check if the Shift key is pressed, and if so we add a tab character and stop there.

The biggest part is jumping to the next field. I can't revert to the browser default for that because every browser default behavior is different and I surely don't want to do some browser sniffing.

I first get the list of all the input fields that have a tabindex value (your fields should have one), then I sort them in tabindex order and then loop through the list and stop when I've found one with a bigger tabindex that the actual field. I focus this one and stop the loop.

One final word

I've tested under Firefox 3.6, Chrome, Safari and Opera. Haven't tested IE yet because I still have a lot of other scripts to debug in IE first.

As said earlier, maybe you could skip the whole tabindex listing if you intelligently revert to browser default for the browser that will jump to the next field but I have no idea how to test for that.

Give your variables meaningful names

I just realized that the fullscreen plugin I was using with tinyMCE (v3.3.5) was throwing an error in my Firebug panel everytime I closed it.

As I wrote some tinyMCE plugins myself I thought I may have done something that was causing this. So I opened up the javascript file and checked for the error line :

var win, de = DOM.doc.documentElement;
if (ed.getParam('fullscreen_is_enabled')) {
if (ed.getParam('fullscreen_new_window'))
closeFullscreen(); // Call to close in new window
else {
DOM.win.setTimeout(function() {
tinymce.dom.Event.remove(DOM.win, 'resize', t.resizeFunc);
                tinyMCE.get(ed.getParam('fullscreen_editor_id')).setContent(ed.getContent({format : 'raw'}), {format : 'raw'});
                tinyMCE.remove(ed);
                DOM.remove('mce_fullscreen_container');
                ed.style.overflow = ed.getParam('fullscreen_html_overflow');
                DOM.setStyle(DOM.doc.body, 'overflow', ed.getParam('fullscreen_overflow'));
                DOM.win.scrollTo(ed.getParam('fullscreen_scrollx'), ed.getParam('fullscreen_scrolly'));
                tinyMCE.settings = tinyMCE.oldSettings; // Restore old settings
}, 10);
}
        return;
}

If you're not familiar with the complex tinyMCE syntax this may seems a little... well... complex.  I'll focus on the error line but I wanted to paste the whole code block so you can see my point.

The error line is this one :

ed.style.overflow = ed.getParam('fullscreen_html_overflow');

Of course it will throw an error, we try to access a property of an element we just removed from the DOM (two lines before : tinyMCE.remove(ed);). Why would someone want to do that ?

Well in fact, this is not ed (that we should try to access, but de (short for DOM.doc.documentElement). In fact the code was correct in 3.3.2, but someone changed it around 3.3.3.

My guess is that someone had the file opened, saw this bit of code, spotted the de, and seeing a lot of references to ed all around, thought it was a typo and 'fixed' it.

Reading the tinyMCE code (whenever you want to understand how it works, or want to study plugins before creating your own) is pretty hard. There are almost no comments, and variable names are only one or two letters long.

The tinyMCE team released a survey asking user what should be improved on the tinyMCE product. I answered it, and my main point was to improve documentation, because reading the tinyMCE core to understand its inner goings is quite a chore.

Tweaking tinyMCE

I'm in the process of tweaking a tinyMCE install to use a custom set of plugins and a custom skin to better fit in the Caracole CMS. This would allow me to re-use CSS and JS code I've already coded.

Fortunatly, I'm getting better and better at understanding the way tinyMCE works and the various configuration options and how to tweak them.

I also want the tinyMCE CSS rules to be integrated into the main CSS file of my app (packed and minified using CSSTidy). So I defined a custom skin in the tinyMCE setup, named 'caracole'. This way, all the CSS skinning classes will be prefixed with caracole as I don't want to mess with the default skins.

I also copied the ui.css file from the themes/advanced/skins/default to my own css directory, renamed it to tinymce.css, replaced all .default occurences to .caracole and added this file to the list of files being processed by the main packer.

This way I can easily update any css file from the same directory, without having to dive into the tinymce subdirectories.

Unfortunatly, by doing so, tinyMCE continue to load the tiny_mce/themes/advanced/skins/caracole/ui.css file (have a look at Firebug if you don't trust me). It took me some time to figure out where that call was made from. It is actually on editor_template.js (around line 150 in dev version).

As I didn't want to load this file twice, I tried to disable this call. I could have manually patched the source file but I don't want to, it would make any future update a pain. So I tried to find an other way.

Looking at the loadCSS method, it appears that there is a mechanism to prevent loading twice the same file. If the file is already present in the .files array, it won't be added. Ok, so now I just have to manually add the file before the call is made.

That's where the setup callback come into play. I just defined in my inyMCE.init :

tinyMCE.init({
skin: 'caracole',
editor_css: 'caracoleDoNotInclude',
setup: function(editor) {
tinymce.DOM.files['http://'+document.domain+'/caracoleDoNotInclude'] = true;
}
}

Doing so, the loadCSS will try to load the caracoleDoNotInclude file (clearly this name is a placeholder, I have no such file). And in the setup method I manually tell tinyMCE that this file is already loaded so when it will initiate, it won't load the file at all.

Same goes for the content_css, the file that handle the look and feel of the editable zone. I don't want to have my file in the tinyMCE skin directory, I'd rather have it with my other CSS files.

So I defined a custom content_css to point to my file. I also had to update the setup method to mark the file as already loaded. But this time, as the file is loaded on a per instance basis, this is the editor.dom.files that I have to update, not the general tinymce.DOM.

Fortunatly, the latest tinyMCE version expose their event API, so it was just a matter of defining the following event in the main setup :

setup: function(editor) {
tinymce.DOM.files[baseUrl+'caracoleDoNotInclude'] = true;
editor.onPreInit.add(function(editor) {
editor.dom.files[baseUrl+'js/vendors/tiny_mce/themes/advanced/skins/caracole/content.css'] = true;
});
},

Edit :

Note that in the latest tinyMCE version, the code is slightly different because you have to call a special method on the url before adding it to the editor.dom.files

editor.onPreInit.add(function(editor) {
editor.dom.files[editor.baseURI.toAbsolute('http://'+document.domain+'/js/vendors/tiny_mce/themes/advanced/skins/caracole/content.css')] = true;
});

 

tinyMCE mceRemoveNode explained

When writing my image plugin for tinyMCE today I came accross the mceRemoveNode command that one can use to alter the tinyMCE editor content.

It has to be used with editor.execCommand('mceRemoveNode', true/false, node).

I don't really get what the true/false argument was about, setting either one or the other didn't change anything (that I could tell) in my case. The node argument is the DOM node you want to remove from the editor.

At first I thought that the command would allow me to remove a node from the editor area. I needed it in my plugin. But it was not exactly what it really does.

In fact it only removes the wrapping node, but keep the content intact. In other words if you have

<a href="http://www.domain.com/"><img src="http://www.domain.com/picture.jpg" /></a>

And you remove the <a> node using mceRemoveNode, you'll end up with

<img src="http://www.domain.com/picture.jpg" />

in place of your link.

I had to manually (well, with the great help of jQuery) remove the content of the link before removing it. I could have remove the node itself using jQuery but I thought that sticking to the tinyMCE methods would be a better approach, I don't want to mess all this stuff up, maybe "simply" removing html nodes like this could interfere with the textarea value update, I don't know. And in doubt, I prefer to use the methods and command exposed by the API