DEV Community

Cover image for How we used JointJS in Voximplant Kit
Irina Maximova
Irina Maximova

Posted on • Edited on

How we used JointJS in Voximplant Kit

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 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, 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

А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 you can watch the video of how the demo mode works or test it yourself after creating a Voximplant Kit account.

We put our cards on the table

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

Demo scheme

The editor calls the 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

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 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:

const link = this.editor.getTestLink(sourceCell, portId);
Enter fullscreen mode Exit fullscreen mode

If there is a connector, we animate the movement from block to block (like on the GIF at the beginning of the article):

if (link) this.setLinkAnimation(link);
Enter fullscreen mode Exit fullscreen mode

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):

setTimeout(this.selectBlock, 700);
Enter fullscreen mode Exit fullscreen mode

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:

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);
     }
   }
Enter fullscreen mode Exit fullscreen mode

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:

link = this.graph.getConnectedLinks(cell, {outbound : true}).find(item => {
     return item.get('source').port === portId;
Enter fullscreen mode Exit fullscreen mode

Then, if there is such a connector, we highlight it:

return link ? (link.toFront() && link) : null;
Enter fullscreen mode Exit fullscreen mode

The full code of the method:

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;
}
Enter fullscreen mode Exit fullscreen mode

The movement from block to block is animated completely by means of JointJS (check the demo).

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:

const center = cell.getBBox().center();
Enter fullscreen mode Exit fullscreen mode

Then, we call the setTestCell method to color the block:

editor.tester.setTestCell(cell);
Enter fullscreen mode Exit fullscreen mode

Finally, we zoom to its center using the self-written zoomToCell function (we’ll talk about it at the end of the article):

editor.paperController.zoomToCell(center, 1, false);
Enter fullscreen mode Exit fullscreen mode

The full code:

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;
}
Enter fullscreen mode Exit fullscreen mode

The method for coloring: it finds the SVG element of our block and adds the .is-tested CSS class to color it:

setTestCell(cell: Cell): void {
 const view = cell.findView(this.paper);
 if (view) view.el.classList.add('is-tested');
}
Enter fullscreen mode Exit fullscreen mode

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:

.animate-viewport {
 will-change: transform;
 transition: transform 0.5s ease-in-out;
Enter fullscreen mode Exit fullscreen mode

Check the full method code below:

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);
 }
Enter fullscreen mode Exit fullscreen mode

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

Top comments (7)

Collapse
 
sobekcinasobek profile image
sobekcinasobek

Nice UX!
It would be great to show us how did you implement features like: "fit to screen", "minimap interaction", "align graph". Showing implementation of the "undo/redo" stack would be also great.

Collapse
 
imaximova profile image
Irina Maximova

thank you! I'll discuss it with our dev team :)

Collapse
 
jayjosh20 profile image
SirJosh

Wow good work guys.. Is there a way I could get voximplant Integrated with Facebook

Collapse
 
imaximova profile image
Irina Maximova

Hey, thanks a lot! Can you please clarify your question? What exactly would you like to achieve?

Collapse
 
hongtm1355 profile image
Tran Hong

Wow, wonderful. Could you show how did you implement undo/redo?
Thank you give me great post

Collapse
 
imaximova profile image
Irina Maximova

Hi! I'm glad you liked it. We discussed the possible topics with our dev team and this time decided to show the readers how to optimize graphics when working with Joint JS. I hope you'll find it no less interesting. Here is the link to my new post: dev.to/imaximova/optimizing-graphi...

Collapse
 
ishaanshettigar profile image
Ishaan Shettigar

It would be great to have a look at the way y'all implemented the undo redo stack.
Which exact events did yall listen for etc.