// ============================================================= // FILE: 300040017.js // PURPOSE: Main Controller with STRICT SINGLETON LOCK // ============================================================= class Dings_Pdf_Viewer_Class extends Dings_App_Class { constructor(Dings_Json, Html_Id) { super(Dings_Json, Html_Id); // --- NUCLEAR SINGLETON LOCK --- // This prevents the class from EVER running twice on the same page load. if (window.DINGS_GLOBAL_LOCK === true) { console.warn("⛔ DUPLICATE INSTANCE BLOCKED: Dings_Pdf_Viewer_Class tried to init twice."); return; } window.DINGS_GLOBAL_LOCK = true; // Lock it immediately this.Html_Id = Html_Id; console.log("URL: " + window.location.href); this.highlightsDismissed = false; this.initialScrollComplete = false; this.abortController = null; this.layoutContainer = document.getElementById('layout-container'); this.editorContainer = document.getElementById('editor-container'); if (this.layoutContainer && this.editorContainer) { // Double check: Clear DOM to remove any ghost iframes from previous attempts this.layoutContainer.innerHTML = ''; this.editorContainer.innerHTML = ''; this.Initialize_Hocr_Proofreader(); } else { console.error("Required containers not found."); } let Dict = Dings_Lib.Dict_From_Url(Html_Id); console.log("Dict: " + Dict); } Update() { console.log("Update") } Initialize_Hocr_Proofreader() { // 1. ABORT SIGNAL SETUP this.abortController = new AbortController(); const signal = this.abortController.signal; const hocrProofreader = new HocrProofreader({ layoutContainer: 'layout-container', editorContainer: 'editor-container' }); this.hocrProofreader = hocrProofreader; this.Setup_Save_Delete_Undo(); this.Setup_Reset_Button(); this.Setup_Load_Edited_Button(); // UI Controls - Use Cloning to strip old listeners if they exist const zoomFull = document.getElementById('zoom-page-full'); const zoomWidth = document.getElementById('zoom-page-width'); if(zoomFull) { const newZoomFull = zoomFull.cloneNode(true); zoomFull.parentNode.replaceChild(newZoomFull, zoomFull); newZoomFull.addEventListener('click', () => hocrProofreader.toggleFullscreen("layout-container")); } if(zoomWidth) { const newZoomWidth = zoomWidth.cloneNode(true); zoomWidth.parentNode.replaceChild(newZoomWidth, zoomWidth); newZoomWidth.addEventListener('click', () => hocrProofreader.toggleIframeFullscreen("editor-container")); } const useProgressive = !!(window.DINGS_USE_PROGRESSIVE); if (useProgressive) { this.Progressive_Load_Hocr(hocrProofreader, { baseUrl: './', batchUpdate: 5, maxPrefetchInitial: 10, signal: signal }); const boundTryScroll = this.Try_Scroll_From_Url_State.bind(this); window.addEventListener('hocr:rendered', boundTryScroll, { passive: true }); } else { this.Detect_Total_Pages((hocrFiles) => { this.Load_All_Hocr_Files(hocrFiles, hocrProofreader); }); } } // --- STANDARD LOADER LOGIC --- Detect_Total_Pages(callback) { let page = 1; let hocrBaseUrl = './'; let pages = []; const checkNextPage = () => { const hocrUrl = `${hocrBaseUrl}${buildHocrFileName(page)}`; Util.get(hocrUrl, (err, hocr) => { if (err || !hocr || !hocr.includes('ocr_page')) { callback(pages); return; } pages.push(hocrUrl); page++; checkNextPage(); }); }; checkNextPage(); } Load_All_Hocr_Files(hocrFiles, hocrProofreader) { let loadedPages = 0; let combinedHocrContent = ''; hocrFiles.forEach(hocrUrl => { Util.get(hocrUrl, function (err, hocr) { if (err) { loadedPages++; return; } combinedHocrContent += hocr; loadedPages++; if (loadedPages === hocrFiles.length) { hocrProofreader.setHocr(combinedHocrContent, './'); setTimeout(() => { window.dispatchEvent(new CustomEvent('hocr:rendered')); }, 200); } }); }); } // --- PROGRESSIVE LOADER (With Smart Edited File Detection) --- Progressive_Load_Hocr(hocrProofreader, opts = {}) { const baseUrl = opts.baseUrl || './'; const batchUpdate = Math.max(1, opts.batchUpdate || 5); const maxPrefetchInitial = Math.max(1, opts.maxPrefetchInitial || 10); const signal = opts.signal; let combined = ''; let nextPageToFetch = 1; let fetching = false; let endReached = false; let renderedOnce = false; let batchCount = 0; const isValidHocr = (content) => { return content && content.length > 0 && content.includes('ocr_page'); }; const flushRender = () => { if (signal && signal.aborted) return; if (!combined) return; const iframe = document.querySelector('.editor'); if (!iframe) return; let savedScrollTop = 0; let savedScrollLeft = 0; if (iframe.contentWindow && iframe.contentWindow.document) { try { const doc = iframe.contentWindow.document; savedScrollTop = iframe.contentWindow.pageYOffset || doc.documentElement.scrollTop || 0; savedScrollLeft = iframe.contentWindow.pageXOffset || doc.documentElement.scrollLeft || 0; } catch(e) {} } setTimeout(() => { if (signal && signal.aborted) return; hocrProofreader.setHocr(combined, baseUrl); if (savedScrollTop > 0 && iframe && iframe.contentWindow) { try { iframe.contentWindow.scrollTo(savedScrollLeft, savedScrollTop); } catch(e) {} } window.dispatchEvent(new CustomEvent('hocr:rendered')); if (!this.initialScrollComplete) this.Try_Scroll_From_Url_State(); renderedOnce = true; }, 0); }; const fetchBatch = (count) => { if (signal && signal.aborted) return; if (fetching || endReached) return; fetching = true; let loadedInThisBatch = 0; const loadNext = () => { if (signal && signal.aborted) return; if (loadedInThisBatch >= count || endReached) { fetching = false; if (loadedInThisBatch > 0) flushRender(); return; } const n = nextPageToFetch; // Cache busting to ensure we get latest files const ts = new Date().getTime(); const originalUrl = `${baseUrl}${buildHocrFileName(n)}?t=${ts}`; const editedUrl = `${baseUrl}${buildHocrFileName(n, '_edited')}?t=${ts}`; // 1. Check for Edited File First Util.get(editedUrl, (err, hocrEdited) => { if (signal && signal.aborted) return; if (!err && isValidHocr(hocrEdited)) { combined += hocrEdited; proceedToNextPage(); } else { // 2. Fallback to Original File Util.get(originalUrl, (errOrig, hocrOriginal) => { if (signal && signal.aborted) return; if (!errOrig && isValidHocr(hocrOriginal)) { combined += hocrOriginal; proceedToNextPage(); } else { endReached = true; fetching = false; if (loadedInThisBatch > 0) flushRender(); } }); } }); function proceedToNextPage() { if (endReached) return; nextPageToFetch++; loadedInThisBatch++; batchCount++; if (!renderedOnce || (batchCount % batchUpdate === 0)) flushRender(); loadNext(); } }; loadNext(); }; // Start Initial Fetch fetchBatch(maxPrefetchInitial); // Infinite Scroll const iframe = document.querySelector('.editor'); if (iframe) { const onScroll = () => { if (endReached || (signal && signal.aborted)) return; try { const doc = iframe.contentDocument || iframe.contentWindow?.document; if (!doc) return; const scroller = doc.scrollingElement || doc.documentElement || doc.body; const pos = (scroller.scrollTop + scroller.clientHeight) / Math.max(1, scroller.scrollHeight); if (pos > 0.85) fetchBatch(maxPrefetchInitial); } catch {} }; const attachScroll = () => { try { iframe.contentWindow.addEventListener('scroll', onScroll, { passive: true }); } catch {} }; if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') attachScroll(); else iframe.addEventListener('load', attachScroll, { once: true }); } } // --- DEEP LINK LOGIC --- Try_Scroll_From_Url_State() { try { if (this.highlightsDismissed) return; const state = Dings_Lib.Dict_From_Url(this.Html_Id) || {}; const bboxList = Array.isArray(state.BBox) ? state.BBox : null; if (!bboxList || bboxList.length === 0) return; const iframe = document.querySelector('.editor'); if (!iframe) return; const doc = iframe.contentDocument || iframe.contentWindow?.document; if (!doc) return; const getBBoxString = (titleAttr) => { if (!titleAttr) return null; const m = titleAttr.match(/bbox\s+([\d\s]+)/); return m ? m[1].trim() : null; }; const findNodes = () => { const nodes = Array.from(doc.querySelectorAll('[title*="bbox"]')); const targetSet = new Set(bboxList.map(s => String(s).trim())); return nodes.filter(n => targetSet.has(getBBoxString(n.getAttribute('title')))); }; const targets = findNodes(); if (targets.length > 0) { targets.forEach(t => { t.classList.add('rag-highlight', 'rag-target'); t.style.backgroundColor = 'yellow'; t.style.outline = '2px solid #ff6600'; }); if (!doc.getElementById("remove-highlight-btn")) { const removeBtn = doc.createElement("button"); removeBtn.id = "remove-highlight-btn"; removeBtn.textContent = "Remove Highlight"; Object.assign(removeBtn.style, { position: "fixed", top: "10px", right: "10px", backgroundColor: "red", color: "white", border: "none", padding: "8px", cursor: "pointer", zIndex: "1000" }); removeBtn.onclick = () => { this.highlightsDismissed = true; const currentTargets = findNodes(); currentTargets.forEach(el => { el.style.backgroundColor = ''; el.style.outline = ''; el.classList.remove('rag-highlight', 'rag-target'); }); removeBtn.remove(); }; doc.body.appendChild(removeBtn); } const win = iframe.contentWindow; const currentScroll = win.pageYOffset || doc.documentElement.scrollTop || 0; if (!this.initialScrollComplete || currentScroll < 10) { this.Scroll_To_BBoxesIn_Iframe(iframe, bboxList); } } } catch (e) { console.warn('Deep-link error:', e); } } Scroll_To_BBoxesIn_Iframe(iframe, bboxList) { let attempts = 0; const maxAttempts = 20; const attemptScroll = () => { try { const win = iframe.contentWindow; const doc = win.document; if (!doc || !doc.body) return; const getBBoxString = (titleAttr) => { if (!titleAttr) return null; const m = titleAttr.match(/bbox\s+([\d\s]+)/); return m ? m[1].trim() : null; }; const nodes = Array.from(doc.querySelectorAll('[title*="bbox"]')); const targetSet = new Set(bboxList.map(s => String(s).trim())); const liveTargets = nodes.filter(n => targetSet.has(getBBoxString(n.getAttribute('title')))); if (liveTargets.length === 0) { if (attempts < maxAttempts) { attempts++; setTimeout(attemptScroll, 100); } return; } const target = liveTargets[0]; const _forceReflow = target.offsetTop; const rect = target.getBoundingClientRect(); const currentScrollY = win.pageYOffset || doc.documentElement.scrollTop || doc.body.scrollTop || 0; const absoluteElementTop = rect.top + currentScrollY; const iframeHeight = iframe.clientHeight; const desiredScroll = absoluteElementTop - (iframeHeight / 2) + (rect.height / 2); const maxScroll = Math.max( doc.body.scrollHeight, doc.body.offsetHeight, doc.documentElement.clientHeight ) - win.innerHeight; const validDesired = Math.max(0, Math.min(desiredScroll, maxScroll)); win.scrollTo({ top: validDesired, left: 0, behavior: 'auto' }); if (Math.abs(currentScrollY - validDesired) > 10) { if (attempts < maxAttempts) { attempts++; setTimeout(attemptScroll, 100); return; } } this.initialScrollComplete = true; } catch(e) { console.warn("Scroll error:", e); } }; setTimeout(attemptScroll, 100); } Get_State_Dict() { let State_Dict = {}; const iframe = document.querySelector('.editor'); if (!iframe) return State_Dict; const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (!iframeDoc) return State_Dict; const selection = iframeDoc.getSelection ? iframeDoc.getSelection() : null; if (!selection || !selection.toString().trim()) return State_Dict; State_Dict["SelectedText"] = selection.toString().trim(); let bboxList = []; try { let BBox_Selected = false for (let i = 0; i < selection.rangeCount; i++) { const range = selection.getRangeAt(i); const selectedNodes = range.cloneContents().querySelectorAll("[title*='bbox']"); selectedNodes.forEach(node => { BBox_Selected = true if (node.hasAttribute("title")) { const titleAttr = node.getAttribute("title"); const bboxMatch = titleAttr.match(/bbox\s+([\d\s]+)/); if (bboxMatch) bboxList.push(bboxMatch[1].trim()); } }); } if (BBox_Selected) console.log("BBox: Selected=TRUE") else console.log("BBox: Selected=FALSE") } catch (error) {} if (bboxList.length > 0) State_Dict["BBox"] = bboxList; return State_Dict; } // --- SAVE / DELETE / UNDO (Fixed Page Logic) --- Setup_Save_Delete_Undo() { const saveButton = document.getElementById('save-hocr-btn'); const undoButton = document.getElementById('undo-delete-btn'); saveButton.addEventListener('click', () => { const iframe = document.querySelector('.editor'); if (!iframe || !iframe.contentDocument) { alert("Editor not initialized."); return; } const doc = iframe.contentDocument; const win = iframe.contentWindow; const allPages = Array.from(doc.querySelectorAll('.ocr_page')); if (allPages.length === 0) { alert("No pages found to save."); return; } let activePageNode = null; // Priority 1: Text Cursor const selection = doc.getSelection(); if (selection && selection.rangeCount > 0 && selection.anchorNode) { let node = selection.anchorNode; while (node && node !== doc.body) { if (node.nodeType === 1 && node.classList.contains('ocr_page')) { activePageNode = node; break; } node = node.parentNode; } } // Priority 2: Scroll Center if (!activePageNode) { const viewportCenterY = win.innerHeight / 2; let closestPage = null; let minDistance = Infinity; allPages.forEach(page => { const rect = page.getBoundingClientRect(); const pageCenterY = rect.top + (rect.height / 2); const distance = Math.abs(pageCenterY - viewportCenterY); if (distance < minDistance) { minDistance = distance; closestPage = page; } }); activePageNode = closestPage; } if (!activePageNode) activePageNode = allPages[0]; // KEY FIX: Determine Page Number by INDEX const pageIndex = allPages.indexOf(activePageNode); const pageNum = pageIndex + 1; const filename = buildHocrFileName(pageNum, '_edited'); const clone = activePageNode.cloneNode(true); const editables = clone.querySelectorAll('[contenteditable]'); editables.forEach(el => el.removeAttribute('contenteditable')); clone.removeAttribute('contenteditable'); const pageContent = ` page_${pageNum} ${clone.outerHTML} `; let Command = JSON.stringify({ action: 'save', filename: filename, content: pageContent }) Dings_Lib.Api_Call_Func("Hocr_Action", Command).then(Result => { console.log("Result Func: " + Result) if (Result.result.status === 'success') { alert(`Saved Page ${pageNum}: ${filename}`); } else { alert(`Save failed: ${Result.result.message}`); } }); }); undoButton.addEventListener('click', () => { if (!window.lastDeletedFile) { alert("No deleted file to restore."); return; } let Command = JSON.stringify({ action: 'undo_delete', backup_filename: window.lastDeletedFile.backup, original_filename: window.lastDeletedFile.original }) Dings_Lib.Api_Call_Func("Hocr_Action", Command).then(Result => { console.log("Result Func: " + Result) if (Result.result.status === 'success') { alert(`Restored: ${Result.result.message}`) } else { alert(`Undo failed: ${Result.result.message}`) } }); /* XXX fetch('300040017.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'undo_delete', backup_filename: window.lastDeletedFile.backup, original_filename: window.lastDeletedFile.original }) }).then(res => res.json()).then(data => { if (data.status === 'success') { alert(`Restored: ${data.message}`); window.lastDeletedFile = null; } else { alert(`Undo failed: ${data.message}`); } }); */ }); } // --- HELPER: HOT SWAP PAGE CONTENT --- Hot_Swap_Page_Content(pageNode, hocrContent) { const parser = new DOMParser(); const newDoc = parser.parseFromString(hocrContent, 'text/html'); const newPageNode = newDoc.querySelector('.ocr_page'); if (!newPageNode) { alert("Error: The loaded file does not contain a valid page."); return; } // Replace the current node in the Iframe DOM with the new one pageNode.parentNode.replaceChild(newPageNode, pageNode); this.hocrProofreader.renderPage(newPageNode); newPageNode.setAttribute('contenteditable', 'true'); } Setup_Reset_Button() { const resetButton = document.getElementById('load-original-hocr'); resetButton.addEventListener('click', () => { const iframe = document.querySelector('.editor'); const doc = iframe.contentDocument; const activePageNode = this.hocrProofreader.currentPage; if (!activePageNode) { alert("Cannot determine current page."); return; } const allPages = Array.from(doc.querySelectorAll('.ocr_page')); const pageIndex = allPages.indexOf(activePageNode); const pageNum = pageIndex + 1; const ts = new Date().getTime(); const originalUrl = `./${buildHocrFileName(pageNum)}?t=${ts}`; Util.get(originalUrl, (err, hocr) => { if (err) { alert(`Failed to load original: ${originalUrl}`); return; } this.Hot_Swap_Page_Content(activePageNode, hocr); alert(`Reset Page ${pageNum} to Original`); }); }); } Setup_Load_Edited_Button() { const editedButton = document.getElementById('load-edited-hocr'); editedButton.addEventListener('click', () => { const iframe = document.querySelector('.editor'); const doc = iframe.contentDocument; const activePageNode = this.hocrProofreader.currentPage; if (!activePageNode) { alert("Cannot determine current page."); return; } const allPages = Array.from(doc.querySelectorAll('.ocr_page')); const pageIndex = allPages.indexOf(activePageNode); const pageNum = pageIndex + 1; const ts = new Date().getTime(); const editedUrl = `./${buildHocrFileName(pageNum, '_edited')}?t=${ts}`; Util.get(editedUrl, (err, hocr) => { if (err) { alert(`No edited file found: ${editedUrl}`); return; } this.Hot_Swap_Page_Content(activePageNode, hocr); alert(`Loaded Edited Version for Page ${pageNum}`); }); }); } }