File previews
@nextgensoftwares/folio-plugin-file-preview turns plugin-media's file cards into cards with a minimized preview of what's inside, and opens the file in the page when the card is clicked:
| File | In the card | In the viewer |
|---|---|---|
| the first page | every page (virtualized), zoom, page navigation, selectable text, find, safe links | |
| XLSX / XLS / ODS | the first ~6 rows × 5 columns of the first sheet | a grid with sheet tabs (virtualized rows) |
| CSV / TSV | the first rows | the same grid |
| DOCX | the first lines, headings in bold | the document as sanitized HTML |
| ODT | the first lines and headings | paragraphs and headings |
| PPTX | the slide titles | per-slide titles and text |
| TXT / MD / JSON / code | the first lines | the text (Markdown headings shown as headings) |
| image / video | the image / a poster frame | native <img> / <video> |
| audio, anything else | the plain icon card | native <audio> / a download prompt |
import { composePlugins } from '@nextgensoftwares/folio-editor';
import { indexedDbUploader, mediaPlugin } from '@nextgensoftwares/folio-plugin-media';
import { filePreviewPlugin } from '@nextgensoftwares/folio-plugin-file-preview';
const media = mediaPlugin({ uploader: indexedDbUploader() });
const composed = composePlugins(standardSchema, [
media,
filePreviewPlugin({ media }), // after mediaPlugin: it takes over the fileAttachment card
]);Nothing else is needed: the card painter, the size-aware renderer, the Enter binding, the context-menu items and the export mappings come from composed.
Card sizes and layout
The card's size is a node attribute, previewSize: 'compact' (the 56px icon card), 'preview' (196px, the default) or 'large' (384px, up to 600px wide). It's chosen by the user (right-click ▸ Card size) and is an ordinary, undoable document edit. Layout never depends on a preview result: the renderer sizes the card from the attribute and the file's name/MIME type only, so pagination is exact and deterministic. Files with nothing to preview (archives, audio) stay compact whatever the attribute. The default for cards without the attribute is the defaultSize option.
Lazy, cached, off the main thread
- Previews are generated only for cards near the viewport (the page list is virtualized, plus an
IntersectionObserver), after a short delay so pages flicked past during a fast scroll start nothing; at most two at a time, newest first. A card that scrolls away before its turn cancels its request. - Results land as a React transition, so typing and scrolling never wait.
- Cache: an in-memory LRU (sync hit on first paint) plus IndexedDB (
folio-file-previews), keyed by the storedsrc, its recorded size and the card size. Failures are remembered for the session only. - Spreadsheets, documents, slides and text are parsed in a Web Worker (
preview-worker.js); pdf.js runs its parser in its own worker (pdf-worker.js). Both are loaded with thenew Worker(new URL(…, import.meta.url))pattern that Vite and webpack 5 bundle. Passworker: null/pdfWorker: nullto parse on the main thread (e.g. a strict CSP withoutworker-src), or your own factories.
The viewer
A click on the card (it also selects the node), or Enter on a selected card, opens an overlay viewer in the same page: focus moves into the dialog, Tab is trapped, Escape (or the backdrop) closes it and focus returns to the editor. The header has Download and Open in new tab (rel="noopener noreferrer"). It follows the document's colour theme (dark when opened from a .folio-doc-dark view).
The viewer is replaceable:
filePreviewPlugin({
media,
// Your own component, mounted and opened by the plugin:
viewer: ({ file, url, dark, service, onClose }) => <MyViewer … />,
// …or take over opening entirely (e.g. route to a page); return true:
onOpen: (file) => (router.push(`/files/${encodeURIComponent(file.src)}`), true),
});loadFileViewer() returns the default component (lazily) to wrap it.
Libraries and lazy chunks
Every library is a dynamic import, so none of it is in the base bundle. Sizes are esbuild-minified, gzip -9:
| Chunk | Loaded when | License | min | gzip |
|---|---|---|---|---|
| plugin base (card, cache, layout, commands) | with the plugin | Apache-2.0 | 21 KB | 8.6 KB |
| our extractors (worker) | first parsed preview | Apache-2.0 | 7.7 KB | 3.5 KB |
| our viewer UI | first open | Apache-2.0 | 27 KB | 9.9 KB |
fflate (unzip: DOCX/ODT/PPTX previews) | first Office preview | MIT | 5.4 KB | 2.7 KB |
xlsx (SheetJS CE 0.20.3) | first spreadsheet | Apache-2.0 | 491 KB | 159 KB |
mammoth 1.13 (DOCX → HTML) | first DOCX opened in the viewer | BSD-2-Clause (jszip used under MIT) | 315 KB | 98 KB |
pdfjs-dist 6.3 legacy build, main | first PDF | Apache-2.0 | 479 KB | 147 KB |
pdfjs-dist worker | first PDF | Apache-2.0 | 1.2 MB | 382 KB |
- SheetJS stopped publishing to npm at 0.18.5 (which has known prototype-pollution and ReDoS advisories). The package pins the vendor tarball
https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz(integrity in the lockfile). - pdf.js: the legacy build is used: same API, transpiled for older Safari/Chrome (the modern build needs
Promise.try) and Node.
Security
The files are untrusted:
- Size caps: no preview over
limits.maxBytes(50 MB), no viewer overlimits.viewerMaxBytes(100 MB). The recorded size and Content-Length are checked before reading, and a body that lies is cut off at the cap. Zip entries are capped too (zip bombs). - Time limits:
limits.timeoutMs(15 s). A worker task that overruns terminates the worker (the next task gets a fresh one); in-thread work stops being waited for. - DOCX HTML is re-serialized through a strict, DOM-free allowlist (
sanitizeHtml) at the point of insertion: no scripts, event handlers,style, iframes/objects/embeds, forms, SVG/MathML, or external resources (images must be inline rasterdata:URLs); links onlyhttp(s)/mailto, opened withtarget=_blank rel="noopener noreferrer nofollow"; ids are prefixed so they can't clobber page globals. mammoth runs withexternalFileAccess: false. - Spreadsheets/CSV are text only: formulas show their cached value (never the formula, never evaluated), hyperlinks and rich text are ignored, and CSV is parsed by a small RFC 4180 parser that keeps
=…,+…,@…cells as literal text. Everything renders as React text nodes. - PDF: no annotation/form layer and no scripting; XFA off;
isEvalSupported: false(pdf.js 5+ never evals anyway); no fetching of fonts, CMaps, wasm or ranges. Onlyhttp(s)/mailtolinks become anchors, withrel="noopener noreferrer nofollow". - CSP-friendly: no
eval/new Function; workers are same-origin module scripts. idb:and other stored sources are resolved through plugin-media's resolver; unloadable schemes (javascript:…) are never fetched.- Any failure (corrupt file, timeout, crash in a painter) falls back to the plain card; the viewer shows a message instead of crashing.
Export
The plugin maps fileAttachment for both exporters to plugin-media's PrintFallback plus an image: the card's thumbnail (a PDF page or video poster as JPEG, a table/text preview drawn to a canvas, an image file's own src).
exportThumbnails | |
|---|---|
'cached' (default) | use a thumbnail only if the card was already previewed (memory or IndexedDB) |
'generate' | make missing ones during export |
false | never |
plugin.prepareExport(doc) generates every missing thumbnail ahead of time. Compact cards stay plain. The PDF exporter draws the image in the card. The DOCX exporter currently embeds only images it prefetched from the document's media sources, so thumbnails (inline data: images) don't appear in DOCX yet (the card's title, detail and link do).
Options
| Option | Default | |
|---|---|---|
media | (required) | the media plugin (resolver + its card painter, used while uploading) |
defaultSize | 'preview' | size of cards without previewSize |
limits | see above | maxBytes, viewerMaxBytes, timeoutMs, rows, cols, lines, maxCells, maxTextChars |
cache | IndexedDB | any PreviewStore, or null for memory only |
worker, pdfWorker | bundled workers | factories, or null for in-thread |
viewer, onOpen | built-in viewer | replace the viewer or the opening |
exportThumbnails | 'cached' | see Export |
API reference: @nextgensoftwares/folio-plugin-file-preview.