A zero dependency, unopinionated node editor built as a reusable web component.
This library is distributed as an ESM module and released on npm.
npm install node-editor
While the module itself exports all of the public types if you need them, it is not strictly necessary to use the library, and the code is otherwise self-contained/-executing.
Assuming you're using a bundler or a <script type="module">
tag,
you simply need to import
it.
import 'node-editor';
From there, you can create HTML tags via document.createElement()
or with plain HTML directly in an .html
file; the library will
automatically upgrade any existing elements found on the page.
A note on stability:
node-editor
is currently in beta stage, and thus the public API/tags might change slightly. Expect bugs (seriously, the logic is quite hairy). Use at your own risk, and please report any issues you face!
node-editor
provides absolutely no "functionality", processing
engine, or built-in nodes. It merely gives you the display components
for building your own nodes and receiving information about
how the user has connected them together.
How the node system reacts to inputs is entirely up to the application
developer; node-editor
makes no assumptions there - it is purely
the display component.
A very basic overview of the components that are exposed:
<node-map>
<node-editor name="unique-name" width="200">
<node-title>
This part is draggable, and moves the entire editor. Use it for node
titles, title bars, etc.
</node-title>
<node-port name="unique-name" color="red">
A port is created here. Any content is automatically resized and the
port handle is automatically placed in the vertical-middle. By
default, ports are inputs...
</node-port>
<node-port out name="unique-name2" color="blue">
... but can be specified as outputs by adding the `out` attribute.
</node-port>
</node-editor>
<!-- in/out ports must be children of their respective editors -->
<node-link
from="src-editor-name"
to="dest-editor-name"
out="src-port-name"
in="dest-port-name"
>
<!--
You MUST have a closing tag, but nothing
here will actually display. For now, please
treat it as a "reserved" space and keep it
empty, as there might be a feature added
that uses this space later on.
-->
</node-link>
</node-map>
<node-map>
(NodeEditorElement
-> HTMLElement
)The NodeMapElement
is the root element for a node editor. All other
node-editor
components must lay within a <node-map>
element; at best,
they will simply do nothing if they are not.
Note that coordinates in almost all cases refer to "world coordinates", or the coorindates that make up the infinite scrollable plane of the editor map itself. That is to say, zooming and panning the editor does not change the world coordinates of the elements within it.
NOTE: By design,
<node-map>
elements are nestable (as in, you should be able to create a<node-map>
within a<node-editor>
without issue). This is largely untested, but should work. Please open any issues you have if you end up relying on this feature.
The <node-map>
element creates an event boundary; all node-editor
-related
events will stop bubbling once they hit their respective <node-map>
element.
transform
(NodeEditorTransformEvent
) - an editor was moved or changed size
event.target
- the <node-editor>
(NodeEditorElement
) elementevent.x
/ event.y
- the new map position of the editor in world coordinatesevent.width
/ event.height
- the new size of the editor in world coordinatesevent.didResize
- true
if the editor resizedevent.didMove
- true
if the editor changed positionposition
(NodePortPositionEvent
) - a port within an editor changed position
event.target
- the <node-port>
(NodePortElement
) elementevent.x
/ event.y
- the new position of the port in world coordinatescolor
(NodePortColorEvent
) - a port within an editor changed its color
event.target
- the <node-port>
(NodePortElement
) elementevent.color
- the new color, as a CSS color string
online
(NodePortOnlineEvent
) - a port was created within an editor
event.target
- the <node-port>
(NodePortElement
) elementevent.port
- same as event.target
offline
(NodePortOfflineEvent
) - a port was deleted from an editor
event.target
- the <node-editor>
(NodeEditorElement
) element that held the portevent.port
- the <node-port>
(NodePortElement
) that was deletedadd
(NodeEditorAddEvent
) - an editor was added to the map
event.target
- the <node-editor>
(NodeEditorElement
) element that was addedevent.editor
- same as event.target
remove
(NodeEditorRemoveEvent
) - an editor was removed from the map
event.target
- the <node-map>
(NodeMapElement
) element from which the editor was removedevent.editor
- the removed <node-editor>
(NodeEditorElement
) elementlink
(NodeLinkEvent
) - a <node-link>
was added to the mapconnect
event)
event.target
- the <node-link>
(NodeLinkElement
) elementevent.link
- same as event.target
unlink
(NodeUnlinkEvent
) - a <node-link>
was removed from the map
event.target
- the <node-map>
(NodeMapElement
) element from which the link was removedevent.link
- the <node-link>
(NodeLinkElement
) that was removedunlink
event will immediately be followedlink
event. In such a case, the unlink
event's event.link
's attributesconnect
(NodeConnectEvent
) - a <node-link>
successfully formed a connection between two ports
event.target
- the <node-link>
(NodeLinkElement
) elementevent.link
- same as event.target
disconnect
(NodeDisconnectEvent
) - a <node-link>
lost its connection
event.target
- the <node-map>
(NodeMapElement
) element from which the link was removedevent.link
- the <node-link>
(NodeLinkElement
) that was removedevent.link
's attributes will always reflect the updated values, and do notname
(NodeNameEvent
) - a <node-editor>
or <node-port>
changed its name
attribute
event.target
- the <node-editor>
(NodeEditorElement
) or <node-port>
(NodePortElement
)event.name
- the new name, or null
if the name attribute was removedevent.oldName
- the old name, or null
if the name attribute was just addedzoom
(read-only) - the current zoom value (1
= 100%, lesser values = zoomed out)getEditor(name)
- get an editor by its name
attribute; returns null
if not found::part(background)
- targets the SVG background element. Use this selectorcursor: grab
.::part(link)
- target the SVG bezier curves that constitute link lines.cursor: finger
(to indicate that the link can be.dragging
- added during click+drag panning event. Use this class tocursor: grabbing
.<node-editor>
(NodeEditorElement
-> HTMLElement
)The <node-editor>
tag is the container for individual draggable/resizable nodes.
All <node-editor>
tags must be defined within a <node-map>
.
The following events might be dispatched from <node-editor>
elements.
See the documentation under <node-map>
for what they mean. Additional
notes or exceptions are added as sub-points.
transform
position
color
online
offline
add
remove
event.target
is the <node-element>
(NodeEditorElement
) that was removedevent.editor.nodeMap
is still valid during the callback, and refers to the<node-map>
(NodeMapElement
) from which the editor was removed.connect
disconnect
name
nodeMap
(read-only) - the <node-map>
(NodeMapElement
) that parents this element (or null
x
/y
- the top-left position of the editor in world coordinatesname
- the name
attribute, or null
if not setwidth
/height
- the editor's width/height in world coordinates; retrieved eitherwidth
/height
attributes respectively, or calculated based on the editors'sThe following <node-editor>
attributes mirror their respective properties; see the above
section for more information.
name
<node-map>
) to be functional.x
/ y
(optional)
0
width
/height
(optional)
width
attribute; height will stillgetPort(name)
- gets a <node-port>
(NodePortElement
) by its name
attribute, or null
if::part(frame)
- targets the root element of the editor. Use this selectorbackground
, border-radius
,box-shadow
, etc.)<node-port>
(NodePortElement
-> HTMLElement
)The <node-port>
is a container element that creates either an input or an output port
on a node editor. Ports can be connected to one another to indicate "links", typically
indicating some relationship or flowing of data between two points in the node system.
Since node-editor
is largely unopinionated, it is up to the application developer
what a port or a link actually represents - only the following is enforced by this
library:
The following are expressly not enforced (and thus would have to be implemented by the application if it so chooses):
color
attribute.Port "handles" (the visible element where link lines connect to) are by default
colored circles, but can be overridden and thus customized by adding elements
to the handle
slot (see Slots below).
The following events might be dispatched from <node-port>
elements.
See the documentation under <node-map>
for what they mean. Additional
notes or exceptions are added as sub-points.
position
color
online
offline
event.target
is the <node-port>
(NodePortElement
) that was removedevent.target.nodeEditor
is still valid during the callback, and refers to the<node-editor>
(NodeEditorElement
) from which the port was removed.connect
<node-link>
itself.disconnect
<node-link>
itself.name
NOTE: The way that the
connect
anddisconnect
events are dispatched may change prior to the v1 release.
numConnections
(read-only) - the current number of connected links to this portconnections
(read-only) - an array of connected links to this port (in no particularname
- the name
attribute, or null
if not setnodeEditor
(read-only) - the <node-editor>
(NodeEditorElement
) that parentsnull
if this port is an orphancolor
- the CSS color stringcolor
attribute if specified, or otherwiseisOutputPort
- Whether or not the port is an output port. Returns true if out
existstrue
to this propertyout
attribute; assigning false
removes it.handleX
/ handleY
(read-only) - The position of the port's handle in world coordinatesNOTE: You can get the
NodeMapEditor
reference for a particular port via the.nodeEditor?.nodeMap
property.
The following <node-port>
attributes mirror their respective properties; see the above
section for more information.
name
<node-editor>
) to be functional.out
<node-port>
as an output port.in
attribute, but you're welcome to specify one if it helps.out
attribute indicates an input port.color
slot="handle"
- places the element within the port handle element, replacingtransform: translate(...)
can be used to offset the visual position of thevar(--port-color)
- useful for custom handles (via slot="handle"
)color
attribute.The following are not unique or specific to the library and could be achieved anyway using normal CSS, but are worthwhile to mention as more of a 'cookbook'.
[out]
- selects all output ports. Useful for applying e.g.text-align: right
[connections]
- selects all connected ports. Useful for applyingvisibility: hidden
. Especially useful when composed asnode-port[connections]:not([out]) > *:not([slot='handle'])
, which only targetsconnections
attribute only existsconnections="0"
<node-title>
(NodeTitleElement
-> HTMLElement
)The <node-title>
element is a container element that does nothing other than to
mark an area within a <node-editor>
as the repositioning handle.
The area making up a <node-title>
can be clicked+dragged to move the parent
editor element.
If the <node-title>
is not a child of a <node-editor>
, it does nothing.
A <node-editor>
may have multiple <node-title>
elements.
NOTE: This element may be expanded and thus renamed prior to the v1 release.
nodeEditor
(read-only) - the <node-editor>
(NodeEditorElement
) that parents<node-link>
(NodeLinkElement
-> HTMLElement
)A <node-link>
is a nuclear (non-container) element that represents
a connection between an output port and an input <node-port>
of two separate
<node-editor>
elements.
NOTE: Despite not being a container element, the Web Components standard still mandates that it must have a full closing tag (i.e.
<node-link ...></node_link>
) and thus cannot be self-closing (e.g.<node-link ... />
is, unfortunately, not allowed). The latter will cause all of your links to become nested within each other.
NOTE: Right now, the contents (child nodes) of
<node-link>
elements are simply ignored and do not display anything. However, this may change before the v1 release. Please treat the inner contents of<node-link>
s as 'reserved', as adding any content there might cause unexpected results when upgrading in the future.
Links can exist without all of their attributes present, or with attributes that refer to editor(s) and/or port(s) that do not exist. They are not automatically removed, but instead have a secondary state indicating whether or not they are "connected".
Links that are missing any one of the four required attributes (from
, to
, in
and out
), or in the case where any one of those attributes refers to a missing
editor or port name
, remain in the disconnected state and do not result in
a link to show up in the editor.
In the event a link is disconnected and a port/editor is either created or has its
name
changed thus that the four required attributes become collectively 'valid',
then the link enters the connected state whereby the connect
event is dispatched
and a visual line is formed between the two referenced ports. This also activates
the fromPort
and toPort
properties, which hold live references to the connected
<node-port>
(NodePortElement
) elements.
<node-link>
elements present in the DOM but that are otherwise invalid (disconnected)
are never automatically removed from the DOM, either at load or during operation.
Connected links between two ports of differing colors result in a color gradient between the two.
Double clicking a connected link line removes it, including from the DOM. This
is the only built-in means by which a <node-link>
can be removed from the DOM.
Of course, application developers are free to remove such elements themselves;
the links will be destroyed as one would expect.
The following events might be dispatched from <node-link>
elements.
See the documentation under <node-map>
for what they mean. Additional
notes or exceptions are added as sub-points.
link
unlink
connect
disconnect
event.target
- the <node-link>
that was removedevent.target.nodeMap
is still valid during this callbacknodeMap
(read-only) - the <node-map>
(NodeMapElement
) that parentsnull
if this link is an orphan)fromName
/ toName
- the string names of the <node-editor>
elementsfrom
and to
attributesnull
if those attributes are not presentoutName
/ inName
- the string names of the <node-port>
elementsout
and in
attributesnull
if those attributes are not presentoutPort
/ inPort
(read-only) - references to the connected<node-port>
(NodePortElement
) elements, or null
if the link isThe following <node-link>
attributes mirror their respective properties; see the above
section for more information.
from
(mirrors the fromName
property)to
(mirrors the toName
property)out
(mirrors the outName
property)in
(mirrors the inName
property)Note that all four of these attributes must be specified in order for
a <node-link>
to be eligible for connection, and a connection is only
created if all four refer to valid editor/port names.
Some questions that aren't so much "frequently" asked, but might come up as you're using the library.
link
/unlink
and connect
/disconnect
events?One of the "unopinionated" design aspects of the library is that it doesn't
immediately remove any <node-link>
elements that might have existed
prior to the library being loaded and the custom components being registered.
Thus, a <node-link>
element can exist/be added to a <node-map>
that
refers to non-existant editors/ports, which immediately fires off a link
event.
It's not until the editors/ports referred to by the <node-link>
element
come into existence that the connect
event is fired.
In most cases, you will only care about the connect
and disconnect
events.
This may change prior to the v1 release - the usefulness of this approach has yet to be determined. The motivation was "be as un-destructive as possible" to the data you feed the components.
node-editor
support?Technically this library works as far back as Chrome 81, but
will leak events and memory all over the place. You need at least
Chrome 88 for this to work properly. This is due to the extensive
use of AbortController
in addEventListener()
s, which was
added in Chrome 88.
All other evergreen browsers released around the same time as Chrome 88 should work. If you'd like to contribute a formal table of browser support, a PR is welcome!
<foreignObject>
?No. Foreign objects had a lot of quirks that I didn't particularly care for, so instead the component uses two layers - an SVG background for the dots pattern and links' bezier curves, and an upper plain HTML layer that holds the editor nodes themselves.
Both layers have their viewports synchronized/scaled in tandem, which
achieves the same effect as a <foreignObject>
but without all of
the limitations/quirks.
It depends. I'll try to give you an idea of what is considered in/out of scope for this library (though it never hurts to open an issue anyway).
Things that are within the scope of node-editor
:
Things that are outside the scope of node-editor
:
Even if you're unsure, open an issue anyway and ask! Just don't be upset
if it's considered outside the scope of the library (node-editor
is
intentionally very unopinionated about how editors look or behave).
(dis)connectedCallback()
/attributeChangedCallback()
manually?Please don't. These are intended to be called directly by the Web Components system in browsers and thus expect to actually be called in response to some lifecycle event. While all internal ("private") methods are hidden away from the public API using symbols, these callbacks couldn't be. This doesn't mean they should be called by user code.
NodeXyzElement.observedAttributes
?Nothing's stopping you, it has to exist for the web components to work correctly, and referring to them is guaranteed not to have any side effects.
Copyright © 2022 by Josh Junon. Released under the MIT License.