JointJS+ PathEditor

ui.PathEditor

Path editor allows users to edit <path> elements via an editing overlay. Users can interact with the path's segments, anchor points and curve control points.

Installation

Include joint.ui.pathEditor.css and joint.ui.pathEditor.js files to your HTML.

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

Create a PathEditor

First, we create a PathEditor object. The constructor needs a reference to a <path> SVGPathElement on the page.

new joint.dia.PathEditor({
    pathElement: document.querySelector('path')
});

Configuration

The joint.ui.PathEditor constructor accepts the following options:

pathElement SVGPathElement Required. The path element to be edited. The PathEditor will be rendered on top of it.
anchorPointMarkup string Optional. The SVG markup of anchor point overlay elements. Default is '<circle r="2.5"/>'. (CSS styling should use the .joint-path-editor .anchor-point CSS selector.)
controlPointMarkup string Optional. The SVG markup of control point overlay elements. Default is '<circle r="2.5"/>'. (CSS styling should use the .joint-path-editor .control-point CSS selector.)

API

The joint.ui.PathEditor object provides the following methods:

render() Render the path editor overlay. Called automatically after object is instantiated.
remove() Remove the path editor.
adjustAnchorPoint(index, dx, dy, evt [, opt]) Adjust the position of an anchor point. Identified by index, the index of the anchor point within path segment list (starting from 0 for the endpoint of the first moveto segment). The anchor point is translated by (dx,dy). The evt is the original user interaction event.

The opt parameter is optional; it is used by internal methods. It can have one property: dry (indicating that corresponding 'path:editing' and 'path:edit' events will be suppressed).
adjustControlPoint(index, controlPointIndex, dx, dy, evt [, opt]) Adjust the position of a control point. Identified by index, the index of the path segment it is attached to within path segment list (starting from 1 for the first non-moveto segment), and controlPointIndex, the index of the control point within the segment (1 for the first control point - the one visually attached to the previous anchor point, or 2 for the second control point - the one visually attached to this segment's anchor point). The control point is translated by (dx,dy). The evt is the original user interaction event.

The opt parameter is optional; it is used by internal methods. It can have one property: dry (indicating that corresponding 'path:editing'/'path:edit' events will be suppressed).
adjustSegment(index, dx, dy, evt) Adjust the position of a segment. Identified by its index within path segment list (starting from 1 for the first non-moveto segment). The evt is the original user interaction event.
getControlPointLockedStates() Return an array of arrays that records, for every control point (identified as [index][controlPointIndex]), whether it is currently locked with another point (true) or not (false). In tandem with setControlPointLockedStates(lockedStates), the function can be used to preserve locking information across multiple path editor sessions for a given element.
setControlPointLockedStates(lockedStates) Change the locked status of the editor's control points, so they correspond to the information in the given lockedStates list (e.g. the output of the getControlPointLockedStates() function). A true value adds the 'locked' class to the control point element; a false value removes the class.

Interaction API

The following methods are meant to be called in response to user interaction events (e.g. click, touch, dblclick). Some of them are bound with user interactions by default (see the Interaction section), but others are not. Those may be called upon by delegated events from your application. For example:

pathEditor.delegate('contextmenu', '.segment-path', function(event) {
    event.stopPropagation();
    event.preventDefault();

    this.convertSegmentPath(event);
}.bind(pathEditor));
startMoving(event) Bound with 'mousedown'/'touchstart' events by default.

The selected anchor point / control point / segment path starts moving with user pointer. Trigger a corresponding 'path:interact' event.

To start listening for the move() and stopMoving() methods, call delegateDocumentEvents().
move(event) Bound with 'mousemove'/'touchmove' events by default (after delegateDocumentEvents() is called).

The selected anchor point / control point / segment path moves with user pointer. Trigger corresponding 'path:editing' events.
stopMoving(event) Bound with 'mouseup'/'touchend'/'touchcancel' events by default (after delegateDocumentEvents() is called).

The selected anchor point / control point / segment path stops moving with user pointer. Trigger a corresponding 'path:edit' event.

To stop listening for the move() and stopMoving() methods, call undelegateDocumentEvents().
createAnchorPoint(event) Bound with 'dblclick .segment-path' event by default.

If event is targeted on a segment path, create an anchor point at the location of the user click and insert it into the path segment list. Trigger the 'path:anchor-point:create' event.

Note: Due to current limitations, the function does not work properly for closepath segments in paths that have 2 or more closepath segments (the anchor points are created at wrong position).
removeAnchorPoint(event) Bound with 'dblclick .anchor-point' event by default.

If event is targeted on an anchor point, remove it. Trigger the 'path:anchor-point:remove' event. If only one anchor point remains in the path after the removal, also trigger the 'path:invalid' event.
lockControlPoint(event) Bound with 'dblclick .control-point' event by default.

If event is targeted on a control point of a curveto segment, and the control point is immediately adjacent to a control point of another curveto segment, toggle its locked status. Trigger the corresponding 'path:control-point:lock'/'path:control-point:unlock' event.

Note that the locking action necessarily causes a change in the path markup which does not trigger an 'path:edit' event!
addClosePathSegment(event) Not bound with any user interaction by default.

If event is targeted on the first or last anchor point, and the path segment list contains no closepath segment, append a closepath segment to the path segment list.
removeClosePathSegment(event) Not bound with any user interaction by default.

If event is targeted on a closepath segment, remove the segment from the path segment list.
convertSegmentPath(event) Not bound with any user interaction by default.

If event is targeted on a path segment, convert the segment from linear to curved or vice versa. A curveto segment is replaced by a lineto segment. A lineto/closepath segment is replaced by a curveto segment whose control points are coincident with anchor points; a replacement closepath segment is appended to the path segment list if the converted segment was a closepath segment.

Note: Due to current limitations, the function does not work properly for closepath segments in paths that have 2 or more closepath segments (the anchor points are created at wrong position).

Interaction

Users can interact with joint.ui.PathEditor objects by clicking/touching editor overlay elements:

mousedown touchstart
.anchor-point Call onAnchorPointPointerDown(event), which calls startMoving() followed by delegateDocumentEvents() by default.
.control-point Calls onControlPointPointerDown(event), which calls startMoving() followed by delegateDocumentEvents() by default.
.segment-path Calls onSegmentPathPointerDown(event), which calls startMoving() followed by delegateDocumentEvents() by default.

Double-clicking editor overlay elements triggers additional actions:

dblclick
.anchor-point Calls onAnchorPointDoubleClick(event), which calls removeAnchorPoint() by default.
.control-point Calls onControlPointDoubleClick(event), which calls lockControlPoint() by default.
.segment-path Calls onSegmentPathDoubleClick(event), which calls createAnchorPoint() by default.

For the documentation of the called functions, see the Interaction API section.

Events

The joint.ui.PathEditor object triggers various events that you can react on:

clear Triggered when the pathEditor is cleared.
path:interact Triggered when the user interacts with (clicks/touches) the path.

The following specific events are triggered alongside this generic event:
path:anchor-point:select Triggered when the user interacts with an anchor point.

The handler is passed the index of the segment that this anchor point is bound to in the path segment list. The first anchor point on the path has an index of 0.
path:control-point:select Triggered when the user interacts with a control point.

The handler is passed two values. The first value is the index of the segment that this control point is bound to in the path segment list. (Since the first moveto segment cannot have control points, the minimal index is 1.) The second value is the controlPointIndex of the control point. It can be either 1 or 2.
path:segment:select Triggered when the user interacts with a path segment.

The handler is passed the index of the segment in the path segment list. (Since the first moveto segment is not clickable, the minimal index is 1.)
path:control-point:lock Triggered when the user successfully locks a control segment (binds it with a control point in an adjacent curveto path).

The handler is passed two values. The first value is the index of the segment that this control point is bound to in the path segment list. (Since the first moveto segment cannot have control points, the minimal index is 1.) The second value is the controlPointIndex of the control point. It can be either 1 or 2.
path:control-point:unlock Triggered when the user unlocks a control segment (stops its binding with a control point in an adjacent curveto path).

The handler is passed two values. The first value is the index of the segment that this control point is bound to in the path segment list. (Since the first moveto segment cannot have control points, the minimal index is 1.) The second value is the controlPointIndex of the control point. It can be either 1 or 2.
path:editing Triggered while the user is editing the path.

The handler is passed a reference to the updated svgPath SVGPathElement, and evt (the user interaction event).

The following specific events are triggered alongside this generic event:
path:anchor-point:adjusting Triggered while the user is adjusting the position of an anchor point.

The handler is passed a reference to the updated svgPath SVGPathElement, evt (the user interaction event), and an opt object with two properties: index (the index of the adjusted anchor point), and segPoint (the new position of the anchor point).
path:control-point:adjusting Triggered while the user is adjusting the position of a control point.

The handler is passed a reference to the updated svgPath SVGPathElement, evt (the user interaction event), and an opt object with three properties: index (the index of the associated anchor point), controlPointIndex (the index of the adjusted control point), and segPoint (the new position of the anchor point).
path:segment:adjusting Triggered while the user is adjusting the position of a segment.

The handler is passed a reference to the updated svgPath SVGPathElement, evt (the user interaction event), and an opt object with one property: index (the index of the adjusted segment).
path:edit Triggered when the user edits the path.

The handler is passed a reference to the updated svgPath SVGPathElement, and evt (the user interaction event).

The following specific events are triggered alongside this generic event:
path:anchor-point:adjust Triggered when the user adjusts the position of an anchor point.

The handler is passed a reference to the updated svgPath SVGPathElement, evt (the user interaction event), and an opt object with two properties: index (the index of the adjusted anchor point), and segPoint (the new position of the anchor point).
path:control-point:adjust Triggered when the user adjusts the position of a control point.

The handler is passed a reference to the updated svgPath SVGPathElement, evt (the user interaction event), and an opt object with three properties: index (the index of the associated anchor point), controlPointIndex (the index of the adjusted control point), and segPoint (the new position of the anchor point).
path:segment:adjust Triggered when the user adjusts the position of a segment.

The handler is passed a reference to the updated svgPath SVGPathElement, evt (the user interaction event), and an opt object with one property: index (the index of the adjusted segment).
path:anchor-point:create Triggered when the user adds a new anchor point to the path.

The handler is passed a reference to the updated svgPath SVGPathElement, and evt (the user interaction event).
path:anchor-point:remove Triggered when the user removes an anchor point from the path.

The handler is passed a reference to the updated svgPath SVGPathElement, and evt (the user interaction event).
path:segment:convert Triggered when the user converts a lineto path segment into a curveto segment or vice versa.

The handler is passed a reference to the updated svgPath SVGPathElement, and evt (the user interaction event).
path:closepath-segment:add Triggered when the user adds a closepath segment to the path.

The handler is passed a reference to the updated svgPath SVGPathElement, and evt (the user interaction event).
path:closepath-segment:add Triggered when the user removes a closepath segment from the path.

The handler is passed a reference to the updated svgPath SVGPathElement, and evt (the user interaction event).
path:invalid Triggered when user modifications result in an invalid path (e.g. after removing, a path that has only one anchor point).

The handler is passed a reference to the svgPath SVGPathElement, and evt (the user interaction event).

You can react to these events by using the on() method:

pathEditor.on({
    'path:control-point:select': console.log('started moving control point'),
    'path:control-point:adjusting': console.log('moving control point'),
    'path:control-point:adjust': console.log('stopped moving control point')
});

It is also possible to specify a custom context for the on() method. This is useful when you need to handle user interaction on your PathEditor from within another Backbone object, for instance:

pathEditor.on({
    'path:edit': this.onPathEdit.bind(this),
}, this);