After the Deadline

Add Grammar and Spell Check to Any WYSIWYG Editor

Posted in HOWTO by rsmudge on January 5, 2010

After the Deadline front-end libraries are available for jQuery and TinyMCE. You’ve asked what it would take to make AtD available in another WYSIWYG environment or even in a web-based word processor. In the past I’d answer that you’d need to study the code to either the jQuery or TinyMCE extensions, visit the mountains, meditate and wait for the answer to come.

Today I’m bringing the mountain to you. Much of the code that makes After the Deadline work in TinyMCE and jQuery is similar. Painfully similar. To the point where a bug in one is a bug in the other. I’ve refactored these extensions and created an After the Deadline Core UI module. This module is browser independent with no external dependencies. It provides functionality to parse the AtD XML into an error data structure, retrieve suggestions and other information given an error phrase and a word of context, and it also abstracts away the logic to traverse a DOM and insert the AtD markup for errors.

To test this module, I gave myself one working day to port After the Deadline to CKEditor. CKEditor is a WYSIWYG editor, similar to TinyMCE. I have never used CKEditor before and this was a challenge. Still, I was able to make it quite far and in this post, I’ll show you how to add AtD to an editor using the Core UI Module.

Here is what an After the Deadline editor plugin must do:

  1. The first step is to setup the AtD Core UI module:
    atd_core = new AtDCore();
  2. The next step is to define several functions that the AtD Core UI module expects. The module will use these functions to manipulate the DOM and find elements in the way that your environment expects. The full list of functions is documented in the AtD Core UI README. Here are a few of these functions from CKEditor:
    atd_core.replaceWith = function(old_node, new_node) {
       return new_node.replace(CKEDITOR.dom.element.get(old_node));
    };
    
    atd_core.create = function(node_html) {
       return CKEDITOR.dom.element.createFromHtml( '<span class="mceItemHidden">' + node_html + '</span>' );
    };
    
  3. Once these functions are defined you can set AtD-specific preferences like the list of strings to ignore and which types of errors to show.
    atd_core.showTypes('Complex Expression, Diacritical Marks, Double Negatives, Redundant Expression');
    atd_core.setIgnoreStrings('CKEditor, was thrown');
    
  4. When a user requests proofreading, it is up to you to extract the contents of the editor and post it to the After the Deadline service. Here is how I extract the editor contents in CKEditor:
    var editor_contents = editor.document.getBody().getHtml();
    
  5. You should receive an XML document from the server with an error message or a data structure containing the AtD data. To check the XML document for an AtD error:
    function ajax_callback(xml_response) {
       if (atd_core.hasErrorMessage(xml_response)) {
          alert(atd_core.getErrorMessage(xml_response));
          return;
       }
    
  6. If there are no errors then you’ll want to parse the XML into a data structure the Shared UI code can use. Use the processXML function to do this. It will return a JavaScript object that you will be using again.
    var results = atd_core.processXML(xml_response);
  7. Now, let’s say you want to highlight errors in your editor. Great! Extract the contents of the editor (should be an array of elements from the root element) and pass these to markMyWords. Using the prototypes you provided earlier, this function will walk through these nodes and highlight the errors. Be thankful that you didn’t have to write the code to do this.
    var nodes = editor.document.getDocumentElement().getChildren().getItem(1)['$'].childNodes;
    atd_core.markMyWords(nodes, results.errors);
    
  8. Earlier, I hope you attached a click or context menu listener to your editor. If you did you can use isMarkedElement on the event target when a click occurs in your editor. If this returns true, this is your clue that a user clicked on a marked error and you should display a menu offering them suggestions.To get the suggestions, call findSuggestion using the marked element. This will return a JavaScript object with the following members that may interest you: suggestions, description, moreinfo
    editor.contextMenu.addListener(function(element) {
       if (atd_core.isMarkedNode(element.$)) {
          var meta = atd_core.findSuggestion(element.$);
          var commands = {};
    
          addItem(editor, meta.description, function() { }, 0, commands, 'AtD_description');
    
          for (var x = 0; x < meta.suggestions.length; x++)
             addItem(editor, meta.suggestions[x], makeCallback(element.$, meta.suggestions[x]), x + 1, commands, 'AtD_suggestions');
    
          addItem(editor, 'Ignore', makeIgnoreCallback(element.$, element.$.innerHTML), 1, commands, 'AtD_ignore');
          addItem(editor, 'Ignore All', makeIgnoreAllCallback(element.$, element.$.innerHTML), 0, commands, 'AtD_ignore');
    
          return commands;
       }
    });
    
  9. For each suggestion, I use makeCallback to generate a function to attach to the menu item. This generated function calls applySuggestion in the Core UI Module. You should use applySuggestion as it’s smart enough to do what the suggestion asks. For example, the (omit) suggestion removes the word in question:
    var makeCallback = function(element, suggestion) {
       return function() {
          atd_core.applySuggestion(element, suggestion);
       };
    };
    
  10. You may want to add functionality to let the user ignore the current error or to ignore all occurrences of it. The “Ignore suggestion” menu item removes the marked node keeping its children. No need to call into the AtD Core UI module:
    var makeIgnoreCallback = function(element, word) {
       return function() {
          CKEDITOR.dom.element.get(element).remove(true);
    };
    

    To ignore all occurrences of an error, you can use the removeWords function in the Core UI Module.

    var makeIgnoreAllCallback = function(element, word) {
       return function() {
          atd_core.removeWords(undefined, word);
       };
    };
    
  11. The last thing you should do is attach a listener to remove the After the Deadline markup when the contents of the editor are grabbed. You can remove the AtD markup using the removeWords function.

And that’s it. Add in the necessary scaffolding for your editor and you have After the Deadline integration. Here are some other things you can do:

  • Make an Ignore Always menu option save the user’s preference in a cookie or on your server. Resurrect this setting later with the setIgnoreStrings function.
  • Keep track of the user’s preference for what types of errors to show. Use showTypes to enable these for the user.

These are the kinds of things we do in WordPress to make AtD a first-class feature for our users. If you’re looking to port After the Deadline to another environment, the AtD Core UI module will save you a lot of time.

Coming up I’ll be releasing versions of the AtD/jQuery and AtD/TinyMCE Extensions using this shared module. The AtD/CKEditor extension is available now.