mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-05-14 02:05:55 +00:00

# Description of Changes Please provide a summary of the changes, including: - Why the change was made - Any challenges encountered - Added google drive integration config to premium settings in setting.yml - Added google drive button to file picker when enabled - Picker appears and allows users to load pdfs and other files into the tools Closes #(2946) --- ### Documentation [Docs Update PR](https://github.com/Stirling-Tools/Stirling-Tools.github.io/pull/67) --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
528 lines
18 KiB
JavaScript
528 lines
18 KiB
JavaScript
import FileIconFactory from './file-icon-factory.js';
|
|
import FileUtils from './file-utils.js';
|
|
import UUID from './uuid.js';
|
|
import { DecryptFile } from './DecryptFiles.js';
|
|
|
|
let isScriptExecuted = false;
|
|
if (!isScriptExecuted) {
|
|
isScriptExecuted = true;
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
document.querySelectorAll('.custom-file-chooser').forEach(setupFileInput);
|
|
});
|
|
}
|
|
let hasDroppedImage = false;
|
|
|
|
const zipTypes = [
|
|
'application/zip',
|
|
'multipart/x-zip',
|
|
'application/zip-compressed',
|
|
'application/x-zip-compressed',
|
|
];
|
|
|
|
const mimeTypes = {
|
|
"png": "image/png",
|
|
"jpg": "image/jpeg",
|
|
"jpeg": "image/jpeg",
|
|
"gif": "image/gif",
|
|
"bmp": "image/bmp",
|
|
"svg": "image/svg+xml",
|
|
"pdf": "application/pdf",
|
|
};
|
|
|
|
function setupFileInput(chooser) {
|
|
const elementId = chooser.getAttribute('data-bs-element-id');
|
|
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";
|
|
const name = chooser.getAttribute('data-bs-unique-id')
|
|
const noFileSelectedPrompt = chooser.getAttribute('data-bs-no-file-selected');
|
|
|
|
let inputContainer = document.getElementById(inputContainerId);
|
|
const input = document.getElementById(elementId);
|
|
|
|
if (inputContainer.id === 'pdf-upload-input-container') {
|
|
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropPDF;
|
|
} else if (inputContainer.id === 'image-upload-input-container') {
|
|
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropImage;
|
|
}
|
|
let allFiles = [];
|
|
let overlay;
|
|
let dragCounter = 0;
|
|
|
|
input.addEventListener('reset', (e) => {
|
|
allFiles = [];
|
|
input.value = null;
|
|
});
|
|
|
|
inputContainer.addEventListener('click', (e) => {
|
|
let inputBtn = document.getElementById(elementId);
|
|
inputBtn.click();
|
|
});
|
|
|
|
// Handle form validation if the input is left empty
|
|
input.addEventListener("invalid", (e) => {
|
|
e.preventDefault();
|
|
alert(noFileSelectedPrompt);
|
|
});
|
|
|
|
const dragenterListener = function () {
|
|
dragCounter++;
|
|
if (!overlay) {
|
|
// Show overlay by removing display: none from pseudo elements (::before and ::after)
|
|
inputContainer.style.setProperty('--overlay-display', "''");
|
|
overlay = true;
|
|
}
|
|
};
|
|
|
|
const dragleaveListener = function () {
|
|
dragCounter--;
|
|
if (dragCounter === 0) {
|
|
hideOverlay();
|
|
}
|
|
};
|
|
|
|
function hideOverlay() {
|
|
if (!overlay) return;
|
|
inputContainer.style.setProperty('--overlay-display', 'none');
|
|
overlay = false;
|
|
}
|
|
|
|
const googleDriveFileListener = function (e) {
|
|
const googleDriveFiles = e.detail;
|
|
|
|
const fileInput = document.getElementById(elementId);
|
|
if (fileInput?.hasAttribute('multiple')) {
|
|
pushFileListTo(googleDriveFiles, allFiles);
|
|
} else if (fileInput) {
|
|
allFiles = [googleDriveFiles[0]];
|
|
}
|
|
|
|
const dataTransfer = new DataTransfer();
|
|
allFiles.forEach((file) => dataTransfer.items.add(file));
|
|
fileInput.files = dataTransfer.files;
|
|
fileInput.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { source: 'drag-drop' } }));
|
|
}
|
|
|
|
const dropListener = function (e) {
|
|
e.preventDefault();
|
|
// Drag and Drop shall only affect the target file chooser
|
|
if (e.target !== inputContainer) {
|
|
hideOverlay();
|
|
dragCounter = 0;
|
|
return;
|
|
}
|
|
|
|
const dt = e.dataTransfer;
|
|
const files = dt.files;
|
|
|
|
const fileInput = document.getElementById(elementId);
|
|
if (fileInput?.hasAttribute('multiple')) {
|
|
pushFileListTo(files, allFiles);
|
|
} else if (fileInput) {
|
|
allFiles = [files[0]];
|
|
}
|
|
|
|
const dataTransfer = new DataTransfer();
|
|
allFiles.forEach((file) => dataTransfer.items.add(file));
|
|
|
|
fileInput.files = dataTransfer.files;
|
|
|
|
hideOverlay();
|
|
|
|
dragCounter = 0;
|
|
|
|
fileInput.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { source: 'drag-drop' } }));
|
|
};
|
|
|
|
function pushFileListTo(fileList, container) {
|
|
for (let file of fileList) {
|
|
container.push(file);
|
|
}
|
|
}
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
|
|
document.body.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
document.body.addEventListener('dragenter', dragenterListener);
|
|
document.body.addEventListener('dragleave', dragleaveListener);
|
|
document.body.addEventListener('drop', dropListener);
|
|
document.body.addEventListener(name + 'GoogleDriveDrivePicked', googleDriveFileListener);
|
|
|
|
$('#' + elementId).on('change', async function (e) {
|
|
let element = e.target;
|
|
const isDragAndDrop = e.detail?.source == 'drag-drop';
|
|
|
|
if (element instanceof HTMLInputElement && element.hasAttribute('multiple')) {
|
|
allFiles = isDragAndDrop ? allFiles : [...allFiles, ...element.files];
|
|
} else {
|
|
allFiles = Array.from(isDragAndDrop ? allFiles : [element.files[0]]);
|
|
}
|
|
|
|
const originalText = inputContainer.querySelector('#fileInputText').innerHTML;
|
|
|
|
inputContainer.querySelector('#fileInputText').innerHTML = window.fileInput.loading;
|
|
|
|
async function checkZipFile() {
|
|
const hasZipFiles = allFiles.some(file => zipTypes.includes(file.type));
|
|
|
|
// Only change to extractPDF message if we actually have zip files
|
|
if (hasZipFiles) {
|
|
inputContainer.querySelector('#fileInputText').innerHTML = window.fileInput.extractPDF;
|
|
}
|
|
|
|
const promises = allFiles.map(async (file, index) => {
|
|
try {
|
|
if (zipTypes.includes(file.type)) {
|
|
await extractZipFiles(file, element.accept);
|
|
allFiles.splice(index, 1);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error extracting ZIP file (${file.name}):`, error);
|
|
allFiles.splice(index, 1);
|
|
}
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
const decryptFile = new DecryptFile();
|
|
|
|
await checkZipFile();
|
|
|
|
allFiles = await Promise.all(
|
|
allFiles.map(async (file) => {
|
|
let decryptedFile = file;
|
|
|
|
try {
|
|
const { isEncrypted, requiresPassword } = await decryptFile.checkFileEncrypted(file);
|
|
if (file.type === 'application/pdf' && isEncrypted) {
|
|
decryptedFile = await decryptFile.decryptFile(file, requiresPassword);
|
|
if (!decryptedFile) throw new Error('File decryption failed.');
|
|
}
|
|
decryptedFile.uniqueId = UUID.uuidv4();
|
|
return decryptedFile;
|
|
|
|
} catch (error) {
|
|
console.error(`Error decrypting file: ${file.name}`, error);
|
|
if (!file.uniqueId) file.uniqueId = UUID.uuidv4();
|
|
return file;
|
|
}
|
|
})
|
|
);
|
|
|
|
inputContainer.querySelector('#fileInputText').innerHTML = originalText;
|
|
if (!isDragAndDrop) {
|
|
let dataTransfer = toDataTransfer(allFiles);
|
|
element.files = dataTransfer.files;
|
|
}
|
|
|
|
handleFileInputChange(this);
|
|
this.dispatchEvent(new CustomEvent('file-input-change', { bubbles: true, detail: { elementId, allFiles } }));
|
|
});
|
|
|
|
function toDataTransfer(files) {
|
|
let dataTransfer = new DataTransfer();
|
|
files.forEach((file) => dataTransfer.items.add(file));
|
|
return dataTransfer;
|
|
}
|
|
|
|
async function extractZipFiles(zipFile, acceptedFileType) {
|
|
const jszip = new JSZip();
|
|
var counter = 0;
|
|
|
|
// do an overall count, then proceed to make the pdf files
|
|
await jszip.loadAsync(zipFile)
|
|
.then(function (zip) {
|
|
|
|
zip.forEach(function (relativePath, zipEntry) {
|
|
counter+=1;
|
|
})
|
|
}
|
|
)
|
|
|
|
if (counter >= 1000) {
|
|
throw Error("Maximum file reached");
|
|
}
|
|
|
|
return jszip.loadAsync(zipFile)
|
|
.then(function (zip) {
|
|
var extractionPromises = [];
|
|
|
|
zip.forEach(function (relativePath, zipEntry) {
|
|
const promise = zipEntry.async('blob').then(function (content) {
|
|
// Assuming that folders have size zero
|
|
if (content.size > 0) {
|
|
const extension = zipEntry.name.split('.').pop().toLowerCase();
|
|
const mimeType = mimeTypes[extension] || 'application/octet-stream';
|
|
|
|
// Check if we're accepting ONLY ZIP files (in which case extract everything)
|
|
// or if the file type matches the accepted type
|
|
if (zipTypes.includes(acceptedFileType) ||
|
|
acceptedFileType === '*/*' ||
|
|
(mimeType && (mimeType.startsWith(acceptedFileType.split('/')[0]) || acceptedFileType === mimeType))) {
|
|
var file = new File([content], zipEntry.name, { type: mimeType });
|
|
file.uniqueId = UUID.uuidv4();
|
|
allFiles.push(file);
|
|
} else {
|
|
console.log(`File ${zipEntry.name} skipped. MIME type (${mimeType}) does not match accepted type (${acceptedFileType})`);
|
|
}
|
|
}
|
|
});
|
|
|
|
extractionPromises.push(promise);
|
|
});
|
|
|
|
return Promise.all(extractionPromises);
|
|
})
|
|
.catch(function (err) {
|
|
console.error("Error extracting ZIP file:", err);
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
function handleFileInputChange(inputElement) {
|
|
|
|
const files = allFiles;
|
|
|
|
showOrHideSelectedFilesContainer(files);
|
|
|
|
const filesInfo = files.map((f) => {
|
|
|
|
const url = URL.createObjectURL(f);
|
|
|
|
return {
|
|
name: f.name,
|
|
size: f.size,
|
|
uniqueId: f.uniqueId,
|
|
type: f.type,
|
|
url: url,
|
|
};
|
|
});
|
|
|
|
const selectedFilesContainer = $(inputContainer).siblings('.selected-files');
|
|
selectedFilesContainer.empty();
|
|
filesInfo.forEach((info) => {
|
|
let fileContainerClasses = 'small-file-container d-flex flex-column justify-content-center align-items-center';
|
|
|
|
let fileContainer = document.createElement('div');
|
|
$(fileContainer).addClass(fileContainerClasses);
|
|
$(fileContainer).attr('id', info.uniqueId);
|
|
|
|
let fileIconContainer = document.createElement('div');
|
|
const isDragAndDropEnabled =
|
|
window.location.pathname.includes('add-image') || window.location.pathname.includes('sign');
|
|
|
|
// add image thumbnail to it
|
|
if (info.type.startsWith('image/')) {
|
|
let imgPreview = document.createElement('img');
|
|
imgPreview.src = info.url;
|
|
imgPreview.alt = 'Preview';
|
|
imgPreview.style.width = '50px';
|
|
imgPreview.style.height = '50px';
|
|
imgPreview.style.objectFit = 'cover';
|
|
$(fileIconContainer).append(imgPreview);
|
|
|
|
if (isDragAndDropEnabled) {
|
|
let dragIcon = document.createElement('div');
|
|
dragIcon.classList.add('drag-icon');
|
|
dragIcon.innerHTML =
|
|
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z"/></svg>';
|
|
fileContainer.appendChild(dragIcon);
|
|
|
|
$(fileContainer).attr('draggable', 'true');
|
|
$(fileContainer).on('dragstart', (e) => {
|
|
e.originalEvent.dataTransfer.setData('fileUrl', info.url);
|
|
e.originalEvent.dataTransfer.setData('uniqueId', info.uniqueId);
|
|
e.originalEvent.dataTransfer.setDragImage(imgPreview, imgPreview.width / 2, imgPreview.height / 2);
|
|
});
|
|
enableImagePreviewOnClick(fileIconContainer);
|
|
} else {
|
|
$(fileContainer).removeAttr('draggable');
|
|
}
|
|
} else {
|
|
fileIconContainer = createFileIconContainer(info);
|
|
}
|
|
|
|
let fileInfoContainer = createFileInfoContainer(info);
|
|
|
|
if (!isDragAndDropEnabled) {
|
|
let removeBtn = document.createElement('div');
|
|
removeBtn.classList.add('remove-selected-file');
|
|
|
|
let removeBtnIconHTML = `<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#C02223"><path d="m339-288 141-141 141 141 51-51-141-141 141-141-51-51-141 141-141-141-51 51 141 141-141 141 51 51ZM480-96q-79 0-149-30t-122.5-82.5Q156-261 126-331T96-480q0-80 30-149.5t82.5-122Q261-804 331-834t149-30q80 0 149.5 30t122 82.5Q804-699 834-629.5T864-480q0 79-30 149t-82.5 122.5Q699-156 629.5-126T480-96Z"/></svg>`;
|
|
$(removeBtn).append(removeBtnIconHTML);
|
|
$(removeBtn).attr('data-file-id', info.uniqueId).click(removeFileListener);
|
|
$(fileContainer).append(removeBtn);
|
|
}
|
|
$(fileContainer).append(fileIconContainer, fileInfoContainer);
|
|
|
|
selectedFilesContainer.append(fileContainer);
|
|
});
|
|
const pageContainers = $('#box-drag-container');
|
|
pageContainers.off('dragover').on('dragover', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
|
|
pageContainers.off('drop').on('drop', (e) => {
|
|
e.preventDefault();
|
|
const fileUrl = e.originalEvent.dataTransfer.getData('fileUrl');
|
|
|
|
if (fileUrl) {
|
|
const existingImages = $(e.target).find(`img[src="${fileUrl}"]`);
|
|
if (existingImages.length === 0) {
|
|
DraggableUtils.createDraggableCanvasFromUrl(fileUrl);
|
|
}
|
|
}
|
|
const overlayElement = chooser.querySelector('.drag-drop-overlay');
|
|
if (overlayElement) {
|
|
overlayElement.style.display = 'none';
|
|
}
|
|
hasDroppedImage = true;
|
|
});
|
|
|
|
showOrHideSelectedFilesContainer(files);
|
|
}
|
|
|
|
function showOrHideSelectedFilesContainer(files) {
|
|
if (showUploads && files && files.length > 0) {
|
|
chooser.style.setProperty('--selected-files-display', 'flex');
|
|
} else {
|
|
chooser.style.setProperty('--selected-files-display', 'none');
|
|
}
|
|
const isDragAndDropEnabled =
|
|
(window.location.pathname.includes('add-image') || window.location.pathname.includes('sign')) &&
|
|
files.some((file) => file.type.startsWith('image/'));
|
|
|
|
if (!isDragAndDropEnabled) return;
|
|
|
|
const selectedFilesContainer = chooser.querySelector('.selected-files');
|
|
|
|
let overlayElement = chooser.querySelector('.drag-drop-overlay');
|
|
if (!overlayElement) {
|
|
selectedFilesContainer.style.position = 'relative';
|
|
overlayElement = document.createElement('div');
|
|
overlayElement.classList.add('draggable-image-overlay');
|
|
|
|
overlayElement.innerHTML = 'Drag images to add them to the page';
|
|
selectedFilesContainer.appendChild(overlayElement);
|
|
}
|
|
if (hasDroppedImage) overlayElement.style.display = files && files.length > 0 ? 'flex' : 'none';
|
|
|
|
selectedFilesContainer.addEventListener('mouseenter', () => {
|
|
overlayElement.style.display = 'none';
|
|
});
|
|
|
|
selectedFilesContainer.addEventListener('mouseleave', () => {
|
|
if (!hasDroppedImage) overlayElement.style.display = files && files.length > 0 ? 'flex' : 'none';
|
|
});
|
|
}
|
|
|
|
function removeFileListener(e) {
|
|
const fileId = e.target.getAttribute('data-file-id');
|
|
|
|
let inputElement = document.getElementById(elementId);
|
|
removeFileById(fileId, inputElement);
|
|
|
|
showOrHideSelectedFilesContainer(allFiles);
|
|
|
|
inputElement.dispatchEvent(new CustomEvent('file-input-change', { bubbles: true }));
|
|
}
|
|
|
|
function removeFileById(fileId, inputElement) {
|
|
let fileContainer = document.getElementById(fileId);
|
|
fileContainer.remove();
|
|
|
|
allFiles = allFiles.filter((v) => v.uniqueId != fileId);
|
|
let dataTransfer = toDataTransfer(allFiles);
|
|
|
|
if (inputElement) inputElement.files = dataTransfer.files;
|
|
}
|
|
|
|
function createFileIconContainer(info) {
|
|
let fileIconContainer = document.createElement('div');
|
|
fileIconContainer.classList.add('file-icon');
|
|
|
|
// Add icon based on the extension
|
|
let fileExtension = FileUtils.extractFileExtension(info.name);
|
|
let fileIcon = FileIconFactory.createFileIcon(fileExtension);
|
|
|
|
$(fileIconContainer).append(fileIcon);
|
|
return fileIconContainer;
|
|
}
|
|
|
|
function createFileInfoContainer(info) {
|
|
let fileInfoContainer = document.createElement('div');
|
|
let fileInfoContainerClasses = 'file-info d-flex flex-column align-items-center justify-content-center';
|
|
|
|
$(fileInfoContainer).addClass(fileInfoContainerClasses);
|
|
|
|
$(fileInfoContainer).append(`<div title="${info.name}">${info.name}</div>`);
|
|
let fileSizeWithUnits = FileUtils.transformFileSize(info.size);
|
|
$(fileInfoContainer).append(`<div title="${info.size}">${fileSizeWithUnits}</div>`);
|
|
return fileInfoContainer;
|
|
}
|
|
|
|
//Listen for event of file being removed and the filter it out of the allFiles array
|
|
document.addEventListener('fileRemoved', function (e) {
|
|
const fileId = e.detail;
|
|
let inputElement = document.getElementById(elementId);
|
|
removeFileById(fileId, inputElement);
|
|
showOrHideSelectedFilesContainer(allFiles);
|
|
});
|
|
function enableImagePreviewOnClick(container) {
|
|
const imagePreviewModal = document.getElementById('imagePreviewModal') || createImagePreviewModal();
|
|
|
|
container.querySelectorAll('img').forEach((img) => {
|
|
if (!img.hasPreviewListener) {
|
|
img.addEventListener('mouseup', function () {
|
|
const imgElement = imagePreviewModal.querySelector('img');
|
|
imgElement.src = this.src;
|
|
imagePreviewModal.style.display = 'flex';
|
|
});
|
|
img.hasPreviewListener = true;
|
|
}
|
|
});
|
|
|
|
function createImagePreviewModal() {
|
|
const modal = document.createElement('div');
|
|
modal.id = 'imagePreviewModal';
|
|
modal.style.position = 'fixed';
|
|
modal.style.top = '0';
|
|
modal.style.left = '0';
|
|
modal.style.width = '100vw';
|
|
modal.style.height = '100vh';
|
|
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
|
|
modal.style.display = 'none';
|
|
modal.style.justifyContent = 'center';
|
|
modal.style.alignItems = 'center';
|
|
modal.style.zIndex = '2000';
|
|
|
|
const imgElement = document.createElement('img');
|
|
imgElement.style.maxWidth = '90%';
|
|
imgElement.style.maxHeight = '90%';
|
|
|
|
modal.appendChild(imgElement);
|
|
document.body.appendChild(modal);
|
|
|
|
modal.addEventListener('click', () => {
|
|
modal.style.display = 'none';
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && modal.style.display === 'flex') {
|
|
modal.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
return modal;
|
|
}
|
|
}
|
|
}
|