svelte-pdf-view 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +13 -0
- package/README.md +293 -0
- package/dist/PdfRenderer.svelte +238 -0
- package/dist/PdfRenderer.svelte.d.ts +21 -0
- package/dist/PdfToolbar.svelte +229 -0
- package/dist/PdfToolbar.svelte.d.ts +3 -0
- package/dist/PdfViewer.svelte +118 -0
- package/dist/PdfViewer.svelte.d.ts +17 -0
- package/dist/PdfViewerInner.svelte +302 -0
- package/dist/PdfViewerInner.svelte.d.ts +11 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/pdf-viewer/EventBus.d.ts +12 -0
- package/dist/pdf-viewer/EventBus.js +42 -0
- package/dist/pdf-viewer/FindController.d.ts +53 -0
- package/dist/pdf-viewer/FindController.js +423 -0
- package/dist/pdf-viewer/PDFPageView.d.ts +58 -0
- package/dist/pdf-viewer/PDFPageView.js +281 -0
- package/dist/pdf-viewer/PDFViewerCore.d.ts +45 -0
- package/dist/pdf-viewer/PDFViewerCore.js +225 -0
- package/dist/pdf-viewer/context.d.ts +31 -0
- package/dist/pdf-viewer/context.js +15 -0
- package/dist/pdf-viewer/renderer-styles.css +203 -0
- package/dist/pdf-viewer/styles.css +281 -0
- package/package.json +88 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
ZoomIn,
|
|
4
|
+
ZoomOut,
|
|
5
|
+
RotateCcw,
|
|
6
|
+
RotateCw,
|
|
7
|
+
Search,
|
|
8
|
+
ChevronLeft,
|
|
9
|
+
ChevronRight
|
|
10
|
+
} from '@lucide/svelte';
|
|
11
|
+
import { getPdfViewerContext } from './pdf-viewer/context.js';
|
|
12
|
+
|
|
13
|
+
const { state: viewerState, actions } = getPdfViewerContext();
|
|
14
|
+
|
|
15
|
+
let searchInput = $state('');
|
|
16
|
+
|
|
17
|
+
function handlePageChange(e: Event) {
|
|
18
|
+
const input = e.target as HTMLInputElement;
|
|
19
|
+
const pageNum = parseInt(input.value, 10);
|
|
20
|
+
if (pageNum >= 1 && pageNum <= viewerState.totalPages) {
|
|
21
|
+
actions.goToPage(pageNum);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function handleSearch() {
|
|
26
|
+
if (!searchInput.trim()) {
|
|
27
|
+
actions.clearSearch();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await actions.search(searchInput);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleSearchKeydown(e: KeyboardEvent) {
|
|
34
|
+
if (e.key === 'Enter') {
|
|
35
|
+
if (e.shiftKey) {
|
|
36
|
+
actions.searchPrevious();
|
|
37
|
+
} else if (viewerState.searchTotal > 0) {
|
|
38
|
+
actions.searchNext();
|
|
39
|
+
} else {
|
|
40
|
+
handleSearch();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div class="pdf-toolbar">
|
|
47
|
+
<!-- Page navigation -->
|
|
48
|
+
<div class="pdf-toolbar-group">
|
|
49
|
+
<input
|
|
50
|
+
type="number"
|
|
51
|
+
value={viewerState.currentPage}
|
|
52
|
+
min="1"
|
|
53
|
+
max={viewerState.totalPages}
|
|
54
|
+
onchange={handlePageChange}
|
|
55
|
+
aria-label="Current page"
|
|
56
|
+
/>
|
|
57
|
+
<span class="page-info">/ {viewerState.totalPages}</span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Zoom controls -->
|
|
61
|
+
<div class="pdf-toolbar-group">
|
|
62
|
+
<button onclick={() => actions.zoomOut()} aria-label="Zoom out" title="Zoom Out">
|
|
63
|
+
<ZoomOut size={18} />
|
|
64
|
+
</button>
|
|
65
|
+
<span class="zoom-level">{Math.round(viewerState.scale * 100)}%</span>
|
|
66
|
+
<button onclick={() => actions.zoomIn()} aria-label="Zoom in" title="Zoom In">
|
|
67
|
+
<ZoomIn size={18} />
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Rotation controls -->
|
|
72
|
+
<div class="pdf-toolbar-group">
|
|
73
|
+
<button
|
|
74
|
+
onclick={() => actions.rotateCounterClockwise()}
|
|
75
|
+
aria-label="Rotate counter-clockwise"
|
|
76
|
+
title="Rotate Left"
|
|
77
|
+
>
|
|
78
|
+
<RotateCcw size={18} />
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
onclick={() => actions.rotateClockwise()}
|
|
82
|
+
aria-label="Rotate clockwise"
|
|
83
|
+
title="Rotate Right"
|
|
84
|
+
>
|
|
85
|
+
<RotateCw size={18} />
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Search -->
|
|
90
|
+
<div class="pdf-toolbar-group">
|
|
91
|
+
<input
|
|
92
|
+
type="text"
|
|
93
|
+
class="search-input"
|
|
94
|
+
placeholder="Search..."
|
|
95
|
+
bind:value={searchInput}
|
|
96
|
+
onkeydown={handleSearchKeydown}
|
|
97
|
+
aria-label="Search in document"
|
|
98
|
+
/>
|
|
99
|
+
<button
|
|
100
|
+
onclick={handleSearch}
|
|
101
|
+
disabled={viewerState.isSearching}
|
|
102
|
+
aria-label="Search"
|
|
103
|
+
title="Search"
|
|
104
|
+
>
|
|
105
|
+
<Search size={18} />
|
|
106
|
+
</button>
|
|
107
|
+
{#if viewerState.searchTotal > 0}
|
|
108
|
+
<button onclick={() => actions.searchPrevious()} aria-label="Previous match" title="Previous">
|
|
109
|
+
<ChevronLeft size={18} />
|
|
110
|
+
</button>
|
|
111
|
+
<button onclick={() => actions.searchNext()} aria-label="Next match" title="Next">
|
|
112
|
+
<ChevronRight size={18} />
|
|
113
|
+
</button>
|
|
114
|
+
<span class="match-info">{viewerState.searchCurrent}/{viewerState.searchTotal}</span>
|
|
115
|
+
{/if}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<style>
|
|
120
|
+
/* Toolbar */
|
|
121
|
+
.pdf-toolbar {
|
|
122
|
+
display: flex;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 1rem;
|
|
126
|
+
padding: 0.625rem 1rem;
|
|
127
|
+
background-color: #ffffff;
|
|
128
|
+
color: #333;
|
|
129
|
+
flex-shrink: 0;
|
|
130
|
+
flex-wrap: wrap;
|
|
131
|
+
border-bottom: 1px solid #e0e0e0;
|
|
132
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.pdf-toolbar-group {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 0.375rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.pdf-toolbar button {
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
width: 32px;
|
|
146
|
+
height: 32px;
|
|
147
|
+
padding: 0;
|
|
148
|
+
border: 1px solid #e0e0e0;
|
|
149
|
+
background-color: #fafafa;
|
|
150
|
+
color: #555;
|
|
151
|
+
border-radius: 6px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
transition: all 0.15s ease;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.pdf-toolbar button:hover:not(:disabled) {
|
|
157
|
+
background-color: #f0f0f0;
|
|
158
|
+
border-color: #d0d0d0;
|
|
159
|
+
color: #333;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.pdf-toolbar button:active:not(:disabled) {
|
|
163
|
+
background-color: #e8e8e8;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.pdf-toolbar button:disabled {
|
|
167
|
+
opacity: 0.4;
|
|
168
|
+
cursor: not-allowed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.pdf-toolbar input[type='text'],
|
|
172
|
+
.pdf-toolbar input[type='number'] {
|
|
173
|
+
height: 28px;
|
|
174
|
+
padding: 0 0.5rem;
|
|
175
|
+
border: 1px solid #e0e0e0;
|
|
176
|
+
border-radius: 6px;
|
|
177
|
+
background-color: #fff;
|
|
178
|
+
color: #333;
|
|
179
|
+
font-size: 0.8rem;
|
|
180
|
+
outline: none;
|
|
181
|
+
transition:
|
|
182
|
+
border-color 0.15s,
|
|
183
|
+
box-shadow 0.15s;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.pdf-toolbar input[type='text']:focus,
|
|
187
|
+
.pdf-toolbar input[type='number']:focus {
|
|
188
|
+
border-color: #0066cc;
|
|
189
|
+
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.15);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.pdf-toolbar input[type='number'] {
|
|
193
|
+
width: 40px;
|
|
194
|
+
text-align: center;
|
|
195
|
+
appearance: textfield;
|
|
196
|
+
-moz-appearance: textfield;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.pdf-toolbar input[type='number']::-webkit-outer-spin-button,
|
|
200
|
+
.pdf-toolbar input[type='number']::-webkit-inner-spin-button {
|
|
201
|
+
-webkit-appearance: none;
|
|
202
|
+
margin: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.pdf-toolbar .search-input {
|
|
206
|
+
width: 160px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.pdf-toolbar .zoom-level {
|
|
210
|
+
min-width: 48px;
|
|
211
|
+
text-align: center;
|
|
212
|
+
font-size: 0.8rem;
|
|
213
|
+
color: #666;
|
|
214
|
+
font-weight: 500;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.pdf-toolbar .page-info {
|
|
218
|
+
font-size: 0.8rem;
|
|
219
|
+
color: #888;
|
|
220
|
+
margin-left: 0.25rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.pdf-toolbar .match-info {
|
|
224
|
+
font-size: 0.75rem;
|
|
225
|
+
color: #888;
|
|
226
|
+
min-width: 60px;
|
|
227
|
+
margin-left: 0.25rem;
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// Export compound components
|
|
3
|
+
export { default as Toolbar } from './PdfToolbar.svelte';
|
|
4
|
+
export { default as Renderer } from './PdfRenderer.svelte';
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { Snippet } from 'svelte';
|
|
9
|
+
import {
|
|
10
|
+
setPdfViewerContext,
|
|
11
|
+
type PdfViewerState,
|
|
12
|
+
type PdfViewerActions
|
|
13
|
+
} from './pdf-viewer/context.js';
|
|
14
|
+
import type { PdfSource } from './PdfRenderer.svelte';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** PDF source - URL string, ArrayBuffer, Uint8Array, or Blob */
|
|
18
|
+
src: PdfSource;
|
|
19
|
+
/** Initial scale (default: 1.0) */
|
|
20
|
+
scale?: number;
|
|
21
|
+
/** CSS class for the container */
|
|
22
|
+
class?: string;
|
|
23
|
+
/** Children (toolbar and renderer) */
|
|
24
|
+
children?: Snippet;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { src, scale: initialScale = 1.0, class: className = '', children }: Props = $props();
|
|
28
|
+
|
|
29
|
+
// Reactive state that will be shared via context
|
|
30
|
+
let state = $state<PdfViewerState>({
|
|
31
|
+
loading: true,
|
|
32
|
+
error: null,
|
|
33
|
+
totalPages: 0,
|
|
34
|
+
currentPage: 1,
|
|
35
|
+
scale: initialScale,
|
|
36
|
+
rotation: 0,
|
|
37
|
+
searchQuery: '',
|
|
38
|
+
searchCurrent: 0,
|
|
39
|
+
searchTotal: 0,
|
|
40
|
+
isSearching: false
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Renderer actions - will be populated when renderer mounts
|
|
44
|
+
let rendererActions: PdfViewerActions | null = null;
|
|
45
|
+
|
|
46
|
+
// Actions that proxy to the renderer
|
|
47
|
+
const actions: PdfViewerActions = {
|
|
48
|
+
zoomIn: () => rendererActions?.zoomIn(),
|
|
49
|
+
zoomOut: () => rendererActions?.zoomOut(),
|
|
50
|
+
setScale: (scale: number) => rendererActions?.setScale(scale),
|
|
51
|
+
rotateClockwise: () => rendererActions?.rotateClockwise(),
|
|
52
|
+
rotateCounterClockwise: () => rendererActions?.rotateCounterClockwise(),
|
|
53
|
+
goToPage: (page: number) => rendererActions?.goToPage(page),
|
|
54
|
+
search: async (query: string) => {
|
|
55
|
+
if (rendererActions) {
|
|
56
|
+
await rendererActions.search(query);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
searchNext: () => rendererActions?.searchNext(),
|
|
60
|
+
searchPrevious: () => rendererActions?.searchPrevious(),
|
|
61
|
+
clearSearch: () => rendererActions?.clearSearch()
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Set up context
|
|
65
|
+
setPdfViewerContext({
|
|
66
|
+
state,
|
|
67
|
+
actions,
|
|
68
|
+
_registerRenderer: (renderer: PdfViewerActions) => {
|
|
69
|
+
rendererActions = renderer;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<div class="pdf-viewer-container {className}">
|
|
75
|
+
{#if state.loading}
|
|
76
|
+
<div class="pdf-loading">Loading PDF...</div>
|
|
77
|
+
{:else if state.error}
|
|
78
|
+
<div class="pdf-error">Error: {state.error}</div>
|
|
79
|
+
{/if}
|
|
80
|
+
|
|
81
|
+
{#if children}
|
|
82
|
+
{@render children()}
|
|
83
|
+
{:else}
|
|
84
|
+
<!-- Default layout if no children provided -->
|
|
85
|
+
{#await import('./PdfToolbar.svelte') then { default: Toolbar }}
|
|
86
|
+
<Toolbar />
|
|
87
|
+
{/await}
|
|
88
|
+
{#await import('./PdfRenderer.svelte') then { default: Renderer }}
|
|
89
|
+
<Renderer {src} />
|
|
90
|
+
{/await}
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<style>
|
|
95
|
+
.pdf-viewer-container {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
background-color: #f0f0f0;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.pdf-loading,
|
|
105
|
+
.pdf-error {
|
|
106
|
+
position: absolute;
|
|
107
|
+
top: 50%;
|
|
108
|
+
left: 50%;
|
|
109
|
+
transform: translate(-50%, -50%);
|
|
110
|
+
color: #666;
|
|
111
|
+
font-size: 1rem;
|
|
112
|
+
z-index: 10;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.pdf-error {
|
|
116
|
+
color: #dc3545;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default as Toolbar } from './PdfToolbar.svelte';
|
|
2
|
+
export { default as Renderer } from './PdfRenderer.svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { PdfSource } from './PdfRenderer.svelte';
|
|
5
|
+
interface Props {
|
|
6
|
+
/** PDF source - URL string, ArrayBuffer, Uint8Array, or Blob */
|
|
7
|
+
src: PdfSource;
|
|
8
|
+
/** Initial scale (default: 1.0) */
|
|
9
|
+
scale?: number;
|
|
10
|
+
/** CSS class for the container */
|
|
11
|
+
class?: string;
|
|
12
|
+
/** Children (toolbar and renderer) */
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
}
|
|
15
|
+
declare const PdfViewer: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type PdfViewer = ReturnType<typeof PdfViewer>;
|
|
17
|
+
export default PdfViewer;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
const browser = typeof window !== 'undefined';
|
|
5
|
+
import {
|
|
6
|
+
ZoomIn,
|
|
7
|
+
ZoomOut,
|
|
8
|
+
RotateCcw,
|
|
9
|
+
RotateCw,
|
|
10
|
+
Search,
|
|
11
|
+
ChevronLeft,
|
|
12
|
+
ChevronRight
|
|
13
|
+
} from '@lucide/svelte';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** URL or path to the PDF file */
|
|
17
|
+
src: string;
|
|
18
|
+
/** Initial scale (default: 1.0) */
|
|
19
|
+
scale?: number;
|
|
20
|
+
/** CSS class for the container */
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let { src, scale: initialScale = 1.0, class: className = '' }: Props = $props();
|
|
25
|
+
|
|
26
|
+
let scrollContainerEl: HTMLDivElement | undefined = $state();
|
|
27
|
+
let mounted = $state(false);
|
|
28
|
+
|
|
29
|
+
// Viewer state
|
|
30
|
+
let loading = $state(true);
|
|
31
|
+
let error = $state<string | null>(null);
|
|
32
|
+
let currentScale = $state(initialScale);
|
|
33
|
+
let currentRotation = $state(0);
|
|
34
|
+
let currentPage = $state(1);
|
|
35
|
+
let totalPages = $state(0);
|
|
36
|
+
|
|
37
|
+
// Search state
|
|
38
|
+
let searchQuery = $state('');
|
|
39
|
+
let searchCurrent = $state(0);
|
|
40
|
+
let searchTotal = $state(0);
|
|
41
|
+
let isSearching = $state(false);
|
|
42
|
+
|
|
43
|
+
// Core instances (loaded dynamically)
|
|
44
|
+
let viewer: import('./pdf-viewer/PDFViewerCore.js').PDFViewerCore | null = null;
|
|
45
|
+
let findController: import('./pdf-viewer/FindController.js').FindController | null = null;
|
|
46
|
+
let pdfjsLib: typeof import('pdfjs-dist') | null = null;
|
|
47
|
+
|
|
48
|
+
async function initPdfJs() {
|
|
49
|
+
if (!browser) return null;
|
|
50
|
+
|
|
51
|
+
pdfjsLib = await import('pdfjs-dist');
|
|
52
|
+
const pdfjsWorker = await import('pdfjs-dist/build/pdf.worker.min.mjs?url');
|
|
53
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker.default;
|
|
54
|
+
|
|
55
|
+
return pdfjsLib;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function loadPdf(url: string) {
|
|
59
|
+
if (!browser || !scrollContainerEl) return;
|
|
60
|
+
|
|
61
|
+
loading = true;
|
|
62
|
+
error = null;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Initialize PDF.js
|
|
66
|
+
const pdfjs = await initPdfJs();
|
|
67
|
+
if (!pdfjs) return;
|
|
68
|
+
|
|
69
|
+
// Initialize viewer
|
|
70
|
+
const { PDFViewerCore } = await import('./pdf-viewer/PDFViewerCore.js');
|
|
71
|
+
const { FindController } = await import('./pdf-viewer/FindController.js');
|
|
72
|
+
const { EventBus } = await import('./pdf-viewer/EventBus.js');
|
|
73
|
+
|
|
74
|
+
// Cleanup existing viewer
|
|
75
|
+
if (viewer) {
|
|
76
|
+
viewer.destroy();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const eventBus = new EventBus();
|
|
80
|
+
|
|
81
|
+
const newViewer = new PDFViewerCore({
|
|
82
|
+
container: scrollContainerEl,
|
|
83
|
+
eventBus,
|
|
84
|
+
initialScale: currentScale,
|
|
85
|
+
initialRotation: currentRotation
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
findController = new FindController(newViewer, eventBus);
|
|
89
|
+
|
|
90
|
+
// Setup event listeners
|
|
91
|
+
eventBus.on('scalechanged', (data: Record<string, unknown>) => {
|
|
92
|
+
currentScale = data.scale as number;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
eventBus.on('rotationchanged', (data: Record<string, unknown>) => {
|
|
96
|
+
currentRotation = data.rotation as number;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
eventBus.on('updateviewarea', (data: Record<string, unknown>) => {
|
|
100
|
+
const location = data.location as { pageNumber: number };
|
|
101
|
+
currentPage = location.pageNumber;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
eventBus.on('pagesloaded', (data: Record<string, unknown>) => {
|
|
105
|
+
totalPages = data.pagesCount as number;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
eventBus.on('updatefindmatchescount', (data: Record<string, unknown>) => {
|
|
109
|
+
const matchesCount = data.matchesCount as { current: number; total: number };
|
|
110
|
+
searchCurrent = matchesCount.current;
|
|
111
|
+
searchTotal = matchesCount.total;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Load document
|
|
115
|
+
const loadingTask = pdfjs.getDocument(url);
|
|
116
|
+
const pdfDocument = await loadingTask.promise;
|
|
117
|
+
|
|
118
|
+
await newViewer.setDocument(pdfDocument);
|
|
119
|
+
|
|
120
|
+
// Set document on find controller for text extraction
|
|
121
|
+
findController.setDocument(pdfDocument);
|
|
122
|
+
|
|
123
|
+
viewer = newViewer;
|
|
124
|
+
|
|
125
|
+
loading = false;
|
|
126
|
+
} catch (e) {
|
|
127
|
+
error = e instanceof Error ? e.message : 'Failed to load PDF';
|
|
128
|
+
loading = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function handleZoomIn() {
|
|
133
|
+
if (viewer) {
|
|
134
|
+
viewer.zoomIn();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleZoomOut() {
|
|
139
|
+
if (viewer) {
|
|
140
|
+
viewer.zoomOut();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleRotateRight() {
|
|
145
|
+
if (viewer) {
|
|
146
|
+
viewer.rotateClockwise();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function handleRotateLeft() {
|
|
151
|
+
if (viewer) {
|
|
152
|
+
viewer.rotateCounterClockwise();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function handlePageChange(e: Event) {
|
|
157
|
+
const input = e.target as HTMLInputElement;
|
|
158
|
+
const pageNum = parseInt(input.value, 10);
|
|
159
|
+
if (viewer && pageNum >= 1 && pageNum <= totalPages) {
|
|
160
|
+
viewer.scrollToPage(pageNum);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function handleSearch() {
|
|
165
|
+
if (!findController || !searchQuery.trim()) {
|
|
166
|
+
searchCurrent = 0;
|
|
167
|
+
searchTotal = 0;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
isSearching = true;
|
|
172
|
+
await findController.find({
|
|
173
|
+
query: searchQuery,
|
|
174
|
+
highlightAll: true,
|
|
175
|
+
caseSensitive: false
|
|
176
|
+
});
|
|
177
|
+
isSearching = false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function handleSearchNext() {
|
|
181
|
+
if (findController) {
|
|
182
|
+
findController.findNext();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function handleSearchPrev() {
|
|
187
|
+
if (findController) {
|
|
188
|
+
findController.findPrevious();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function handleSearchKeydown(e: KeyboardEvent) {
|
|
193
|
+
if (e.key === 'Enter') {
|
|
194
|
+
if (e.shiftKey) {
|
|
195
|
+
handleSearchPrev();
|
|
196
|
+
} else if (searchTotal > 0) {
|
|
197
|
+
handleSearchNext();
|
|
198
|
+
} else {
|
|
199
|
+
handleSearch();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Load PDF when src changes
|
|
205
|
+
$effect(() => {
|
|
206
|
+
if (browser && src && scrollContainerEl && mounted) {
|
|
207
|
+
loadPdf(src);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
onMount(() => {
|
|
212
|
+
mounted = true;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
onDestroy(() => {
|
|
216
|
+
if (viewer) {
|
|
217
|
+
viewer.destroy();
|
|
218
|
+
viewer = null;
|
|
219
|
+
}
|
|
220
|
+
findController = null;
|
|
221
|
+
});
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<div class="pdf-viewer-container {className}">
|
|
225
|
+
{#if loading}
|
|
226
|
+
<div class="pdf-loading">
|
|
227
|
+
<span>Loading PDF...</span>
|
|
228
|
+
</div>
|
|
229
|
+
{:else if error}
|
|
230
|
+
<div class="pdf-error">
|
|
231
|
+
<span>Error: {error}</span>
|
|
232
|
+
</div>
|
|
233
|
+
{:else}
|
|
234
|
+
<!-- Toolbar -->
|
|
235
|
+
<div class="pdf-toolbar">
|
|
236
|
+
<!-- Page navigation -->
|
|
237
|
+
<div class="pdf-toolbar-group">
|
|
238
|
+
<input
|
|
239
|
+
type="number"
|
|
240
|
+
value={currentPage}
|
|
241
|
+
min="1"
|
|
242
|
+
max={totalPages}
|
|
243
|
+
onchange={handlePageChange}
|
|
244
|
+
aria-label="Current page"
|
|
245
|
+
/>
|
|
246
|
+
<span class="page-info">/ {totalPages}</span>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- Zoom controls -->
|
|
250
|
+
<div class="pdf-toolbar-group">
|
|
251
|
+
<button onclick={handleZoomOut} aria-label="Zoom out" title="Zoom Out"
|
|
252
|
+
><ZoomOut size={18} /></button
|
|
253
|
+
>
|
|
254
|
+
<span class="zoom-level">{Math.round(currentScale * 100)}%</span>
|
|
255
|
+
<button onclick={handleZoomIn} aria-label="Zoom in" title="Zoom In"
|
|
256
|
+
><ZoomIn size={18} /></button
|
|
257
|
+
>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<!-- Rotation controls -->
|
|
261
|
+
<div class="pdf-toolbar-group">
|
|
262
|
+
<button
|
|
263
|
+
onclick={handleRotateLeft}
|
|
264
|
+
aria-label="Rotate counter-clockwise"
|
|
265
|
+
title="Rotate Left"
|
|
266
|
+
>
|
|
267
|
+
<RotateCcw size={18} />
|
|
268
|
+
</button>
|
|
269
|
+
<button onclick={handleRotateRight} aria-label="Rotate clockwise" title="Rotate Right">
|
|
270
|
+
<RotateCw size={18} />
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<!-- Search -->
|
|
275
|
+
<div class="pdf-toolbar-group">
|
|
276
|
+
<input
|
|
277
|
+
type="text"
|
|
278
|
+
class="search-input"
|
|
279
|
+
placeholder="Search..."
|
|
280
|
+
bind:value={searchQuery}
|
|
281
|
+
onkeydown={handleSearchKeydown}
|
|
282
|
+
aria-label="Search in document"
|
|
283
|
+
/>
|
|
284
|
+
<button onclick={handleSearch} disabled={isSearching} aria-label="Search" title="Search">
|
|
285
|
+
<Search size={18} />
|
|
286
|
+
</button>
|
|
287
|
+
{#if searchTotal > 0}
|
|
288
|
+
<button onclick={handleSearchPrev} aria-label="Previous match" title="Previous">
|
|
289
|
+
<ChevronLeft size={18} />
|
|
290
|
+
</button>
|
|
291
|
+
<button onclick={handleSearchNext} aria-label="Next match" title="Next">
|
|
292
|
+
<ChevronRight size={18} />
|
|
293
|
+
</button>
|
|
294
|
+
<span class="match-info">{searchCurrent}/{searchTotal}</span>
|
|
295
|
+
{/if}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
{/if}
|
|
299
|
+
|
|
300
|
+
<!-- PDF scroll container -->
|
|
301
|
+
<div class="pdf-scroll-container" bind:this={scrollContainerEl}></div>
|
|
302
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** URL or path to the PDF file */
|
|
3
|
+
src: string;
|
|
4
|
+
/** Initial scale (default: 1.0) */
|
|
5
|
+
scale?: number;
|
|
6
|
+
/** CSS class for the container */
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const PdfViewerInner: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type PdfViewerInner = ReturnType<typeof PdfViewerInner>;
|
|
11
|
+
export default PdfViewerInner;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { default as PdfViewer, Toolbar as PdfToolbar, Renderer as PdfRenderer } from './PdfViewer.svelte';
|
|
2
|
+
export type { PdfSource } from './PdfRenderer.svelte';
|
|
3
|
+
export { getPdfViewerContext, type PdfViewerState, type PdfViewerActions, type PdfViewerContext } from './pdf-viewer/context.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple event bus for PDF viewer component communication.
|
|
3
|
+
* This is a derivative work based on PDF.js event_utils.js
|
|
4
|
+
*/
|
|
5
|
+
export declare class EventBus {
|
|
6
|
+
private listeners;
|
|
7
|
+
on(eventName: string, listener: EventListener): void;
|
|
8
|
+
off(eventName: string, listener: EventListener): void;
|
|
9
|
+
dispatch(eventName: string, data?: Record<string, unknown>): void;
|
|
10
|
+
destroy(): void;
|
|
11
|
+
}
|
|
12
|
+
export type EventListener = (data: Record<string, unknown>) => void;
|