WYSIWYG Editor Evaluation

Last modified by Vincent Massol on 2024/02/26 17:55

 XWiki
 Design
 Completed

Description

Currently XWiki has developed its own WYSIWYG editor. The challenge is in maintaining it over time and to compete with existing solutions such as TinyMCE, CKEditor, VisualEditor, etc. It's not XWiki's business to work on the WYSIWYG and as a consequence not a lot is invested nowadays on the WYSIWYG editor. The risk is in other editors improving fast while we stagnate.

Thus there are 2 solutions for us:

  • Start investing more in our WYSIWYG editor to catch up/invent new powerful features + work on usability
  • Consider replacing it with an existing editor without loosing the features we like so much ATM (perfect round-tripping between markup and HTML, visually rendered Macros and edition, etc)

This page serves as a location where to put our analysis of the XWiki WYSIWYG editor vs other editors.

Ideas of editors to evaluate:

  • Visual Editor
  • CKEditor
  • TinyMCE

Comparison on ohloh

Discarded editors:

Common Integration Steps

[Step 1] Ideally the editor should be available as a WebJar. If this is not the case then we need to write a Maven module to create the WebJar. Until we fix XWIKI-10881 we may have to work around two problems:

  • the editor attempting to load resources from the WebJar using the URL of its code as the base path. In order to overcome this the editor should have a configuration option that controls the base path.
  • the editor CSS attempting to load images from the WebJar. In order to overcome this we'll have to overwrite those styles (e.g. background-image).

[Step 2] The Edit menu is currently hard-coded. We should provide an extension point so that new edit modes can be added from XWiki extensions. It may be also interesting to have an extension point dedicated to the WYSIWYG edit mode and an administration section to choose from the available WYSIWYG editors (similar to the way we choose the search engine). Then each editor can provide its own configuration section.

[Step 3] We load the editor code using RequireJS:

require.config({
  paths: {
    editor: "$!services.webjars.url('editor-webjar', 'editor.js')"
  },
  shim: {
    editor: {
      exports: 'EDITOR'
    }
  }
});

require(['jquery', 'editor'], function($, editor) {
 // Load the editor.
});

[Step 4] We need to choose how to edit the content. There are two main options:

  • Inline: the edited content is a fragment of the page where the editor is loaded. This means that the edited content is affected by the CSS and, more importantly, the JavaScript code on that page. This is a true WYSIWYG editing mode, but the downside is that the edited content can have dynamic behaviour due to the JavaScript code on the page. In any case, this mode is suited for section editing (some helpers are needed to detect and wrap the section content).
  • Isolated: the edited content is loaded inside an iframe so it isn't affected by the CSS and the JavaScript code on the page where the editor is loaded. For this we need to tell the editor which styles to apply to the edited content. One option is to pass to the editor a full HTML page that includes the needed style sheets in the HEAD element (and omits the JavaScript!). This mode is not a true (100%) WYSIWYG edit mode unless you make sure the HTML structure is the same as in view mode, which may be difficult. Usually the edited content is put directly under the BODY element (of the iframe document) which is not the case in view mode. To overcome this we need to write some custom CSS that is applied to the edited content to remove the differences.

[Step 5] We need to check what is the expected input and the output of the editor. All web rich text editors work on HTML so most probably they will expect HTML as input and they will produce HTML as output. On our side the content is stored in wiki syntax so we need services to convert the wiki syntax to HTML and from HTML back to wiki syntax. There are 3 ways to feed the input to the editor:

  1. Put the HTML input inside a (DIV) container and tell the editor to edit it (this is suited for inline edit mode described above)
  2. Put the HTML input in a textarea and tell the editor to edit it. This can work with both inline and isolated modes.
  3. Give the editor an URL that can be used to load the edited content. This is suited for isolated mode, when the resource represented by the URL provides the full HTML page (with style sheets).

The current editor uses both (2) and (3). We can reuse some of the existing work like this:

<form>
 ...
  <textarea name="content">$escapetool.xml($renderedContent)</textarea>
  <input value="content" name="RequiresHTMLConversion" type="hidden" />
  <input value="$doc.syntax.toIdString()" name="content_syntax" type="hidden" />
  ...
</form>

No matter what editor we integrate, as long as it reads the HTML input from the textarea and puts the modified version back then when the form is submitted the HTML will be automatically converted to the specified syntax on the server, provided there is a dedicated rendered. This is done in a servlet filter (ConversionFilter) when the RequiresHTMLConversion parameter is detected.

Providing HTML input for the editor is a bit tricky when editing TextArea xobject properties because at the moment TextAreaClass#displayEdit() puts the wiki syntax in the generated HTML textarea.

; Description
: $doc.display('description')
...
## See textarea_wysiwyg.vm
#set ($editors = $xcontext.getEditorWysiwyg())
#if ($editors)
  ## $editors is a comma separated list of textarea Ids for which you need to load the WYSIWYG editor.
#end

We need to convert the wiki syntax from the text area to HTML before the editor is loaded, probably by making an AJAX request. Changing the TextArea xproperty displayer to put the HTML (rendered content) in the text area for WYSIWYG fields would be nice but it may break existing applications.

[Step 6] Check whether the editor supports XHTML 1.0 or HTML5. For the moment the rendering framework can parse only XHTML. We will probably have to write an HTML5 parser too. Check also if the HTML cleaner used on the server side supports HTML5.

[Step 7] The HTML input that is passed to the editor should be obtained by rendering the edited content using the annotatedxhtml/1.0 renderer. The annotations are required in order to know how to compute the wiki syntax back. There are annotations for links to wiki pages (internal links) and annotations that mark the output of wiki macros (and transformations in general), for instance. At the moment the annotations are implemented as XML comments thus we need to ensure these comments are not removed or messed up by the editor.

<!--startmacro:info|-||-|This is an **info** message.-->
<div class="box infomessage">
  This is an <strong>info</strong> message.
</div>
<!--stopmacro-->

In the future we will probably move to an annotated HTML5 renderer that uses the data-* attributes to store the annotations.

<div data-macro="info" data-macroContent="This is an **info** message.">
  <div class="box infomessage">
    This is an <strong>info</strong> message.
  </div>
</div>

[Step 8] Not all the HTML elements have an associated wiki syntax element. For instance you cannot represent an HTML button in wiki syntax. For this you need to use the HTML macro. Ideally the editor should provide a way to prevent the user from generating HTML content that cannot be converted to wiki syntax. For instance we should be able to say "disable all features that generate form elements". Otherwise these elements will be lost in the conversion and the user will be very surprised after saving the content.

[Step 9] The editor needs to have support for read-only fragments. This is required in order to protect the output of the wiki macros (and any transformation in general). For instance we should be able to say "don't allow the user to edit the content of a DIV or SPAN element that has the 'wiki-macro' class".

[Step 10] We should be able to extend the editor with custom features, using plugins, ideally, or a documented SDK/API. The following features are specific to XWiki:

  • Link to a wiki page
  • Link to a file attached to a wiki page
  • Link to any XWiki entity in general
  • Insert an image from the attachments
  • Insert a wiki macro
  • Import an office document

Note that all these features don't depend on the editor being used so we should implement them separately so that they can be reused. We already have this need for the Insert Macro wizard which is needed by the Dashboard Application.

CKEditor Analysis

You can follow the progress on the CKEditor Integration extension page.

Project contributions

Documentation

CKEditor has a lot of good quality documentation that is very easy to navigate.

Custom WebJar Build

The WebJar that is available by default provides 3 pre-packaged configurations: basic, standard and full. This is useful if you want to play with the editor but the documentation says:

The most recommended approach to adjusting the editor to your needs is to start from creating a custom build, removing unwanted features before they even make it to your toolbar. It is a bad practice to download the Full package and then remove plugins or buttons in your configuration. You will only be loading unnecessary stuff without any good reason.

So I've decided to create a custom WebJar build. Since neither the sources nor the CKBuilder are available on Maven central (at least not a recent version) I had to use the SCM Maven plugin to checkout the sources from GitHub and build the editor with custom configuration, including only the plugins that are useful for XWiki.

CKEditor has a base path configuration option that can be used to overcome XWIKI-10881:

window.CKEDITOR_BASEPATH = "$!services.webjars.url('org.xwiki.contrib:application-ckeditor-webjar', '')";

For the CSS we had to overwrite the URLs used for the toolbar icons:

a.cke_button > span.cke_button_icon {
 /* Fix for XWIKI-10880 (A CSS file inside a webjar cannot use a resource from that webjar). */
 background-image: url("$services.webjars.url('org.xwiki.contrib:application-ckeditor-webjar', 'skins/moono/icons.png')") !important;
}

User Interface

The CKEditor has 2 UI modes that that interesting for us:

  • Fixed UI (suited for editing the entire document content, in a dedicated edit mode)
  • Floating UI (suite for section editing and maybe editing TextArea xproperties)

For the fixed UI the editor can accept the the full page HTML, meaning that we can control the edit "template" and styles.

There are many UI configuration options. We managed to configure the tool bar easily.

Data Processor

The CKEditor uses a Data Processor API to parse the input and to generate the output. This means that theoretically you can give any syntax as input and get it back as output if you write a Data Processor that knows how to transform that syntax into HTML (on the client side, in JavaScript code). Unfortunately, although they advertise it:

Even server side transformation could be considered, by executing Ajax style calls from the data processor code.

the Data Processor interface is synchronous so I don't see how we could use it to call our rendering framework on the server side. This means that for the moment we'll have to feed HTML to the editor and not directly wiki syntax. But this Data Processor is something to keep in mind in case they make the API asynchronous.

The default XHTML Data Processor is very flexible as it provides many hooks that we can use to adjust the content before it is loaded or saved.

Advanced Content Filter

CKEditor provides an Advanced Content Filter that:

limits and adapts input data (HTML code added in source mode or by the editor.setData method, pasted HTML code, etc.) so it matches the editor configuration in the best possible way. It may also deactivate features which generate HTML code that is not allowed by the configuration.

Ideally we could use this filter to ensure that the editor doesn't generate HTML elements that cannot be converted to wiki syntax. The filter has two modes: automatic and custom.

We cannot use the automatic mode because the XWiki syntax is too powerful. For instance we can set custom parameters to almost any element using wiki syntax:

(% foo="bar" %)
Some text here.

The "foo" attribute is going to be filtered out as long as there is no editor feature that declares support for it (add/edit/remove).

Unfortunately we cannot use the custom mode either, because there is no way to exclude the macro output from filtering. The HTML macro can produce any HTML elements and we don't want them to be filtered out when the editor is loaded.

Plugins

We can extend the CKEditor with plugins. There are many plugins available. I managed to write a plugin that enhances the "Source" feature with the ability to edit XWiki syntax quite easily following the tutorial. The plugins can be included in the CKEditor build (that generates a single JavaScript file) but we can also register external plugins, written in JSX objects for instance (this is helpful while developing the plugin).

Widgets

The CKEditor has support for grouping HTML elements and protecting them from direct editing. These groups are called widgets and I think we can use them to implement the wiki macros.


 

Get Connected