mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-09-13 12:15:29 +02:00
Resolve conflicts using files from Stirling-2.0
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
class DragDropManager {
|
||||
constructor(id, wrapperId) {
|
||||
this.dragContainer = document.getElementById(id);
|
||||
this.pageDirection = document.documentElement.getAttribute('dir');
|
||||
this.wrapper = document.getElementById(wrapperId);
|
||||
this.pageDragging = false;
|
||||
this.hoveredEl = undefined;
|
||||
this.draggedImageEl = undefined;
|
||||
this.draggedEl = undefined;
|
||||
this.selectedPageElements = []; // Store selected pages for multi-page mode
|
||||
this.elementTimeouts = new Map();
|
||||
|
||||
// Add CSS dynamically
|
||||
const styleElement = document.createElement('link');
|
||||
styleElement.rel = 'stylesheet';
|
||||
styleElement.href = 'css/dragdrop.css';
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
// Create the endpoint element
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('page-container');
|
||||
div.classList.add('drag-manager_endpoint');
|
||||
div.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
|
||||
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293V6.5z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
|
||||
</svg>`;
|
||||
this.endInsertionElement = div;
|
||||
|
||||
// Bind methods
|
||||
this.startDraggingPage = this.startDraggingPage.bind(this);
|
||||
this.onDragEl = this.onDragEl.bind(this);
|
||||
this.stopDraggingPage = this.stopDraggingPage.bind(this);
|
||||
|
||||
this.adapt(div);
|
||||
}
|
||||
|
||||
startDraggingPage(div) {
|
||||
if (window.selectPage) {
|
||||
// Multi-page drag logic
|
||||
this.selectedPageElements = window.selectedPages
|
||||
.map((index) => {
|
||||
const pageEl = Array.from(this.wrapper.childNodes)[index];
|
||||
if (pageEl) {
|
||||
pageEl.initialTransform = pageEl.style.transform || 'translate(0px, 0px)';
|
||||
pageEl.classList.add('drag-manager_dragging');
|
||||
}
|
||||
return pageEl;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (this.selectedPageElements.length === 0) return;
|
||||
|
||||
this.pageDragging = true;
|
||||
this.draggedImageEl = document.createElement('div');
|
||||
this.draggedImageEl.classList.add('multidrag');
|
||||
this.draggedImageEl.textContent = `${this.selectedPageElements.length} ${window.translations.dragDropMessage}`;
|
||||
this.draggedImageEl.style.visibility = 'hidden';
|
||||
|
||||
this.dragContainer.appendChild(this.draggedImageEl);
|
||||
} else {
|
||||
// Single-page drag logic
|
||||
this.pageDragging = true;
|
||||
this.draggedEl = div;
|
||||
const img = div.querySelector('img');
|
||||
div.classList.add('drag-manager_dragging');
|
||||
div.classList.remove('moved-element', 'remove');
|
||||
const imgEl = document.createElement('img');
|
||||
imgEl.classList.add('dragged-img');
|
||||
imgEl.src = img.src;
|
||||
imgEl.style.visibility = 'hidden';
|
||||
imgEl.style.transform = `rotate(${img.style.rotate === '' ? '0deg' : img.style.rotate}) translate(-50%, -50%)`;
|
||||
this.draggedImageEl = imgEl;
|
||||
this.dragContainer.appendChild(imgEl);
|
||||
}
|
||||
|
||||
// Common setup for both modes
|
||||
window.addEventListener('mouseup', this.stopDraggingPage);
|
||||
window.addEventListener('mousemove', this.onDragEl);
|
||||
this.wrapper.classList.add('drag-manager_dragging-container');
|
||||
this.wrapper.appendChild(this.endInsertionElement);
|
||||
}
|
||||
|
||||
onDragEl(mouseEvent) {
|
||||
const { clientX, clientY } = mouseEvent;
|
||||
if (this.draggedImageEl) {
|
||||
this.draggedImageEl.style.visibility = 'visible';
|
||||
this.draggedImageEl.style.left = `${clientX}px`;
|
||||
this.draggedImageEl.style.top = `${clientY}px`;
|
||||
}
|
||||
}
|
||||
|
||||
stopDraggingPage() {
|
||||
window.removeEventListener('mousemove', this.onDragEl);
|
||||
this.wrapper.classList.remove('drag-manager_dragging-container');
|
||||
this.wrapper.removeChild(this.endInsertionElement);
|
||||
window.removeEventListener('mouseup', this.stopDraggingPage);
|
||||
|
||||
if (this.draggedImageEl) {
|
||||
this.dragContainer.removeChild(this.draggedImageEl);
|
||||
this.draggedImageEl = undefined;
|
||||
}
|
||||
|
||||
if (window.selectPage) {
|
||||
// Multi-page drop logic
|
||||
if (
|
||||
!this.hoveredEl ||
|
||||
!this.hoveredEl.classList.contains('page-container') ||
|
||||
this.selectedPageElements.includes(this.hoveredEl)
|
||||
) {
|
||||
this.selectedPageElements.forEach((pageEl) => {
|
||||
pageEl.style.transform = pageEl.initialTransform || 'translate(0px, 0px)';
|
||||
pageEl.classList.remove('drag-manager_dragging');
|
||||
});
|
||||
} else {
|
||||
this.selectedPageElements.forEach((pageEl) => {
|
||||
pageEl.classList.remove('drag-manager_dragging');
|
||||
});
|
||||
|
||||
this.movePageTo(
|
||||
this.selectedPageElements,
|
||||
this.hoveredEl === this.endInsertionElement
|
||||
? null
|
||||
: this.hoveredEl);
|
||||
|
||||
this.selectedPageElements.forEach((pageEl) => {
|
||||
// Handle timeout for the current element
|
||||
this.handleTimeoutForElement(pageEl);
|
||||
});
|
||||
}
|
||||
this.selectedPageElements = [];
|
||||
window.resetPages();
|
||||
} else {
|
||||
// Single-page drop logic
|
||||
if (
|
||||
!this.hoveredEl ||
|
||||
!this.hoveredEl.classList.contains('page-container') ||
|
||||
this.hoveredEl === this.draggedEl
|
||||
) {
|
||||
this.draggedEl.style.transform = this.draggedEl.initialTransform || 'translate(0px, 0px)';
|
||||
this.draggedEl.classList.remove('drag-manager_dragging');
|
||||
return;
|
||||
}
|
||||
|
||||
this.draggedEl.classList.remove('drag-manager_dragging');
|
||||
|
||||
if (this.hoveredEl === this.endInsertionElement) {
|
||||
this.movePageTo(this.draggedEl);
|
||||
} else {
|
||||
this.movePageTo(this.draggedEl, this.hoveredEl);
|
||||
}
|
||||
|
||||
// Handle timeout for the current element
|
||||
this.handleTimeoutForElement(this.draggedEl);
|
||||
}
|
||||
|
||||
this.pageDragging = false;
|
||||
}
|
||||
|
||||
// Helper function to manage independent timeouts
|
||||
handleTimeoutForElement(element) {
|
||||
// Clear existing timeout if present
|
||||
if (this.elementTimeouts.has(element)) {
|
||||
clearTimeout(this.elementTimeouts.get(element));
|
||||
}
|
||||
|
||||
// Add the moved-element class and set a new timeout
|
||||
element.classList.remove('remove');
|
||||
element.classList.add('moved-element');
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
element.classList.add('remove');
|
||||
this.elementTimeouts.delete(element); // Cleanup the timeout map
|
||||
}, 2000);
|
||||
|
||||
// Store the timeout ID for this element
|
||||
this.elementTimeouts.set(element, timeoutId);
|
||||
}
|
||||
|
||||
setActions({ movePageTo }) {
|
||||
this.movePageTo = movePageTo;
|
||||
}
|
||||
|
||||
adapt(div) {
|
||||
const onDragStart = (e) => {
|
||||
e.preventDefault();
|
||||
this.startDraggingPage(div);
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (this.pageDragging) {
|
||||
this.hoveredEl = div;
|
||||
div.classList.add('drag-manager_draghover');
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
this.hoveredEl = undefined;
|
||||
div.classList.remove('drag-manager_draghover');
|
||||
};
|
||||
|
||||
div.addEventListener('dragstart', onDragStart);
|
||||
div.addEventListener('mouseenter', onMouseEnter);
|
||||
div.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
||||
export default DragDropManager;
|
||||
@@ -0,0 +1,66 @@
|
||||
class ImageHighlighter {
|
||||
imageHighlighter;
|
||||
constructor(id) {
|
||||
this.imageHighlighter = document.getElementById(id);
|
||||
this.imageHighlightCallback = this.imageHighlightCallback.bind(this);
|
||||
|
||||
var styleElement = document.createElement("link");
|
||||
styleElement.rel = "stylesheet";
|
||||
styleElement.href = "css/imageHighlighter.css";
|
||||
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
this.imageHighlighter.onclick = () => {
|
||||
this.imageHighlighter.childNodes.forEach((child) => {
|
||||
child.classList.add("remove");
|
||||
setTimeout(() => {
|
||||
this.imageHighlighter.removeChild(child);
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
bigImg.style.rotate = highlightEvent.target.style.rotate;
|
||||
this.imageHighlighter.appendChild(bigImg);
|
||||
}
|
||||
|
||||
setActions() {
|
||||
// not needed in this case
|
||||
}
|
||||
|
||||
adapt(div) {
|
||||
const img = div.querySelector(".page-image");
|
||||
img.addEventListener("click", this.imageHighlightCallback);
|
||||
return div;
|
||||
}
|
||||
|
||||
async addImageFile(file, nextSiblingElement) {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("page-container");
|
||||
|
||||
var img = document.createElement("img");
|
||||
img.classList.add("page-image");
|
||||
img.src = URL.createObjectURL(file);
|
||||
div.appendChild(img);
|
||||
|
||||
this.pdfAdapters.forEach((adapter) => {
|
||||
adapter.adapt?.(div);
|
||||
});
|
||||
if (nextSiblingElement) {
|
||||
this.pagesContainer.insertBefore(div, nextSiblingElement);
|
||||
} else {
|
||||
this.pagesContainer.appendChild(div);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageHighlighter;
|
||||
@@ -0,0 +1,272 @@
|
||||
import { DeletePageCommand } from "./commands/delete-page.js";
|
||||
import { SelectPageCommand } from "./commands/select.js";
|
||||
import { SplitFileCommand } from "./commands/split.js";
|
||||
import { UndoManager } from "./UndoManager.js";
|
||||
|
||||
class PdfActionsManager {
|
||||
pageDirection;
|
||||
pagesContainer;
|
||||
static selectedPages = []; // Static property shared across all instances
|
||||
undoManager;
|
||||
|
||||
constructor(id, undoManager) {
|
||||
this.pagesContainer = document.getElementById(id);
|
||||
this.pageDirection = document.documentElement.getAttribute("dir");
|
||||
|
||||
this.undoManager = undoManager || new UndoManager();
|
||||
|
||||
var styleElement = document.createElement("link");
|
||||
styleElement.rel = "stylesheet";
|
||||
styleElement.href = "css/pdfActions.css";
|
||||
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
getPageContainer(element) {
|
||||
var container = element;
|
||||
while (!container.classList.contains("page-container")) {
|
||||
container = container.parentNode;
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
moveUpButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
|
||||
const sibling = imgContainer.previousSibling;
|
||||
if (sibling) {
|
||||
this.movePageTo(imgContainer, sibling, true);
|
||||
}
|
||||
}
|
||||
|
||||
moveDownButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
const sibling = imgContainer.nextSibling;
|
||||
if (sibling) {
|
||||
this.movePageTo(
|
||||
imgContainer,
|
||||
sibling.nextSibling,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
rotateCCWButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
const img = imgContainer.querySelector("img");
|
||||
|
||||
let rotateCommand = this.rotateElement(img, -90);
|
||||
this._pushUndoClearRedo(rotateCommand);
|
||||
}
|
||||
|
||||
rotateCWButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
const img = imgContainer.querySelector("img");
|
||||
|
||||
let rotateCommand = this.rotateElement(img, 90);
|
||||
this._pushUndoClearRedo(rotateCommand);
|
||||
}
|
||||
|
||||
deletePageButtonCallback(e) {
|
||||
let imgContainer = this.getPageContainer(e.target);
|
||||
let deletePageCommand = new DeletePageCommand(
|
||||
imgContainer,
|
||||
this.pagesContainer
|
||||
);
|
||||
deletePageCommand.execute();
|
||||
|
||||
this._pushUndoClearRedo(deletePageCommand);
|
||||
}
|
||||
|
||||
insertFileButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
this.addFiles(imgContainer);
|
||||
}
|
||||
|
||||
insertFileBlankButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
this.addFiles(imgContainer, true);
|
||||
}
|
||||
|
||||
splitFileButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
|
||||
let splitFileCommand = new SplitFileCommand(imgContainer, "split-before");
|
||||
splitFileCommand.execute();
|
||||
|
||||
this._pushUndoClearRedo(splitFileCommand);
|
||||
}
|
||||
|
||||
_pushUndoClearRedo(command) {
|
||||
this.undoManager.pushUndoClearRedo(command);
|
||||
}
|
||||
|
||||
setActions({ movePageTo, addFiles, rotateElement }) {
|
||||
this.movePageTo = movePageTo;
|
||||
this.addFiles = addFiles;
|
||||
this.rotateElement = rotateElement;
|
||||
|
||||
this.moveUpButtonCallback = this.moveUpButtonCallback.bind(this);
|
||||
this.moveDownButtonCallback = this.moveDownButtonCallback.bind(this);
|
||||
this.rotateCCWButtonCallback = this.rotateCCWButtonCallback.bind(this);
|
||||
this.rotateCWButtonCallback = this.rotateCWButtonCallback.bind(this);
|
||||
this.deletePageButtonCallback = this.deletePageButtonCallback.bind(this);
|
||||
this.insertFileButtonCallback = this.insertFileButtonCallback.bind(this);
|
||||
this.insertFileBlankButtonCallback = this.insertFileBlankButtonCallback.bind(this);
|
||||
this.splitFileButtonCallback = this.splitFileButtonCallback.bind(this);
|
||||
}
|
||||
|
||||
|
||||
adapt(div) {
|
||||
div.classList.add("pdf-actions_container");
|
||||
const leftDirection = this.pageDirection === "rtl" ? "right" : "left";
|
||||
const rightDirection = this.pageDirection === "rtl" ? "left" : "right";
|
||||
const buttonContainer = document.createElement("div");
|
||||
|
||||
buttonContainer.classList.add("btn-group", "pdf-actions_button-container", "hide-on-drag");
|
||||
|
||||
const moveUp = document.createElement("button");
|
||||
moveUp.classList.add("pdf-actions_move-left-button", "btn", "btn-secondary");
|
||||
moveUp.setAttribute('title', window.translations.moveLeft);
|
||||
moveUp.innerHTML = `<span class="material-symbols-rounded">arrow_${leftDirection}_alt</span>`;
|
||||
moveUp.onclick = this.moveUpButtonCallback;
|
||||
buttonContainer.appendChild(moveUp);
|
||||
|
||||
const moveDown = document.createElement("button");
|
||||
moveDown.classList.add("pdf-actions_move-right-button", "btn", "btn-secondary");
|
||||
moveDown.setAttribute('title', window.translations.moveRight);
|
||||
moveDown.innerHTML = `<span class="material-symbols-rounded">arrow_${rightDirection}_alt</span>`;
|
||||
moveDown.onclick = this.moveDownButtonCallback;
|
||||
buttonContainer.appendChild(moveDown);
|
||||
|
||||
|
||||
const rotateCCW = document.createElement("button");
|
||||
rotateCCW.classList.add("btn", "btn-secondary");
|
||||
rotateCCW.setAttribute('title', window.translations.rotateLeft);
|
||||
rotateCCW.innerHTML = `<span class="material-symbols-rounded">rotate_left</span>`;
|
||||
rotateCCW.onclick = this.rotateCCWButtonCallback;
|
||||
buttonContainer.appendChild(rotateCCW);
|
||||
|
||||
const rotateCW = document.createElement("button");
|
||||
rotateCW.classList.add("btn", "btn-secondary");
|
||||
rotateCW.setAttribute('title', window.translations.rotateRight);
|
||||
rotateCW.innerHTML = `<span class="material-symbols-rounded">rotate_right</span>`;
|
||||
rotateCW.onclick = this.rotateCWButtonCallback;
|
||||
buttonContainer.appendChild(rotateCW);
|
||||
|
||||
const deletePage = document.createElement("button");
|
||||
deletePage.classList.add("btn", "btn-danger");
|
||||
deletePage.setAttribute('title', window.translations.delete);
|
||||
deletePage.innerHTML = `<span class="material-symbols-rounded">delete</span>`;
|
||||
deletePage.onclick = this.deletePageButtonCallback;
|
||||
buttonContainer.appendChild(deletePage);
|
||||
|
||||
div.appendChild(buttonContainer);
|
||||
|
||||
//enerate checkbox to select individual pages
|
||||
const selectCheckbox = document.createElement("input");
|
||||
selectCheckbox.type = "checkbox";
|
||||
selectCheckbox.classList.add("pdf-actions_checkbox", "form-check-input");
|
||||
selectCheckbox.id = `selectPageCheckbox`;
|
||||
selectCheckbox.checked = window.selectAll;
|
||||
|
||||
div.appendChild(selectCheckbox);
|
||||
|
||||
//only show whenpage select mode is active
|
||||
if (!window.selectPage) {
|
||||
selectCheckbox.classList.add("hidden");
|
||||
} else {
|
||||
selectCheckbox.classList.remove("hidden");
|
||||
}
|
||||
|
||||
selectCheckbox.onchange = () => {
|
||||
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
|
||||
let selectPageCommand = new SelectPageCommand(pageNumber, selectCheckbox);
|
||||
selectPageCommand.execute();
|
||||
};
|
||||
|
||||
const insertFileButtonContainer = document.createElement("div");
|
||||
|
||||
insertFileButtonContainer.classList.add(
|
||||
"pdf-actions_insert-file-button-container",
|
||||
leftDirection,
|
||||
`align-center-${leftDirection}`,
|
||||
);
|
||||
|
||||
const insertFileButton = document.createElement("button");
|
||||
insertFileButton.classList.add("btn", "btn-primary");
|
||||
moveUp.setAttribute('title', window.translations.addFile);
|
||||
insertFileButton.innerHTML = `<span class="material-symbols-rounded">add</span>`;
|
||||
insertFileButton.onclick = this.insertFileButtonCallback;
|
||||
insertFileButtonContainer.appendChild(insertFileButton);
|
||||
|
||||
const splitFileButton = document.createElement("button");
|
||||
splitFileButton.classList.add("btn", "btn-primary");
|
||||
splitFileButton.setAttribute('title', window.translations.split);
|
||||
splitFileButton.innerHTML = `<span class="material-symbols-rounded">cut</span>`;
|
||||
splitFileButton.onclick = this.splitFileButtonCallback;
|
||||
insertFileButtonContainer.appendChild(splitFileButton);
|
||||
|
||||
const insertFileBlankButton = document.createElement("button");
|
||||
insertFileBlankButton.classList.add("btn", "btn-primary");
|
||||
insertFileBlankButton.setAttribute('title', window.translations.insertPageBreak);
|
||||
insertFileBlankButton.innerHTML = `<span class="material-symbols-rounded">insert_page_break</span>`;
|
||||
insertFileBlankButton.onclick = this.insertFileBlankButtonCallback;
|
||||
insertFileButtonContainer.appendChild(insertFileBlankButton);
|
||||
|
||||
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(
|
||||
"pdf-actions_insert-file-button-container",
|
||||
rightDirection,
|
||||
`align-center-${rightDirection}`,
|
||||
);
|
||||
|
||||
const insertFileButtonRight = document.createElement("button");
|
||||
insertFileButtonRight.classList.add("btn", "btn-primary");
|
||||
insertFileButtonRight.innerHTML = `<span class="material-symbols-rounded">add</span>`;
|
||||
insertFileButtonRight.onclick = () => addFiles();
|
||||
insertFileButtonRightContainer.appendChild(insertFileButtonRight);
|
||||
|
||||
div.appendChild(insertFileButtonRightContainer);
|
||||
|
||||
const adaptPageNumber = (pageNumber, div) => {
|
||||
const pageNumberElement = document.createElement("span");
|
||||
pageNumberElement.classList.add("page-number");
|
||||
pageNumberElement.textContent = pageNumber;
|
||||
|
||||
div.insertBefore(pageNumberElement, div.firstChild);
|
||||
};
|
||||
|
||||
div.addEventListener("mouseenter", () => {
|
||||
window.updatePageNumbersAndCheckboxes();
|
||||
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
|
||||
adaptPageNumber(pageNumber, div);
|
||||
const checkbox = document.getElementById(`selectPageCheckbox-${pageNumber}`);
|
||||
if (checkbox && !window.selectPage) {
|
||||
checkbox.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
div.addEventListener("mouseleave", () => {
|
||||
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
|
||||
const pageNumberElement = div.querySelector(".page-number");
|
||||
if (pageNumberElement) {
|
||||
div.removeChild(pageNumberElement);
|
||||
}
|
||||
const checkbox = document.getElementById(`selectPageCheckbox-${pageNumber}`);
|
||||
if (checkbox && !window.selectPage) {
|
||||
checkbox.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("selectedPagesUpdated", () => {
|
||||
window.updateSelectedPagesDisplay();
|
||||
});
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
||||
export default PdfActionsManager;
|
||||
@@ -0,0 +1,966 @@
|
||||
import { MovePageCommand } 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';
|
||||
import { DecryptFile } from '../DecryptFiles.js';
|
||||
import { CommandSequence } from './commands/commands-sequence.js';
|
||||
|
||||
class PdfContainer {
|
||||
fileName;
|
||||
pagesContainer;
|
||||
pagesContainerWrapper;
|
||||
pdfAdapters;
|
||||
downloadLink;
|
||||
undoManager;
|
||||
|
||||
constructor(id, wrapperId, pdfAdapters, undoManager) {
|
||||
this.pagesContainer = document.getElementById(id);
|
||||
this.pagesContainerWrapper = document.getElementById(wrapperId);
|
||||
this.downloadLink = null;
|
||||
this.movePageTo = this.movePageTo.bind(this);
|
||||
this.addFiles = this.addFiles.bind(this);
|
||||
this.addFilesFromFiles = this.addFilesFromFiles.bind(this);
|
||||
this.rotateElement = this.rotateElement.bind(this);
|
||||
this.rotateAll = this.rotateAll.bind(this);
|
||||
this.exportPdf = this.exportPdf.bind(this);
|
||||
this.updateFilename = this.updateFilename.bind(this);
|
||||
this.setDownloadAttribute = this.setDownloadAttribute.bind(this);
|
||||
this.preventIllegalChars = this.preventIllegalChars.bind(this);
|
||||
this.addImageFile = this.addImageFile.bind(this);
|
||||
this.nameAndArchiveFiles = this.nameAndArchiveFiles.bind(this);
|
||||
this.splitPDF = this.splitPDF.bind(this);
|
||||
this.splitAll = this.splitAll.bind(this);
|
||||
this.deleteSelected = this.deleteSelected.bind(this);
|
||||
this.selectAll = this.selectAll.bind(this);
|
||||
this.deselectAll = this.deselectAll.bind(this);
|
||||
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.removeAllElements = this.removeAllElements.bind(this);
|
||||
this.resetPages = this.resetPages.bind(this);
|
||||
|
||||
this.decryptFile = new DecryptFile();
|
||||
|
||||
this.undoManager = undoManager || new UndoManager();
|
||||
|
||||
this.pdfAdapters = pdfAdapters;
|
||||
|
||||
this.pdfAdapters.forEach((adapter) => {
|
||||
adapter.setActions({
|
||||
movePageTo: this.movePageTo,
|
||||
addFiles: this.addFiles,
|
||||
rotateElement: this.rotateElement,
|
||||
updateFilename: this.updateFilename,
|
||||
deleteSelected: this.deleteSelected,
|
||||
});
|
||||
});
|
||||
|
||||
window.addFiles = this.addFiles;
|
||||
window.exportPdf = this.exportPdf;
|
||||
window.rotateAll = this.rotateAll;
|
||||
window.splitAll = this.splitAll;
|
||||
window.deleteSelected = this.deleteSelected;
|
||||
window.selectAll = this.selectAll;
|
||||
window.deselectAll = this.deselectAll;
|
||||
window.updateSelectedPagesDisplay = this.updateSelectedPagesDisplay;
|
||||
window.toggleSelectPageVisibility = this.toggleSelectPageVisibility;
|
||||
window.updatePagesFromCSV = this.updatePagesFromCSV;
|
||||
window.updateSelectedPagesDisplay = this.updateSelectedPagesDisplay;
|
||||
window.updatePageNumbersAndCheckboxes = this.updatePageNumbersAndCheckboxes;
|
||||
window.addFilesBlankAll = this.addFilesBlankAll;
|
||||
window.removeAllElements = this.removeAllElements;
|
||||
window.resetPages = this.resetPages;
|
||||
|
||||
let undoBtn = document.getElementById('undo-btn');
|
||||
let redoBtn = document.getElementById('redo-btn');
|
||||
|
||||
document.addEventListener('undo-manager-update', (e) => {
|
||||
let canUndo = e.detail.canUndo;
|
||||
let canRedo = e.detail.canRedo;
|
||||
|
||||
undoBtn.disabled = !canUndo;
|
||||
redoBtn.disabled = !canRedo;
|
||||
});
|
||||
|
||||
window.undo = () => {
|
||||
if (undoManager.canUndo()) undoManager.undo();
|
||||
else {
|
||||
undoBtn.disabled = !undoManager.canUndo();
|
||||
redoBtn.disabled = !undoManager.canRedo();
|
||||
}
|
||||
};
|
||||
|
||||
window.redo = () => {
|
||||
if (undoManager.canRedo()) undoManager.redo();
|
||||
else {
|
||||
undoBtn.disabled = !undoManager.canUndo();
|
||||
redoBtn.disabled = !undoManager.canRedo();
|
||||
}
|
||||
};
|
||||
|
||||
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 = '';
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
movePagesTo(startElements, endElement, scrollTo = false) {
|
||||
let commands = [];
|
||||
startElements.forEach((page) => {
|
||||
let command = new MovePageCommand(
|
||||
page,
|
||||
endElement,
|
||||
this.pagesContainer,
|
||||
this.pagesContainerWrapper,
|
||||
scrollTo
|
||||
)
|
||||
command.execute();
|
||||
commands.push(command);
|
||||
})
|
||||
|
||||
let commandSequence = new CommandSequence(commands);
|
||||
this.undoManager.pushUndoClearRedo(commandSequence);
|
||||
return commandSequence;
|
||||
}
|
||||
|
||||
showButton(button, show) {
|
||||
button.classList.toggle('hidden', !show);
|
||||
}
|
||||
|
||||
movePageTo(startElements, endElement, scrollTo = false) {
|
||||
|
||||
if (Array.isArray(startElements)){
|
||||
return this.movePagesTo(startElements, endElement, scrollTo = false);
|
||||
}
|
||||
|
||||
let movePageCommand = new MovePageCommand(
|
||||
startElements,
|
||||
endElement,
|
||||
this.pagesContainer,
|
||||
this.pagesContainerWrapper,
|
||||
scrollTo
|
||||
);
|
||||
|
||||
movePageCommand.execute();
|
||||
this.undoManager.pushUndoClearRedo(movePageCommand);
|
||||
return movePageCommand;
|
||||
}
|
||||
|
||||
async addFiles(element) {
|
||||
let addFilesCommand = new AddFilesCommand(
|
||||
element,
|
||||
window.selectedPages,
|
||||
this.addFilesAction.bind(this),
|
||||
this.pagesContainer
|
||||
);
|
||||
|
||||
await addFilesCommand.execute();
|
||||
|
||||
this.undoManager.pushUndoClearRedo(addFilesCommand);
|
||||
window.tooltipSetup();
|
||||
|
||||
}
|
||||
|
||||
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.onchange = async (e) => {
|
||||
const files = e.target.files;
|
||||
if (files.length > 0) {
|
||||
pages = await this.addFilesFromFiles(files, nextSiblingElement, pages);
|
||||
this.updateFilename(files[0].name);
|
||||
|
||||
if(window.selectPage){
|
||||
this.showButton(document.getElementById('select-pages-container'), true);
|
||||
}
|
||||
}
|
||||
resolve(pages);
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
async handleDroppedFiles(files, nextSiblingElement = null) {
|
||||
if (files.length > 0) {
|
||||
const pages = await this.addFilesFromFiles(files, nextSiblingElement, []);
|
||||
this.updateFilename(files[0]?.name || 'untitled');
|
||||
|
||||
if(window.selectPage) {
|
||||
this.showButton(document.getElementById('select-pages-container'), true);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let decryptedFile = files[i];
|
||||
let isEncrypted = false;
|
||||
let requiresPassword = false;
|
||||
await this.decryptFile
|
||||
.checkFileEncrypted(decryptedFile)
|
||||
.then((result) => {
|
||||
isEncrypted = result.isEncrypted;
|
||||
requiresPassword = result.requiresPassword;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
if (decryptedFile.type === 'application/pdf' && isEncrypted) {
|
||||
decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
|
||||
if (!decryptedFile) {
|
||||
throw new Error('File decryption failed.');
|
||||
}
|
||||
}
|
||||
|
||||
if (decryptedFile.type === 'application/pdf') {
|
||||
const { renderer, pdfDocument } = await this.loadFile(decryptedFile);
|
||||
pageCount = renderer.pageCount || 0;
|
||||
pages = await this.addPdfFile(renderer, pdfDocument, nextSiblingElement, pages);
|
||||
} else if (decryptedFile.type.startsWith('image/')) {
|
||||
pages = await this.addImageFile(decryptedFile, nextSiblingElement, pages);
|
||||
}
|
||||
|
||||
processingTime = Date.now() - startTime;
|
||||
this.captureFileProcessingEvent(true, decryptedFile, 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;
|
||||
}
|
||||
|
||||
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 renderer = await this.toRenderer(url);
|
||||
pages = await this.addPdfFile(renderer, doc, nextSiblingElement, pages);
|
||||
return pages;
|
||||
}
|
||||
|
||||
rotateElement(element, deg) {
|
||||
let rotateCommand = new RotateElementCommand(element, deg);
|
||||
rotateCommand.execute();
|
||||
|
||||
return rotateCommand;
|
||||
}
|
||||
|
||||
async addPdfFile(renderer, pdfDocument, nextSiblingElement, pages) {
|
||||
for (var i = 0; i < renderer.pageCount; i++) {
|
||||
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');
|
||||
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, pages) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('page-container');
|
||||
|
||||
var img = document.createElement('img');
|
||||
img.classList.add('page-image');
|
||||
img.src = URL.createObjectURL(file);
|
||||
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 loadFile(file) {
|
||||
var objectUrl = URL.createObjectURL(file);
|
||||
var pdfDocument = await this.toPdfLib(objectUrl);
|
||||
var renderer = await this.toRenderer(objectUrl);
|
||||
return { renderer, pdfDocument };
|
||||
}
|
||||
|
||||
async toRenderer(objectUrl) {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
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 toPdfLib(objectUrl) {
|
||||
const existingPdfBytes = await fetch(objectUrl).then((res) => res.arrayBuffer());
|
||||
const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
return pdfDoc;
|
||||
}
|
||||
|
||||
rotateAll(deg) {
|
||||
let elementsToRotate = [];
|
||||
for (let i = 0; i < this.pagesContainer.childNodes.length; i++) {
|
||||
const child = this.pagesContainer.children[i];
|
||||
if (!child) continue;
|
||||
|
||||
const pageIndex = i + 1;
|
||||
//if in page select mode is active rotate only selected pages
|
||||
if (window.selectPage && !window.selectedPages.includes(pageIndex)) continue;
|
||||
|
||||
const img = child.querySelector('img');
|
||||
if (!img) continue;
|
||||
|
||||
elementsToRotate.push(img);
|
||||
}
|
||||
|
||||
let rotateAllCommand = new RotateAllCommand(elementsToRotate, deg);
|
||||
rotateAllCommand.execute();
|
||||
|
||||
this.undoManager.pushUndoClearRedo(rotateAllCommand);
|
||||
}
|
||||
|
||||
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) => {
|
||||
element.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
deleteSelected() {
|
||||
window.selectedPages.sort((a, b) => a - b);
|
||||
let removeSelectedCommand = new RemoveSelectedCommand(
|
||||
this.pagesContainer,
|
||||
window.selectedPages,
|
||||
this.updatePageNumbersAndCheckboxes
|
||||
);
|
||||
removeSelectedCommand.execute();
|
||||
this.undoManager.pushUndoClearRedo(removeSelectedCommand);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const checkboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const selectIcon = document.getElementById('select-All-Container');
|
||||
const deselectIcon = document.getElementById('deselect-All-Container');
|
||||
|
||||
this.showButton(selectIcon, false);
|
||||
this.showButton(deselectIcon, true);
|
||||
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = true;
|
||||
|
||||
const pageNumber = Array.from(checkbox.parentNode.parentNode.children).indexOf(checkbox.parentNode) + 1;
|
||||
|
||||
if (!window.selectedPages.includes(pageNumber)) {
|
||||
window.selectedPages.push(pageNumber);
|
||||
}
|
||||
});
|
||||
|
||||
this.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
const checkboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const selectIcon = document.getElementById('select-All-Container');
|
||||
const deselectIcon = document.getElementById('deselect-All-Container');
|
||||
|
||||
this.showButton(selectIcon, true);
|
||||
this.showButton(deselectIcon, false);
|
||||
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
this.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
parseCSVInput(csvInput, maxPageIndex) {
|
||||
const pages = new Set();
|
||||
|
||||
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
|
||||
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
|
||||
pages.add(page);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(pages).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
updatePagesFromCSV() {
|
||||
const csvInput = document.getElementById('csv-input').value;
|
||||
|
||||
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');
|
||||
allCheckboxes.forEach((checkbox) => {
|
||||
const page = parseInt(checkbox.getAttribute('data-page-number'));
|
||||
checkbox.checked = window.selectedPages.includes(page);
|
||||
});
|
||||
}
|
||||
|
||||
formatSelectedPages(pages) {
|
||||
if (pages.length === 0) return '';
|
||||
|
||||
pages.sort((a, b) => a - b); // Sort the page numbers in ascending order
|
||||
const ranges = [];
|
||||
let start = pages[0];
|
||||
let end = start;
|
||||
|
||||
for (let i = 1; i < pages.length; i++) {
|
||||
if (pages[i] === end + 1) {
|
||||
// Consecutive page, update end
|
||||
end = pages[i];
|
||||
} else {
|
||||
// Non-consecutive page, finalize current range
|
||||
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
||||
start = pages[i];
|
||||
end = start;
|
||||
}
|
||||
}
|
||||
// Add the last range
|
||||
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
||||
|
||||
return ranges.join(', ');
|
||||
}
|
||||
|
||||
updateSelectedPagesDisplay() {
|
||||
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 pageNumber = document.createElement('span');
|
||||
const pagelabel = /*[[#{multiTool.page}]]*/ 'Page';
|
||||
pageNumber.className = 'selected-page-number';
|
||||
pageNumber.innerText = `${pagelabel} ${page}`;
|
||||
pageItem.appendChild(pageNumber);
|
||||
|
||||
const removeBtn = document.createElement('span');
|
||||
removeBtn.className = 'remove-btn';
|
||||
removeBtn.innerHTML = '✕';
|
||||
|
||||
// Remove page from selected pages list and update display and checkbox
|
||||
removeBtn.onclick = () => {
|
||||
window.selectedPages = window.selectedPages.filter((p) => p !== page);
|
||||
this.updateSelectedPagesDisplay();
|
||||
|
||||
const checkbox = document.getElementById(`selectPageCheckbox-${page}`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
};
|
||||
|
||||
pageItem.appendChild(removeBtn);
|
||||
selectedPagesList.appendChild(pageItem);
|
||||
});
|
||||
|
||||
// Update the input field with the formatted page list
|
||||
selectedPagesInput.value = this.formatSelectedPages(window.selectedPages);
|
||||
|
||||
const selectIcon = document.getElementById('select-All-Container');
|
||||
const deselectIcon = document.getElementById('deselect-All-Container');
|
||||
|
||||
if (window.selectPage) { // Check if selectPage mode is active
|
||||
console.log("Page Select on. Showing buttons");
|
||||
//Check if no pages are selected
|
||||
if (window.selectedPages.length === 0) {
|
||||
this.showButton(selectIcon, true);
|
||||
this.showButton(deselectIcon, false);
|
||||
} else {
|
||||
this.showButton(deselectIcon, true);
|
||||
}
|
||||
|
||||
//Check if all pages are selected
|
||||
const allCheckboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const allSelected = Array.from(allCheckboxes).every((checkbox) => checkbox.checked);
|
||||
if (allSelected) {
|
||||
this.showButton(selectIcon, false);
|
||||
this.showButton(deselectIcon, true);
|
||||
} else {
|
||||
this.showButton(selectIcon, true);
|
||||
}
|
||||
} else {
|
||||
console.log("Page Select off. Hidding buttons");
|
||||
this.showButton(selectIcon, false);
|
||||
this.showButton(deselectIcon, false);
|
||||
}
|
||||
}
|
||||
|
||||
parsePageRanges(ranges) {
|
||||
const pages = new Set();
|
||||
|
||||
ranges.split(',').forEach((range) => {
|
||||
const [start, end] = range.split('-').map(Number);
|
||||
if (end) {
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.add(i);
|
||||
}
|
||||
} else {
|
||||
pages.add(start);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(pages).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
async addFilesBlankAll() {
|
||||
const allPages = this.pagesContainer.querySelectorAll('.page-container');
|
||||
|
||||
let pageBreakCommand = new PageBreakCommand(
|
||||
allPages,
|
||||
window.selectPage,
|
||||
window.selectedPages,
|
||||
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);
|
||||
}
|
||||
|
||||
async splitPDF(baseDocBytes, splitters) {
|
||||
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.push(pageNum); // We'll also add a faux separator at the end in order to get the pages after the last separator.
|
||||
|
||||
const splitDocuments = [];
|
||||
for (const splitterPosition of splitters) {
|
||||
const subDocument = await PDFLib.PDFDocument.create();
|
||||
|
||||
const splitterIndex = splitters.indexOf(splitterPosition);
|
||||
|
||||
let firstPage = splitterIndex === 0 ? 0 : splitters[splitterIndex - 1];
|
||||
|
||||
const pageIndices = Array.from({ length: splitterPosition - firstPage }, (value, key) => firstPage + key);
|
||||
|
||||
const copiedPages = await subDocument.copyPages(baseDocument, pageIndices);
|
||||
|
||||
copiedPages.forEach((copiedPage) => {
|
||||
subDocument.addPage(copiedPage);
|
||||
});
|
||||
|
||||
const subDocumentBytes = await subDocument.save();
|
||||
|
||||
splitDocuments.push(subDocumentBytes);
|
||||
}
|
||||
|
||||
return splitDocuments;
|
||||
}
|
||||
|
||||
async nameAndArchiveFiles(pdfBytesArray, baseNameString) {
|
||||
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);
|
||||
}
|
||||
|
||||
return zip;
|
||||
}
|
||||
|
||||
async exportPdf(selected) {
|
||||
const pdfDoc = await PDFLib.PDFDocument.create();
|
||||
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
|
||||
if (!img) continue;
|
||||
let page;
|
||||
if (img.doc) {
|
||||
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx]);
|
||||
page = pages[0];
|
||||
pdfDoc.addPage(page);
|
||||
} else {
|
||||
page = pdfDoc.addPage([img.naturalWidth, img.naturalHeight]);
|
||||
const imageBytes = await fetch(img.src).then((res) => res.arrayBuffer());
|
||||
const uint8Array = new Uint8Array(imageBytes);
|
||||
const imageType = detectImageType(uint8Array);
|
||||
|
||||
let image;
|
||||
switch (imageType) {
|
||||
case 'PNG':
|
||||
image = await pdfDoc.embedPng(imageBytes);
|
||||
break;
|
||||
case 'JPEG':
|
||||
image = await pdfDoc.embedJpg(imageBytes);
|
||||
break;
|
||||
case 'TIFF':
|
||||
image = await pdfDoc.embedTiff(imageBytes);
|
||||
break;
|
||||
case 'GIF':
|
||||
console.warn(`Unsupported image type: ${imageType}`);
|
||||
continue; // Skip this image
|
||||
default:
|
||||
console.warn(`Unsupported image type: ${imageType}`);
|
||||
continue; // Skip this image
|
||||
}
|
||||
page.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
const rotation = img.style.rotate;
|
||||
if (rotation) {
|
||||
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ''));
|
||||
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle));
|
||||
}
|
||||
}
|
||||
}
|
||||
pdfDoc.setCreator(stirlingPDFLabel);
|
||||
pdfDoc.setProducer(stirlingPDFLabel);
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
|
||||
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
|
||||
|
||||
if (inputArr.length > 1) {
|
||||
inputArr.pop(); // remove right part after last dot
|
||||
}
|
||||
|
||||
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 pagesArray = Array.from(this.pagesContainer.children);
|
||||
const splitters = [];
|
||||
separators.forEach((page) => {
|
||||
const pageIndex = pagesArray.indexOf(page);
|
||||
if (pageIndex !== 0) {
|
||||
splitters.push(pageIndex);
|
||||
}
|
||||
});
|
||||
|
||||
const splitDocuments = await this.splitPDF(pdfBytes, splitters);
|
||||
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');
|
||||
self.downloadLink.href = url;
|
||||
self.downloadLink.setAttribute('download', baseName + '.zip');
|
||||
self.downloadLink.setAttribute('target', '_blank');
|
||||
self.downloadLink.click();
|
||||
});
|
||||
} else {
|
||||
// Continue normally if there are no separators
|
||||
|
||||
const url = URL.createObjectURL(pdfBlob);
|
||||
const downloadOption = localStorage.getItem('downloadOption');
|
||||
|
||||
if (!filenameInput.value.includes('.pdf')) {
|
||||
filenameInput.value = filenameInput.value + '.pdf';
|
||||
this.fileName = filenameInput.value;
|
||||
}
|
||||
|
||||
if (downloadOption === 'sameWindow') {
|
||||
// Open the file in the same window
|
||||
window.location.href = url;
|
||||
} else if (downloadOption === 'newWindow') {
|
||||
// Open the file in a new window
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
// Download the file
|
||||
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.onclick = this.setDownloadAttribute;
|
||||
this.downloadLink.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetPages() {
|
||||
const pageContainers = this.pagesContainer.querySelectorAll('.page-container');
|
||||
|
||||
pageContainers.forEach((container, index) => {
|
||||
container.id = 'page-container-' + (index + 1);
|
||||
});
|
||||
|
||||
const checkboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const selectIcon = document.getElementById('select-All-Container');
|
||||
const deselectIcon = document.getElementById('deselect-All-Container');
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
|
||||
setDownloadAttribute() {
|
||||
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');
|
||||
|
||||
downloadBtn.disabled = pagesContainer.childElementCount === 0;
|
||||
|
||||
if (!this.fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
if (!filenameInput.value) {
|
||||
filenameInput.value = this.fileName;
|
||||
}
|
||||
}
|
||||
|
||||
preventIllegalChars(e) {
|
||||
// const filenameInput = document.getElementById('filename-input');
|
||||
//
|
||||
// filenameInput.value = filenameInput.value.replace('.pdf', '');
|
||||
//
|
||||
// // prevent .
|
||||
// if (filenameInput.value.includes('.')) {
|
||||
// filenameInput.value.replace('.','');
|
||||
// }
|
||||
}
|
||||
|
||||
toggleSelectPageVisibility() {
|
||||
window.selectPage = !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);
|
||||
|
||||
if(!window.selectPage)
|
||||
{
|
||||
this.showButton(document.getElementById('deselect-All-Container'), false);
|
||||
this.showButton(document.getElementById('select-All-Container'), false);
|
||||
|
||||
// Uncheck all checkboxes and clear selected pages
|
||||
const allCheckboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
allCheckboxes.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
window.selectedPages = [];
|
||||
this.updateSelectedPagesDisplay();
|
||||
}
|
||||
else{
|
||||
const allCheckboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const allSelected = Array.from(allCheckboxes).every((checkbox) => checkbox.checked);
|
||||
if (!allSelected) {
|
||||
this.showButton(document.getElementById('select-All-Container'), true);
|
||||
}
|
||||
|
||||
if (window.selectedPages.length > 0) {
|
||||
this.showButton(document.getElementById('deselect-All-Container'), true);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
pageDivs.forEach((div, index) => {
|
||||
const pageNumber = index + 1;
|
||||
const checkbox = div.querySelector('.pdf-actions_checkbox');
|
||||
checkbox.id = `selectPageCheckbox-${pageNumber}`;
|
||||
checkbox.setAttribute('data-page-number', pageNumber);
|
||||
checkbox.checked = window.selectedPages.includes(pageNumber);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function detectImageType(uint8Array) {
|
||||
// Check for PNG signature
|
||||
if (uint8Array[0] === 137 && uint8Array[1] === 80 && uint8Array[2] === 78 && uint8Array[3] === 71) {
|
||||
return 'PNG';
|
||||
}
|
||||
|
||||
// Check for JPEG signature
|
||||
if (uint8Array[0] === 255 && uint8Array[1] === 216 && uint8Array[2] === 255) {
|
||||
return 'JPEG';
|
||||
}
|
||||
|
||||
// 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)
|
||||
) {
|
||||
return 'TIFF';
|
||||
}
|
||||
|
||||
// Check for GIF signature
|
||||
if (uint8Array[0] === 71 && uint8Array[1] === 73 && uint8Array[2] === 70) {
|
||||
return 'GIF';
|
||||
}
|
||||
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
export default PdfContainer;
|
||||
@@ -0,0 +1,65 @@
|
||||
export class UndoManager {
|
||||
_undoStack;
|
||||
_redoStack;
|
||||
|
||||
constructor() {
|
||||
this._undoStack = [];
|
||||
this._redoStack = [];
|
||||
}
|
||||
|
||||
pushUndo(command) {
|
||||
this._undoStack.push(command);
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
pushRedo(command) {
|
||||
this._redoStack.push(command);
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
pushUndoClearRedo(command) {
|
||||
this._undoStack.push(command);
|
||||
this._redoStack = [];
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.canUndo()) return;
|
||||
|
||||
let cmd = this._undoStack.pop();
|
||||
cmd.undo();
|
||||
|
||||
this._redoStack.push(cmd);
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
canUndo() {
|
||||
return this._undoStack && this._undoStack.length > 0;
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!this.canRedo()) return;
|
||||
|
||||
let cmd = this._redoStack.pop();
|
||||
cmd.redo();
|
||||
|
||||
this._undoStack.push(cmd);
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
canRedo() {
|
||||
return this._redoStack && this._redoStack.length > 0;
|
||||
}
|
||||
|
||||
_dispatchStateChange() {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("undo-manager-update", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
canUndo: this.canUndo(),
|
||||
canRedo: this.canRedo(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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 downloadBtn = document.getElementById('export-button');
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = '';
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
element._nextSibling = nextSibling;
|
||||
});
|
||||
this.addedElements = [];
|
||||
}
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class Command {
|
||||
execute() {}
|
||||
undo() {}
|
||||
redo() {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Command} from './command.js';
|
||||
|
||||
export class CommandSequence extends Command {
|
||||
constructor(commands) {
|
||||
super();
|
||||
this.commands = commands;
|
||||
|
||||
}
|
||||
execute() {
|
||||
this.commands.forEach((command) => command.execute())
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.commands.slice().reverse().forEach((command) => command.undo())
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class DeletePageCommand extends Command {
|
||||
constructor(element, pagesContainer) {
|
||||
super();
|
||||
|
||||
this.element = element;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
this.filenameInputValue = document.getElementById("filename-input").value;
|
||||
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
this.filenameParagraphText = filenameParagraph
|
||||
? filenameParagraph.innerText
|
||||
: "";
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.nextSibling = this.element.nextSibling;
|
||||
|
||||
this.pagesContainer.removeChild(this.element);
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
let node = this.nextSibling;
|
||||
if (node) this.pagesContainer.insertBefore(this.element, node);
|
||||
else this.pagesContainer.appendChild(this.element);
|
||||
|
||||
const pageNumberElement = this.element.querySelector(".page-number");
|
||||
if (pageNumberElement) {
|
||||
this.element.removeChild(pageNumberElement);
|
||||
}
|
||||
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = false;
|
||||
filenameInput.value = this.filenameInputValue;
|
||||
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
redo() {
|
||||
const pageNumberElement = this.element.querySelector(".page-number");
|
||||
if (pageNumberElement) {
|
||||
this.element.removeChild(pageNumberElement);
|
||||
}
|
||||
|
||||
this.pagesContainer.removeChild(this.element);
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {Command} from './command.js';
|
||||
|
||||
export class MovePageCommand extends Command {
|
||||
constructor(startElement, endElement, pagesContainer, pagesContainerWrapper, scrollTo = false) {
|
||||
super();
|
||||
|
||||
this.pagesContainer = pagesContainer;
|
||||
const childArray = Array.from(this.pagesContainer.childNodes);
|
||||
|
||||
this.startIndex = childArray.indexOf(startElement);
|
||||
this.endIndex = childArray.indexOf(endElement);
|
||||
|
||||
this.startElement = startElement;
|
||||
this.endElement = endElement;
|
||||
|
||||
this.scrollTo = scrollTo;
|
||||
this.pagesContainerWrapper = pagesContainerWrapper;
|
||||
}
|
||||
execute() {
|
||||
// Check & remove page number elements here too if they exist because Firefox doesn't fire the relevant event on page move.
|
||||
const pageNumberElement = this.startElement.querySelector('.page-number');
|
||||
if (pageNumberElement) {
|
||||
this.startElement.removeChild(pageNumberElement);
|
||||
}
|
||||
|
||||
this.pagesContainer.removeChild(this.startElement);
|
||||
if (!this.endElement) {
|
||||
this.pagesContainer.append(this.startElement);
|
||||
} else {
|
||||
this.pagesContainer.insertBefore(this.startElement, this.endElement);
|
||||
}
|
||||
|
||||
if (this.scrollTo) {
|
||||
const {width} = this.startElement.getBoundingClientRect();
|
||||
const vector = this.endIndex !== -1 && this.startIndex > this.endIndex ? 0 - width : width;
|
||||
|
||||
this.pagesContainerWrapper.scroll({
|
||||
left: this.pagesContainerWrapper.scrollLeft + vector,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.startElement) {
|
||||
this.pagesContainer.removeChild(this.startElement);
|
||||
let previousNeighbour = Array.from(this.pagesContainer.childNodes)[this.startIndex];
|
||||
previousNeighbour?.insertAdjacentElement('beforebegin', this.startElement)
|
||||
?? this.pagesContainer.append(this.startElement);
|
||||
}
|
||||
|
||||
if (this.scrollTo) {
|
||||
const {width} = this.startElement.getBoundingClientRect();
|
||||
const vector = this.endIndex === -1 || this.startIndex <= this.endIndex ? 0 - width : width;
|
||||
|
||||
this.pagesContainerWrapper.scroll({
|
||||
left: this.pagesContainerWrapper.scrollLeft - vector,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class RemoveSelectedCommand extends Command {
|
||||
constructor(pagesContainer, selectedPages, updatePageNumbersAndCheckboxes) {
|
||||
super();
|
||||
this.pagesContainer = pagesContainer;
|
||||
this.selectedPages = selectedPages;
|
||||
|
||||
this.deletedChildren = [];
|
||||
|
||||
if (updatePageNumbersAndCheckboxes) {
|
||||
this.updatePageNumbersAndCheckboxes = updatePageNumbersAndCheckboxes;
|
||||
} else {
|
||||
const pageDivs = document.querySelectorAll(".pdf-actions_container");
|
||||
|
||||
pageDivs.forEach((div, index) => {
|
||||
const pageNumber = index + 1;
|
||||
const checkbox = div.querySelector(".pdf-actions_checkbox");
|
||||
checkbox.id = `selectPageCheckbox-${pageNumber}`;
|
||||
checkbox.setAttribute("data-page-number", pageNumber);
|
||||
checkbox.checked = window.selectedPages.includes(pageNumber);
|
||||
});
|
||||
}
|
||||
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
|
||||
this.originalFilenameInputValue = filenameInput ? filenameInput.value : "";
|
||||
if (filenameParagraph)
|
||||
this.originalFilenameParagraphText = filenameParagraph.innerText;
|
||||
}
|
||||
|
||||
execute() {
|
||||
let deletions = 0;
|
||||
|
||||
this.selectedPages.forEach((pageIndex) => {
|
||||
const adjustedIndex = pageIndex - 1 - deletions;
|
||||
const child = this.pagesContainer.children[adjustedIndex];
|
||||
if (child) {
|
||||
this.pagesContainer.removeChild(child);
|
||||
deletions++;
|
||||
|
||||
this.deletedChildren.push({
|
||||
idx: adjustedIndex,
|
||||
childNode: child,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
if (filenameInput) filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
if (filenameParagraph) filenameParagraph.innerText = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
window.selectedPages = [];
|
||||
this.updatePageNumbersAndCheckboxes();
|
||||
document.dispatchEvent(new Event("selectedPagesUpdated"));
|
||||
}
|
||||
|
||||
undo() {
|
||||
while (this.deletedChildren.length > 0) {
|
||||
let deletedChild = this.deletedChildren.pop();
|
||||
if (this.pagesContainer.children.length <= deletedChild.idx)
|
||||
this.pagesContainer.appendChild(deletedChild.childNode);
|
||||
else {
|
||||
this.pagesContainer.insertBefore(
|
||||
deletedChild.childNode,
|
||||
this.pagesContainer.children[deletedChild.idx]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pagesContainer.childElementCount > 0) {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
if (filenameInput) filenameInput.disabled = false;
|
||||
filenameInput.value = this.originalFilenameInputValue;
|
||||
if (filenameParagraph)
|
||||
filenameParagraph.innerText = this.originalFilenameParagraphText;
|
||||
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
window.selectedPages = this.selectedPages;
|
||||
this.updatePageNumbersAndCheckboxes();
|
||||
document.dispatchEvent(new Event("selectedPagesUpdated"));
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class RotateElementCommand extends Command {
|
||||
constructor(element, degree) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
execute() {
|
||||
let lastTransform = this.element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const newAngle = lastAngle + parseInt(this.degree);
|
||||
|
||||
this.element.style.rotate = newAngle + "deg";
|
||||
}
|
||||
|
||||
undo() {
|
||||
let lastTransform = this.element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
|
||||
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const undoAngle = currentAngle + -parseInt(this.degree);
|
||||
|
||||
this.element.style.rotate = undoAngle + "deg";
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
export class RotateAllCommand extends Command {
|
||||
constructor(elements, degree) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
execute() {
|
||||
for (let element of this.elements) {
|
||||
let lastTransform = element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const newAngle = lastAngle + this.degree;
|
||||
|
||||
element.style.rotate = newAngle + "deg";
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
for (let element of this.elements) {
|
||||
let lastTransform = element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const undoAngle = currentAngle + -this.degree;
|
||||
|
||||
element.style.rotate = undoAngle + "deg";
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class SelectPageCommand extends Command {
|
||||
constructor(pageNumber, checkbox) {
|
||||
super();
|
||||
this.pageNumber = pageNumber;
|
||||
this.selectCheckbox = checkbox;
|
||||
}
|
||||
|
||||
execute() {
|
||||
if (this.selectCheckbox.checked) {
|
||||
//adds to array of selected pages
|
||||
window.selectedPages.push(this.pageNumber);
|
||||
} else {
|
||||
//remove page from selected pages array
|
||||
const index = window.selectedPages.indexOf(this.pageNumber);
|
||||
if (index !== -1) {
|
||||
window.selectedPages.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.selectedPages.length > 0 && !window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
if (window.selectedPages.length == 0 && window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
|
||||
window.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.selectCheckbox.checked = !this.selectCheckbox.checked;
|
||||
if (this.selectCheckbox.checked) {
|
||||
//adds to array of selected pages
|
||||
window.selectedPages.push(this.pageNumber);
|
||||
} else {
|
||||
//remove page from selected pages array
|
||||
const index = window.selectedPages.indexOf(this.pageNumber);
|
||||
if (index !== -1) {
|
||||
window.selectedPages.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.selectedPages.length > 0 && !window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
if (window.selectedPages.length == 0 && window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
|
||||
window.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.selectCheckbox.checked = !this.selectCheckbox.checked;
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class SplitFileCommand extends Command {
|
||||
constructor(element, splitClass) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.splitClass = splitClass;
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.element.classList.toggle(this.splitClass);
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.element.classList.toggle(this.splitClass);
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
export class SplitAllCommand extends Command {
|
||||
constructor(elements, isSelectedInWindow, selectedPages, splitClass) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.isSelectedInWindow = isSelectedInWindow;
|
||||
this.selectedPages = selectedPages;
|
||||
this.splitClass = splitClass;
|
||||
}
|
||||
|
||||
execute() {
|
||||
if (!this.isSelectedInWindow) {
|
||||
const hasSplit = this._hasSplit(this.elements, this.splitClass);
|
||||
if (hasSplit) {
|
||||
this.elements.forEach((page) => {
|
||||
page.classList.remove(this.splitClass);
|
||||
});
|
||||
} else {
|
||||
this.elements.forEach((page) => {
|
||||
page.classList.add(this.splitClass);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements.forEach((page, index) => {
|
||||
const pageIndex = index;
|
||||
if (this.isSelectedInWindow && !this.selectedPages.includes(pageIndex))
|
||||
return;
|
||||
|
||||
if (page.classList.contains(this.splitClass)) {
|
||||
page.classList.remove(this.splitClass);
|
||||
} else {
|
||||
page.classList.add(this.splitClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_hasSplit() {
|
||||
if (!this.elements || this.elements.length == 0) return false;
|
||||
|
||||
for (const node of this.elements) {
|
||||
if (node.classList.contains(this.splitClass)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.isSelectedInWindow) {
|
||||
const hasSplit = this._hasSplit(this.elements, this.splitClass);
|
||||
if (hasSplit) {
|
||||
this.elements.forEach((page) => {
|
||||
page.classList.remove(this.splitClass);
|
||||
});
|
||||
} else {
|
||||
this.elements.forEach((page) => {
|
||||
page.classList.add(this.splitClass);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements.forEach((page, index) => {
|
||||
const pageIndex = index;
|
||||
if (this.isSelectedInWindow && !this.selectedPages.includes(pageIndex))
|
||||
return;
|
||||
|
||||
if (page.classList.contains(this.splitClass)) {
|
||||
page.classList.remove(this.splitClass);
|
||||
} else {
|
||||
page.classList.add(this.splitClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user