mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
* Implement Command class for Command Pattern Created a base `Command` class to implement the **Command Pattern**. This class provides a skeletal implementation for `execute`, `undo`, and `redo` methods. **Note:** This class is intended to be subclassed and not instantiated directly. * Add undo/redo stacks and operations * Use rotate element command to perform execute/undo/redo operations * Handle commands executed through events - Add "command-execution" event listener to execute commands that are not invoked from the same class while adding the command to the undo stack and clearing the redo stack. * Add and use rotate all command to rotate/redo/undo all elements * Use command pattern to delete pages * Use command pattern for page selection * Use command pattern to move pages up and down * Use command pattern to remove selected pages * Use command pattern to perform the splitting operation * Add undo/redo functionality with filename input exclusion - Implement undo (Ctrl+Z) and redo (Ctrl+Y) functionality. - Prevent undo/redo actions when the filename input field is focused. - Ensures proper handling of undo/redo actions without interfering with text editing. * Introduce UndoManager for managing undo/redo operations - Encapsulate undo/redo stacks and operations within UndoManager. - Simplify handling of undo/redo functionality through a dedicated manager. * Call execute on splitAllCommand - Fix a bug that caused split all functionality to not work as execute() wasn't called on splitAllCommand * Add undo/redo buttons to multi tool - Add undo/redo buttons to multi tool - Dispatch an event upon state change (such as changes in the undo/redo stacks) to update the UI accordingly. * Add undo/redo to translations * Replace hard-coded "Undo"/"Redo" with translation keys in multi tool --------- Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
de9c21b3de
commit
61e750646c
@@ -0,0 +1,5 @@
|
||||
export class Command {
|
||||
execute() {}
|
||||
undo() {}
|
||||
redo() {}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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 filenameParagraph = document.getElementById("filename");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
filenameParagraph.innerText = "";
|
||||
|
||||
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 filenameParagraph = document.getElementById("filename");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = false;
|
||||
filenameInput.value = this.filenameInputValue;
|
||||
if (this.filenameParagraph)
|
||||
filenameParagraph.innerText = this.filenameParagraphText;
|
||||
|
||||
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 filenameParagraph = document.getElementById("filename");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
filenameParagraph.innerText = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class AbstractMovePageCommand 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() {
|
||||
// Requires overriding in child classes
|
||||
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
export class MovePageUpCommand extends AbstractMovePageCommand {
|
||||
constructor(
|
||||
startElement,
|
||||
endElement,
|
||||
pagesContainer,
|
||||
pagesContainerWrapper,
|
||||
scrollTo = false
|
||||
) {
|
||||
super(startElement, endElement, pagesContainer, pagesContainerWrapper, scrollTo);
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.endElement) {
|
||||
this.pagesContainer.removeChild(this.endElement);
|
||||
this.startElement.insertAdjacentElement("beforebegin", 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
export class MovePageDownCommand extends AbstractMovePageCommand {
|
||||
constructor(
|
||||
startElement,
|
||||
endElement,
|
||||
pagesContainer,
|
||||
pagesContainerWrapper,
|
||||
scrollTo = false
|
||||
) {
|
||||
super(startElement, endElement, pagesContainer, pagesContainerWrapper, scrollTo);
|
||||
}
|
||||
|
||||
undo() {
|
||||
let previousElement = this.startElement.previousSibling;
|
||||
|
||||
if (this.startElement) {
|
||||
this.pagesContainer.removeChild(this.startElement);
|
||||
previousElement.insertAdjacentElement("beforebegin", 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,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