Skip to main content

3 posts tagged with "plugin"

View All Tags

· 6 min read
Simon Porritt

Snaplines plugin

In version 6.7.0 we released a new plugin - Snaplines - which you can see in action in our Flowchart builder demonstration. In this post we're going to look at a couple of methods offered by the surface component which allowed us to implement snaplines in a matter of hours.

This is a picture of the snaplines plugin in action:

Snaplines - align your flowcharts perfectly - jsPlumb Toolkit, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

The internal workings of this plugin are quite simple:

  1. At drag start, record the position of the edges and center of each of the other elements in the canvas
  2. As the user drags an element, check these positions to see if one of the element's edges, or its center, is in proximity to an edge or the center of one or more other elements
  3. When the drag element is in proximity to an edge or center of some other element, draw a snapline.

Steps (1) and (2) are just a bit of maths, and we're not going to cover them here. It's step 3 we're here to talk about today - "draw a snapline". As always with computers, it's easy to say something, but how do you actually do it? Fortunately the Toolkit's surface component makes it straightforward.

Fixing elements

The key to the ease with which snaplines was developed is this method that is available on the surface component:

fixElement(el:Element, position:PointXY, constraints?:{left?:boolean, top?:boolean})

Given some DOM element, fixElement will place it on the surface canvas at position - where position is a point in the coordinate space of the elements of the canvas, and then fix it in one or both axes so that it never disappears from view. That's right: fixElement abstracts away all of the considerations about the current zoom level, or where the user has panned the canvas to, and just plonks down the element where you asked it to. When you subsequently pan and/or zoom the canvas, the position of the fixed element is updated so that it stays where you requested. Great!

Snaplines Example

Click on a node in this example and we'll create a horizontal and vertical line, each one passing through the element's center. Then we'll call fixElement for each line:

surface.fixElement(horizontalLine, 
{
x:p.obj.data.left - (p.obj.data.width * 0.5),
y:p.obj.data.top + (p.obj.data.height / 2)
}
)

surface.fixElement(verticalLine,
{
x:p.obj.data.left + (p.obj.data.width * 0.5),
y:p.obj.data.top - (p.obj.data.height / 2)
}
)

Now when you pan/zoom the canvas, the red cross stays in place where it was fixed. Note that it does not follow the node around - you can drag a node that you previously clicked and the cross will not move. This is by design.

It's important to keep in mind that you can fix arbitrary HTML elements using this method - they don't have to be simple red lines like in this example. You could fix any HTML you like.

Unfixing an element

Notice as you click on new nodes the cross moves? And when you click on whitespace it goes away? There is a companion method - unfixElement - which will remove a fixed element from the canvas. We call this whenever a click occurs on a node or the canvas:

surface.unfixElement(horizontalLine)
surface.unfixElement(verticalLine)

Constraints

You may have noticed the constraints argument to the fixElement method - and also that in both examples above we did not provide a value. The snaplines plugin uses fixElement in the simplest way, but it has some extra tricks: using constraints, you can fix an element to a specific position on the canvas and then if that position ever goes out of the viewport the element will be shifted to ensure it is always visible.

In this next example we have positioned a label at {x:50, y:150} and fixed it so that it never goes into the negative x axis, and we've positioned another label at {x:300, y:20} and fixed it so that it never goes into the negative y axis:

surface.fixElement(myLabel, { x:50, y:150}, {left:true})
surface.fixElement(myOtherLabel, { x:300, y:20}, {top:true})

You can of course fix one element in both axes:

surface.fixElement(myLabel, { x:50, y:150}, {left:true, top:true})

Swim Lanes example

You can use fixed elements to implement sticky headers for swim lanes, which is pretty cool. Try dragging the canvas below to pan it and see how the lane headers stay in place when you pan to the left.

We fix the headers above with this code (this is for the first header):
surface.fixElement(l,{x:50, y:50},  {left:true})

Floating Elements

Another way to decorate your canvas is with floating elements - elements that are positioned at some position relative to the viewport origin, and which do not pan and zoom with the content:

const l = document.createElement("div")
l.innerHTML = "I AM FLOATING"
surface.floatElement(l,{x:10, y:10})

Floating inspector example

In this example we use floatElement to float an HTML element which is configured as an inspector - code is shown below. Click on nodes to inspect them and change their label/background color. Click the canvas to reset the selection.

Our nodes are like this:

{ id:"1", label:"Un", left:150, top:50, bg:"darkslateblue" },

We create the element for the inspector and initialize it like this:

const l = document.createElement("div")
l.className = "floating-inspector"

new VanillaInspector({
container:l,
surface:s,
emptyTemplate:`<span>No selection</span>`,
templateResolver:() => {
return `<div>
<div style="display:flex;align-items:center">
<strong>ID:</strong>
{{id}}
</div>
<strong>Label:</strong>
<input jtk-att="label" jtk-focus/>
<strong>Background:</strong>
<select jtk-att="bg">
<option value="darkslateblue">Dark Blue</option>
<option value="#396a1e">Dark Green</option>
</select>
</div>`
}
})

And then we ask the surface to float the element it is mounted in:

surface.floatElement(l, {x:10, y:10})

Summary

fixElement and floatElement are powerful methods that you can use to bring your apps to a new level. They're tightly integrated with the Toolkit ecosystem and they're DOM agnostic - you can use any markup you like, meaning it's simple to keep things on brand. The examples here only scratch the surface - pun not intended, but I'm leaving it in - of what you can do.


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· 2 min read
Simon Porritt

jsPlumb uses Docusaurus, a super handy static site generator, for various parts of our ecosystem, including our Documentation and also our recently released stand alone components product.

While developing the site for jsPlumb Components, we wanted to include a couple of pieces of information that would vary depending on whether we were running in development mode locally (ie docusaurus start) or building for production. We looked into the various options available, all based on the dotenv plugin for Webpack, but couldn't find what we were looking for: a solution that worked, with minimal manual intervention, in both development mode and when building for production.

So we ended up building our own plugin. This plugin reads values from a JSON file and makes them available to your site via the customFields map in the Docusaurus siteConfig.

note

When I say "Docusaurus" in this post I am talking about v2. I've not used v1.

Installation

npm i @jsplumb/docusaurus-plugin-env-loader-json

Configuration

You just have to add the plugin to the list of plugins in docusaurus.config.js:

plugins:[
"@jsplumb/docusaurus-plugin-env-loader-json"
],

and then create an env.json file in the Docusaurus project directory.

env.json

Your environment variables should be keyed under a section that identifies the environment you are targetting - "development" when using docusaurus start and "production" when running a build. The plugin reads the environment name from the environment variable NODE_ENV.

{
"production":{
"SERVER_URL":"https://some.server.com/anEndpoint"
},
"development":{
"SERVER_URL":"http://localhost:4200/anEndpoint"
}
}

Accessing values

You can access these values via the customFields section of the Docusaurus site config:

import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

export function MyApp {

const {siteConfig} = useDocusaurusContext();
const serverUrl = siteConfig.customFields.SERVER_URL

...

}

Changing the configuration file name

If you want to specify some file other than env.json in your project's root, you can do so by setting the sourceFile option of the plugin:

plugins:[
[
"@jsplumb/docusaurus-plugin-env-loader-json",
{
"sourceFile":"path/to/aFile.json"
}
]
],

This path should be specified relative to the project root. No leading slash is required.


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 5 min read
Simon Porritt

jsPlumb uses Statcounter to keep track of what pages in the documentation people are looking at. Recently, at the tail end of the work to migrate to Typescript and release version 5.x of the Community and Toolkit editions, we started using Docusaurus, a super handy static site generator that not only runs on React but has React baked right into it, allowing us to easily embed working demonstrations throughout our docs, or to embed snippets from the api documentation directly into the main documentation via React components. It's a really handy tool, and if you're looking for a static site generator I recommend giving it a look.

note

When I say "Docusaurus" in this post I am talking about v2. I've not used v1.

This is not a post about how great Docusaurus is, though. This is a quick post about a Statcounter plugin I wrote while working on the jsPlumb API documentation. Docusaurus ships with a plugin for Google Analytics, allowing to quickly add support for GA to all the pages on your site, but jsPlumb doesn't use Google Analytics. Statcounter's interface has a pleasing directness to it and gives us all the information we need. A quick look around the internets came up blank on existing Docusaurus/Statcounter integrations, so I wrote a plugin.

Installation

npm i @jsplumb/docusaurus-plugin-statcounter

Configuration

First you have to add the plugin to the list of plugins in docusaurus.config.js:

plugins:[
"@jsplumb/docusaurus-plugin-statcounter"
],

Then you need to configure the plugin via a statCounter block in the themeConfig of your docusaurus.config.js. It takes two arguments, both required:

themeConfig: {
statCounter:{
projectId: "2222222",
securityCode: "2222222"
},
...
}

projectId and securityCode are available in the Statcounter console for the project you wish to target.

Internals

This is the first plugin I've written for Docusaurus and it doesn't have the most complex requirements, but I was quite impressed with the plugin mechanism. Let's start with the package.json:

{
"main": "src/index.js",
"name": "@jsplumb/docusaurus-plugin-statcounter",
"version": "1.0.0"
}

We deliver a basic package with a single entry point.

Bootstrap

Let's take a look at the first few lines of index.js:


const path = require('path');

module.exports = function (context) {

const {siteConfig} = context;
const {themeConfig} = siteConfig;
const {statCounter} = themeConfig || {};


};

Our module exports a single function that takes a context object, inside of which we can extract the siteConfig, and, from that, the themeConfig, which contains our statCounter settings.

Once we have our statCounter object, we extract the things we need from it, and complain about stuff that is missing:

const {projectId, securityCode} = statCounter;

if (!projectId) {
throw new Error(`The statcounter plugin requires a "projectId" to be set`)
}

if (!securityCode) {
throw new Error(`The statcounter plugin requires a "securityCode" to be set`)
}

Linking with Statcounter

Statcounter works by importing a JS file in your document's head, after setting a couple of global variables. Docusaurus makes this very easy for us - all we have to do is declare an injectHtmlTags() method in our plugin:

return {
name: 'docusaurus-plugin-statcounter',

getClientModules() {
return [path.resolve(__dirname, './statcounter')]
},

injectHtmlTags() {

return {
headTags:[
{
tagName:'script',
innerHTML: `
var sc_project="${projectId}";
var sc_invisible=1;
var sc_security="${securityCode}";
`
},
{
tagName:'script',
attributes:{
src:"https://www.statcounter.com/counter/counter.js"
}
}
]
}
}
};

We inject two tags into the head: first we inject a script element and provide its innerHTML - this sets up the global variables. Next, we inject another script, but this time we set its src attribute, so it loads the JS from Statcounter's site. Documentation for injectHtmlTags(..) can be found here.

Tracking page changes

Given that Docusaurus is an SPA, pages get swapped in and out without new page loads, and Statcounter would be oblivious, were we not to advise it. Fortunately this was also straightforward. Note this block in our plugin's code:

getClientModules() {
return [path.resolve(__dirname, './statcounter')]
}

This instructs Docusaurus to load the module found in ./statcounter.js. The source code for that file looks like this:

import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

export default (function () {
if (!ExecutionEnvironment.canUseDOM) {
return null;
}

return {
onRouteUpdate({location}) {
_statcounter.record_pageview()
},
};
})();

This module hooks into onRouteUpdate, which I think is perhaps undocumented (for inspiration on this bit I used the Google Analytics plugin).

When a route update event occurs, we call _statcounter.record_pageview(). _statcounter is an object in the global space that was added by Statcounter's JS.

Conclusion

That's the whole plugin - perhaps 80 lines of code. Very straightforward - it took me about half an hour to write and deploy. Injecting HTML tags is not the only thing you can do with a Docusaurus plugin, though. Take a look through the lifecycle APIs to get a feel for what's possible.

If you want to follow up on this, the source is on Github at https://github.com/jsplumb/docusaurus-plugin-statcounter.


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.