2385 feature request pdf multi tool to use new file input box (#3201)

# Description of Changes

Please provide a summary of the changes, including:

- Multitool now makes use of the common file input. 
- deleted multitool file input
- moved tool bar to floating at the bottom of the view window

Closes #(2385)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] 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/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] 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/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
ConnorYoh 2025-03-20 00:06:47 +00:00 committed by GitHub
parent a93e3698f9
commit 75db6c80aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 163 additions and 245 deletions

View File

@ -14,26 +14,30 @@ label {
border-radius: 16px !important;
padding: 0.75rem;
border: 1px solid var(--theme-color-outline-variant);
flex-grow: 5;
}
.mt-action-bar {
display: flex;
gap: 10px;
align-items: start;
background-color: var(--md-sys-color-surface-5);
border: none;
backdrop-filter: blur(2px);
top: 10px;
z-index: 10;
z-index: 11;
padding: 1.25rem;
border-radius: 2rem;
margin: 0px 25px;
justify-content:center;
}
.mt-action-bar>* {
padding-bottom: 0.5rem;
}
.mt-file-uploader {
width:100%
}
.mt-action-bar svg,
.mt-action-btn svg {
width: 20px;
@ -42,21 +46,29 @@ label {
.mt-action-bar .mt-filename {
width: 100%;
display: flex;
gap: 10px;
}
.mt-action-btn {
position: sticky;
bottom: 10%;
margin: auto;
margin-bottom: 25px;
border-radius: 2rem;
z-index: 12;
background-color: var(--md-sys-color-surface-container-low) ;
display: flex;
gap: 10px;
align-items: start;
top: 10px;
z-index: 10;
padding: 12px 0px 0px;
width: 100%;
width: fit-content;
justify-content: center;
padding: 10px 20px
}
.mt-action-btn .btn {
width: 3rem;
height: 3rem;
width: 3.5rem;
height: 3.5rem;
border-radius: 20px;
padding: 0;
}
@ -64,7 +76,7 @@ label {
.bg-card {
background-color: var(--md-sys-color-surface-5);
border-radius: 3rem;
padding: 25px 0 0;
padding: 25px 0;
}
#pages-container-wrapper {
@ -73,7 +85,7 @@ label {
flex-direction: column;
padding: 1rem;
border-radius: 25px;
overflow-y: auto;
overflow-y: clip;
overflow-x: auto;
min-height: 275px;
margin: 0 0 30px 0;
@ -136,10 +148,6 @@ label {
display: none;
}
/* Pushes the last item to the left */
.page-container:last-child {
margin-right: auto;
}
.page-container:last-child:lang(ar),
/* Arabic */

View File

@ -34,8 +34,10 @@ function setupFileInput(chooser) {
const filesSelected = chooser.getAttribute('data-bs-files-selected');
const pdfPrompt = chooser.getAttribute('data-bs-pdf-prompt');
const inputContainerId = chooser.getAttribute('data-bs-element-container-id');
const showUploads = chooser.getAttribute('data-bs-show-uploads') === "true";
let inputContainer = document.getElementById(inputContainerId);
const input = document.getElementById(elementId);
if (inputContainer.id === 'pdf-upload-input-container') {
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropPDF;
@ -46,6 +48,11 @@ function setupFileInput(chooser) {
let overlay;
let dragCounter = 0;
input.addEventListener('reset', (e) => {
allFiles = [];
input.value = null;
});
inputContainer.addEventListener('click', (e) => {
let inputBtn = document.getElementById(elementId);
inputBtn.click();
@ -353,7 +360,7 @@ function setupFileInput(chooser) {
}
function showOrHideSelectedFilesContainer(files) {
if (files && files.length > 0) {
if (showUploads && files && files.length > 0) {
chooser.style.setProperty('--selected-files-display', 'flex');
} else {
chooser.style.setProperty('--selected-files-display', 'none');

View File

@ -72,6 +72,7 @@ class PdfContainer {
window.addFilesBlankAll = this.addFilesBlankAll;
window.removeAllElements = this.removeAllElements;
window.resetPages = this.resetPages;
window.selectAll = false;
let undoBtn = document.getElementById('undo-btn');
let redoBtn = document.getElementById('redo-btn');
@ -129,6 +130,10 @@ class PdfContainer {
return commandSequence;
}
showButton(button, show) {
button.classList.toggle('hidden', !show);
}
movePageTo(startElements, endElement, scrollTo = false) {
if (Array.isArray(startElements)){
@ -176,8 +181,10 @@ class PdfContainer {
if (files.length > 0) {
pages = await this.addFilesFromFiles(files, nextSiblingElement, pages);
this.updateFilename(files[0].name);
const selectAll = document.getElementById('select-pages-container');
selectAll.classList.toggle('hidden', false);
if(window.selectPage){
this.showButton(document.getElementById('select-pages-container'), true);
}
}
resolve(pages);
};
@ -191,9 +198,8 @@ class PdfContainer {
const pages = await this.addFilesFromFiles(files, nextSiblingElement, []);
this.updateFilename(files[0]?.name || 'untitled');
const selectAll = document.getElementById('select-pages-container');
if (selectAll) {
selectAll.classList.remove('hidden');
if(window.selectPage) {
this.showButton(document.getElementById('select-pages-container'), true);
}
return pages;
@ -433,12 +439,12 @@ class PdfContainer {
const selectIcon = document.getElementById('select-All-Container');
const deselectIcon = document.getElementById('deselect-All-Container');
if (selectIcon.style.display === 'none') {
selectIcon.style.display = 'inline';
deselectIcon.style.display = 'none';
if (!window.selectAll) {
this.showButton(selectIcon, true);
this.showButton(deselectIcon, false);
} else {
selectIcon.style.display = 'none';
deselectIcon.style.display = 'inline';
this.showButton(selectIcon, false);
this.showButton(deselectIcon, true);
}
checkboxes.forEach((checkbox) => {
checkbox.checked = window.selectAll;
@ -846,8 +852,20 @@ class PdfContainer {
deleteButton.classList.toggle('hidden', !window.selectPage);
const selectedPages = document.getElementById('selected-pages-display');
selectedPages.classList.toggle('hidden', !window.selectPage);
const selectAll = document.getElementById('select-All-Container');
selectAll.classList.toggle('hidden', !window.selectPage);
if(!window.selectPage)
{
this.showButton(document.getElementById('deselect-All-Container'), false);
this.showButton(document.getElementById('select-All-Container'), false);
}
else if(window.selectAll){
this.showButton(document.getElementById('deselect-All-Container'), true);
this.showButton(document.getElementById('select-All-Container'), false);
}
else{
this.showButton(document.getElementById('deselect-All-Container'), false);
this.showButton(document.getElementById('select-All-Container'), true);
}
const exportSelected = document.getElementById('export-selected-button');
exportSelected.classList.toggle('hidden', !window.selectPage);
const selectPagesButton = document.getElementById('select-pages-button');

View File

@ -1,114 +0,0 @@
class FileDragManager {
overlay;
dragCounter;
updateFilename;
constructor(cb = null) {
this.dragCounter = 0;
this.setCallback(cb);
// Prevent default behavior for drag events
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
this.dragenterListener = this.dragenterListener.bind(this);
this.dragleaveListener = this.dragleaveListener.bind(this);
this.dropListener = this.dropListener.bind(this);
document.body.addEventListener('dragenter', this.dragenterListener);
document.body.addEventListener('dragleave', this.dragleaveListener);
// Add drop event listener
document.body.addEventListener('drop', this.dropListener);
}
setActions({updateFilename}) {
this.updateFilename = updateFilename;
}
setCallback(cb) {
if (cb) {
this.callback = cb;
} else {
this.callback = (files) => console.warn('FileDragManager not set');
}
}
dragenterListener() {
this.dragCounter++;
if (!this.overlay) {
// Create and show the overlay
this.overlay = document.createElement('div');
this.overlay.style.position = 'fixed';
this.overlay.style.top = 0;
this.overlay.style.left = 0;
this.overlay.style.width = '100%';
this.overlay.style.height = '100%';
this.overlay.style.background = 'rgba(0, 0, 0, 0.5)';
this.overlay.style.color = '#fff';
this.overlay.style.zIndex = '1000';
this.overlay.style.display = 'flex';
this.overlay.style.alignItems = 'center';
this.overlay.style.justifyContent = 'center';
this.overlay.style.pointerEvents = 'none';
this.overlay.innerHTML = '<p>Drop files anywhere to upload</p>';
document.getElementById('content-wrap').appendChild(this.overlay);
}
}
dragleaveListener() {
this.dragCounter--;
if (this.dragCounter === 0) {
// Hide and remove the overlay
if (this.overlay) {
this.overlay.remove();
this.overlay = null;
}
}
}
dropListener(e) {
const dt = e.dataTransfer;
const files = dt.files;
this.callback(files)
.catch((err) => {
console.error(err);
//maybe
})
.finally(() => {
// Hide and remove the overlay
if (this.overlay) {
this.overlay.remove();
this.overlay = null;
}
this.updateFilename(files ? files[0].name : '');
});
}
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 FileDragManager;

View File

@ -196,7 +196,7 @@
</th:block>
<th:block th:fragment="fileSelector(name, multipleInputsForSingleRequest)"
th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, disableMultipleFiles=${disableMultipleFiles} ?: false, notRequired=${notRequired} ?: false">
th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, disableMultipleFiles=${disableMultipleFiles} ?: false, showUploads=${showUploads} ?: true, notRequired=${notRequired} ?: false">
<script th:inline="javascript">
(function () {
window.stirlingPDF.pdfPasswordPrompt = /*[[#{error.pdfPassword}]]*/ '';
@ -227,7 +227,7 @@
extractPDF: '[[#{fileChooser.extractPDF}]]'
};</script>
<div class="custom-file-chooser mb-3"
th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-show-uploads=${showUploads}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
<div class="mb-3 d-flex flex-row justify-content-center align-items-center flex-wrap input-container"
th:name="${name}+'-input'" th:id="${name}+'-input-container'" th:data-text="#{fileChooser.hoveredDragAndDrop}">
<label class="file-input-btn d-none">

View File

@ -14,117 +14,113 @@
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon advance">construction</span>
<span class="tool-header-text" th:text="#{multiTool.header}"></span>
</div>
<div class="mt-action-bar d-flex flex-wrap">
<div class="mt-filename">
<label for="filename-input" th:text="#{multiTool.uploadPrompts}">Filename</label>
<input type="text" class="form-control" id="filename-input"
th:placeholder="#{multiTool.uploadPrompts}">
</div>
<div class="mt-action-btn">
<button class="btn btn-primary" th:title="#{multiTool.addFile}" onclick="addFiles()">
<span class="material-symbols-rounded">
add
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateLeft}"
onclick="rotateAll(-90)" disabled>
<span class="material-symbols-rounded">
rotate_left
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateRight}"
onclick="rotateAll(90)" disabled>
<span class="material-symbols-rounded">
rotate_right
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.split}" onclick="splitAll()"
disabled>
<span class="material-symbols-rounded">
cut
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.insertPageBreak}"
onclick="addFilesBlankAll()" disabled>
<span class="material-symbols-rounded">
insert_page_break
</span>
</button>
<button id="undo-btn" th:title="#{multiTool.undo}" class="btn btn-secondary" onclick="undo()"
disabled>
<span class="material-symbols-rounded">
undo
</span>
</button>
<button id="redo-btn" class="btn btn-secondary" th:title="#{multiTool.redo}" onclick="redo()"
disabled>
<span class="material-symbols-rounded">
redo
</span>
</button>
</button>
<button id="select-pages-container" th:title="#{multiTool.selectPages}"
class="btn btn-secondary enable-on-file" onclick="toggleSelectPageVisibility()" disabled>
<span id="select-pages-button" class="material-symbols-rounded">
event_list
</span>
</button>
<button id="deselect-All-Container" th:title="#{multiTool.deselectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="toggleSelectAll()" disabled>
<span class="material-symbols-rounded" id="deselect-icon">deselect</span>
</button>
<button id="select-All-Container" th:title="#{multiTool.selectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="toggleSelectAll()" disabled>
<span class="material-symbols-rounded" id="select-icon">select_all</span>
</button>
<div class="button-container">
<button id="delete-button" th:title="#{multiTool.deleteSelected}"
class="btn btn-danger delete hidden" onclick="deleteSelected()">
<span class="material-symbols-rounded">delete</span>
</button>
</div>
<div style="margin-left:auto">
<button id="export-selected-button" th:title="#{multiTool.downloadSelected}"
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
class="btn btn-primary enable-on-file hidden" onclick="exportPdf(true)" disabled>
<span class="material-symbols-rounded">
file_save
</span>
</button>
<button style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
th:title="#{multiTool.downloadAll}" id="export-button" class="btn btn-primary enable-on-file"
onclick="exportPdf(false)" disabled>
<span class="material-symbols-rounded">
download
</span>
</button>
</div>
</div>
<div id="selected-pages-display" class="selected-pages-container hidden">
<div style="display:flex; height:3rem; margin-right:1rem">
<h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected
Pages</h5>
<input type="text" id="csv-input" class="form-control" style="height:2.5rem" placeholder="1,3,5-10"
value="">
</div>
<ul id="selected-pages-list" class="pages-list"></ul>
<div class="mt-action-bar d-flex flex-wrap">
<div class="mt-filename">
<input type="text" class="form-control" id="filename-input"
th:placeholder="#{multiTool.uploadPrompts}">
</div>
<div class="mt-file-uploader">
<div
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, accept='image/*, application/pdf', showUploads=false)}">
</div>
</div>
<div id="selected-pages-display" class="selected-pages-container hidden">
<div style="display:flex; height:3rem; margin-right:1rem">
<h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected
Pages</h5>
<input type="text" id="csv-input" class="form-control" style="height:2.5rem" placeholder="1,3,5-10"
value="">
</div>
<ul id="selected-pages-list" class="pages-list"></ul>
</div>
</div>
<div class="multi-tool-container">
<div class="d-flex flex-wrap" id="pages-container-wrapper">
<div id="pages-container">
</div>
</div>
</div>
<div class="mt-action-btn">
<button id="undo-btn" th:title="#{multiTool.undo}" class="btn btn-secondary" onclick="undo()"
disabled>
<span class="material-symbols-rounded">
undo
</span>
</button>
<button id="redo-btn" class="btn btn-secondary" th:title="#{multiTool.redo}" onclick="redo()"
disabled>
<span class="material-symbols-rounded">
redo
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateLeft}"
onclick="rotateAll(-90)" disabled>
<span class="material-symbols-rounded">
rotate_left
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateRight}"
onclick="rotateAll(90)" disabled>
<span class="material-symbols-rounded">
rotate_right
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.split}" onclick="splitAll()"
disabled>
<span class="material-symbols-rounded">
cut
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.insertPageBreak}"
onclick="addFilesBlankAll()" disabled>
<span class="material-symbols-rounded">
insert_page_break
</span>
</button>
<button id="select-pages-container" th:title="#{multiTool.selectPages}"
class="btn btn-secondary enable-on-file" onclick="toggleSelectPageVisibility()" disabled>
<span id="select-pages-button" class="material-symbols-rounded">
event_list
</span>
</button>
<button id="deselect-All-Container" th:title="#{multiTool.deselectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="toggleSelectAll()" disabled>
<span class="material-symbols-rounded" id="deselect-icon">deselect</span>
</button>
<button id="select-All-Container" th:title="#{multiTool.selectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="toggleSelectAll()" disabled>
<span class="material-symbols-rounded" id="select-icon">select_all</span>
</button>
<button id="delete-button" th:title="#{multiTool.deleteSelected}"
class="btn btn-danger delete hidden" onclick="deleteSelected()">
<span class="material-symbols-rounded">delete</span>
</button>
<button id="export-selected-button" th:title="#{multiTool.downloadSelected}"
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
class="btn btn-primary enable-on-file hidden" onclick="exportPdf(true)" disabled>
<span class="material-symbols-rounded">
file_save
</span>
</button>
<button style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
th:title="#{multiTool.downloadAll}" id="export-button" class="btn btn-primary enable-on-file"
onclick="exportPdf(false)" disabled>
<span class="material-symbols-rounded">
download
</span>
</button>
</div>
</div>
</div>
</div>
@ -180,8 +176,14 @@
import DragDropManager from "./js/multitool/DragDropManager.js";
import ImageHighlighter from "./js/multitool/ImageHighlighter.js";
import PdfActionsManager from './js/multitool/PdfActionsManager.js';
import FileDragManager from './js/multitool/fileInput.js';
// enables drag and drop
const pdfUpload = document.querySelector("input[name=pdf-upload]");
pdfUpload.addEventListener("change", async (e) => {
if (!e.target.files) return;
await pdfContainer.handleDroppedFiles( e.target.files);
e.target.dispatchEvent(new CustomEvent('reset', {}));
});
var undoManager = new UndoManager();
const dragDropManager = new DragDropManager('drag-container', 'pages-container');
@ -189,7 +191,6 @@
const imageHighlighter = new ImageHighlighter('image-highlighter');
// enables the default action buttons on each file
const pdfActionsManager = new PdfActionsManager('pages-container', undoManager);
const fileDragManager = new FileDragManager();
// Scroll the wrapper horizontally
// Automatically exposes rotateAll, addFiles and exportPdf to the window for the global buttons.
@ -199,13 +200,11 @@
[
dragDropManager,
imageHighlighter,
pdfActionsManager,
fileDragManager
pdfActionsManager
],
undoManager
)
fileDragManager.setCallback(async (files) => pdfContainer.handleDroppedFiles(files));
document.addEventListener('keydown', function (event) {
let targetElementId = event.target.id;