Inserting Content Between Uneditable Sections

One of our long time partners came to us with a problem recently - they use the contenteditable attribute heavily to make sections of the document uneditable for users. The problem was that when two uneditable sections were next to each other, users can't insert anything between them. For many of our clients this is the desired behavior but it doesn't suit everyone. To solve the problem we actually need a new method to temporarily ignore the contenteditable attribute and allow our code to make changes. That new method is now available in all the early access builds via the DocumentModifier.setOverrideContentEditable method. We'll also make use of the technique for finding specific parent elements described in last week's article.

The solution we'll create is to add a custom menu item that allows a user to insert content before the current element. In the process, we'll demonstrate a very powerful form of the insertHtml method available from DocumentModifier that enables a wide range of possibilities.

First we need to define the custom menu item in our XML configuration file:

<shrtMenuItem name="insertBefore" action="raiseEvent"
  value="insertBefore" text="Insert Before" />

Simple enough, when the menu's selected it will raise an event we can catch with the value "insertBefore". We need to register an editor event listener to catch that event:

bean.addEditorEventListener(this);

Then implement the EventListener interface with a raise event method:

public void raiseEvent(TextEvent e) {
  if (e.getActionCommand() == TextEvent.CUSTOM_ACTION &&
    e.getExtraInt() == TextEvent.CustomAction.RAISE_EVENT &&
    "insertBefore".equals(e.getExtraString())) {
      insertBeforeSection();
  }
}

We're looking for the text event with the TextEvent.CUSTOM_ACTION action command, TextEvent.CustomAction.RAISE_EVENT as the extra int and "insertBefore" as the extra string. When we get it we call the method that does all the work.

The basic algorithm we'll follow is:

  1. Find the element for the uneditable section.
  2. Override the contenteditable attribute.
  3. Insert an empty paragraph before the uneditable section.
  4. Put the caret into the new paragraph.
  5. Enable the contenteditable attribute again.

To find the uneditable section we use the getUneditableBlock method from the previous article.

Element uneditableBlock = getUneditableBlock(bean);

Next we use the new setOverrideContentEditable method to make the section editable temporarily.

DocumentModifier modifier = bean.getDocumentModifier();
modifier.setOverrideContentEditable(true);

Inserting the empty paragraph is a little complex because we need to insert it at the right point in the HTML element structure. If we used the normal insertHTMLAtCursor function, the paragraph would be inserted into the uneditable section:

What we really want is to insert the new paragraph into the parent element of the uneditable DIV so that it becomes a sibling of the DIV:

To achieve this, we need to use the full form of the insertHTML method in DocumentModifier:

modifier.insertHtml(uneditableBlock.getStartOffset(),
  "<p></p>", HTML.Tag.P, uneditableBlock.getParentElement());

The first two arguments are fairly simple - the character offset to insert the content at and the HTML string to insert. The next two arguments are what give this method so much power.

The HTML.Tag.P argument specifies the first tag to start inserting content from. When the parser parses the HTML string we're inserting, it always creates a complete, well-formed HTML document including the HTML and BODY tags. To insert just a snippet of HTML instead we have to specify when to start inserting. With the simple form of insertHTML this is automatically set to the first tag you actually specify in the HTML string, but complex insertions occasionally require manually setting this parameter. In this case, we want to start inserting from the P tag we specified so we pass in HTML.Tag.P.

The final argument is the one we really need in this case. It allows us to specify how far up the element tree to insert the new content. The simple form of insertHtml always inserts the content as close to the leaf elements as allowed in HTML - in our case that would insert the P tag into the uneditable DIV because that's the first element that is allowed to contain a P tag. If we were inserting an IMG tag it would be inserted into the P tag inside the DIV if there were one etc. By specifying the parent of the uneditable DIV here, we ensure that the inserted paragraph has the same parent element as the uneditable DIV, making them siblings.

Now we need to move the character into the newly inserted paragraph. Since the inserted paragraph is immediately before the uneditable DIV we can just set the caret one position before the start of the DIV and it will be in the new paragraph.

bean.getHTMLPane().setCaretPosition(uneditableBlock.getStartOffset() - 1);

Finally we enable the contenteditable attribute again.

modifier.setOverrideContentEditable(false);

When we put the method together it winds up looking like:

private void insertBeforeSection() {
  Element uneditableBlock = getUneditableBlock(bean);
  DocumentModifier modifier = bean.getDocumentModifier();
  modifier.setOverrideContentEditable(true);
  try {
    modifier.insertHtml(uneditableBlock.getStartOffset(),
      "<p></p>", HTML.Tag.P,
      uneditableBlock.getParentElement());
  } catch (BadLocationException e1) {
    e1.printStackTrace();
  }
  bean.getHTMLPane().setCaretPosition(uneditableBlock.getStartOffset() - 1);
  modifier.setOverrideContentEditable(false);
}

While some of the concepts involved are quite foreign to developers who haven't worked with the Swing text APIs before, the resulting code is fairly simple and our users can now insert content between uneditable sections with a simple menu item.

Finding Specific Parent Elements

When developing custom functionality within EditLive! you occasionally want to work with a specific element in the tree. For example, you might want to find the element that wraps an uneditable section that the user has clicked within. Other examples might be implementing a properties dialog for a custom block tag you've registered or finding the table tag that the caret is within.

We'll use the uneditable section example in this article, but it's simple to adjust this technique for whatever type of element you need to find.

Set Up

We'll assume you've already got a plugin set up and have access to the ELJBean instance for the editor. If not, take a look at our previous article on creating plugins.

Main Loop

The basic idea is to start from the character element (the leaf element in the tree) and work our way back up until we find an element that matches our criteria. If there isn't a matching element we'll eventually reach the root of the tree and return null.

public Element getUneditableBlock(ELJBean bean) {
  JTextPane pane = bean.getHTMLPane();
  HTMLDocument document = (HTMLDocument) pane.getDocument();
  Element element = document.getCharacterElement(pane.getCaretPosition());
  while (element != null && !isUneditableBlock(element)) {
    element = element.getParentElement();
  }
  return element;
}

Note that the getHTMLPane() method returns a standard JTextPane so all the usual Swing text component methods are available. Iterating up the tree is very similar to iterating up a standard DOM.

Conditional Selection

All that's left is to implement the isUneditableBlock method which provides the conditional logic for selecting the element we're looking for. This is the method that will vary based on exactly what you're looking for. In this case we're looking for an element that has a "contenteditable" attribute that's set to "false".

private boolean isUneditableBlock(Element element) {
  AttributeSet attributes = element.getAttributes();
  return attributes.isDefined("contenteditable") &&
    attributes.getAttribute("contenteditable").equals("false");
}

Note that we check attributes.isDefined("contenteditable") as well as that it's set to false because getAttribute will search through the parent elements as well. Also note that the contenteditable attribute isn't recognized by the parser so it appears as a string - recognized attributes use usually instances of HTML.Attribute or CSS.Attribute, for example HTML.Attribute.WIDTH or CSS.Attribute.COLOR.

An alternate implementation of the method that finds the table element would be:

private boolean isUneditableBlock(Element element) {
  return HTML.Tag.TABLE ==
    element.getAttributes().getAttribute(AttributeSet.NameAttribute);
}

Doing Something Useful

Just to round out our example, here's a mouseClicked implementation that would use these methods to select the entire uneditable section whenever the user clicks within it.

public void mouseClicked(MouseEvent e) {
  Element block = getUneditableBlock(bean);
  if (block != null) {
    bean.raiseEvent(new TextEvent(this, TextEvent.SELECT_NODE_ACTION, block, -1));
  }
}

Note that you have to wait for the LOADING_COMPLETE event to fire before adding the mouse listener, otherwise ELJBean.getHTMLPane will return null. For more information, see the previous article on initializing plugins.

Filtering Pasted Content

Sometimes you need to go beyond the options that EditLive! provides for pasting content into the editor and filter the content yourself. For example, to remove images, update or change the URL of links or just forbid certain tags from being used. You can achieve this through our advanced APIs by setting a paste filter.

To provide a simple example, let's write a paste filter for a cat lovers site to change any reference to the word "dog" to be "cat". It may be silly, but it shows the important concepts to let you filter pasted content however you want.

Firstly, we need a class that implements com.ephox.editlive.PasteFilter:

public class SimplePasteFilter implements PasteFilter {

Next, we need the usual constructor that all plugins have, which accepts an ELJBean instance and we'll simply set the current class as the paste filter:

public SimplePasteFilter(ELJBean bean) {
  bean.setPasteFilter(this);
}

Finally, the one method that PasteFilter specifies in String filterIn(String) which is called whenever text or HTML content is pasted to allow the paste filter to make any required changes before the content is inserted into the document. In our case, we do a simple replaceAll:

public String filterIn(String source) {
  return source.replaceAll("dog", "cat");
}

That's it. Now whenever you paste content that contains the string "dog", it is replaced with "cat". You can download the complete source code for the SimplePasteFilter class as a starting point for whatever filter you need.

Creating A Custom View

One of the most powerful, and unfortunately most complex, features of EditLive! is the ability to register your own custom view for an element. You can do this either for existing elements such as IMG to handle them your own way, or generally more usefully for your own custom elements so they don't just render as yellow boxes. As a trivial example, let's look at creating a custom view for the rather simplistic tag:

<simple value="Hello Custom View World" />

We want to give users the ability to easily change the value attribute inline without popping up a separate dialog. To do so, we'll embed a JTextField into the HTML document and keep the contents in sync with the attribute's value. First we create a really simple class that registers the custom view:

public class SimplePlugin {
  // Must have a constructor that takes an ELJBean
  public SimplePlugin(ELJBean bean) {
    // We pass the bean in as the "extra data".
    // This could be anything but it tends to be really useful to
    // pass in the bean so you can access all of EditLive! from within the view.
    bean.registerCustomEmptyView("simple", SimpleView.class.getName(), bean);
  }
}

The key parts of this are:

  • We're registering a view for an "empty" tag. This lets the HTML parser know that there shouldn't be any content in the "simple" tag (just like for an img tag). We could have chosen "inline" or "block" if there was content and it would have been parsed like a span or div tag respectively.
  • The tag name is "simple".
  • We pass in the fully qualified class name to specify the view to load. The class will be instantiated automatically when needed. Note that from EditLive! 6.4 onwards you can (and should) pass in the actual class instead of the name (so SimpleView.class instead of SimpleView.class.getName()).
  • The third parameter takes any object and passes it into the view's constructor. This is a useful way to pass configuration information through to the view or in this case, we pass in the ELJBean instance to let the view interact with the rest of the editor easily.

Now we just have to define the SimpleView class. Firstly, it must extend from javax.swing.text.View - in this case we're extending from a subclass of View designed to make it easy to embed Swing components, ComponentView:

public class SimpleView extends ComponentView implements DocumentListener {

We implement DocumentListener to get the updates from the JTextField which we'll see later.

Secondly, the constructor must either accept just a javax.swing.text.Element or a javax.swing.text.Element and an Object. The Object parameter is that third parameter in the call to registerCustomEmptyView. So our constructor is:

public SimpleView(Element elem, Object extraData) {
  super(elem);
  _bean = (ELJBean)extraData;
} 

The meat of this view is to create the actual JTextField which we do by overriding the createComponent method that ComponentView defines:

protected Component createComponent() {
  _textField = new JTextField();
  String value = (String)getAttributes().getAttribute(HTML.Attribute.VALUE);
  if (value != null) {
    _textField.setText(value);
  }
  _textField.getDocument().addDocumentListener(this);
  return _textField;
}

Creating the text field is pretty straight forward, but we need to keep the text in sync with the value attribute in our tag. To retrieve the initial value we get the attributes for the view using getAttributes() (which is defined in javax.swing.text.View). The values in the returned AttributeSet are in a parsed form so any standard HTML attributes (like value) are actually stored as instances of HTML.Attribute instead of just under the string "value". The same applies to recognized CSS values which are stored under CSS.Attribute instances. The values themselves may also be parsed (particularly for CSS values) but the toString() methods will give you the original String value back.

Finally, we add the view as a document listener on the text field so we can apply any changes the user makes back to the original document. Each of the DocumentListener methods delegate to:

private void updateValue() {
  String currentValue = _textField.getText();
  SimpleAttributeSet add = new SimpleAttributeSet();
  add.addAttribute(HTML.Attribute.VALUE, currentValue);
  try {
    // Always make changes to the document through the
    // document modifier otherwise track changes
    // data will be corrupted and you may accidentally
    // break one of the many assumptions that Swing makes
    // about the way the document is structured.
    _bean.getDocumentModifier().adjustCharacterAttributes(
      getStartOffset(), getEndOffset(), add, Collections.EMPTY_LIST);
  } catch (BadLocationException e) {
    e.printStackTrace();
  }
}

So we simply retrieve the new value from the text field and use the DocumentModifier class. It's important that you always use the DocumentModifier class to make changes to the document as it ensures that the changes are tracked correctly with track changes (and that any existing tracked changes are updated to account for the change) and that the document structure remains valid.

That's it. Whenever the user makes changes in the text field, the attribute in the HTML is updated automatically. You can download the full source of both classes for a better look.

Automatically Linking URLs

If you're using EditLive! to edit HTML emails, it's common for users to simple type in a URL like http://www.ephox.com/ directly into the message and expect that it will automatically converted into a hyperlink. Out of the box EditLive! doesn't do this as it's generally better to use descriptive link text and only specify the URL in the actual link tag1. Fortunately, it's fairly simple to add this functionality to EditLive! using a plugin and it turns out to be a good example of how to monitor and react to the user's actions in the editor. If you'd prefer to skip the gory details and just grab the working plugin, it's available in ready-to-go form. We've previously looked at how to initialize a plugin using background loading so we'll start from there and build the actual linking functionality.

So to get started, in the last article we created the class below that handles the initialization.

public class Autolink implements EventListener {
  private ELJBean _bean;
  public Autolink(ELJBean bean) {
    _bean = bean;
    _bean.addEditorEventListener(this);
    if (_bean.isInitFinished()) {
      initializePlugin();
    }
  }
  public void raiseEvent(TextEvent e) {
    if (e.getActionCommand() == TextEvent.LOADING_COMPLETE) {
      initializePlugin();
    }
  }
}

Now we need to implement the actual initializePlugin method. We'll keep things modular by creating a second class to handle converting links and just instantiate it in initializePlugin():

private void initializePlugin() {
  new LinkConverter(_bean.getDocumentModifier(), _bean.getHTMLPane());
}

The LinkConverter works by listening for key strokes in the HTML pane and when the user types a character that could signify the end of a hyperlink (typically a space, enter or a bracket), it looks at the text just before the caret position and if it is a valid URL, uses the document modifier to apply a hyperlink to it. It's a very simple technique that uses a number of heuristics to make the user experience as pleasant as possible.

Firstly, we need to do some house keeping and register as a key listener:

public class LinkConverter extends KeyAdapter {
  private final JTextPane _pane;
  private final DocumentModifier _modifier;
  public LinkConverter(DocumentModifier modifier, JTextPane pane) {
    _modifier = modifier;
    _pane = pane;
    _pane.addKeyListener(this);
  }

Next we want to implement keyTyped and look for characters the user has typed that might indicate the end of a hyperlink. We use the heuristic that space, enter or a closing bracket might indicate the end of a hyperlink, this should cover the vast majority of cases even if it's not entirely comprehensive.

public void keyTyped(KeyEvent e) {
  if (isTriggerCharacter(e.getKeyChar())) {
    checkForUrlsToConvert();
  }
}
private boolean isTriggerCharacter(char keyChar) {
  return keyChar == ' ' ||    keyChar == '\n' ||    keyChar == '\r' ||    keyChar == ')';
}

The checkForUrlsToConvert method delegates most of the actual work but provides the structure for the algorithm, specifically:

  1. Get the text before the current caret position that could be a URL.
  2. Determine if the text is a valid URL.
  3. If it is, apply a hyperlink to it.
private void checkForUrlsToConvert() {
  try {
    String link = getMostLikelyLinkText();
    if (isUrl(link)) {
      int endOffset = _pane.getCaretPosition();
      applyHyperlink(endOffset - link.length(), endOffset, link);
    }
  } catch (Exception ex) {
    System.err.println("Failed to create URL from text.");
    ex.printStackTrace();
  }
}

Getting the text applies two more heuristics:

  • Links don't cross paragraph boundaries so we only need to search from the start of the current paragraph.
  • Valid links don't contain spaces or opening brackets so we only want the text after the last space or bracket.

The opening bracket may seem like an odd heuristic to apply, but it handles the case where the hyperlink is placed in brackets, eg: (http://www.ephox.com/). That's also why the closing bracket is included as a trigger character.

private String getMostLikelyLinkText() throws BadLocationException {
  int caretPos = _pane.getCaretPosition();
  HTMLDocument document = (HTMLDocument)_pane.getDocument();
  // Get the text from the start of the paragraph to the caret position.
  // Note that document.getText takes an offset and a length, not an end offset.
  Element paragraph = document.getParagraphElement(caretPos);
  String link = document.getText(paragraph.getStartOffset(), caretPos - paragraph.getStartOffset());
  // This is the second heuristic - the last space or bracket
  // indicates the start of the hyperlink.
  int startLink = Math.max(link.lastIndexOf(' '), link.lastIndexOf('('));
  if (startLink >= 0) {
    link = link.substring(startLink + 1);
  }
  return link;
}

Now that we've got the text of the most likely URL candidate, we need to check if it really is a URL, we do this with two steps:

  • Does it start with a valid protocol?
  • Can we parse it as a URI?
private boolean isUrl(String link) {
  if (link.startsWith("http://") || link.startsWith("ftp://") || link.startsWith("https://")) {
    try {
      new URI(link);
      return true;
    } catch (URISyntaxException e) {}
  }
  return false;
}

Finally, we need to actually apply the hyperlink. Most of the details of this are taken care of by the DocumentModifier class. In fact, by using the DocumentModifier the change will be tracked correctly by track changes, undo and redo will work and any formatting that has been applied to the text will be preserved. What we have to do is describe what attributes we want to add or remove and what range to add them in. The range is simple, and we don't need to remove any attributes, so let's look at what attributes we need to add.

The attributes to add are specified as an AttributeSet so we first create one of those. Since we want to add a hyperlink which is an inline tag, we need to add a HTML.Tag.A attribute. The value is another attribute set with the attributes of the link tag we're adding. Fortunately, it's harder to explain than it is to code:

private SimpleAttributeSet getAttributesToAdd(String href) {
  SimpleAttributeSet add = new SimpleAttributeSet();
  SimpleAttributeSet linkAttributes = new SimpleAttributeSet();
  add.addAttribute(HTML.Tag.A, linkAttributes);
  // We can add any attributes we need to the link by adding them
  // to the linkAttributes attributes set.
  linkAttributes.addAttribute(HTML.Attribute.HREF, href);
  return add;
}

Note that we use the constants HTML.Tag.A and HTML.Attribute.HREF instead of the strings "a" and "href", the constants are actually not strings and it won't work like you expect if you use strings.

Now that we've got all the components we need, applying the hyperlink is a simple matter of calling DocumentModifier.adjustCharacterAttributes:

private void applyHyperlink(int startOffset, int endOffset, String href)
throws BadLocationException {
  _modifier.adjustCharacterAttributes(startOffset, endOffset,
    getAttributesToAdd(href), Collections.EMPTY_LIST);
}

When you put all of that together, you have surprisingly effective automatic linking functionality that covers most of the potential use cases. There are obviously a few gaps, the most obvious of which are links in quotes and email addresses, but they use the same fundamental techniques. The final plugin is available for download as a complete, ready to go package with full source code and build scripts - in the future we'll look to build on it to support more use cases and make it more robust.

Finally, let's take a look at the key techniques that we used:

  • Safe initializing of plugins that load in the background.
  • Adding a KeyListener to the HTMLPane to monitor and react to user actions. Obviously any other type of swing listeners could be used when required.
  • Retrieving blocks of plain text content from the document with the getText() method. It's much faster and usually easier to retrieve a block of content from the document and work with it than to iterate over a range one character at a time.
  • Using the DocumentModifier to make changes to the document in a way that works with undo/redo, track changes and handles all the other little details that user's expect like preserving content formatting correctly.
  • Using nested attribute sets and the constants in javax.swing.text.html.HTML.Tag and javax.swing.text.html.HTML.Attribute to define inline tags and attributes.

1 - this is actually quite important for accessibility - having a screen reader speak a URL is much less pleasant than having it read a description of where the link actually goes

Initializing Background Loading Plugins

Plugins that specify a loading policy of "background" download in the background while EditLive! is initializing which gives faster start up times and a better user experience. The downside is that the plugin has to handle being loaded at an indeterminate time, possibly during or possibly after the editor has started up. Fortunately, handling this is actually quite straight forward.

As an example, we'll develop a simple framework for a plugin that will automatically convert links the user types in to clickable hyperlinks. We'll develop the initializing code in this article and the link conversion in part two. If you just want to get the functionality straight away, we've made available a ready-to-go plugin.

To get started, we need the standard constructor that takes an instance of ELJBean and stores it for use later. We also add ourselves as an editor event listener which is safe to do anytime and allows us to get notifications when the editor finishes loading.

public class Autolink implements EventListener {
  private ELJBean _bean;
  public Autolink(ELJBean bean) {
    _bean = bean;
    _bean.addEditorEventListener(this);
  }
}

Next, we add the raiseEvent method that looks for the TextEvent.LOADING_COMPLETE event and adds our listener.

public void raiseEvent(TextEvent e) {
  if (e.getActionCommand() == TextEvent.LOADING_COMPLETE) {
    initializePlugin();
  }
}

At this point (assuming we define initializePlugin, which we'll cover in part two), we have a plugin that works perfectly if it's loading policy is set to early. With background loading we also need to handle being loaded after the LOADING_COMPLETE event fires - with the current code, our plugin wouldn't initialize if it missed that event.

Revisiting the constructor, all we have to do is add the highlighted code below to check if the editor has loaded and immediately initialize if it has.

public Autolink(ELJBean bean) {
  _bean = bean;
  _bean.addEditorEventListener(this);
  if (_bean.isInitFinished()) {
    initializePlugin();
  }
}

That's it! Now if the editor is loaded when our plugin loads, the constructor will take care of initialization, otherwise the LOADING_COMPLETE event will trigger it. You can download the complete file and use it as a template for your own plugins.

Astute readers might be tempted to add synchronization to the class to avoid race conditions, however EditLive! actually takes care of it for us. Since plugins are so heavily oriented towards accessing Swing methods, they are always created on the Swing thread and EditLive! only ever calls the raiseEvent function on the swing thread. So our initialization code will only ever be called from the swing thread and we don't have to worry about threading issues.

The downside is that if your plugin needs to do anything complex for initializing, it needs to spawn a new thread to do the initialization off the swing thread or it may interrupt the user's work. Just make sure to use SwingUtilities.invokeLater to jump back onto the swing thread before accessing any Swing or EditLive! methods.

Making Plugins Even Easier

The new plugin architecture makes it a two step process to deploy extensions to EditLive!:

  1. Upload the plugin files.
  2. Edit the page that embeds EditLive! and add a call to addPlugin.

That's not bad, but that second step is a pain, particularly if it's over a remote connection to the server. It would be really nice if we could just upload the files and have them magically be picked up. Turns out, with a little bit of code on the server that's easy to do. As an added bonus it will make the applet start up faster too.

The secret is to create a directory that is specifically for EditLive! plugins and have the server iterate through the files in that directory and add them as plugins. Lets take a look at an example - this is in Java but the concepts are pretty easy to apply in any language.

We'll assume that the plugins directory is available via http://example.com/eljplugins/

First, get the plugins directory as a File:

File pluginsDir = new File(getServletContext().getRealPath("/eljplugins/"));

Now we want to iterate through all the files in the plugins directory and work just on the XML files as they're the plugin descriptors we want to load. There may be other files in the directory like jar files or javascript files for the plugins that we want to skip.

File[] pluginConfigFiles = pluginsDir.listFiles(new FileFilter() {
	public boolean accept(File testFile) {
		return testFile.getName().endsWith(".xml");
	}
});

Now that we have the list of plugin config files to load, for each one we read the file contents and add it as a plugin.

for (int i = 0; i < pluginConfigFiles.length; i++) {
	try {
		BufferedReader in = new BufferedReader(new FileReader(pluginConfigFiles[i]));
		StringBuffer configContent = new StringBuffer();
		for (String line = in.readLine(); line != null; line = in.readLine()) {
			configContent.append(line);
			configContent.append("\n");
		}
		out.println("elj.addPluginAsText('" +
                    URLEncoder.encode(configContent.toString()) + "',
	            '/eljplugins/" + pluginConfigFiles[i].getName() + "');\n");
	} catch (Exception e) {
		e.printStackTrace();
	}
}

There's a fair bit going on there, the majority of it is just reading in the contents of the file. The important line is:

out.println("elj.addPluginAsText('" + URLEncoder.encode(configContent.toString()) + "',
	'/eljplugins/" + pluginConfigFiles[i].getName() + "');\n");

This outputs a JavaScript call to addPluginAsText with two parameters:

  1. The URL encoded content of the plugin config file.
  2. The URL to the plugin config file. This is used to resolve relative references against so that the plugin resources can be found correctly.

The main catch with this code is that it doesn't correctly detect the character set from the XML config files and always uses the platform default setting. If you need to deal with international character sets in plugin config files, you'll need to add in code to handle that1.

Here's the entire code snippet:

File pluginsDir = new File(getServletContext().getRealPath("/eljplugins/"));
if (pluginsDir.exists()) {
	File[] pluginConfigFiles = pluginsDir.listFiles(new FileFilter() {
	    public boolean accept(File testFile) {
		return testFile.getName().endsWith(".xml");
	    }
	});
	for (int i = 0; i < pluginConfigFiles.length; i++) {
	    try {
		BufferedReader in = new BufferedReader(
                    new FileReader(pluginConfigFiles[i]));
		StringBuffer configContent = new StringBuffer();
		for (String line = in.readLine(); line != null; line = in.readLine()) {
			configContent.append(line);
			configContent.append("\n");
		}
		out.println("elj.addPluginAsText('" +
                    URLEncoder.encode(configContent.toString()) + "',
	            'eljplugins/" + pluginConfigFiles[i].getName() + "');\n");
	    } catch(Exception e) {
		e.printStackTrace();
	    }
	}
}

Here's a similar function in PHP:

function loadPlugins($jsVariableName, $pluginDir, $pluginUrl) {
    if (is_dir($pluginDir) && $dh = opendir($pluginDir)) {
        while (($file = readdir($dh)) !== false) {
            if (fnmatch("*.xml", $file)) {
            	$filename = $pluginDir . '/' . $file;
            	$content = file_get_contents($filename);
            	if ($content) {
            	    $content = rawurlencode($content);
	            echo "$jsVariableName.addPluginAsText('$content', '$pluginUrl');\n";
	        }
            }
        }
        closedir($dh);
    }
}

 

1 - I'd recommend sticking with US-ASCII for plugin config files, you can always use XML entities if you need special characters and it makes it a lot easier for people to deploy the plugin.

Using Plug-ins To Extend EditLive!

The EditLive 6.1 release adds a new plugin architecture to make it easier to extend EditLive! Since EditLive! has always been extremely customizable and extendable, plugins don't actually add much more flexibility, what they do provide is:

  • Easier management and deployment of extensions, particularly across multiple sites.
  • More flexibility in deployment, particularly in terms of reducing the start up time of the applet.

Easier Management and Deployment

Using the plugin architecture, allows developers to bundle up their extensions to EditLive! easily and deploy them to multiple sites really easily, without having to edit multiple files server side. The plugins on LiveWorks! will make use of this ease of deployment by providing plugin definitions for each plugin going forward. To take an example, previously the instructions for deploying the footnotes plugin were:

Installing the footnotes plugin requires two steps, installing the plugin jar and adding the menu item the configuration file.

JavaScript for adding the plugin jar:

elj.addJar("footnotes.jar", "com.ephox.footnotes.Footnotes");

Custom menu item to add to the configuration file:

<customMenuItem name="insFootnote" action="raiseEvent" value="addFootnote"
text="Insert Footnote" shortcut="control shift F" />

With the plugin architecture this will be reduced to just one step:

Register the plugin with the following JavaScript:

       elj.addPlugin("footnotes.xml");

Not only do we avoid having to edit two files, but it also hides the "magical incantations" to define the custom menu item and specify the class to load. The plugin descriptor provides all that information. Soon we'll show you how to set up your server so you that all you have to do is drop the plugin files into a specific directory.

Oh and relative URLs in plugin definition files, resolve relative to the plugin definition itself, so you shouldn't need to adjust the URLs for different sites. You can specify the base URL to use for each plugin if you use addPluginAsText.

More Flexibility In Deployment

When you use the addJar function to load advanced API classes, the jar is downloaded and the class initialized before the editor even begins to load which increases the start up time for the applet. With plugins however, you can specify that the plugin should be loaded in the background so that it doesn't delay the applet start up. The immediate loading behavior of addJar can be provided when required using the "immediate" loading mode, however most plugins should use one of the delayed loading modes:

Background

Background loading is most useful for plugins like the link info plugin, where the functionality isn't required immediately, but also isn't triggered by a menu item being selected. Background loading plugins start downloading when the applet first loads, but in a background thread so the applet can continue starting up. As soon as the jar files are downloaded, the plugin class is initialized and the plugin starts working.

Lazy

Lazy loading is like background loading but, well, lazier. The plugin jars start downloading in a background thread, but the plugin class isn't created until the user specifically activates the plugins custom menu item. Obviously, this requires that the plugin is activated by selecting a custom menu item and that the menu item is defined in the plugin's definition file. The first advantage of this approach is that the plugin doesn't take up any resources at all until it is actually used. However, the main advantage is that because EditLive! knows when the plugin is required, if it's not ready a progress dialog is displayed to let the user know how much longer until the plugin is available and give them a chance to cancel the operation. Since the plugin downloads in the background, it is pretty rare for users to ever have to wait for the plugin to load, but it you have very big plugins1 or users with slow connections, it's nice to know they'll get the best possible experience.

More Than Java

Plugins don't have to use Java code and jars at all, you can specify JavaScript files to load too - it works just like the script was included via a <script> tag in the HTML page. Custom menu items can then raise events out to the JavaScript methods from the script file to trigger custom extensions.

Hints and Tips

There are a few simple things you can do to make best use of the new plugin architecture:

  • Make sure your plugins use "background" or "lazy" loading mode if at all possible.
  • Use addPluginAsText instead of addPlugin2 to avoid extra HTTP connections back to the server to get the plugin files. Stay tuned for an upcoming post on a really neat way to set this up.

Future Plans

We're keen to keep improving the plugin architecture to maximize its usefulness for developers. We'd love to hear your thoughts on how we can make it better, feel free to leave a comment below or chat to us about it on the LiveWorks! mailing list. Some of the things that are on our radar for possible inclusion:

  • More flexible ways to specify when plugins are required, for example ensuring the plugin is loaded before a paste occurs.
  • More flexibility in specifying custom user interface elements. Where should custom menu items be added? Do we need ways of specifying toolbar buttons and shortcut menu items?
  • Server side management of plugins. Do we need to create server-side management tools for enabling, disabling and configuring plugins? What sort of things would this do?
  • Reducing the size of the editor. We'd like to investigate splitting parts of the editor out into plugins to help improve start up time and reduce the download size.
  • Seriously, is it spelt plug-in or plugin?

Useful Resources

There's a heap of information on using and creating plugins in our SDK:

1 - for testing, we had to use the rt.jar from the JRE that contains the entire class library so that there was enough time to see that the progress bar was actually working correctly.

2 - and setConfigurationText instead of setConfigurationURL for your config file while you're there