Skip to content

Commit afa1f7b

Browse files
committed
Add and refactor Svelte components for PDF viewer
Introduces new Svelte components for the PDF viewer example, including dialogs, toolbars, icons, navigation, split view, tab bar, and UI elements. Refactors existing components to use documentId props and plugin hooks for improved modularity and state management. Enhances print, capture, pan, search, and page settings functionality with new UI and plugin integration.
1 parent e097f75 commit afa1f7b

86 files changed

Lines changed: 3171 additions & 828 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<script lang="ts">
2+
import { useCapture } from '@embedpdf/plugin-capture/svelte';
3+
import { Dialog, DialogContent, DialogFooter, Button } from './ui';
4+
5+
interface CaptureData {
6+
pageIndex: number;
7+
rect: any;
8+
blob: Blob;
9+
}
10+
11+
interface CaptureDialogProps {
12+
documentId: string;
13+
}
14+
15+
let { documentId }: CaptureDialogProps = $props();
16+
17+
const capturePlugin = useCapture(() => documentId);
18+
19+
let open = $state(false);
20+
let captureData = $state<CaptureData | null>(null);
21+
let previewUrl = $state<string | null>(null);
22+
let downloadUrl = $state<string | null>(null);
23+
let urlRef: string | null = null;
24+
let downloadLinkRef: HTMLAnchorElement | null = null;
25+
26+
const handleClose = () => {
27+
// Clean up object URLs
28+
if (urlRef) {
29+
URL.revokeObjectURL(urlRef);
30+
urlRef = null;
31+
}
32+
if (downloadUrl) {
33+
URL.revokeObjectURL(downloadUrl);
34+
downloadUrl = null;
35+
}
36+
open = false;
37+
captureData = null;
38+
previewUrl = null;
39+
};
40+
41+
const handleDownload = () => {
42+
if (!captureData || !downloadLinkRef) return;
43+
44+
// Create download URL and trigger download
45+
const url = URL.createObjectURL(captureData.blob);
46+
downloadUrl = url;
47+
48+
// Use the ref to trigger download
49+
downloadLinkRef.href = url;
50+
downloadLinkRef.download = `pdf-capture-page-${captureData.pageIndex + 1}.png`;
51+
downloadLinkRef.click();
52+
53+
handleClose();
54+
};
55+
56+
$effect(() => {
57+
if (!capturePlugin.provides) return;
58+
59+
return capturePlugin.provides.onCaptureArea(({ pageIndex, rect, blob }) => {
60+
captureData = { pageIndex, rect, blob };
61+
62+
// Create preview URL
63+
const objectUrl = URL.createObjectURL(blob);
64+
urlRef = objectUrl;
65+
previewUrl = objectUrl;
66+
open = true;
67+
});
68+
});
69+
70+
const handleImageLoad = () => {
71+
// Clean up the object URL after image loads
72+
if (urlRef) {
73+
URL.revokeObjectURL(urlRef);
74+
urlRef = null;
75+
}
76+
};
77+
</script>
78+
79+
<Dialog {open} onClose={handleClose} title="Capture PDF Area">
80+
<DialogContent>
81+
<div class="flex justify-center">
82+
{#if previewUrl}
83+
<img
84+
src={previewUrl}
85+
onload={handleImageLoad}
86+
alt="Captured PDF area"
87+
class="block max-h-[400px] max-w-full rounded border border-gray-200"
88+
/>
89+
{/if}
90+
</div>
91+
</DialogContent>
92+
<DialogFooter>
93+
<Button variant="secondary" onclick={handleClose}>Cancel</Button>
94+
<Button variant="primary" onclick={handleDownload} disabled={!captureData}>Download</Button>
95+
</DialogFooter>
96+
</Dialog>
97+
98+
<!-- Hidden download link -->
99+
<a bind:this={downloadLinkRef} style="display: none" href="" download=""><!-- hidden --></a>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<script lang="ts">
2+
import { useExport } from '@embedpdf/plugin-export/svelte';
3+
import { useCapture } from '@embedpdf/plugin-capture/svelte';
4+
import { useFullscreen } from '@embedpdf/plugin-fullscreen/svelte';
5+
import {
6+
MenuIcon,
7+
PrintIcon,
8+
DownloadIcon,
9+
ScreenshotIcon,
10+
FullscreenIcon,
11+
FullscreenExitIcon,
12+
} from './icons';
13+
import { ToolbarButton, DropdownMenu, DropdownItem } from './ui';
14+
import CaptureDialog from './CaptureDialog.svelte';
15+
import PrintDialog from './PrintDialog.svelte';
16+
17+
interface DocumentMenuProps {
18+
documentId: string;
19+
}
20+
21+
let { documentId }: DocumentMenuProps = $props();
22+
23+
const exportPlugin = useExport(() => documentId);
24+
const capturePlugin = useCapture(() => documentId);
25+
const fullscreenPlugin = useFullscreen();
26+
27+
let isMenuOpen = $state(false);
28+
let isPrintDialogOpen = $state(false);
29+
30+
const handleDownload = () => {
31+
exportPlugin.provides?.download();
32+
isMenuOpen = false;
33+
};
34+
35+
const handlePrint = () => {
36+
isMenuOpen = false;
37+
isPrintDialogOpen = true;
38+
};
39+
40+
const handleScreenshot = () => {
41+
if (capturePlugin.provides) {
42+
capturePlugin.provides.toggleMarqueeCapture();
43+
}
44+
isMenuOpen = false;
45+
};
46+
47+
const handleFullscreen = () => {
48+
fullscreenPlugin.provides?.toggleFullscreen(`#${documentId}`);
49+
isMenuOpen = false;
50+
};
51+
</script>
52+
53+
{#if exportPlugin.provides}
54+
<div class="relative">
55+
<ToolbarButton
56+
onclick={() => (isMenuOpen = !isMenuOpen)}
57+
isActive={isMenuOpen}
58+
aria-label="Document Menu"
59+
title="Document Menu"
60+
>
61+
<MenuIcon class="h-4 w-4" />
62+
</ToolbarButton>
63+
64+
<DropdownMenu isOpen={isMenuOpen} onClose={() => (isMenuOpen = false)} className="w-48">
65+
<DropdownItem
66+
isActive={capturePlugin.state.isMarqueeCaptureActive}
67+
onclick={handleScreenshot}
68+
>
69+
{#snippet icon()}
70+
<ScreenshotIcon class="h-4 w-4" title="Capture Area" />
71+
{/snippet}
72+
Capture Area
73+
</DropdownItem>
74+
<DropdownItem onclick={handlePrint}>
75+
{#snippet icon()}
76+
<PrintIcon class="h-4 w-4" title="Print" />
77+
{/snippet}
78+
Print
79+
</DropdownItem>
80+
<DropdownItem onclick={handleDownload}>
81+
{#snippet icon()}
82+
<DownloadIcon class="h-4 w-4" title="Download" />
83+
{/snippet}
84+
Download
85+
</DropdownItem>
86+
<DropdownItem onclick={handleFullscreen}>
87+
{#snippet icon()}
88+
{#if fullscreenPlugin.state.isFullscreen}
89+
<FullscreenExitIcon class="h-4 w-4" title="Exit Fullscreen" />
90+
{:else}
91+
<FullscreenIcon class="h-4 w-4" title="Fullscreen" />
92+
{/if}
93+
{/snippet}
94+
{fullscreenPlugin.state.isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
95+
</DropdownItem>
96+
</DropdownMenu>
97+
</div>
98+
99+
<!-- Print Dialog -->
100+
<PrintDialog
101+
{documentId}
102+
isOpen={isPrintDialogOpen}
103+
onClose={() => (isPrintDialogOpen = false)}
104+
/>
105+
106+
<!-- Capture Dialog -->
107+
<CaptureDialog {documentId} />
108+
{/if}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<script lang="ts">
2+
import type { DocumentState } from '@embedpdf/core';
3+
import { useDocumentManagerCapability } from '@embedpdf/plugin-document-manager/svelte';
4+
5+
interface DocumentPasswordPromptProps {
6+
documentState: DocumentState | null;
7+
}
8+
9+
let { documentState }: DocumentPasswordPromptProps = $props();
10+
11+
const documentManager = useDocumentManagerCapability();
12+
13+
let password = $state('');
14+
let error = $state('');
15+
16+
const handleSubmit = (e: Event) => {
17+
e.preventDefault();
18+
if (!documentState?.id || !password) return;
19+
20+
documentManager.provides?.providePassword(documentState.id, password);
21+
password = '';
22+
error = '';
23+
};
24+
</script>
25+
26+
<div class="flex h-full items-center justify-center bg-gray-50 p-8">
27+
<div class="w-full max-w-md rounded-lg bg-white p-6 shadow-lg">
28+
<h2 class="mb-4 text-xl font-semibold text-gray-900">Document Password Required</h2>
29+
<p class="mb-4 text-sm text-gray-600">
30+
This document is password protected. Please enter the password to continue.
31+
</p>
32+
33+
<form onsubmit={handleSubmit} class="space-y-4">
34+
<div>
35+
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
36+
<input
37+
id="password"
38+
type="password"
39+
bind:value={password}
40+
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
41+
placeholder="Enter password"
42+
autocomplete="off"
43+
/>
44+
</div>
45+
46+
{#if error}
47+
<div class="rounded-md bg-red-50 p-3 text-sm text-red-700">
48+
{error}
49+
</div>
50+
{/if}
51+
52+
<button
53+
type="submit"
54+
class="w-full rounded-md bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
55+
>
56+
Unlock Document
57+
</button>
58+
</form>
59+
</div>
60+
</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script lang="ts" context="module">
2+
export interface IconProps {
3+
class?: string;
4+
title?: string;
5+
}
6+
</script>
7+
8+
<script lang="ts">
9+
let { class: className, title, children }: IconProps & { children?: any } = $props();
10+
</script>
11+
12+
<svg
13+
class={className}
14+
fill="none"
15+
stroke="currentColor"
16+
viewBox="0 0 24 24"
17+
aria-hidden={!title}
18+
role={title ? 'img' : 'presentation'}
19+
>
20+
{#if title}
21+
<title>{title}</title>
22+
{/if}
23+
{@render children?.()}
24+
</svg>
25+
26+
<!-- Specific Icons as separate files would be better, but for now let's create them inline -->
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts">
2+
interface LoadingSpinnerProps {
3+
size?: 'sm' | 'md' | 'lg';
4+
message?: string;
5+
class?: string;
6+
}
7+
8+
let { size = 'md', message, class: className = '' }: LoadingSpinnerProps = $props();
9+
10+
const sizeClasses = {
11+
sm: 'h-4 w-4',
12+
md: 'h-5 w-5',
13+
lg: 'h-8 w-8',
14+
};
15+
</script>
16+
17+
<div class="flex items-center gap-2 text-gray-600 {className}">
18+
<svg class="{sizeClasses[size]} animate-spin" fill="none" viewBox="0 0 24 24">
19+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
20+
<path
21+
class="opacity-75"
22+
fill="currentColor"
23+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
24+
/>
25+
</svg>
26+
{#if message}
27+
<span>{message}</span>
28+
{/if}
29+
</div>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
// No props or state needed for this component
3+
</script>
4+
5+
<div class="flex items-center justify-between border-b bg-gray-800 px-4 py-2 text-white">
6+
<div class="flex items-center gap-4">
7+
<a href="/" class="rounded px-3 py-1 text-sm font-medium transition-colors hover:bg-gray-700">
8+
← Home
9+
</a>
10+
<span class="text-gray-400">|</span>
11+
<span class="text-sm font-semibold">PDF Viewer</span>
12+
</div>
13+
<div class="text-xs text-gray-400">EmbedPDF Svelte Example</div>
14+
</div>

examples/svelte-tailwind/src/lib/components/PageControls.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
import { useViewportCapability } from '@embedpdf/plugin-viewport/svelte';
33
import { useScroll } from '@embedpdf/plugin-scroll/svelte';
44
5+
interface PageControlsProps {
6+
documentId: string;
7+
}
8+
9+
let { documentId }: PageControlsProps = $props();
10+
511
const viewport = useViewportCapability();
6-
const scroll = useScroll();
12+
const scroll = useScroll(() => documentId);
713
814
let isVisible = $state(false);
915
let isHovering = $state(false);

0 commit comments

Comments
 (0)