window.envelopeEditor = { _moveThrottle: { rafId: 0, latest: null, waiters: [] }, /** * Returns the click position normalised to [0,1] relative to the rendered PDF page * element inside DxPdfViewer (or DxReportViewer as fallback). * * Normalising means the result is independent of zoom level: no matter how much the * user has zoomed in/out, the same physical spot on the PDF will always yield the same * normalised value. C# multiplies by the page's point dimensions to get PDF points. * * @param {string} viewerCssClass - CssClass set on DxPdfViewer (e.g. "sender-editor-pdf-viewer") * @param {number} clientX - MouseEvent.clientX from Blazor * @param {number} clientY - MouseEvent.clientY from Blazor * @returns {{ normX, normY, pageIndex } | null} * normX / normY : 0..1 fraction within the page element * pageIndex : 0-based index of the page the click landed on (-1 if not found) */ getClickCoordsOnPdfPage: function (viewerCssClass, clientX, clientY) { // Find the viewer root element const viewer = document.querySelector('.' + viewerCssClass); if (!viewer) { console.warn('[envelopeEditor] viewer not found for class:', viewerCssClass); return null; } // --- Candidate page elements (ordered by preference) --- // Sender editor custom renderer uses data-editor-page blocks. // Legacy Dx viewers remain as fallback selectors. const pageSelectors = [ '[data-editor-page]', '.dxbl-pdfv-page', '.dxbrv-report-preview-page', '.dxbrv-report-preview-content-img', ]; let allPages = []; for (const sel of pageSelectors) { const found = Array.from(viewer.querySelectorAll(sel)); if (found.length > 0) { allPages = found; break; } } if (allPages.length === 0) { console.warn('[envelopeEditor] no page elements found inside viewer'); return null; } // --- Find which page the click landed on --- // Walk through all pages; pick the one whose bounding rect contains the click. // If none contains it exactly, fall back to the page closest vertically. let targetPage = null; let targetIndex = -1; let minDist = Infinity; for (let i = 0; i < allPages.length; i++) { const rect = allPages[i].getBoundingClientRect(); // Exact hit if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) { targetPage = allPages[i]; targetIndex = i; break; } // Track closest page (vertical centre distance) as fallback const cy = rect.top + rect.height / 2; const dist = Math.abs(clientY - cy); if (dist < minDist) { minDist = dist; targetPage = allPages[i]; targetIndex = i; } } if (!targetPage) return null; const pageRect = targetPage.getBoundingClientRect(); // Clamp click inside page boundaries before normalising const clampedX = Math.max(pageRect.left, Math.min(clientX, pageRect.right)); const clampedY = Math.max(pageRect.top, Math.min(clientY, pageRect.bottom)); const normX = (clampedX - pageRect.left) / pageRect.width; const normY = (clampedY - pageRect.top) / pageRect.height; return { normX: normX, normY: normY, pageIndex: targetIndex, pageNumber: parseInt(targetPage.getAttribute('data-editor-page') || String(targetIndex + 1), 10) }; }, getClickCoordsOnPdfPageThrottled: function (viewerCssClass, clientX, clientY) { return new Promise((resolve) => { const state = window.envelopeEditor._moveThrottle; state.latest = { viewerCssClass, clientX, clientY }; state.waiters.push(resolve); if (state.rafId !== 0) { return; } state.rafId = window.requestAnimationFrame(() => { state.rafId = 0; const args = state.latest; state.latest = null; const result = args ? window.envelopeEditor.getClickCoordsOnPdfPage(args.viewerCssClass, args.clientX, args.clientY) : null; const waiters = state.waiters.splice(0, state.waiters.length); for (const waiter of waiters) { waiter(result); } }); }); }, scrollToPdfPage: function (viewerCssClass, pageNumber) { const viewer = document.querySelector('.' + viewerCssClass); if (!viewer) { return; } const targetPageNumber = Math.max(1, Number(pageNumber) || 1); const page = viewer.querySelector('[data-editor-page="' + targetPageNumber + '"]'); if (!page) { return; } const viewerRect = viewer.getBoundingClientRect(); const pageRect = page.getBoundingClientRect(); const targetScrollTop = viewer.scrollTop + (pageRect.top - viewerRect.top) - 8; viewer.scrollTo({ top: Math.max(0, targetScrollTop), behavior: 'smooth' }); }, getVisiblePdfPageNumber: function (viewerCssClass) { const viewer = document.querySelector('.' + viewerCssClass); if (!viewer) { return null; } const pages = Array.from(viewer.querySelectorAll('[data-editor-page]')); if (pages.length === 0) { return null; } const viewerRect = viewer.getBoundingClientRect(); let bestPage = null; let bestVisibleHeight = -1; for (const page of pages) { const rect = page.getBoundingClientRect(); const visibleTop = Math.max(rect.top, viewerRect.top); const visibleBottom = Math.min(rect.bottom, viewerRect.bottom); const visibleHeight = Math.max(0, visibleBottom - visibleTop); if (visibleHeight > bestVisibleHeight) { bestVisibleHeight = visibleHeight; bestPage = page; } } if (!bestPage) { return null; } const raw = bestPage.getAttribute('data-editor-page'); const parsed = Number.parseInt(raw || '0', 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : null; }, scrollPdfViewerByDelta: function (viewerCssClass, deltaY) { const viewer = document.querySelector('.' + viewerCssClass); if (!viewer) { return; } viewer.scrollTop += Number(deltaY) || 0; }, ensureZoomOverride: function (viewerCssClass) { const viewer = document.querySelector('.' + viewerCssClass); if (!viewer) { return; } if (viewer.dataset.zoomOverrideAttached === '1') { return; } viewer.dataset.zoomOverrideAttached = '1'; viewer.addEventListener('wheel', function (event) { event.preventDefault(); }, { passive: false }); viewer.addEventListener('keydown', function (event) { if (!event.ctrlKey) { return; } const key = event.key; if (key === '+' || key === '=' || key === '-' || key === '0') { event.preventDefault(); } }); viewer.addEventListener('touchmove', function () { }, { passive: true }); }, downloadDataUrlAsFile: function (dataUrl, fileName, mimeType) { if (!dataUrl || typeof dataUrl !== 'string') { return; } const downloadName = fileName || 'document.pdf'; const link = document.createElement('a'); link.href = dataUrl; link.download = downloadName; link.rel = 'noopener'; const host = document.body || document.documentElement; host.appendChild(link); link.click(); host.removeChild(link); } };