🎉 JointJS has new documentation! ðŸ¥³

JointJS+ TextEditor

ui.TextEditor

ui.TextEditor is a powerful (rich-text) inline text editor that can be used on any SVG text element. The ui.TextEditor provides the following features:

  • rich-text editing of SVG text
  • caret & selections
  • caret & selections can be styled in CSS
  • double-click to select words, triple-click to select the entire text
  • keyboard navigation native to the underlying OS
  • API for programmatic access
  • support for editing scaled & rotated text
  • automatic URL hyperlinks detection and styling

Installation

Include joint.ui.textEditor.js and joint.ui.textEditor.css files to your HTML:

<link rel="stylesheet" type="text/css" href="joint.ui.textEditor.css">
<script src="joint.ui.textEditor.js"></script>

Usage

In the following example, we will show how to start inline text editing when the user double-clicks a text inside a JointJS element or a link. First we handle the cell:pointerdblclick event triggered by the paper. Inside our handler, we call the joint.ui.TextEditor.edit() method which starts the text editor on the SVG text under the target of the event. The only parameters we have to pass to this method is the SVG text DOM element, the cell view and the property path that will the text editor use to store the resulting text. Moreover, we also show how easy it is to apply auto-sizing on the text inside our elements so that they stretch based on the bounding box inside it.

// RECOMMENDED

paper.on('element:pointerdblclick', function(elementView, evt) {
    joint.ui.TextEditor.edit(evt.target, {
        cellView: elementView,
        textProperty: ['attrs', 'label', 'text'],
        annotationsProperty: ['attrs', 'label', 'annotations']
    });
});

paper.on('link:pointerdblclick', function(linkView, evt) {
    // Editing a link label
    var index = Number(linkView.findAttribute('label-idx', evt.target));
    joint.ui.TextEditor.edit(evt.target, {
        cellView: linkView,
        textProperty: ['labels', index, 'attrs', 'text', 'text'],
        annotationsProperty: ['labels', index, 'attrs', 'text', 'annotations']
    });
});

function autoSize(element) {

    var view = paper.findViewByModel(element);
    var textVel = view.vel.findOne('text');
    // Use bounding box without transformations so that our auto-sizing works
    // even on e.g. rotated element.
    var bbox = textVel.getBBox();
    // 16 = 2*8 which is the translation defined via ref-x ref-y for our rb element.
    element.resize(bbox.width + 16, bbox.height + 16);
}

graph.on('change:attrs', function(cell) { autoSize(cell) });

Note that there are two methods for starting up a text editor. The one we showed above (via the joint.ui.TextEditor.edit() function) is the easiest and recommended. The joint.ui.TextEditor.edit() function is a helper method that does a lot of things for us. First, it finds the nearest SVG <text> DOM element. Then it creates an instance of the joint.ui.TextEditor type and binds handlers for the 'text:change' event where it stores the new text content to your JointJS cells (elements or links). It also deals with annotations in case of rich text editing and more.

However, for completeness, we show another way to start text editing and that is by manually creating the instance of the joint.ui.TextEditor (not recommended if you don't know what you're doing):

// NOT RECOMMENDED

var ed;
paper.on('cell:pointerdblclick', function(cellView, evt) {
    var text = joint.ui.TextEditor.getTextElement(evt.target);
    if (text) {
        if (ed) ed.remove();   // Remove old editor if there was one.
        ed = new joint.ui.TextEditor({ text: text });
        ed.render(paper.el);

        ed.on('text:change', function(newText) {
            // Set the new text to the property that you use to change text in your views.
            cellView.model.attr('text/text', newText);
        });
    }
});

Note that using the first method, you can still access the actual instance of the text editor via joint.ui.TextEditor.ed property.

See the source code of the demo below for a complete example on using the inline text editor, including inline text editing of link labels.

Source Code

Configuration

The following table lists options that you can pass to the ui.TextEditor.edit(el, opt) method (this is the recommended way to start you text editor):

el SVG <text> element or any of its descendants. Usually, you pass the event target object when the user doubleclicks your JointJS element/link (see examples above).
opt.cellView The view for your cell. Usually, you either have a direct access to this view (in the paper triggered 'cell:pointerclick' or 'cell:pointerdblclick' events) but you can always find a view for any JointJS element or link by using paper.findViewByModel(cell).
opt.textProperty The path to the text property that causes the element/link to re-render the text inside it. (e.g. 'attrs/text/text').
opt.annotationsProperty The path to the property that holds the text annotations. (e.g. 'attrs/text/annotations'). (Rich-text specific.)
opt.placeholder Set to true to show a placeholder when the text is empty. The placeholder text and visual look can be styled in CSS as follows:
.text-editor .caret.placeholder:before {        // The caret.
    background-color: red;
}
.text-editor .caret.placeholder:after {         // The placeholder text.
    content: 'Enter text...';
    font-style: italic;
    color: blue;
}
opt.annotations Annotations that will be set on the ui.TextEditor instance. (Rich-text specific.)
opt.annotateUrls Set to true to enable the text editor to automatically detect URL hyperlinks and annotate them using the default urlAnnotation.
opt.urlAnnotation The default annotation for detected URL hyperlinks:
{
    attrs: {
        'class': 'url-annotation',
        fill: 'lightblue',
        'text-decoration': 'underline'
    }
}
It could be defined as an object or a function that returns an object.
function(url) {
    return {
        attrs: {
            'data-url': url,
            'fill': 'blue',
            'text-decoration': 'underline'
        }
    }
}
opt.useNativeSelection ui.TextEditor uses native selections for selecting text. This can be disabled by setting useNativeSelection to false in which case ui.TextEditor renders its own selections. This is an advanced feature that is useful only in very rare occasions. For example, if you, for any reason, need to select text in two different elements at the same time, you should set the useNativeSelection to false. This is because if native selections are used, two or more text elements cannot have focus at the same time.
opt.textareaAttributes Set attributes on the underlying (invisible) textarea that is used internally by the text editor to handle keyboard text input. This is an advanced option and is useful in very rare situations. The default value is:
textareaAttributes: {
    autocorrect: 'off',
    autocomplete: 'off',
    autocapitalize: 'off',
    spellcheck: 'false',
    tabindex: '0'
}
Some jQuery UI users reported an issue with autocomplete attribute being set on the textarea, resulting in the following error thrown in the jQuery UI: Uncaught Error: cannot call methods on autocomplete prior to initialization; attempted to call method 'off'. In this case, you might want to set your own textareaAttributes to overcome this issue (in this specific case, omitting the autocomplete attribute).
opt.onKeydown The callback is triggered when a key is pressed. It is possible to stop the event from propagating to the text editor by calling evt.stopPropagation().
onKeydown: (evt: KeyboardEvent) => {
    if (evt.code === 'Enter') {
        evt.stopPropagation();
        TextEditor.close();
    }
}
opt.onOutsidePointerdown The callback is triggered when the user clicks/touches outside the text editor.
onOutsidePointerdown: (evt: PointerEvent) => {
    TextEditor.close();
}

The following table lists options that you can pass to the ui.TextEditor constructor function (only for advanced use):

text SVG <text> element. It is recommended to use the joint.ui.TextEditor.getTextElement(el) method to find the container <text> SVG element wrapping all the lines. You can use this method directly on the evt.target element in your event handlers and just check if the returned value is truthy.
placeholder Set to true to show a default placeholder (Enter text...) when the text is empty. Set it to a string to show a custom placeholder. The placeholder visual look can be styled in CSS as follows:
.text-editor .caret.placeholder:before { // The caret.
    background-color: red;
}
.text-editor .caret.placeholder:after { // The placeholder text.
    font-style: italic;
    color: blue;
}

API

The following table lists methods that can be used on the joint.ui.TextEditor instance. If you, however, use the recommended way of creating the text editor via the joint.ui.TextEditor.edit() function, you usually do not want to deal with the actual instance (even though you can as you can always access the actual instance via joint.ui.TextEditor.ed property). To make it easy to work with the instance as with a black-box, joint.ui.TextEditor constructor exposes many of the methods directly (internally, it just proxies the methods to the actual instance.) The table below differentiates the instance methods from the class methods by using the full namespace access for the class methods (such as joint.ui.TextEditor.getAnnotations() = class method - vs getAnnotations() = instance method).

render([root]) Render the Text Editor. Call this method right after you have constructed the text editor instance and you're ready to display the inline text editor. If root (HTML DOM element) is specified, the text editor HTML container will be appended to that element instead of to the document.body. This is useful if you want the text editor to scroll with some other container.
remove() Remove the Text Editor. Call this when you're done with inline text editing.
selectAll() Programmatically select all the text inside the text editor.
select(selectionStart, selectionEnd) Programmatically select portion of the text inside the text editor starting at selectionStart ending at selectionEnd. This method automatically swaps selectionStart and selectionEnd if they are in a wrong order.
deselect() Programmatically deselect all the selected text inside the text editor.
getSelectionStart() Return the start character position of the current selection.
getSelectionEnd() Return the end character position of the current selection.
getSelectionRange() Return an object of the form { start: Number, end: Number } containing the start and end position of the current selection. Note that the start and end positions are returned normalized. This means that the start index will always be lower than the end index even though the user started selecting the text from the end back to the start.
getSelectionLength() Return the number of characters in the current selection.
getSelection() Return the selected text.
startSelecting() Starts the text selection (i.e. updates the selection range based on the mouse coordinates until the user releases the mouse).
findAnnotationsUnderCursor(annotations, selectionStart) Find all the annotations in the `annotations` array that the cursor at `selectionStart` position falls into.
findAnnotationsInSelection(annotations, selectionStart, selectionEnd) Find all the annotations that fall into the selection range specified by `selectionStart` and `selectionEnd`. This method assumes the selection range is normalized.
setCaret(charNum [, opt]) Programmatically set the caret position. If opt.silent is true, the text editor will not trigger the 'caret:change' event.
hideCaret() Programmatically hide the caret.
updateCaret() Manually update the visual attributes (e.g. size, color) of the caret. It's useful when the containing SVG was transformed.
getTextContent() Return the text content (including new line characters) inside the text editor.
getWordBoundary(charNum) Return the start and end character positions for a word under charNum character position.
getURLBoundary(charNum) Return the start and end character positions for a URL under charNum character position. Return undefined if there was no URL recognized at the charNum index.
getNumberOfChars() Return the number of characters in the text.
getCharNumFromEvent(evt) Return the character position the user clicked on. If there is no such a position found, return the last one.
setCurrentAnnotation(attrs) This method stores annotation attributes that will be used for the very next insert operation. This is useful, for example, when we have a toolbar and the user changes text to e.g. bold. At this point, we can just call setCurrentAnnotation({ 'font-weight': 'bold' }) and let the text editor know that once the user starts typing, the text should be bold. Note that the current annotation will be removed right after the first text operation came. This is because after that, the next inserted character will already inherit properties from the previous character which is our 'bold' text. (Rich-text specific.)
setAnnotations(annotations) Set annotations of the text inside the text editor. These annotations will be modified during the course of using the text editor. (Rich-text specific.)
getAnnotations() Return the annotations of the text inside the text editor. (Rich-text specific.)
getCombinedAnnotationAttrsAtIndex(index, annotations) Get the combined (merged) attributes for a character at the position index taking into account all the annotations that apply. (Rich-text specific.)
getSelectionAttrs(range, annotations) Find a common annotation among all the annotations that fall into the range (an object with start and end properties - normalized). For characters that don't fall into any of the annotations, assume defaultAnnotation (default annotation does not need start and end properties). The common annotation denotes the attributes that all the characters in the range share. If any of the attributes for any character inside range differ, undefined is returned. This is useful e.g. when your toolbar needs to reflect the text attributes of a selection. (Rich-text specific.)
joint.ui.TextEditor.getTextElement(el) Return the containing SVG <text> element closest to the SVG element el. This is especially handy in event handlers where you can just pass evt.target element to this function and it will return the element that you can directly use as a parameter to the ui.TextElement constructor function. If no such element is exists, the function returns undefined.
joint.ui.TextEditor.edit(el, opt) See the Configuration section for details.
joint.ui.TextEditor.close() Close the currently opened text editor (if there was one). Note that before actually removing the editor (and if the annotateUrls option is set to true), the close() method tries to find if there was a URL hyperlink at the current cursor position and annotates it. The reason is that normally, URL hyperlinks are detected after the user types a non-visible character such as SPACE or new line. In this case though, the user closed the editor by e.g. clicking outside the element/link and so if there was a URL hyperlink at the end of the text content, it would not have been annotated.
joint.ui.TextEditor.applyAnnotation(annotations) Apply annotations to the current selection. This is useful if the user has something selected in the text editor and then changes e.g. the font size via a toolbar. In this case, you construct the annotations array from the toolbar changes and apply it using this function on the current selection.
joint.ui.TextEditor.setCurrentAnnotation(attributes) See the instance setCurrentAnnotation() method above.
joint.ui.TextEditor.getAnnotations() See the instance getAnnotations() method above.
joint.ui.TextEditor.setCaret(charNum, opt) See the instance setCaret() method above.
joint.ui.TextEditor.deselect() See the instance deselect() method above.
joint.ui.TextEditor.selectAll() See the instance selectAll() method above.
joint.ui.TextEditor.select(start, end) See the instance select() method above.
joint.ui.TextEditor.getNumberOfChars() See the instance getNumberOfChars() method above.
joint.ui.TextEditor.getCharNumFromEvent(evt) See the instance getCharNumFromEvent() method above.
joint.ui.TextEditor.getWordBoundary() See the instance getWordBoundary() method above.
joint.ui.TextEditor.findAnnotationsUnderCursor() See the instance findAnnotationsUnderCursor() method above except the annotations and index arguments are automatically added so this method does not accept any arguments.
joint.ui.TextEditor.findAnnotationsInSelection() See the instance findAnnotationsInSelection() method above except the annotations and start and end index of the selection are automatically added so this method does not accept any arguments.
joint.ui.TextEditor.getSelectionAttrs(annotations) See the instance getSelectionAttrs() method above except the range is automatically added.
joint.ui.TextEditor.getSelectionLength() See the instance getSelectionLength() method above.
joint.ui.TextEditor.getSelectionRange() See the instance getSelectionRange() method above.
joint.ui.TextEditor.isLineStart(text, index) Return true if the character at the position index is the first character of some line.
joint.ui.TextEditor.isLineEnding(text, index) Return true if the character at the position index is a newline character but does not denote an empty line. In other words, the newline character under index terminates a non-empty line.
joint.ui.TextEditor.isEmptyLine(text, index) Return true if the character at the position index is a newline character that denotes an empty line.
joint.ui.TextEditor.normalizeAnnotations(annotations) Returns an array of annotations that contains no duplicates and is sorted by the start index of each annotation. The method generates annotations containing a maximum number of attributes rather than aiming for the maximum range.
joint.ui.TextEditor.getCombinedAnnotationAttrsAtIndex(annotations, index) Get the combined (merged) attributes for a character at the position index taking into account all the annotations that apply.
joint.ui.TextEditor.getCombinedAnnotationAttrsBetweenIndexes(annotations, start, end) Find a common annotation among all the annotations that fall into the range between start and end. Common annotation refers to attributes that all features in the range have in common. If any of the attributes of any character within the range differ, it is omitted from the result.

Events

The following table lists events that are triggered on the ui.TextEditor instance but also on the joint.ui.TextEditor class (which is useful if you use the recommended way of creating a text editor via the joint.ui.TextEditor.edit() function):

text:change Triggered when the text inside the text editor changes. This is the most important event and the one you want to always react on. The callback has the following signature: function(newText) and is therefore called with the new text as a parameter. You are assumed to set the new text to the property on your JointJS element/link that is used for storing the associated text.
select:change Triggered when the text inside the text editor is being selected (or if the user double-clicked to select a word or triple-clicked to select the entire text). The callback has the following signature: function(selectionStart, selectionEnd). For example, you can see the selected text like this:
ed.on('select:change', function(selectionStart, selectionEnd) {
    var text = ed.getTextContent().substring(selectionStart, selectionEnd);
});
select:changed Triggered when the user is finished selecting text inside the text editor. The callback has the following signature: function(selectionStart, selectionEnd). For example, you can see the selected text like this:
ed.on('select:changed', function(selectionStart, selectionEnd) {
    var text = ed.getTextContent().substring(selectionStart, selectionEnd);
});
caret:change Triggered when the caret has changed position. The position is passed to the callback as the only argument.
caret:out-of-range Triggered when the caret is outside the visible text area. This is a very special event that you don't usually deal with. The only situation this event can occur is when ui.TextEditor is used on a text rendered along a path (see Vectorizer#text(str, { textPath: '' }))). In this case, if the user moves his cursor outside the visible text area, caret:out-of-range event is triggered so that the programmer has chance to react (if he wants to because these situations are handled seamlessly in ui.TextEditor by hiding the caret).
open Triggered when a new text editor instance is created.
TextEditor.on('open', function(textNode, cellView) {
    // Editing of an SVGTextElement `textNode` (from the `cellView`) started.
});
close Triggered when the text editor instance is destroyed.
TextEditor.on('close', function(textNode, cellView) {
    // Editing of an SVGTextElement `textNode` (from the `cellView`) finished.
});