diff --git a/README.md b/README.md index 76d3be8..6a51cff 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ The element is block-level and needs an explicit height. | viewer‑css‑theme | Viewer theme: `AUTOMATIC`, `LIGHT`, `DARK`. | `AUTOMATIC` | | assets‑base | Directory URL of the viewer runtime files (`viewer.mjs`, `pdf.mjs`, `pdf.worker.min.mjs`, `viewer.css`, `paper-and-ink.css`). Only needed when your bundler does not handle `new URL('./file', import.meta.url)`, see [Bundlers](#bundlers-and-assets-base). | folder of `pdfjs-viewer-element.js` | | worker‑src | PDF.js worker URL override. | bundled worker (`./build/pdf.worker.mjs` in dev, `pdf.worker.min.mjs` next to the element or in `assets-base`) | +| pdfjs‑lib‑src | PDF.js library module URL override. | bundled `pdf.mjs`, or `assets-base + pdf.mjs` | +| viewer‑src | PDF.js viewer module URL override. | bundled `viewer.mjs`, or `assets-base + viewer.mjs` | +| viewer‑css‑src | PDF.js viewer stylesheet URL override. | bundled `viewer.css`, or `assets-base + viewer.css` | +| theme‑css‑src | Paper & Ink theme stylesheet URL override. | bundled `paper-and-ink.css`, or `assets-base + paper-and-ink.css` | | debugger‑src | PDF.js debugger script URL (`debuggerSrc` option). | `./debugger.mjs` | | c‑map‑url | CMap directory URL (`cMapUrl` option). | `../web/cmaps/` | | icc‑url | ICC profile directory URL (`iccUrl` option). | `../web/iccs/` | @@ -120,7 +124,7 @@ Most attributes can be updated dynamically: - `viewer-css-theme` updates the viewer theme at runtime. - `worker-src`, `debugger-src`, `c-map-url`, `icc-url`, `image-resources-path`, `sandbox-bundle-src`, `standard-font-data-url`, `wasm-url` update viewer options for subsequent document loads. - `locale` rebuilds the viewer so localization resources can be applied. -- `assets-base` and `locale-src-template` are read when the viewer is (re)initialized, so set them before the element is attached. +- `assets-base`, `pdfjs-lib-src`, `viewer-src`, `viewer-css-src`, `theme-css-src` and `locale-src-template` are read when the viewer is (re)initialized, so set them before the element is attached. Changing them alone does not reload the viewer. ## Bundlers and assets base @@ -136,7 +140,7 @@ Bundlers that do not analyse `new URL(..., import.meta.url)`, esbuild among them ``` - The value is a directory URL, relative to the page or absolute. A trailing slash is added when missing. -- `worker-src` still takes precedence for the worker file. +- Individual runtime URL attributes take precedence over `assets-base` for their respective files, including `worker-src`. - The attribute is read when the viewer is (re)initialized, so set it before the element is attached to the document. PDF.js annotation icons are a separate case: the viewer loads them at runtime as `imageResourcesPath + 'annotation-' + name + '.svg'`, a dynamic name no bundler can emit. If `assets-base` is set, `imageResourcesPath` defaults to `assets-base + 'images/'`. Otherwise serve the package `dist/images` folder and set `image-resources-path` explicitly. diff --git a/demo/asset-attributes.html b/demo/asset-attributes.html new file mode 100644 index 0000000..7e86dae --- /dev/null +++ b/demo/asset-attributes.html @@ -0,0 +1,55 @@ + + + + + + pdfjs-viewer-element | Static file attributes demo + + + + + + + + + + diff --git a/src/pdfjs-viewer-element.ts b/src/pdfjs-viewer-element.ts index f807eeb..b557e34 100644 --- a/src/pdfjs-viewer-element.ts +++ b/src/pdfjs-viewer-element.ts @@ -32,7 +32,7 @@ const DEFAULT_PAPER_AND_INK_THEME_CSS_SRC = import.meta.env.DEV // File names inside the package `dist` folder, used when `assets-base` is set. const DIST_FILE_NAMES = { workerSrc: 'pdf.worker.min.mjs', - pdfSrc: 'pdf.mjs', + pdfjsLibSrc: 'pdf.mjs', viewerSrc: 'viewer.mjs', viewerCssSrc: 'viewer.css', themeCssSrc: 'paper-and-ink.css' @@ -42,7 +42,7 @@ type RuntimeFileUrls = Record const DEFAULT_RUNTIME_FILE_URLS: RuntimeFileUrls = { workerSrc: DEFAULT_BUILT_IN_WORKER_SRC, - pdfSrc: DEFAULT_PDF_SRC, + pdfjsLibSrc: DEFAULT_PDF_SRC, viewerSrc: DEFAULT_VIEWER_SRC, viewerCssSrc: DEFAULT_VIEWER_CSS_SRC, themeCssSrc: DEFAULT_PAPER_AND_INK_THEME_CSS_SRC @@ -140,9 +140,13 @@ export class PdfjsViewerElement extends HTMLElement { // Read when the viewer is (re)built, so the attribute must be present before connection. private getRuntimeFileUrls(): RuntimeFileUrls { const assetsBase = this.getAssetsBase() - if (!assetsBase) return DEFAULT_RUNTIME_FILE_URLS - const resolveFile = (fileName: string) => { + const resolveFile = (field: keyof typeof DIST_FILE_NAMES) => { + const attr = this.getAttribute(field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`))?.trim() + if (attr) return this.getFullPath(attr) + if (!assetsBase) return DEFAULT_RUNTIME_FILE_URLS[field] + + const fileName = DIST_FILE_NAMES[field] try { return new URL(fileName, assetsBase).href } catch { @@ -151,18 +155,14 @@ export class PdfjsViewerElement extends HTMLElement { } return { - workerSrc: resolveFile(DIST_FILE_NAMES.workerSrc), - pdfSrc: resolveFile(DIST_FILE_NAMES.pdfSrc), - viewerSrc: resolveFile(DIST_FILE_NAMES.viewerSrc), - viewerCssSrc: resolveFile(DIST_FILE_NAMES.viewerCssSrc), - themeCssSrc: resolveFile(DIST_FILE_NAMES.themeCssSrc) + workerSrc: resolveFile('workerSrc'), + pdfjsLibSrc: resolveFile('pdfjsLibSrc'), + viewerSrc: resolveFile('viewerSrc'), + viewerCssSrc: resolveFile('viewerCssSrc'), + themeCssSrc: resolveFile('themeCssSrc') } } - private getWorkerSrc() { - return this.getAttribute('worker-src') || this.getRuntimeFileUrls().workerSrc - } - private getImageResourcesPath() { const imageResourcesPath = this.getAttribute('image-resources-path') if (imageResourcesPath) return imageResourcesPath @@ -383,7 +383,7 @@ export class PdfjsViewerElement extends HTMLElement { private applyViewerOptions = () => { const viewerOptions = this.iframe.contentWindow?.PDFViewerApplicationOptions - viewerOptions?.set('workerSrc', this.getWorkerSrc()) + viewerOptions?.set('workerSrc', this.getRuntimeFileUrls().workerSrc) viewerOptions?.set('debuggerSrc', this.getAttribute('debugger-src') || DEFAULTS.debuggerSrc) viewerOptions?.set('cMapUrl', this.getAttribute('c-map-url') || DEFAULTS.cMapUrl) viewerOptions?.set('iccUrl', this.getAttribute('icc-url') || DEFAULTS.iccUrl) @@ -432,13 +432,13 @@ export class PdfjsViewerElement extends HTMLElement { const toSourceList = (sources: string[]) => Array.from(new Set(sources.filter(Boolean))).join(' ') - const { pdfSrc, viewerSrc, viewerCssSrc, themeCssSrc } = this.getRuntimeFileUrls() - const workerSrcOrigin = resolveHttpOrigin(this.getWorkerSrc()) + const { pdfjsLibSrc, viewerSrc, viewerCssSrc, themeCssSrc, workerSrc } = this.getRuntimeFileUrls() + const workerSrcOrigin = resolveHttpOrigin(workerSrc) const scriptSources = toSourceList([ "'self'", window.location.origin, - resolveHttpOrigin(pdfSrc), + resolveHttpOrigin(pdfjsLibSrc), resolveHttpOrigin(viewerSrc), workerSrcOrigin ]) @@ -508,8 +508,8 @@ export class PdfjsViewerElement extends HTMLElement { await this.injectLocaleData() - const { pdfSrc, viewerSrc } = this.getRuntimeFileUrls() - await this.injectScript(pdfSrc) + const { pdfjsLibSrc, viewerSrc } = this.getRuntimeFileUrls() + await this.injectScript(pdfjsLibSrc) await this.injectScript(viewerSrc) return await setupPromise @@ -547,7 +547,7 @@ export class PdfjsViewerElement extends HTMLElement { if (name === 'worker-src') { const viewerOptions = this.iframe.contentWindow?.PDFViewerApplicationOptions - viewerOptions?.set('workerSrc', newValue || this.getRuntimeFileUrls().workerSrc) + viewerOptions?.set('workerSrc', this.getRuntimeFileUrls().workerSrc) return } diff --git a/tests/basic.test.ts b/tests/basic.test.ts index c5d6917..7127e97 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { getFileData, getViewerElement, mountViewer, getIframe } from './utils' -import '../src/pdfjs-viewer-element' +import { PdfjsViewerElement } from '../src/pdfjs-viewer-element' describe('Basic tests', async () => { it('should render the PDF file', async () => { @@ -108,11 +108,12 @@ describe('Basic tests', async () => { ) const viewer = document.body.querySelector('pdfjs-viewer-element') as HTMLElement - const workerSrc = 'https://example.com/pdf.worker.min.mjs' - viewer.setAttribute('worker-src', workerSrc) + for (const workerSrc of ['https://example.com/pdf.worker.min.mjs', '/static/pdfjs/pdf.worker.abc.mjs', './pdf.worker.mjs']) { + viewer.setAttribute('worker-src', workerSrc) - const options = getIframe().contentWindow.PDFViewerApplicationOptions.getAll() - expect(options.workerSrc).eq(workerSrc) + const options = getIframe().contentWindow.PDFViewerApplicationOptions.getAll() + expect(options.workerSrc).eq(new URL(workerSrc, location.href).href) + } }) it('should include external worker origin in CSP script and worker directives', async () => { @@ -258,6 +259,29 @@ describe('Basic tests', async () => { } }) + it('should load individual runtime URLs without assets-base', async () => { + await mountViewer(` + + ` + ) + + const doc = getIframe().contentDocument! + for (const path of ['/src/build/pdf.mjs', '/src/web/viewer.mjs']) { + expect(doc.querySelector(`script[src="${location.origin}${path}?manifest=123"]`)).exist + } + for (const path of ['/src/web/viewer.css', '/src/themes/paper-and-ink.css']) { + expect(doc.querySelector(`link[href="${location.origin}${path}?manifest=123"]`)).exist + } + expect(getIframe().contentWindow.PDFViewerApplicationOptions.getAll().workerSrc) + .eq(new URL('/src/build/pdf.worker.mjs?manifest=123', location.href).href) + expect(getViewerElement()).exist + }) + it('should open a new document when src changes at runtime', async () => { const viewerApp = await mountViewer(` ` @@ -282,4 +306,40 @@ describe('Basic tests', async () => { viewerApp.open = originalOpen } }) -}) \ No newline at end of file +}) + +describe('Runtime file URL overrides', () => { + const files = { + workerSrc: ['worker-src', 'pdf.worker.min.mjs'], + pdfjsLibSrc: ['pdfjs-lib-src', 'pdf.mjs'], + viewerSrc: ['viewer-src', 'viewer.mjs'], + viewerCssSrc: ['viewer-css-src', 'viewer.css'], + themeCssSrc: ['theme-css-src', 'paper-and-ink.css'] + } as const + + it.each([undefined, '/static/pdfjs'])('resolves individual URLs with assets-base=%s', (assetsBase) => { + const viewer = new PdfjsViewerElement() + if (assetsBase) viewer.setAttribute('assets-base', assetsBase) + const defaults = viewer['getRuntimeFileUrls']() + + for (const [key, [attribute, file]] of Object.entries(files)) { + const field = key as keyof typeof files + const path = `/static/hashed/${file}?v=123` + viewer.setAttribute(attribute, path) + expect(viewer['getRuntimeFileUrls']()[field]).eq(new URL(path, location.href).href) + viewer.setAttribute(attribute, 'https://cdn.example.com/' + file) + expect(viewer['getRuntimeFileUrls']()[field]).eq('https://cdn.example.com/' + file) + viewer.setAttribute(attribute, '') + expect(viewer['getRuntimeFileUrls']()[field]).eq(defaults[field]) + viewer.setAttribute(attribute, ' \t\n ') + expect(viewer['getRuntimeFileUrls']()[field]).eq(defaults[field]) + viewer.setAttribute(attribute, ` ${path} `) + expect(viewer['getRuntimeFileUrls']()[field]).eq(new URL(path, location.href).href) + viewer.removeAttribute(attribute) + expect(viewer['getRuntimeFileUrls']()).toEqual(defaults) + if (assetsBase) { + expect(defaults[field]).eq(new URL(`${assetsBase}/${file}`, location.href).href) + } + } + }) +}) diff --git a/types/pdfjs-viewer-element.d.ts b/types/pdfjs-viewer-element.d.ts index f0668e2..ec81124 100644 --- a/types/pdfjs-viewer-element.d.ts +++ b/types/pdfjs-viewer-element.d.ts @@ -32,7 +32,6 @@ export declare class PdfjsViewerElement extends HTMLElement { private getFullPath; private getAssetsBase; private getRuntimeFileUrls; - private getWorkerSrc; private getImageResourcesPath; private getCssThemeOption; private applyIframeHash;