Annotation of kupuMPIWG/doc/EXTENDING.txt, revision 1.1.1.1
1.1 dwinter 1: ==============
2: Extending Kupu
3: ==============
4:
5: XXX this document needs a major overhaul
6: XXX we should cover the the typical process of implementing a new feature
7: XXX writing a new implementation of features -> INTEGRATING.txt
8:
9: Abstract
10: --------
11:
12: This document describes the typical process of extending Kupu with a
13: new feature, such as a toolbox with the underlying functionality.
14: Both, the ECMAScript API and the templating system are explained.
15:
16: 1. Kupu Tools
17: -------------
18:
19: Apart from the context menu, all UI functionality in Kupu is added to
20: the core using a simple plugin mechanism. This can also be used to
21: write your own extensions, either by copying existing tools and
22: modifying them or by subclassing KupuTool (and sometimes KupuToolBox)
23: and writing your own functionality.
24:
25: 2. Tool interface
26: -----------------
27:
28: The plugins have a simple interface to adhere to:
29:
30: interface KupuTool:
31:
32: attribute toolboxes = {};
33: - id -> toolbox mapping
34:
35: method initialize(editor):
36: - store reference to the editor, register event handlers, etc.
37:
38: method registerToolBox(id, toolbox):
39: - register a UI element (ToolBox, more about those later)
40:
41: method updateState(selNode, event):
42: - update the state of self and any toolboxes
43:
44: method createContextMenuElements(selNode, event) (optional):
45: - return an array of ContextMenuElements
46:
47: 3. Writing a simple tool
48: ------------------------
49:
50: As an example we'll write our first simple tool, which displays the
51: path to the current node in the tree in the status bar. This is an
52: actual tool in the current Kupu code and you can see the result in the
53: status bar of any default Kupu installation. First we'll write a
54: class, and add a method called 'updateState' that will change the
55: status bar::
56:
57: // Example tool class
58: function ExampleTool() {
59: this.updateState = function(selNode, event) {
60: };
61: };
62:
63: The updateState method is a method each tool can implement (actually
64: the code assumes it's available on each tool, but since the superclass
65: defines it as well it's not strictly necessary to add it to your
66: class) and that will be called when the user clicks inside the iframe,
67: presses enter or hits a cursor key (so basically, when the cursor can
68: end up in a different element).
69:
70: As you can see we use in-line methods in Kupu, this is a choice of
71: style rather than for a functional reason, and if you wish you can
72: choose to use another coding style. Using
73: '<class>.prototype.<method> = function() {...};' will do equally well.
74:
75: Now we'll add some functionality to our updateState method. The
76: updateState method's first argument will always be a reference to the
77: current selected element (or the element currently containing the
78: selection). To get a path to that element from the root, we can write
79: something like::
80:
81: // Example tool class
82: function ExampleTool() {
83: /* shows the path to the current element in the status bar */
84: this.updateState = function(selNode, event) {
85: /* calculate and display the path */
86: var path = '';
87: var currnode = selNode;
88: while (currnode.nodeName != '#document') {
89: path = '/' + currnode.nodeName.toLowerCase() + path;
90: currnode = currnode.parentNode;
91: };
92:
93: window.status = path;
94: };
95: };
96:
97: The second argument is the event, if any, which started the
98: updateState chain (this doesn't have to be available, since
99: updateState can also be called by tools in certain situations).
100:
101: Now that we have the updateState method complete, we have the
102: functionality we wanted available. To make sure the tool provides all
103: functionality required by the system, the tool should subclass
104: KupuTool. This is not strictly required, but is a nice way to provide
105: all the methods the editor requires to be available.
106:
107: // subclass KupuTool
108: ExampleTool.prototype = new KupuTool;
109:
110: To hook the tool up with the editor, it needs to be registered. For
111: this purpose the registerTool method is available on the KupuEditor
112: object, which should be called with an id and a tool as arguments
113: (note that the KupuEditor is called 'kupu' here, like in the default
114: initKupu function, where registration of our tool will most likely be
115: done)::
116:
117: var exampletool = new ExampleTool();
118: kupu.registerTool('exampletool', exampletool);
119:
120: That's it, we just wrote a replacement for (or actually a copy of) the
121: ShowPathTool, the most basic Kupu tool.
122:
123: 4. A more complex example
124: -------------------------
125:
126: Now we'll take a look at a more complex example: the ImageTool. This
127: will provide a way for users to add images to the iframe. First thing
128: we do is write a class like we did for the example tool::
129:
130: function ImageTool() {
131: this.initialize = function(editor) {
132: this.editor = editor;
133: this.editor.logMessage('Image tool initialized');
134: };
135: };
136:
137: As you can see we override the superclass' initialize method, we don't
138: strictly have to here, but it's a custom to send a message to the log
139: window telling the user the tool has initialized, and it shows us how
140: a reference to the editor object is stored on the tool and used to
141: call logMessage on (which in turn calls the logger registered in
142: initKupu).
143:
144: Now let's add some functionality to the tool::
145:
146: function ImageTool() {
147:
148: // this will be required for later use, when the toolbox is added
149: this.toolboxes = {};
150:
151: this.initialize = function(editor) {
152: this.editor = editor;
153: this.editor.logMessage('Image tool initialized');
154: };
155:
156: this.createImage = function(url) {
157: /* insert an image */
158: this.editor.execCommand('InsertImage', url);
159: this.editor.logMessage('Image inserted');
160: };
161: };
162:
163: This method calls 'execCommand' on the editor object, which in turn
164: calls execCommand on the iframe (actually there's another step, but
165: that's not important now). execCommand is a built-in method provided
166: by browsers, that provides a limited amount of commands to WYSIWYG
167: editor applications. The InsertImage command will, as you may have
168: guessed, insert an image at the cursor location.
169:
170: All that is left to turn the tool into an Kupu Tool is set the prototype::
171:
172: ImageTool.prototype = new KupuTool;
173:
174: register it to the editor (in initKupu or the HTML page)::
175:
176: var imagetool = new ImageTool();
177: kupu.registerTool('imagetool', imagetool);
178:
179: and we're done with the basic tool.
180:
181: 5. ToolBoxes
182: ------------
183:
184: Now we have a class with a method that is capable of inserting an
185: image, but no way to call the method yet. The most basic way to call
186: the method would be from an HTML button, so we could simply create an
187: HTML button that would call the method. For larger tools, however,
188: it's nice to attach the event handlers from the code rather then from
189: the HTML itself, keeps things neat and clean, but on the other hand
190: it's nasty to have references to UI elements in the classes that
191: contain the functionality, for instance since someone using your tool
192: may want to use an element with another event or interface, or perhaps
193: even a completely different type of input (e.g. the context menu
194: mentioned below). Therefore we want another abstraction layer,
195: toolboxes.
196:
197: A ToolBox is basically a UI part of a Tool, containing references to
198: the HTML elements and containing and registering event handlers. For
199: the image tool we'd probably want a class with a reference to 2 HTML
200: elements: an input field to enter a URL in and a button to click on to
201: create the image::
202:
203: function ImageToolBox(inputel, buttonel) {
204: this.inputel = inputel;
205: this.buttonel = buttonel;
206:
207: this.initialize(tool, editor) {
208: // always store the references to the tool and editor
209: this.tool = tool;
210: this.editor = editor;
211:
212: // addEventHandler registers an event handler in both IE and
213: // Mozilla, last argument is the context in which to call the
214: // method
215: addEventHandler(this.button, 'click', this.createImage, this);
216: };
217:
218: this.createImage = function() {
219: var src = this.inputel.value;
220: if (!src) {
221: this.editor.logMessage('No image created since no URL was ' +
222: 'specified');
223: return;
224: };
225: this.tool.createImage(src);
226: };
227: };
228:
229: We don't create an updateState method here, although we could, since
230: we don't have a way to change the image's src attribute anyway. The
231: updateState method of toolboxes will usually update the state of the
232: UI elements of the tool (as happens with the TableTool, when inside a
233: tool you will see editing elements, when you're not inside one you'll
234: find elements to add a new table) or set the value of certain form
235: fields (as in the LinkTool, when inside a link it will show you the
236: href of the link), both not appropriate in this case (although a
237: usecase could easily be written I would say ;).
238:
239: 6. ContentFilters
240: -----------------
241:
242: Before Kupu sends its contents to the server, it is converted from
243: HTML to XHTML and then passed through any registered content filters.
244:
245: Content filters are simple classes that should have 2 methods::
246: initialize, which will be called on registration of the filter, with a
247: single argument which is a reference to the editor, and filter, which
248: will be called with 2 arguments: a reference to the owner document
249: (the document element of the iframe) and the html node of the
250: document. The idea is that the filter should use DOM functionality to
251: filter the incoming DOM and return the filtered version. To register
252: a content filter to Kupu, use 'kupu.registerFilter(filter)'.
253:
254: The XHTML conversion is configurable. By default it accepts any tags
255: defined in XHTML 1.0 transitional, as well as any attributes except
256: for the event attributes (those beginning with 'on'). Tags not in the
257: html namespace are stripped.
258:
259: A tag is permitted if it is defined in XHTML and not blacklisted.
260: Optionally kupu can also filter to maintain a valid XHTML nesting of
261: tags, but this option is off by default (put a
262: `<filterstructure>1</filterstructure>` tags into the htmlfilter
263: configuration to turn it on, or set `editor.xhtmlvalid.filterstructure`
264: to true. When the structure filtering is turned on a tag is permitted
265: if it is allowed within the current context (e.g. you cannot nest `<A>`
266: tags directly inside other `<A>` tags, so any nested `<A>` tags will be
267: stripped). If a tag is stripped its content will still be present. An
268: attribute is permitted if it is defined in XHTML and not blacklisted
269: for that tag. Text data is stripped if it occurs in a context not
270: valid in XHTML (e.g. inside a table but outside `<TH>` or `<TD>`).
271:
272: The blacklist is taken from Kupu's xml configuration. An `<htmlfilter>` tag
273: encloses the blacklist, `<t>` marks a tag to be blacklisted, <a> marks
274: an attribute to be blacklisted, `<c>` marks attributes to be excluded
275: for specific tags. e.g. the following would blacklist the `center`
276: and `font` tags, the `cellspacing` attribute everywhere, and the
277: `width` attribute for the `th` and `td` tags only. ::
278:
279: <htmlfilter>
280: <t>center></t><t>font</t>
281: <a>cellspacing</a>
282: <c><t>th</t><t>td</t><a>width</a></c>
283: </htmlfilter>
284:
285: From Javascript you can use the excludeAttributes, excludeTags and
286: excludeTagAttributes methods of `editor.xhtmlvalid` to get the same
287: effect.
288:
289: The `style` attribute filters its content to permit `text-align` and
290: `list-style-type` styles and reject all others (these styles are used
291: by kupu). The style whitelist may be extended by adding additional
292: entries to `editor.xhtmlvalid.styleWhitelist`.
293:
294: The `class` attribute is filtered to reject classnames which appear in
295: the class blacklist. Add entries to `editor.xhtmlvalid.classBlacklist`
296: to reject additional classes.
297:
298: Entries may be added to the style whitelist or the class blacklist
299: through the `<htmlfilter>` section of the configuration::
300:
301: <htmlfilter>
302: ... as above ...
303: <style>list-style-image</style>
304: <class>badClassName</class>
305: </htmlfilter>
306:
307: Additional attributes may be added to existing tags by calling the
308: `setTagAttributes` and `setAttrFilter` methods. e.g. ::
309:
310: editor.xhtmlvalid.setTagAttributes(['div'], ['special']);
311: editor.xhtmlvalid.setAttrFilter(['special']);
312:
313: adds an attribute `special` to the `div` tag. The method tags three
314: parameters: a list of attributes, a list of tags, and optionally a
315: filter method to be called for that attribute. If the filter method is
316: omitted the attribute is simply copied.
317:
318: The validation API is as follows (for the purposes of this api, a
319: `dict` means an object whose property names are the dict keys):
320:
321: Set
322: a javascript class used to quickly create a 'dict' where all
323: the values are 1. The constructor takes a single keyname, or a list
324: of keynames, or a Set.
325:
326: attrFilters
327: a dict keyed by attributeName, of filter functions for each
328: attribute. Any attribute not in this dict is invalid.
329:
330: tagAttributes
331: a dict keyed by tagName, of arrays of valid attribute names for
332: each tag. Any tag not in this dict is invalid. N.B. Tags which share
333: a common set of attributes may be sharing the same array.
334:
335: All attributeNames in the sets in tagAttributes must exist in
336: attrFilters.
337:
338: setAttrFilter(attributes,filter)
339: sets the filter function in attrFilters for the list of attributes,
340: adding the attributes if they don't already exist. A simple copy
341: function is used if filter is not provided.
342:
343: setTagAttributes(tags,attributes)
344: for each tag in the list of tags, sets tagAttributes[tag] to the list of
345: attributes. An empty list of attributes is used if attributes is
346: omitted.
347:
348: includeTagAttributes(tags, attributes)
349: for each tag in the list of tags, adds the list of attributes to
350: tagAttributes[tag]
351:
352: excludeTagAttributes(tags, attributes)
353: for each tag in the list of tags, removes any attribute in the list
354: of attributes from the list in tagAttributes[tag].
355:
356: excludeTags(tags)
357: removes the list of tags from tagAttributes.
358:
359: excludeAttributes(attributes)
360: removes the set of attributes from attrFilters and all tags in
361: tagAttributes.
362:
363: styleWhitelist
364: a Set. Each key is the css name of a style. e.g. text-align.
365: Only those elements of the style attribute which are present in this
366: set are permitted through the filtering.
367: The default setting includes only those styles which may be set by
368: kupu.
369:
370: classBlacklist
371: a Set. Each key is a classname which should not be permitted. All
372: other classnames are allowed through the filtering. The default
373: setting is a list of styles likely to be present when a pasting text
374: copied out of Microsoft Word.
375:
376:
377: 7. Loggers
378: ----------
379:
380: Loggers are classes to log messages. A logger will usually do nothing,
381: or at most send everything to some text area or div, unless an error
382: log message comes in, in which case most loggers will probably raise
383: an exception. Loggers should have a single method called 'log' that
384: gets one mandatory and one optional argument: it must get a message
385: (which is a string that will, or in some cases will not, get logged)
386: and it may get a severity argument, which can be 0 for debug messages,
387: 1 for warnings and 2 for errors. The default should be 0.
388:
389: A logger should be passed to the editor on instantiation time (it's
390: one of the constructor's arguments).
391:
392: 8. And the rest
393: ---------------
394:
395: Of course there's a lot more to Kupu that can be customized, added or
396: rewritten. For more details see the source: it should be quite clear
397: and simple and is written in a nice object oriented manner. Also make
398: sure to check out JSAPI.txt, which is a reference to Kupu' JavaScript
399: JSAPI.
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>