svelte-pdf-view 0.1.13 → 0.2.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.
@@ -7,6 +7,7 @@
7
7
  type PdfSource
8
8
  } from './pdf-viewer/context.js';
9
9
  import { getPdfJs } from './pdf-viewer/pdfjs-singleton.js';
10
+ import { PdfPresentationMode } from './pdf-viewer/PdfPresentationMode.js';
10
11
  import { rendererStyles } from './pdf-viewer/renderer-styles.js';
11
12
 
12
13
  interface Props {
@@ -51,6 +52,17 @@
51
52
  let viewer: import('./pdf-viewer/PDFViewerCore.js').PDFViewerCore | null = null;
52
53
  let findController: import('./pdf-viewer/FindController.js').FindController | null = null;
53
54
 
55
+ // Presentation mode
56
+ const presentationMode = new PdfPresentationMode({
57
+ onStateChange: (newState) => {
58
+ viewerState.presentationMode = newState;
59
+ },
60
+ onPageChange: (pageNumber) => {
61
+ // Sync page number back to main viewer when changed in presentation mode
62
+ viewer?.scrollToPage(pageNumber);
63
+ }
64
+ });
65
+
54
66
  async function loadPdf(source: PdfSource) {
55
67
  if (!BROWSER || !scrollContainerEl) return;
56
68
 
@@ -127,10 +139,13 @@
127
139
  }
128
140
 
129
141
  const loadingTask = pdfjs.getDocument(documentSource);
130
- const pdfDocument = await loadingTask.promise;
142
+ const loadedPdfDocument = await loadingTask.promise;
131
143
 
132
- await newViewer.setDocument(pdfDocument);
133
- findController.setDocument(pdfDocument);
144
+ await newViewer.setDocument(loadedPdfDocument);
145
+ findController.setDocument(loadedPdfDocument);
146
+
147
+ // Set document on presentation mode
148
+ presentationMode.setDocument(loadedPdfDocument);
134
149
 
135
150
  viewer = newViewer;
136
151
  viewerState.loading = false;
@@ -174,7 +189,14 @@
174
189
  viewerState.searchTotal = 0;
175
190
  }
176
191
  },
177
- download: async () => {} // Download is handled by PdfViewer, not renderer
192
+ download: async () => {}, // Download is handled by PdfViewer, not renderer
193
+ enterPresentationMode: async () => {
194
+ presentationMode.setCurrentPage(viewerState.currentPage);
195
+ return presentationMode.request();
196
+ },
197
+ exitPresentationMode: async () => {
198
+ await presentationMode.exit();
199
+ }
178
200
  };
179
201
 
180
202
  onMount(async () => {
@@ -225,6 +247,7 @@
225
247
  viewer = null;
226
248
  }
227
249
  findController = null;
250
+ presentationMode.destroy();
228
251
  // Note: Worker is a global singleton, not cleaned up per-component
229
252
  // Use destroyPdfJs() from pdfjs-singleton.js if you need to fully cleanup
230
253
  });
@@ -7,7 +7,8 @@
7
7
  Search,
8
8
  ChevronLeft,
9
9
  ChevronRight,
10
- Download
10
+ Download,
11
+ Presentation
11
12
  } from '@lucide/svelte';
12
13
  import { getPdfViewerContext } from './pdf-viewer/context.js';
13
14
 
@@ -116,8 +117,15 @@
116
117
  {/if}
117
118
  </div>
118
119
 
119
- <!-- Download -->
120
+ <!-- Presentation & Download -->
120
121
  <div class="pdf-toolbar-group">
122
+ <button
123
+ onclick={() => actions.enterPresentationMode()}
124
+ aria-label="Presentation Mode"
125
+ title="Presentation Mode"
126
+ >
127
+ <Presentation size={18} />
128
+ </button>
121
129
  <button onclick={() => actions.download()} aria-label="Download PDF" title="Download">
122
130
  <Download size={18} />
123
131
  </button>
@@ -8,6 +8,7 @@
8
8
  import type { Snippet } from 'svelte';
9
9
  import {
10
10
  setPdfViewerContext,
11
+ PresentationModeState,
11
12
  type PdfViewerState,
12
13
  type PdfViewerActions,
13
14
  type PdfSource
@@ -52,7 +53,8 @@
52
53
  searchQuery: '',
53
54
  searchCurrent: 0,
54
55
  searchTotal: 0,
55
- isSearching: false
56
+ isSearching: false,
57
+ presentationMode: PresentationModeState.NORMAL
56
58
  });
57
59
 
58
60
  // Renderer actions - will be populated when renderer mounts
@@ -116,7 +118,18 @@
116
118
  searchNext: () => rendererActions?.searchNext(),
117
119
  searchPrevious: () => rendererActions?.searchPrevious(),
118
120
  clearSearch: () => rendererActions?.clearSearch(),
119
- download: downloadPdf
121
+ download: downloadPdf,
122
+ enterPresentationMode: async () => {
123
+ if (rendererActions) {
124
+ return rendererActions.enterPresentationMode();
125
+ }
126
+ return false;
127
+ },
128
+ exitPresentationMode: async () => {
129
+ if (rendererActions) {
130
+ await rendererActions.exitPresentationMode();
131
+ }
132
+ }
120
133
  };
121
134
 
122
135
  // Set up context
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { default as PdfViewer, Toolbar as PdfToolbar, Renderer as PdfRenderer } from './PdfViewer.svelte';
2
2
  export type { PdfSource } from './pdf-viewer/context.js';
3
- export { getPdfViewerContext, type PdfViewerState, type PdfViewerActions, type PdfViewerContext } from './pdf-viewer/context.js';
3
+ export { getPdfViewerContext, PresentationModeState, type PdfViewerState, type PdfViewerActions, type PdfViewerContext } from './pdf-viewer/context.js';
4
4
  export { destroyPdfJs } from './pdf-viewer/pdfjs-singleton.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Reexport your entry components here
2
2
  export { default as PdfViewer, Toolbar as PdfToolbar, Renderer as PdfRenderer } from './PdfViewer.svelte';
3
3
  // Export context for custom toolbars
4
- export { getPdfViewerContext } from './pdf-viewer/context.js';
4
+ export { getPdfViewerContext, PresentationModeState } from './pdf-viewer/context.js';
5
5
  // Export PDF.js singleton utilities
6
6
  export { destroyPdfJs } from './pdf-viewer/pdfjs-singleton.js';
@@ -0,0 +1,96 @@
1
+ /**
2
+ * PdfPresentationMode - Fullscreen presentation mode for PDF viewing.
3
+ * This is a derivative work based on PDF.js pdf_presentation_mode.js
4
+ *
5
+ * Features:
6
+ * - Browser fullscreen mode
7
+ * - Black background
8
+ * - Single page view scaled to fit screen
9
+ * - No text layer or toolbar
10
+ * - Mouse/keyboard/touch navigation
11
+ */
12
+ import type { PDFDocumentProxy } from 'pdfjs-dist/legacy/build/pdf.mjs';
13
+ export declare enum PresentationModeState {
14
+ UNKNOWN = 0,
15
+ NORMAL = 1,
16
+ CHANGING = 2,
17
+ FULLSCREEN = 3
18
+ }
19
+ export interface PresentationModeCallbacks {
20
+ onStateChange?: (state: PresentationModeState) => void;
21
+ onPageChange?: (pageNumber: number) => void;
22
+ }
23
+ export declare class PdfPresentationMode {
24
+ private state;
25
+ private pdfDocument;
26
+ private currentPageNumber;
27
+ private totalPages;
28
+ private container;
29
+ private canvas;
30
+ private callbacks;
31
+ private fullscreenChangeAbortController;
32
+ private windowAbortController;
33
+ private mouseScrollTimeStamp;
34
+ private mouseScrollDelta;
35
+ private touchSwipeState;
36
+ private renderingPage;
37
+ constructor(callbacks?: PresentationModeCallbacks);
38
+ /**
39
+ * Set the PDF document for presentation
40
+ */
41
+ setDocument(pdfDocument: PDFDocumentProxy | null): void;
42
+ /**
43
+ * Set the current page number (used when entering presentation mode)
44
+ */
45
+ setCurrentPage(pageNumber: number): void;
46
+ /**
47
+ * Check if presentation mode is active
48
+ */
49
+ get active(): boolean;
50
+ /**
51
+ * Get current state
52
+ */
53
+ get currentState(): PresentationModeState;
54
+ /**
55
+ * Request entering fullscreen presentation mode
56
+ */
57
+ request(): Promise<boolean>;
58
+ /**
59
+ * Exit presentation mode
60
+ */
61
+ exit(): Promise<void>;
62
+ /**
63
+ * Go to next page
64
+ */
65
+ nextPage(): boolean;
66
+ /**
67
+ * Go to previous page
68
+ */
69
+ previousPage(): boolean;
70
+ /**
71
+ * Go to a specific page
72
+ */
73
+ goToPage(pageNumber: number): boolean;
74
+ /**
75
+ * Destroy and cleanup
76
+ */
77
+ destroy(): void;
78
+ private createPresentationContainer;
79
+ private destroyPresentationContainer;
80
+ private renderCurrentPage;
81
+ private notifyStateChange;
82
+ private enter;
83
+ private doExit;
84
+ private handleMouseWheel;
85
+ private normalizeWheelDelta;
86
+ private handleMouseDown;
87
+ private handleKeyDown;
88
+ private handleContextMenu;
89
+ private handleTouchSwipe;
90
+ private handleResize;
91
+ private resetMouseScrollState;
92
+ private addWindowListeners;
93
+ private removeWindowListeners;
94
+ private addFullscreenChangeListeners;
95
+ private removeFullscreenChangeListeners;
96
+ }
@@ -0,0 +1,437 @@
1
+ /* Copyright 2024 Mozilla Foundation
2
+ *
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ */
15
+ export var PresentationModeState;
16
+ (function (PresentationModeState) {
17
+ PresentationModeState[PresentationModeState["UNKNOWN"] = 0] = "UNKNOWN";
18
+ PresentationModeState[PresentationModeState["NORMAL"] = 1] = "NORMAL";
19
+ PresentationModeState[PresentationModeState["CHANGING"] = 2] = "CHANGING";
20
+ PresentationModeState[PresentationModeState["FULLSCREEN"] = 3] = "FULLSCREEN";
21
+ })(PresentationModeState || (PresentationModeState = {}));
22
+ const MOUSE_SCROLL_COOLDOWN_TIME = 50; // in ms
23
+ const PAGE_SWITCH_THRESHOLD = 0.1;
24
+ const SWIPE_MIN_DISTANCE_THRESHOLD = 50;
25
+ const SWIPE_ANGLE_THRESHOLD = Math.PI / 6;
26
+ export class PdfPresentationMode {
27
+ state = PresentationModeState.UNKNOWN;
28
+ pdfDocument = null;
29
+ currentPageNumber = 1;
30
+ totalPages = 0;
31
+ container = null;
32
+ canvas = null;
33
+ callbacks;
34
+ fullscreenChangeAbortController = null;
35
+ windowAbortController = null;
36
+ mouseScrollTimeStamp = 0;
37
+ mouseScrollDelta = 0;
38
+ touchSwipeState = null;
39
+ renderingPage = false;
40
+ constructor(callbacks = {}) {
41
+ this.callbacks = callbacks;
42
+ }
43
+ /**
44
+ * Set the PDF document for presentation
45
+ */
46
+ setDocument(pdfDocument) {
47
+ this.pdfDocument = pdfDocument;
48
+ this.totalPages = pdfDocument?.numPages ?? 0;
49
+ }
50
+ /**
51
+ * Set the current page number (used when entering presentation mode)
52
+ */
53
+ setCurrentPage(pageNumber) {
54
+ this.currentPageNumber = Math.max(1, Math.min(pageNumber, this.totalPages));
55
+ }
56
+ /**
57
+ * Check if presentation mode is active
58
+ */
59
+ get active() {
60
+ return (this.state === PresentationModeState.CHANGING ||
61
+ this.state === PresentationModeState.FULLSCREEN);
62
+ }
63
+ /**
64
+ * Get current state
65
+ */
66
+ get currentState() {
67
+ return this.state;
68
+ }
69
+ /**
70
+ * Request entering fullscreen presentation mode
71
+ */
72
+ async request() {
73
+ if (this.active || !this.pdfDocument || this.totalPages === 0) {
74
+ return false;
75
+ }
76
+ // Create the presentation container
77
+ this.createPresentationContainer();
78
+ if (!this.container) {
79
+ return false;
80
+ }
81
+ this.addFullscreenChangeListeners();
82
+ this.notifyStateChange(PresentationModeState.CHANGING);
83
+ try {
84
+ await this.container.requestFullscreen();
85
+ return true;
86
+ }
87
+ catch {
88
+ this.removeFullscreenChangeListeners();
89
+ this.notifyStateChange(PresentationModeState.NORMAL);
90
+ this.destroyPresentationContainer();
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * Exit presentation mode
96
+ */
97
+ async exit() {
98
+ if (!this.active) {
99
+ return;
100
+ }
101
+ if (document.fullscreenElement) {
102
+ await document.exitFullscreen();
103
+ }
104
+ }
105
+ /**
106
+ * Go to next page
107
+ */
108
+ nextPage() {
109
+ if (this.currentPageNumber >= this.totalPages) {
110
+ return false;
111
+ }
112
+ this.currentPageNumber++;
113
+ this.renderCurrentPage();
114
+ this.callbacks.onPageChange?.(this.currentPageNumber);
115
+ return true;
116
+ }
117
+ /**
118
+ * Go to previous page
119
+ */
120
+ previousPage() {
121
+ if (this.currentPageNumber <= 1) {
122
+ return false;
123
+ }
124
+ this.currentPageNumber--;
125
+ this.renderCurrentPage();
126
+ this.callbacks.onPageChange?.(this.currentPageNumber);
127
+ return true;
128
+ }
129
+ /**
130
+ * Go to a specific page
131
+ */
132
+ goToPage(pageNumber) {
133
+ if (pageNumber < 1 || pageNumber > this.totalPages) {
134
+ return false;
135
+ }
136
+ this.currentPageNumber = pageNumber;
137
+ this.renderCurrentPage();
138
+ this.callbacks.onPageChange?.(this.currentPageNumber);
139
+ return true;
140
+ }
141
+ /**
142
+ * Destroy and cleanup
143
+ */
144
+ destroy() {
145
+ this.exit();
146
+ this.removeWindowListeners();
147
+ this.removeFullscreenChangeListeners();
148
+ this.destroyPresentationContainer();
149
+ }
150
+ // Private methods
151
+ createPresentationContainer() {
152
+ // Create fullscreen container
153
+ this.container = document.createElement('div');
154
+ this.container.className = 'pdf-presentation-mode';
155
+ this.container.style.cssText = `
156
+ position: fixed;
157
+ top: 0;
158
+ left: 0;
159
+ width: 100%;
160
+ height: 100%;
161
+ background-color: #000;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ z-index: 999999;
166
+ `;
167
+ // Create canvas for rendering
168
+ this.canvas = document.createElement('canvas');
169
+ this.canvas.style.cssText = `
170
+ max-width: 100%;
171
+ max-height: 100%;
172
+ object-fit: contain;
173
+ `;
174
+ this.container.appendChild(this.canvas);
175
+ document.body.appendChild(this.container);
176
+ }
177
+ destroyPresentationContainer() {
178
+ if (this.container) {
179
+ this.container.remove();
180
+ this.container = null;
181
+ this.canvas = null;
182
+ }
183
+ }
184
+ async renderCurrentPage() {
185
+ if (!this.pdfDocument || !this.canvas || !this.container || this.renderingPage) {
186
+ return;
187
+ }
188
+ this.renderingPage = true;
189
+ try {
190
+ const page = await this.pdfDocument.getPage(this.currentPageNumber);
191
+ // Calculate scale to fit the screen while maintaining aspect ratio
192
+ const containerWidth = window.innerWidth;
193
+ const containerHeight = window.innerHeight;
194
+ const viewport = page.getViewport({ scale: 1, rotation: 0 });
195
+ const pageWidth = viewport.width;
196
+ const pageHeight = viewport.height;
197
+ // Calculate scale to fit
198
+ const scaleX = containerWidth / pageWidth;
199
+ const scaleY = containerHeight / pageHeight;
200
+ const scale = Math.min(scaleX, scaleY);
201
+ const scaledViewport = page.getViewport({ scale, rotation: 0 });
202
+ // Set canvas size
203
+ this.canvas.width = scaledViewport.width;
204
+ this.canvas.height = scaledViewport.height;
205
+ const context = this.canvas.getContext('2d');
206
+ if (!context) {
207
+ return;
208
+ }
209
+ // Clear and render
210
+ context.fillStyle = '#fff';
211
+ context.fillRect(0, 0, this.canvas.width, this.canvas.height);
212
+ await page.render({
213
+ canvasContext: context,
214
+ viewport: scaledViewport,
215
+ canvas: this.canvas
216
+ }).promise;
217
+ }
218
+ catch (e) {
219
+ console.error('Failed to render presentation page:', e);
220
+ }
221
+ finally {
222
+ this.renderingPage = false;
223
+ }
224
+ }
225
+ notifyStateChange(newState) {
226
+ this.state = newState;
227
+ this.callbacks.onStateChange?.(newState);
228
+ }
229
+ enter() {
230
+ this.notifyStateChange(PresentationModeState.FULLSCREEN);
231
+ this.addWindowListeners();
232
+ this.renderCurrentPage();
233
+ // Clear any text selection
234
+ document.getSelection()?.empty();
235
+ }
236
+ doExit() {
237
+ this.removeWindowListeners();
238
+ this.destroyPresentationContainer();
239
+ this.resetMouseScrollState();
240
+ this.removeFullscreenChangeListeners();
241
+ this.notifyStateChange(PresentationModeState.NORMAL);
242
+ }
243
+ handleMouseWheel = (evt) => {
244
+ if (!this.active) {
245
+ return;
246
+ }
247
+ evt.preventDefault();
248
+ const delta = this.normalizeWheelDelta(evt);
249
+ const currentTime = Date.now();
250
+ const storedTime = this.mouseScrollTimeStamp;
251
+ // Cooldown to prevent accidental double-switching
252
+ if (currentTime > storedTime && currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME) {
253
+ return;
254
+ }
255
+ // Reset if direction changed
256
+ if ((this.mouseScrollDelta > 0 && delta < 0) || (this.mouseScrollDelta < 0 && delta > 0)) {
257
+ this.resetMouseScrollState();
258
+ }
259
+ this.mouseScrollDelta += delta;
260
+ if (Math.abs(this.mouseScrollDelta) >= PAGE_SWITCH_THRESHOLD) {
261
+ const totalDelta = this.mouseScrollDelta;
262
+ this.resetMouseScrollState();
263
+ const success = totalDelta > 0 ? this.previousPage() : this.nextPage();
264
+ if (success) {
265
+ this.mouseScrollTimeStamp = currentTime;
266
+ }
267
+ }
268
+ };
269
+ normalizeWheelDelta(evt) {
270
+ let delta = Math.hypot(evt.deltaX, evt.deltaY);
271
+ const angle = Math.atan2(evt.deltaY, evt.deltaX);
272
+ if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
273
+ delta = -delta;
274
+ }
275
+ if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
276
+ delta *= 30;
277
+ }
278
+ else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
279
+ delta *= 30 * 10;
280
+ }
281
+ return delta / 30;
282
+ }
283
+ handleMouseDown = (evt) => {
284
+ // Left click (0) = next page, Right click (2) = previous page
285
+ if (evt.button === 0) {
286
+ evt.preventDefault();
287
+ if (evt.shiftKey) {
288
+ this.previousPage();
289
+ }
290
+ else {
291
+ this.nextPage();
292
+ }
293
+ }
294
+ else if (evt.button === 2) {
295
+ evt.preventDefault();
296
+ this.previousPage();
297
+ }
298
+ };
299
+ handleKeyDown = (evt) => {
300
+ if (!this.active) {
301
+ return;
302
+ }
303
+ this.resetMouseScrollState();
304
+ switch (evt.key) {
305
+ case 'ArrowRight':
306
+ case 'ArrowDown':
307
+ case ' ':
308
+ case 'PageDown':
309
+ case 'Enter':
310
+ evt.preventDefault();
311
+ this.nextPage();
312
+ break;
313
+ case 'ArrowLeft':
314
+ case 'ArrowUp':
315
+ case 'PageUp':
316
+ case 'Backspace':
317
+ evt.preventDefault();
318
+ this.previousPage();
319
+ break;
320
+ case 'Home':
321
+ evt.preventDefault();
322
+ this.goToPage(1);
323
+ break;
324
+ case 'End':
325
+ evt.preventDefault();
326
+ this.goToPage(this.totalPages);
327
+ break;
328
+ case 'Escape':
329
+ // Escape is handled by the browser for fullscreen exit
330
+ break;
331
+ }
332
+ };
333
+ handleContextMenu = (evt) => {
334
+ // Prevent context menu - right-click is handled in handleMouseDown
335
+ evt.preventDefault();
336
+ };
337
+ handleTouchSwipe = (evt) => {
338
+ if (!this.active) {
339
+ return;
340
+ }
341
+ if (evt.touches.length > 1) {
342
+ this.touchSwipeState = null;
343
+ return;
344
+ }
345
+ switch (evt.type) {
346
+ case 'touchstart':
347
+ this.touchSwipeState = {
348
+ startX: evt.touches[0].pageX,
349
+ startY: evt.touches[0].pageY,
350
+ endX: evt.touches[0].pageX,
351
+ endY: evt.touches[0].pageY
352
+ };
353
+ break;
354
+ case 'touchmove':
355
+ if (this.touchSwipeState === null) {
356
+ return;
357
+ }
358
+ this.touchSwipeState.endX = evt.touches[0].pageX;
359
+ this.touchSwipeState.endY = evt.touches[0].pageY;
360
+ evt.preventDefault();
361
+ break;
362
+ case 'touchend': {
363
+ if (this.touchSwipeState === null) {
364
+ return;
365
+ }
366
+ const dx = this.touchSwipeState.endX - this.touchSwipeState.startX;
367
+ const dy = this.touchSwipeState.endY - this.touchSwipeState.startY;
368
+ const absAngle = Math.abs(Math.atan2(dy, dx));
369
+ let delta = 0;
370
+ if (Math.abs(dx) > SWIPE_MIN_DISTANCE_THRESHOLD &&
371
+ (absAngle <= SWIPE_ANGLE_THRESHOLD || absAngle >= Math.PI - SWIPE_ANGLE_THRESHOLD)) {
372
+ // Horizontal swipe
373
+ delta = dx;
374
+ }
375
+ else if (Math.abs(dy) > SWIPE_MIN_DISTANCE_THRESHOLD &&
376
+ Math.abs(absAngle - Math.PI / 2) <= SWIPE_ANGLE_THRESHOLD) {
377
+ // Vertical swipe
378
+ delta = dy;
379
+ }
380
+ if (delta > 0) {
381
+ this.previousPage();
382
+ }
383
+ else if (delta < 0) {
384
+ this.nextPage();
385
+ }
386
+ this.touchSwipeState = null;
387
+ break;
388
+ }
389
+ }
390
+ };
391
+ handleResize = () => {
392
+ if (this.active) {
393
+ this.renderCurrentPage();
394
+ }
395
+ };
396
+ resetMouseScrollState() {
397
+ this.mouseScrollTimeStamp = 0;
398
+ this.mouseScrollDelta = 0;
399
+ }
400
+ addWindowListeners() {
401
+ if (this.windowAbortController) {
402
+ return;
403
+ }
404
+ this.windowAbortController = new AbortController();
405
+ const { signal } = this.windowAbortController;
406
+ window.addEventListener('mousedown', this.handleMouseDown, { signal });
407
+ window.addEventListener('wheel', this.handleMouseWheel, { passive: false, signal });
408
+ window.addEventListener('keydown', this.handleKeyDown, { signal });
409
+ window.addEventListener('contextmenu', this.handleContextMenu, { signal });
410
+ window.addEventListener('touchstart', this.handleTouchSwipe, { signal });
411
+ window.addEventListener('touchmove', this.handleTouchSwipe, { passive: false, signal });
412
+ window.addEventListener('touchend', this.handleTouchSwipe, { signal });
413
+ window.addEventListener('resize', this.handleResize, { signal });
414
+ }
415
+ removeWindowListeners() {
416
+ this.windowAbortController?.abort();
417
+ this.windowAbortController = null;
418
+ }
419
+ addFullscreenChangeListeners() {
420
+ if (this.fullscreenChangeAbortController) {
421
+ return;
422
+ }
423
+ this.fullscreenChangeAbortController = new AbortController();
424
+ window.addEventListener('fullscreenchange', () => {
425
+ if (document.fullscreenElement) {
426
+ this.enter();
427
+ }
428
+ else {
429
+ this.doExit();
430
+ }
431
+ }, { signal: this.fullscreenChangeAbortController.signal });
432
+ }
433
+ removeFullscreenChangeListeners() {
434
+ this.fullscreenChangeAbortController?.abort();
435
+ this.fullscreenChangeAbortController = null;
436
+ }
437
+ }
@@ -1,5 +1,12 @@
1
1
  /** PDF source - can be a URL string, ArrayBuffer, Uint8Array, or Blob */
2
2
  export type PdfSource = string | ArrayBuffer | Uint8Array | Blob;
3
+ /** Presentation mode state */
4
+ export declare enum PresentationModeState {
5
+ UNKNOWN = 0,
6
+ NORMAL = 1,
7
+ CHANGING = 2,
8
+ FULLSCREEN = 3
9
+ }
3
10
  export interface PdfViewerState {
4
11
  loading: boolean;
5
12
  error: string | null;
@@ -11,6 +18,7 @@ export interface PdfViewerState {
11
18
  searchCurrent: number;
12
19
  searchTotal: number;
13
20
  isSearching: boolean;
21
+ presentationMode: PresentationModeState;
14
22
  }
15
23
  export interface PdfViewerActions {
16
24
  zoomIn: () => void;
@@ -24,6 +32,10 @@ export interface PdfViewerActions {
24
32
  searchPrevious: () => void;
25
33
  clearSearch: () => void;
26
34
  download: (filename?: string) => Promise<void>;
35
+ /** Enter fullscreen presentation mode */
36
+ enterPresentationMode: () => Promise<boolean>;
37
+ /** Exit fullscreen presentation mode */
38
+ exitPresentationMode: () => Promise<void>;
27
39
  }
28
40
  export interface PdfViewerContext {
29
41
  state: PdfViewerState;
@@ -3,6 +3,14 @@
3
3
  */
4
4
  import { getContext, setContext } from 'svelte';
5
5
  const PDF_VIEWER_CONTEXT_KEY = Symbol('pdf-viewer');
6
+ /** Presentation mode state */
7
+ export var PresentationModeState;
8
+ (function (PresentationModeState) {
9
+ PresentationModeState[PresentationModeState["UNKNOWN"] = 0] = "UNKNOWN";
10
+ PresentationModeState[PresentationModeState["NORMAL"] = 1] = "NORMAL";
11
+ PresentationModeState[PresentationModeState["CHANGING"] = 2] = "CHANGING";
12
+ PresentationModeState[PresentationModeState["FULLSCREEN"] = 3] = "FULLSCREEN";
13
+ })(PresentationModeState || (PresentationModeState = {}));
6
14
  export function setPdfViewerContext(ctx) {
7
15
  setContext(PDF_VIEWER_CONTEXT_KEY, ctx);
8
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-pdf-view",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "description": "A modern, modular PDF viewer component for Svelte 5. Built on PDF.js with TypeScript support",
5
5
  "author": "Louis Li",
6
6
  "license": "Apache-2.0",