Improve signature navigation and rendering stability
Enhanced signature navigation and rendering logic in `pdf-viewer.js`: - Added `_renderLock` to prevent concurrent page renders. - Refactored `renderPage` and `queueRenderPage` for stability. - Updated `goToNextSignature` to support cross-page navigation. - Filtered out applied signatures during rendering and navigation. - Improved handling of applied signatures visibility per page. Updated `EnvelopeViewer.razor`: - Added `OnPageChangedBySignatureNav` to handle page changes triggered by signature navigation. Improved code readability, added comments, and removed outdated logic to ensure smooth transitions and better user experience.
This commit is contained in:
@@ -616,6 +616,12 @@ const int MaxThumbnailWidth = 400;
|
|||||||
await UpdateSignatureCounterAsync();
|
await UpdateSignatureCounterAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnPageChangedBySignatureNav(int newPage) {
|
||||||
|
_currentPage = newPage;
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
async Task UpdateSignatureCounterAsync() {
|
async Task UpdateSignatureCounterAsync() {
|
||||||
try {
|
try {
|
||||||
var state = await JSRuntime.InvokeAsync<SignatureNavState>("pdfViewer.getSignatureNavState");
|
var state = await JSRuntime.InvokeAsync<SignatureNavState>("pdfViewer.getSignatureNavState");
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ window.pdfViewer = {
|
|||||||
currentRenderTask: null,
|
currentRenderTask: null,
|
||||||
dotNetReference: null,
|
dotNetReference: null,
|
||||||
wheelEventAttached: false,
|
wheelEventAttached: false,
|
||||||
|
_renderLock: false, // Lock to prevent concurrent renders
|
||||||
|
|
||||||
// Quality options (configurable from appsettings.json)
|
// Quality options (configurable from appsettings.json)
|
||||||
qualityOptions: {
|
qualityOptions: {
|
||||||
@@ -119,6 +120,26 @@ window.pdfViewer = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async renderPage(num) {
|
async renderPage(num) {
|
||||||
|
// CRITICAL: Single render at a time - use a lock
|
||||||
|
if (this._renderLock) {
|
||||||
|
// Another render is in progress, queue it
|
||||||
|
this.pageNumPending = num;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._renderLock = true;
|
||||||
|
|
||||||
|
// Cancel any existing render task
|
||||||
|
if (this.currentRenderTask) {
|
||||||
|
try {
|
||||||
|
this.currentRenderTask.cancel();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore cancellation errors
|
||||||
|
}
|
||||||
|
this.currentRenderTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.pageRendering = true;
|
this.pageRendering = true;
|
||||||
|
|
||||||
// Add rendering class for smooth transition (if enabled)
|
// Add rendering class for smooth transition (if enabled)
|
||||||
@@ -164,14 +185,11 @@ window.pdfViewer = {
|
|||||||
viewport: viewport
|
viewport: viewport
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.currentRenderTask) {
|
|
||||||
this.currentRenderTask.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable high-quality rendering
|
// Enable high-quality rendering
|
||||||
this.ctx.imageSmoothingEnabled = true;
|
this.ctx.imageSmoothingEnabled = true;
|
||||||
this.ctx.imageSmoothingQuality = 'high';
|
this.ctx.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
|
// Start new render task
|
||||||
this.currentRenderTask = page.render(renderContext);
|
this.currentRenderTask = page.render(renderContext);
|
||||||
await this.currentRenderTask.promise;
|
await this.currentRenderTask.promise;
|
||||||
|
|
||||||
@@ -198,10 +216,6 @@ window.pdfViewer = {
|
|||||||
this.currentRenderTask = null;
|
this.currentRenderTask = null;
|
||||||
this.pageRendering = false;
|
this.pageRendering = false;
|
||||||
|
|
||||||
if (this.pageNumPending !== null) {
|
|
||||||
this.renderPage(this.pageNumPending);
|
|
||||||
this.pageNumPending = null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name !== 'RenderingCancelledException') {
|
if (error.name !== 'RenderingCancelledException') {
|
||||||
console.error('Render error:', error);
|
console.error('Render error:', error);
|
||||||
@@ -209,6 +223,16 @@ window.pdfViewer = {
|
|||||||
this.canvas.classList.remove('rendering');
|
this.canvas.classList.remove('rendering');
|
||||||
this.currentRenderTask = null;
|
this.currentRenderTask = null;
|
||||||
this.pageRendering = false;
|
this.pageRendering = false;
|
||||||
|
} finally {
|
||||||
|
// Always release lock
|
||||||
|
this._renderLock = false;
|
||||||
|
|
||||||
|
// Process pending render
|
||||||
|
if (this.pageNumPending !== null) {
|
||||||
|
const pendingPage = this.pageNumPending;
|
||||||
|
this.pageNumPending = null;
|
||||||
|
this.renderPage(pendingPage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -250,7 +274,8 @@ window.pdfViewer = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
queueRenderPage(num) {
|
queueRenderPage(num) {
|
||||||
if (this.pageRendering) {
|
// Always use pending mechanism to avoid race conditions
|
||||||
|
if (this.pageRendering || this.currentRenderTask) {
|
||||||
this.pageNumPending = num;
|
this.pageNumPending = num;
|
||||||
} else {
|
} else {
|
||||||
this.renderPage(num);
|
this.renderPage(num);
|
||||||
@@ -492,41 +517,55 @@ window.pdfViewer = {
|
|||||||
/**
|
/**
|
||||||
* Navigates to the next unsigned signature button.
|
* Navigates to the next unsigned signature button.
|
||||||
* Scrolls to button position and changes page if necessary.
|
* Scrolls to button position and changes page if necessary.
|
||||||
|
* Cross-page navigation: searches ALL pages for next unsigned signature.
|
||||||
*/
|
*/
|
||||||
async goToNextSignature(dotNetRef) {
|
async goToNextSignature(dotNetRef) {
|
||||||
if (this.signatureButtons.length === 0) {
|
// Global imza listesi yoksa ç?k
|
||||||
|
if (!this._allSignatures || this._allSignatures.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get first unsigned signature button
|
// TÜM sayfalar aras?nda ilk imzalanmam?? imzay? bul
|
||||||
const button = this.signatureButtons[0];
|
const appliedIds = new Set(this.appliedSignatures.map(s => s.id));
|
||||||
const signatureId = parseInt(button.getAttribute('data-signature-id'));
|
const nextSignature = this._allSignatures.find(sig => !appliedIds.has(sig.id));
|
||||||
|
|
||||||
// Find signature in original list to get page number
|
if (!nextSignature) {
|
||||||
const signature = this._allSignatures?.find(s => s.id === signatureId);
|
return false; // Hepsi imzalanm??
|
||||||
if (!signature) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change page if needed
|
// Farkl? sayfadaysa sayfa de?i?tir
|
||||||
if (signature.page !== this.pageNum) {
|
if (nextSignature.page !== this.pageNum) {
|
||||||
await this.goToPage(signature.page);
|
// Sayfa de?i?tir
|
||||||
|
this.pageNum = nextSignature.page;
|
||||||
|
this.queueRenderPage(this.pageNum);
|
||||||
|
|
||||||
// Wait for page render and button re-creation
|
// Render tamamlanana kadar bekle
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
let waitCount = 0;
|
||||||
|
while (this.pageRendering && waitCount < 20) {
|
||||||
// Re-find button after page change
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
const newButton = this.signatureButtons.find(btn =>
|
waitCount++;
|
||||||
parseInt(btn.getAttribute('data-signature-id')) === signatureId);
|
|
||||||
|
|
||||||
if (newButton) {
|
|
||||||
this.scrollToButton(newButton);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
// Blazor'a haber ver - signature butonlar?n? yeniden ?iz
|
||||||
|
if (dotNetRef) {
|
||||||
|
await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Butonlar?n DOM'a eklenmesini bekle
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 150));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mevcut sayfadaki (art?k yeni sayfa olabilir) butonu bul
|
||||||
|
const button = this.signatureButtons.find(btn =>
|
||||||
|
parseInt(btn.getAttribute('data-signature-id')) === nextSignature.id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Butonu görünür hale getir (scroll ile)
|
||||||
|
if (button) {
|
||||||
this.scrollToButton(button);
|
this.scrollToButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify Blazor to update counter
|
// Counter'? güncelle (Blazor'a bildir)
|
||||||
if (dotNetRef) {
|
if (dotNetRef) {
|
||||||
dotNetRef.invokeMethodAsync('OnSignatureNavChanged');
|
dotNetRef.invokeMethodAsync('OnSignatureNavChanged');
|
||||||
}
|
}
|
||||||
@@ -548,8 +587,24 @@ window.pdfViewer = {
|
|||||||
|
|
||||||
// Change page if needed
|
// Change page if needed
|
||||||
if (lastSig.page !== this.pageNum) {
|
if (lastSig.page !== this.pageNum) {
|
||||||
await this.goToPage(lastSig.page);
|
// Sayfa de?i?tir
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
this.pageNum = lastSig.page;
|
||||||
|
this.queueRenderPage(this.pageNum);
|
||||||
|
|
||||||
|
// Render tamamlanana kadar bekle
|
||||||
|
let waitCount = 0;
|
||||||
|
while (this.pageRendering && waitCount < 20) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
waitCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blazor'a haber ver - signature butonlar?n? yeniden çiz
|
||||||
|
if (dotNetRef) {
|
||||||
|
await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM güncellenmesini bekle
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 150));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find applied signature container
|
// Find applied signature container
|
||||||
@@ -614,7 +669,7 @@ window.pdfViewer = {
|
|||||||
* @param {object} dotNetRef - .NET reference for callbacks
|
* @param {object} dotNetRef - .NET reference for callbacks
|
||||||
*/
|
*/
|
||||||
async renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
|
async renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
|
||||||
// Clear existing buttons
|
// Clear existing buttons (NOT applied signatures!)
|
||||||
this.clearSignatureButtons();
|
this.clearSignatureButtons();
|
||||||
|
|
||||||
if (!this.pdfDoc || !signatures || signatures.length === 0) {
|
if (!this.pdfDoc || !signatures || signatures.length === 0) {
|
||||||
@@ -625,8 +680,11 @@ window.pdfViewer = {
|
|||||||
this._allSignatures = signatures; // Store for navigation
|
this._allSignatures = signatures; // Store for navigation
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Filter signatures for current page
|
// CRITICAL: Filter OUT already applied signatures!
|
||||||
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
|
const appliedIds = new Set(this.appliedSignatures.map(s => s.id));
|
||||||
|
const pageSignatures = signatures.filter(sig =>
|
||||||
|
sig.page === currentPageNum && !appliedIds.has(sig.id) // ? Skip applied ones!
|
||||||
|
);
|
||||||
|
|
||||||
if (pageSignatures.length === 0) {
|
if (pageSignatures.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -650,7 +708,7 @@ window.pdfViewer = {
|
|||||||
signatureLayer.style.width = `${viewport.width / dpr}px`;
|
signatureLayer.style.width = `${viewport.width / dpr}px`;
|
||||||
signatureLayer.style.height = `${viewport.height / dpr}px`;
|
signatureLayer.style.height = `${viewport.height / dpr}px`;
|
||||||
|
|
||||||
// Create button for each signature
|
// Create button for each UNSIGNED signature
|
||||||
pageSignatures.forEach(sig => {
|
pageSignatures.forEach(sig => {
|
||||||
// Coordinates are in PDF POINTS - convert to display pixels
|
// Coordinates are in PDF POINTS - convert to display pixels
|
||||||
const xPx = (sig.x * this.scale);
|
const xPx = (sig.x * this.scale);
|
||||||
@@ -738,14 +796,35 @@ window.pdfViewer = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all signature buttons from the canvas.
|
* Clears all signature buttons from the canvas.
|
||||||
|
* Also hides applied signatures that don't belong to current page.
|
||||||
*/
|
*/
|
||||||
clearSignatureButtons() {
|
clearSignatureButtons() {
|
||||||
|
// Remove unsigned signature buttons
|
||||||
this.signatureButtons.forEach(button => {
|
this.signatureButtons.forEach(button => {
|
||||||
if (button.parentNode) {
|
if (button.parentNode) {
|
||||||
button.parentNode.removeChild(button);
|
button.parentNode.removeChild(button);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.signatureButtons = [];
|
this.signatureButtons = [];
|
||||||
|
|
||||||
|
// Hide/show applied signatures based on current page
|
||||||
|
const signatureLayer = document.getElementById('pdf-signature-layer');
|
||||||
|
if (signatureLayer) {
|
||||||
|
const appliedContainers = signatureLayer.querySelectorAll('.applied-signature');
|
||||||
|
appliedContainers.forEach(container => {
|
||||||
|
const signatureId = parseInt(container.getAttribute('data-signature-id'));
|
||||||
|
const signature = this._allSignatures?.find(s => s.id === signatureId);
|
||||||
|
|
||||||
|
if (signature) {
|
||||||
|
// Show only if on current page, hide otherwise
|
||||||
|
if (signature.page === this.pageNum) {
|
||||||
|
container.style.display = ''; // Show
|
||||||
|
} else {
|
||||||
|
container.style.display = 'none'; // Hide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user