Skip to content

Commit 3c1e5ce

Browse files
authored
Merge pull request #301 from embedpdf/feature/pinch-scroll-zoom
Feature/pinch scroll zoom
2 parents 11a22c2 + ca2a260 commit 3c1e5ce

53 files changed

Lines changed: 1455 additions & 446 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@embedpdf/plugin-viewport': minor
3+
---
4+
5+
## Viewport Element Context
6+
7+
Added a React context to share the viewport DOM element reference with child components.
8+
9+
### New Features
10+
11+
- **ViewportElementContext**: New context that provides access to the viewport container element
12+
- **useViewportElement hook**: Hook to consume the viewport element reference from context
13+
14+
This allows child components (like `ZoomGestureWrapper`) to access the viewport container element without DOM traversal, enabling gesture events to work anywhere within the viewport area.
15+
16+
### Usage
17+
18+
```tsx
19+
import { useViewportElement } from '@embedpdf/plugin-viewport/react';
20+
21+
function MyComponent() {
22+
const viewportRef = useViewportElement();
23+
// viewportRef.current is the viewport container element
24+
}
25+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@embedpdf/plugin-zoom': major
3+
---
4+
5+
## ZoomGestureWrapper (formerly PinchWrapper)
6+
7+
Renamed `PinchWrapper` to `ZoomGestureWrapper` and added wheel zoom support alongside pinch-to-zoom.
8+
9+
### Breaking Changes
10+
11+
- **Renamed Component**: `PinchWrapper``ZoomGestureWrapper`
12+
- **Renamed Hook**: `usePinch``useZoomGesture`
13+
- **Removed Hammer.js dependency**: Gesture handling is now implemented natively
14+
15+
### New Features
16+
17+
- **Wheel zoom**: Ctrl/Cmd + scroll wheel now zooms the document
18+
- **Configurable gestures**: New props to enable/disable individual gesture types:
19+
- `enablePinch` (default: `true`) - Enable/disable pinch-to-zoom
20+
- `enableWheel` (default: `true`) - Enable/disable wheel zoom
21+
- **Improved performance**: Uses `useLayoutEffect` to prevent flashing during zoom operations
22+
- **Simplified internals**: Uses direct DOM measurements instead of plugin metrics
23+
24+
### Migration
25+
26+
```diff
27+
- import { PinchWrapper } from '@embedpdf/plugin-zoom/react';
28+
+ import { ZoomGestureWrapper } from '@embedpdf/plugin-zoom/react';
29+
30+
- <PinchWrapper documentId={documentId}>
31+
+ <ZoomGestureWrapper documentId={documentId}>
32+
<Scroller ... />
33+
- </PinchWrapper>
34+
+ </ZoomGestureWrapper>
35+
```
36+
37+
To disable a specific gesture:
38+
39+
```tsx
40+
<ZoomGestureWrapper
41+
documentId={documentId}
42+
enablePinch={false} // Disable pinch-to-zoom
43+
enableWheel={true} // Keep wheel zoom
44+
>
45+
```

examples/react-tailwind/src/pages/viewer-schema.tsx

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ import {
1313
GlobalPointerProvider,
1414
PagePointerProvider,
1515
} from '@embedpdf/plugin-interaction-manager/react';
16-
import { ZoomMode, ZoomPluginPackage, MarqueeZoom } from '@embedpdf/plugin-zoom/react';
16+
import {
17+
ZoomMode,
18+
ZoomPluginPackage,
19+
MarqueeZoom,
20+
ZoomGestureWrapper,
21+
} from '@embedpdf/plugin-zoom/react';
1722
import { PanPluginPackage } from '@embedpdf/plugin-pan/react';
1823
import { SpreadMode, SpreadPluginPackage } from '@embedpdf/plugin-spread/react';
1924
import { Rotate, RotatePluginPackage } from '@embedpdf/plugin-rotate/react';
@@ -254,48 +259,50 @@ function ViewerLayout({ documentId }: { documentId: string }) {
254259
<div className="relative h-full w-full">
255260
<GlobalPointerProvider documentId={documentId}>
256261
<Viewport className="bg-gray-100" documentId={documentId}>
257-
<Scroller
258-
documentId={documentId}
259-
renderPage={({ pageIndex }) => (
260-
<Rotate
261-
documentId={documentId}
262-
pageIndex={pageIndex}
263-
style={{ backgroundColor: '#fff' }}
264-
>
265-
<PagePointerProvider documentId={documentId} pageIndex={pageIndex}>
266-
<RenderLayer
267-
documentId={documentId}
268-
pageIndex={pageIndex}
269-
scale={1}
270-
style={{ pointerEvents: 'none' }}
271-
/>
272-
<TilingLayer
273-
documentId={documentId}
274-
pageIndex={pageIndex}
275-
style={{ pointerEvents: 'none' }}
276-
/>
277-
<SearchLayer documentId={documentId} pageIndex={pageIndex} />
278-
<MarqueeZoom documentId={documentId} pageIndex={pageIndex} />
279-
<MarqueeCapture documentId={documentId} pageIndex={pageIndex} />
280-
<SelectionLayer
281-
documentId={documentId}
282-
pageIndex={pageIndex}
283-
selectionMenu={selectionMenu}
284-
/>
285-
<RedactionLayer
286-
documentId={documentId}
287-
pageIndex={pageIndex}
288-
selectionMenu={redactionMenu}
289-
/>
290-
<AnnotationLayer
291-
documentId={documentId}
292-
pageIndex={pageIndex}
293-
selectionMenu={annotationMenu}
294-
/>
295-
</PagePointerProvider>
296-
</Rotate>
297-
)}
298-
/>
262+
<ZoomGestureWrapper documentId={documentId}>
263+
<Scroller
264+
documentId={documentId}
265+
renderPage={({ pageIndex }) => (
266+
<Rotate
267+
documentId={documentId}
268+
pageIndex={pageIndex}
269+
style={{ backgroundColor: '#fff' }}
270+
>
271+
<PagePointerProvider documentId={documentId} pageIndex={pageIndex}>
272+
<RenderLayer
273+
documentId={documentId}
274+
pageIndex={pageIndex}
275+
scale={1}
276+
style={{ pointerEvents: 'none' }}
277+
/>
278+
<TilingLayer
279+
documentId={documentId}
280+
pageIndex={pageIndex}
281+
style={{ pointerEvents: 'none' }}
282+
/>
283+
<SearchLayer documentId={documentId} pageIndex={pageIndex} />
284+
<MarqueeZoom documentId={documentId} pageIndex={pageIndex} />
285+
<MarqueeCapture documentId={documentId} pageIndex={pageIndex} />
286+
<SelectionLayer
287+
documentId={documentId}
288+
pageIndex={pageIndex}
289+
selectionMenu={selectionMenu}
290+
/>
291+
<RedactionLayer
292+
documentId={documentId}
293+
pageIndex={pageIndex}
294+
selectionMenu={redactionMenu}
295+
/>
296+
<AnnotationLayer
297+
documentId={documentId}
298+
pageIndex={pageIndex}
299+
selectionMenu={annotationMenu}
300+
/>
301+
</PagePointerProvider>
302+
</Rotate>
303+
)}
304+
/>
305+
</ZoomGestureWrapper>
299306
{/* Page Controls */}
300307
<PageControls documentId={documentId} />
301308
</Viewport>

examples/svelte-tailwind/src/routes/viewer-schema/+page.svelte

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
GlobalPointerProvider,
1515
PagePointerProvider,
1616
} from '@embedpdf/plugin-interaction-manager/svelte';
17-
import { ZoomMode, ZoomPluginPackage, MarqueeZoom } from '@embedpdf/plugin-zoom/svelte';
17+
import {
18+
ZoomMode,
19+
ZoomPluginPackage,
20+
MarqueeZoom,
21+
ZoomGestureWrapper,
22+
} from '@embedpdf/plugin-zoom/svelte';
1823
import { RedactionLayer, RedactionPluginPackage } from '@embedpdf/plugin-redaction/svelte';
1924
import { PanPluginPackage } from '@embedpdf/plugin-pan/svelte';
2025
import { SpreadMode, SpreadPluginPackage } from '@embedpdf/plugin-spread/svelte';
@@ -263,47 +268,49 @@
263268
<div class="relative h-full w-full">
264269
<GlobalPointerProvider {documentId}>
265270
<Viewport class="bg-gray-100" {documentId}>
266-
<Scroller {documentId}>
267-
{#snippet renderPage(page)}
268-
<Rotate
269-
{documentId}
270-
pageIndex={page.pageIndex}
271-
style="background-color: #fff"
272-
>
273-
<PagePointerProvider {documentId} pageIndex={page.pageIndex}>
274-
<RenderLayer
275-
{documentId}
276-
pageIndex={page.pageIndex}
277-
scale={1}
278-
style="pointer-events: none"
279-
/>
280-
<TilingLayer
281-
{documentId}
282-
pageIndex={page.pageIndex}
283-
style="pointer-events: none"
284-
/>
285-
<SearchLayer {documentId} pageIndex={page.pageIndex} />
286-
<MarqueeZoom {documentId} pageIndex={page.pageIndex} />
287-
<MarqueeCapture {documentId} pageIndex={page.pageIndex} />
288-
<RedactionLayer
289-
{documentId}
290-
pageIndex={page.pageIndex}
291-
selectionMenu={redactionMenu.renderFn}
292-
/>
293-
<SelectionLayer
294-
{documentId}
295-
pageIndex={page.pageIndex}
296-
selectionMenu={selectionMenu.renderFn}
297-
/>
298-
<AnnotationLayer
299-
{documentId}
300-
pageIndex={page.pageIndex}
301-
selectionMenu={annotationMenu.renderFn}
302-
/>
303-
</PagePointerProvider>
304-
</Rotate>
305-
{/snippet}
306-
</Scroller>
271+
<ZoomGestureWrapper {documentId}>
272+
<Scroller {documentId}>
273+
{#snippet renderPage(page)}
274+
<Rotate
275+
{documentId}
276+
pageIndex={page.pageIndex}
277+
style="background-color: #fff"
278+
>
279+
<PagePointerProvider {documentId} pageIndex={page.pageIndex}>
280+
<RenderLayer
281+
{documentId}
282+
pageIndex={page.pageIndex}
283+
scale={1}
284+
style="pointer-events: none"
285+
/>
286+
<TilingLayer
287+
{documentId}
288+
pageIndex={page.pageIndex}
289+
style="pointer-events: none"
290+
/>
291+
<SearchLayer {documentId} pageIndex={page.pageIndex} />
292+
<MarqueeZoom {documentId} pageIndex={page.pageIndex} />
293+
<MarqueeCapture {documentId} pageIndex={page.pageIndex} />
294+
<RedactionLayer
295+
{documentId}
296+
pageIndex={page.pageIndex}
297+
selectionMenu={redactionMenu.renderFn}
298+
/>
299+
<SelectionLayer
300+
{documentId}
301+
pageIndex={page.pageIndex}
302+
selectionMenu={selectionMenu.renderFn}
303+
/>
304+
<AnnotationLayer
305+
{documentId}
306+
pageIndex={page.pageIndex}
307+
selectionMenu={annotationMenu.renderFn}
308+
/>
309+
</PagePointerProvider>
310+
</Rotate>
311+
{/snippet}
312+
</Scroller>
313+
</ZoomGestureWrapper>
307314
<!-- Page Controls -->
308315
<PageControls {documentId} />
309316
</Viewport>

examples/vue-tailwind/src/components/ViewerSchemaLayout.vue

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,45 +21,47 @@
2121
<div v-else-if="isLoaded" class="relative h-full w-full">
2222
<GlobalPointerProvider :documentId="documentId">
2323
<Viewport class="bg-gray-100" :documentId="documentId">
24-
<Scroller :documentId="documentId" v-slot="{ page }">
25-
<Rotate
26-
:documentId="documentId"
27-
:pageIndex="page.pageIndex"
28-
style="background-color: #fff"
29-
>
30-
<PagePointerProvider :documentId="documentId" :pageIndex="page.pageIndex">
31-
<RenderLayer
32-
:documentId="documentId"
33-
:pageIndex="page.pageIndex"
34-
:scale="1"
35-
style="pointer-events: none"
36-
/>
37-
<TilingLayer
38-
:documentId="documentId"
39-
:pageIndex="page.pageIndex"
40-
style="pointer-events: none"
41-
/>
42-
<SearchLayer :documentId="documentId" :pageIndex="page.pageIndex" />
43-
<MarqueeZoom :documentId="documentId" :pageIndex="page.pageIndex" />
44-
<MarqueeCapture :documentId="documentId" :pageIndex="page.pageIndex" />
45-
<SelectionLayer
46-
:documentId="documentId"
47-
:pageIndex="page.pageIndex"
48-
:selectionMenu="selectionMenu"
49-
/>
50-
<RedactionLayer
51-
:documentId="documentId"
52-
:pageIndex="page.pageIndex"
53-
:selectionMenu="redactionMenu"
54-
/>
55-
<AnnotationLayer
56-
:documentId="documentId"
57-
:pageIndex="page.pageIndex"
58-
:selectionMenu="annotationMenu"
59-
/>
60-
</PagePointerProvider>
61-
</Rotate>
62-
</Scroller>
24+
<ZoomGestureWrapper :documentId="documentId">
25+
<Scroller :documentId="documentId" v-slot="{ page }">
26+
<Rotate
27+
:documentId="documentId"
28+
:pageIndex="page.pageIndex"
29+
style="background-color: #fff"
30+
>
31+
<PagePointerProvider :documentId="documentId" :pageIndex="page.pageIndex">
32+
<RenderLayer
33+
:documentId="documentId"
34+
:pageIndex="page.pageIndex"
35+
:scale="1"
36+
style="pointer-events: none"
37+
/>
38+
<TilingLayer
39+
:documentId="documentId"
40+
:pageIndex="page.pageIndex"
41+
style="pointer-events: none"
42+
/>
43+
<SearchLayer :documentId="documentId" :pageIndex="page.pageIndex" />
44+
<MarqueeZoom :documentId="documentId" :pageIndex="page.pageIndex" />
45+
<MarqueeCapture :documentId="documentId" :pageIndex="page.pageIndex" />
46+
<SelectionLayer
47+
:documentId="documentId"
48+
:pageIndex="page.pageIndex"
49+
:selectionMenu="selectionMenu"
50+
/>
51+
<RedactionLayer
52+
:documentId="documentId"
53+
:pageIndex="page.pageIndex"
54+
:selectionMenu="redactionMenu"
55+
/>
56+
<AnnotationLayer
57+
:documentId="documentId"
58+
:pageIndex="page.pageIndex"
59+
:selectionMenu="annotationMenu"
60+
/>
61+
</PagePointerProvider>
62+
</Rotate>
63+
</Scroller>
64+
</ZoomGestureWrapper>
6365
<!-- Page Controls -->
6466
<PageControls :documentId="documentId" />
6567
</Viewport>
@@ -87,7 +89,7 @@ import { Rotate } from '@embedpdf/plugin-rotate/vue';
8789
import { RenderLayer } from '@embedpdf/plugin-render/vue';
8890
import { TilingLayer } from '@embedpdf/plugin-tiling/vue';
8991
import { SearchLayer } from '@embedpdf/plugin-search/vue';
90-
import { MarqueeZoom } from '@embedpdf/plugin-zoom/vue';
92+
import { MarqueeZoom, ZoomGestureWrapper } from '@embedpdf/plugin-zoom/vue';
9193
import { MarqueeCapture } from '@embedpdf/plugin-capture/vue';
9294
import { SelectionLayer } from '@embedpdf/plugin-selection/vue';
9395
import { RedactionLayer } from '@embedpdf/plugin-redaction/vue';

packages/plugin-viewport/src/lib/reducer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const initialViewportDocumentState: ViewportDocumentState = {
2626
clientHeight: 0,
2727
scrollWidth: 0,
2828
scrollHeight: 0,
29+
clientLeft: 0,
30+
clientTop: 0,
2931
relativePosition: { x: 0, y: 0 },
3032
},
3133
isScrolling: false,
@@ -142,6 +144,8 @@ export const viewportReducer: Reducer<ViewportState, ViewportAction> = (
142144
clientHeight: metrics.clientHeight,
143145
scrollWidth: metrics.scrollWidth,
144146
scrollHeight: metrics.scrollHeight,
147+
clientLeft: metrics.clientLeft,
148+
clientTop: metrics.clientTop,
145149
relativePosition: {
146150
x:
147151
metrics.scrollWidth <= metrics.clientWidth

0 commit comments

Comments
 (0)