🎉 JointJS has new documentation! ðŸ¥³

JointJS+ PaperScroller

ui.PaperScroller

Paper scroller wraps the paper element and implements scrolling, panning, centering and auto-resizing of the paper content.

Installation

Include joint.ui.paperScroller.js and joint.ui.paperScroller.css files to your HTML:

<link rel="stylesheet" type="text/css" href="joint.ui.paperScroller.css">
<script src="joint.ui.paperScroller.js"></script>

Create a PaperScroller

PaperScroller creates a window to the paper area. This way, the actual paper can be large but the user will see only the PaperScroller area which he can scroll & pan. First, we create a PaperScroller object. Then we pass the automatically created PaperScroller element as a container for our paper. Once we have our paper object created, we pass it back to the PaperScroller as it needs a reference to the actual paper.

var paper = new joint.dia.Paper({
    width: 2000,
    height: 2000,
    model: graph
});

var paperScroller = new joint.ui.PaperScroller({
    paper: paper
});

$('#paper-container').append(paperScroller.render().el);

The next step is to hook the startPanning method of the paper scroller on the blank:pointerdown paper event which will cause panning whenever the user drags a blank area of the paper:

paper.on('blank:pointerdown', paperScroller.startPanning);

In order to help users understand what action will happen when they drag the paperScroller, use setCursor(cursor) method. It sets the mouse cursor, which is displayed when the mouse pointer is over the PaperScroller. It accepts any valid CSS cursor property value. Alternatively the cursor can be passed to the constructor. It defaults to 'default' (an arrow).

var paperScroller = new joint.ui.PaperScroller({
    paper: paper,
    cursor: 'grab'
});
// Or dynamically change cursor e.g. when user is about to select elements.
paperScroller.setCursor('crosshair');

Padding

The default padding around the paper's content. The padding ensures the paper inside the PaperScroller is always pannable. It also makes sure that there is always at least a fragment of the paper visible. Padding can be a number, an object, or a function.

const scroller = new joint.ui.PaperScroller({
  paper: paper,
  // Default Padding function
  padding: function() {

    var clientSize = this.getClientSize();
    var minVisibleSize = Math.max(this.options.minVisiblePaperSize, 1) || 1;
    var padding = {};

    padding.left = padding.right = Math.max(clientSize.width - minVisibleSize, 0);
    padding.top = padding.bottom = Math.max(clientSize.height - minVisibleSize, 0);

    return padding;
  }
});

/*
  Example Usage
  padding: 10
  padding: { top: 20, left: 20 }
  padding: function() { return 10; }
*/

Scrolling the paper

User scrolling of the paper is allowed by default. Two methods are provided to toggle this functionality:

lock()

Lock the current viewport by disabling user scrolling.

unlock()

Enable user scrolling if previously locked.

PaperScroller also provides methods for programmatic scrolling. These methods try scroll the paper so that a specified point on the paper lies at the center of the paper scroller viewport. If this is not possible (e.g. if the requested point lies in a corner of the paper), the paper is scrolled as far as possible without requiring paddings. This means that the requested point will not actually move into the center of the viewport if there is not enough room for it. (Use the center functions to add paddings if necessary and force the requested point to move to the center of paper scroller viewport.)

All scroll methods optionally support animation if an opt.animation object is provided. The object may contain jQuery animation properties, most notably opt.animation.duration - a number specifying the time, in milliseconds, determining how long the animation will run. (Note that animated transitions are better supported with the transition functions.)

scroll(x, y [, opt])

Try to scroll the paper scroller so that the position (x,y) on the paper (in local coordinates) is at the center of the paper scroller viewport. If only one of the coordinates is specified, only scroll in the specified dimension and keep the other coordinate unchanged.

paperScroller.scroll(100, 200); // scroll to the point (100,200)
paperScroller.scroll(100); // scroll to the point (100,[y of center of visible area])
paperScroller.scroll(null, 200); // center at the point ([x of center of visible area],200)
paperScroller.scroll(100, 200, { animation: { duration: 400 }});
paperScroller.scroll(100, null, { animation: { duration: 200, easing: 'linear' }});
paperScroller.scroll(null, 200, { animation: { duration: 600 }});
scrollToContent([opt])

Try to scroll the paper scroller so that the center of paper content is at the center of the paper scroller viewport.

paperScroller.scrollToContent();
paperScroller.scrollToContent({ animation: { duration: 600 }});
scrollToElement(element [, opt])

Try to scroll the paper scroller so that the center of element (an instance of joint.dia.Element) is at the center of the paper scroller viewport.

paperScroller.scrollToElement(element);
paperScroller.scrollToElement(element, { animation: { duration: 600 }});

The PaperScroller constructor takes an optional parameter scrollWhileDragging. When this parameter is set to true, the PaperScroller automatically scrolls the paper to ensure that the dragged shape stays within the viewport. This scrolling behavior can be adjusted further by providing the following options:

ScrollWhileDragging
option type default description
interval number (miliseconds) 25 A debounce interval between changes in the scrollLeft and scrollTop properties of the PaperScroller, in ms.
padding number (pixels) -20 Additional padding to add to the viewport area until scrolling is triggered. A negative value means that scrolling is triggered when the dragging pointer approaches the viewport boundary from the inside. A value of 0 means that scrolling is triggered when the dragging pointer crosses the viewport boundary. A positive value means that scrolling is triggered when the dragging pointer is that much farther from the viewport boundary.
scrollingFunction function
(distance) => ((distance < 20) ? 5 : 20)
A function that translates the distance between the pointer and the viewport boundary into the number of pixels by which to scroll. The default function scrolls slowly while the pointer is within the viewport and goes faster when the pointer leaves the viewport.

Another optional parameter is inertia. If set to true, panning of the PaperScroller will have an inertia effect where the scroller builds momentum while dragging and continues moving in the same direction for some time. The parameter can be also enabled with an object containing the following properties.

inertia
option type default description
friction number 0.92 Rate at which values slow down after the user releases the pointer. A value between 0 and 1.

The optional parameter borderless changes the default behavior of the padding. If set to true, the paper inside the paperScroller extends beyond the padding. i.e. the blank space around the paper is converted to the paper itself making the borders of the paper visible no more.

Centering and positioning paper content

The center methods are more aggressive than the scroll methods. These methods position the paper so that a specific point on the paper lies at the center of the paper scroller viewport, adding paddings around the paper if necessary (e.g. if the requested point lies in a corner of the paper). This means that the requested point will always move into the center of the viewport. (Use the scroll functions to avoid adding paddings and only scroll the paper scroller viewport as far as the paper boundary.)

The position methods are a more general version of the center methods. They position the paper so that a specific point on the paper lies at requested coordinates inside the paper scroller viewport.

Padding can be added with opt.padding, to offset positioned elements away from the edges of client viewport. This stacks up with the x and y coordinates; it also affects the center calculations. The padding value is normalized using the joint.util.normalizeSides function; either a single number may be passed (e.g. padding: 10 applies padding of 10px to all edges), or an object with numeric properties (e.g. padding: { top: 20, horizontal: 10 } applies padding of 20px to the top edge and 10px to the right and left edges).

center([opt])

If no coordinate is specified, position the center of paper to the center of the paper scroller viewport.

paperScroller.center();

Adding 100px of left padding causes the effective center of the paper scroller window to shift by 50px to the right. That is where the center of paper will be positioned.

paperScroller.center({ padding: { left: 100 }});
center(x, y [, opt])

Position the point (x,y) on the paper (in local coordinates) to the center of the paper scroller viewport. If only one of the coordinates is specified, only center along the specified dimension and keep the other coordinate unchanged.

paperScroller.center(100, 200); // center at the point (100,200)
paperScroller.center(100); // center at the point (100,[y of center of visible area])
paperScroller.center(null, 200); // center at the point ([x of center of visible area],200)

Adding 100px of left padding causes the effective center of the paper scroller window to shift by 50px to the right. That is where the point will be positioned.

paperScroller.center(100, 200, { padding: { left: 100 }});
paperScroller.center(100, null, { padding: { left: 100 }});
paperScroller.center(null, 200, { padding: { left: 100 }});
centerContent([opt])

Position the center of paper content to the center of the paper scroller viewport.

paperScroller.centerContent();
paperScroller.centerContent({ padding: { left: 100 }});
centerElement(element [, opt])

Position the center of element (an instance of joint.dia.Element) to the center of the paper scroller viewport.

paperScroller.centerElement(element);
paperScroller.centerContent(element, { padding: { left: 100 }});
positionContent(positionName [, opt])

Align paper content inside the paper scroller viewport as specified by positionName. There are 9 possible positionName values: 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left', 'top-left', 'center'. An appropriate point of the paper content area is positioned to the corresponding point inside the paper scroller viewport. For example, if positionName was 'bottom-left', the bottom-left corner of paper content area is positioned to the bottom-left corner of the paper scroller viewport (and inside requested paper scroller paddings, if provided).

paperScroller.positionContent('bottom-left');
paperScroller.centerContent('bottom-left', { padding: { horizontal: 20, vertical: 10 }});
positionElement(element, positionName [, opt])

Align element (an instance of joint.dia.Element) inside the paper scroller viewport as specified by positionName (detailed above). An appropriate point of the element bbox is positioned to the corresponding point inside the paper scroller viewport, similar to positionContent method.

paperScroller.positionElement(element, 'right');
paperScroller.centerContent('right', { padding: { right: 50 }});
positionRect(rect, positionName [, opt])

Align a custom rect inside the paper scroller viewport as specified by positionName (detailed above). An appropriate point of the rectangle is positioned to the corresponding point inside the paper scroller viewport, similar to positionContent method.

paperScroller.positionElement(element1.getBBox().union(element2.getBBox()), 'left-top');
paperScroller.centerContent(path.bbox(), 'right-top', { padding: { vertical: 10, right: 50 }});
positionPoint(point, x, y [, opt])

Position a custom point on the paper (in local coordinates) to the point (x,y) inside the paper scroller viewport (in client coordinates). This is useful when you need complete control over what to position where. Percentage values are allowed for x and y; the percentages are relative to the paperScroller area (only the area inside padding, if padding is provided). Negative values/percentages cause the algrithm to look from opposite edge (i.e. from right/bottom).

The following example positions the origin of the paper to the origin (top-left point) of the paper scroller viewport:

paperScroller.positionPoint(new g.Point(0,0), 0, 0);
paperScroller.positionPoint(new g.Point(0,0), '-100%', '-100%'); // same result

The following example positions point to the point 25% of the way between the right and the left edge of the paper scroller viewport and 40px above the bottom edge:

paperScroller.positionPoint(point, '25%', -40);

The following example positions point to the point 30px away from right edge of the paper scroller viewport (10px + 20px padding) and 20px above bottom edge (10px + 10px padding):

paperScroller.positionPoint(point, 10, -10, { padding: { horizontal: 20, vertical: 10 }})

Paper visiblity

ui.PaperScroller provides methods for checking whether elements or points are visible and not scrolled outside the viewport.

getVisibleArea() Returns the size and origin of the current paperScroller viewport. The returned value is a g.Rect object, which contains x, y, width and height describing the visible area of the paper ignoring paper transformations (as would no scale, translate or rotate be applied).
// return elements currenly visible in the paperScroller viewport.
var visibleElements = graph.findModelsInArea(paperScroller.getVisibleArea());
isElementVisible(element [, opt]) Returns true if the element is visible in the current paperScroller viewport. It returns false otherwise. Use { strict: [Boolean] } option for toggling complete true vs. partial false visibility. It's non-strict by default.
// Scroll to an element only if it is not completely in the viewport.
if (!paperScroller.isElementVisible(rect, { strict: true })) {
    paperScroller.scrollToElement(rect);
}
isPointVisible(point) Returns true if the point (e.g. { x: 100, y: 200 }) is visible in the current paperScroller viewport. It returns false otherwise.

Zooming the paper

The PaperScroller exposes an API to scale the contained paper more easily:

paperScroller.zoom();

Return the current zoom level of the paper. Technically speaking, the zoom level is the scaling factor (sx) of the paper's SVGElement.

paperScroller.zoom(value [, opt]);

The zoom() method accepts a value by which we want to increase/decrease the scaling factor of the paper. A positive value increases the scaling factor, which causes the paper to be zoomed in. A negative value decreases the scaling factor, which causes the paper to be zoomed out.

You can pass two options to cap the range of possible scaling factors as part of an opt object - min and max. The scaling factor cannot then be decreased below min (if provided) or increased above max (if provided). These options are often useful in practice, with UI buttons bound to the paperScroller's zooming operations:

$('#zoom-in').on('click', function() {
   paperScroller.zoom(0.2, { max: 4 });
});

$('#zoom-out').on('click', function() {
   paperScroller.zoom(-0.2, { min: 0.2 });
});

Options of ox and oy are also available to transform the origin of the zoom. By default, the origin is the center of the viewport.

paper.on('blank:mousewheel', (evt, ox, oy, delta) => {
  evt.preventDefault();
  paperScroller.zoom(delta * 0.2, { min: 0.4, max: 3, grid: 0.2, ox, oy });
});

Additionally, passing a value for the grid option causes the resulting scaling factor to be rounded to the closest multiple of the grid value.

Passing absolute: true as an option sets the scaling factor directly to the provided value (instead of treating it as a relative value). The constraints imposed by the min, max, and grid are still in effect, however.

paperScroller.zoomToFit([opt]);

Scale the paper so that all of the contents of its graph fit into the user's viewport. For a list of available options, refer to paper.scaleContentToFit() documentation.

paperScroller.zoomToRect(rect [, opt]);

Zoom and reposition the paper so that the area defined by rect (in the paper local coordinates) fits into the user's viewport. For a list of available options, refer to paper.scaleContentToFit() documentation.

Animated transitions

The paperScroller provides few methods for panning and zooming in animated fashion. These are:

paperScroller.transitionToPoint(x, y [, opt])
paperScroller.transitionToPoint(point [, opt])

The method pans and optionally zooms the paperScroller to a given point. It accepts a point defined in the paper local coordinate system and an option object with several properties to control the transition.

scale The zoom level to reach at the end of the transition. It defaults to the current paperScroller zoom level.
duration A string representing the number of seconds or milliseconds a transition animation should take to complete. E.g. '500ms', '2s'. It defaults to '1s'
delay A string representing the amount of time to wait before a transition starts. It defaults to '0s'
timingFunction The option to describe how the intermediate pan positions and zoom values are calculated. For all available values visit CSS transition-timing-function property documentation. It defaults to 'ease'.
onTransitionEnd A callback function fired with transitionend event as its first parameter when the transition ends.
// Move paper point (x=100, y=100) to the center of the viewport.
paperScroller.transitionToPoint(100, 100);
// Move and scale paper, so the point { x: 100, y: 100 } will appear in the center of the viewport.
paperScroller.transitionToPoint(100, 100, { scale: 2 });
// Move the the center of the `element` to the center of the viewport.
paperScroller.transitionToPoint(element.getBBox().center(), {
    duration: '500ms',
    onTransitionEnd: function(transitionEvent) {
      // do something when the animation ends
    }
});
paperScroller.transitionToRect(rect [, opt]);

The method pans and zooms the paperScroller over a specific period so the given rectangular area is at the end of the transition entirely visible in the viewport. The rectangular area is an object with x, y, width and height properties and defined in the paper local coordinate system. The method accepts all options from transitionToPoint() (but scale) and the following options from the table below.

center An object with x and y properties, which defines the point to be in the center of the viewport when the transition ends. By default the center of the rectangular area is used.
visibility A number to change the percentage coverage of the viewport. The rectangular area covers 100% of the viewport by default (i.e. { visibility: 1 }). For instance 80% coverage could be set with { visibility: .8 }
minScale A number to make the resulting scale always greater than or equal to minScale value.
maxScale A number to make the resulting scale always less than or equal to maxScale value.
scaleGrid A number to make the resulting scale the largest multiple of scaleGrid value less than or equal to the calculated scale.
// Zoom the paperScroller so the given area covers 90% of the viewport.
// Also do not let the paperScroller exceed the zoom level 3.
paperScroller.transitionToRect({
  x: 100,
  y: 100
  width: 200,
  height: 200
}, {
    visibility: 0.9,
    maxScale: 3,
    onTransitionEnd: function(transitionEvent) {
      // do something when the animation ends
    }
});
// Zoom the paperScroller in a way the elements `el1`, `el2`, `el3` cover the entire viewport
// and the center of the `el1` moves to the center of the viewport.
var rect = graph.getCellsBBox([el1, el2, el3]);
paperScroller.transitionToRect(rect, {
    duration: '500ms',
    center: el1.getBBox().center()
});
paperScroller.removeTransition();

When method called, the ongoing (if any) paperScroller transition is removed and the associated onTransitionEnd callback is not fired.

Paper auto-resize/shrink

The PaperScroller constructor takes an optional parameter autoResizePaper. When this parameter is set to true, the PaperScroller automatically resizes the paper so that it fits the content inside it. This makes it possible to have a fixed (even smaller) size of the paper and when the user drags an element outside this area, the PaperScroller extends this paper in order to accommodate for the new element. By default, the ui.PaperScroller resizes the paper by the paper width/height. If you want the paper to extend by a different amount, pass the baseWidth/baseHeight options to the ui.PaperScroller constructor function. The paper adjustment is internally handled by calculating proper parameters for the joint.dia.Paper:fitToContent method. You can further adjust the behavior by setting the contentOptions object to the ui.PaperScroller constructor function. This object can contain additional parameters that will be mixed with the calculated ones before calling the joint.dia.Paper:fitToContent method. This is handy if you, for example, want to set the maximum width and height of the paper. In this case, you can just set contentOptions: { maxWidth: 3000, maxHeight: 3000 } to the ui.PaperScroller constructor function. contentOptions can be also defined as a function returning the options.

new joint.ui.PaperScroller({
  padding: 0,
  contentOptions: function(paperScroller) {
    var visibleArea = paperScroller.getVisibleArea();
    return {
        padding: {
            bottom: visibleArea.height / 2,
            top: visibleArea.height / 2,
            left: visibleArea.width / 2,
            right: visibleArea.width / 2
        },
        allowNewOrigin: 'any'
    };
  }
});

Try how this works in the following demo by zooming the paper out so that you see its borders and move your element outside the paper area. You should see how the paper gets automatically resized. When you drag the element back to its original location, the PaperScroller resizes the paper back to its original size.

var paperScroller = new joint.ui.PaperScroller({ autoResizePaper: true });

Events

The plugin fires the following events.

Event Name Handler Signature Description
pan:start (evt) Triggered when the user starts panning.
pan:stop (evt) Triggered when the user stops panning.
scroll (evt) Triggered when the paper has been scrolled.
paperScroller.on('scroll', function(evt) {
  console.log('The visible area has been changed:', this.getVisibleArea());
});