Dialog File Preview para Quasar

🇬🇧 English version | 🇧🇷 Versão em português

npm version license Vue Quasar TypeScript Node

Extensión Quasar para previsualizar archivos en un diálogo modal en apps Vue 3 + Quasar, con API imperativa basada en composable.

Tabla de contenidos

En 30 segundos

Flujo base recomendado:

  1. Instalar la extensión en tu app Quasar (quasar ext add).
  2. Asegurar que el boot de Pinia del host corre antes que el boot de esta extensión.
  3. Importar useDialogFilePreview() donde necesites abrir la vista previa.
  4. Llamar void preview.show(archivo) — el diálogo se monta globalmente; no hace falta añadir componentes a tus plantillas.

La extensión registra boots y CSS en el proyecto host automáticamente tras quasar ext add.

Qué es y qué incluye

Diálogo modal global controlado por useDialogFilePreview() — no es un componente embebible.

Requisitos y compatibilidad

Item Valor
Node >= 20.0.0
Quasar ^2.6.0
Vue ^3.4.18
Pinia ^2.0.11 | ^3.0.0
Formato del paquete ES modules
CLI recomendado @quasar/app-vite ^2.x o ^3.x
PDF en el paquete vue-pdf-embed / pdf.js incluidos (sin dependencia extra en el host)

El boot de Pinia del host debe ejecutarse antes del boot de esta extensión.

Instalación en app Quasar

Agregar la extensión en tu app Quasar:

quasar ext add @benjaminor-dev/dialog-file-preview

Remover:

quasar ext remove @benjaminor-dev/dialog-file-preview

Tras quasar ext add, la extensión registra boots y CSS en tu app automáticamente. No necesitas pasos adicionales en una instalación normal desde npm.

Qué agrega la extensión al host

Recursos que instala o registra la extensión:

Recurso Ruta npm
Boot del diálogo ~@benjaminor-dev/quasar-app-extension-dialog-file-preview/boot/dialog
Estilos ~@benjaminor-dev/quasar-app-extension-dialog-file-preview/main.css
Defaults boot (host) src/boot/bor/bor-dialog-file-preview-defaults.ts — creado en install; Skip/Overwrite si ya existe

Install (quasar ext add / invoke): crea src/boot/bor/bor-dialog-file-preview-defaults.ts (o .js) y lo registra en quasar.configboot: [] automáticamente. No hace falta editarlo a mano en una instalación normal. El boot de paquete (boot/dialog) y main.css los inyecta la extensión en cada dev/build (no aparecen como entradas cortas en quasar.config).

Remove (quasar ext remove): elimina el defaults boot y su entrada en quasar.config.

Orden de boots (esta extensión): Pinia → boot dialog DFP → bor/bor-dialog-file-preview-defaults → resto. En quasar.config verás bor/bor-dialog-file-preview-defaults; el boot boot/dialog lo añade el runner al compilar.

El boot monta el diálogo en el documento y conecta la store de Pinia del host. No necesitas importar DialogFilePreview en App.vue.

Defaults globales (boot)

Evita repetir showPrint / restrictInteraction en cada show(). Prioridad: show(input, options) → boot (configureDialogFilePreviewDefaults) → built-in.

Tras quasar ext add, revisa src/boot/bor/bor-dialog-file-preview-defaults.ts — plantilla con todas las opciones boot (// recomendado: …). El boot no admite title ni forcePreview — esas opciones van solo en show(input, options).

// src/boot/bor/bor-dialog-file-preview-defaults.ts
import { configureDialogFilePreviewDefaults } from "@benjaminor-dev/quasar-app-extension-dialog-file-preview";

configureDialogFilePreviewDefaults({
  showPrint: false,
  restrictInteraction: true,
});

configureDialogFilePreview es alias de configureDialogFilePreviewDefaults.

Idioma (i18n)

Integrado en, es y pt. Por defecto etiquetas de toolbar, tooltips, controles PDF y mensajes fallback siguen el idioma de Quasar ($q.lang.isoName); si Quasar no tiene idioma, fallback en.

Resolución de locale: boot locale → prefijo de $q.lang.isoName (es*es, pt*pt) → en.

Opción Cuándo usarla
locale: 'es' en boot Fijar idioma (en | es | pt); ignora cambios de $q.lang
messages: { download: '…' } Cambiar un texto sin fijar locale

Ejemplo en quasar.config (español para toda la app):

framework: {
  lang: "es",
},

Plantilla boot (tras install):

configureDialogFilePreviewDefaults({
  // locale: "es", // descomenta para fijar solo el idioma de Dialog File Preview
  showPrint: true,
});

Override de un solo string:

configureDialogFilePreviewDefaults({
  messages: { loadingFile: "Abriendo archivo…" },
});

Uso rápido

<script setup lang="ts">
import { useDialogFilePreview } from "@benjaminor-dev/quasar-app-extension-dialog-file-preview";

const preview = useDialogFilePreview();

function onViewPdf(file: File) {
  if (preview.canPreview(file)) {
    void preview.show(file);
  }
}
</script>

<template>
  <q-btn label="Ver PDF" @click="onViewPdf(selectedFile)" />
</template>

Varios archivos (galería o adjuntos):

void preview.show([imageFile, pdfFile, anotherImage]);

URL remota con nombre explícito:

void preview.show({
  source:
    "https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf",
  name: "tracemonkey-pldi-09.pdf",
  mimeType: "application/pdf",
});

TypeScript en tu IDE

El composable expone una API tipada: úsala con autocompletado en lugar de adivinar opciones.

Para comprobar si un archivo se puede previsualizar antes de abrir el modal, canPreview() también está tipado según la fuente que pases.

API pública

Punto de entrada: useDialogFilePreview().

import { useDialogFilePreview } from "@benjaminor-dev/quasar-app-extension-dialog-file-preview";

const preview = useDialogFilePreview();

Métodos

Método Descripción
show(input, options?) Abre el diálogo de inmediato y devuelve una Promise que resuelve cuando terminó la normalización/sondeo MIME de las fuentes. El segundo argumento options configura la sesión (toolbar, título, restricciones, forcePreview). Por defecto, archivos locales que superan el límite de tamaño abren en modo fallback (mensaje + descarga); ver forcePreview para forzar el visor embebido. Mientras tanto, y hasta que el visor emita estado listo, el viewport muestra «Cargando archivo...».
hide() Cierra el diálogo y libera URLs blob: creadas por la extensión.
next() Siguiente archivo (si hay varios).
previous() Archivo anterior.
downloadCurrent() Descarga el archivo visible (desde File/Blob o enlace si solo hay URL).
canPreview(source) true si el MIME tiene visor en el diálogo y el archivo local no supera el límite de tamaño. Evaluación síncrona (sin sondeo remoto).
canPreviewAll(sources) true si todos los ítems del arreglo son previsualizables (también síncrono).

Estado reactivo (solo lectura)

Ref Descripción
visible Si el diálogo está abierto.
current Metadatos del ítem actual (PreviewItem) o null.
hasPrevious / hasNext Navegación en listas multi-archivo.
hasMultiple Hay más de un archivo en la sesión actual.

Usa canPreview() para mostrar u ocultar botones «Ver» en tu UI sin abrir el diálogo. Si la URL no tiene extensión ni MIME conocido, pasa mimeType en el descriptor o llama show() directamente (el sondeo remoto ocurre al abrir, no en canPreview). Si el archivo local supera el límite de tamaño, canPreview() devuelve false aunque el MIME sea válido; show() muestra fallback salvo que pases forcePreview: true (bajo tu responsabilidad).

Comportamiento al abrir (show)

  1. El diálogo se hace visible al instante.
  2. Un único overlay de carga cubre el viewport (fondo oscuro, spinner blanco, «Cargando archivo...») mientras:
    • se enriquecen las fuentes (File/Blob/URL → ítems con MIME y URL de visualización), y
    • el visor del ítem actual termina su carga visible (en PDF: primer render del canvas, no solo metadatos del documento).
  3. Los botones imprimir y descargar en la toolbar aparecen solo cuando el visor emite estado listo (ready) y la opción de sesión correspondiente está activa (showPrint / showDownload).
  4. Imprimir solo se ofrece para PDF, imágenes y texto; video y audio no muestran el botón aunque showPrint sea true.
  5. Al cambiar de archivo en una galería (next / previous), el loader vuelve hasta que el nuevo visor esté listo.
  6. Al imprimir (botón o Ctrl/Cmd+P), si la preparación del iframe/PDF tarda, el mismo overlay muestra «Preparando impresión...» tras ~250 ms y se oculta en el instante en que aparece el diálogo del sistema (no espera a que confirmes o canceles).

El estado interno de preparación no se expone en useDialogFilePreview(); basta con visible y la UX del overlay.

Opciones de sesión (PreviewSessionOptions)

Segundo argumento opcional de show(). Aplica a toda la sesión del diálogo (incluida la galería multi-archivo).

import type { PreviewSessionOptions } from "@benjaminor-dev/quasar-app-extension-dialog-file-preview";

void preview.show(confidentialPdf, {
  showPrint: false,
  showDownload: true,
  restrictInteraction: true,
  title: "Contrato confidencial",
});

void preview.show([scan1, scan2], {
  showGalleryNav: true,
  showPdfZoom: false,
});

// Forzar visor embebido aunque supere el límite de tamaño (riesgo de rendimiento / crash)
void preview.show(heavyLocalPdf, { forcePreview: true });
Opción Default Descripción
showPrint true Muestra el botón imprimir cuando el tipo lo permite (PDF, imagen, texto).
showDownload true Muestra el botón descargar en la toolbar.
showGalleryNav true Muestra navegación anterior/siguiente si hay varios archivos.
showPdfZoom true Muestra «Zoom: N%» en la toolbar para PDF.
title nombre del archivo Título centrado en la toolbar.
restrictInteraction false Bloquea copiar, cortar, seleccionar, menú contextual y atajos comunes en el visor. No evita capturas de pantalla ni herramientas avanzadas del navegador.
forcePreview false Omite el límite de tamaño y abre el visor embebido aunque canPreview() sea false por tamaño. Responsabilidad del desarrollador: archivos muy grandes pueden ralentizar la UI, congelar la pestaña o cerrar el navegador. No cambia el resultado de canPreview().

Si omites options, se usan los valores por defecto de la tabla.

Límites de tamaño para vista previa

Para File / Blob con .size conocido, la extensión aplica un tope antes de usar el visor embebido. Los mismos límites usan canPreview() y show() por defecto.

Visor MIME / categoría Límite
Imagen image/* 50 MB
PDF application/pdf 100 MB
Video video/mp4, video/webm, video/ogg 150 MB
Audio audio/mpeg, audio/wav, audio/ogg, audio/webm 50 MB
Texto text/plain, text/csv, application/json, application/xml, text/xml 10 MB

Constante exportada: PREVIEW_MAX_BYTES_BY_KIND (bytes por tipo de visor).

Comportamiento:

Fuentes de archivo aceptadas

Tipo unificado PreviewSourceInput:

Forma Ejemplo
File Archivo de <input type="file"> o new File(...)
Blob Blob de API o canvas
string URL https://... o blob:...
Objeto descriptor Ver abajo
// File o Blob directo
void preview.show(file);
void preview.show([imageFile, pdfFile]);

// URL con metadatos explícitos (recomendado sin extensión clara)
void preview.show({
  source: "https://ejemplo.com/documento",
  name: "contrato.pdf",
  mimeType: "application/pdf",
});

// Descriptor con File
void preview.show({
  source: selectedFile,
  name: "anexo-renombrado.pdf",
});
Situación Qué hacer
File / Blob Nombre y MIME se infieren del objeto
URL sin extensión Pasa name y mimeType en el descriptor
URL remota al abrir Puede sondearse (HEAD/GET parcial) para inferir MIME
canPreview() devuelve false (MIME) Evalúa de forma síncrona; con mimeType explícito o tras sondeo, show() puede abrir igual (o fallback si el tipo no tiene visor)
canPreview() devuelve false (tamaño) El File/Blob supera el límite de tamaño. show() abre fallback con descarga; usa { forcePreview: true } solo si asumes el riesgo de rendimiento
URLs blob: externas La extensión no las revoca; solo libera las que crea desde File/Blob

Tipos de archivo soportados

Categoría MIME / prefijos Visor en el diálogo Imprimible
Imágenes image/* Imagen centrada y contenida en el viewport; clic para zoom ×2 con pan; sin scroll en el área del visor
PDF application/pdf pdf.js (vue-pdf-embed): modo adaptativo (ver abajo); barra de página con salto directo, zoom y capas de texto/anotaciones
Video video/mp4, video/webm, video/ogg <video controls> No
Audio audio/mpeg, audio/wav, audio/ogg, audio/webm <audio controls> con icono central y ecualizador al reproducir No
Texto text/plain, text/csv, application/json, application/xml, text/xml Texto monoespaciado con scroll
Otros Mensaje + botón descargar No

Si el MIME no tiene visor, show() igual puede abrir el diálogo en modo fallback para permitir la descarga.

Visor de imágenes

Visor de PDF (modo adaptativo)

El visor elige automáticamente entre dos presentaciones según el tamaño del documento:

Modo Cuándo Experiencia
Scroll continuo Archivo ligero (típicamente ≤ 25 MB, ≤ 40 páginas) Todas las páginas en columna con separación; scroll vertical; la barra inferior sigue la página visible
Paginado Archivo pesado o muchas páginas Una página a la vez (adecuado para PDFs muy grandes)

En ambos modos:

No hay opción pública para forzar un modo: la heurística protege al navegador ante documentos enormes (p. ej. cientos de MB o miles de páginas).

Tipos TypeScript exportados

Desde el entrypoint principal:

import type {
  DialogFilePreviewApi,
  DialogFilePreviewDefaults,
  PreviewBlockReason,
  PreviewSourceInput,
  PreviewItemInput,
  PreviewItem,
  PreviewKind,
  PreviewSessionOptions,
} from "@benjaminor-dev/quasar-app-extension-dialog-file-preview";
Tipo Uso
DialogFilePreviewApi Tipo de retorno de useDialogFilePreview()
DialogFilePreviewDefaults Opciones admitidas en configureDialogFilePreviewDefaults (boot)
PreviewBlockReason Motivo de bloqueo por tamaño ("size", etc.)
PreviewSourceInput Primer argumento de show() y argumento de canPreview()
PreviewSessionOptions Segundo argumento opcional de show()
PreviewItemInput Descriptor con metadatos opcionales
PreviewItem Ítem normalizado del estado reactivo (current); incluye blockedReason: PreviewBlockReason | null cuando aplica
PreviewKind "image" | "pdf" | "video" | "audio" | "text" | "unknown"
type PreviewItemInput = {
  source: File | Blob | string;
  name?: string;
  mimeType?: string;
};

Utilidades MIME

También se exportan para validar en tu UI sin abrir el diálogo:

import {
  isPreviewableMime,
  PREVIEWABLE_MIME_PREFIXES,
} from "@benjaminor-dev/quasar-app-extension-dialog-file-preview";

if (isPreviewableMime(file.type)) {
  // mostrar botón de vista previa
}

Valores de PREVIEWABLE_MIME_PREFIXES (prefijo con / = familia completa; resto = MIME exacto):

Entrada Categoría
image/ Imágenes
application/pdf PDF
video/mp4, video/webm, video/ogg Video
audio/mpeg, audio/wav, audio/ogg, audio/webm Audio
text/plain, text/csv, application/json, application/xml, text/xml Texto

Extensiones opcionales relacionadas

Extensión Propósito Relación con Dialog File Preview
@benjaminor-dev/form-builder Formularios declarativos con defineForm y FormBuilder InputFile e InputFileMultiple detectan esta extensión cuando showPreview no es false (default true): botones ver/descargar en el append o en la tabla de gestión. No es dependencia de Dialog File Preview

Si usas ambas en el host:

quasar ext add @benjaminor-dev/form-builder
quasar ext add @benjaminor-dev/dialog-file-preview

Orden de boots (stack completo):

1. Pinia (host)
2. Form Builder — boot/store
3. bor/bor-form-builder-defaults (host)
4. Table Builder — boot/store
5. bor/bor-table-builder-defaults (host)
6. Dialog File Preview — boot/dialog
7. bor/bor-dialog-file-preview-defaults (host)
8. Dialog Loader — boot/dialog
9. bor/bor-dialog-loader-defaults (host)
10. Dialog Message — boot/dialog
11. bor/bor-dialog-message-defaults (host)

En quasar.config verás el defaults boot de cada extensión (p. ej. bor/bor-dialog-file-preview-defaults); los boots del paquete los inyecta la extensión al compilar — no aparecen como entradas cortas en tu config.

Uso manual desde un callback o botón custom (sin depender de Form Builder):

<script setup lang="ts">
import { useDialogFilePreview } from "@benjaminor-dev/quasar-app-extension-dialog-file-preview";

const preview = useDialogFilePreview();

function onPreviewFile(file: File) {
  void preview.show(file);
}
</script>

El boot también expone la API vía provide (DIALOG_FILE_PREVIEW_API_KEY) para integraciones de librerías hermanas — misma instancia que useDialogFilePreview().

Entrypoints públicos

Entrypoint Propósito
@benjaminor-dev/quasar-app-extension-dialog-file-preview useDialogFilePreview, configureDialogFilePreviewDefaults, i18n (resolveDialogFilePreviewMessages, resolveExtensionLocale), utilidades MIME — tipos en IDE (PreviewSessionOptions, DialogFilePreviewDefaults, DialogFilePreviewMessages, …)
@benjaminor-dev/quasar-app-extension-dialog-file-preview/boot/dialog Boot Quasar (registrado automáticamente por la extensión)
@benjaminor-dev/quasar-app-extension-dialog-file-preview/main.css Estilos del diálogo (registrado automáticamente)

Troubleshooting

Los textos de la UI siguen en inglés

Boots o estilos no aparecen en quasar.config

En condiciones normales el install registra el defaults boot en boot: [] y el runner inyecta boot/dialog + CSS al compilar. Si falta el defaults boot (proyecto antiguo):

npx quasar ext invoke @benjaminor-dev/dialog-file-preview

Error: Pinia no inicializada / store no disponible

useDialogFilePreview() fuera de setup

Llámalo dentro de setup, otro composable o <script setup>. Si necesitas usarlo fuera del contexto de componente, asegúrate de que el boot de la extensión ya se ejecutó (app montada).

El botón «Ver» no debería mostrarse

Usa preview.canPreview(file) antes de renderizar la acción. Devuelve false si el MIME no tiene visor o si el archivo local supera el límite de tamaño. isPreviewableMime() solo evalúa MIME, no tamaño.

canPreview() devuelve false por tamaño

El archivo local supera el límite de su tipo (p. ej. imagen > 50 MB). Por defecto show(file) abre fallback con descarga. Para forzar el visor embebido: show(file, { forcePreview: true })bajo tu responsabilidad (rendimiento, pestaña congelada o cierre del navegador). canPreview() seguirá siendo false.

canPreview() devuelve false en una URL sin extensión

canPreview() no hace sondeo remoto. Si la URL no tiene extensión ni heurística conocida, pasa mimeType en el descriptor o llama show() directamente.

PDF muy grande: solo veo una página a la vez

Es el comportamiento esperado en modo paginado: archivos por encima de ~25 MB, con más de 40 páginas, o documentos largos sin tamaño conocido (> 15 páginas) se renderizan de una en una para no saturar memoria del navegador. Usa la barra de páginas para navegar.

El contador de página del PDF no cambia al hacer scroll

En modo continuo con zoom alto (≥ 90 %), espera un instante al detener el scroll; el contador sigue la página con mayor área visible. Si el documento entró en modo paginado, el scroll del viewport no aplica — usa los botones de página.

PDF o texto desde URL externa no carga

Ejemplo de URL pública que funciona en pruebas:

preview.show({
  source:
    "https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf",
  name: "tracemonkey-pldi-09.pdf",
  mimeType: "application/pdf",
});

El botón de descarga no aparece al abrir

Es esperado al inicio: la descarga en la toolbar solo se habilita cuando el visor emite estado listo (ready) y showDownload no es false. Mientras prepara fuentes o carga PDF/imagen/texto verás «Cargando archivo...» en el viewport. Si el loader no desaparece, revisa errores de red/CORS en la consola.

No aparece el botón imprimir

El loader de impresión tarda o no aparece

restrictInteraction no bloquea todo

La opción reduce copia, selección y menú contextual en el visor, pero no sustituye DRM ni impide capturas de pantalla. Un usuario con conocimientos técnicos puede seguir accediendo al contenido.

El diálogo abre vacío un instante o tarda en mostrar el archivo

show() abre el modal de inmediato y normaliza las fuentes en segundo plano. Con URLs remotas o varios archivos, el overlay puede permanecer unos segundos; no indica fallo por sí solo.

La vista previa de imagen/PDF funciona en local pero no en producción

Revisa que la extensión esté instalada/invocada en el build de producción y que main.css de la extensión esté en la config generada.

Varios archivos: no aparece navegación

Pasa un arreglo a show([...]). La barra anterior/siguiente solo se muestra cuando hay más de un ítem y showGalleryNav no es false.


MIT © Benjamín Olvera R.