fix: resolve i18n issues - URL duplication, translation loading, and caching

- Fix URL path duplication when clicking logo (added missing leading slash)
- Use network-first caching for translation files in service worker
- Add missing translation keys (common.close, upload.clearFiles) to all languages
- Add Dutch (nl) language support to URL regex patterns
- Bump service worker cache version to v8
This commit is contained in:
alam00000
2026-01-26 22:29:51 +05:30
parent 2d99a28b07
commit 62c373d76a
16 changed files with 10397 additions and 7524 deletions

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDFs oder Bilder", "pdfOrImages": "PDFs oder Bilder",
"filesNeverLeave": "Ihre Dateien verlassen nie Ihr Gerät.", "filesNeverLeave": "Ihre Dateien verlassen nie Ihr Gerät.",
"addMore": "Weitere Dateien hinzufügen", "addMore": "Weitere Dateien hinzufügen",
"clearAll": "Alle löschen" "clearAll": "Alle löschen",
"clearFiles": "Dateien löschen"
}, },
"loader": { "loader": {
"processing": "Verarbeitung..." "processing": "Verarbeitung..."
@@ -226,7 +227,8 @@
"error": "Fehler", "error": "Fehler",
"success": "Erfolg", "success": "Erfolg",
"file": "Datei", "file": "Datei",
"files": "Dateien" "files": "Dateien",
"close": "Schließen"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDFs or Images", "pdfOrImages": "PDFs or Images",
"filesNeverLeave": "Your files never leave your device.", "filesNeverLeave": "Your files never leave your device.",
"addMore": "Add More Files", "addMore": "Add More Files",
"clearAll": "Clear All" "clearAll": "Clear All",
"clearFiles": "Clear Files"
}, },
"loader": { "loader": {
"processing": "Processing..." "processing": "Processing..."
@@ -226,7 +227,8 @@
"error": "Error", "error": "Error",
"success": "Success", "success": "Success",
"file": "File", "file": "File",
"files": "Files" "files": "Files",
"close": "Close"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDFs o Imágenes", "pdfOrImages": "PDFs o Imágenes",
"filesNeverLeave": "Tus archivos nunca salen de tu dispositivo.", "filesNeverLeave": "Tus archivos nunca salen de tu dispositivo.",
"addMore": "Agregar Más Archivos", "addMore": "Agregar Más Archivos",
"clearAll": "Limpiar Todo" "clearAll": "Limpiar Todo",
"clearFiles": "Borrar archivos"
}, },
"loader": { "loader": {
"processing": "Procesando..." "processing": "Procesando..."
@@ -226,7 +227,8 @@
"error": "Error", "error": "Error",
"success": "Éxito", "success": "Éxito",
"file": "Archivo", "file": "Archivo",
"files": "Archivos" "files": "Archivos",
"close": "Cerrar"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF ou images", "pdfOrImages": "PDF ou images",
"filesNeverLeave": "Vos fichiers restent sur votre appareil.", "filesNeverLeave": "Vos fichiers restent sur votre appareil.",
"addMore": "Ajouter dautres fichiers", "addMore": "Ajouter dautres fichiers",
"clearAll": "Tout effacer" "clearAll": "Tout effacer",
"clearFiles": "Effacer les fichiers"
}, },
"loader": { "loader": {
"processing": "Traitement en cours..." "processing": "Traitement en cours..."
@@ -226,7 +227,8 @@
"error": "Erreur", "error": "Erreur",
"success": "Succès", "success": "Succès",
"file": "Fichier", "file": "Fichier",
"files": "Fichiers" "files": "Fichiers",
"close": "Fermer"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF atau Gambar", "pdfOrImages": "PDF atau Gambar",
"filesNeverLeave": "File Anda tidak pernah meninggalkan perangkat Anda.", "filesNeverLeave": "File Anda tidak pernah meninggalkan perangkat Anda.",
"addMore": "Tambah Lebih Banyak File", "addMore": "Tambah Lebih Banyak File",
"clearAll": "Hapus Semua" "clearAll": "Hapus Semua",
"clearFiles": "Hapus file"
}, },
"loader": { "loader": {
"processing": "Memproses..." "processing": "Memproses..."
@@ -226,7 +227,8 @@
"error": "Kesalahan", "error": "Kesalahan",
"success": "Berhasil", "success": "Berhasil",
"file": "File", "file": "File",
"files": "File" "files": "File",
"close": "Tutup"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF o immagini", "pdfOrImages": "PDF o immagini",
"filesNeverLeave": "I tuoi file non lasciano mai il tuo dispositivo.", "filesNeverLeave": "I tuoi file non lasciano mai il tuo dispositivo.",
"addMore": "Aggiungi altri file", "addMore": "Aggiungi altri file",
"clearAll": "Svuota tutto" "clearAll": "Svuota tutto",
"clearFiles": "Cancella file"
}, },
"loader": { "loader": {
"processing": "Elaborazione..." "processing": "Elaborazione..."
@@ -226,7 +227,8 @@
"error": "Errore", "error": "Errore",
"success": "Successo", "success": "Successo",
"file": "File", "file": "File",
"files": "File" "files": "File",
"close": "Chiudi"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -57,7 +57,7 @@
"toolsLabel": "Tools", "toolsLabel": "Tools",
"subtitle": "Klik een tool om de bestandslader te openen", "subtitle": "Klik een tool om de bestandslader te openen",
"searchPlaceholder": "Zoek een tool (bijv., 'splitsen', 'organiseren'...)", "searchPlaceholder": "Zoek een tool (bijv., 'splitsen', 'organiseren'...)",
"backToTools": "Terug naar Tools" "backToTools": "Terug naar Tools",
"firstLoadNotice": "De eerste keer duurt het even om onze conversiemachine te laden. Daarna gaat alles meteen." "firstLoadNotice": "De eerste keer duurt het even om onze conversiemachine te laden. Daarna gaat alles meteen."
}, },
"upload": { "upload": {
@@ -66,7 +66,8 @@
"pdfOrImages": "PDF's of Afbeeldingen", "pdfOrImages": "PDF's of Afbeeldingen",
"filesNeverLeave": "Je bestanden blijven op jouw apparaat.", "filesNeverLeave": "Je bestanden blijven op jouw apparaat.",
"addMore": "Meer bestanden toevoegen", "addMore": "Meer bestanden toevoegen",
"clearAll": "Alles wissen" "clearAll": "Alles wissen",
"clearFiles": "Bestanden wissen"
}, },
"loader": { "loader": {
"processing": "Verwerken..." "processing": "Verwerken..."
@@ -226,7 +227,8 @@
"error": "Fout", "error": "Fout",
"success": "Success", "success": "Success",
"file": "Bestand", "file": "Bestand",
"files": "Bestanden" "files": "Bestanden",
"close": "Sluiten"
}, },
"about": { "about": {
"hero": { "hero": {
@@ -321,4 +323,3 @@
"failedToLoad": "Laden is mislukt" "failedToLoad": "Laden is mislukt"
} }
} }

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDFs ou Imagens", "pdfOrImages": "PDFs ou Imagens",
"filesNeverLeave": "Seus arquivos nunca saem do seu dispositivo.", "filesNeverLeave": "Seus arquivos nunca saem do seu dispositivo.",
"addMore": "Adicionar Mais Arquivos", "addMore": "Adicionar Mais Arquivos",
"clearAll": "Limpar Tudo" "clearAll": "Limpar Tudo",
"clearFiles": "Limpar arquivos"
}, },
"loader": { "loader": {
"processing": "Processando..." "processing": "Processando..."
@@ -226,7 +227,8 @@
"error": "Erro", "error": "Erro",
"success": "Sucesso", "success": "Sucesso",
"file": "Arquivo", "file": "Arquivo",
"files": "Arquivos" "files": "Arquivos",
"close": "Fechar"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF veya Görseller", "pdfOrImages": "PDF veya Görseller",
"filesNeverLeave": "Dosyalarınız cihazınızı asla terk etmez.", "filesNeverLeave": "Dosyalarınız cihazınızı asla terk etmez.",
"addMore": "Daha Fazla Dosya Ekle", "addMore": "Daha Fazla Dosya Ekle",
"clearAll": "Tümünü Temizle" "clearAll": "Tümünü Temizle",
"clearFiles": "Dosyaları temizle"
}, },
"loader": { "loader": {
"processing": "İşleniyor..." "processing": "İşleniyor..."
@@ -226,7 +227,8 @@
"error": "Hata", "error": "Hata",
"success": "Başarılı", "success": "Başarılı",
"file": "Dosya", "file": "Dosya",
"files": "Dosya" "files": "Dosya",
"close": "Kapat"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF hoặc Hình ảnh", "pdfOrImages": "PDF hoặc Hình ảnh",
"filesNeverLeave": "Tệp của bạn không bao giờ rời khỏi thiết bị.", "filesNeverLeave": "Tệp của bạn không bao giờ rời khỏi thiết bị.",
"addMore": "Thêm tệp", "addMore": "Thêm tệp",
"clearAll": "Xóa tất cả" "clearAll": "Xóa tất cả",
"clearFiles": "Xóa tệp"
}, },
"loader": { "loader": {
"processing": "Đang xử lý..." "processing": "Đang xử lý..."
@@ -226,7 +227,8 @@
"error": "Lỗi", "error": "Lỗi",
"success": "Thành công", "success": "Thành công",
"file": "Tệp", "file": "Tệp",
"files": "Tệp" "files": "Tệp",
"close": "Đóng"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF 或圖片", "pdfOrImages": "PDF 或圖片",
"filesNeverLeave": "你的檔案永遠不會離開你的裝置。", "filesNeverLeave": "你的檔案永遠不會離開你的裝置。",
"addMore": "添加更多檔案", "addMore": "添加更多檔案",
"clearAll": "清除全部" "clearAll": "清除全部",
"clearFiles": "清除檔案"
}, },
"loader": { "loader": {
"processing": "正在處理..." "processing": "正在處理..."
@@ -226,7 +227,8 @@
"error": "錯誤", "error": "錯誤",
"success": "成功", "success": "成功",
"file": "檔案", "file": "檔案",
"files": "檔案" "files": "檔案",
"close": "關閉"
}, },
"about": { "about": {
"hero": { "hero": {

View File

@@ -66,7 +66,8 @@
"pdfOrImages": "PDF 或图片", "pdfOrImages": "PDF 或图片",
"filesNeverLeave": "您的文件从未离开您的设备。", "filesNeverLeave": "您的文件从未离开您的设备。",
"addMore": "添加更多文件", "addMore": "添加更多文件",
"clearAll": "清空所有" "clearAll": "清空所有",
"clearFiles": "清除文件"
}, },
"loader": { "loader": {
"processing": "处理中..." "processing": "处理中..."
@@ -226,7 +227,8 @@
"error": "错误", "error": "错误",
"success": "成功", "success": "成功",
"file": "文件", "file": "文件",
"files": "文件" "files": "文件",
"close": "关闭"
}, },
"about": { "about": {
"hero": { "hero": {

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,9 @@
* Version: 1.1.0 * Version: 1.1.0
*/ */
const CACHE_VERSION = 'bentopdf-v7'; const CACHE_VERSION = 'bentopdf-v8';
const CACHE_NAME = `${CACHE_VERSION}-static`; const CACHE_NAME = `${CACHE_VERSION}-static`;
const getBasePath = () => { const getBasePath = () => {
const scope = self.registration?.scope || self.location.href; const scope = self.registration?.scope || self.location.href;
const url = new URL(scope); const url = new URL(scope);
@@ -44,7 +43,8 @@ self.addEventListener('install', (event) => {
// console.log('📦 [ServiceWorker] Will cache', CRITICAL_ASSETS.length, 'critical assets'); // console.log('📦 [ServiceWorker] Will cache', CRITICAL_ASSETS.length, 'critical assets');
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME) caches
.open(CACHE_NAME)
.then((cache) => { .then((cache) => {
// console.log('[ServiceWorker] Caching critical assets...'); // console.log('[ServiceWorker] Caching critical assets...');
return cacheInBatches(cache, CRITICAL_ASSETS, 5); return cacheInBatches(cache, CRITICAL_ASSETS, 5);
@@ -64,7 +64,8 @@ self.addEventListener('activate', (event) => {
// console.log('🔄 [ServiceWorker] Activating version:', CACHE_VERSION); // console.log('🔄 [ServiceWorker] Activating version:', CACHE_VERSION);
event.waitUntil( event.waitUntil(
caches.keys() caches
.keys()
.then((cacheNames) => { .then((cacheNames) => {
return Promise.all( return Promise.all(
cacheNames.map((cacheName) => { cacheNames.map((cacheName) => {
@@ -92,18 +93,33 @@ self.addEventListener('fetch', (event) => {
if (!isLocal && !isCDN) { if (!isLocal && !isCDN) {
return; return;
} }
if (isLocal && (url.searchParams.has('t') || url.searchParams.has('import') || url.searchParams.has('direct'))) { if (
isLocal &&
(url.searchParams.has('t') ||
url.searchParams.has('import') ||
url.searchParams.has('direct'))
) {
// console.log('🔧 [Dev Mode] Skipping Vite HMR request:', url.pathname); // console.log('🔧 [Dev Mode] Skipping Vite HMR request:', url.pathname);
return; return;
} }
if (isLocal && (url.pathname.includes('/@vite') || url.pathname.includes('/@id') || url.pathname.includes('/@fs'))) { if (
isLocal &&
(url.pathname.includes('/@vite') ||
url.pathname.includes('/@id') ||
url.pathname.includes('/@fs'))
) {
return; return;
} }
if (shouldCache(url.pathname, isCDN)) { if (isLocal && url.pathname.includes('/locales/')) {
event.respondWith(networkFirstStrategy(event.request));
} else if (shouldCache(url.pathname, isCDN)) {
event.respondWith(cacheFirstStrategyWithDedup(event.request, isCDN)); event.respondWith(cacheFirstStrategyWithDedup(event.request, isCDN));
} else if (isLocal && (url.pathname.endsWith('.html') || url.pathname === '/')) { } else if (
isLocal &&
(url.pathname.endsWith('.html') || url.pathname === '/')
) {
event.respondWith(networkFirstStrategy(event.request)); event.respondWith(networkFirstStrategy(event.request));
} }
}); });
@@ -154,7 +170,10 @@ async function cacheFirstStrategyWithDedup(request, isCDN) {
return fallbackResponse; return fallbackResponse;
} }
} catch (fallbackError) { } catch (fallbackError) {
console.error('[ServiceWorker] Both CDN and local failed for:', fileName); console.error(
'[ServiceWorker] Both CDN and local failed for:',
fileName
);
} }
} }
} }
@@ -252,7 +271,9 @@ function shouldCache(pathname, isCDN = false) {
pathname.includes('/ghostscript-wasm/') || pathname.includes('/ghostscript-wasm/') ||
pathname.includes('/embedpdf/') || pathname.includes('/embedpdf/') ||
pathname.includes('/assets/') || pathname.includes('/assets/') ||
pathname.match(/\.(js|mjs|css|wasm|whl|zip|json|png|jpg|jpeg|gif|svg|woff|woff2|ttf|gz|br)$/) pathname.match(
/\.(js|mjs|css|wasm|whl|zip|json|png|jpg|jpeg|gif|svg|woff|woff2|ttf|gz|br)$/
)
); );
} }
@@ -269,7 +290,10 @@ async function cacheInBatches(cache, urls, batchSize = 5) {
try { try {
await cache.add(url); await cache.add(url);
const fileName = url.split('/').pop(); const fileName = url.split('/').pop();
const fileSize = fileName.includes('.wasm') || fileName.includes('.whl') ? '(large file)' : ''; const fileSize =
fileName.includes('.wasm') || fileName.includes('.whl')
? '(large file)'
: '';
// console.log(` ✓ Cached: ${fileName} ${fileSize}`); // console.log(` ✓ Cached: ${fileName} ${fileSize}`);
} catch (error) { } catch (error) {
console.warn('[ServiceWorker] Failed to cache:', url, error.message); console.warn('[ServiceWorker] Failed to cache:', url, error.message);

View File

@@ -1,5 +1,4 @@
import i18next from 'i18next'; import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend'; import HttpBackend from 'i18next-http-backend';
// Supported languages // Supported languages
@@ -48,7 +47,6 @@ export const getLanguageFromUrl = (): SupportedLanguage => {
const langMatch = path.match( const langMatch = path.match(
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl)(?:\/|$)/ /^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl)(?:\/|$)/
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(?:\/|$)/
); );
if ( if (
langMatch && langMatch &&
@@ -75,28 +73,25 @@ export const initI18n = async (): Promise<typeof i18next> => {
const currentLang = getLanguageFromUrl(); const currentLang = getLanguageFromUrl();
await i18next localStorage.setItem('i18nextLng', currentLang);
.use(HttpBackend)
.use(LanguageDetector) await i18next.use(HttpBackend).init({
.init({
lng: currentLang, lng: currentLang,
fallbackLng: 'en', fallbackLng: 'en',
supportedLngs: supportedLanguages as unknown as string[], supportedLngs: supportedLanguages as unknown as string[],
ns: ['common', 'tools'], ns: ['common', 'tools'],
defaultNS: 'common', defaultNS: 'common',
preload: [currentLang],
backend: { backend: {
loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`, loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`,
}, },
detection: {
order: ['path', 'localStorage', 'navigator'],
lookupFromPathIndex: 0,
caches: ['localStorage'],
},
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },
}); });
await i18next.loadNamespaces('tools');
initialized = true; initialized = true;
return i18next; return i18next;
}; };
@@ -122,7 +117,7 @@ export const changeLanguage = (lang: SupportedLanguage): void => {
let pagePathWithoutLang = relativePath; let pagePathWithoutLang = relativePath;
const langPrefixMatch = relativePath.match( const langPrefixMatch = relativePath.match(
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(\/.*)?$/ /^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl)(\/.*)?$/
); );
if (langPrefixMatch) { if (langPrefixMatch) {
pagePathWithoutLang = langPrefixMatch[2] || '/'; pagePathWithoutLang = langPrefixMatch[2] || '/';
@@ -237,7 +232,7 @@ export const rewriteLinks = (): void => {
newHref = `/${currentLang}/`; newHref = `/${currentLang}/`;
} }
} else { } else {
newHref = `${currentLang}/${href}`; newHref = `/${currentLang}/${href}`;
} }
newHref = newHref.replace(/([^:])\/+/g, '$1/'); newHref = newHref.replace(/([^:])\/+/g, '$1/');

View File

@@ -1,6 +1,9 @@
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
/** /**
* Configuration for progressive rendering * Configuration for progressive rendering
@@ -25,7 +28,12 @@ interface PageTask {
fileName?: string; fileName?: string;
container: HTMLElement; container: HTMLElement;
scale?: number; scale?: number;
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement; createWrapper: (
canvas: HTMLCanvasElement,
pageNumber: number,
fileName?: string
) => HTMLElement;
placeholderElement?: HTMLElement;
} }
/** /**
@@ -34,6 +42,7 @@ interface PageTask {
interface LazyLoadState { interface LazyLoadState {
observer: IntersectionObserver | null; observer: IntersectionObserver | null;
pendingTasks: Map<HTMLElement, PageTask>; pendingTasks: Map<HTMLElement, PageTask>;
pendingTasksByPageNumber: Map<number, PageTask>;
isRendering: boolean; isRendering: boolean;
eagerLoadQueue: PageTask[]; eagerLoadQueue: PageTask[];
nextEagerIndex: number; nextEagerIndex: number;
@@ -42,6 +51,7 @@ interface LazyLoadState {
const lazyLoadState: LazyLoadState = { const lazyLoadState: LazyLoadState = {
observer: null, observer: null,
pendingTasks: new Map(), pendingTasks: new Map(),
pendingTasksByPageNumber: new Map(),
isRendering: false, isRendering: false,
eagerLoadQueue: [], eagerLoadQueue: [],
nextEagerIndex: 0, nextEagerIndex: 0,
@@ -50,7 +60,10 @@ const lazyLoadState: LazyLoadState = {
/** /**
* Creates a placeholder element for a page that will be lazy-loaded * Creates a placeholder element for a page that will be lazy-loaded
*/ */
export function createPlaceholder(pageNumber: number, fileName?: string): HTMLElement { export function createPlaceholder(
pageNumber: number,
fileName?: string
): HTMLElement {
const placeholder = document.createElement('div'); const placeholder = document.createElement('div');
placeholder.className = placeholder.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 rounded-lg bg-gray-800 transition-colors'; 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 rounded-lg bg-gray-800 transition-colors';
@@ -62,7 +75,8 @@ export function createPlaceholder(pageNumber: number, fileName?: string): HTMLEl
// Create skeleton loader // Create skeleton loader
const skeletonContainer = document.createElement('div'); const skeletonContainer = document.createElement('div');
skeletonContainer.className = 'relative w-full h-36 bg-gray-700 rounded-md animate-pulse flex items-center justify-center'; skeletonContainer.className =
'relative w-full h-36 bg-gray-700 rounded-md animate-pulse flex items-center justify-center';
const loadingText = document.createElement('span'); const loadingText = document.createElement('span');
loadingText.className = 'text-gray-500 text-xs'; loadingText.className = 'text-gray-500 text-xs';
@@ -107,7 +121,7 @@ async function renderPageBatch(
tasks: PageTask[], tasks: PageTask[],
onProgress?: (current: number, total: number) => void onProgress?: (current: number, total: number) => void
): Promise<void> { ): Promise<void> {
const renderPromises = tasks.map(async (task) => { for (const task of tasks) {
try { try {
const canvas = await renderPageToCanvas( const canvas = await renderPageToCanvas(
task.pdfjsDoc, task.pdfjsDoc,
@@ -115,30 +129,50 @@ async function renderPageBatch(
task.scale || 0.5 task.scale || 0.5
); );
const wrapper = task.createWrapper(canvas, task.pageNumber, task.fileName); const wrapper = task.createWrapper(
canvas,
// Find and replace the placeholder for this specific page number task.pageNumber,
const placeholder = task.container.querySelector( task.fileName
`[data-page-number="${task.pageNumber}"][data-lazy-load="true"]`
); );
if (placeholder) { let placeholder: Element | null = task.placeholderElement || null;
// Replace placeholder with rendered page if (!placeholder) {
task.container.replaceChild(wrapper, placeholder); placeholder = task.container.querySelector(
`[data-page-number="${task.pageNumber}"][data-lazy-load="true"]`
);
}
if (placeholder && placeholder.parentNode) {
const parent = placeholder.parentNode;
parent.insertBefore(wrapper, placeholder);
parent.removeChild(placeholder);
} else {
const allChildren = Array.from(
task.container.children
) as HTMLElement[];
let insertBefore: Element | null = null;
for (const child of allChildren) {
const childPageNum = parseInt(child.dataset.pageNumber || '0', 10);
if (childPageNum > task.pageNumber) {
insertBefore = child;
break;
}
}
if (insertBefore) {
task.container.insertBefore(wrapper, insertBefore);
} else { } else {
// Fallback: shouldn't happen with new approach, but just in case
console.warn(`No placeholder found for page ${task.pageNumber}, appending instead`);
task.container.appendChild(wrapper); task.container.appendChild(wrapper);
} }
console.warn(
return wrapper; `Placeholder not found for page ${task.pageNumber}, inserted at calculated position`
);
}
} catch (error) { } catch (error) {
console.error(`Error rendering page ${task.pageNumber}:`, error); console.error(`Error rendering page ${task.pageNumber}:`, error);
return null;
} }
}); }
await Promise.all(renderPromises);
} }
/** /**
@@ -158,12 +192,19 @@ function setupLazyRendering(
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
const placeholder = entry.target as HTMLElement; const placeholder = entry.target as HTMLElement;
const task = lazyLoadState.pendingTasks.get(placeholder); const pageNumberStr = placeholder.dataset.pageNumber;
if (!pageNumberStr) return;
const pageNumber = parseInt(pageNumberStr, 10);
const task = lazyLoadState.pendingTasksByPageNumber.get(pageNumber);
if (task) { if (task) {
// Immediately unobserve to prevent multiple triggers // Immediately unobserve to prevent multiple triggers
observer.unobserve(placeholder); observer.unobserve(placeholder);
lazyLoadState.pendingTasks.delete(placeholder); lazyLoadState.pendingTasks.delete(placeholder);
lazyLoadState.pendingTasksByPageNumber.delete(pageNumber);
task.placeholderElement = placeholder;
// Render this page immediately (not waiting for isRendering flag) // Render this page immediately (not waiting for isRendering flag)
renderPageBatch([task], config.onProgress) renderPageBatch([task], config.onProgress)
@@ -174,13 +215,19 @@ function setupLazyRendering(
} }
// Check if all pages are rendered // Check if all pages are rendered
if (lazyLoadState.pendingTasks.size === 0 && lazyLoadState.observer) { if (
lazyLoadState.pendingTasks.size === 0 &&
lazyLoadState.observer
) {
lazyLoadState.observer.disconnect(); lazyLoadState.observer.disconnect();
lazyLoadState.observer = null; lazyLoadState.observer = null;
} }
}) })
.catch((error) => { .catch((error) => {
console.error(`Error lazy loading page ${task.pageNumber}:`, error); console.error(
`Error lazy loading page ${task.pageNumber}:`,
error
);
}); });
} }
} }
@@ -208,7 +255,11 @@ function requestIdleCallbackPolyfill(callback: () => void): void {
export async function renderPagesProgressively( export async function renderPagesProgressively(
pdfjsDoc: any, pdfjsDoc: any,
container: HTMLElement, container: HTMLElement,
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement, createWrapper: (
canvas: HTMLCanvasElement,
pageNumber: number,
fileName?: string
) => HTMLElement,
config: RenderConfig = {} config: RenderConfig = {}
): Promise<void> { ): Promise<void> {
const { const {
@@ -236,7 +287,7 @@ export async function renderPagesProgressively(
const tasks: PageTask[] = []; const tasks: PageTask[] = [];
// Create tasks for all pages // Create tasks for all pages with direct placeholder references
for (let i = 1; i <= totalPages; i++) { for (let i = 1; i <= totalPages; i++) {
tasks.push({ tasks.push({
pageNumber: i, pageNumber: i,
@@ -244,6 +295,7 @@ export async function renderPagesProgressively(
container, container,
scale: config.useLazyLoading ? 0.3 : 0.5, scale: config.useLazyLoading ? 0.3 : 0.5,
createWrapper, createWrapper,
placeholderElement: placeholders[i - 1],
}); });
} }
@@ -253,15 +305,17 @@ export async function renderPagesProgressively(
for (let i = initialRenderCount + 1; i <= totalPages; i++) { for (let i = initialRenderCount + 1; i <= totalPages; i++) {
const placeholder = placeholders[i - 1]; const placeholder = placeholders[i - 1];
const task = tasks[i - 1];
// Store the task for lazy rendering // Store the task for lazy rendering
lazyLoadState.pendingTasks.set(placeholder, tasks[i - 1]); lazyLoadState.pendingTasks.set(placeholder, task);
lazyLoadState.pendingTasksByPageNumber.set(task.pageNumber, task);
observer.observe(placeholder); observer.observe(placeholder);
} }
// Prepare eager load queue // Prepare eager load queue
const eagerStartIndex = initialRenderCount; const eagerStartIndex = initialRenderCount;
const eagerEndIndex = Math.min( const eagerEndIndex = Math.min(
eagerStartIndex + (eagerLoadBatches * batchSize), eagerStartIndex + eagerLoadBatches * batchSize,
totalPages totalPages
); );
lazyLoadState.eagerLoadQueue = tasks.slice(eagerStartIndex, eagerEndIndex); lazyLoadState.eagerLoadQueue = tasks.slice(eagerStartIndex, eagerEndIndex);
@@ -294,7 +348,11 @@ export async function renderPagesProgressively(
} }
// Start eager loading AFTER initial batch is complete // Start eager loading AFTER initial batch is complete
if (useLazyLoading && eagerLoadBatches > 0 && totalPages > initialRenderCount) { if (
useLazyLoading &&
eagerLoadBatches > 0 &&
totalPages > initialRenderCount
) {
renderEagerBatch(config); renderEagerBatch(config);
} }
} }
@@ -311,6 +369,7 @@ export function observePlaceholder(
return; return;
} }
lazyLoadState.pendingTasks.set(placeholder, task); lazyLoadState.pendingTasks.set(placeholder, task);
lazyLoadState.pendingTasksByPageNumber.set(task.pageNumber, task);
lazyLoadState.observer.observe(placeholder); lazyLoadState.observer.observe(placeholder);
} }
@@ -339,12 +398,12 @@ function renderEagerBatch(config: RenderConfig): void {
if (config.shouldCancel?.()) return; if (config.shouldCancel?.()) return;
// Remove these tasks from pending since we're rendering them eagerly // Remove these tasks from pending since we're rendering them eagerly
batch.forEach(task => { batch.forEach((task) => {
const placeholder = Array.from(lazyLoadState.pendingTasks.entries()) const placeholder = task.placeholderElement;
.find(([_, t]) => t.pageNumber === task.pageNumber)?.[0];
if (placeholder && lazyLoadState.observer) { if (placeholder && lazyLoadState.observer) {
lazyLoadState.observer.unobserve(placeholder); lazyLoadState.observer.unobserve(placeholder);
lazyLoadState.pendingTasks.delete(placeholder); lazyLoadState.pendingTasks.delete(placeholder);
lazyLoadState.pendingTasksByPageNumber.delete(task.pageNumber);
} }
}); });
@@ -358,7 +417,9 @@ function renderEagerBatch(config: RenderConfig): void {
lazyLoadState.nextEagerIndex = batchEnd; lazyLoadState.nextEagerIndex = batchEnd;
// Queue next eager batch // Queue next eager batch
const remainingBatches = Math.ceil((eagerLoadQueue.length - batchEnd) / batchSize); const remainingBatches = Math.ceil(
(eagerLoadQueue.length - batchEnd) / batchSize
);
if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) { if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) {
// Continue eager loading if we have more batches within the eager threshold // Continue eager loading if we have more batches within the eager threshold
renderEagerBatch(config); renderEagerBatch(config);
@@ -375,6 +436,7 @@ export function cleanupLazyRendering(): void {
lazyLoadState.observer = null; lazyLoadState.observer = null;
} }
lazyLoadState.pendingTasks.clear(); lazyLoadState.pendingTasks.clear();
lazyLoadState.pendingTasksByPageNumber.clear();
lazyLoadState.isRendering = false; lazyLoadState.isRendering = false;
lazyLoadState.eagerLoadQueue = []; lazyLoadState.eagerLoadQueue = [];
lazyLoadState.nextEagerIndex = 0; lazyLoadState.nextEagerIndex = 0;