How we used JointJS in Voximplant Kit
Tags: javascript, webdev, showdev, programming
Published: 2020-04-20 14:00:19

Hey, community! My name is Irina, I'm a technical writer at Voximplant. It's exciting to have an ability to share something intriguing with you guys, so let's get straight to the point – as my job includes lots and lots of "how it works" investigation, I'd like to tell you about nice tweaks that our frontend developers applied to a popular JS library, JointJS. But before we go deeper to the JavaScript jungle, let me give you a tiny onboarding to our product in which we implemented the aforementioned tweaks. I'm talking about Voximplant Kit that has recently undergone a major update. So, let's begin, shall we?

[Voximplant Kit](https://voximplant.com/kit) is a smart and flexible tool for customer experience automation, formerly known as Smartcalls. With Voximplant Kit you can build smart IVRs, launch automated call campaigns and make trigger outbound calls with our visual flow builder to improve customer experience in no time.

And now we’re ready to share what we changed in our interface and how we tamed [JointJS](https://www.jointjs.com/opensource), a JavaScript beast that helped us a lot in updating our visual editor.

#What has actually changed?

Although the concept remains the same, this update brings a new user interface crafted by our UX experts, along with new tools and features for even smarter call processing automation.

New design of the registration page, the top menu position change, an option of grouping and sorting scenarios and audio recordings; campaign cards showing the average duration of a successful call and the total amount of money spent. 

#Voximplant visual editor 

Demo mode (spoiler: this is the main killer feature we want to tell you about).

![Kit demo](https://dev-to-uploads.s3.amazonaws.com/i/qc18qbexwhew63s6rwfc.gif)

Аlong with real-time scenario execution, the demo mode highlights the blocks used in the current call and shows the result of the call (Flow and Log), which makes the debugging process easier and faster.

[Here](https://www.youtube.com/watch?v=hhYVauBwb0A) you can watch the video of how the demo mode works or test it yourself after creating a [Voximplant Kit account](https://kit.voximplant.com/registration).

#We put our cards on the table

It's time to figure out how the block animation is implemented in the code.

![Demo scheme](https://dev-to-uploads.s3.amazonaws.com/i/jqaz5h4iudzhovn12ruv.png)

The editor calls the [StartScenarios](https://voximplant.com/docs/references/httpapi/managing_scenarios#startscenarios) HTTP API method to run a cloud scenario. The Voximplant cloud starts the scenario and gives the **media_access_url** to the editor. From this moment on, the editor calls the **media_access_url** every second, receiving information on the blocks used by the scenario. Based on this data, the editor highlights the necessary blocks and animates the lines that connect them. 

And here we’ll need the movement history stored in the HISTORY object – the JSON object with the following fields:

And here we’ll need the movement history stored in the HISTORY object – the JSON object with the following fields:

- timestamp;
- idSource - initial block;
- idTarget - final block;
- port (there may be several outputs from 1 block).

![History JSON](https://dev-to-uploads.s3.amazonaws.com/i/3n6e4r9pw1f02tb8bntj.png)

These custom and service values help the front-end understand from which block to which the scenario moves. How exactly? When a new block is added, it immediately gets an ID, which is then used in HISTORY as **idSource** / **idTarget**.

To implement this functionality, we used [JointJS](https://www.jointjs.com/opensource) and some self-written code.

Let's begin with the main method – **selectBlock**. It works as follows: we go through the array of movements history (**idSource**, **idTarget**) and as soon as we find the start and end points, we search for a connector between them:
```javascript
const link = this.editor.getTestLink(sourceCell, portId);
```
If there is a connector, we animate the movement from block to block (like on the GIF at the beginning of the article):
```javascript
if (link) this.setLinkAnimation(link);
```
The **selectBlock** method is called every time **this.testHistory** is updated. Since several blocks can be passed to **this.testHistory** at the same time, we recursively call **selectBlock** every 700 ms (this is the approximate time spent on animating the movement from block to block):
```javascript
setTimeout(this.selectBlock, 700);
```
The full code of this method is given below. Pay attention to the methods **selectTestBlock** and **getTestLink**, lines 7 and 10 – in a moment, we will talk about each of them:
```javascript
selectBlock():void {
     if (this.historyIndex < this.testHistory.length) {
       const i = this.historyIndex;
       const targetCellId = this.testHistory[i].idTarget;
       const sourceCellId = this.testHistory[i].idSource;
       const portId = this.testHistory[i].port;
       const targetCell = this.editor.selectTestBlock(targetCellId);
       const sourceCell = this.editor.getCell(sourceCellId);
       if (sourceCell && targetCell) {
         const link = this.editor.getTestLink(sourceCell, portId);
         if (link) this.setLinkAnimation(link);
       }
       this.historyIndex += 1;
       setTimeout(this.selectBlock, 700);
     }
   }
```

###Drawing a line connector

The **getTestLink** method helps us get a connector between blocks. It is based on **getConnectedLinks**, a built-in JointJS method that receives a block and returns an array of its connectors. In this array, we look for a connector to the port with **portId** as the value of the **source** property:
```javascript
link = this.graph.getConnectedLinks(cell, {outbound : true}).find(item => {
     return item.get('source').port === portId;
```
Then, if there is such a connector, we highlight it:
```javascript
return link ? (link.toFront() && link) : null;
```
The full code of the method:
```javascript
getTestLink(sourceCell: Cell, portId: string): Link {
  let link = null;
  if (sourceCell && sourceCell.id) {
    let cell = null;
    if (sourceCell.type === 'ScenarioStart' || sourceCell.type === 'IncomingStart') {
      cell = this.getStartCell()
    } else {
      cell = this.graph.getCell(sourceCell.id);
    }
    link = this.graph.getConnectedLinks(cell, {outbound : true}).find(item => {
      return item.get('source').port === portId;
    });
  }
  return link ? (link.toFront() && link) : null;
}
```

The movement from block to block is animated completely by means of JointJS ([check the demo](https://resources.jointjs.com/demos/pn)).

###Moving to the current block

We call the **selectTestBlock** method when it is necessary to select the final block and move the canvas to it. Here we get the coordinates of the block center:
```javascript
const center = cell.getBBox().center();
```
Then, we call the **setTestCell** method to color the block:
```javascript
editor.tester.setTestCell(cell);
```
Finally, we zoom to its center using the self-written **zoomToCell** function (we’ll talk about it at the end of the article):
```javascript
editor.paperController.zoomToCell(center, 1, false);
```
The full code:
```javascript
selectTestBlock(id: string): Cell {
 const cell = (id === 'ScenarioStart') ? editor.tester.getStartCell() : editor.graph.getCell(id);
 if (cell) {
   const center = cell.getBBox().center();
   editor.tester.setTestCell(cell);
   editor.paperController.zoomToCell(center, 1, false);
 }
 return cell;
}
```
The method for coloring: it finds the SVG element of our block and adds the **.is-tested** CSS class to color it:
```javascript
setTestCell(cell: Cell): void {
 const view = cell.findView(this.paper);
 if (view) view.el.classList.add('is-tested');
}
```
###Smooth zoom

And finally, **zoomToCell**! JointJS has a built-in method for moving the canvas along the X and Y axes. At first, we wanted to work with it, however, this method uses **transform** as an attribute of the SVG tag. It does not support smooth animation in the Firefox browser and utilizes the CPU only.

We used a small hack – created our own **zoomToCell** function, which, in essence, does the same thing but **transform** here is an inline CSS. This way, we enable GPU rendering because WebGL is involved in the process. Thus, the problem of cross-browser compatibility has been solved.

Our function not only moves the canvas along X and Y but also allows us to zoom simultaneously by means of the **transform matrix**.

The **will-change** property of the **.animate-viewport** class informs the browser that the element will be changed and optimization must be applied, including the use of the GPU. And the **transition** property sets the smoothness of moving the canvas to the block:
```javascript
.animate-viewport {
 will-change: transform;
 transition: transform 0.5s ease-in-out;
```
Check the full method code below:
```javascript
public zoomToCell(center: g.Point, zoom: number, offset: boolean = true): void {
   this.updateGridSize();
   const currentMatrix = this.paper.layers.getAttribute('transform');
   // Get a new SVG matrix to move the canvas to a point from the center argument
    // and destructure it to set the style attribute
   const { a, b, c, d, e, f } = this.zoomMatrix(zoom, center, offset);
// For FireFox you need to set the original matrix, otherwise there is an abrupt motion of the canvas
   this.paper.layers.style.transform = currentMatrix;
   // Without the first timeout, FF skips the fact that we set the original matrix, and an abrupt motion occurs again
   setTimeout(() => {
// Add a CSS selector .animate-viewport, which has its own transition;
// Set the new matrix in the style attribute and calculate the duration of the transition
     this.paper.layers.classList.add('animate-viewport');
     this.paper.layers.style.transform = `matrix(${ a }, ${ b }, ${ c }, ${ d }, ${ e }, ${ f })`;
     const duration = parseFloat(getComputedStyle(this.paper.layers)['transitionDuration']) * 1000;
     // After the animation is completed, remove the selector and style attribute;
      // set the matrix for the canvas using JointJS
     setTimeout(() => {
       this.paper.layers.classList.remove('animate-viewport');
       this.paper.layers.style.transform = null;
       this.paper.matrix(newMatrix);
       this.paper.trigger('paper:zoom');
       this.updateGridSize();
       this.paper.trigger('paper:update');
     }, duration);
   }, 100);
 }
```
As it turned out, sometimes even the most advanced libraries have to be modified if your needs require that. We hope you enjoyed taking a deep dive into the library insides. We wish you successful development in general and with the Voximplant Kit in particular!  Find other interesting stories on Voximplant and Voximplant Kit blogs.

#P.S.
Like the article and want to know more about our frontend-related challenges? For example, how we implemented undo/redo (1), proper shortcuts processing (2), pop-up menu with the minimap, zooming, sharing, and other tools (3), and so on. Leave your comment below, let us know what would grab your attention (if any) :) Thanks for reading!

![Kit updates](https://dev-to-uploads.s3.amazonaws.com/i/urqzd4dzrnm2lu6ss9du.png)