diff --git a/src/main/resources/static/js/multitool/dragDrop.js b/src/main/resources/static/js/multitool/dragDrop.js new file mode 100644 index 00000000..7a1066fd --- /dev/null +++ b/src/main/resources/static/js/multitool/dragDrop.js @@ -0,0 +1,88 @@ + + +class DragDropManager { + dragContainer; + movePageTo; + pageDragging; + draggelEl; + draggedImageEl; + hoveredEl; + + constructor(id, movePageTo) { + this.dragContainer = document.getElementById(id); + this.movePageTo = movePageTo; + this.pageDragging = false; + this.hoveredEl = undefined; + this.draggelEl = undefined + this.draggedImageEl = undefined; + + this.startDraggingPage = this.startDraggingPage.bind(this); + this.onDragEl = this.onDragEl.bind(this); + this.stopDraggingPage = this.stopDraggingPage.bind(this); + this.attachDragDropCallbacks = this.attachDragDropCallbacks.bind(this); + + } + + startDraggingPage(div, imageSrc) { + this.pageDragging = true; + this.draggedEl = div; + div.classList.add('dragging'); + const imgEl = document.createElement('img'); + imgEl.classList.add('dragged-img'); + imgEl.src = imageSrc; + this.draggedImageEl = imgEl; + this.draggedImageEl.style.left = screenX; + this.draggedImageEl.style.right = screenY; + this.dragContainer.appendChild(imgEl); + window.addEventListener('mouseup', (e) => { + this.stopDraggingPage(); + }) + window.addEventListener('mousemove', this.onDragEl) + } + + onDragEl(mouseEvent) { + const { clientX, clientY } = mouseEvent; + if(this.draggedImageEl) { + this.draggedImageEl.style.left = `${clientX}px`; + this.draggedImageEl.style.top = `${clientY}px`; + } + } + + + stopDraggingPage() { + window.removeEventListener('mousemove', this.onDragEl); + this.draggedImageEl = undefined; + this.pageDragging = false; + this.draggedEl.classList.remove('dragging'); + this.hoveredEl.classList.remove('draghover'); + this.dragContainer.childNodes.forEach((dragChild) => { + this.dragContainer.removeChild(dragChild); + }) + this.movePageTo(this.draggedEl, this.hoveredEl); + } + + + attachDragDropCallbacks(div, imageSrc) { + const onDragStart = () => { + this.startDraggingPage(div, imageSrc); + } + + const onMouseEnter = () => { + if (this.pageDragging) { + this.hoveredEl = div; + div.classList.add('draghover'); + } + } + + const onMouseLeave = () => { + this.hoveredEl = undefined + div.classList.remove('draghover'); + } + + div.addEventListener('dragstart', onDragStart); + div.addEventListener('mouseenter', onMouseEnter); + div.addEventListener('mouseleave', onMouseLeave); + } +} + +export default DragDropManager; \ No newline at end of file diff --git a/src/main/resources/static/js/multitool/horizontalScroll.js b/src/main/resources/static/js/multitool/horizontalScroll.js new file mode 100644 index 00000000..c1c5ca87 --- /dev/null +++ b/src/main/resources/static/js/multitool/horizontalScroll.js @@ -0,0 +1,37 @@ + + +const scrollDivHorizontally = (id) => { + var scrollDelta = 0; // variable to store the accumulated scroll delta + var isScrolling = false; // variable to track if scroll is already in progress + const pagesContainerWrapper = document.getElementById(id); + function scrollLoop() { + // Scroll the div horizontally by a fraction of the accumulated scroll delta + pagesContainerWrapper.scrollLeft += scrollDelta * 0.1; + + // Reduce the accumulated scroll delta by a fraction + scrollDelta *= 0.9; + + // If scroll delta is still significant, continue the scroll loop + if (Math.abs(scrollDelta) > 0.1) { + requestAnimationFrame(scrollLoop); + } else { + isScrolling = false; // Reset scroll in progress flag + } + } + + const divToScrollHorizontally = document.getElementById(id) + divToScrollHorizontally.addEventListener("wheel", function(e) { + e.preventDefault(); // prevent default mousewheel behavior + + // Accumulate the horizontal scroll delta + scrollDelta -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY; + + // If scroll is not already in progress, start the scroll loop + if (!isScrolling) { + isScrolling = true; + requestAnimationFrame(scrollLoop); + } + }); +} + +export default scrollDivHorizontally; diff --git a/src/main/resources/static/js/multitool/imageHighlighter.js b/src/main/resources/static/js/multitool/imageHighlighter.js new file mode 100644 index 00000000..15f54c98 --- /dev/null +++ b/src/main/resources/static/js/multitool/imageHighlighter.js @@ -0,0 +1,27 @@ +const getImageHighlighterCallback = (id) => { + const imageHighlighter = document.getElementById(id); + imageHighlighter.onclick = () => { + imageHighlighter.childNodes.forEach((child) => { + child.classList.add('remove'); + setTimeout(() => { + imageHighlighter.removeChild(child); + }, 100) + }) + } + + const imageHighlightCallback = (highlightEvent) => { + var bigImg = document.createElement('img'); + bigImg.onclick = (imageClickEvent) => { + // This prevents the highlighter's onClick from closing the image when clicking on the image + // instead of next to it. + imageClickEvent.preventDefault(); + imageClickEvent.stopPropagation(); + }; + bigImg.src = highlightEvent.target.src; + imageHighlighter.appendChild(bigImg); + }; + + return imageHighlightCallback +} + +export default getImageHighlighterCallback; \ No newline at end of file diff --git a/src/main/resources/static/js/multitool/pdfActions.js b/src/main/resources/static/js/multitool/pdfActions.js new file mode 100644 index 00000000..14bfc349 --- /dev/null +++ b/src/main/resources/static/js/multitool/pdfActions.js @@ -0,0 +1,156 @@ +class PdfActionsManager { + callbacks; + pageDirection; + constructor(id, { movePageTo, addPdfs, rotateElement }) { + this.pageDirection = document.documentElement.getAttribute("lang-direction"); + const moveUpButtonCallback = e => { + var imgContainer = e.target; + while (!imgContainer.classList.contains(id)) { + imgContainer = imgContainer.parentNode; + } + + const sibling = imgContainer.previousSibling; + if (sibling) { + movePageTo(imgContainer, sibling, true); + } + }; + const moveDownButtonCallback = e => { + var imgContainer = e.target; + while (!imgContainer.classList.contains(id)) { + imgContainer = imgContainer.parentNode; + } + const sibling = imgContainer.nextSibling; + if (sibling) { + movePageTo(imgContainer, sibling.nextSibling, true); + } + }; + const rotateCCWButtonCallback = e => { + var imgContainer = e.target; + while (!imgContainer.classList.contains(id)) { + imgContainer = imgContainer.parentNode; + } + const img = imgContainer.querySelector("img"); + + rotateElement(img, -90) + }; + const rotateCWButtonCallback = e => { + var imgContainer = e.target; + while (!imgContainer.classList.contains(id)) { + imgContainer = imgContainer.parentNode; + } + const img = imgContainer.querySelector("img"); + + rotateElement(img, 90) + }; + const deletePageButtonCallback = e => { + var imgContainer = e.target; + while (!imgContainer.classList.contains(id)) { + imgContainer = imgContainer.parentNode; + } + pagesContainer.removeChild(imgContainer); + }; + const insertFileButtonCallback = e => { + var imgContainer = e.target; + while (!imgContainer.classList.contains(id)) { + imgContainer = imgContainer.parentNode; + } + addPdfs(imgContainer) + }; + + this.callbacks = { + moveUpButtonCallback, + moveDownButtonCallback, + rotateCCWButtonCallback, + rotateCWButtonCallback, + deletePageButtonCallback, + insertFileButtonCallback + } + } + + attachPDFActions(div) { + const leftDirection = this.pageDirection === 'rtl' ? 'right' : 'left' + const rightDirection = this.pageDirection === 'rtl' ? 'left' : 'right' + const buttonContainer = document.createElement('div'); + + buttonContainer.classList.add("button-container"); + + const moveUp = document.createElement('button'); + moveUp.classList.add("move-left-button","btn", "btn-secondary"); + moveUp.innerHTML = ``; + moveUp.onclick = this.callbacks.moveUpButtonCallback; + buttonContainer.appendChild(moveUp); + + const moveDown = document.createElement('button'); + moveDown.classList.add("move-right-button","btn", "btn-secondary"); + moveDown.innerHTML = ``; + moveDown.onclick = this.callbacks.moveDownButtonCallback; + buttonContainer.appendChild(moveDown); + + const rotateCCW = document.createElement('button'); + rotateCCW.classList.add("btn", "btn-secondary"); + rotateCCW.innerHTML = ` + + + `; + rotateCCW.onclick = this.callbacks.rotateCCWButtonCallback; + buttonContainer.appendChild(rotateCCW); + + const rotateCW = document.createElement('button'); + rotateCW.classList.add("btn", "btn-secondary"); + rotateCW.innerHTML = ` + + + `; + rotateCW.onclick = this.callbacks.rotateCWButtonCallback; + buttonContainer.appendChild(rotateCW); + + const deletePage = document.createElement('button'); + deletePage.classList.add("btn", "btn-danger"); + deletePage.innerHTML = ` + + + `; + deletePage.onclick = this.callbacks.deletePageButtonCallback; + buttonContainer.appendChild(deletePage); + + div.appendChild(buttonContainer); + + const insertFileButtonContainer = document.createElement('div'); + + insertFileButtonContainer.classList.add( + "insert-file-button-container", + leftDirection, + `align-center-${leftDirection}`); + + const insertFileButton = document.createElement('button'); + insertFileButton.classList.add("btn", "btn-primary", "insert-file-button"); + insertFileButton.innerHTML = ` + + + `; + insertFileButton.onclick = this.callbacks.insertFileButtonCallback; + insertFileButtonContainer.appendChild(insertFileButton); + + div.appendChild(insertFileButtonContainer); + + // add this button to every element, but only show it on the last one :D + const insertFileButtonRightContainer = document.createElement('div'); + insertFileButtonRightContainer.classList.add( + "insert-file-button-container", + rightDirection, + `align-center-${rightDirection}`); + + const insertFileButtonRight = document.createElement('button'); + insertFileButtonRight.classList.add("btn", "btn-primary", "insert-file-button"); + insertFileButtonRight.innerHTML = ` + + + insertFileButtonRight`; + insertFileButtonRight.onclick = () => addPdfs(); + insertFileButtonRightContainer.appendChild(insertFileButtonRight); + + div.appendChild(insertFileButtonRightContainer); + } +} + +export default PdfActionsManager; \ No newline at end of file diff --git a/src/main/resources/static/js/multitool/pdfContainer.js b/src/main/resources/static/js/multitool/pdfContainer.js new file mode 100644 index 00000000..64c5efa4 --- /dev/null +++ b/src/main/resources/static/js/multitool/pdfContainer.js @@ -0,0 +1,217 @@ +import DragDropManager from "./dragDrop.js"; +import scrollDivHorizontally from "./horizontalScroll.js"; +import getImageHighlighterCallback from "./imageHighlighter.js"; +import PdfActionsManager from './pdfActions.js'; + +const createPdfContainer = (id, wrapperId, highlighterId, dragElId) => { + var fileName = null; + const pagesContainer = document.getElementById(id); + const pagesContainerWrapper = document.getElementById(wrapperId); + + + const movePageTo = (startElement, endElement, scrollTo = false) => { + const childArray = Array.from(pagesContainer.childNodes); + const startIndex = childArray.indexOf(startElement); + const endIndex = childArray.indexOf(endElement); + pagesContainer.removeChild(startElement); + if(!endElement) { + pagesContainer.append(startElement); + } else { + pagesContainer.insertBefore(startElement, endElement); + } + + if(scrollTo) { + const { width } = startElement.getBoundingClientRect(); + const vector = (endIndex !== -1 && startIndex > endIndex) + ? 0-width + : width; + + pagesContainerWrapper.scroll({ + left: pagesContainerWrapper.scrollLeft + vector, + }) + } + } + + function addPdfs(nextSiblingElement) { + var input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.setAttribute("accept", "application/pdf"); + + input.onchange = async(e) => { + const files = e.target.files; + fileName = files[0].name; + for (var i=0; i < files.length; i++) { + addPdfFile(files[i], nextSiblingElement); + } + + document.querySelectorAll(".enable-on-file").forEach(element => { + element.disabled = false; + }); + } + + input.click(); + } + + function rotateElement(element, deg) { + var lastTransform = element.style.rotate; + if (!lastTransform) { + lastTransform = "0"; + } + const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, '')); + const newAngle = lastAngle + deg; + + element.style.rotate = newAngle + "deg"; + } + + scrollDivHorizontally(wrapperId); + + var imageHighlighterCallback; + if (highlighterId) { + imageHighlighterCallback = getImageHighlighterCallback(highlighterId); + } + var dragDropManager; + if(dragElId) { + dragDropManager = new DragDropManager('drag-container', movePageTo); + } + + var pdfActionManager = new PdfActionsManager('page-container', { movePageTo, addPdfs, rotateElement }); + + async function addPdfFile(file, nextSiblingElement) { + const { renderer, pdfDocument } = await loadFile(file); + + for (var i=0; i < renderer.pageCount; i++) { + const div = document.createElement('div'); + + div.classList.add("page-container"); + + 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); + + + if(dragDropManager) { + dragDropManager.attachDragDropCallbacks(div, imageSrc); + } + + /** + * Making pages larger when clicking on them + */ + if(imageHighlighterCallback) { + img.addEventListener('click', imageHighlighterCallback) + } + + /** + * Rendering the various buttons to manipulate and move pdf pages + */ + pdfActionManager.attachPDFActions(div); + + if (nextSiblingElement) { + pagesContainer.insertBefore(div, nextSiblingElement); + } else { + pagesContainer.appendChild(div); + } + } + } + + async function toRenderer(objectUrl) { + const pdf = await pdfjsLib.getDocument(objectUrl).promise; + return { + document: pdf, + pageCount: pdf.numPages, + renderPage: async function(pageIdx) { + const page = await this.document.getPage(pageIdx+1); + + const canvas = document.createElement("canvas"); + + // set the canvas size to the size of the page + if (page.rotate == 90 || page.rotate == 270) { + canvas.width = page.view[3]; + canvas.height = page.view[2]; + } else { + canvas.width = page.view[2]; + canvas.height = page.view[3]; + } + + // render the page onto the canvas + var renderContext = { + canvasContext: canvas.getContext("2d"), + viewport: page.getViewport({ scale: 1 }) + }; + + await page.render(renderContext).promise; + return canvas.toDataURL(); + } + }; + } + + async function toPdfLib(objectUrl) { + const existingPdfBytes = await fetch(objectUrl).then(res => res.arrayBuffer()); + const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes); + return pdfDoc; + } + + async function loadFile(file) { + var objectUrl = URL.createObjectURL(file); + var pdfDocument = await toPdfLib(objectUrl); + var renderer = await toRenderer(objectUrl); + return { renderer, pdfDocument }; + } + + function rotateAll(deg) { + for (var i=0; i -
@@ -60,460 +59,22 @@
-