============== Extending Kupu ============== XXX this document needs a major overhaul XXX we should cover the the typical process of implementing a new feature XXX writing a new implementation of features -> INTEGRATING.txt Abstract -------- This document describes the typical process of extending Kupu with a new feature, such as a toolbox with the underlying functionality. Both, the ECMAScript API and the templating system are explained. 1. Kupu Tools ------------- Apart from the context menu, all UI functionality in Kupu 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 KupuTool (and sometimes KupuToolBox) and writing your own functionality. 2. Tool interface ----------------- The plugins have a simple interface to adhere to: interface KupuTool: 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 Kupu code and you can see the result in the status bar of any default Kupu 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 Kupu, 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 KupuTool. This is not strictly required, but is a nice way to provide all the methods the editor requires to be available. // subclass KupuTool ExampleTool.prototype = new KupuTool; To hook the tool up with the editor, it needs to be registered. For this purpose the registerTool method is available on the KupuEditor object, which should be called with an id and a tool as arguments (note that the KupuEditor is called 'kupu' here, like in the default initKupu function, where registration of our tool will most likely be done):: var exampletool = new ExampleTool(); kupu.registerTool('exampletool', exampletool); That's it, we just wrote a replacement for (or actually a copy of) the ShowPathTool, the most basic Kupu 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 initKupu). 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 Kupu Tool is set the prototype:: ImageTool.prototype = new KupuTool; register it to the editor (in initKupu or the HTML page):: var imagetool = new ImageTool(); kupu.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 Kupu sends its contents to the server, it is converted from HTML to XHTML and then 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 Kupu, use 'kupu.registerFilter(filter)'. The XHTML conversion is configurable. By default it accepts any tags defined in XHTML 1.0 transitional, as well as any attributes except for the event attributes (those beginning with 'on'). Tags not in the html namespace are stripped. A tag is permitted if it is defined in XHTML and not blacklisted. Optionally kupu can also filter to maintain a valid XHTML nesting of tags, but this option is off by default (put a `1` tags into the htmlfilter configuration to turn it on, or set `editor.xhtmlvalid.filterstructure` to true. When the structure filtering is turned on a tag is permitted if it is allowed within the current context (e.g. you cannot nest `` tags directly inside other `` tags, so any nested `` tags will be stripped). If a tag is stripped its content will still be present. An attribute is permitted if it is defined in XHTML and not blacklisted for that tag. Text data is stripped if it occurs in a context not valid in XHTML (e.g. inside a table but outside `` or ``). The blacklist is taken from Kupu's xml configuration. An `` tag encloses the blacklist, `` marks a tag to be blacklisted, marks an attribute to be blacklisted, `` marks attributes to be excluded for specific tags. e.g. the following would blacklist the `center` and `font` tags, the `cellspacing` attribute everywhere, and the `width` attribute for the `th` and `td` tags only. :: center>font cellspacing thtdwidth From Javascript you can use the excludeAttributes, excludeTags and excludeTagAttributes methods of `editor.xhtmlvalid` to get the same effect. The `style` attribute filters its content to permit `text-align` and `list-style-type` styles and reject all others (these styles are used by kupu). The style whitelist may be extended by adding additional entries to `editor.xhtmlvalid.styleWhitelist`. The `class` attribute is filtered to reject classnames which appear in the class blacklist. Add entries to `editor.xhtmlvalid.classBlacklist` to reject additional classes. Entries may be added to the style whitelist or the class blacklist through the `` section of the configuration:: ... as above ... list-style-image badClassName Additional attributes may be added to existing tags by calling the `setTagAttributes` and `setAttrFilter` methods. e.g. :: editor.xhtmlvalid.setTagAttributes(['div'], ['special']); editor.xhtmlvalid.setAttrFilter(['special']); adds an attribute `special` to the `div` tag. The method tags three parameters: a list of attributes, a list of tags, and optionally a filter method to be called for that attribute. If the filter method is omitted the attribute is simply copied. The validation API is as follows (for the purposes of this api, a `dict` means an object whose property names are the dict keys): Set a javascript class used to quickly create a 'dict' where all the values are 1. The constructor takes a single keyname, or a list of keynames, or a Set. attrFilters a dict keyed by attributeName, of filter functions for each attribute. Any attribute not in this dict is invalid. tagAttributes a dict keyed by tagName, of arrays of valid attribute names for each tag. Any tag not in this dict is invalid. N.B. Tags which share a common set of attributes may be sharing the same array. All attributeNames in the sets in tagAttributes must exist in attrFilters. setAttrFilter(attributes,filter) sets the filter function in attrFilters for the list of attributes, adding the attributes if they don't already exist. A simple copy function is used if filter is not provided. setTagAttributes(tags,attributes) for each tag in the list of tags, sets tagAttributes[tag] to the list of attributes. An empty list of attributes is used if attributes is omitted. includeTagAttributes(tags, attributes) for each tag in the list of tags, adds the list of attributes to tagAttributes[tag] excludeTagAttributes(tags, attributes) for each tag in the list of tags, removes any attribute in the list of attributes from the list in tagAttributes[tag]. excludeTags(tags) removes the list of tags from tagAttributes. excludeAttributes(attributes) removes the set of attributes from attrFilters and all tags in tagAttributes. styleWhitelist a Set. Each key is the css name of a style. e.g. text-align. Only those elements of the style attribute which are present in this set are permitted through the filtering. The default setting includes only those styles which may be set by kupu. classBlacklist a Set. Each key is a classname which should not be permitted. All other classnames are allowed through the filtering. The default setting is a list of styles likely to be present when a pasting text copied out of Microsoft Word. 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 Kupu 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 JSAPI.txt, which is a reference to Kupu' JavaScript JSAPI.