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.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
-
-