Check out "Do you speak JavaScript?" - my latest video course on advanced JavaScript.
Language APIs, Popular Concepts, Design Patterns, Advanced Techniques In the Browser

Creating simple WYSIWYG editor with AS3 (Flex)

There are dozen of JavaScript WYSIWYG editors available, but most of them are too complicated and a little bit buggy. From time to time I'm using my own tool to provide such kind of functionality. It accepts and exports valid html. I decided to share it with you and explain how I built it.

The result of this tutorial is available here. Download the source code from here.Most of this types of editors are used in content management systems, so they should support importing and exporting html. That's why I'll split the tutorial in two parts.

1. The JavaScript part

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
      <link href="css/styles.css" rel="stylesheet" type="text/css" media="all" />
      <script type="text/javascript" src="js/fo.js"></script>
    </head>
    <body>
      <div id="editor"></div>
      <div id="result"></div>
      <div id="result-html"></div>
      <script type="text/javascript">
        var rand = Math.floor(Math.random() * 1000000);
        var swf = new FlashObject("swf/Project.swf?tmp=" + rand, "editorSWF", "600", "230", "9", "#FFFFFF");
        window.onload = function () {
          swf.addVariable("callback", "onTextChange");
          swf.write("editor");
          setTimeout("setDefaultText()", 2000);
        }
        
        function onTextChange(str) {
          document.getElementById("result").innerHTML = str;
          document.getElementById("result-html").innerHTML = str.replace(/</gi, "<");
        }
        
        function setDefaultText() {
          if (document.getElementById("editorSWF").setText) {
            document.getElementById("editorSWF").setText('<font color="#FF0000">Default</font> text</font>');
          }
        }</script>
    </body>
  </html>

Basically the idea is to pass a name of javascript function to the .swf file. This function will be called every time when the user changes something in the editor. In the code above this is onTextChange. The function accepts only one parameter which is actually the HTML string. You can do whatever you want in this function. For example you can change the value of a textarea or hidden input field which is part of a form for submitting. In this example I just displayed the html code below the flash. The editor has also a method for changing the text from javascript. It's called setText. Have in mind that it is available only when the flash is fully loaded and displayed. That's why I used setTimeout to send the default text.

2. The ActionScript part

The starting point of our editor is the following .mxml file.

<?xml version="1.0"?><mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" frameRate="31" backgroundColor="#E5E5E5" width="600" height="230" layout="absolute" applicationComplete="initApp()" backgroundGradientColors="[0xE5E5E5, 0xE5E5E5]">
  <mx:Style>
  RichTextEditor {
    textAreaStyleName: "textArea";
    titleStyleName: "richTextEditorTitle";
    fontSize: 13;
    fontFamily: Verdana;
    dropShadowEnabled: false;
  }
  
  .richTextEditorTitle {
    color: #000000;
    fontFamily: Verdana;
  }
  
  .textArea {
    color: #000000;
    themeColor: #F0F0F0;
    fontWeight: normal;
    fontFamily: Verdana;
    backgroundColor: #F0F0F0;
    borderColor: #999999;
    borderThickness: 2;
  }
  
  .button {
    themeColor: #999999;
    fontFamily: Verdana;
  }
  </mx:Style>
  <mx:Script>
    <![CDATA[
      import lib.document.App;
      private function initApp():void {
        addChild(new App(this.loaderInfo));
      }
    ]]>	
  </mx:Script>
</mx:Application>

Actually the logic of the editor is in lib.document.App. I always prefer to work with purely AS code instead of mxml. Please note that I passed the loaderInfo of the very root component. I needed it to get the callback method which has to be fired when the user changes the content.The first thing that we should do is to add the RichTextEditor component on the stage. It's available in mx.controls package so don't forget to add import mx.controls.RichTextEditor; on top of your class. Here is the constructor of App class.

public
  function App(rootLoaderInfo: LoaderInfo) {
    // getting the callback method which has to be fired when the user changes the content
    _callback = rootLoaderInfo.parameters.callback || "null";
    // resizing the App to fit the screen	
    percentWidth = percentHeight = 100;
    // init the editor	
    _editor = new RichTextEditor();
    _editor.title = "Text:"
    _editor.percentWidth = _editor.percentHeight = 100;
    _editor.addEventListener(Event.CHANGE, onTextChange);
    _editor.htmlText = _defaultText;
    addChild(_editor);
    // removing some of the editor's controls	
    _editor.toolbar.removeChild(_editor.fontFamilyCombo);
    _editor.toolbar.removeChild(_editor.bulletButton);
    _editor.toolbar.removeChild(_editor.alignButtons);
    _editor.toolbar.removeChild(_editor.fontSizeCombo);
    // _editor.toolbar.removeChild(_editor.colorPicker);	
    // _editor.toolbar.removeChild(_editor.linkTextInput);		
    // setting styles of the editor's buttons	
    _editor.boldButton.styleName = "button";
    _editor.italicButton.styleName = "button";
    _editor.underlineButton.styleName = "button";
    _editor.alignButtons.styleName = "button";
    // adding the callback of setText function, which is called from javascript	
    if (isInBrowser()) {
      ExternalInterface.addCallback("setText", receiveText);
    }
  }

I created an instance of RichTextEditor and added it to the stage. I also manually removed some of the controls that are available by default. I disabled the choose of font type and size, the making of bullets and the aligning. I did it because these features add more html tags or attributes which I have to handle with and basically I don't want to give such power to the user, because changing the align of the text or the size of the font can break the page's design and make things look really bad.There is also a listener registered for Event.CHANGE event. This event will be dispatched every time when the user makes some change on the text. Here is the code of the onTextChange method:

private function onTextChange(e: Event): void {
    var str: String = _editor.htmlText;
    str = removeTagsFromString(str, ["TEXTFORMAT"]);
    str = removeAttributesFromString(str, ["SIZE", "FACE", "ALIGN", "LETTERSPACING", "KERNING"]);
    str = lowerCaseAllTags(str);
    send(str);
  }

When the text (_editor.htmlText) comes to this function it looks like that:

<TEXTFORMAT LEADING="2">
  <P ALIGN="LEFT">
    <FONT FACE="Verdana" SIZE="13" COLOR="#000000" LETTERSPACING="0" KERNING="0">
      test
    <FONT COLOR="#FF0000">test</FONT>
    <A HREF="http://krasimirtsonev.com/" TARGET="_blank">test</A>
  </FONT>
</P>
</TEXTFORMAT>

This code doesn't look good, because it contains some invalid attributes and all the tags are in uppercase style. That's why I wrote several functions that transformed this string to a valid html.Removing the invalid tags:

...str = removeTagsFromString(str, ["TEXTFORMAT"]);...private
  function removeTagsFromString(text: String, tags: Array = null): String {
    if (text.length == 0) {
      return text;
    }
    if (tags == null) {
      var removeHTML: RegExp = new RegExp("<[^>]*>", "gi");
      text = text.replace(removeHTML, "");
    } else {
      var numOfTags: int = tags.length;
      for (var i: int = 0; i < numOfTags; i++) {
        var tag: String = tags[i];
        removeHTML = new RegExp("<" + tag + "[^>]*>", "gi");
        text = text.replace(removeHTML, "");
        removeHTML = new RegExp("]*>", "gi");
        text = text.replace(removeHTML, "");
      }
    }
    return text;
  }

Remove the unnecessary tags' attributes:

...str = removeAttributesFromString(str, ["SIZE", "FACE", "ALIGN", "LETTERSPACING", "KERNING"]);...private
  function removeAttributesFromString(text: String, attributes: Array): String {
    if (text.length == 0) {
      return text;
    }
    var numOfAttr: int = attributes.length;
    for (var i: int = 0; i < numOfAttr; i++) {
      var attr: String = attributes[i];
      var removeHTML: RegExp = new RegExp(attr + "=\\" [0 - 9 a - zA - Z~!@# $ % ^ & * () _ + -] * \\" ?", "gi");
      text = text.replace(removeHTML, "");
    }
    return text;
  }

And at the end lowercase all the tags:

...str = lowerCaseAllTags(str);...private
  function lowerCaseAllTags(text: String): String {
    if (text.length == 0) {
      return text;
    }
    var removeHTML: RegExp = new RegExp("(<[^>]*>)", "gi");
    text = text.replace(removeHTML, function (): String {
      return arguments[0].toLowerCase();
    });
    return text;
  }

The result is:

<p>
  <font color="#000000">
    test
    <font color="#ff0000">
      test
    </font>
    <a href="http://krasimirtsonev.com" target="_blank">test</a>
  </font>
</p>

The valid code is sent to the javascript method:

private function send(str: String): void {
    if (isInBrowser()) {
      ExternalInterface.call(_callback, str);
    }
  }
If you enjoy this post, share it on Twitter, Facebook or LinkedIn.