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 '.prototype. = 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.