-
Notifications
You must be signed in to change notification settings - Fork 34
Collaborative Text Editor
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.
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.
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.
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.
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.
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);
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");
}
});
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.
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>
.
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"
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
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.
Changes in a text that is already annotated can be listened using the onChange(Range: range)
function defined in the handler.
The editor method getAnnotationSet(String: key)
returns an array of Annotations objects with the same annotation key in the sequential order of the document.
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
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) {
}
}
}
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 |
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.
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 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 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);
},
}
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 '
- 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.
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
Texts and images of the SwellRT Wiki are released as open content, under the Creative Commons Attribution 4.0 International.