Skip to main content

Copy/paste

Version 5.4.1 of the Toolkit introduced two new packages - @jsplumbtoolkit/copy-paste and @jsplumbtoolkit/browser-ui-copy-paste. These packages offer an easy to use API for copying parts of your UI and pasting them elsewhere.

Installation

You'll need to install the browser-ui-copy-paste package, which has a dependency on the core copy-paste package:

{
...,
"@jsplumbtoolkit/browser-ui-copy-paste":"5.4.1",
...
}

Usage

The basic configuration is to import a reference to BrowserUIClipboard, and then instantiate it with a Surface object:

import { BrowserUIClipboard } from "@jsplumbtoolkit/browser-ui-copy-paste"

const clipboard = new BrowserUIClipboard(someSurface)

We don't show here how someSurface came into existence - it depends on the details of your app, and where in your app you are instantiating the clipboard.

Once you have a clipboard, you can copy things in to it. The signature of the copy method is:


copy(obj: Base | Array<Base> | Path | Selection):void

So, you can pass a single object in, or an array of objects, or a Path, or a selection. Let's grab the node with id "1" and copy it:

const node1 = toolkitInstance.getNode("1")
clipboard.copy(node1)

Now we can paste it. When you paste, it is actually optional where you paste to, but if you do not provide an origin as we do here then the pasted node(s) will appear on top of the copied node(s).

clipboard.paste({origin:500, 500})

Paste origin

A set of vertices has an implicit origin: the left position of the leftmost vertex, and the top position of the topmost vertex. When you call paste and provide an origin, the location of each vertex to be pasted is translated by the delta between the paste origin and the computed origin of the clipboard.

Event as origin

The example above shows the usage of an object in the form {x:.., y:..} to define the origin (as an aside, this is modelled by the PointXY interface in jsPlumb). It is also possible, though, to use a MouseEvent as the origin, and in many applications employing copy/paste this is simpler (in the demonstrations on this page you'll see we use a MouseEvent as the origin rather than specifying an x,y location).

At any point, the canvas for a given Surface has been panned in one or both axes, and is likely zoomed in or out to some value other than 1:1. When you provide a MouseEvent as the origin for a paste, this location of this event is automatically mapped to a location onto the Surface.

Example - copy/paste a selection

Here, we listen for clicks on any type of vertex, and we toggle each vertex in the Toolkit's selection when we detect a tap. When the user clicks in whitespace on the canvas, the contents of the clipboard are pasted to the location at which the mouse was clicked, and then the selection is cleared and the viewport zoomed to show all the content. We use the method pasteCurrentSelection(..) to copy and paste the Toolkit's current selection in one go. Further down in the page you'll find examples of working with nodes and edges directly.

import { BrowserUIClipboard } from "@jsplumbtoolkit/browser-ui-copy-paste"
import { newInstance } from "@jsplumbtoolkit/browser-ui-vanilla"

let clipboard
const toolkit = newInstance()
const surface = toolkit.render(someDOMElement, {
view:{
nodes:{
default:{
events:{
tap:(p) => {
toolkit.toggleSelection(p.vertex)
}
}
}
}
},
events:{
canvasClick:(surface, event) => {
clipboard.pasteCurrentSelection({origin:event})
toolkit.clearSelection()
surface.zoomToFit() // for the demo, so you can see what you pasted. not a requirement, of course.
}
}
})
clipboard = new BrowserUIClipboard(surface)

Copying edges

Edges can also be copied and pasted - you can pass one in to a copy call:


// ... import and create a clipboard

const source = toolkit.addNode({id:"source"})
const target = toolkit.addNode({id:"target"})
const edge = toolkit.addEdge({source, target})

clipboard.copy([source, target, edge]) // copy both nodes and the edge

When you paste the contents of this clipboard, a copy of source will be created, a copy of target will be created, and a copy of edge - whose source and target are the new vertices - will also be created.

Vertices not in clipboard

Imagine instead of copying everything from the previous example to the clipboard we copied only the source vertex and the edge:


// ... import and create a clipboard

const source = toolkit.addNode({id:"source"})
const target = toolkit.addNode({id:"target"})
const edge = toolkit.addEdge({source, target})

clipboard.copy([source, edge]) // copy source node only and the edge

What happens now if we paste this?

clipboard.paste({origin:someMouseEvent})

By default, the clipboard will create a copy of source, and a copy of edge. The cloned source will be the source for the cloned edge, but the target for the cloned edge will be the original target vertex.

This behaviour can be modified via the use of the hermetic flag.

clipboard.paste({event:someMouseEvent, hermetic:true})

In this call, hermetic:true instructs the clipboard to only paste edges for which both the source and target vertices were also in the clipboard. So in this case, only a clone of the source vertex will be pasted. hermetic defaults to false.

Edge geometry

An edge that has "geometry" attached to it is an edge that has either been loaded with a geometry section in the JSON, or has been edited by a connector editor, using the mouse, or fingers. When you copy an edge with geometry and subsequently paste it, the rules for the associated geometry are:

  • if you copy and paste the edge and both its source and target vertices, the pasted edge has geometry attached, the value for which is the original edge's geometry translated according to the transformation of the origin as discussed above.

  • if you copy and paste an edge but not both its source and target vertices, the pasted edge does not have geometry attached, and will be painted according to the Toolkit's default algorithm for the specific connector.

Nested groups and nodes

If you copy and paste a group that has child nodes or groups, the child nodes or groups will also be copied, and pasted as children of the pasted group. This mechanism works to an arbitrary level of nesting. If you wish to copy a group without any of its child content, you can specify a "shallow" paste. Compare the two paste method calls in this snippet:


// ... import and create a clipboard

const group = toolkit.adGroup({id:"group"})
const node = toolkit.addNode({id:"node", group:"group"})

clipboard.copy(group) // copy the group

clipboard.paste({origin:{x:500, y:500}}) // <-- paste a copy of the group, with a copy of the node as a child of the pasted group

clipboard.paste({origin:{x:500, y:500}, shallow:true}) // <-- paste a copy of the group, but without the child node.

Example - copy/paste nodes and edges

In this example we load up some data and after load, we copy all the nodes and edges to the clipboard. When you click on the canvas, the contents of the clipboard are pasted. This is of course a bit of a made up scenario; the purpose is to demonstrate that you can copy individual nodes and edges in to the clipboard.

The code for this looks like this:


let clipboard
const toolkit = newInstance()
const surface = toolkit.render(someElement, {
"layout":{
"type":"Absolute"
},
"events":{
canvasClick:(surface, event) => {
clipboard.paste({event})
surface.zoomToFit()
}
},
"defaults":{
anchor:"Continuous"
}
})

clipboard = new BrowserUIClipboard(surface)

toolkit.load({
data:{
"nodes":[
{ id:"1", left:50, top:50 },
{ id:"2", left:150, top:150 },
{ id:"3", left:250, top:30 }
],
"edges":[
{ source:"1", target:"2", data:{ id:"edge1" } },
{ source:"1", target:"3", data:{ id:"edge2" } }
]
}
}, onload:() => {
clipboard.copy([
toolkit.getNode("1"),
toolkit.getNode("2"),
toolkit.getNode("3"),
toolkit.getEdge("edge1"),
toolkit.getEdge("edge2")
])
})

Clearing the clipboard

The clipboard offers a clear method that will remove all copied content:

clipboard.clear()

You can also instruct the clipboard to clear the content that was just pasted:

clipboard.paste({origin:{x:50, y:50}, clear:true})

This first release of the clipboard supports an unlimited number of entries in its stack, although there are no methods for working with any entry other than the head, and there is no means of limiting the size of the stack. We invite feedback from licensees and evaluators on future updates to the clipboard packages.

Example - shallow vs deep copy

In this example we three nested groups, the innermost of which has a node inside of it. After loading the data, the root group - Group 1 - is copied to the clipboard. You can then click in the canvas to paste a copy of the group. If you left-click, the paste is a shallow paste, and only a copy of Group 1 is pasted. If you right-click, the paste is a deep paste, and a copy of all of the groups plus the node is made.

The code looks like this:


let clipboard
const toolkit = newInstance()
const surface = toolkit.render(someElement, {
"layout":{
"type":"Absolute"
},
"view":{
"nodes":{
"default":{
"template":'<div><h5>Node ${id.substring(0, 5)}</h5></div>'
}
},
"groups":{
"default":{
"template":'<div><h5>Group ${id.substring(0, 5)}</h5></div>',
"autoSize":true
}
}
},
"events":{
canvasClick:(surface, event) => {
clipboard.paste({event, shallow:true})
surface.zoomToFit()
},
contextmenu:(surface, event) => {
clipboard.paste({event})
surface.zoomToFit()
}
},
"defaults":{
anchor:"Continuous"
}
})

clipboard = new BrowserUIClipboard(surface)

toolkit.load({
data:{
"groups":[
{id:"g1", left:50, top:50 },
{id:"g2", left:50, top:50, group:"g1" },
{id:"g3", left:50, top:50, group:"g2" }
],
"nodes":[
{ id:"1", left:50, top:50, group:"g3" }
]
}
}, onload:() => {
clipboard.copy([
toolkit.getGroup("g1")
])
})