Skip to content

Collaborative Text Editor

Pablo Ojanguren edited this page Mar 20, 2017 · 1 revision

SwellRT provides a powerful editor Web component supporting real time collaborative editing.

Text documents and editor support by default styles for text and paragraphs but also customization using widgets (images, tables...) annotations (links, comments...) and CSS.

Introduction

Text documents

A text document in SwellRT is any field of type "TextType" within a collaborative object. This design allows to have multiple text documents in a single collaborative object.

// Open a collaborative object
var co = SwellRT.openModel(...);

// Create a text field
var text = co.createText("Write here initial content");

//  Add text field to the collaborative object
text = co.root.put("text", text);

Text fields can store plain text plus annotations and widgets. The editor is responsible to provide a basic user interface to edit the text and interact with annotations and widgets.

Text annotations

An annotation is a range of text in a document associated with a key/value pair. Annotations are rendered by the editor. The editor supports text style annotations by default but it can be extended to support custom annotations (e.g. links, comments) that can be controlled by the app trough handlers.

Text widgets

A widget is a special content (no text) that can be embedded in a document. It allows to provide complex interactions within a rendered document, for example, tables, images...

Apps can define custom widgets and to control then within the editor. Documents store only the type and state of the widget.

Editor Web Component

An editor instance provides a web canvas to render and edit text documents of a collaborative object. The editor also shows real time interaction from other users. First of all, an editor must be attached to a parent DOM element.

Having the following "div" element

<div id="editor-panel"/></div>

a new editor can be instanced using the SwellRT object:

<script>
  var editor = SwellRT.editor("editor-panel", <widgets>, <annotations>);
</script>

The editor must be instanced after the SwellRT client is ready and the page's DOM is totally built.

The arguments <widgets> and <annotations> allow to register custom handlers for widgets and annotations. See further information later on this document.

Editor life cycle

An editor instance is designed to be recycled and used to edit different documents during the same web session. By default, after editor is created it shows an empty canvas and any input is ignored until a "TextType" instance is associated.

Following the previous example, get a "TextType" field named "text"...

// Open a collaborative object
var co = SwellRT.openModel(...);

// Create a text field, then add it to the collab. object
var text = co.createText("Write here initial content");
text = co.root.put("text", text);

Attach the text document to the editor with the method edit(TextType: text). The document will be rendered and user can type in the canvas:

editor.edit(text);

To disable typing but keeping the rendered text use the setEditing(boolean: enableEditing) method:

editor.setEditing(false);

To stop showing the text and clear the editor canvas, use the cleanUp() method:

editor.cleanUp();

To edit a different text invoke again the edit(TextType: text) method:

editor.edit(anotherTextField);

Live carets and participants activity

When multiple participants are editing the document, the editor will show the caret for each participant in different colors. Apps can be notified on participants activity in the document and which caret's color is assigned to them, using the following callback object;

editor.edit(text, {

        onActive: function(address, color) {
            console.log("The participant "+address+" is editing now with the caret color "+color);
        },

        onExpire: function(address) {
            console.log("The participant "+address+" is not editing");
        }

});

Annotations

Annotation handlers

Annotation handlers are passed to the editor method as third argument: SwellRT.editor(String: elementId; Object: widgets; Object: annotations). The "annotations" object has the following structure:

var annotations = {

  "annotation-key-1" : <handler-1>,

  "annotation-key-2" : <handler-2>,

  ...
}

Each annotation definition has a key and a handler object. A general example a simple comment annotation handler definition follows. It will be used along this documentation. The meaning of each part of a handler are discussed in the following sections.

A reference to the Editor instance can be get inside a handler with the expression this.editor.

var annotations = {

    "comment" : {

        styleClass: "comment-on",

        style: {
            "backgroundColor" : "red"
        },

        onEvent: function(range, event) {

          if (event.type == "click") {
            var annotation = this.editor.getAnnotationInRange(range, "comment");
            var comment = prompt("Comment: ", annotation.getValue());
            if (url != null)
               this.editor.setAnnotationInRange(range, "comment", comment);
          }

        },

        onChange: function(range) {

        }

    }
}

Note: The editor provides some default annotations for text and paragraph styles. No handlers are required for them.

Annotate text and rendering

Every time a range of text is annotated with annotation, the editor will render the annotated text wrapped in the following element (considering the "comment" example)

If the user has selected a sentence in the text with the mouse...

It’s a reality that is present in basically every single new car

that text can be annotated invoking the method setAnnotation(String: key; String value;)

editor.setAnnotation("comment", "please, provide some examples");

the editor will render the text again, wrapped in a special <span> element

<span class="comment comment-on" data-comment="please, provide some examples">

It’s a reality that is present in basically every single new car

</span>

This element will have by default a CSS class equals to the annotation key therefore all texts annotated with same key can be selected in the DOM. Also, a CSS class defined in the handler ("styleClass") will be added. The value of the annotation for that text will be set in the attribute data-<annotation key>.

Get annotations on the caret or selected text: onSelectionChanged() and Range objects

Usually, an app will need to know which annotations are set in the current editor's selection or caret in order to trigger UI interactions. For example, if the user sets the caret on a word within a commented text, then comment is displayed in a pop up dialog.

The editor notifies any change of the current selection or caret position invoking a listener defined with the onSelectionChanged(Function: listener) method:

editor.onSelectionChanged(function(range) {

    if (range.anntations.comment != null) {

      var annotation = editor.getAnnotationInRange(range, "comment");
      var comment = prompt("Comment: ", annotation.value);
      if (url != null)
          editor.setAnnotationInRange(range, comment);			
    }

});

The listener receives a "Range" object that provides the following information:

  • range.annotations : map of annotation keys/values in the current position.
  • range.start : the start position of the selected text
  • range.end : the end position of the selected text
  • range.text : the text in the selection, empty if it a caret position
  • range.node : the closest DOM node to the range

if there is not a selection -just a caret position- "range.start" is equals to "range.end"

Update annotated text: Annotation objects

Use setAnnotation(String key, String value):Annotation or setAnnotationInRange(Range range; String key; String value):Annotation;`

The annotation object provides following information:

  • annotation.key : the annotation key
  • annotation.start : the start position of the annotated text
  • annotation.end : the end position of the annotated text
  • annotation.value : the value of the annotation
  • annotation.text : returns the text wrapped by the annotation
  • annotation.id : the DOM element id attribute
  • annotation.element : the DOM element

Capture Events for annotated text

Capturing mouse and keyboard events on an annotated text enables special interactions. Events are passed to the onEvent(Range: range; Event: event) function defined in the handler.

Capture changes in annotated text

Changes in a text that is already annotated can be listened using the onChange(Range: range) function defined in the handler.

Get all text annotated with same key

The editor method getAnnotationSet(String: key) returns an array of Annotations objects with the same annotation key in the sequential order of the document.

Remove annotations (unannotate text)

Text can be unannotated with the following editor methods:

  • clearAnnotatio(String keyOrPrefix) clear annotation in the current selection
  • clearAnnotationInRange(Range: range; String key) clear the annotation in the range

Predefined Annotations

Links

The editor has a default annotation for HTML anchors defined by the key "link". The value of the annotation is a URL that will set as a href attribute in the <a> HTML element. Listeners for this default link annotation can be defined registering a handler for the key "link":

var  annotations = {

  "link" : {

    onEvent: function(range, event) {

    },

    onChange: function(range) {

    }

  }

}

Text & paragraph styles

To change text styles some predefined annotations can be used.

For example, to apply the bold style in the current caret position or on the current selected text of the editor invoke the setAnnotation(String: key; String value) method:

editor.setAnnotation("style/fontWeight","bold");

This is the list of predefined style annotations keys and theirs legal values:

Annotation Name Values
style/backgroundColor (a valid CSS color expression e.g. #FF33CC)
style/color (a valid CSS color expressione.g. #FF33CC)
style/fontFamily (a CSS valid value for font-family)
style/fontSize size in pixels
style/fontStyle (a CSS valid value or null) normal, italic, oblique, inherit
style/fontWeight (a CSS valid value or null) normal, bold, bolder, lighter
style/textDecoration (a CSS valid value or null) underline, overline, line-through, blink
style/verticalAlign (a CSS valid value or null) Baseline, sub, super, top, text-top, middle, bottom, text-bottom
paragraph/header h1 ... h5
paragraph/listStyleType unordered, decimal
paragraph/textAlign left, center, right

Headings

Text headers provided by the "paragrap/header" annotation support handlers for mutations and events. Register a handler for this annotation as usual:

var  annotations = {

  "paragraph/header" : {

    onEvent: function(range, event) {
      console.log("Header annotation event received!");
    },

    onChange: function(range) {
      console.log("Header annotation content changed");      
    }

  }

}

Header annotations are rendered with a id attribute, allowing to scroll to the header. By now, this id is not consistent across different browser instances, so don't use it as part of a URL.

Example: implementing a text style toolbar

Buttons or other UI elements can be bind to annotations to provide a toolbar to turn on/off styles in the editor. This is a straightforward example of a button handling Bold text style:

First define CSS styles to display a "Bold" button with state "on" or "off" (the CSS class is used as state of the button)

<style>
  .b-enabled {
    font-weight: bold;
  }

  .b-disabled {
    font-weight: normal;
  }
 </style>

Declare the HTML button:

<input id="btnBold" type="button" name="bold" value="B" class="b-disabled" />

Define button's behavior:

document.getElementById("btnBold")
  .onclick = function(e) {

  if (this.className == "b-disabled") {

    editor.setAnnotation("style/fontWeight","bold");
    this.className = "b-enabled";

  } else {

    editor.setAnnotation("style/fontWeight",null);
    this.className = "b-disabled";

  }
}

Finally, change the state of the button according to the current selection or caret:

editor.onSelectionChanged(

  function(range) {

    var btnBold = document.getElementById("btnBold");
    if (range.annotations["style/fontWeight"] == "bold") {
      btnBold.className = "b-enabled";
    } else {
      btnBold.className = "b-disabled";
    }

});

Widgets

Widgets are special content embedded in a document. It enables to have complex interactions within a rendered document, like tables, images...

Apps can define custom widgets and to control then within the editor. When a widget is inserted in a document, this only stores the type and state of the widget. The state can be changed by any participant of the document. The widget is rendered in the editor according to its handler.

Widget handlers

Widget handlers are passed to the editor method as second argument: SwellRT.editor(String: elementId; Object: widgets; Object: annotations). The "widgets" object has the following structure:

var widgets = {

  "widget-type-1" : <handler-1>,

  "widget-type-2" : <handler-2>,

  ...
}

Each widget definition has a type and a handler object. A general example of a simple image widget handler follows. It will be used along this documentation. The meaning of each part of a handler are discussed in the following sections.

var widgets = {

  "image": {

    // Mandatory handler functions

    onInit: function(element, state) {
      element.innerHTML="<img src='"+state+"'>";
    },

    onChangeState: function(element, oldState, newState) {
      element.innerHTML="<img src='"+newState+"'>";
    },

    onActivated: function(element) {
      element.addEventListener("click", this.handleOnClick, false);
    },

    onDeactivated: function(element) {
      element.removeEventListener("click", this.handleOnClick, false);
    }

    // This an example method.
    // It is not a mandatory function for widget handler.

    handleOnClick: function(e) {
      var widget = editor.getWidget(e.target);
      var url = prompt("Set Image link: ", widget.getState());
      if (url)
        widget.setState(url);
    },

  }

Insert a widget, rendering and event listeners

To insert a widget in the current caret position of the document use the method editor.addWidget(String: type; String state). For example, calling

editor.addWidget("image", "http://swellrt.org/logo.png");

will render the following HTML in the editor:

<div  id="image-1470852082439"
      data-image="http://swellrt.org/logo.png"
      class="widget image"
      contenteditable="true">

      <img src="http://swellrt.org/logo.png"></div>

</div>

when a widget is renderer for the first time in the editor, its handler's method onInit(Element: element; String: state) is executed, allowing to render customized HTML inside the a container <div>. In this example, a <img> element is added to the DOM.

When the state of the widget changes, it is rendered again calling the handler's method onChangeState(Element: element; String oldState; String newState)

Notice the attributes automatically added in the widet's root '

' element:
  • id: is a unique id for the widget in the document with the syntax <widget-type>-<timestamp>
  • data-: data attribute to store the state of the widget.
  • class: two classes are automatically added by the editor to identify the type of element, "widget" and "" of the widget.

If the editor is ready to capture events the handler's method onActivated(Element element) is executed, allowing to register custom event handlers in the DOM. Be careful to unregister event handlers in the method onDeactivated(Element element) which is call when the editor stops capturing events.

Updating the widget state: Widget objects

In order to update the state of a widget a "Widget" object must be used. Usually, it can be obtained passing an inner DOM element of the widget to the method editor.getWidget(Element: element).

A Widget object exposes following methods:

  • widget.getElement() Gets the root DOM element of this widget
  • widget.setState(String: state) Sets the state of the widget
  • widget.getState() Gets the current state
  • widget.getType() Gets the type of the widget
  • widget.getId() Gets an unique id within the document
  • widget.remove() Remove the widget from the document
  • widget.isOk() Check if this objects is an active widget