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).
А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.
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).
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);
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);
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);
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);
}
}
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;
Then, if there is such a connector, we highlight it:
return link ? (link.toFront() && link) : null;
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;
}
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();
Then, we call the setTestCell method to color the block:
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):
editor.paperController.zoomToCell(center, 1, false);
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;
}
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');
}
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;
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);
}
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!
Top comments (7)
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.
thank you! I'll discuss it with our dev team :)
Wow good work guys.. Is there a way I could get voximplant Integrated with Facebook
Hey, thanks a lot! Can you please clarify your question? What exactly would you like to achieve?
Wow, wonderful. Could you show how did you implement undo/redo?
Thank you give me great post
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...
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.