feat(rotate,delete-pages): Add custom rotation angles and enhance page management UI

- Add TypeScript type definitions for merge and alternate-merge Web Workers
- Implement custom rotation angle input with increment/decrement controls for rotate tool
- Extend delete-pages tool to render page thumbnails for better UX
- Hide number input spin buttons across all number inputs via CSS
- Refactor rotateAll function to accept angle parameter instead of direction multiplier
- Update fileHandler to support delete-pages tool thumbnail rendering
- Improve type safety in alternate-merge logic with proper interface definitions
- Enhance rotate tool UI with custom angle input field and adjustment buttons
This commit is contained in:
abdullahalam123
2025-12-01 14:54:46 +05:30
parent c5764e4172
commit 09436a689d
10 changed files with 397 additions and 109 deletions

View File

@@ -129,7 +129,7 @@ function initializeOrganizeSortable(containerId: any) {
* @param {object} pdfDoc The loaded pdf-lib document instance.
*/
export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator';
const containerId = toolId === 'organize' ? 'page-organizer' : toolId === 'delete-pages' ? 'delete-pages-preview' : 'page-rotator';
const container = document.getElementById(containerId);
if (!container) return;
@@ -138,6 +138,9 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
// Cleanup any previous lazy loading observers
cleanupLazyRendering();
const currentRenderId = Date.now();
container.dataset.renderId = currentRenderId.toString();
showLoader('Rendering page previews...');
const pdfData = await pdfDoc.save();
@@ -189,7 +192,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
wrapper.append(pageNumSpan, deleteBtn);
} else if (toolId === 'rotate') {
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2';
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2 relative group';
// Read rotation from state (handles "Rotate All" on lazy-loaded pages)
const rotationStateArray = getRotationState();
@@ -206,35 +209,122 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
wrapper.appendChild(imgContainer);
const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
// Page Number Overlay (Top Left)
const pageNumSpan = document.createElement('span');
pageNumSpan.className = 'font-medium text-sm text-white';
pageNumSpan.className =
'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
pageNumSpan.textContent = pageNumber.toString();
wrapper.appendChild(pageNumSpan);
const rotateBtn = document.createElement('button');
rotateBtn.className =
'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
rotateBtn.title = 'Rotate 90°';
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
rotateBtn.addEventListener('click', (e) => {
e.stopPropagation();
const card = (e.currentTarget as HTMLElement).closest(
'.page-rotator-item'
) as HTMLElement;
const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1';
// Custom Stepper Component
const stepperContainer = document.createElement('div');
stepperContainer.className = 'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8';
const decrementBtn = document.createElement('button');
decrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center';
decrementBtn.innerHTML = '<i data-lucide="minus" class="w-3 h-3"></i>';
const angleInput = document.createElement('input');
angleInput.type = 'number';
angleInput.className = 'no-spinner w-full h-full bg-transparent text-white text-xs text-center focus:outline-none appearance-none m-0 p-0 border-none';
angleInput.value = initialRotation.toString();
angleInput.placeholder = "0";
const incrementBtn = document.createElement('button');
incrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center';
incrementBtn.innerHTML = '<i data-lucide="plus" class="w-3 h-3"></i>';
// Helper to update rotation
const updateRotation = (newRotation: number) => {
const card = wrapper; // Closure capture
const imgEl = card.querySelector('img');
const pageIndex = pageNumber - 1;
let currentRotation = parseInt(card.dataset.rotation);
currentRotation = (currentRotation + 90) % 360;
card.dataset.rotation = currentRotation.toString();
imgEl.style.transform = `rotate(${currentRotation}deg)`;
updateRotationState(pageIndex, currentRotation);
// Update UI
angleInput.value = newRotation.toString();
card.dataset.rotation = newRotation.toString();
imgEl.style.transform = `rotate(${newRotation}deg)`;
// Update State
updateRotationState(pageIndex, newRotation);
};
// Event Listeners
decrementBtn.addEventListener('click', (e) => {
e.stopPropagation();
let current = parseInt(angleInput.value) || 0;
updateRotation(current - 1);
});
controlsDiv.append(pageNumSpan, rotateBtn);
incrementBtn.addEventListener('click', (e) => {
e.stopPropagation();
let current = parseInt(angleInput.value) || 0;
updateRotation(current + 1);
});
angleInput.addEventListener('change', (e) => {
e.stopPropagation();
let val = parseInt((e.target as HTMLInputElement).value) || 0;
updateRotation(val);
});
angleInput.addEventListener('click', (e) => e.stopPropagation());
stepperContainer.append(decrementBtn, angleInput, incrementBtn);
const rotateBtn = document.createElement('button');
rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-1.5 rounded-md text-gray-200 transition-colors flex-shrink-0';
rotateBtn.title = 'Rotate +90°';
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
rotateBtn.addEventListener('click', (e) => {
e.stopPropagation();
let current = parseInt(angleInput.value) || 0;
updateRotation(current + 90);
});
controlsDiv.append(stepperContainer, rotateBtn);
wrapper.appendChild(controlsDiv);
} else if (toolId === 'delete-pages') {
wrapper.className = 'page-thumbnail relative group cursor-pointer transition-all duration-200';
wrapper.dataset.pageNumber = pageNumber.toString();
const innerContainer = document.createElement('div');
innerContainer.className = 'relative w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600 transition-colors duration-200';
innerContainer.appendChild(img);
wrapper.appendChild(innerContainer);
const pageNumSpan = document.createElement('span');
pageNumSpan.className =
'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
pageNumSpan.textContent = pageNumber.toString();
wrapper.appendChild(pageNumSpan);
wrapper.addEventListener('click', () => {
const input = document.getElementById('pages-to-delete') as HTMLInputElement;
if (!input) return;
const currentVal = input.value;
let pages = currentVal.split(',').map(s => s.trim()).filter(s => s);
const pageStr = pageNumber.toString();
if (pages.includes(pageStr)) {
pages = pages.filter(p => p !== pageStr);
} else {
pages.push(pageStr);
}
pages.sort((a, b) => {
const numA = parseInt(a.split('-')[0]);
const numB = parseInt(b.split('-')[0]);
return numA - numB;
});
input.value = pages.join(', ');
input.dispatchEvent(new Event('input'));
});
}
return wrapper;
@@ -255,12 +345,17 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
},
onBatchComplete: () => {
createIcons({ icons });
},
shouldCancel: () => {
return container.dataset.renderId !== currentRenderId.toString();
}
}
);
if (toolId === 'organize') {
initializeOrganizeSortable(containerId);
} else if (toolId === 'delete-pages') {
// No sortable needed for delete pages
}
// Reinitialize lucide icons for dynamically added elements
@@ -580,15 +675,49 @@ export const toolTemplates = {
<div id="rotate-all-controls" class="hidden my-6">
<div class="bg-gray-900/50 border border-gray-700 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-400 mb-3 text-center">BATCH ACTIONS</h3>
<div class="flex justify-center gap-4">
<button id="rotate-all-left-btn" class="flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95">
<i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i>
Rotate All Left
</button>
<button id="rotate-all-right-btn" class="flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95">
<i data-lucide="rotate-cw" class="mr-2 h-4 w-4"></i>
Rotate All Right
</button>
<div class="flex flex-col md:flex-row justify-center gap-6 items-center">
<!-- 90 Degree Rotation Group -->
<div class="flex flex-col gap-2 items-center">
<span class="text-xs text-gray-400 font-medium uppercase tracking-wider">Rotate By 90 degrees</span>
<div class="flex gap-2">
<button id="rotate-all-left-btn" class="flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95" title="Rotate Left 90°">
<i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i>
Left
</button>
<button id="rotate-all-right-btn" class="flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95" title="Rotate Right 90°">
<i data-lucide="rotate-cw" class="mr-2 h-4 w-4"></i>
Right
</button>
</div>
</div>
<div class="hidden md:block w-px h-12 bg-gray-700"></div>
<!-- Custom Rotation Group -->
<div class="flex flex-col gap-2 items-center">
<span class="text-xs text-gray-400 font-medium uppercase tracking-wider">Rotate By Custom Degrees</span>
<div class="flex gap-2 items-center">
<!-- Custom Stepper -->
<div class="flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden h-10">
<button id="rotate-all-decrement-btn" class="px-3 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center">
<i data-lucide="minus" class="w-4 h-4"></i>
</button>
<div class="relative w-20 h-full">
<input type="number" id="custom-rotate-all-input" placeholder="0" class="no-spinner w-full h-full bg-transparent text-white text-sm text-center focus:outline-none appearance-none m-0 p-0 border-none" min="-360" max="360">
</div>
<button id="rotate-all-increment-btn" class="px-3 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center">
<i data-lucide="plus" class="w-4 h-4"></i>
</button>
</div>
<button id="rotate-all-custom-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-medium px-4 py-2 rounded-lg text-sm transition-colors shadow-sm h-10">
Apply
</button>
</div>
</div>
</div>
</div>
</div>
@@ -901,6 +1030,7 @@ export const toolTemplates = {
<p class="mb-2 font-medium text-white">Total Pages: <span id="total-pages"></span></p>
<label for="pages-to-delete" class="block mb-2 text-sm font-medium text-gray-300">Enter pages to delete (e.g., 2, 4-6, 9):</label>
<input type="text" id="pages-to-delete" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-6" placeholder="e.g., 2, 4-6, 9">
<div id="delete-pages-preview" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4 my-6"></div>
<button id="process-btn" class="btn-gradient w-full">Delete Pages & Download</button>
</div>
`,