One of our popular features is the ability to create clips from research videos. A given research session has a handful of key observations that showcase the customer’s pain point or highlight. These observations range from five seconds to five minutes and cutting these clips out of a 30 to 60-minute video needs to be quick and easy. We are always tuning the UI for how best to efficiently cut up a research video so our users can move on to the next task quickly. A very common way to cut video is selecting a range on a timeline, like the Trim tool in QuickTime.
To implement this in the browser we needed to create a draggable and resizable component in React. The requirements were draggable horizontally only, resizable horizontally only, and stay within the bounds of a parent component. There are many pre-existing components out there, like react-grid-layout and react-resizable, react-drag-drop-container, and re-resizable to name a few. But, none had the combination of draggable and resizable component we were looking for. After researching each component, we realized that when combined, they all meet our needs. We went with react-drag-drop-container + react-resizable.
Since React is designed to modularly combine components, it’s a trivial exercise to combine the two components, right? Well technically yes, but with many quirks. For one, the drag container took precedence and the component would not resize only move. Another tricky bit was keeping the clipping selector within the parent timeline component. It wouldn’t do to have it draggable outside the timeline to clip another part of the app.
Okay, that’s enough forced phatic speech from an engineer. Let’s get to the code.
There were two main issues to resolve: how to get `DragDropContainer` and `ResizableBox` to play nicely together (drag when dragging and resize when resizing); and how to keep the clipper inside the thumbnail timeline (parent HTML element).
How to get `DragDropContainer` and `ResizableBox` to play nicely together
When these two components are nested, they do not automatically play nice. When you click to resize the DragDropContainer also picks up the click and enables dragging. What we want is to drag when we click on the middle and resize when we click on the edge. To help these components behave we have to keep track of the state. Dragging is enabled by default, so we need to tell DragDropContainer when to NOT drag.
Once these functions are wired up it will drag when dragging is desired and resize appropriately.
How to keep the clipper inside the thumbnail timeline (parent HTML element)
The purpose of this is to select the part of the video to clip or cut out, thus the component must stay within the bounds of the timeline. This involved getting the width of the timeline as well as the location of this component. While there are many ways to measure react components, we went at this using `refs` to get access to the DOM properties of the element: clientWidth and offsetLeft. Using the refs we can traverse the DOM try to obtain the clientWidth of the timeline, the offsetLet of the DragDropContainer, and the clientWidth of the clipper. From there we can use basic match to find the maxWidth the Clipper can be resized as well as how far to the right or to the left the Clipper can be dragged.
The very last trick is the user can drag faster than the React state will update. Meaning if they drag outside the bounding box it will stop a few pixels past the end. Then, when they start dragging again it is outside the bounding box, which forces them to stop, so they have to click-drag-click-drag in rapid succession to free themselves. To solve the “sticking” we track which direction the user is dragging with a private startX variable. If we are on the left most side and startX is greater than draggingX we are moving left, so stop dragging. Then reverse that for the right-hand side.
Below is the source code for this component combining react-drag-drop-container and react-resizable to create a draggable and resizable component.
Gist of the Source Code
Links to libraries referenced.