Files
EnvelopeGenerator/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js
TekH 021d7cfe29 Add PDF download functionality to editor components
Introduced the ability to download PDFs in the `SenderPdfImageEditor` component:
- Added `PdfFileName` parameter to specify the file name.
- Implemented `CanDownloadPdf` property to enable/disable downloads.
- Created `OnDownloadPdfAsync` method to handle download logic via JS interop.

Updated `SenderPdfImageEditorToolbar` to support downloads:
- Added `CanDownloadPdf` and `DownloadPdf` parameters.
- Enabled "Download PDF" button based on `CanDownloadPdf`.
- Added `DownloadPdfAsync` method to trigger the download callback.

Enhanced `SenderEditorPage` to pass `PdfFileName` to `SenderPdfImageEditor`.

Added `downloadDataUrlAsFile` function in `envelope-editor.js` to facilitate file downloads using a temporary `<a>` element.

Made minor adjustments for consistency, including adding `ZoomStepPercent` to the toolbar and refining `SetZoomPercent` logic.
2026-09-30 16:48:51 +02:00

248 lines
8.2 KiB
JavaScript

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);
}
};