mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
# Description of Changes
- Introduced **Duplicate Page** action in the MultiTool UI:
- New `DuplicatePageCommand` with full undo/redo support
- Button added next to Delete, with tooltip text wired via i18n
(`multiTool.duplicate`)
- `PdfContainer.duplicatePage()` clones page image, rotation, and
adapter state; inserts after source; refreshes numbering
- Hooked duplication into the actions pipeline:
- `PdfActionsManager` now receives `duplicatePage` via `setActions(...)`
- Added `duplicatePageButtonCallback` and control markup creation
- Refactored command architecture for robust undo/redo:
- Added `CommandWithAnchors` for capturing positions and deterministic
reinsertion (used by add/duplicate/page-break)
- Modernized `AddFilesCommand`, `PageBreakCommand` to use anchors, avoid
DOM state on nodes, and handle arrays consistently
- Improved `RemoveSelectedCommand`, `DeletePageCommand`,
`MovePageCommand`, `Rotate*` and `Split*` commands:
- Safer null checks for filename/export controls
- Clearer semantics and documentation
- Consistent redo mirroring execute
- Minor UI/UX fix:
- Corrected tooltip wiring for the **Add File** button
(`insertFileButton.setAttribute('title', ...)`)
- Internationalization:
- Added `multiTool.duplicate=Duplicate` to `messages_en_GB.properties`
- Exposed `translations.duplicate` in `multi-tool.html`
**Why:** Adds a commonly requested workflow improvement (duplicate a
single page quickly) and makes the command stack more resilient
(especially redo after complex DOM mutations), improving reliability and
maintainability.
before:
<img width="186" height="34" alt="image"
src="https://github.com/user-attachments/assets/7edc8e9e-fc3d-411e-aaa1-4da7099c3173"
/>
after:
<img width="225" height="34" alt="image"
src="https://github.com/user-attachments/assets/bdf228c9-b3db-4690-bfb9-f198225459f4"
/>
Closes #4322
---
## Checklist
### General
- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings
### Documentation
- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)
### UI Changes (if applicable)
- [x] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)
### Testing (if applicable)
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
parent
6b6699ed70
commit
e4cfb8befe
@ -1402,6 +1402,7 @@ multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo (CTRL + Z)
|
||||
multiTool.redo=Redo (CTRL + Y)
|
||||
multiTool.duplicate=Duplicate
|
||||
|
||||
multiTool.svgNotSupported=SVG files are not supported in Multi Tool and were ignored.
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { DeletePageCommand } from "./commands/delete-page.js";
|
||||
import { DuplicatePageCommand } from "./commands/duplicate-page.js";
|
||||
import { SelectPageCommand } from "./commands/select.js";
|
||||
import { SplitFileCommand } from "./commands/split.js";
|
||||
import { UndoManager } from "./UndoManager.js";
|
||||
@ -78,6 +79,18 @@ class PdfActionsManager {
|
||||
this._pushUndoClearRedo(deletePageCommand);
|
||||
}
|
||||
|
||||
duplicatePageButtonCallback(e) {
|
||||
let imgContainer = this.getPageContainer(e.target);
|
||||
let duplicatePageCommand = new DuplicatePageCommand(
|
||||
imgContainer,
|
||||
this.duplicatePage,
|
||||
this.pagesContainer
|
||||
);
|
||||
duplicatePageCommand.execute();
|
||||
|
||||
this._pushUndoClearRedo(duplicatePageCommand);
|
||||
}
|
||||
|
||||
insertFileButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
this.addFiles(imgContainer);
|
||||
@ -101,10 +114,11 @@ class PdfActionsManager {
|
||||
this.undoManager.pushUndoClearRedo(command);
|
||||
}
|
||||
|
||||
setActions({ movePageTo, addFiles, rotateElement }) {
|
||||
setActions({ movePageTo, addFiles, rotateElement, duplicatePage }) {
|
||||
this.movePageTo = movePageTo;
|
||||
this.addFiles = addFiles;
|
||||
this.rotateElement = rotateElement;
|
||||
this.duplicatePage = duplicatePage;
|
||||
|
||||
this.moveUpButtonCallback = this.moveUpButtonCallback.bind(this);
|
||||
this.moveDownButtonCallback = this.moveDownButtonCallback.bind(this);
|
||||
@ -114,6 +128,7 @@ class PdfActionsManager {
|
||||
this.insertFileButtonCallback = this.insertFileButtonCallback.bind(this);
|
||||
this.insertFileBlankButtonCallback = this.insertFileBlankButtonCallback.bind(this);
|
||||
this.splitFileButtonCallback = this.splitFileButtonCallback.bind(this);
|
||||
this.duplicatePageButtonCallback = this.duplicatePageButtonCallback.bind(this);
|
||||
}
|
||||
|
||||
|
||||
@ -154,6 +169,13 @@ class PdfActionsManager {
|
||||
rotateCW.onclick = this.rotateCWButtonCallback;
|
||||
buttonContainer.appendChild(rotateCW);
|
||||
|
||||
const duplicatePage = document.createElement("button");
|
||||
duplicatePage.classList.add("btn", "btn-secondary");
|
||||
duplicatePage.setAttribute('title', window.translations.duplicate);
|
||||
duplicatePage.innerHTML = `<span class="material-symbols-rounded">control_point_duplicate</span>`;
|
||||
duplicatePage.onclick = this.duplicatePageButtonCallback;
|
||||
buttonContainer.appendChild(duplicatePage);
|
||||
|
||||
const deletePage = document.createElement("button");
|
||||
deletePage.classList.add("btn", "btn-danger");
|
||||
deletePage.setAttribute('title', window.translations.delete);
|
||||
@ -195,7 +217,7 @@ class PdfActionsManager {
|
||||
|
||||
const insertFileButton = document.createElement("button");
|
||||
insertFileButton.classList.add("btn", "btn-primary");
|
||||
moveUp.setAttribute('title', window.translations.addFile);
|
||||
insertFileButton.setAttribute('title', window.translations.addFile);
|
||||
insertFileButton.innerHTML = `<span class="material-symbols-rounded">add</span>`;
|
||||
insertFileButton.onclick = this.insertFileButtonCallback;
|
||||
insertFileButtonContainer.appendChild(insertFileButton);
|
||||
|
||||
@ -73,6 +73,7 @@ class PdfContainer {
|
||||
this.setDownloadAttribute = this.setDownloadAttribute.bind(this);
|
||||
this.preventIllegalChars = this.preventIllegalChars.bind(this);
|
||||
this.addImageFile = this.addImageFile.bind(this);
|
||||
this.duplicatePage = this.duplicatePage.bind(this);
|
||||
this.nameAndArchiveFiles = this.nameAndArchiveFiles.bind(this);
|
||||
this.splitPDF = this.splitPDF.bind(this);
|
||||
this.splitAll = this.splitAll.bind(this);
|
||||
@ -99,6 +100,7 @@ class PdfContainer {
|
||||
rotateElement: this.rotateElement,
|
||||
updateFilename: this.updateFilename,
|
||||
deleteSelected: this.deleteSelected,
|
||||
duplicatePage: this.duplicatePage,
|
||||
});
|
||||
});
|
||||
|
||||
@ -197,19 +199,31 @@ class PdfContainer {
|
||||
return movePageCommand;
|
||||
}
|
||||
|
||||
async addFiles(element) {
|
||||
let addFilesCommand = new AddFilesCommand(
|
||||
/**
|
||||
* Adds files or a single blank page (when blank=true) near an anchor element.
|
||||
* @param {HTMLElement|null} element - Anchor element (insert before its nextSibling).
|
||||
* @param {boolean} [blank=false] - When true, insert a single blank page.
|
||||
*/
|
||||
async addFiles(element, blank = false) {
|
||||
// Choose the action: real file picker or blank page generator.
|
||||
const action = blank
|
||||
? async (nextSiblingElement) => {
|
||||
// Create exactly one blank page and return the created elements array.
|
||||
const pages = await this.addFilesBlank(nextSiblingElement, []);
|
||||
return pages; // array of inserted elements
|
||||
}
|
||||
: this.addFilesAction.bind(this);
|
||||
|
||||
const addFilesCommand = new AddFilesCommand(
|
||||
element,
|
||||
window.selectedPages,
|
||||
this.addFilesAction.bind(this),
|
||||
action,
|
||||
this.pagesContainer
|
||||
);
|
||||
|
||||
await addFilesCommand.execute();
|
||||
|
||||
this.undoManager.pushUndoClearRedo(addFilesCommand);
|
||||
window.tooltipSetup();
|
||||
|
||||
}
|
||||
|
||||
async addFilesAction(nextSiblingElement) {
|
||||
@ -433,6 +447,30 @@ class PdfContainer {
|
||||
return pages;
|
||||
}
|
||||
|
||||
duplicatePage(element) {
|
||||
const clone = document.createElement('div');
|
||||
clone.classList.add('page-container');
|
||||
|
||||
const originalImg = element.querySelector('img');
|
||||
const img = document.createElement('img');
|
||||
img.classList.add('page-image');
|
||||
img.src = originalImg.src;
|
||||
img.pageIdx = originalImg.pageIdx;
|
||||
img.rend = originalImg.rend;
|
||||
img.doc = originalImg.doc;
|
||||
img.style.rotate = originalImg.style.rotate;
|
||||
clone.appendChild(img);
|
||||
|
||||
this.pdfAdapters.forEach((adapter) => {
|
||||
adapter?.adapt?.(clone);
|
||||
});
|
||||
|
||||
const nextSibling = element.nextSibling;
|
||||
this.pagesContainer.insertBefore(clone, nextSibling);
|
||||
this.updatePageNumbersAndCheckboxes();
|
||||
return clone;
|
||||
}
|
||||
|
||||
async loadFile(file) {
|
||||
var objectUrl = URL.createObjectURL(file);
|
||||
var pdfDocument = await this.toPdfLib(objectUrl);
|
||||
|
||||
@ -1,51 +1,84 @@
|
||||
import {Command} from './command.js';
|
||||
import { CommandWithAnchors } from './command.js';
|
||||
|
||||
export class AddFilesCommand extends Command {
|
||||
export class AddFilesCommand extends CommandWithAnchors {
|
||||
/**
|
||||
* @param {HTMLElement|null} element - Anchor element (optional, forwarded to addFilesAction)
|
||||
* @param {number[]} selectedPages
|
||||
* @param {Function} addFilesAction - async (nextSiblingElement|false) => HTMLElement[]|HTMLElement|null
|
||||
* @param {HTMLElement} pagesContainer
|
||||
*/
|
||||
constructor(element, selectedPages, addFilesAction, pagesContainer) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.selectedPages = selectedPages;
|
||||
this.addFilesAction = addFilesAction;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
/** @type {HTMLElement[]} */
|
||||
this.addedElements = [];
|
||||
|
||||
/**
|
||||
* Anchors captured on undo to support redo reinsertion.
|
||||
* @type {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }[]}
|
||||
*/
|
||||
this._anchors = [];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (undoBtn) undoBtn.disabled = true;
|
||||
|
||||
const result = await this.addFilesAction(this.element || false);
|
||||
if (Array.isArray(result)) {
|
||||
this.addedElements = result;
|
||||
} else if (result) {
|
||||
this.addedElements = [result];
|
||||
} else {
|
||||
const newElement = await this.addFilesAction(false);
|
||||
if (newElement) {
|
||||
this.addedElements = newElement;
|
||||
}
|
||||
this.addedElements = [];
|
||||
}
|
||||
undoBtn.disabled = false;
|
||||
|
||||
// Capture anchors right after insertion so redo does not depend on undo.
|
||||
this._anchors = this.addedElements.map((el) => this.captureAnchor(el, this.pagesContainer));
|
||||
|
||||
if (undoBtn) undoBtn.disabled = false;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.addedElements.forEach((element) => {
|
||||
const nextSibling = element.nextSibling;
|
||||
this.pagesContainer.removeChild(element);
|
||||
this._anchors = [];
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
for (const el of this.addedElements) {
|
||||
this._anchors.push(this.captureAnchor(el, this.pagesContainer));
|
||||
this.pagesContainer.removeChild(el);
|
||||
}
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = '';
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
element._nextSibling = nextSibling;
|
||||
});
|
||||
this.addedElements = [];
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
if (!this.addedElements.length) return;
|
||||
// If the elements are already in the DOM (no prior undo), do nothing.
|
||||
const alreadyInDom =
|
||||
this.addedElements[0].parentNode === this.pagesContainer;
|
||||
if (alreadyInDom) return;
|
||||
|
||||
// Use pre-captured anchors (from execute) or fall back to capturing now.
|
||||
const anchors = (this._anchors && this._anchors.length)
|
||||
? this._anchors
|
||||
: this.addedElements.map((el) =>
|
||||
this.captureAnchor(el, this.pagesContainer));
|
||||
|
||||
for (const anchor of anchors) {
|
||||
this.insertWithAnchor(this.pagesContainer, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,3 +3,63 @@ export class Command {
|
||||
undo() {}
|
||||
redo() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class that provides anchor capture and reinsertion helpers
|
||||
* to avoid storing custom state on DOM nodes.
|
||||
*/
|
||||
export class CommandWithAnchors extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }[]} */
|
||||
this._anchors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child index of an element in a container.
|
||||
* @param {HTMLElement} container
|
||||
* @param {HTMLElement} el
|
||||
* @returns {number}
|
||||
*/
|
||||
_indexOf(container, el) {
|
||||
return Array.prototype.indexOf.call(container.children, el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures an anchor for later reinsertion.
|
||||
* @param {HTMLElement} el
|
||||
* @param {HTMLElement} container
|
||||
* @returns {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }}
|
||||
*/
|
||||
captureAnchor(el, container) {
|
||||
return {
|
||||
el,
|
||||
nextSibling: el.nextSibling,
|
||||
index: this._indexOf(container, el),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinserts an element using a previously captured anchor.
|
||||
* Prefers stored nextSibling when still valid; otherwise falls back to index.
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }} anchor
|
||||
*/
|
||||
insertWithAnchor(container, anchor) {
|
||||
const { el, nextSibling, index } = anchor;
|
||||
const nextValid = nextSibling && nextSibling.parentNode === container;
|
||||
|
||||
let ref = null;
|
||||
if (nextValid) {
|
||||
ref = nextSibling;
|
||||
} else if (
|
||||
Number.isInteger(index) &&
|
||||
index >= 0 &&
|
||||
index < container.children.length
|
||||
) {
|
||||
ref = container.children[index] || null;
|
||||
}
|
||||
|
||||
container.insertBefore(el, ref || null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,27 @@
|
||||
import {Command} from './command.js';
|
||||
import { Command } from './command.js';
|
||||
|
||||
/**
|
||||
* Composes multiple commands into a single atomic operation.
|
||||
* Executes in order; undo in reverse order.
|
||||
*/
|
||||
export class CommandSequence extends Command {
|
||||
/** @param {Command[]} commands - Commands to be executed/undone/redone as a group. */
|
||||
constructor(commands) {
|
||||
super();
|
||||
this.commands = commands;
|
||||
|
||||
}
|
||||
|
||||
/** Execute: run each command in order. */
|
||||
execute() {
|
||||
this.commands.forEach((command) => command.execute())
|
||||
this.commands.forEach((command) => command.execute());
|
||||
}
|
||||
|
||||
/** Undo: undo in reverse order. */
|
||||
undo() {
|
||||
this.commands.slice().reverse().forEach((command) => command.undo())
|
||||
this.commands.slice().reverse().forEach((command) => command.undo());
|
||||
}
|
||||
|
||||
/** Redo: simply execute again. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
@ -1,20 +1,32 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Removes a page from the container and restores it on undo.
|
||||
*/
|
||||
export class DeletePageCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} element - Page container to delete.
|
||||
* @param {HTMLElement} pagesContainer - Parent container holding all pages.
|
||||
*/
|
||||
constructor(element, pagesContainer) {
|
||||
super();
|
||||
|
||||
this.element = element;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
this.filenameInputValue = document.getElementById("filename-input").value;
|
||||
/** @type {ChildNode|null} */
|
||||
this.nextSibling = null;
|
||||
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
/** @type {string} */
|
||||
this.filenameInputValue = filenameInput ? filenameInput.value : "";
|
||||
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
this.filenameParagraphText = filenameParagraph
|
||||
? filenameParagraph.innerText
|
||||
: "";
|
||||
/** @type {string} */
|
||||
this.filenameParagraphText = filenameParagraph ? filenameParagraph.innerText : "";
|
||||
}
|
||||
|
||||
/** Execute: remove the page and update empty-state UI if needed. */
|
||||
execute() {
|
||||
this.nextSibling = this.element.nextSibling;
|
||||
|
||||
@ -23,15 +35,19 @@ export class DeletePageCommand extends Command {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Undo: reinsert the page at its original position. */
|
||||
undo() {
|
||||
let node = this.nextSibling;
|
||||
const node = /** @type {ChildNode|null} */ (this.nextSibling);
|
||||
if (node) this.pagesContainer.insertBefore(this.element, node);
|
||||
else this.pagesContainer.appendChild(this.element);
|
||||
|
||||
@ -43,12 +59,16 @@ export class DeletePageCommand extends Command {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = false;
|
||||
filenameInput.value = this.filenameInputValue;
|
||||
|
||||
downloadBtn.disabled = false;
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = false;
|
||||
filenameInput.value = this.filenameInputValue;
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Redo: remove again and maintain empty-state UI. */
|
||||
redo() {
|
||||
const pageNumberElement = this.element.querySelector(".page-number");
|
||||
if (pageNumberElement) {
|
||||
@ -60,10 +80,13 @@ export class DeletePageCommand extends Command {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import { CommandWithAnchors } from './command.js';
|
||||
|
||||
export class DuplicatePageCommand extends CommandWithAnchors {
|
||||
/**
|
||||
* @param {HTMLElement} element - The page element to duplicate.
|
||||
* @param {Function} duplicatePageAction - (element) => HTMLElement (new clone already inserted)
|
||||
* @param {HTMLElement} pagesContainer
|
||||
*/
|
||||
constructor(element, duplicatePageAction, pagesContainer) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.duplicatePageAction = duplicatePageAction;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
/** @type {HTMLElement|null} */
|
||||
this.newElement = null;
|
||||
|
||||
/** @type {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }|null} */
|
||||
this._anchor = null;
|
||||
}
|
||||
|
||||
execute() {
|
||||
// Create and insert a duplicate next to the original
|
||||
this.newElement = this.duplicatePageAction(this.element);
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.newElement) return;
|
||||
|
||||
// Capture anchor before removal so redo can reinsert at the same position
|
||||
this._anchor = this.captureAnchor(this.newElement, this.pagesContainer);
|
||||
|
||||
this.pagesContainer.removeChild(this.newElement);
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = '';
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.updatePageNumbersAndCheckboxes?.();
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!this.newElement) {
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
if (this._anchor) {
|
||||
this.insertWithAnchor(this.pagesContainer, this._anchor);
|
||||
} else {
|
||||
// Fallback: insert relative to the original element
|
||||
this.pagesContainer.insertBefore(this.newElement, this.element.nextSibling || null);
|
||||
}
|
||||
window.updatePageNumbersAndCheckboxes?.();
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,25 @@
|
||||
import {Command} from './command.js';
|
||||
import { Command } from './command.js';
|
||||
|
||||
/**
|
||||
* Moves a page (or multiple pages, via PdfContainer wrapper) inside the container.
|
||||
*/
|
||||
export class MovePageCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} startElement - Dragged page container.
|
||||
* @param {HTMLElement|null} endElement - Destination reference; insert before this node. Null = append.
|
||||
* @param {HTMLElement} pagesContainer - Parent container with all pages.
|
||||
* @param {HTMLElement} pagesContainerWrapper - Scrollable wrapper element.
|
||||
* @param {boolean} [scrollTo=false] - Whether to apply a subtle scroll after move.
|
||||
*/
|
||||
constructor(startElement, endElement, pagesContainer, pagesContainerWrapper, scrollTo = false) {
|
||||
super();
|
||||
|
||||
this.pagesContainer = pagesContainer;
|
||||
const childArray = Array.from(this.pagesContainer.childNodes);
|
||||
|
||||
/** @type {number} */
|
||||
this.startIndex = childArray.indexOf(startElement);
|
||||
/** @type {number} */
|
||||
this.endIndex = childArray.indexOf(endElement);
|
||||
|
||||
this.startElement = startElement;
|
||||
@ -16,8 +28,10 @@ export class MovePageCommand extends Command {
|
||||
this.scrollTo = scrollTo;
|
||||
this.pagesContainerWrapper = pagesContainerWrapper;
|
||||
}
|
||||
|
||||
/** Execute: perform DOM move and optional scroll. */
|
||||
execute() {
|
||||
// Check & remove page number elements here too if they exist because Firefox doesn't fire the relevant event on page move.
|
||||
// Remove stale page number badge if present (Firefox sometimes misses the event)
|
||||
const pageNumberElement = this.startElement.querySelector('.page-number');
|
||||
if (pageNumberElement) {
|
||||
this.startElement.removeChild(pageNumberElement);
|
||||
@ -31,7 +45,7 @@ export class MovePageCommand extends Command {
|
||||
}
|
||||
|
||||
if (this.scrollTo) {
|
||||
const {width} = this.startElement.getBoundingClientRect();
|
||||
const { width } = this.startElement.getBoundingClientRect();
|
||||
const vector = this.endIndex !== -1 && this.startIndex > this.endIndex ? 0 - width : width;
|
||||
|
||||
this.pagesContainerWrapper.scroll({
|
||||
@ -40,16 +54,17 @@ export class MovePageCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Undo: restore original order and optional scroll back. */
|
||||
undo() {
|
||||
if (this.startElement) {
|
||||
this.pagesContainer.removeChild(this.startElement);
|
||||
let previousNeighbour = Array.from(this.pagesContainer.childNodes)[this.startIndex];
|
||||
const 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 { width } = this.startElement.getBoundingClientRect();
|
||||
const vector = this.endIndex === -1 || this.startIndex <= this.endIndex ? 0 - width : width;
|
||||
|
||||
this.pagesContainerWrapper.scroll({
|
||||
@ -58,6 +73,7 @@ export class MovePageCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Redo: same as execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
import {Command} from './command.js';
|
||||
import { CommandWithAnchors } from './command.js';
|
||||
|
||||
export class PageBreakCommand extends Command {
|
||||
export class PageBreakCommand extends CommandWithAnchors {
|
||||
/**
|
||||
* @param {HTMLElement[]} elements
|
||||
* @param {boolean} isSelectedInWindow
|
||||
* @param {number[]} selectedPages - 0-based indices of selected pages
|
||||
* @param {Function} pageBreakCallback - async (element, addedSoFar) => HTMLElement[]|HTMLElement|null
|
||||
* @param {HTMLElement} pagesContainer
|
||||
*/
|
||||
constructor(elements, isSelectedInWindow, selectedPages, pageBreakCallback, pagesContainer) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
@ -8,7 +15,17 @@ export class PageBreakCommand extends Command {
|
||||
this.selectedPages = selectedPages;
|
||||
this.pageBreakCallback = pageBreakCallback;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
/** @type {HTMLElement[]} */
|
||||
this.addedElements = [];
|
||||
|
||||
/**
|
||||
* Anchors captured on undo to support redo reinsertion.
|
||||
* @type {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }[]}
|
||||
*/
|
||||
this._anchors = [];
|
||||
|
||||
// Keep content snapshot if needed for future enhancements
|
||||
this.originalStates = Array.from(elements, (element) => ({
|
||||
element,
|
||||
hasContent: element.innerHTML.trim() !== '',
|
||||
@ -17,43 +34,72 @@ export class PageBreakCommand extends Command {
|
||||
|
||||
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 (undoBtn) undoBtn.disabled = true;
|
||||
|
||||
if (newElement) {
|
||||
this.addedElements = newElement;
|
||||
}
|
||||
for (const [index, element] of this.elements.entries()) {
|
||||
const withinSelection = !this.isSelectedInWindow || this.selectedPages.includes(index);
|
||||
if (!withinSelection) continue;
|
||||
|
||||
if (index !== 0) {
|
||||
const result = await this.pageBreakCallback(element, this.addedElements);
|
||||
if (!Array.isArray(this.addedElements)) {
|
||||
this.addedElements = [];
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
this.addedElements.push(...result);
|
||||
} else if (result) {
|
||||
this.addedElements.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
undoBtn.disabled = false;
|
||||
|
||||
// Capture anchors right after insertion so redo does not depend on undo.
|
||||
this._anchors = this.addedElements.map((el) => this.captureAnchor(el, this.pagesContainer));
|
||||
|
||||
if (undoBtn) undoBtn.disabled = false;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.addedElements.forEach((element) => {
|
||||
const nextSibling = element.nextSibling;
|
||||
this._anchors = [];
|
||||
|
||||
this.pagesContainer.removeChild(element);
|
||||
for (const el of this.addedElements) {
|
||||
this._anchors.push(this.captureAnchor(el, this.pagesContainer));
|
||||
this.pagesContainer.removeChild(el);
|
||||
}
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const filenameParagraph = document.getElementById('filename');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
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 = '';
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
element._nextSibling = nextSibling;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
// If elements are already present (no prior undo), do nothing.
|
||||
if (!this.addedElements.length) return;
|
||||
const alreadyInDom =
|
||||
this.addedElements[0].parentNode === this.pagesContainer;
|
||||
if (alreadyInDom) return;
|
||||
|
||||
// Use pre-captured anchors (from execute) or fall back to current ones.
|
||||
const anchors = (this._anchors && this._anchors.length)
|
||||
? this._anchors
|
||||
: this.addedElements.map((el) => this.captureAnchor(el, this.pagesContainer));
|
||||
|
||||
for (const anchor of anchors) {
|
||||
this.insertWithAnchor(this.pagesContainer, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,35 +1,45 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Deletes a set of selected pages and restores them on undo.
|
||||
*/
|
||||
export class RemoveSelectedCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} pagesContainer - Parent container.
|
||||
* @param {number[]} selectedPages - 1-based page numbers to remove.
|
||||
* @param {Function} updatePageNumbersAndCheckboxes - Callback to refresh UI state.
|
||||
*/
|
||||
constructor(pagesContainer, selectedPages, updatePageNumbersAndCheckboxes) {
|
||||
super();
|
||||
this.pagesContainer = pagesContainer;
|
||||
this.selectedPages = selectedPages;
|
||||
|
||||
/** @type {{idx:number, childNode:HTMLElement}[]} */
|
||||
this.deletedChildren = [];
|
||||
|
||||
if (updatePageNumbersAndCheckboxes) {
|
||||
this.updatePageNumbersAndCheckboxes = updatePageNumbersAndCheckboxes;
|
||||
} else {
|
||||
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);
|
||||
if (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");
|
||||
|
||||
/** @type {string} */
|
||||
this.originalFilenameInputValue = filenameInput ? filenameInput.value : "";
|
||||
if (filenameParagraph)
|
||||
this.originalFilenameParagraphText = filenameParagraph.innerText;
|
||||
/** @type {string|undefined} */
|
||||
this.originalFilenameParagraphText = filenameParagraph?.innerText;
|
||||
}
|
||||
|
||||
/** Execute: remove selected pages and update empty state. */
|
||||
execute() {
|
||||
let deletions = 0;
|
||||
|
||||
@ -53,10 +63,9 @@ export class RemoveSelectedCommand extends Command {
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
if (filenameInput) filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
if (filenameInput) filenameInput.value = "";
|
||||
if (filenameParagraph) filenameParagraph.innerText = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
if (downloadBtn) downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
window.selectedPages = [];
|
||||
@ -64,12 +73,13 @@ export class RemoveSelectedCommand extends Command {
|
||||
document.dispatchEvent(new Event("selectedPagesUpdated"));
|
||||
}
|
||||
|
||||
/** Undo: restore all removed nodes at their original indices. */
|
||||
undo() {
|
||||
while (this.deletedChildren.length > 0) {
|
||||
let deletedChild = this.deletedChildren.pop();
|
||||
if (this.pagesContainer.children.length <= deletedChild.idx)
|
||||
const deletedChild = this.deletedChildren.pop();
|
||||
if (this.pagesContainer.children.length <= deletedChild.idx) {
|
||||
this.pagesContainer.appendChild(deletedChild.childNode);
|
||||
else {
|
||||
} else {
|
||||
this.pagesContainer.insertBefore(
|
||||
deletedChild.childNode,
|
||||
this.pagesContainer.children[deletedChild.idx]
|
||||
@ -83,11 +93,11 @@ export class RemoveSelectedCommand extends Command {
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
if (filenameInput) filenameInput.disabled = false;
|
||||
filenameInput.value = this.originalFilenameInputValue;
|
||||
if (filenameParagraph)
|
||||
if (filenameInput) filenameInput.value = this.originalFilenameInputValue;
|
||||
if (filenameParagraph && this.originalFilenameParagraphText !== undefined) {
|
||||
filenameParagraph.innerText = this.originalFilenameParagraphText;
|
||||
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
if (downloadBtn) downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
window.selectedPages = this.selectedPages;
|
||||
@ -95,6 +105,7 @@ export class RemoveSelectedCommand extends Command {
|
||||
document.dispatchEvent(new Event("selectedPagesUpdated"));
|
||||
}
|
||||
|
||||
/** Redo mirrors execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
@ -1,53 +1,61 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Rotates a single image element by a relative degree.
|
||||
*/
|
||||
export class RotateElementCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} element - The <img> element to rotate.
|
||||
* @param {number|string} degree - Relative degrees to add (e.g., 90 or "-90").
|
||||
*/
|
||||
constructor(element, degree) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
/** Execute: apply rotation. */
|
||||
execute() {
|
||||
let lastTransform = this.element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
let lastTransform = this.element.style.rotate || "0";
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const newAngle = lastAngle + parseInt(this.degree);
|
||||
|
||||
this.element.style.rotate = newAngle + "deg";
|
||||
}
|
||||
|
||||
/** Undo: revert by subtracting the same delta. */
|
||||
undo() {
|
||||
let lastTransform = this.element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
|
||||
let lastTransform = this.element.style.rotate || "0";
|
||||
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const undoAngle = currentAngle + -parseInt(this.degree);
|
||||
|
||||
this.element.style.rotate = undoAngle + "deg";
|
||||
}
|
||||
|
||||
/** Redo mirrors execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a set of image elements by a relative degree.
|
||||
*/
|
||||
export class RotateAllCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement[]} elements - Image elements to rotate.
|
||||
* @param {number} degree - Relative degrees to add for all.
|
||||
*/
|
||||
constructor(elements, degree) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
/** Execute: apply rotation to all. */
|
||||
execute() {
|
||||
for (let element of this.elements) {
|
||||
let lastTransform = element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
for (const element of this.elements) {
|
||||
let lastTransform = element.style.rotate || "0";
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const newAngle = lastAngle + this.degree;
|
||||
|
||||
@ -55,12 +63,10 @@ export class RotateAllCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Undo: revert rotation for all. */
|
||||
undo() {
|
||||
for (let element of this.elements) {
|
||||
let lastTransform = element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
for (const element of this.elements) {
|
||||
let lastTransform = element.style.rotate || "0";
|
||||
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const undoAngle = currentAngle + -this.degree;
|
||||
|
||||
@ -68,6 +74,7 @@ export class RotateAllCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Redo mirrors execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
@ -1,57 +1,45 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Toggles selection state of a single page via its checkbox.
|
||||
*/
|
||||
export class SelectPageCommand extends Command {
|
||||
/**
|
||||
* @param {number} pageNumber - 1-based page number.
|
||||
* @param {HTMLInputElement} checkbox - Checkbox linked to the page.
|
||||
*/
|
||||
constructor(pageNumber, checkbox) {
|
||||
super();
|
||||
this.pageNumber = pageNumber;
|
||||
this.selectCheckbox = checkbox;
|
||||
}
|
||||
|
||||
/** Execute: apply current checkbox state to global selection. */
|
||||
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 (index !== -1) window.selectedPages.splice(index, 1);
|
||||
}
|
||||
|
||||
if (window.selectedPages.length > 0 && !window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
if (window.selectedPages.length == 0 && window.selectPage) {
|
||||
if (window.selectedPages.length === 0 && window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
|
||||
window.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
/** Undo: invert checkbox and apply same logic as execute. */
|
||||
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();
|
||||
this.execute();
|
||||
}
|
||||
|
||||
/** Redo: invert again then execute. */
|
||||
redo() {
|
||||
this.selectCheckbox.checked = !this.selectCheckbox.checked;
|
||||
this.execute();
|
||||
|
||||
@ -1,26 +1,45 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Toggles a split class on a single page element.
|
||||
*/
|
||||
export class SplitFileCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} element - Target page container.
|
||||
* @param {string} splitClass - CSS class to toggle for split markers.
|
||||
*/
|
||||
constructor(element, splitClass) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.splitClass = splitClass;
|
||||
}
|
||||
|
||||
/** Execute: toggle split class. */
|
||||
execute() {
|
||||
this.element.classList.toggle(this.splitClass);
|
||||
}
|
||||
|
||||
/** Undo: toggle split class back. */
|
||||
undo() {
|
||||
this.element.classList.toggle(this.splitClass);
|
||||
}
|
||||
|
||||
/** Redo: same as execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles split class across a set of elements, optionally limited by selection.
|
||||
*/
|
||||
export class SplitAllCommand extends Command {
|
||||
/**
|
||||
* @param {NodeListOf<HTMLElement>|HTMLElement[]} elements - All page containers.
|
||||
* @param {boolean} isSelectedInWindow - Whether multi-select mode is active.
|
||||
* @param {number[]} selectedPages - 0-based indices of selected pages (when active).
|
||||
* @param {string} splitClass - CSS class used as split marker.
|
||||
*/
|
||||
constructor(elements, isSelectedInWindow, selectedPages, splitClass) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
@ -29,72 +48,41 @@ export class SplitAllCommand extends Command {
|
||||
this.splitClass = splitClass;
|
||||
}
|
||||
|
||||
/** Execute: toggle split for all or selected pages. */
|
||||
execute() {
|
||||
if (!this.isSelectedInWindow) {
|
||||
const hasSplit = this._hasSplit(this.elements, this.splitClass);
|
||||
if (hasSplit) {
|
||||
this.elements.forEach((page) => {
|
||||
const hasSplit = this._hasSplit();
|
||||
(this.elements || []).forEach((page) => {
|
||||
if (hasSplit) {
|
||||
page.classList.remove(this.splitClass);
|
||||
});
|
||||
} else {
|
||||
this.elements.forEach((page) => {
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
if (!this.selectedPages.includes(index)) return;
|
||||
page.classList.toggle(this.splitClass);
|
||||
});
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if any element currently has the split class. */
|
||||
_hasSplit() {
|
||||
if (!this.elements || this.elements.length == 0) return false;
|
||||
|
||||
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 mirrors execute logic. */
|
||||
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);
|
||||
}
|
||||
});
|
||||
this.execute();
|
||||
}
|
||||
|
||||
/** Redo mirrors execute logic. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
split: '[[#{multiTool.split}]]',
|
||||
addFile: '[[#{multiTool.addFile}]]',
|
||||
insertPageBreak: '[[#{multiTool.insertPageBreak}]]',
|
||||
duplicate: '[[#{multiTool.duplicate}]]',
|
||||
dragDropMessage: '[[#{multiTool.dragDropMessage}]]',
|
||||
undo: '[[#{multiTool.undo}]]',
|
||||
redo: '[[#{multiTool.redo}]]',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user