@file-viewer/svelte 2.0.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.
@@ -0,0 +1,593 @@
1
+ import { createViewer } from '@file-viewer/core';
2
+ import {
3
+ DEFAULT_FILE_VIEWER_SOURCE_FILENAME,
4
+ getExtension,
5
+ normalizeFilename,
6
+ readFileViewerBuffer,
7
+ resolveFileViewerSourceFilename,
8
+ wrapFileViewerFileRef,
9
+ type FileViewerAiOptions,
10
+ type FileViewerArchiveOptions,
11
+ type FileViewerCadOptions,
12
+ type FileViewerDocxOptions,
13
+ type FileViewerDocumentAnchor,
14
+ type FileViewerDocumentChunk,
15
+ type FileViewerEvent,
16
+ type FileViewerEventHandler,
17
+ type FileViewerEventType,
18
+ type FileViewerFileRef,
19
+ type FileViewerInstance,
20
+ type FileViewerLifecycleContext,
21
+ type FileViewerOperationAvailability,
22
+ type FileViewerOperationContext,
23
+ type FileViewerOptions,
24
+ type FileViewerPdfOptions,
25
+ type FileViewerSpreadsheetOptions,
26
+ type FileViewerPublicApi,
27
+ type FileViewerSource,
28
+ type FileViewerSearchOptions,
29
+ type FileViewerSearchState,
30
+ type FileViewerThemeMode,
31
+ type FileViewerToolbarOptions,
32
+ type FileViewerToolbarPosition,
33
+ type FileViewerTypstOptions,
34
+ type FileViewerWatermarkOptions,
35
+ type FileViewerZoomState,
36
+ type RendererRegistry,
37
+ type RendererSession,
38
+ } from '@file-viewer/core';
39
+
40
+ export type FileRef = FileViewerFileRef;
41
+
42
+ export type ViewerWatermarkOptions = FileViewerWatermarkOptions;
43
+ export type ViewerToolbarPosition = FileViewerToolbarPosition;
44
+ export type ViewerToolbarOptions = FileViewerToolbarOptions;
45
+ export type ViewerArchiveOptions = FileViewerArchiveOptions;
46
+ export type ViewerPdfOptions = FileViewerPdfOptions;
47
+ export type ViewerSpreadsheetOptions = FileViewerSpreadsheetOptions;
48
+ export type ViewerDocxOptions = FileViewerDocxOptions;
49
+ export type ViewerTypstOptions = FileViewerTypstOptions;
50
+ export type ViewerCadOptions = FileViewerCadOptions;
51
+ export type ViewerSearchOptions = FileViewerSearchOptions;
52
+ export type ViewerAiOptions = FileViewerAiOptions;
53
+ export type ViewerThemeMode = FileViewerThemeMode;
54
+ export type ViewerOptions = FileViewerOptions;
55
+ export type ViewerEventType = FileViewerEventType;
56
+ export type ViewerEvent = FileViewerEvent;
57
+ export type ViewerEventHandler = FileViewerEventHandler;
58
+ export type ViewerLifecycleContext = FileViewerLifecycleContext;
59
+ export type ViewerOperationContext = FileViewerOperationContext;
60
+
61
+ export interface ViewerState {
62
+ loading: boolean;
63
+ ready: boolean;
64
+ error: unknown | null;
65
+ lastEvent: ViewerEvent | null;
66
+ lifecycle: ViewerLifecycleContext | null;
67
+ availability: FileViewerOperationAvailability | null;
68
+ search: FileViewerSearchState | null;
69
+ zoom: FileViewerZoomState | null;
70
+ location: FileViewerDocumentAnchor | null;
71
+ }
72
+
73
+ export type ViewerStateListener = (
74
+ state: ViewerState,
75
+ event?: ViewerEvent
76
+ ) => void;
77
+
78
+ export interface ViewerMountOptions {
79
+ url?: string;
80
+ file?: FileRef;
81
+ buffer?: ArrayBuffer;
82
+ name?: string;
83
+ filename?: string;
84
+ type?: string;
85
+ size?: number;
86
+ options?: ViewerOptions;
87
+ onEvent?: ViewerEventHandler;
88
+ onStateChange?: ViewerStateListener;
89
+ }
90
+
91
+ export interface ViewerSourceInput {
92
+ url?: string;
93
+ file?: FileRef;
94
+ buffer?: ArrayBuffer;
95
+ filename?: string;
96
+ name?: string;
97
+ type?: string;
98
+ size?: number;
99
+ }
100
+
101
+ export interface ViewerFetchInput {
102
+ url: string;
103
+ signal?: AbortSignal;
104
+ source: ViewerSourceInput;
105
+ }
106
+
107
+ export type ViewerFetchFile = (
108
+ input: ViewerFetchInput
109
+ ) => Promise<FileRef | null | undefined>;
110
+
111
+ export interface ViewerCoreOptions {
112
+ registry?: RendererRegistry;
113
+ fetchFile?: ViewerFetchFile;
114
+ onError?: (error: unknown, source: ViewerSourceInput) => void;
115
+ }
116
+
117
+ export interface ViewerController {
118
+ readonly container: HTMLElement;
119
+ load(options: ViewerMountOptions): Promise<void>;
120
+ update(options?: ViewerMountOptions): Promise<void>;
121
+ reload(): Promise<void>;
122
+ destroy(): void;
123
+ getApi(): FileViewerPublicApi | FileViewerInstance | null;
124
+ downloadOriginalFile(): Promise<void>;
125
+ printRenderedHtml(): Promise<void>;
126
+ exportRenderedHtml(): Promise<void>;
127
+ zoomIn(): Promise<FileViewerZoomState | null>;
128
+ zoomOut(): Promise<FileViewerZoomState | null>;
129
+ resetZoom(): Promise<FileViewerZoomState | null>;
130
+ searchDocument(query: string): Promise<FileViewerSearchState | null>;
131
+ clearDocumentSearch(): Promise<FileViewerSearchState | null>;
132
+ nextSearchResult(): Promise<FileViewerSearchState | null>;
133
+ previousSearchResult(): Promise<FileViewerSearchState | null>;
134
+ collectDocumentAnchors(): Promise<FileViewerDocumentAnchor[]>;
135
+ scrollToAnchor(anchor: FileViewerDocumentAnchor | string): Promise<boolean>;
136
+ scrollToLine(line: number): Promise<boolean>;
137
+ getDocumentTextChunks(): FileViewerDocumentChunk[];
138
+ getOperationAvailability(): FileViewerOperationAvailability | null;
139
+ getZoomState(): FileViewerZoomState | null;
140
+ getSearchState(): FileViewerSearchState | null;
141
+ getState(): ViewerState;
142
+ subscribe(listener: ViewerStateListener): () => void;
143
+ }
144
+
145
+ export type ViewerControllerAccessor = () => ViewerController | null;
146
+
147
+ export interface ViewerControllerHandle {
148
+ load(options: ViewerMountOptions): Promise<void>;
149
+ update(options?: ViewerMountOptions): Promise<void>;
150
+ reload(): Promise<void>;
151
+ destroy(): void;
152
+ getController(): ViewerController | null;
153
+ getApi(): FileViewerPublicApi | FileViewerInstance | null;
154
+ downloadOriginalFile(): Promise<void>;
155
+ printRenderedHtml(): Promise<void>;
156
+ exportRenderedHtml(): Promise<void>;
157
+ zoomIn(): Promise<FileViewerZoomState | null>;
158
+ zoomOut(): Promise<FileViewerZoomState | null>;
159
+ resetZoom(): Promise<FileViewerZoomState | null>;
160
+ searchDocument(query: string): Promise<FileViewerSearchState | null>;
161
+ clearDocumentSearch(): Promise<FileViewerSearchState | null>;
162
+ nextSearchResult(): Promise<FileViewerSearchState | null>;
163
+ previousSearchResult(): Promise<FileViewerSearchState | null>;
164
+ collectDocumentAnchors(): Promise<FileViewerDocumentAnchor[]>;
165
+ scrollToAnchor(anchor: FileViewerDocumentAnchor | string): Promise<boolean>;
166
+ scrollToLine(line: number): Promise<boolean>;
167
+ getDocumentTextChunks(): FileViewerDocumentChunk[];
168
+ getOperationAvailability(): FileViewerOperationAvailability | null;
169
+ getZoomState(): FileViewerZoomState | null;
170
+ getSearchState(): FileViewerSearchState | null;
171
+ getState(): ViewerState | null;
172
+ subscribe(listener: ViewerStateListener): () => void;
173
+ }
174
+
175
+ const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
176
+
177
+ const hasSource = (options: ViewerMountOptions = {}) => {
178
+ return !!(options.url || options.file || options.buffer);
179
+ };
180
+
181
+ const toViewerSourceInput = (options: ViewerMountOptions = {}): ViewerSourceInput => ({
182
+ url: options.url,
183
+ file: options.file,
184
+ buffer: options.buffer,
185
+ filename: options.filename || options.name,
186
+ name: options.name,
187
+ type: options.type,
188
+ size: options.size,
189
+ });
190
+
191
+ const canUseFetch = () => typeof fetch === 'function';
192
+
193
+ const defaultFetchFile: ViewerFetchFile = async ({ url, signal }) => {
194
+ if (!canUseFetch()) {
195
+ throw new Error('fetch is not available in the current environment.');
196
+ }
197
+
198
+ const response = await fetch(url, { signal });
199
+ if (!response.ok) {
200
+ throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
201
+ }
202
+ return response.blob();
203
+ };
204
+
205
+ const resolveViewerSourceFilename = (source: ViewerSourceInput) => {
206
+ return normalizeFilename(
207
+ source.filename || source.name || resolveFileViewerSourceFilename({
208
+ file: source.file,
209
+ url: source.url,
210
+ fallback: DEFAULT_FILE_VIEWER_SOURCE_FILENAME,
211
+ }),
212
+ source.type ? `preview.${source.type}` : DEFAULT_FILE_VIEWER_SOURCE_FILENAME
213
+ );
214
+ };
215
+
216
+ const resolveViewerLoadSource = async (
217
+ source: ViewerSourceInput,
218
+ options: {
219
+ fetchFile?: ViewerFetchFile;
220
+ signal?: AbortSignal;
221
+ } = {}
222
+ ): Promise<FileViewerSource> => {
223
+ const filename = resolveViewerSourceFilename(source);
224
+ const type = source.type || getExtension(filename);
225
+
226
+ if (source.buffer) {
227
+ return {
228
+ buffer: source.buffer,
229
+ filename,
230
+ type,
231
+ size: source.size ?? source.buffer.byteLength,
232
+ url: source.url,
233
+ };
234
+ }
235
+
236
+ if (source.file) {
237
+ const file = wrapFileViewerFileRef(source.file, filename);
238
+ return {
239
+ file,
240
+ buffer: await readFileViewerBuffer(file),
241
+ filename: file.name || filename,
242
+ type: type || getExtension(file.name),
243
+ size: source.size ?? file.size,
244
+ url: source.url,
245
+ };
246
+ }
247
+
248
+ if (source.url) {
249
+ const fileRef = await (options.fetchFile || defaultFetchFile)({
250
+ url: source.url,
251
+ signal: options.signal,
252
+ source,
253
+ });
254
+ if (!fileRef) {
255
+ throw new Error('Downloaded file is empty.');
256
+ }
257
+
258
+ const file = wrapFileViewerFileRef(fileRef, filename);
259
+ return {
260
+ file,
261
+ buffer: await readFileViewerBuffer(file),
262
+ filename: file.name || filename,
263
+ type: type || getExtension(file.name),
264
+ size: source.size ?? file.size,
265
+ url: source.url,
266
+ };
267
+ }
268
+
269
+ return {
270
+ filename,
271
+ type,
272
+ };
273
+ };
274
+
275
+ export const createViewerControllerHandle = (
276
+ getController: ViewerControllerAccessor,
277
+ dispose: () => void
278
+ ): ViewerControllerHandle => ({
279
+ load(options) {
280
+ return getController()?.load(options) ?? Promise.resolve();
281
+ },
282
+ update(options) {
283
+ return getController()?.update(options) ?? Promise.resolve();
284
+ },
285
+ reload() {
286
+ return getController()?.reload() ?? Promise.resolve();
287
+ },
288
+ destroy() {
289
+ dispose();
290
+ },
291
+ getController,
292
+ getApi() {
293
+ return getController()?.getApi() ?? null;
294
+ },
295
+ downloadOriginalFile() {
296
+ return getController()?.downloadOriginalFile() ?? Promise.resolve();
297
+ },
298
+ printRenderedHtml() {
299
+ return getController()?.printRenderedHtml() ?? Promise.resolve();
300
+ },
301
+ exportRenderedHtml() {
302
+ return getController()?.exportRenderedHtml() ?? Promise.resolve();
303
+ },
304
+ zoomIn() {
305
+ return getController()?.zoomIn() ?? Promise.resolve(null);
306
+ },
307
+ zoomOut() {
308
+ return getController()?.zoomOut() ?? Promise.resolve(null);
309
+ },
310
+ resetZoom() {
311
+ return getController()?.resetZoom() ?? Promise.resolve(null);
312
+ },
313
+ searchDocument(query) {
314
+ return getController()?.searchDocument(query) ?? Promise.resolve(null);
315
+ },
316
+ clearDocumentSearch() {
317
+ return getController()?.clearDocumentSearch() ?? Promise.resolve(null);
318
+ },
319
+ nextSearchResult() {
320
+ return getController()?.nextSearchResult() ?? Promise.resolve(null);
321
+ },
322
+ previousSearchResult() {
323
+ return getController()?.previousSearchResult() ?? Promise.resolve(null);
324
+ },
325
+ collectDocumentAnchors() {
326
+ return getController()?.collectDocumentAnchors() ?? Promise.resolve([]);
327
+ },
328
+ scrollToAnchor(anchor) {
329
+ return getController()?.scrollToAnchor(anchor) ?? Promise.resolve(false);
330
+ },
331
+ scrollToLine(line) {
332
+ return getController()?.scrollToLine(line) ?? Promise.resolve(false);
333
+ },
334
+ getDocumentTextChunks() {
335
+ return getController()?.getDocumentTextChunks() ?? [];
336
+ },
337
+ getOperationAvailability() {
338
+ return getController()?.getOperationAvailability() ?? null;
339
+ },
340
+ getZoomState() {
341
+ return getController()?.getZoomState() ?? null;
342
+ },
343
+ getSearchState() {
344
+ return getController()?.getSearchState() ?? null;
345
+ },
346
+ getState() {
347
+ return getController()?.getState() ?? null;
348
+ },
349
+ subscribe(listener) {
350
+ return getController()?.subscribe(listener) ?? (() => {});
351
+ },
352
+ });
353
+
354
+ const callApi = async <Result>(
355
+ api: FileViewerInstance | null,
356
+ action: (api: FileViewerInstance) => Promise<Result> | Result,
357
+ fallback: Result
358
+ ) => {
359
+ if (!api) {
360
+ return fallback;
361
+ }
362
+ return action(api);
363
+ };
364
+
365
+ const isAbortError = (error: unknown) => {
366
+ return Boolean(error && typeof error === 'object' && (error as { name?: string }).name === 'AbortError');
367
+ };
368
+
369
+ export const mountViewer = (
370
+ container: HTMLElement,
371
+ initialOptions: ViewerMountOptions = {},
372
+ coreOptions: ViewerCoreOptions = {}
373
+ ): ViewerController => {
374
+ if (!isBrowser()) {
375
+ throw new Error('Flyfish File Viewer can only be mounted in a browser DOM environment.');
376
+ }
377
+
378
+ let disposed = false;
379
+ let currentOptions: ViewerMountOptions = initialOptions;
380
+ let currentSource: ViewerSourceInput | null = hasSource(currentOptions)
381
+ ? toViewerSourceInput(currentOptions)
382
+ : null;
383
+ let abortController: AbortController | null = null;
384
+ const listeners = new Set<ViewerStateListener>();
385
+ const state: ViewerState = {
386
+ loading: false,
387
+ ready: false,
388
+ error: null,
389
+ lastEvent: null,
390
+ lifecycle: null,
391
+ availability: null,
392
+ search: null,
393
+ zoom: null,
394
+ location: null,
395
+ };
396
+ const snapshotState = (): ViewerState => ({
397
+ ...state,
398
+ search: state.search
399
+ ? { ...state.search, matches: [...state.search.matches] }
400
+ : null,
401
+ });
402
+ const notifyState = (event?: ViewerEvent) => {
403
+ const snapshot = snapshotState();
404
+ currentOptions.onStateChange?.(snapshot, event);
405
+ listeners.forEach(listener => listener(snapshot, event));
406
+ };
407
+ const applyViewerEvent = (event: ViewerEvent) => {
408
+ state.lastEvent = event;
409
+ if (event.type === 'load-start') {
410
+ state.loading = true;
411
+ state.ready = false;
412
+ state.error = null;
413
+ state.lifecycle = event.payload;
414
+ } else if (event.type === 'load-complete') {
415
+ state.loading = false;
416
+ state.ready = true;
417
+ state.lifecycle = event.payload;
418
+ } else if (event.type === 'unload-start') {
419
+ state.loading = true;
420
+ state.ready = false;
421
+ state.lifecycle = event.payload;
422
+ } else if (event.type === 'unload-complete') {
423
+ state.loading = false;
424
+ state.ready = false;
425
+ state.lifecycle = event.payload;
426
+ } else if (event.type === 'operation-availability-change') {
427
+ state.availability = event.payload;
428
+ } else if (event.type === 'search-change') {
429
+ state.search = event.payload;
430
+ } else if (event.type === 'location-change') {
431
+ state.location = event.payload;
432
+ } else if (event.type === 'zoom-change') {
433
+ state.zoom = event.payload;
434
+ }
435
+ currentOptions.onEvent?.(event);
436
+ notifyState(event);
437
+ };
438
+ const instance = createViewer(container, {
439
+ registry: coreOptions.registry,
440
+ options: currentOptions.options,
441
+ onEvent: applyViewerEvent,
442
+ });
443
+
444
+ const cancel = () => {
445
+ abortController?.abort();
446
+ abortController = null;
447
+ };
448
+
449
+ const loadSource = async (nextSource: ViewerSourceInput): Promise<RendererSession | null> => {
450
+ cancel();
451
+ currentSource = nextSource;
452
+ abortController = typeof AbortController !== 'undefined' ? new AbortController() : null;
453
+ const controller = abortController;
454
+ try {
455
+ state.loading = true;
456
+ state.error = null;
457
+ notifyState();
458
+ const resolvedSource = await resolveViewerLoadSource(nextSource, {
459
+ fetchFile: coreOptions.fetchFile,
460
+ signal: controller?.signal,
461
+ });
462
+ return await instance.load(resolvedSource);
463
+ } catch (error) {
464
+ if (isAbortError(error) && controller?.signal.aborted) {
465
+ return null;
466
+ }
467
+ state.loading = false;
468
+ state.ready = false;
469
+ state.error = error;
470
+ notifyState();
471
+ coreOptions.onError?.(error, nextSource);
472
+ throw error;
473
+ } finally {
474
+ if (abortController === controller) {
475
+ abortController = null;
476
+ }
477
+ }
478
+ };
479
+
480
+ if (currentSource) {
481
+ void loadSource(currentSource);
482
+ }
483
+
484
+ const controller: ViewerController = {
485
+ container,
486
+ async load(nextOptions) {
487
+ if (disposed) return;
488
+ currentOptions = nextOptions;
489
+ instance.updateOptions(currentOptions.options || {});
490
+ if (hasSource(currentOptions)) {
491
+ await loadSource(toViewerSourceInput(currentOptions));
492
+ }
493
+ },
494
+ async update(nextOptions = {}) {
495
+ if (disposed) return;
496
+ currentOptions = {
497
+ ...currentOptions,
498
+ ...nextOptions,
499
+ options: nextOptions.options ?? currentOptions.options,
500
+ };
501
+ instance.updateOptions(currentOptions.options || {});
502
+ if (hasSource(currentOptions)) {
503
+ await loadSource(toViewerSourceInput(currentOptions));
504
+ } else {
505
+ currentSource = null;
506
+ await instance.load({ filename: DEFAULT_FILE_VIEWER_SOURCE_FILENAME });
507
+ }
508
+ },
509
+ async reload() {
510
+ if (disposed) return;
511
+ if (currentSource) {
512
+ await loadSource(currentSource);
513
+ }
514
+ },
515
+ destroy() {
516
+ if (disposed) return;
517
+ disposed = true;
518
+ cancel();
519
+ void instance.destroy('component-unmount');
520
+ container.innerHTML = '';
521
+ },
522
+ getApi() {
523
+ return instance;
524
+ },
525
+ downloadOriginalFile() {
526
+ return callApi(instance, api => api.download(), undefined);
527
+ },
528
+ printRenderedHtml() {
529
+ return callApi(instance, api => api.print(), undefined);
530
+ },
531
+ exportRenderedHtml() {
532
+ return callApi(
533
+ instance,
534
+ api => api.exportHtml({ download: true }).then(() => undefined),
535
+ undefined
536
+ );
537
+ },
538
+ zoomIn() {
539
+ return callApi(instance, api => api.zoomIn(), null);
540
+ },
541
+ zoomOut() {
542
+ return callApi(instance, api => api.zoomOut(), null);
543
+ },
544
+ resetZoom() {
545
+ return callApi(instance, api => api.resetZoom(), null);
546
+ },
547
+ searchDocument(query) {
548
+ return callApi(instance, api => api.search(query), null);
549
+ },
550
+ clearDocumentSearch() {
551
+ return callApi(instance, api => api.clearSearch(), null);
552
+ },
553
+ nextSearchResult() {
554
+ return callApi(instance, api => api.nextSearchResult(), null);
555
+ },
556
+ previousSearchResult() {
557
+ return callApi(instance, api => api.previousSearchResult(), null);
558
+ },
559
+ collectDocumentAnchors() {
560
+ return callApi(instance, api => api.collectDocumentAnchors(), []);
561
+ },
562
+ scrollToAnchor(anchor) {
563
+ return callApi(instance, api => api.scrollToDocumentAnchor(anchor), false);
564
+ },
565
+ scrollToLine(line) {
566
+ return callApi(instance, api => api.scrollToLine(line), false);
567
+ },
568
+ getDocumentTextChunks() {
569
+ return instance.getDocumentTextChunks();
570
+ },
571
+ getOperationAvailability() {
572
+ return instance.getCapabilities();
573
+ },
574
+ getZoomState() {
575
+ return instance.getZoomState();
576
+ },
577
+ getSearchState() {
578
+ return instance.getSearchState();
579
+ },
580
+ getState() {
581
+ return snapshotState();
582
+ },
583
+ subscribe(listener) {
584
+ listeners.add(listener);
585
+ listener(snapshotState());
586
+ return () => {
587
+ listeners.delete(listener);
588
+ };
589
+ },
590
+ };
591
+
592
+ return controller;
593
+ };