Skip to content

DevGuide RichTextEditors

Violet edited this page Nov 2, 2010 · 2 revisions

MDG: Custom and Pluggable Rich Text Editors - Melody's ArcheType Framework

There are few topics as polarizing as what WYSIWYG editor one should use. Everyone has their own personal favorite and everyone has a reason to sincerely dislike another. Knowing that there was no rich text editor that we as a community could select that everyone would like, we opted to create an abstraction layer that let's people plug in whatever editor they want. Several editors have already been made available to Melody through this system, including:

  • Simple Rich Text Editor (Movable Type's legacy editor)
  • YUI's Editor
  • TinyMCE
  • CKEditor (formerly FCKeditor)

The Melody ArcheType Framework

The Melody Entry and Page editing interface is enabled through an extensible framework called Archetype. In the past developers have confused the label of "Archetype" with the rich text editor itself. This however is not accurate. Archetype is instead a system by which a developer can easily insert which ever rich text editor (or "WYSIWYG" editor) they desire.

The Archetype framework though does much more than define what rich text editor to use, it also provides support for a simple plain text editor, that allows for developers to create custom formatting syntax handlers for their plain text markup language of choice. The allow the same "Bold" button for example to produce:

  • <strong>Text</strong> for HTML
  • **Text** for Markdown
  • *Text* for Textile

Depending of course on what text format is currently selected.

The Archetype framework therefore supports two different text editors that users can toggle between. They are:

  1. A simple plain text editor, which uses just a plain old HTML <textarea> input field.
  2. An iframe based editor, typically used for the rich text editor.

Here are a few things you need to know about the ArcheType framework:

  • Melody only permits the use of one rich text editor system wide at any one time. In other words, two users in the same Melody instance cannot select between the TinyMCE editor or the YUI editor. Either all users use one, or they use the other.

  • To develop a custom RTE, developers will need to create a template and almost certainly a javascript file to initialize the RTE.

Defining a Custom Rich Text Editor

If you would like to add support for a different editor, then you need to follow three relatively simple steps, the hardest part being the Javascript file that initializes the editor.

  1. Register the editor allowing a user to select it via their config.cgi configuration file.

    Here is the actual config.yaml for the Simple Text Editor plugin:

     id: SimpleEditor
     name: 'Simple Rich Text Editor'
     description: 'The legacy Movable Type rich text editor developed by Six Apart.'
     plugin_author: 'Six Apart. Ltd.'
     static_version: 1
     
     richtext_editors:
       sixapart:
         label: 'Six Apart Editor'
         template: 'sixapart_editor.tmpl'
    
  2. Create a template fragment that contains all the necessary HTML and Javascript to properly initialize and load the editor. Most of this template can be copied as is. Right now the ArcheType framework in fact requires for a lot of copy and paste, and is not as modular as it could be.

    One of the most important aspects of this template is line #3, which includes the editor javascript file which initialized the editor for the user to use, and maps important functions to the custom editor through a simple Javascript abstraction layer. More on that in a second.

    The template code then looks like this:

     <mt:setvarblock name="js_include" append="1">
       <script type="text/javascript" src="<mt:var name="static_uri">js/archetype_editor.js?v=<$mt:var name="mt_version_id" escape="url"$>"></script>
       <script type="text/javascript" src="<mt:PluginStaticWebPath component="SixApartRTE">js/sixapart_editor.js?v=<$mt:var name="mt_version_id" escape="url"$>"></script>
     </mt:setvarblock>
     <mt:setvarblock name="html_head" prepend="1">
       <script type="text/javascript" src="<$mt:var name="static_uri"$>js/common/DOM/Proxy.js?v=<$mt:var name="mt_version_id" escape="url"$>"></script>
       <script type="text/javascript" src="<$mt:var name="static_uri"$>js/common/SelectionRange.js?v=<$mt:var name="mt_version_id" escape="url"$>"></script>
       <script type="text/javascript" src="<$mt:var name="static_uri"$>js/common/Editor.js?v=<$mt:var name="mt_version_id" escape="url"$>"></script>
       <script type="text/javascript" src="<$mt:var name="static_uri"$>js/common/Editor/Iframe.js?v=<$mt:var name="mt_version_id" escape="url"$>"></script>
       <script type="text/javascript" src="<$mt:var name="static_uri"$>js/common/Editor/Textarea.js?v=<$mt:var name="mt_version_id" escape="url"$>"></script>
       <script type="text/javascript" src="<$mt:var name="static_uri"$>js/common/Editor/Toolbar.js?v=<$mt:var name="mt_version_id" escape="url"$>"></script>
       <link rel="stylesheet" type="text/css" href="<$mt:var name="static_uri"$>css/editor/editor.css?v=<$mt:var name="mt_version_id" escape="url"$>" />
     </mt:setvarblock>
     <mt:setvarblock name="editor_content">
         <div id="formatted" class="editor-panel">
             <div id="entry-body-field" class="field">
                 <div class="field-content">
                     <div id="editor-content-toolbar" class="field-buttons editor-toolbar">
                         <div class="field-buttons-formatting pkg">
                             <a href="javascript: void 0;" title="<__trans phrase="Decrease Text Size" escape="html">" class="command-font-size-smaller toolbar button"><b>a</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Increase Text Size" escape="html">" class="command-font-size-larger toolbar button"><b>A</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Bold" escape="html">" class="command-bold toolbar button"><b>B</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Italic" escape="html">" class="command-italic toolbar button"><b>I</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Underline" escape="html">" class="command-underline toolbar button"><b>U</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Strikethrough" escape="html">" class="command-strikethrough toolbar button"><b>S</b><s></s></a>
                             <!-- a href="javascript: void 0;" title="<__trans phrase="Text Color" escape="html">" class="command-color toolbar button"><b>Text Color</b><s></s></a -->
                             <a href="javascript: void 0;" title="<__trans phrase="Link" escape="html">" class="command-insert-link toolbar button"><b>Link</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Email Link" escape="html">" class="command-insert-email toolbar button"><b>Email Link</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Begin Blockquote" escape="html">" class="command-indent toolbar button"><b>Begin Blockquote</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="End Blockquote" escape="html">" class="command-outdent toolbar button"><b>End Blockquote</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Bulleted List" escape="html">" class="command-insert-unordered-list toolbar button"><b>Bulleted List</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Numbered List" escape="html">" class="command-insert-ordered-list toolbar button"><b>Numbered List</b><s></s></a>
                             <!-- a href="javascript: void 0;" title="<__trans phrase="Left Align Item" escape="html">" class="command-enclosure-align-left toolbar button"><b>Left Align Item</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Center Item" escape="html">" class="command-enclosure-align-center toolbar button"><b>Center Item</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Right Align Item" escape="html">" class="command-enclosure-align-right toolbar button"><b>Right Align Item</b><s></s></a -->
                             <a href="javascript: void 0;" title="<__trans phrase="Left Align Text" escape="html">" class="command-justify-left toolbar button"><b>Left Align Text</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Center Text" escape="html">" class="command-justify-center toolbar button"><b>Center Text</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Right Align Text" escape="html">" class="command-justify-right toolbar button"><b>Right Align Text</b><s></s></a>
                             <!-- a href="javascript: void 0;" title="Spell Check" class="command-spell-check toolbar button"><b>Check Spelling</b><s></s></a-->
                             <a href="javascript: void 0;" title="<__trans phrase="Insert Image" escape="html">" mt:command="open-dialog" mt:dialog-params="__mode=list_assets&amp;_type=asset&amp;edit_field=<mt:var name="toolbar_edit_field">&amp;blog_id=<mt:var name="blog_id">&amp;dialog_view=1&amp;filter=class&amp;filter_val=image" class="command-insert-image toolbar button"><b>Insert Image</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="Insert File" escape="html">" mt:command="open-dialog" mt:dialog-params="__mode=list_assets&amp;_type=asset&amp;edit_field=<mt:var name="toolbar_edit_field">&amp;blog_id=<mt:var name="blog_id">&amp;dialog_view=1" class="command-insert-file toolbar button"><b>Insert File</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="WYSIWYG Mode" escape="html">" mt:command="set-mode-iframe" class="command-toggle-wysiwyg toolbar button"><b>WYSIWYG Mode</b><s></s></a>
                             <a href="javascript: void 0;" title="<__trans phrase="HTML Mode" escape="html">" mt:command="set-mode-textarea" class="command-toggle-html toolbar button"><b>HTML Mode</b><s></s></a>
                             <a href="javascript: void 0;" onclick="toggleFullscreen()" title="<__trans phrase="Toggle Fullscreen" escape="html">" class="toolbar button fullscreen"><b>Toggle Fullscreen</b><s></s></a>
                         </div>
                     </div>
                     <mt:setvarblock name="editor_content_height"><mt:if name="disp_prefs_height_body"><$mt:var name="disp_prefs_height_body"$><mt:else>194</mt:if></mt:setvarblock>
                     <div class="editor" id="editor-content-enclosure" mt:min-height="66" mt:update-field-height="editor-content-height" style="height: <$mt:var name="editor_content_height"$>px">
                         <textarea id="editor-content-textarea" name="_text_" class="full-width" style="background: #fff; height: <$mt:var name="editor_content_height"$>px"></textarea>
                         <!-- the iframe bootstraps the js -->
                         <$mt:setvar name="delayed_bootstrap" value="1"$>
                         <iframe tabindex="3" id="editor-content-iframe" frameborder="0" scrolling="yes" src="<$mt:var name="static_uri"$>html/editor-content.html?cs=<$mt:var name="language_encoding"$>" style="height: <$mt:var name="editor_content_height"$>px"></iframe>
                         <input type="hidden" name="text_height" id="editor-content-height" value="<$mt:var name="editor_content_height"$>" />
                         <input type="hidden" id="editor-input-content" name="text" value="<$mt:var name="text" escape="html"$>" />
                         <input type="hidden" id="editor-input-extended" name="text_more" value="<$mt:var name="text_more" escape="html"$>" />
                         <div class="resizer" mt:delegate="resizer" mt:target="editor-content-enclosure" mt:lock="x">
                             <img src="<$mt:var name="static_uri"$>images/spacer.gif" width="100%" height="10"/>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
     </mt:setvarblock>
    
  3. Defining the Custom Editor Javascript

    The last piece of the puzzle is the custom javascript you need to write. This javascript file needs to define a Javascript Class that inherits from the Archetype Editor.Iframe object. It then needs to implement a number of key member functions to map certain events to the editor you are integrating with. It is also responsible for initializing the editor, and performing a small handful of other specific functions. Developers will need to implement, at a minimum, the following functions:

    • initObject()
    • eventFocusIn( event )
    • eventFocus( event )
    • eventClick( event )
    • eventKeyPress( event )
    • eventKeyUp( event )
    • eventKeyDown( event )
    • extendedExecCommand( command, userInterface, argument )
    • mutateFontSize( element, bigger )
    • changeFontSizeOfSelection( bigger )
    • getHTML

    Here is the Javascript file for the Simple Editor plugin to use as a reference:

     MT.App.Editor.Iframe = new Class( Editor.Iframe, {
       initObject: function() {
         arguments.callee.applySuper( this, arguments );
         this.isWebKit = navigator.userAgent.toLowerCase().match(/webkit/);
       },
       eventFocusIn: function( event ) {
         this.eventFocus( event );
       },
       eventFocus: function( event ) {
         if ( this.editor.mode == "textarea" )
           this.editor.focus();
       },
       eventClick: function( event ) {
         /* for safari */
         if ( this.isWebKit && event.target.nodeName == "A" )
           return event.stop();
         return arguments.callee.applySuper( this, arguments );
       },
       eventKeyPress: function( event ) {
         /* safari forward delete */
         if ( this.isWebKit && event.keyCode == 63272 )
           return event.stop();
       },
       eventKeyDown: function( event ) {
         /* safari forward delete */
         if ( this.isWebKit && event.keyCode == 46 ) {
           this.document.execCommand( "forwardDelete", false, true );
           return false;
         }
       },
       eventKeyUp: function( event ) {
         /* safari always makes this event. ignore for language input method */
         if ( this.isWebKit ) {
           return false;
         }
       },
       extendedExecCommand: function( command, userInterface, argument ) {
         switch( command ) {
           case "fontSizeSmaller":     
             this.changeFontSizeOfSelection( false );
             break;
           case "fontSizeLarger":
             this.changeFontSizeOfSelection( true );
             break;
           default:
             return arguments.callee.applySuper( this, arguments );
         }
       },
       mutateFontSize: function( element, bigger ) {
         // Basic settings:
         var goSmaller = 0.8;
         var goBigger = 1.25;
         var biggest = Math.pow( goBigger, 3 );
         var smallest = Math.pow( goSmaller, 3);
         var defaultSize = bigger ? goBigger + "em" : goSmaller + "em";
         // Initial detection, rejection, adjusting:
         var fontSize = element.style.fontSize.match( /([\d\.]+)(%|em|$)/ );
         if( fontSize == null || isNaN( fontSize[ 1 ] ) ) // "px" sizes are rejected.
           return defaultSize; // A browser problem or bad user data would lead to "NaN" fontSize.
         var size;
         if( fontSize[ 2 ] == "%" )
           size = fontSize[ 1 ] / 100; // Convert to "em" units.
         else if( fontSize[ 2 ] == "em" || fontSize[ 2 ] == "" )
           size = fontSize[ 1 ];
         // Mutation:
         var factor = bigger ? goBigger : goSmaller;
         size = size * factor;
         if( size > biggest ) 
           size = biggest;
         else if( size < smallest ) 
           size = smallest;
         return size + "em";                            
       },
       changeFontSizeOfSelection: function( bigger ) {
         var bogus = "-editor-proxy";
         this.document.execCommand( "fontName", false, bogus );
         var elements = this.document.getElementsByTagName( "font" );
         for( var i = 0; i < elements.length; i++ ) {
           var element = elements[ i ];
           if( element.face == bogus ) {
             element.removeAttribute( "face" );
             element.style.fontSize = this.mutateFontSize( element, bigger );
           }
         }
       },
       getHTML: function() {
         var html = this.document.body.innerHTML;
         // cleanup innerHTML garbage browser regurgitates
         // #1 - lowercase tag names (open and closing tags)
         html = html.replace(/<\/?[A-Z0-9]+[\s>]/g, function (m) {
           return m.toLowerCase();
         });
         // #2 - lowercase attribute names
         html = html.replace(/(<[\w\d]+\s+)([^>]+)>/g, function (x, m1, m2) {
           return m1 + m2.replace(/\b([\w\d:-]+)\s*=\s*(?:'([^']*?)'|"([^"]*?)"|(\S+))/g, function (x, m1, m2, m3, m4) {
             if ( !m2 ) m2 = ''; // for ie
             if ( !m3 ) m3 = ''; // for ie
             if ( !m4 ) m4 = ''; // for ie
             return m1.toLowerCase() + '="' + m2 + m3 + m4 + '"';
           }) + ">";
         });
         // #3 - close singlet tags for img, br, input, param, hr
         html = html.replace(/<(br|img|input|param)([^>]+)?([^\/])?>/g, "<$1$2$3 />");
         // #4 - get absolute path and delete from converted URL
         var path = this.document.URL;
         path = path.replace(/(.*)editor-content.html.*/, "$1");
         var regex = new RegExp(path, "g");
         html = html.replace(regex, "");
         /* XXX for save on ff */
         regex = new RegExp(path.replace(/~/, "%7E"), "g");
         html = html.replace(regex, "");
         return html;
       }
    } );
    

Continue Reading

The next entry is part of the Advanced Developer Topics section.

 


Questions, comments, can't find something? Let us know at our community outpost on Get Satisfaction.

Credits

  • Author: Byrne Reese
  • Edited by: Violet Bliss Dietz
Clone this wiki locally