Skip to content

Gestures enhancer

Add swipe, pan, tap and pinch gestures to the reader with @prose-reader/enhancer-gestures, configure them, and react to each gesture through gestures$.

This enhancer implements common gesture patterns using https://github.com/mbret/gesturx. Tap on edge or swipe to turn pages, pinch to change font scale, etc. It implements the most common gestures while trying to stay generic and configurable.

Terminal window
npm install @prose-reader/enhancer-gestures @prose-reader/core gesturx rxjs

All three are peer dependencies. The enhancer runs against the reader your app creates, so it shares that reader's @prose-reader/core rather than bringing its own. It needs the core it was released with or a later one of the same major, gesturx 2 (^2.0.0), and rxjs 7.5.5 or any later 7.x (^7.5.5).

Connect the enhancer to your reader:

import { gesturesEnhancer } from "@prose-reader/enhancer-gestures"
const createAppReader = gesturesEnhancer(createReader)
/**
* There is no required configuration for this
* enhancer.
*/
const reader = createAppReader({ manifest })

Gesture recognitions are done relative to the reader container by default. This is to ensure consistency in behaviors no matter how your reader is being integrated.

For example, we detect taps on edge of screen to navigate pages. If we were using window object as reference, tapping on the side of the book would not work in this situation:

A reader in the middle of a wider page: the edge of the book, where a tap turns the page, is far from the edge of the window

In this situation the user is using the reader in only a certain part of the web page and therefore the gestures should work relative to the reader container.

  • Swipe to turn pages
  • Pan to turn pages
  • Tap to turn pages

Gestures start on a press of the primary button: a left click, a finger or a pen's tip. A right click, which opens the context menu, and a middle click start none: they tap, pan and swipe nothing, turn no page and are not reported on gestures$.

  • Pinch to start zooming
  • Pan to move zoom position (When zooming)
  • Pinch to start changing font size. This will take priority over zooming for reflowable books and whenever the user does not start pinching on an image.

The API is under the gestures namespace.

type Options = {
panNavigation?: "pan" | "swipe" | false
fontScalePinchEnabled?: boolean
fontScaleMaxScale?: number
fontScaleMinScale?: number
fontScalePinchThrottleTime?: number
zoomMaxScale?: number
pinchCancelPan?: boolean
/**
* List of selectors that will be tested to ignore gestures.
* eg: preventing gestures on a bookmark button.
*/
ignore?: string[]
}

Read or update the enhancer settings.

type PageContext = undefined | {
spineItem: SpineItem
spineItemPageIndex: number
spineItemPagePosition: UnsafeSpineItemPagePosition
pageSize: {
width: number
height: number
}
}
type Hook = {
/**
* Use this hook to control whether a gesture should be recognized or not.
* eg: You can check whether a gesture is on a bookmark button and cancel it.
*/
name: "beforeTapGesture"
runFn: (
params: { event$: Observable<{ event: GestureEvent; page: PageContext }> }
) => Observable<boolean>
}
Observable<{
type: "tap";
gestureEvent: TapEvent;
handled: boolean;
} | {
type: "pan";
gestureEvent: PanEvent;
} | {
type: "swipe";
gestureEvent: SwipeEvent;
} | {
type: "pinch";
gestureEvent: PinchEvent;
}>

Emits all gesture events that are recognized and processed by the enhancer. It will attach a boolean to let you know whether the gesture has been handled or not. Typically whether an action happened following this gesture (like a passthrough).

It completes when the reader is destroyed.

Imagine you want to show/hide a menu when the user clicks on the screen. The enhancer will trigger a navigation when the user taps on the left or right edge so you likely don't want to show the menu at the moment. However clicking outside of this range will have no effect. This is where you can intercept the gesture and show your menu.

/**
* Here we have a tap event that does not trigger any effects.
* We can fallback to trigger the menu.
*/
reader?.gestures.gestures$.pipe(
tap((event) => {
if (event.type === "tap" && !event.handled) {
toggleMenu()
}
})
)

Basically if you want to do something based on a gesture and make sure it does not happen with another effect this observable is a good candidate.