Making Custom Changes To The Document

Advanced integrators of EditLive! may want to provide custom editing operations specific to their users needs. One such example is the footnotes plugin which essentially just automates a series of document changes to make life easier for the user. In many cases, the standard events that EditLive! provides (see the TextEvent class) are sufficient and the developer can just raise the right sequence of events, but when that's not enough we need to delve into the DocumentModifier class.

The first thing to remember when working with EditLive! is that the editor is based on a standard Swing JTextPane, so all the familiar text APIs for caret manipulation, text retrieval etc. However, the document model used by Swing for HTML documents is actually fairly difficult to work with and can lead to unexpected results if it is modified in the wrong way. That's why we've introduced the DocumentModifier class - to make it easier to make advanced changes to the document with the smallest risk of error. To get an instance of DocumentModifier, simply call the getDocumentModifier() method on the ELJBean.

The document modifier provides methods to perform the basic operations you need to build up nearly any kind of modification to the document. There are four types of operations:

  • Insert
  • Remove
  • Adjust Properties
  • List Operations

You probably shouldn't use the list operations methods - it's much simpler and easier to just raise the appropriate text event.

Inserting

Mostly, inserting is straight forward, pass in the content to insert and the offset to insert as and it just happens. You can either insert a HTML fragment with insertHTML or a plain text string with insertString. insertString will take care of escaping any special characters like &, < and > correctly. There is one slightly more complex method though:

public void insertHtml(int offset,
                       String html,
                       HTML.Tag firstTagToInsert,
                       Element insertIntoElement)
                throws BadLocationException

It turns out that parsing completely arbitrary HTML fragments is really hard and the HTML parser Swing uses just can't do it, but sometimes you really want to insert a HTML fragment that isn't well formed. For example, to append some text to the end of the current paragraph and insert a new paragraph with one insert you would want to insert "end of paragraph</p><p>New paragraph</p>". Except that's not a well formed fragment without an opening <p> tag at the start. So the HTML we insert is "<p>end of paragraph</p><p>New paragraph</p>" and we set firstTagToInsert to HTML.Tag.CONTENT which indicates that everything before the first CONTENT element should be skipped (in this case, the opening <p> tag).

The insertIntoElement allows you to insert at different levels in the tree. For instance, to insert a new table row, you set insertIntoElement to the table element, whereas the standard insertHTML(int, String) would attempt to insert the row into the table cell instead. You can also use insertIntoElement to insert at the correct level in nested tables and lists.

Removing

Removing is really straight forward. You can remove a particular range of the document content (including any elements that are completely contained within that range) with the remove(int, int) method. This is what EditLive! uses when the user hits the backspace key. The remove(Element) method is most useful when working with nested elements, for example when removing a table within another table. It will make sure that the resulting HTML structure is "sensible", in other words if you remove the last row from a table, the table itself will also be removed.

Adjust Properties

Firstly, it's important to understand that element properties are more than just the attributes of an element - they also include any inline tags like B, I and U plus the CSS equivalents (nearly all rendering in EditLive! is actually based on CSS, not HTML tags). So the HTML fragment "<b><i><sup>content</sup></i></b>" would result in one Element in the document model with the attributes:

Attribute Name Attribute Value
StyleConstants.NameAttribute HTML.Tag.CONTENT
HTML.Tag.B (Empty AttributeSet)
HTML.Tag.I (Empty AttributeSet)
HTML.Tag.SUP (Empty AttributeSet)
CSS.Attribute.FONT_WEIGHT CSS.Value instance
CSS.Attribute.FONT_STYLE CSS.Value instance
CSS.Attribute.VERTICAL_ALIGN CSS.Value instance

The B, I and SUP tags, like all inline HTML tags, are mapped to another attribute set which contains their attributes (eg: if the bold tag had a class attribute, it's nested attribute set would map HTML.Attribute.CLASS to the class name). If this concept of nested attribute sets doesn't make sense to you, don't worry - it confuses all our new employees for a while too. The best way to understand it is to inspect the attributes of elements that have been parsed from various HTML source documents.

The CSS.Value instances are a rather annoying internal Swing representation of the CSS value. The classes involved are all private to Swing but fortunately the toString() method always returns the CSS value as the user supplied it. Finally, note that the attibute names are all specific constants, not strings.  The HTML.Tag, HTML.Attribute and CSS.Attribute classes define the constants that you're likely to see. Unrecognized attributes generally do end up as plain Strings.  Future LiveWorks! articles will delve into the structure of attribute sets in more detail.

There are two important methods for adjusting element properties:

  • setElementProperties
  • adjustCharacterAttributes

setElementProperties affects only the specified element, whereas adjustCharacterAttributes affects a range within the document and can intelligently preserve existing formatting within the range (eg: if bold is applied to part, but not all of the range). Note that adjustCharacterAttributes only affects leaf elements like CONTENT so you can't use it to make changes to paragraph elements. Both methods take an AttributeSet containing the new name/value mappings to add and a Collection of attribute names to remove. The attributes are removed first, then the new ones added so the resulting set always contains the new attributes you specify.

Track Changes

One of the things that DocumentModifier takes care of for you is track changes. Any changes you make through the DocumentModifier will be correctly tracked and users can accept and reject the changes like normal.

Adrian spends his days working out ways to make life easier for Ephox clients through initiatives like LiveWorks! Previously a senior engineer, he has now moved on to the fancier sounding title of CTO.

Leave a Reply