Extending Epoz

  Written by: Guido Wesdorp
  Email: guido@infrae.com
  Valid for: Epoz 1.1

1. Epoz Tools

  Apart from the context menu, all UI functionality in Epoz is added to the
  core using a simple plugin mechanism. This can also be used to write your
  own extensions, either by copying existing tools and modifying them or by
  subclassing EpozTool (and sometimes EpozToolBox) and writing your own
  functionality.

2. Tool interface

  The plugins have a simple interface to adhere to:

    interface EpozTool:

    attribute toolboxes = {};
      - id -> toolbox mapping

    method initialize(editor):
      - store reference to the editor, register event handlers, etc.

    method registerToolBox(id, toolbox):
      - register a UI element (ToolBox, more about those later)

    method updateState(selNode, event):
      - update the state of self and any toolboxes

    method createContextMenuElements(selNode, event) (optional):
      - return an array of ContextMenuElements

3. Writing a simple tool

  As an example we'll write our first simple tool, which displays the path to 
  the current node in the tree in the status bar. This is an actual tool in 
  the current Epoz code and you can see the result in the status bar of any
  default Epoz installation. First we'll write a class, and add a method
  called 'updateState' that will change the status bar:

    // Example tool class
    function ExampleTool() {
        this.updateState = function(selNode, event) {
        };
    };

  The updateState method is a method each tool can implement (actually the
  code assumes it's available on each tool, but since the superclass defines
  it as well it's not strictly necessary to add it to your class) and that
  will be called when the user clicks inside the iframe, presses enter or hits
  a cursor key (so basically, when the cursor can end up in a different
  element). 
  As you can see we use in-line methods in Epoz, this is a choice of style
  rather than for a functional reason, and if you wish you can choose to use
  another coding style. Using '<class>.prototype.<method> = function() {...};' 
  will do equally well.
  Now we'll add some functionality to our updateState method. The updateState
  method's first argument will always be a reference to the current selected 
  element (or the element currently containing the selection). To get a path
  to that element from the root, we can write something like:
    
    // Example tool class
    function ExampleTool() {
        /* shows the path to the current element in the status bar */
        this.updateState = function(selNode, event) {
            /* calculate and display the path */
            var path = '';
            var currnode = selNode;
            while (currnode.nodeName != '#document') {
                path = '/' + currnode.nodeName.toLowerCase() + path;
                currnode = currnode.parentNode;
            };
            
            window.status = path;
        };
    };

  The second argument is the event, if any, which started the updateState
  chain (this doesn't have to be available, since updateState can also be
  called by tools in certain situations).
  Now that we have the updateState method complete, we have the functionality
  we wanted available. To make sure the tool provides all functionality
  required by the system, the tool should subclass EpozTool. This is not
  strictly required, but is a nice way to provide all the methods the editor
  requires to be available.

    // subclass EpozTool
    ExampleTool.prototype = new EpozTool;

  To hook the tool up with the editor, it needs to be registered. For this
  purpose the registerTool method is available on the EpozEditor object, which
  should be called with an id and a tool as arguments (note that the
  EpozEditor is called 'epoz' here, like in the default initEpoz function,
  where registration of our tool will most likely be done):

    var exampletool = new ExampleTool();
    epoz.registerTool('exampletool', exampletool);

  That's it, we just wrote a replacement for (or actually a copy of) the
  ShowPathTool, the most basic Epoz tool.

4. A more complex example

  Now we'll take a look at a more complex example: the ImageTool. This will 
  provide a way for users to add images to the iframe. First thing we do is
  write a class like we did for the example tool:

    function ImageTool() {
        this.initialize = function(editor) {
            this.editor = editor;
            this.editor.logMessage('Image tool initialized');
        };
    };

  As you can see we override the superclass' initialize method, we don't
  strictly have to here, but it's a custom to send a message to the log window
  telling the user the tool has initialized, and it shows us how a reference
  to the editor object is stored on the tool and used to call logMessage on
  (which in turn calls the logger registered in initEpoz).

  Now let's add some functionality to the tool:

    function ImageTool() {

        // this will be required for later use, when the toolbox is added
        this.toolboxes = {};
        
        this.initialize  = function(editor) {
            this.editor = editor;
            this.editor.logMessage('Image tool initialized');
        };

        this.createImage = function(url) {
            /* insert an image */
            this.editor.execCommand('InsertImage', url);
            this.editor.logMessage('Image inserted');
        };
    };

  This method calls 'execCommand' on the editor object, which in turn calls
  execCommand on the iframe (actually there's another step, but that's not
  important now). execCommand is a built-in method provided by browsers, that
  provides a limited amount of commands to WYSIWYG editor applications. The
  InsertImage command will, as you may have guessed, insert an image at the
  cursor location.

  All that is left to turn the tool into an Epoz Tool is set the prototype:

    ImageTool.prototype = new EpozTool;

  register it to the editor (in initEpoz or the HTML page):

    var imagetool = new ImageTool();
    epoz.registerTool('imagetool', imagetool);

  and we're done with the basic tool.

5. ToolBoxes

  Now we have a class with a method that is capable of inserting an image, but
  no way to call the method yet. The most basic way to call the method would
  be from an HTML button, so we could simply create an HTML button that would
  call the method. For larger tools, however, it's nice to attach the event
  handlers from the code rather then from the HTML itself, keeps things neat
  and clean, but on the other hand it's nasty to have references to UI
  elements in the classes that contain the functionality, for instance since
  someone using your tool may want to use an element with another event or
  interface, or perhaps even a completely different type of input (e.g. the
  context menu mentioned below). Therefore we want another abstraction layer,
  toolboxes.
  A ToolBox is basically a UI part of a Tool, containing references to the
  HTML elements and containing and registering event handlers. For the image
  tool we'd probably want a class with a reference to 2 HTML elements: an
  input field to enter a URL in and a button to click on to create the image:

    function ImageToolBox(inputel, buttonel) {
        this.inputel = inputel;
        this.buttonel = buttonel;

        this.initialize(tool, editor) {
            // always store the references to the tool and editor
            this.tool = tool;
            this.editor = editor;

            // addEventHandler registers an event handler in both IE and
            // Mozilla, last argument is the context in which to call the 
            // method
            addEventHandler(this.button, 'click', this.createImage, this);
        };
        
        this.createImage = function() {
            var src = this.inputel.value;
            if (!src) {
                this.editor.logMessage('No image created since no URL was ' +
                                        'specified');
                return;
            };
            this.tool.createImage(src);
        };
    };

  We don't create an updateState method here, although we could, since we
  don't have a way to change the image's src attribute anyway. The updateState
  method of toolboxes will usually update the state of the UI elements of the 
  tool (as happens with the TableTool, when inside a tool you will see editing
  elements, when you're not inside one you'll find elements to add a new table)
  or set the value of certain form fields (as in the LinkTool, when inside a
  link it will show you the href of the link), both not appropriate in this
  case (although a usecase could easily be written I would say ;).
  
6. ContentFilters

  Before Epoz sends its contents to the server, it will be passed through any
  registered content filters. Content filters are simple classes that should
  have 2 methods: initialize, which will be called on registration of the
  filter, with a single argument which is a reference to the editor, and 
  filter, which will be called with 2 arguments: a reference to the owner
  document (the document element of the iframe) and the html node of the
  document. The idea is that the filter should use DOM functionality to filter
  the incoming DOM and return the filtered version.
  To register a content filter to Epoz, use 'epoz.registerFilter(filter)'.

7. Loggers

  Loggers are classes to log messages. A logger will usually do nothing, or at
  most send everything to some text area or div, unless an error log message
  comes in, in which case most loggers will probably raise an exception.
  Loggers should have a single method called 'log' that gets one mandatory and
  one optional argument: it must get a message (which is a string that will,
  or in some cases will not, get logged) and it may get a severity argument,
  which can be 0 for debug messages, 1 for warnings and 2 for errors. The
  default should be 0.
  A logger should be passed to the editor on instantiation time (it's one of
  the constructor's arguments).

8. And the rest

  Of course there's a lot more to Epoz that can be customized, added or
  rewritten. For more details see the source: it should be quite clear and
  simple and is written in a nice object oriented manner. Also make sure to
  check out API.txt, which is a reference to Epoz' JavaScript API.
