From d888ed1ae0f37acabcbf19eb3aee13a341807e63 Mon Sep 17 00:00:00 2001 From: reecebrowne <74901996+reecebrowne@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:43:31 +0000 Subject: [PATCH] Feature/undo page break (#2389) * Fix delete selected Fix add page break where selected Added undo logic for page breaks * Add pages undo capability * Fix page break when selected logic --- .../static/js/multitool/PdfContainer.js | 445 ++++++++++-------- .../static/js/multitool/commands/add-page.js | 53 +++ .../js/multitool/commands/delete-page.js | 4 +- .../js/multitool/commands/page-break.js | 59 +++ 4 files changed, 351 insertions(+), 210 deletions(-) create mode 100644 src/main/resources/static/js/multitool/commands/add-page.js create mode 100644 src/main/resources/static/js/multitool/commands/page-break.js diff --git a/src/main/resources/static/js/multitool/PdfContainer.js b/src/main/resources/static/js/multitool/PdfContainer.js index b5721dfac..4eaf43f14 100644 --- a/src/main/resources/static/js/multitool/PdfContainer.js +++ b/src/main/resources/static/js/multitool/PdfContainer.js @@ -1,8 +1,10 @@ -import { MovePageUpCommand, MovePageDownCommand } from "./commands/move-page.js"; -import { RemoveSelectedCommand } from "./commands/remove.js"; -import { RotateAllCommand, RotateElementCommand } from "./commands/rotate.js"; -import { SplitAllCommand } from "./commands/split.js"; -import { UndoManager } from "./UndoManager.js"; +import {MovePageUpCommand, MovePageDownCommand} from './commands/move-page.js'; +import {RemoveSelectedCommand} from './commands/remove.js'; +import {RotateAllCommand, RotateElementCommand} from './commands/rotate.js'; +import {SplitAllCommand} from './commands/split.js'; +import {UndoManager} from './UndoManager.js'; +import {PageBreakCommand} from './commands/page-break.js'; +import {AddFilesCommand} from './commands/add-page.js'; class PdfContainer { fileName; @@ -34,7 +36,7 @@ class PdfContainer { this.updateSelectedPagesDisplay = this.updateSelectedPagesDisplay.bind(this); this.toggleSelectPageVisibility = this.toggleSelectPageVisibility.bind(this); this.updatePagesFromCSV = this.updatePagesFromCSV.bind(this); - this.addFilesBlankAll = this.addFilesBlankAll.bind(this) + this.addFilesBlankAll = this.addFilesBlankAll.bind(this); this.removeAllElements = this.removeAllElements.bind(this); this.resetPages = this.resetPages.bind(this); @@ -63,7 +65,7 @@ class PdfContainer { window.updatePagesFromCSV = this.updatePagesFromCSV; window.updateSelectedPagesDisplay = this.updateSelectedPagesDisplay; window.updatePageNumbersAndCheckboxes = this.updatePageNumbersAndCheckboxes; - window.addFilesBlankAll = this.addFilesBlankAll + window.addFilesBlankAll = this.addFilesBlankAll; window.removeAllElements = this.removeAllElements; window.resetPages = this.resetPages; @@ -76,7 +78,7 @@ class PdfContainer { undoBtn.disabled = !canUndo; redoBtn.disabled = !canRedo; - }) + }); window.undo = () => { if (undoManager.canUndo()) undoManager.undo(); @@ -84,7 +86,7 @@ class PdfContainer { undoBtn.disabled = !undoManager.canUndo(); redoBtn.disabled = !undoManager.canRedo(); } - } + }; window.redo = () => { if (undoManager.canRedo()) undoManager.redo(); @@ -92,15 +94,15 @@ class PdfContainer { undoBtn.disabled = !undoManager.canUndo(); redoBtn.disabled = !undoManager.canRedo(); } - } + }; - const filenameInput = document.getElementById("filename-input"); - const downloadBtn = document.getElementById("export-button"); + const filenameInput = document.getElementById('filename-input'); + const downloadBtn = document.getElementById('export-button'); filenameInput.onkeyup = this.updateFilename; filenameInput.onkeydown = this.preventIllegalChars; filenameInput.disabled = false; - filenameInput.innerText = ""; + filenameInput.innerText = ''; downloadBtn.disabled = true; } @@ -128,86 +130,99 @@ class PdfContainer { return movePageCommand; } - addFiles(nextSiblingElement, blank = false) { - if (blank) { + async addFiles(element) { + let addFilesCommand = new AddFilesCommand( + element, + window.selectedPages, + this.addFilesAction.bind(this), + this.pagesContainer + ); - this.addFilesBlank(nextSiblingElement); + await addFilesCommand.execute(); - } else { - var input = document.createElement("input"); - input.type = "file"; + this.undoManager.pushUndoClearRedo(addFilesCommand); + } + + async addFilesAction(nextSiblingElement) { + let pages = []; + return new Promise((resolve) => { + var input = document.createElement('input'); + input.type = 'file'; input.multiple = true; - input.setAttribute("accept", "application/pdf,image/*"); + input.setAttribute('accept', 'application/pdf,image/*'); + input.onchange = async (e) => { const files = e.target.files; - - this.addFilesFromFiles(files, nextSiblingElement); - this.updateFilename(files ? files[0].name : ""); - const selectAll = document.getElementById("select-pages-container"); - selectAll.classList.toggle("hidden", false); + if (files.length > 0) { + pages = await this.addFilesFromFiles(files, nextSiblingElement, pages); + this.updateFilename(files[0].name); + const selectAll = document.getElementById('select-pages-container'); + selectAll.classList.toggle('hidden', false); + } + resolve(pages); }; input.click(); - } - } - - async addFilesFromFiles(files, nextSiblingElement) { - this.fileName = files[0].name; - for (var i = 0; i < files.length; i++) { - const startTime = Date.now(); - let processingTime, errorMessage = null, pageCount = 0; - try { - const file = files[i]; - if (file.type === "application/pdf") { - const { renderer, pdfDocument } = await this.loadFile(file); - pageCount = renderer.pageCount || 0; - await this.addPdfFile(renderer, pdfDocument, nextSiblingElement); - } else if (file.type.startsWith("image/")) { - await this.addImageFile(file, nextSiblingElement); - } - processingTime = Date.now() - startTime; - this.captureFileProcessingEvent(true, file, processingTime, null, pageCount); - } catch (error) { - processingTime = Date.now() - startTime; - errorMessage = error.message || "Unknown error"; - this.captureFileProcessingEvent(false, files[i], processingTime, errorMessage, pageCount); - } - } - - document.querySelectorAll(".enable-on-file").forEach((element) => { - element.disabled = false; }); } - captureFileProcessingEvent(success, file, processingTime, errorMessage, pageCount) { - try{ - if(analyticsEnabled){ - posthog.capture('file_processing', { - success, - file_type: file?.type || 'unknown', - file_size: file?.size || 0, - processing_time: processingTime, - error_message: errorMessage, - pdf_pages: pageCount, - }); -} -}catch{ -} -} + async addFilesFromFiles(files, nextSiblingElement, pages) { + this.fileName = files[0].name; + for (var i = 0; i < files.length; i++) { + const startTime = Date.now(); + let processingTime, + errorMessage = null, + pageCount = 0; + try { + const file = files[i]; + if (file.type === 'application/pdf') { + const {renderer, pdfDocument} = await this.loadFile(file); + pageCount = renderer.pageCount || 0; + pages = await this.addPdfFile(renderer, pdfDocument, nextSiblingElement, pages); + } else if (file.type.startsWith('image/')) { + pages = await this.addImageFile(file, nextSiblingElement, pages); + } + processingTime = Date.now() - startTime; + this.captureFileProcessingEvent(true, file, processingTime, null, pageCount); + } catch (error) { + processingTime = Date.now() - startTime; + errorMessage = error.message || 'Unknown error'; + this.captureFileProcessingEvent(false, files[i], processingTime, errorMessage, pageCount); + } + } + document.querySelectorAll('.enable-on-file').forEach((element) => { + element.disabled = false; + }); + return pages; + } - async addFilesBlank(nextSiblingElement) { + captureFileProcessingEvent(success, file, processingTime, errorMessage, pageCount) { + try { + if (analyticsEnabled) { + posthog.capture('file_processing', { + success, + file_type: file?.type || 'unknown', + file_size: file?.size || 0, + processing_time: processingTime, + error_message: errorMessage, + pdf_pages: pageCount, + }); + } + } catch {} + } + + async addFilesBlank(nextSiblingElement, pages) { let doc = await PDFLib.PDFDocument.create(); let docBytes = await doc.save(); - const url = URL.createObjectURL(new Blob([docBytes], { type: 'application/pdf' })); + const url = URL.createObjectURL(new Blob([docBytes], {type: 'application/pdf'})); const renderer = await this.toRenderer(url); - - await this.addPdfFile(renderer, doc, nextSiblingElement); + pages = await this.addPdfFile(renderer, doc, nextSiblingElement, pages); + return pages; } - rotateElement(element, deg) { let rotateCommand = new RotateElementCommand(element, deg); rotateCommand.execute(); @@ -215,37 +230,43 @@ class PdfContainer { return rotateCommand; } - async addPdfFile(renderer, pdfDocument, nextSiblingElement) { + async addPdfFile(renderer, pdfDocument, nextSiblingElement, pages) { for (var i = 0; i < renderer.pageCount; i++) { - const div = document.createElement("div"); + const div = document.createElement('div'); - div.classList.add("page-container"); - div.id = "page-container-" + (i + 1); - var img = document.createElement("img"); - img.classList.add("page-image"); + div.classList.add('page-container'); + div.id = 'page-container-' + (i + 1); + var img = document.createElement('img'); + img.classList.add('page-image'); const imageSrc = await renderer.renderPage(i); img.src = imageSrc; img.pageIdx = i; img.rend = renderer; img.doc = pdfDocument; div.appendChild(img); + this.pdfAdapters.forEach((adapter) => { adapter.adapt?.(div); }); + if (nextSiblingElement) { this.pagesContainer.insertBefore(div, nextSiblingElement); } else { this.pagesContainer.appendChild(div); } + + pages.push(div); } + + return pages; } - async addImageFile(file, nextSiblingElement) { - const div = document.createElement("div"); - div.classList.add("page-container"); + async addImageFile(file, nextSiblingElement, pages) { + const div = document.createElement('div'); + div.classList.add('page-container'); - var img = document.createElement("img"); - img.classList.add("page-image"); + var img = document.createElement('img'); + img.classList.add('page-image'); img.src = URL.createObjectURL(file); div.appendChild(img); @@ -257,17 +278,19 @@ class PdfContainer { } else { this.pagesContainer.appendChild(div); } + pages.push(div); + return pages; } async loadFile(file) { var objectUrl = URL.createObjectURL(file); var pdfDocument = await this.toPdfLib(objectUrl); var renderer = await this.toRenderer(objectUrl); - return { renderer, pdfDocument }; + return {renderer, pdfDocument}; } async toRenderer(objectUrl) { - pdfjsLib.GlobalWorkerOptions.workerSrc = "./pdfjs-legacy/pdf.worker.mjs"; + pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; const pdf = await pdfjsLib.getDocument(objectUrl).promise; return { document: pdf, @@ -275,7 +298,7 @@ class PdfContainer { renderPage: async function (pageIdx) { const page = await this.document.getPage(pageIdx + 1); - const canvas = document.createElement("canvas"); + const canvas = document.createElement('canvas'); // set the canvas size to the size of the page if (page.rotate == 90 || page.rotate == 270) { @@ -288,8 +311,8 @@ class PdfContainer { // render the page onto the canvas var renderContext = { - canvasContext: canvas.getContext("2d"), - viewport: page.getViewport({ scale: 1 }), + canvasContext: canvas.getContext('2d'), + viewport: page.getViewport({scale: 1}), }; await page.render(renderContext).promise; @@ -316,7 +339,7 @@ class PdfContainer { //if in page select mode is active rotate only selected pages if (window.selectPage && !window.selectedPages.includes(pageIndex)) continue; - const img = child.querySelector("img"); + const img = child.querySelector('img'); if (!img) continue; elementsToRotate.push(img); @@ -328,12 +351,12 @@ class PdfContainer { this.undoManager.pushUndoClearRedo(rotateAllCommand); } - removeAllElements(){ - let pageContainerNodeList = document.querySelectorAll(".page-container"); + removeAllElements() { + let pageContainerNodeList = document.querySelectorAll('.page-container'); for (var i = 0; i < pageContainerNodeList.length; i++) { pageContainerNodeList[i].remove(); } - document.querySelectorAll(".enable-on-file").forEach((element) => { + document.querySelectorAll('.enable-on-file').forEach((element) => { element.disabled = true; }); } @@ -345,25 +368,24 @@ class PdfContainer { window.selectedPages, this.updatePageNumbersAndCheckboxes ); - + removeSelectedCommand.execute(); this.undoManager.pushUndoClearRedo(removeSelectedCommand); } toggleSelectAll() { - const checkboxes = document.querySelectorAll(".pdf-actions_checkbox"); + const checkboxes = document.querySelectorAll('.pdf-actions_checkbox'); window.selectAll = !window.selectAll; - const selectIcon = document.getElementById("select-All-Container"); - const deselectIcon = document.getElementById("deselect-All-Container"); + const selectIcon = document.getElementById('select-All-Container'); + const deselectIcon = document.getElementById('deselect-All-Container'); - if (selectIcon.style.display === "none") { - selectIcon.style.display = "inline"; - deselectIcon.style.display = "none"; + if (selectIcon.style.display === 'none') { + selectIcon.style.display = 'inline'; + deselectIcon.style.display = 'none'; } else { - selectIcon.style.display = "none"; - deselectIcon.style.display = "inline"; + selectIcon.style.display = 'none'; + deselectIcon.style.display = 'inline'; } checkboxes.forEach((checkbox) => { - checkbox.checked = window.selectAll; const pageNumber = Array.from(checkbox.parentNode.parentNode.children).indexOf(checkbox.parentNode) + 1; @@ -386,18 +408,20 @@ class PdfContainer { parseCSVInput(csvInput, maxPageIndex) { const pages = new Set(); - csvInput.split(",").forEach((item) => { - const range = item.split("-").map((p) => parseInt(p.trim())); + csvInput.split(',').forEach((item) => { + const range = item.split('-').map((p) => parseInt(p.trim())); if (range.length === 2) { const [start, end] = range; for (let i = start; i <= end && i <= maxPageIndex; i++) { - if (i > 0) { // Ensure the page number is greater than 0 + if (i > 0) { + // Ensure the page number is greater than 0 pages.add(i); } } } else if (range.length === 1 && Number.isInteger(range[0])) { const page = range[0]; - if (page > 0 && page <= maxPageIndex) { // Ensure page is within valid range + if (page > 0 && page <= maxPageIndex) { + // Ensure page is within valid range pages.add(page); } } @@ -407,24 +431,24 @@ class PdfContainer { } updatePagesFromCSV() { - const csvInput = document.getElementById("csv-input").value; + const csvInput = document.getElementById('csv-input').value; - const allPages = this.pagesContainer.querySelectorAll(".page-container"); + const allPages = this.pagesContainer.querySelectorAll('.page-container'); const maxPageIndex = allPages.length; window.selectedPages = this.parseCSVInput(csvInput, maxPageIndex); this.updateSelectedPagesDisplay(); - const allCheckboxes = document.querySelectorAll(".pdf-actions_checkbox"); + const allCheckboxes = document.querySelectorAll('.pdf-actions_checkbox'); allCheckboxes.forEach((checkbox) => { - const page = parseInt(checkbox.getAttribute("data-page-number")); + const page = parseInt(checkbox.getAttribute('data-page-number')); checkbox.checked = window.selectedPages.includes(page); }); } formatSelectedPages(pages) { - if (pages.length === 0) return ""; + if (pages.length === 0) return ''; pages.sort((a, b) => a - b); // Sort the page numbers in ascending order const ranges = []; @@ -445,27 +469,27 @@ class PdfContainer { // Add the last range ranges.push(start === end ? `${start}` : `${start}-${end}`); - return ranges.join(", "); + return ranges.join(', '); } updateSelectedPagesDisplay() { - const selectedPagesList = document.getElementById("selected-pages-list"); - const selectedPagesInput = document.getElementById("csv-input"); - selectedPagesList.innerHTML = ""; // Clear the list + const selectedPagesList = document.getElementById('selected-pages-list'); + const selectedPagesInput = document.getElementById('csv-input'); + selectedPagesList.innerHTML = ''; // Clear the list window.selectedPages.sort((a, b) => a - b); window.selectedPages.forEach((page) => { - const pageItem = document.createElement("div"); - pageItem.className = "page-item"; + const pageItem = document.createElement('div'); + pageItem.className = 'page-item'; - const pageNumber = document.createElement("span"); + const pageNumber = document.createElement('span'); const pagelabel = /*[[#{multiTool.page}]]*/ 'Page'; - pageNumber.className = "selected-page-number"; + pageNumber.className = 'selected-page-number'; pageNumber.innerText = `${pagelabel} ${page}`; pageItem.appendChild(pageNumber); - const removeBtn = document.createElement("span"); - removeBtn.className = "remove-btn"; - removeBtn.innerHTML = "✕"; + const removeBtn = document.createElement('span'); + removeBtn.className = 'remove-btn'; + removeBtn.innerHTML = '✕'; // Remove page from selected pages list and update display and checkbox removeBtn.onclick = () => { @@ -489,7 +513,7 @@ class PdfContainer { parsePageRanges(ranges) { const pages = new Set(); - ranges.split(',').forEach(range => { + ranges.split(',').forEach((range) => { const [start, end] = range.split('-').map(Number); if (end) { for (let i = start; i <= end; i++) { @@ -503,23 +527,25 @@ class PdfContainer { return Array.from(pages).sort((a, b) => a - b); } - addFilesBlankAll() { - const allPages = this.pagesContainer.querySelectorAll(".page-container"); - allPages.forEach((page, index) => { - if (index !== 0) { - this.addFiles(page, true) - } - }); - } + async addFilesBlankAll() { + const allPages = this.pagesContainer.querySelectorAll('.page-container'); - splitAll() { - const allPages = this.pagesContainer.querySelectorAll(".page-container"); - let splitAllCommand = new SplitAllCommand( + let pageBreakCommand = new PageBreakCommand( allPages, window.selectPage, window.selectedPages, - "split-before" + this.addFilesBlank.bind(this), + this.pagesContainer ); + + await pageBreakCommand.execute(); + + this.undoManager.pushUndoClearRedo(pageBreakCommand); + } + + splitAll() { + const allPages = this.pagesContainer.querySelectorAll('.page-container'); + let splitAllCommand = new SplitAllCommand(allPages, window.selectPage, window.selectedPages, 'split-before'); splitAllCommand.execute(); this.undoManager.pushUndoClearRedo(splitAllCommand); @@ -529,7 +555,7 @@ class PdfContainer { const baseDocument = await PDFLib.PDFDocument.load(baseDocBytes); const pageNum = baseDocument.getPages().length; - splitters.sort((a, b) => a - b);; // We'll sort the separator indexes just in case querySelectorAll does something funny. + splitters.sort((a, b) => a - b); // We'll sort the separator indexes just in case querySelectorAll does something funny. splitters.push(pageNum); // We'll also add a faux separator at the end in order to get the pages after the last separator. const splitDocuments = []; @@ -540,18 +566,18 @@ class PdfContainer { let firstPage = splitterIndex === 0 ? 0 : splitters[splitterIndex - 1]; - const pageIndices = Array.from({ length: splitterPosition - firstPage }, (value, key) => firstPage + key); + const pageIndices = Array.from({length: splitterPosition - firstPage}, (value, key) => firstPage + key); const copiedPages = await subDocument.copyPages(baseDocument, pageIndices); - copiedPages.forEach(copiedPage => { + copiedPages.forEach((copiedPage) => { subDocument.addPage(copiedPage); }); const subDocumentBytes = await subDocument.save(); splitDocuments.push(subDocumentBytes); - }; + } return splitDocuments; } @@ -560,8 +586,10 @@ class PdfContainer { const zip = new JSZip(); for (let i = 0; i < pdfBytesArray.length; i++) { - const documentBlob = new Blob([pdfBytesArray[i]], { type: "application/pdf" }); - zip.file(baseNameString + "-" + (i + 1) + ".pdf", documentBlob); + const documentBlob = new Blob([pdfBytesArray[i]], { + type: 'application/pdf', + }); + zip.file(baseNameString + '-' + (i + 1) + '.pdf', documentBlob); } return zip; @@ -569,10 +597,10 @@ class PdfContainer { async exportPdf(selected) { const pdfDoc = await PDFLib.PDFDocument.create(); - const pageContainers = this.pagesContainer.querySelectorAll(".page-container"); // Select all .page-container elements + const pageContainers = this.pagesContainer.querySelectorAll('.page-container'); // Select all .page-container elements for (var i = 0; i < pageContainers.length; i++) { if (!selected || window.selectedPages.includes(i + 1)) { - const img = pageContainers[i].querySelector("img"); // Find the img element within each .page-container + const img = pageContainers[i].querySelector('img'); // Find the img element within each .page-container if (!img) continue; let page; if (img.doc) { @@ -612,7 +640,7 @@ class PdfContainer { } const rotation = img.style.rotate; if (rotation) { - const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, "")); + const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, '')); page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle)); } } @@ -621,11 +649,11 @@ class PdfContainer { pdfDoc.setProducer(stirlingPDFLabel); const pdfBytes = await pdfDoc.save(); - const pdfBlob = new Blob([pdfBytes], { type: "application/pdf" }); + const pdfBlob = new Blob([pdfBytes], {type: 'application/pdf'}); - const filenameInput = document.getElementById("filename-input"); + const filenameInput = document.getElementById('filename-input'); - let inputArr = filenameInput.value.split("."); + let inputArr = filenameInput.value.split('.'); if (inputArr !== null && inputArr !== undefined && inputArr.length > 0) { inputArr = inputArr.filter((n) => n); // remove all empty strings, nulls or undefined @@ -634,17 +662,18 @@ class PdfContainer { inputArr.pop(); // remove right part after last dot } - filenameInput.value = inputArr.join(""); + filenameInput.value = inputArr.join(''); this.fileName = filenameInput.value; } - const separators = this.pagesContainer.querySelectorAll(".split-before"); - if (separators.length !== 0) { // Split the pdf if there are separators. - const baseName = this.fileName ? this.fileName : "managed"; + const separators = this.pagesContainer.querySelectorAll('.split-before'); + if (separators.length !== 0) { + // Split the pdf if there are separators. + const baseName = this.fileName ? this.fileName : 'managed'; const pagesArray = Array.from(this.pagesContainer.children); const splitters = []; - separators.forEach(page => { + separators.forEach((page) => { const pageIndex = pagesArray.indexOf(page); if (pageIndex !== 0) { splitters.push(pageIndex); @@ -655,80 +684,80 @@ class PdfContainer { const archivedDocuments = await this.nameAndArchiveFiles(splitDocuments, baseName); const self = this; - archivedDocuments.generateAsync({ type: "base64" }).then(function (base64) { - const url = "data:application/zip;base64," + base64; - self.downloadLink = document.createElement("a"); + archivedDocuments.generateAsync({type: 'base64'}).then(function (base64) { + const url = 'data:application/zip;base64,' + base64; + self.downloadLink = document.createElement('a'); self.downloadLink.href = url; - self.downloadLink.setAttribute("download", baseName + ".zip"); - self.downloadLink.setAttribute("target", "_blank"); + self.downloadLink.setAttribute('download', baseName + '.zip'); + self.downloadLink.setAttribute('target', '_blank'); self.downloadLink.click(); }); - - } else { // Continue normally if there are no separators + } else { + // Continue normally if there are no separators const url = URL.createObjectURL(pdfBlob); - const downloadOption = localStorage.getItem("downloadOption"); + const downloadOption = localStorage.getItem('downloadOption'); - if (!filenameInput.value.includes(".pdf")) { - filenameInput.value = filenameInput.value + ".pdf"; + if (!filenameInput.value.includes('.pdf')) { + filenameInput.value = filenameInput.value + '.pdf'; this.fileName = filenameInput.value; } - if (downloadOption === "sameWindow") { + if (downloadOption === 'sameWindow') { // Open the file in the same window window.location.href = url; - } else if (downloadOption === "newWindow") { + } else if (downloadOption === 'newWindow') { // Open the file in a new window - window.open(url, "_blank"); + window.open(url, '_blank'); } else { // Download the file - this.downloadLink = document.createElement("a"); - this.downloadLink.id = "download-link"; + this.downloadLink = document.createElement('a'); + this.downloadLink.id = 'download-link'; this.downloadLink.href = url; // downloadLink.download = this.fileName ? this.fileName : 'managed.pdf'; // downloadLink.download = this.fileName; - this.downloadLink.setAttribute("download", this.fileName ? this.fileName : "managed.pdf"); - this.downloadLink.setAttribute("target", "_blank"); + this.downloadLink.setAttribute('download', this.fileName ? this.fileName : 'managed.pdf'); + this.downloadLink.setAttribute('target', '_blank'); this.downloadLink.onclick = this.setDownloadAttribute; this.downloadLink.click(); } } } - resetPages() { - const pageContainers = this.pagesContainer.querySelectorAll(".page-container"); + resetPages() { + const pageContainers = this.pagesContainer.querySelectorAll('.page-container'); pageContainers.forEach((container, index) => { - container.id = "page-container-" + (index + 1); + container.id = 'page-container-' + (index + 1); }); - const checkboxes = document.querySelectorAll(".pdf-actions_checkbox"); + const checkboxes = document.querySelectorAll('.pdf-actions_checkbox'); window.selectAll = false; - const selectIcon = document.getElementById("select-All-Container"); - const deselectIcon = document.getElementById("deselect-All-Container"); + const selectIcon = document.getElementById('select-All-Container'); + const deselectIcon = document.getElementById('deselect-All-Container'); - selectIcon.style.display = "inline"; - deselectIcon.style.display = "none"; + selectIcon.style.display = 'inline'; + deselectIcon.style.display = 'none'; checkboxes.forEach((checkbox) => { const pageNumber = Array.from(checkbox.parentNode.parentNode.children).indexOf(checkbox.parentNode) + 1; - const index = window.selectedPages.indexOf(pageNumber); - if (index !== -1) { - window.selectedPages.splice(index, 1); - } + const index = window.selectedPages.indexOf(pageNumber); + if (index !== -1) { + window.selectedPages.splice(index, 1); + } }); window.toggleSelectPageVisibility(); } setDownloadAttribute() { - this.downloadLink.setAttribute("download", this.fileName ? this.fileName : "managed.pdf"); + this.downloadLink.setAttribute('download', this.fileName ? this.fileName : 'managed.pdf'); } - updateFilename(fileName = "") { - const filenameInput = document.getElementById("filename-input"); - const pagesContainer = document.getElementById("pages-container"); - const downloadBtn = document.getElementById("export-button"); + updateFilename(fileName = '') { + const filenameInput = document.getElementById('filename-input'); + const pagesContainer = document.getElementById('pages-container'); + const downloadBtn = document.getElementById('export-button'); downloadBtn.disabled = pagesContainer.childElementCount === 0; @@ -752,38 +781,36 @@ class PdfContainer { // } } - toggleSelectPageVisibility() { window.selectPage = !window.selectPage; - const checkboxes = document.querySelectorAll(".pdf-actions_checkbox"); - checkboxes.forEach(checkbox => { - checkbox.classList.toggle("hidden", !window.selectPage); + const checkboxes = document.querySelectorAll('.pdf-actions_checkbox'); + checkboxes.forEach((checkbox) => { + checkbox.classList.toggle('hidden', !window.selectPage); }); - const deleteButton = document.getElementById("delete-button"); - deleteButton.classList.toggle("hidden", !window.selectPage); - const selectedPages = document.getElementById("selected-pages-display"); - selectedPages.classList.toggle("hidden", !window.selectPage); - const selectAll = document.getElementById("select-All-Container"); - selectAll.classList.toggle("hidden", !window.selectPage); - const exportSelected = document.getElementById("export-selected-button"); - exportSelected.classList.toggle("hidden", !window.selectPage); - const selectPagesButton = document.getElementById("select-pages-button"); - selectPagesButton.style.opacity = window.selectPage ? "1" : "0.5"; + const deleteButton = document.getElementById('delete-button'); + deleteButton.classList.toggle('hidden', !window.selectPage); + const selectedPages = document.getElementById('selected-pages-display'); + selectedPages.classList.toggle('hidden', !window.selectPage); + const selectAll = document.getElementById('select-All-Container'); + selectAll.classList.toggle('hidden', !window.selectPage); + const exportSelected = document.getElementById('export-selected-button'); + exportSelected.classList.toggle('hidden', !window.selectPage); + const selectPagesButton = document.getElementById('select-pages-button'); + selectPagesButton.style.opacity = window.selectPage ? '1' : '0.5'; if (window.selectPage) { this.updatePageNumbersAndCheckboxes(); } } - updatePageNumbersAndCheckboxes() { - const pageDivs = document.querySelectorAll(".pdf-actions_container"); + const pageDivs = document.querySelectorAll('.pdf-actions_container'); pageDivs.forEach((div, index) => { const pageNumber = index + 1; - const checkbox = div.querySelector(".pdf-actions_checkbox"); + const checkbox = div.querySelector('.pdf-actions_checkbox'); checkbox.id = `selectPageCheckbox-${pageNumber}`; - checkbox.setAttribute("data-page-number", pageNumber); + checkbox.setAttribute('data-page-number', pageNumber); checkbox.checked = window.selectedPages.includes(pageNumber); }); } @@ -801,8 +828,10 @@ function detectImageType(uint8Array) { } // Check for TIFF signature (little-endian and big-endian) - if ((uint8Array[0] === 73 && uint8Array[1] === 73 && uint8Array[2] === 42 && uint8Array[3] === 0) || - (uint8Array[0] === 77 && uint8Array[1] === 77 && uint8Array[2] === 0 && uint8Array[3] === 42)) { + if ( + (uint8Array[0] === 73 && uint8Array[1] === 73 && uint8Array[2] === 42 && uint8Array[3] === 0) || + (uint8Array[0] === 77 && uint8Array[1] === 77 && uint8Array[2] === 0 && uint8Array[3] === 42) + ) { return 'TIFF'; } @@ -814,6 +843,4 @@ function detectImageType(uint8Array) { return 'UNKNOWN'; } - - export default PdfContainer; diff --git a/src/main/resources/static/js/multitool/commands/add-page.js b/src/main/resources/static/js/multitool/commands/add-page.js new file mode 100644 index 000000000..cd67eba3a --- /dev/null +++ b/src/main/resources/static/js/multitool/commands/add-page.js @@ -0,0 +1,53 @@ +import {Command} from './command.js'; + +export class AddFilesCommand extends Command { + constructor(element, selectedPages, addFilesAction, pagesContainer) { + super(); + this.element = element; + this.selectedPages = selectedPages; + this.addFilesAction = addFilesAction; + this.pagesContainer = pagesContainer; + this.addedElements = []; + } + + async execute() { + const undoBtn = document.getElementById('undo-btn'); + undoBtn.disabled = true; + if (this.element) { + const newElement = await this.addFilesAction(this.element); + if (newElement) { + this.addedElements = newElement; + } + } else { + const newElement = await this.addFilesAction(false); + if (newElement) { + this.addedElements = newElement; + } + } + undoBtn.disabled = false; + } + + undo() { + this.addedElements.forEach((element) => { + const nextSibling = element.nextSibling; + this.pagesContainer.removeChild(element); + + if (this.pagesContainer.childElementCount === 0) { + const filenameInput = document.getElementById('filename-input'); + const filenameParagraph = document.getElementById('filename'); + const downloadBtn = document.getElementById('export-button'); + + filenameInput.disabled = true; + filenameInput.value = ''; + filenameParagraph.innerText = ''; + downloadBtn.disabled = true; + } + + element._nextSibling = nextSibling; + }); + this.addedElements = []; + } + redo() { + this.execute(); + } +} diff --git a/src/main/resources/static/js/multitool/commands/delete-page.js b/src/main/resources/static/js/multitool/commands/delete-page.js index f9a00e7a9..89fbe8a24 100644 --- a/src/main/resources/static/js/multitool/commands/delete-page.js +++ b/src/main/resources/static/js/multitool/commands/delete-page.js @@ -10,7 +10,9 @@ export class DeletePageCommand extends Command { this.filenameInputValue = document.getElementById("filename-input").value; const filenameParagraph = document.getElementById("filename"); - this.filenameParagraphText = filenameParagraph ? filenameParagraph.innerText : ""; + this.filenameParagraphText = filenameParagraph + ? filenameParagraph.innerText + : ""; } execute() { diff --git a/src/main/resources/static/js/multitool/commands/page-break.js b/src/main/resources/static/js/multitool/commands/page-break.js new file mode 100644 index 000000000..2321a5e0b --- /dev/null +++ b/src/main/resources/static/js/multitool/commands/page-break.js @@ -0,0 +1,59 @@ +import {Command} from './command.js'; + +export class PageBreakCommand extends Command { + constructor(elements, isSelectedInWindow, selectedPages, pageBreakCallback, pagesContainer) { + super(); + this.elements = elements; + this.isSelectedInWindow = isSelectedInWindow; + this.selectedPages = selectedPages; + this.pageBreakCallback = pageBreakCallback; + this.pagesContainer = pagesContainer; + this.addedElements = []; + this.originalStates = Array.from(elements, (element) => ({ + element, + hasContent: element.innerHTML.trim() !== '', + })); + } + + async execute() { + const undoBtn = document.getElementById('undo-btn'); + undoBtn.disabled = true; + for (const [index, element] of this.elements.entries()) { + if (!this.isSelectedInWindow || this.selectedPages.includes(index)) { + if (index !== 0) { + const newElement = await this.pageBreakCallback(element, this.addedElements); + + if (newElement) { + this.addedElements = newElement; + } + } + } + } + undoBtn.disabled = false; + } + + undo() { + this.addedElements.forEach((element) => { + const nextSibling = element.nextSibling; + + this.pagesContainer.removeChild(element); + + if (this.pagesContainer.childElementCount === 0) { + const filenameInput = document.getElementById('filename-input'); + const filenameParagraph = document.getElementById('filename'); + const downloadBtn = document.getElementById('export-button'); + + filenameInput.disabled = true; + filenameInput.value = ''; + filenameParagraph.innerText = ''; + downloadBtn.disabled = true; + } + + element._nextSibling = nextSibling; + }); + } + + redo() { + this.execute(); + } +}