mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-09-13 12:15:29 +02:00
Multi module refactor (#3640)
# Description of Changes Migrated Stirling PDF to a multi-module structure: * Introduced new `:stirling-pdf` module * Moved all the core logic and features of Stirling PDF into `:stirling-pdf` * Updated paths of jobs and scripts --- ## 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:
@@ -0,0 +1,150 @@
|
||||
export class DecryptFile {
|
||||
|
||||
constructor(){
|
||||
this.decryptWorker = null
|
||||
}
|
||||
|
||||
async decryptFile(file, requiresPassword) {
|
||||
|
||||
try {
|
||||
async function getCsrfToken() {
|
||||
const cookieValue = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (cookieValue) {
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
const csrfElement = document.querySelector('input[name="_csrf"]');
|
||||
return csrfElement ? csrfElement.value : null;
|
||||
}
|
||||
const csrfToken = await getCsrfToken();
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
if (requiresPassword) {
|
||||
const password = prompt(`${window.decrypt.passwordPrompt}`);
|
||||
|
||||
if (password === null) {
|
||||
// User cancelled
|
||||
console.error(`Password prompt cancelled for PDF: ${file.name}`);
|
||||
return null; // No file to return
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
// No password provided
|
||||
console.error(`No password provided for encrypted PDF: ${file.name}`);
|
||||
this.showErrorBanner(
|
||||
`${window.decrypt.noPassword.replace('{0}', file.name)}`,
|
||||
'',
|
||||
`${window.decrypt.unexpectedError}`
|
||||
);
|
||||
return null; // No file to return
|
||||
}
|
||||
|
||||
formData.append('password', password);
|
||||
}
|
||||
// Send decryption request
|
||||
const response = await fetch('/api/v1/security/remove-password', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: csrfToken ? {'X-XSRF-TOKEN': csrfToken} : undefined,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.removeErrorBanner();
|
||||
const decryptedBlob = await response.blob();
|
||||
return new File([decryptedBlob], file.name, {
|
||||
type: "application/pdf",
|
||||
});
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error(`${window.decrypt.invalidPassword} ${errorText}`);
|
||||
this.showErrorBanner(
|
||||
`${window.decrypt.invalidPassword}`,
|
||||
errorText,
|
||||
`${window.decrypt.invalidPasswordHeader.replace('{0}', file.name)}`
|
||||
);
|
||||
return null; // No file to return
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle network or unexpected errors
|
||||
console.error(`Failed to decrypt PDF: ${file.name}`, error);
|
||||
this.showErrorBanner(
|
||||
`${window.decrypt.unexpectedError.replace('{0}', file.name)}`,
|
||||
`${error.message || window.decrypt.unexpectedError}`,
|
||||
error
|
||||
);
|
||||
return null; // No file to return
|
||||
}
|
||||
}
|
||||
|
||||
async checkFileEncrypted(file) {
|
||||
try {
|
||||
if (file.type !== 'application/pdf') {
|
||||
return {isEncrypted: false, requiresPassword: false};
|
||||
}
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const arrayBufferForPdfLib = arrayBuffer.slice(0);
|
||||
var loadingTask;
|
||||
|
||||
if(this.decryptWorker == null){
|
||||
loadingTask = pdfjsLib.getDocument({
|
||||
data: arrayBuffer,
|
||||
});
|
||||
this.decryptWorker = loadingTask._worker
|
||||
|
||||
}else {
|
||||
loadingTask = pdfjsLib.getDocument({
|
||||
data: arrayBuffer,
|
||||
worker: this.decryptWorker
|
||||
});
|
||||
}
|
||||
|
||||
await loadingTask.promise;
|
||||
|
||||
try {
|
||||
//Uses PDFLib.PDFDocument to check if unpassworded but encrypted
|
||||
const pdfDoc = await PDFLib.PDFDocument.load(arrayBufferForPdfLib);
|
||||
return {isEncrypted: false, requiresPassword: false};
|
||||
} catch (error) {
|
||||
if (error.message.includes('Input document to `PDFDocument.load` is encrypted')) {
|
||||
return {isEncrypted: true, requiresPassword: false};
|
||||
}
|
||||
console.error('Error checking encryption:', error);
|
||||
throw new Error('Failed to determine if the file is encrypted.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'PasswordException') {
|
||||
if (error.code === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
|
||||
return {isEncrypted: true, requiresPassword: true};
|
||||
} else if (error.code === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
|
||||
return {isEncrypted: true, requiresPassword: false};
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Error checking encryption:', error);
|
||||
throw new Error('Failed to determine if the file is encrypted.');
|
||||
}
|
||||
}
|
||||
|
||||
showErrorBanner(message, stackTrace, error) {
|
||||
const errorContainer = document.getElementById('errorContainer');
|
||||
errorContainer.style.display = 'block'; // Display the banner
|
||||
errorContainer.querySelector('.alert-heading').textContent = error;
|
||||
errorContainer.querySelector('p').textContent = message;
|
||||
document.querySelector('#traceContent').textContent = stackTrace;
|
||||
}
|
||||
|
||||
removeErrorBanner() {
|
||||
const errorContainer = document.getElementById('errorContainer');
|
||||
errorContainer.style.display = 'none'; // Hide the banner
|
||||
errorContainer.querySelector('.alert-heading').textContent = '';
|
||||
errorContainer.querySelector('p').textContent = '';
|
||||
document.querySelector('#traceContent').textContent = '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
var cacheInputs = localStorage.getItem("cacheInputs") || "disabled";
|
||||
if (cacheInputs !== "enabled") {
|
||||
return; // Stop execution if caching is not enabled
|
||||
}
|
||||
|
||||
// Function to generate a key based on the form's action attribute
|
||||
function generateStorageKey(form) {
|
||||
const action = form.getAttribute('action');
|
||||
if (!action || action.length < 3) {
|
||||
return null; // Not a valid action, return null to skip processing
|
||||
}
|
||||
return 'formData_' + encodeURIComponent(action);
|
||||
}
|
||||
|
||||
// Function to save form data to localStorage
|
||||
function saveFormData(form) {
|
||||
const formKey = generateStorageKey(form);
|
||||
if (!formKey) return; // Skip if no valid key
|
||||
|
||||
const formData = {};
|
||||
const elements = form.elements;
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i];
|
||||
// Skip elements without names, passwords, files, hidden fields, and submit/reset buttons
|
||||
if (!element.name ||
|
||||
element.type === 'password' ||
|
||||
element.type === 'file' ||
|
||||
//element.type === 'hidden' ||
|
||||
element.type === 'submit' ||
|
||||
element.type === 'reset') {
|
||||
continue;
|
||||
}
|
||||
// Handle checkboxes: store only if checked
|
||||
if (element.type === 'checkbox') {
|
||||
if (element.checked) {
|
||||
formData[element.name] = element.value;
|
||||
} else {
|
||||
continue; // Skip unchecked boxes
|
||||
}
|
||||
} else {
|
||||
// Skip saving empty values
|
||||
if (element.value === "" || element.value == null) {
|
||||
continue;
|
||||
}
|
||||
formData[element.name] = element.value;
|
||||
}
|
||||
}
|
||||
if (Object.keys(formData).length > 0) {
|
||||
localStorage.setItem(formKey, JSON.stringify(formData));
|
||||
}
|
||||
}
|
||||
|
||||
// Function to load form data from localStorage
|
||||
function loadFormData(form) {
|
||||
const formKey = generateStorageKey(form);
|
||||
if (!formKey) return; // Skip if no valid key
|
||||
|
||||
const savedData = localStorage.getItem(formKey);
|
||||
if (savedData) {
|
||||
const formData = JSON.parse(savedData);
|
||||
for (const key in formData) {
|
||||
if (formData.hasOwnProperty(key) && form.elements[key]) {
|
||||
const element = form.elements[key];
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = true;
|
||||
} else {
|
||||
element.value = formData[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attach event listeners and load data for all forms
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(event) {
|
||||
saveFormData(form);
|
||||
});
|
||||
loadFormData(form);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
importScripts('./diff.js');
|
||||
|
||||
self.onmessage = async function (e) {
|
||||
const { text1, text2, color1, color2 } = e.data;
|
||||
console.log('Received text for comparison:', { text1, text2 });
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
if (text1.trim() === "" || text2.trim() === "") {
|
||||
self.postMessage({ status: 'error', message: 'One or both of the texts are empty.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const words1 = text1.split(' ');
|
||||
const words2 = text2.split(' ');
|
||||
const MAX_WORD_COUNT = 150000;
|
||||
const COMPLEX_WORD_COUNT = 50000;
|
||||
const BATCH_SIZE = 5000; // Define a suitable batch size for processing
|
||||
const OVERLAP_SIZE = 200; // Number of words to overlap - bigger increases accuracy but affects performance
|
||||
|
||||
const isComplex = words1.length > COMPLEX_WORD_COUNT || words2.length > COMPLEX_WORD_COUNT;
|
||||
const isTooLarge = words1.length > MAX_WORD_COUNT || words2.length > MAX_WORD_COUNT;
|
||||
|
||||
let complexMessage = 'One or both of the provided documents are large files, accuracy of comparison may be reduced';
|
||||
let tooLargeMessage = 'One or Both of the provided documents are too large to process';
|
||||
|
||||
// Listen for messages from the main thread
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'SET_TOO_LARGE_MESSAGE') {
|
||||
tooLargeMessage = event.data.message;
|
||||
}
|
||||
if (event.data.type === 'SET_COMPLEX_MESSAGE') {
|
||||
complexMessage = event.data.message;
|
||||
}
|
||||
});
|
||||
|
||||
if (isTooLarge) {
|
||||
self.postMessage({
|
||||
status: 'warning',
|
||||
message: tooLargeMessage,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
|
||||
if (isComplex) {
|
||||
self.postMessage({
|
||||
status: 'warning',
|
||||
message: complexMessage,
|
||||
});
|
||||
}
|
||||
// Perform diff operation depending on document size
|
||||
const differences = isComplex
|
||||
? await staggeredBatchDiff(words1, words2, color1, color2, BATCH_SIZE, OVERLAP_SIZE)
|
||||
: diff(words1, words2, color1, color2);
|
||||
|
||||
console.log(`Diff operation took ${performance.now() - startTime} milliseconds`);
|
||||
self.postMessage({ status: 'success', differences });
|
||||
}
|
||||
};
|
||||
|
||||
//Splits text into smaller batches to run through diff checking algorithms. overlaps the batches to help ensure
|
||||
async function staggeredBatchDiff(words1, words2, color1, color2, batchSize, overlapSize) {
|
||||
const differences = [];
|
||||
const totalWords1 = words1.length;
|
||||
const totalWords2 = words2.length;
|
||||
|
||||
let previousEnd1 = 0; // Track where the last batch ended in words1
|
||||
let previousEnd2 = 0; // Track where the last batch ended in words2
|
||||
|
||||
// Function to determine if differences are large, differences that are too large indicate potential error in batching
|
||||
const isLargeDifference = (differences) => {
|
||||
return differences.length > 50;
|
||||
};
|
||||
|
||||
while (previousEnd1 < totalWords1 || previousEnd2 < totalWords2) {
|
||||
// Define the next chunk boundaries
|
||||
const start1 = previousEnd1;
|
||||
const end1 = Math.min(start1 + batchSize, totalWords1);
|
||||
|
||||
const start2 = previousEnd2;
|
||||
const end2 = Math.min(start2 + batchSize, totalWords2);
|
||||
|
||||
//If difference is too high decrease batch size for more granular check
|
||||
const dynamicBatchSize = isLargeDifference(differences) ? batchSize / 2 : batchSize;
|
||||
|
||||
// Adjust the size of the current chunk using dynamic batch size
|
||||
const batchWords1 = words1.slice(start1, end1 + dynamicBatchSize);
|
||||
const batchWords2 = words2.slice(start2, end2 + dynamicBatchSize);
|
||||
|
||||
// Include overlap from the previous chunk
|
||||
const overlapWords1 = previousEnd1 > 0 ? words1.slice(Math.max(0, previousEnd1 - overlapSize), previousEnd1) : [];
|
||||
const overlapWords2 = previousEnd2 > 0 ? words2.slice(Math.max(0, previousEnd2 - overlapSize), previousEnd2) : [];
|
||||
|
||||
// Combine overlaps and current batches for comparison
|
||||
const combinedWords1 = overlapWords1.concat(batchWords1);
|
||||
const combinedWords2 = overlapWords2.concat(batchWords2);
|
||||
|
||||
// Perform the diff on the combined words
|
||||
const batchDifferences = diff(combinedWords1, combinedWords2, color1, color2);
|
||||
differences.push(...batchDifferences);
|
||||
|
||||
// Update the previous end indices based on the results of this batch
|
||||
previousEnd1 = end1;
|
||||
previousEnd2 = end2;
|
||||
}
|
||||
|
||||
return differences;
|
||||
}
|
||||
|
||||
|
||||
// Standard diff function for small text comparisons
|
||||
function diff(words1, words2, color1, color2) {
|
||||
console.log(`Starting diff between ${words1.length} words and ${words2.length} words`);
|
||||
const matrix = Array.from({ length: words1.length + 1 }, () => Array(words2.length + 1).fill(0));
|
||||
|
||||
for (let i = 1; i <= words1.length; i++) {
|
||||
for (let j = 1; j <= words2.length; j++) {
|
||||
matrix[i][j] = words1[i - 1] === words2[j - 1]
|
||||
? matrix[i - 1][j - 1] + 1
|
||||
: Math.max(matrix[i][j - 1], matrix[i - 1][j]);
|
||||
}
|
||||
}
|
||||
return backtrack(matrix, words1, words2, color1, color2);
|
||||
}
|
||||
|
||||
// Backtrack function to find differences
|
||||
function backtrack(matrix, words1, words2, color1, color2) {
|
||||
let i = words1.length, j = words2.length;
|
||||
const differences = [];
|
||||
|
||||
while (i > 0 || j > 0) {
|
||||
if (i > 0 && j > 0 && words1[i - 1] === words2[j - 1]) {
|
||||
differences.unshift(['black', words1[i - 1]]);
|
||||
i--; j--;
|
||||
} else if (j > 0 && (i === 0 || matrix[i][j] === matrix[i][j - 1])) {
|
||||
differences.unshift([color2, words2[j - 1]]);
|
||||
j--;
|
||||
} else {
|
||||
differences.unshift([color1, words1[i - 1]]);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return differences;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get CSRF token from cookie
|
||||
const getCsrfToken = () => {
|
||||
return document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1];
|
||||
};
|
||||
|
||||
// Function to decode the URI-encoded cookie value
|
||||
const decodeCsrfToken = (token) => {
|
||||
if (token) {
|
||||
return decodeURIComponent(token);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Find all forms and add CSRF token
|
||||
const forms = document.querySelectorAll('form');
|
||||
const csrfToken = decodeCsrfToken(getCsrfToken());
|
||||
|
||||
// Only proceed if we have a cookie-based token
|
||||
if (csrfToken) {
|
||||
forms.forEach(form => {
|
||||
// Only now remove existing CSRF input fields since we have a new token
|
||||
const existingCsrfInputs = form.querySelectorAll('input[name="_csrf"]');
|
||||
existingCsrfInputs.forEach(input => input.remove());
|
||||
|
||||
// Create and add new CSRF input field
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = '_csrf';
|
||||
csrfInput.value = csrfToken;
|
||||
form.appendChild(csrfInput);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
var toggleCount = 0;
|
||||
var lastToggleTime = Date.now();
|
||||
|
||||
var elements = {
|
||||
lightModeStyles: null,
|
||||
darkModeStyles: null,
|
||||
rainbowModeStyles: null,
|
||||
darkModeIcon: null,
|
||||
searchBar: null,
|
||||
formControls: null,
|
||||
navIcons: null,
|
||||
navDropdownMenus: null,
|
||||
};
|
||||
|
||||
function getElements() {
|
||||
elements.lightModeStyles = document.getElementById("light-mode-styles");
|
||||
elements.darkModeStyles = document.getElementById("dark-mode-styles");
|
||||
elements.rainbowModeStyles = document.getElementById("rainbow-mode-styles");
|
||||
elements.darkModeIcon = document.getElementById("dark-mode-icon");
|
||||
elements.searchBar = document.getElementById("searchBar");
|
||||
elements.formControls = document.querySelectorAll(".form-control");
|
||||
elements.navDropdownMenus = document.querySelectorAll(".dropdown-menu");
|
||||
}
|
||||
function setMode(mode) {
|
||||
var event = new CustomEvent("modeChanged", { detail: mode });
|
||||
document.dispatchEvent(event);
|
||||
|
||||
if (elements && elements.lightModeStyles) {
|
||||
elements.lightModeStyles.disabled = mode !== "off";
|
||||
}
|
||||
if (elements && elements.darkModeStyles) {
|
||||
elements.darkModeStyles.disabled = mode !== "on";
|
||||
}
|
||||
if (elements && elements.rainbowModeStyles) {
|
||||
elements.rainbowModeStyles.disabled = mode !== "rainbow";
|
||||
}
|
||||
|
||||
var jumbotron = document.getElementById("jumbotron");
|
||||
|
||||
if (mode === "on") {
|
||||
if (elements && elements.darkModeIcon) {
|
||||
elements.darkModeIcon.textContent = "dark_mode";
|
||||
}
|
||||
var tables = document.querySelectorAll(".table");
|
||||
tables.forEach((table) => {
|
||||
table.classList.add("table-dark");
|
||||
});
|
||||
} else if (mode === "off") {
|
||||
if (elements && elements.darkModeIcon) {
|
||||
elements.darkModeIcon.textContent = "light_mode";
|
||||
}
|
||||
var tables = document.querySelectorAll(".table-dark");
|
||||
tables.forEach((table) => {
|
||||
table.classList.remove("table-dark");
|
||||
});
|
||||
} else if (mode === "rainbow") {
|
||||
if (elements && elements.darkModeIcon) {
|
||||
elements.darkModeIcon.textContent = "looks";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
var currentTime = Date.now();
|
||||
if (currentTime - lastToggleTime < 1000) {
|
||||
toggleCount++;
|
||||
} else {
|
||||
toggleCount = 1;
|
||||
}
|
||||
lastToggleTime = currentTime;
|
||||
|
||||
document.body.classList.add("transition-theme");
|
||||
|
||||
if (toggleCount >= 18) {
|
||||
localStorage.setItem("dark-mode", "rainbow");
|
||||
setMode("rainbow");
|
||||
} else if (localStorage.getItem("dark-mode") == "on") {
|
||||
localStorage.setItem("dark-mode", "off");
|
||||
setMode("off");
|
||||
} else {
|
||||
localStorage.setItem("dark-mode", "on");
|
||||
setMode("on");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
getElements();
|
||||
|
||||
var currentMode = localStorage.getItem("dark-mode");
|
||||
if (currentMode === "on" || currentMode === "off" || currentMode === "rainbow") {
|
||||
setMode(currentMode);
|
||||
} else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
setMode("on");
|
||||
} else {
|
||||
setMode("off");
|
||||
}
|
||||
|
||||
var darkModeToggle = document.getElementById("dark-mode-toggle");
|
||||
if (darkModeToggle !== null) {
|
||||
darkModeToggle.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
toggleDarkMode();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
document.getElementById('download-pdf').addEventListener('click', async () => {
|
||||
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
|
||||
let decryptedFile = modifiedPdf;
|
||||
let isEncrypted = false;
|
||||
let requiresPassword = false;
|
||||
await this.decryptFile
|
||||
.checkFileEncrypted(decryptedFile)
|
||||
.then((result) => {
|
||||
isEncrypted = result.isEncrypted;
|
||||
requiresPassword = result.requiresPassword;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
if (decryptedFile.type === 'application/pdf' && isEncrypted) {
|
||||
decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
|
||||
if (!decryptedFile) {
|
||||
throw new Error('File decryption failed.');
|
||||
}
|
||||
}
|
||||
const modifiedPdfBytes = await modifiedPdf.save();
|
||||
const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = originalFileName + '_signed.pdf';
|
||||
link.click();
|
||||
});
|
||||
@@ -0,0 +1,500 @@
|
||||
(function () {
|
||||
if (window.isDownloadScriptInitialized) return; // Prevent re-execution
|
||||
window.isDownloadScriptInitialized = true;
|
||||
|
||||
const {
|
||||
pdfPasswordPrompt,
|
||||
multipleInputsForSingleRequest,
|
||||
disableMultipleFiles,
|
||||
remoteCall,
|
||||
sessionExpired,
|
||||
refreshPage,
|
||||
error,
|
||||
} = window.stirlingPDF;
|
||||
|
||||
function showErrorBanner(message, stackTrace) {
|
||||
const errorContainer = document.getElementById('errorContainer');
|
||||
errorContainer.style.display = 'block'; // Display the banner
|
||||
errorContainer.querySelector('.alert-heading').textContent = error;
|
||||
errorContainer.querySelector('p').textContent = message;
|
||||
document.querySelector('#traceContent').textContent = stackTrace;
|
||||
}
|
||||
|
||||
function showSessionExpiredPrompt() {
|
||||
const errorContainer = document.getElementById('errorContainer');
|
||||
errorContainer.style.display = 'block';
|
||||
errorContainer.querySelector('.alert-heading').textContent = sessionExpired;
|
||||
errorContainer.querySelector('p').textContent = sessionExpired;
|
||||
document.querySelector('#traceContent').textContent = '';
|
||||
|
||||
// Optional: Add a refresh button
|
||||
const refreshButton = document.createElement('button');
|
||||
refreshButton.textContent = refreshPage;
|
||||
refreshButton.className = 'btn btn-primary mt-3';
|
||||
refreshButton.onclick = () => location.reload();
|
||||
errorContainer.appendChild(refreshButton);
|
||||
}
|
||||
|
||||
let firstErrorOccurred = false;
|
||||
|
||||
$(document).ready(function () {
|
||||
$('form').submit(async function (event) {
|
||||
event.preventDefault();
|
||||
firstErrorOccurred = false;
|
||||
const url = this.action;
|
||||
let files = $('#fileInput-input')[0].files;
|
||||
const uploadLimit = window.stirlingPDF?.uploadLimit ?? 0;
|
||||
if (uploadLimit > 0) {
|
||||
const oversizedFiles = Array.from(files).filter(f => f.size > uploadLimit);
|
||||
if (oversizedFiles.length > 0) {
|
||||
const names = oversizedFiles.map(f => `"${f.name}"`).join(', ');
|
||||
if (names.length === 1) {
|
||||
alert(`${names} ${window.stirlingPDF.uploadLimitExceededSingular} ${window.stirlingPDF.uploadLimitReadable}.`);
|
||||
} else {
|
||||
alert(`${names} ${window.stirlingPDF.uploadLimitExceededPlural} ${window.stirlingPDF.uploadLimitReadable}.`);
|
||||
}
|
||||
files = Array.from(files).filter(f => f.size <= uploadLimit);
|
||||
if (files.length === 0) return;
|
||||
}
|
||||
}
|
||||
const formData = new FormData(this);
|
||||
const submitButton = document.getElementById('submitBtn');
|
||||
const showGameBtn = document.getElementById('show-game-btn');
|
||||
const originalButtonText = submitButton.textContent;
|
||||
var boredWaiting = localStorage.getItem('boredWaiting') || 'disabled';
|
||||
|
||||
if (showGameBtn) {
|
||||
showGameBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Remove empty file entries
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (value instanceof File && !value.name) {
|
||||
formData.delete(key);
|
||||
}
|
||||
}
|
||||
const override = $('#override').val() || '';
|
||||
console.log(override);
|
||||
|
||||
// Set a timeout to show the game button if operation takes more than 5 seconds
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (boredWaiting === 'enabled' && showGameBtn) {
|
||||
showGameBtn.style.display = 'block';
|
||||
showGameBtn.parentNode.insertBefore(document.createElement('br'), showGameBtn.nextSibling);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
if (!url.includes('remove-password')) {
|
||||
// Check if any PDF files are encrypted and handle decryption if necessary
|
||||
const decryptedFiles = await checkAndDecryptFiles(url, files);
|
||||
files = decryptedFiles;
|
||||
}
|
||||
|
||||
submitButton.textContent = 'Processing...';
|
||||
submitButton.disabled = true;
|
||||
|
||||
if (remoteCall === true) {
|
||||
if (override === 'multi' || (!multipleInputsForSingleRequest && files.length > 1 && override !== 'single')) {
|
||||
await submitMultiPdfForm(url, files, this);
|
||||
} else {
|
||||
await handleSingleDownload(url, formData);
|
||||
}
|
||||
}
|
||||
|
||||
//clearFileInput();
|
||||
clearTimeout(timeoutId);
|
||||
if (showGameBtn) {
|
||||
showGameBtn.style.display = 'none';
|
||||
showGameBtn.style.marginTop = '';
|
||||
}
|
||||
submitButton.textContent = originalButtonText;
|
||||
submitButton.disabled = false;
|
||||
|
||||
// After process finishes, check for boredWaiting and gameDialog open status
|
||||
const gameDialog = document.getElementById('game-container-wrapper');
|
||||
if (boredWaiting === 'enabled' && gameDialog && gameDialog.open) {
|
||||
// Display a green banner at the bottom of the screen saying "Download complete"
|
||||
let downloadCompleteText = 'Download Complete';
|
||||
if (window.downloadCompleteText) {
|
||||
downloadCompleteText = window.downloadCompleteText;
|
||||
}
|
||||
$('body').append(
|
||||
'<div id="download-complete-banner" style="position:fixed;bottom:0;left:0;width:100%;background-color:green;color:white;text-align:center;padding:10px;font-size:16px;z-index:1000;">' +
|
||||
downloadCompleteText +
|
||||
'</div>'
|
||||
);
|
||||
setTimeout(function () {
|
||||
$('#download-complete-banner').fadeOut('slow', function () {
|
||||
$(this).remove(); // Remove the banner after fading out
|
||||
});
|
||||
}, 5000); // Banner will fade out after 5 seconds
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if(showGameBtn){
|
||||
showGameBtn.style.display = 'none';
|
||||
}
|
||||
submitButton.textContent = originalButtonText;
|
||||
submitButton.disabled = false;
|
||||
handleDownloadError(error);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function getPDFPageCount(file) {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise;
|
||||
return pdf.numPages;
|
||||
} catch (error) {
|
||||
console.error('Error getting PDF page count:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAndDecryptFiles(url, files) {
|
||||
const decryptedFiles = [];
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
|
||||
// Extract the base URL
|
||||
const baseUrl = new URL(url);
|
||||
let removePasswordUrl = `${baseUrl.origin}`;
|
||||
|
||||
// Check if there's a path before /api/
|
||||
const apiIndex = baseUrl.pathname.indexOf('/api/');
|
||||
if (apiIndex > 0) {
|
||||
removePasswordUrl += baseUrl.pathname.substring(0, apiIndex);
|
||||
}
|
||||
|
||||
// Append the new endpoint
|
||||
removePasswordUrl += '/api/v1/security/remove-password';
|
||||
|
||||
console.log(`Remove password URL: ${removePasswordUrl}`);
|
||||
|
||||
for (const file of files) {
|
||||
console.log(`Processing file: ${file.name}`);
|
||||
if (file.type !== 'application/pdf') {
|
||||
console.log(`Skipping non-PDF file: ${file.name}`);
|
||||
decryptedFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const loadingTask = pdfjsLib.getDocument({data: arrayBuffer});
|
||||
|
||||
console.log(`Attempting to load PDF: ${file.name}`);
|
||||
const pdf = await loadingTask.promise;
|
||||
console.log(`File is not encrypted: ${file.name}`);
|
||||
decryptedFiles.push(file); // If no error, file is not encrypted
|
||||
} catch (error) {
|
||||
if (error.name === 'PasswordException' && error.code === 1) {
|
||||
console.log(`PDF requires password: ${file.name}`, error);
|
||||
console.log(`Attempting to remove password from PDF: ${file.name} with password.`);
|
||||
const password = prompt(`${window.translations.decrypt.passwordPrompt}`);
|
||||
|
||||
if (!password) {
|
||||
console.error(`No password provided for encrypted PDF: ${file.name}`);
|
||||
showErrorBanner(
|
||||
`${window.translations.decrypt.noPassword.replace('{0}', file.name)}`,
|
||||
`${window.translations.decrypt.unexpectedError}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
// Prepare FormData for the decryption request
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
formData.append('password', password);
|
||||
|
||||
// Use handleSingleDownload to send the request
|
||||
const decryptionResult = await fetch(removePasswordUrl, {method: 'POST', body: formData});
|
||||
|
||||
if (decryptionResult && decryptionResult.blob) {
|
||||
const decryptedBlob = await decryptionResult.blob();
|
||||
const decryptedFile = new File([decryptedBlob], file.name, {type: 'application/pdf'});
|
||||
|
||||
/* // Create a link element to download the file
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(decryptedBlob);
|
||||
link.download = 'test.pdf';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
*/
|
||||
decryptedFiles.push(decryptedFile);
|
||||
console.log(`Successfully decrypted PDF: ${file.name}`);
|
||||
} else {
|
||||
throw new Error('Decryption failed: No valid response from server');
|
||||
}
|
||||
} catch (decryptError) {
|
||||
console.error(`Failed to decrypt PDF: ${file.name}`, decryptError);
|
||||
showErrorBanner(
|
||||
`${window.translations.invalidPasswordHeader.replace('{0}', file.name)}`,
|
||||
`${window.translations.invalidPassword}`
|
||||
);
|
||||
throw decryptError;
|
||||
}
|
||||
} else {
|
||||
console.log(`Error loading PDF: ${file.name}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return decryptedFiles;
|
||||
}
|
||||
|
||||
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
|
||||
const startTime = performance.now();
|
||||
const file = formData.get('fileInput');
|
||||
let success = false;
|
||||
let errorMessage = null;
|
||||
|
||||
try {
|
||||
const response = await window.fetchWithCsrf(url, {method: 'POST', body: formData});
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (!response.ok) {
|
||||
errorMessage = response.status;
|
||||
if (response.status === 401) {
|
||||
showSessionExpiredPrompt();
|
||||
return;
|
||||
}
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
console.error('Throwing error banner, response was not okay');
|
||||
return handleJsonResponse(response);
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = getFilenameFromContentDisposition(contentDisposition);
|
||||
|
||||
const blob = await response.blob();
|
||||
success = true;
|
||||
|
||||
if (contentType.includes('application/pdf') || contentType.includes('image/')) {
|
||||
//clearFileInput();
|
||||
return handleResponse(blob, filename, !isMulti, isZip);
|
||||
} else {
|
||||
//clearFileInput();
|
||||
return handleResponse(blob, filename, false, isZip);
|
||||
}
|
||||
} catch (error) {
|
||||
success = false;
|
||||
errorMessage = error.message;
|
||||
console.error('Error in handleSingleDownload:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
const processingTime = performance.now() - startTime;
|
||||
|
||||
// Capture analytics
|
||||
const pageCount = file && file.type === 'application/pdf' ? await getPDFPageCount(file) : null;
|
||||
if (analyticsEnabled) {
|
||||
posthog.capture('file_processing', {
|
||||
success: success,
|
||||
file_type: file ? file.type || 'unknown' : 'unknown',
|
||||
file_size: file ? file.size : 0,
|
||||
processing_time: processingTime,
|
||||
error_message: errorMessage,
|
||||
pdf_pages: pageCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFilenameFromContentDisposition(contentDisposition) {
|
||||
let filename;
|
||||
|
||||
if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) {
|
||||
filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, '')).trim();
|
||||
} else {
|
||||
// If the Content-Disposition header is not present or does not contain the filename, use a default filename
|
||||
filename = 'download';
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
async function handleJsonResponse(response) {
|
||||
const json = await response.json();
|
||||
const errorMessage = JSON.stringify(json, null, 2);
|
||||
if (
|
||||
errorMessage.toLowerCase().includes('the password is incorrect') ||
|
||||
errorMessage.toLowerCase().includes('Password is not provided') ||
|
||||
errorMessage.toLowerCase().includes('PDF contains an encryption dictionary')
|
||||
) {
|
||||
if (!firstErrorOccurred) {
|
||||
firstErrorOccurred = true;
|
||||
alert(pdfPasswordPrompt);
|
||||
}
|
||||
} else {
|
||||
showErrorBanner(json.error + ':' + json.message, json.trace);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponse(blob, filename, considerViewOptions = false, isZip = false) {
|
||||
if (!blob) return;
|
||||
const downloadOption = localStorage.getItem('downloadOption');
|
||||
if (considerViewOptions) {
|
||||
if (downloadOption === 'sameWindow') {
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.location.href = url;
|
||||
return;
|
||||
} else if (downloadOption === 'newWindow') {
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!isZip) {
|
||||
downloadFile(blob, filename);
|
||||
}
|
||||
return {filename, blob};
|
||||
}
|
||||
|
||||
function handleDownloadError(error) {
|
||||
const errorMessage = error.message;
|
||||
showErrorBanner(errorMessage);
|
||||
}
|
||||
|
||||
let urls = []; // An array to hold all the URLs
|
||||
|
||||
function downloadFile(blob, filename) {
|
||||
if (!(blob instanceof Blob)) {
|
||||
console.error('Invalid blob passed to downloadFile function');
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
urls.push(url); // Store the URL so it doesn't get garbage collected too soon
|
||||
|
||||
return {filename, blob};
|
||||
}
|
||||
|
||||
async function submitMultiPdfForm(url, files, form) {
|
||||
const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4;
|
||||
const zipFiles = files.length > zipThreshold;
|
||||
let jszip = null;
|
||||
// Add Space below Progress Bar before Showing
|
||||
$('.progressBarContainer').after($('<br>'));
|
||||
$('.progressBarContainer').show();
|
||||
// Initialize the progress bar
|
||||
|
||||
let progressBar = $('.progressBar');
|
||||
progressBar.css('width', '0%');
|
||||
progressBar.attr('aria-valuenow', 0);
|
||||
progressBar.attr('aria-valuemax', files.length);
|
||||
|
||||
if (zipFiles) {
|
||||
jszip = new JSZip();
|
||||
}
|
||||
|
||||
// Get the form with the method attribute set to POST
|
||||
let postForm = document.querySelector('form[method="POST"]');
|
||||
|
||||
// Get existing form data
|
||||
let formData;
|
||||
if (form) {
|
||||
formData = new FormData(form);
|
||||
} else if (postForm) {
|
||||
formData = new FormData($(postForm)[0]); // Convert the form to a jQuery object and get the raw DOM element
|
||||
} else {
|
||||
console.log('No form with POST method found.');
|
||||
}
|
||||
//Remove file to reuse parameters for other runs
|
||||
formData.delete('fileInput');
|
||||
// Remove empty file entries
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (value instanceof File && !value.name) {
|
||||
formData.delete(key);
|
||||
}
|
||||
}
|
||||
const CONCURRENCY_LIMIT = 8;
|
||||
const chunks = [];
|
||||
for (let i = 0; i < Array.from(files).length; i += CONCURRENCY_LIMIT) {
|
||||
chunks.push(Array.from(files).slice(i, i + CONCURRENCY_LIMIT));
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const promises = chunk.map(async (file) => {
|
||||
let fileFormData = new FormData();
|
||||
fileFormData.append('fileInput', file);
|
||||
for (let [key, value] of fileFormData.entries()) {
|
||||
console.log(key, value);
|
||||
} // Add other form data
|
||||
for (let pair of formData.entries()) {
|
||||
fileFormData.append(pair[0], pair[1]);
|
||||
console.log(pair[0] + ', ' + pair[1]);
|
||||
}
|
||||
|
||||
try {
|
||||
const downloadDetails = await handleSingleDownload(url, fileFormData, true, zipFiles);
|
||||
console.log(downloadDetails);
|
||||
if (zipFiles) {
|
||||
jszip.file(downloadDetails.filename, downloadDetails.blob);
|
||||
} else {
|
||||
//downloadFile(downloadDetails.blob, downloadDetails.filename);
|
||||
}
|
||||
updateProgressBar(progressBar, Array.from(files).length);
|
||||
} catch (error) {
|
||||
handleDownloadError(error);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
if (zipFiles) {
|
||||
try {
|
||||
const content = await jszip.generateAsync({type: 'blob'});
|
||||
downloadFile(content, 'files.zip');
|
||||
} catch (error) {
|
||||
console.error('Error generating ZIP file: ' + error);
|
||||
}
|
||||
}
|
||||
progressBar.css('width', '100%');
|
||||
progressBar.attr('aria-valuenow', Array.from(files).length);
|
||||
}
|
||||
|
||||
function updateProgressBar(progressBar, files) {
|
||||
let progress = (progressBar.attr('aria-valuenow') / files.length) * 100 + 100 / files.length;
|
||||
progressBar.css('width', progress + '%');
|
||||
progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1);
|
||||
}
|
||||
window.addEventListener('unload', () => {
|
||||
for (const url of urls) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear file input after job
|
||||
function clearFileInput() {
|
||||
let pathname = document.location.pathname;
|
||||
if (pathname != '/merge-pdfs') {
|
||||
let formElement = document.querySelector('#fileInput-input');
|
||||
formElement.value = '';
|
||||
let editSectionElement = document.querySelector('#editSection');
|
||||
if (editSectionElement) {
|
||||
editSectionElement.style.display = 'none';
|
||||
}
|
||||
let cropPdfCanvas = document.querySelector('#cropPdfCanvas');
|
||||
let overlayCanvas = document.querySelector('#overlayCanvas');
|
||||
if (cropPdfCanvas && overlayCanvas) {
|
||||
cropPdfCanvas.width = 0;
|
||||
cropPdfCanvas.height = 0;
|
||||
|
||||
overlayCanvas.width = 0;
|
||||
overlayCanvas.height = 0;
|
||||
}
|
||||
} else {
|
||||
console.log("Disabled for 'Merge'");
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,631 @@
|
||||
const DraggableUtils = {
|
||||
boxDragContainer: document.getElementById('box-drag-container'),
|
||||
pdfCanvas: document.getElementById('pdf-canvas'),
|
||||
nextId: 0,
|
||||
pdfDoc: null,
|
||||
pageIndex: 0,
|
||||
elementAllPages: [],
|
||||
documentsMap: new Map(),
|
||||
lastInteracted: null,
|
||||
padding: 15,
|
||||
maintainRatioEnabled: true,
|
||||
init() {
|
||||
interact('.draggable-canvas')
|
||||
.draggable({
|
||||
listeners: {
|
||||
start(event) {
|
||||
const target = event.target;
|
||||
x = parseFloat(target.getAttribute('data-bs-x'));
|
||||
y = parseFloat(target.getAttribute('data-bs-y'));
|
||||
},
|
||||
move: (event) => {
|
||||
const target = event.target;
|
||||
|
||||
// Retrieve position attributes
|
||||
let x = parseFloat(target.getAttribute('data-bs-x')) || 0;
|
||||
let y = parseFloat(target.getAttribute('data-bs-y')) || 0;
|
||||
const angle = parseFloat(target.getAttribute('data-angle')) || 0;
|
||||
|
||||
// Update position based on drag movement
|
||||
x += event.dx;
|
||||
y += event.dy;
|
||||
|
||||
// Apply translation to the parent container (bounding box)
|
||||
target.style.transform = `translate(${x}px, ${y}px)`;
|
||||
|
||||
// Preserve rotation on the inner canvas
|
||||
const canvas = target.querySelector('.display-canvas');
|
||||
|
||||
const canvasWidth = parseFloat(canvas.style.width);
|
||||
const canvasHeight = parseFloat(canvas.style.height);
|
||||
|
||||
const cosAngle = Math.abs(Math.cos(angle));
|
||||
const sinAngle = Math.abs(Math.sin(angle));
|
||||
|
||||
const rotatedWidth = canvasWidth * cosAngle + canvasHeight * sinAngle;
|
||||
const rotatedHeight = canvasWidth * sinAngle + canvasHeight * cosAngle;
|
||||
|
||||
const offsetX = (rotatedWidth - canvasWidth) / 2;
|
||||
const offsetY = (rotatedHeight - canvasHeight) / 2;
|
||||
|
||||
canvas.style.transform = `translate(${offsetX}px, ${offsetY}px) rotate(${angle}rad)`;
|
||||
|
||||
// Update attributes for persistence
|
||||
target.setAttribute('data-bs-x', x);
|
||||
target.setAttribute('data-bs-y', y);
|
||||
|
||||
// Set the last interacted element
|
||||
this.lastInteracted = target;
|
||||
},
|
||||
},
|
||||
})
|
||||
.resizable({
|
||||
edges: { left: true, right: true, bottom: true, top: true },
|
||||
listeners: {
|
||||
start: (event) => {
|
||||
const target = event.target;
|
||||
x = parseFloat(target.getAttribute('data-bs-x')) || 0;
|
||||
y = parseFloat(target.getAttribute('data-bs-y')) || 0;
|
||||
},
|
||||
move: (event) => {
|
||||
const target = event.target;
|
||||
|
||||
const MAX_CHANGE = 60;
|
||||
|
||||
let width = event.rect.width - 2 * this.padding;
|
||||
let height = event.rect.height - 2 * this.padding;
|
||||
|
||||
const canvas = target.querySelector('.display-canvas');
|
||||
if (canvas) {
|
||||
const originalWidth = parseFloat(canvas.style.width) || canvas.width;
|
||||
const originalHeight = parseFloat(canvas.style.height) || canvas.height;
|
||||
const angle = parseFloat(target.getAttribute('data-angle')) || 0;
|
||||
|
||||
const aspectRatio = originalWidth / originalHeight;
|
||||
|
||||
if (!event.ctrlKey && this.maintainRatioEnabled) {
|
||||
if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) {
|
||||
height = width / aspectRatio;
|
||||
} else {
|
||||
width = height * aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
const widthChange = width - originalWidth;
|
||||
const heightChange = height - originalHeight;
|
||||
|
||||
if (Math.abs(widthChange) > MAX_CHANGE || Math.abs(heightChange) > MAX_CHANGE) {
|
||||
const scale = MAX_CHANGE / Math.max(Math.abs(widthChange), Math.abs(heightChange));
|
||||
width = originalWidth + widthChange * scale;
|
||||
height = originalHeight + heightChange * scale;
|
||||
}
|
||||
|
||||
const cosAngle = Math.abs(Math.cos(angle));
|
||||
const sinAngle = Math.abs(Math.sin(angle));
|
||||
const boundingWidth = width * cosAngle + height * sinAngle;
|
||||
const boundingHeight = width * sinAngle + height * cosAngle;
|
||||
|
||||
if (event.edges.left) {
|
||||
const dx = event.deltaRect.left;
|
||||
x += dx;
|
||||
}
|
||||
if (event.edges.top) {
|
||||
const dy = event.deltaRect.top;
|
||||
y += dy;
|
||||
}
|
||||
|
||||
target.style.transform = `translate(${x}px, ${y}px)`;
|
||||
target.style.width = `${boundingWidth + 2 * this.padding}px`;
|
||||
target.style.height = `${boundingHeight + 2 * this.padding}px`;
|
||||
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
canvas.style.transform = `translate(${(boundingWidth - width) / 2}px, ${(boundingHeight - height) / 2
|
||||
}px) rotate(${angle}rad)`;
|
||||
|
||||
target.setAttribute('data-bs-x', x);
|
||||
target.setAttribute('data-bs-y', y);
|
||||
|
||||
this.lastInteracted = target;
|
||||
}
|
||||
},
|
||||
},
|
||||
modifiers: [
|
||||
interact.modifiers.restrictSize({
|
||||
min: { width: 50, height: 50 },
|
||||
}),
|
||||
],
|
||||
inertia: true,
|
||||
});
|
||||
|
||||
//Arrow key Support for Add-Image and Sign pages
|
||||
if (window.location.pathname.endsWith('sign') || window.location.pathname.endsWith('add-image')) {
|
||||
window.addEventListener('keydown', (event) => {
|
||||
//Check for last interacted element
|
||||
if (!this.lastInteracted) {
|
||||
return;
|
||||
}
|
||||
// Get the currently selected element
|
||||
const target = this.lastInteracted;
|
||||
|
||||
// Step size relatively to the elements size
|
||||
const stepX = target.offsetWidth * 0.05;
|
||||
const stepY = target.offsetHeight * 0.05;
|
||||
|
||||
// Get the current x and y coordinates
|
||||
let x = parseFloat(target.getAttribute('data-bs-x')) || 0;
|
||||
let y = parseFloat(target.getAttribute('data-bs-y')) || 0;
|
||||
|
||||
// Check which key was pressed and update the coordinates accordingly
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
y -= stepY;
|
||||
event.preventDefault(); // Prevent the default action
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
y += stepY;
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
x -= stepX;
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
x += stepX;
|
||||
event.preventDefault();
|
||||
break;
|
||||
default:
|
||||
return; // Listen only to arrow keys
|
||||
}
|
||||
|
||||
// Update position
|
||||
const angle = parseFloat(target.getAttribute('data-angle')) || 0;
|
||||
target.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`;
|
||||
target.setAttribute('data-bs-x', x);
|
||||
target.setAttribute('data-bs-y', y);
|
||||
|
||||
DraggableUtils.onInteraction(target);
|
||||
});
|
||||
}
|
||||
},
|
||||
onInteraction(target) {
|
||||
this.lastInteracted = target;
|
||||
// this.boxDragContainer.appendChild(target);
|
||||
// target.appendChild(target.querySelector(".display-canvas"));
|
||||
},
|
||||
createDraggableCanvasFromUrl(dataUrl) {
|
||||
return new Promise((resolve) => {
|
||||
const canvasContainer = document.createElement('div');
|
||||
const createdCanvas = document.createElement('canvas'); // Inner canvas
|
||||
const padding = this.padding;
|
||||
|
||||
canvasContainer.id = `draggable-canvas-${this.nextId++}`;
|
||||
canvasContainer.classList.add('draggable-canvas');
|
||||
createdCanvas.classList.add('display-canvas');
|
||||
|
||||
canvasContainer.style.position = 'absolute';
|
||||
canvasContainer.style.padding = `${padding}px`;
|
||||
canvasContainer.style.overflow = 'hidden';
|
||||
|
||||
let x = 0,
|
||||
y = 30,
|
||||
angle = 0;
|
||||
canvasContainer.style.transform = `translate(${x}px, ${y}px)`;
|
||||
canvasContainer.setAttribute('data-bs-x', x);
|
||||
canvasContainer.setAttribute('data-bs-y', y);
|
||||
canvasContainer.setAttribute('data-angle', angle);
|
||||
|
||||
canvasContainer.addEventListener('click', () => {
|
||||
this.lastInteracted = canvasContainer;
|
||||
this.showRotationControls(canvasContainer);
|
||||
});
|
||||
canvasContainer.appendChild(createdCanvas);
|
||||
this.boxDragContainer.appendChild(canvasContainer);
|
||||
|
||||
const myImage = new Image();
|
||||
myImage.src = dataUrl;
|
||||
myImage.onload = () => {
|
||||
const context = createdCanvas.getContext('2d');
|
||||
|
||||
createdCanvas.width = myImage.width;
|
||||
createdCanvas.height = myImage.height;
|
||||
|
||||
const imgAspect = myImage.width / myImage.height;
|
||||
const containerWidth = this.boxDragContainer.offsetWidth;
|
||||
const containerHeight = this.boxDragContainer.offsetHeight;
|
||||
|
||||
let scaleMultiplier = Math.min(containerWidth / myImage.width, containerHeight / myImage.height);
|
||||
const scaleFactor = 0.5;
|
||||
|
||||
const newWidth = myImage.width * scaleMultiplier * scaleFactor;
|
||||
const newHeight = myImage.height * scaleMultiplier * scaleFactor;
|
||||
|
||||
// Calculate initial bounding box size
|
||||
const cosAngle = Math.abs(Math.cos(angle));
|
||||
const sinAngle = Math.abs(Math.sin(angle));
|
||||
const boundingWidth = newWidth * cosAngle + newHeight * sinAngle;
|
||||
const boundingHeight = newWidth * sinAngle + newHeight * cosAngle;
|
||||
|
||||
createdCanvas.style.width = `${newWidth}px`;
|
||||
createdCanvas.style.height = `${newHeight}px`;
|
||||
|
||||
canvasContainer.style.width = `${boundingWidth + 2 * padding}px`;
|
||||
canvasContainer.style.height = `${boundingHeight + 2 * padding}px`;
|
||||
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.imageSmoothingQuality = 'high';
|
||||
context.drawImage(myImage, 0, 0, myImage.width, myImage.height);
|
||||
this.showRotationControls(canvasContainer);
|
||||
this.lastInteracted = canvasContainer;
|
||||
|
||||
resolve(canvasContainer);
|
||||
};
|
||||
|
||||
myImage.onerror = () => {
|
||||
console.error('Failed to load the image.');
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
},
|
||||
toggleMaintainRatio() {
|
||||
this.maintainRatioEnabled = !this.maintainRatioEnabled;
|
||||
const button = document.getElementById('ratioToggleBtn');
|
||||
if (this.maintainRatioEnabled) {
|
||||
button.classList.remove('btn-danger');
|
||||
button.classList.add('btn-outline-secondary');
|
||||
} else {
|
||||
button.classList.remove('btn-outline-secondary');
|
||||
button.classList.add('btn-danger');
|
||||
}
|
||||
},
|
||||
|
||||
deleteAllDraggableCanvases() {
|
||||
this.boxDragContainer.querySelectorAll('.draggable-canvas').forEach((el) => el.remove());
|
||||
},
|
||||
async addAllPagesDraggableCanvas(element) {
|
||||
if (element) {
|
||||
let currentPage = this.pageIndex;
|
||||
if (!this.elementAllPages.includes(element)) {
|
||||
this.elementAllPages.push(element);
|
||||
element.style.filter = 'sepia(1) hue-rotate(90deg) brightness(1.2)';
|
||||
let newElement = {
|
||||
element: element,
|
||||
offsetWidth: element.width,
|
||||
offsetHeight: element.height,
|
||||
};
|
||||
|
||||
let pagesMap = this.documentsMap.get(this.pdfDoc);
|
||||
|
||||
if (!pagesMap) {
|
||||
pagesMap = {};
|
||||
this.documentsMap.set(this.pdfDoc, pagesMap);
|
||||
}
|
||||
let page = this.pageIndex;
|
||||
|
||||
for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
|
||||
if (pagesMap[`${pageIndex}-offsetWidth`]) {
|
||||
if (!pagesMap[pageIndex].includes(newElement)) {
|
||||
pagesMap[pageIndex].push(newElement);
|
||||
}
|
||||
} else {
|
||||
pagesMap[pageIndex] = [];
|
||||
pagesMap[pageIndex].push(newElement);
|
||||
pagesMap[`${pageIndex}-offsetWidth`] = pagesMap[`${page}-offsetWidth`];
|
||||
pagesMap[`${pageIndex}-offsetHeight`] = pagesMap[`${page}-offsetHeight`];
|
||||
}
|
||||
await this.goToPage(pageIndex);
|
||||
}
|
||||
} else {
|
||||
const index = this.elementAllPages.indexOf(element);
|
||||
if (index !== -1) {
|
||||
this.elementAllPages.splice(index, 1);
|
||||
}
|
||||
element.style.filter = '';
|
||||
let pagesMap = this.documentsMap.get(this.pdfDoc);
|
||||
|
||||
if (!pagesMap) {
|
||||
pagesMap = {};
|
||||
this.documentsMap.set(this.pdfDoc, pagesMap);
|
||||
}
|
||||
for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
|
||||
if (pagesMap[`${pageIndex}-offsetWidth`] && pageIndex != currentPage) {
|
||||
const pageElements = pagesMap[pageIndex];
|
||||
pageElements.forEach((elementPage) => {
|
||||
const elementIndex = pageElements.findIndex((elementPage) => elementPage['element'].id === element.id);
|
||||
if (elementIndex !== -1) {
|
||||
pageElements.splice(elementIndex, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
await this.goToPage(pageIndex);
|
||||
}
|
||||
}
|
||||
await this.goToPage(currentPage);
|
||||
}
|
||||
},
|
||||
deleteDraggableCanvas(element) {
|
||||
if (element) {
|
||||
//Check if deleted element is the last interacted
|
||||
if (this.lastInteracted === element) {
|
||||
// If it is, set lastInteracted to null
|
||||
this.lastInteracted = null;
|
||||
}
|
||||
element.remove();
|
||||
}
|
||||
},
|
||||
getLastInteracted() {
|
||||
return this.lastInteracted;
|
||||
},
|
||||
showRotationControls(element) {
|
||||
const rotationControls = document.getElementById('rotation-controls');
|
||||
const rotationInput = document.getElementById('rotation-input');
|
||||
rotationControls.style.display = 'flex';
|
||||
rotationInput.value = Math.round((parseFloat(element.getAttribute('data-angle')) * 180) / Math.PI);
|
||||
rotationInput.addEventListener('input', this.handleRotationInputChange);
|
||||
},
|
||||
hideRotationControls() {
|
||||
const rotationControls = document.getElementById('rotation-controls');
|
||||
const rotationInput = document.getElementById('rotation-input');
|
||||
rotationControls.style.display = 'none';
|
||||
rotationInput.addEventListener('input', this.handleRotationInputChange);
|
||||
},
|
||||
applyRotationToElement(element, degrees) {
|
||||
const radians = degrees * (Math.PI / 180); // Convert degrees to radians
|
||||
|
||||
// Get current position
|
||||
const x = parseFloat(element.getAttribute('data-bs-x')) || 0;
|
||||
const y = parseFloat(element.getAttribute('data-bs-y')) || 0;
|
||||
|
||||
// Get the inner canvas (image)
|
||||
const canvas = element.querySelector('.display-canvas');
|
||||
if (canvas) {
|
||||
const originalWidth = parseFloat(canvas.style.width);
|
||||
const originalHeight = parseFloat(canvas.style.height);
|
||||
const padding = this.padding; // Access the padding value
|
||||
|
||||
// Calculate rotated bounding box dimensions
|
||||
const cosAngle = Math.abs(Math.cos(radians));
|
||||
const sinAngle = Math.abs(Math.sin(radians));
|
||||
const boundingWidth = originalWidth * cosAngle + originalHeight * sinAngle + 2 * padding;
|
||||
const boundingHeight = originalWidth * sinAngle + originalHeight * cosAngle + 2 * padding;
|
||||
|
||||
// Update parent container to fit the rotated bounding box
|
||||
element.style.width = `${boundingWidth}px`;
|
||||
element.style.height = `${boundingHeight}px`;
|
||||
|
||||
// Center the canvas within the bounding box, accounting for padding
|
||||
const offsetX = (boundingWidth - originalWidth) / 2 - padding;
|
||||
const offsetY = (boundingHeight - originalHeight) / 2 - padding;
|
||||
|
||||
canvas.style.transform = `translate(${offsetX}px, ${offsetY}px) rotate(${radians}rad)`;
|
||||
}
|
||||
|
||||
// Keep the bounding box positioned properly
|
||||
element.style.transform = `translate(${x}px, ${y}px)`;
|
||||
element.setAttribute('data-angle', radians);
|
||||
},
|
||||
handleRotationInputChange() {
|
||||
const rotationInput = document.getElementById('rotation-input');
|
||||
const degrees = parseFloat(rotationInput.value) || 0;
|
||||
DraggableUtils.applyRotationToElement(DraggableUtils.lastInteracted, degrees);
|
||||
},
|
||||
storePageContents() {
|
||||
var pagesMap = this.documentsMap.get(this.pdfDoc);
|
||||
if (!pagesMap) {
|
||||
pagesMap = {};
|
||||
}
|
||||
|
||||
const elements = [...this.boxDragContainer.querySelectorAll('.draggable-canvas')];
|
||||
const draggablesData = elements.map((el) => {
|
||||
return {
|
||||
element: el,
|
||||
offsetWidth: el.offsetWidth,
|
||||
offsetHeight: el.offsetHeight,
|
||||
};
|
||||
});
|
||||
elements.forEach((el) => this.boxDragContainer.removeChild(el));
|
||||
|
||||
pagesMap[this.pageIndex] = draggablesData;
|
||||
pagesMap[this.pageIndex + '-offsetWidth'] = this.pdfCanvas.offsetWidth;
|
||||
pagesMap[this.pageIndex + '-offsetHeight'] = this.pdfCanvas.offsetHeight;
|
||||
|
||||
this.documentsMap.set(this.pdfDoc, pagesMap);
|
||||
},
|
||||
loadPageContents() {
|
||||
var pagesMap = this.documentsMap.get(this.pdfDoc);
|
||||
this.deleteAllDraggableCanvases();
|
||||
if (!pagesMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggablesData = pagesMap[this.pageIndex];
|
||||
if (draggablesData && Array.isArray(draggablesData)) {
|
||||
draggablesData.forEach((draggableData) => this.boxDragContainer.appendChild(draggableData.element));
|
||||
}
|
||||
|
||||
this.documentsMap.set(this.pdfDoc, pagesMap);
|
||||
},
|
||||
|
||||
async renderPage(pdfDocument, pageIdx) {
|
||||
this.pdfDoc = pdfDocument ? pdfDocument : this.pdfDoc;
|
||||
this.pageIndex = pageIdx;
|
||||
|
||||
// persist
|
||||
const page = await this.pdfDoc.getPage(this.pageIndex + 1);
|
||||
|
||||
// set the canvas size to the size of the page
|
||||
if (page.rotate == 90 || page.rotate == 270) {
|
||||
this.pdfCanvas.width = page.view[3];
|
||||
this.pdfCanvas.height = page.view[2];
|
||||
} else {
|
||||
this.pdfCanvas.width = page.view[2];
|
||||
this.pdfCanvas.height = page.view[3];
|
||||
}
|
||||
|
||||
// render the page onto the canvas
|
||||
var renderContext = {
|
||||
canvasContext: this.pdfCanvas.getContext('2d'),
|
||||
viewport: page.getViewport({ scale: 1 }),
|
||||
};
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
//return pdfCanvas.toDataURL();
|
||||
},
|
||||
|
||||
async goToPage(pageIndex) {
|
||||
this.storePageContents();
|
||||
await this.renderPage(this.pdfDoc, pageIndex);
|
||||
this.loadPageContents();
|
||||
},
|
||||
|
||||
async incrementPage() {
|
||||
if (this.pageIndex < this.pdfDoc.numPages - 1) {
|
||||
this.storePageContents();
|
||||
await this.renderPage(this.pdfDoc, this.pageIndex + 1);
|
||||
this.loadPageContents();
|
||||
}
|
||||
},
|
||||
async decrementPage() {
|
||||
if (this.pageIndex > 0) {
|
||||
this.storePageContents();
|
||||
await this.renderPage(this.pdfDoc, this.pageIndex - 1);
|
||||
this.loadPageContents();
|
||||
}
|
||||
},
|
||||
async getOverlayedPdfDocument() {
|
||||
const pdfBytes = await this.pdfDoc.getData();
|
||||
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
this.storePageContents();
|
||||
|
||||
const pagesMap = this.documentsMap.get(this.pdfDoc);
|
||||
|
||||
for (let pageIdx in pagesMap) {
|
||||
if (pageIdx.includes('offset')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const page = pdfDocModified.getPage(parseInt(pageIdx));
|
||||
let draggablesData = pagesMap[pageIdx];
|
||||
|
||||
const offsetWidth = pagesMap[pageIdx + '-offsetWidth'];
|
||||
const offsetHeight = pagesMap[pageIdx + '-offsetHeight'];
|
||||
|
||||
for (const draggableData of draggablesData) {
|
||||
// Embed the draggable canvas
|
||||
const draggableElement = draggableData.element.querySelector('.display-canvas');
|
||||
const response = await fetch(draggableElement.toDataURL());
|
||||
const draggableImgBytes = await response.arrayBuffer();
|
||||
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
|
||||
|
||||
// Extract transformation data
|
||||
const transform = draggableData.element.style.transform || '';
|
||||
const translateRegex = /translate\((-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px\)/;
|
||||
|
||||
const translateMatch = transform.match(translateRegex);
|
||||
|
||||
const translateX = translateMatch ? parseFloat(translateMatch[1]) : 0;
|
||||
const translateY = translateMatch ? parseFloat(translateMatch[2]) : 0;
|
||||
|
||||
const childTransform = draggableElement.style.transform || '';
|
||||
const childTranslateMatch = childTransform.match(translateRegex);
|
||||
|
||||
const childOffsetX = childTranslateMatch ? parseFloat(childTranslateMatch[1]) : 0;
|
||||
const childOffsetY = childTranslateMatch ? parseFloat(childTranslateMatch[2]) : 0;
|
||||
|
||||
const rotateAngle = parseFloat(draggableData.element.getAttribute('data-angle')) || 0;
|
||||
|
||||
const draggablePositionPixels = {
|
||||
x: translateX + childOffsetX + this.padding + 2,
|
||||
y: translateY + childOffsetY + this.padding + 2,
|
||||
width: parseFloat(draggableElement.style.width),
|
||||
height: parseFloat(draggableElement.style.height),
|
||||
angle: rotateAngle, // Store rotation
|
||||
};
|
||||
|
||||
const pageRotation = page.getRotation();
|
||||
|
||||
// Normalize page rotation angle
|
||||
let normalizedAngle = pageRotation.angle % 360;
|
||||
if (normalizedAngle < 0) {
|
||||
normalizedAngle += 360;
|
||||
}
|
||||
|
||||
// Determine the viewed page dimensions based on the normalized rotation angle
|
||||
let viewedPageWidth = (normalizedAngle === 90 || normalizedAngle === 270) ? page.getHeight() : page.getWidth();
|
||||
let viewedPageHeight = (normalizedAngle === 90 || normalizedAngle === 270) ? page.getWidth() : page.getHeight();
|
||||
|
||||
const draggablePositionRelative = {
|
||||
x: draggablePositionPixels.x / offsetWidth,
|
||||
y: draggablePositionPixels.y / offsetHeight,
|
||||
width: draggablePositionPixels.width / offsetWidth,
|
||||
height: draggablePositionPixels.height / offsetHeight,
|
||||
angle: draggablePositionPixels.angle,
|
||||
};
|
||||
|
||||
const draggablePositionPdf = {
|
||||
x: draggablePositionRelative.x * viewedPageWidth,
|
||||
y: draggablePositionRelative.y * viewedPageHeight,
|
||||
width: draggablePositionRelative.width * viewedPageWidth,
|
||||
height: draggablePositionRelative.height * viewedPageHeight,
|
||||
};
|
||||
|
||||
// Calculate position based on normalized page rotation
|
||||
let x = draggablePositionPdf.x;
|
||||
let y = viewedPageHeight - draggablePositionPdf.y - draggablePositionPdf.height;
|
||||
|
||||
if (normalizedAngle === 90) {
|
||||
x = draggablePositionPdf.y;
|
||||
y = draggablePositionPdf.x;
|
||||
} else if (normalizedAngle === 180) {
|
||||
x = viewedPageWidth - draggablePositionPdf.x - draggablePositionPdf.width;
|
||||
y = draggablePositionPdf.y;
|
||||
} else if (normalizedAngle === 270) {
|
||||
x = viewedPageHeight - draggablePositionPdf.y - draggablePositionPdf.height;
|
||||
y = viewedPageWidth - draggablePositionPdf.x - draggablePositionPdf.width;
|
||||
}
|
||||
|
||||
// Convert rotation angle to radians
|
||||
let pageRotationInRadians = PDFLib.degreesToRadians(normalizedAngle);
|
||||
const rotationInRadians = pageRotationInRadians - draggablePositionPixels.angle;
|
||||
|
||||
// Calculate the center of the image
|
||||
const imageCenterX = x + draggablePositionPdf.width / 2;
|
||||
const imageCenterY = y + draggablePositionPdf.height / 2;
|
||||
|
||||
// Apply transformations to rotate the image about its center
|
||||
page.pushOperators(
|
||||
PDFLib.pushGraphicsState(),
|
||||
PDFLib.concatTransformationMatrix(1, 0, 0, 1, imageCenterX, imageCenterY), // Translate to center
|
||||
PDFLib.concatTransformationMatrix(
|
||||
Math.cos(rotationInRadians),
|
||||
Math.sin(rotationInRadians),
|
||||
-Math.sin(rotationInRadians),
|
||||
Math.cos(rotationInRadians),
|
||||
0,
|
||||
0
|
||||
), // Rotate
|
||||
PDFLib.concatTransformationMatrix(1, 0, 0, 1, -imageCenterX, -imageCenterY) // Translate back
|
||||
);
|
||||
|
||||
page.drawImage(pdfImageObject, {
|
||||
x: x,
|
||||
y: y,
|
||||
width: draggablePositionPdf.width,
|
||||
height: draggablePositionPdf.height,
|
||||
});
|
||||
|
||||
// Restore the graphics state
|
||||
page.pushOperators(PDFLib.popGraphicsState());
|
||||
}
|
||||
}
|
||||
|
||||
this.loadPageContents();
|
||||
return pdfDocModified;
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
DraggableUtils.init();
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
var traceVisible = false;
|
||||
|
||||
function toggletrace() {
|
||||
var traceDiv = document.getElementById("trace");
|
||||
if (!traceVisible) {
|
||||
traceDiv.style.maxHeight = "500px";
|
||||
traceVisible = true;
|
||||
} else {
|
||||
traceDiv.style.maxHeight = "0px";
|
||||
traceVisible = false;
|
||||
}
|
||||
adjustContainerHeight();
|
||||
}
|
||||
|
||||
function copytrace() {
|
||||
var flip = false;
|
||||
if (!traceVisible) {
|
||||
toggletrace();
|
||||
flip = true;
|
||||
}
|
||||
var traceContent = document.getElementById("traceContent");
|
||||
var range = document.createRange();
|
||||
range.selectNode(traceContent);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
document.execCommand("copy");
|
||||
window.getSelection().removeAllRanges();
|
||||
if (flip) {
|
||||
toggletrace();
|
||||
}
|
||||
}
|
||||
|
||||
function dismissError() {
|
||||
var errorContainer = document.getElementById("errorContainer");
|
||||
errorContainer.style.display = "none";
|
||||
errorContainer.style.height = "0";
|
||||
}
|
||||
|
||||
function adjustContainerHeight() {
|
||||
var errorContainer = document.getElementById("errorContainer");
|
||||
var traceDiv = document.getElementById("trace");
|
||||
if (traceVisible) {
|
||||
errorContainer.style.height = errorContainer.scrollHeight - traceDiv.scrollHeight + traceDiv.offsetHeight + "px";
|
||||
} else {
|
||||
errorContainer.style.height = "auto";
|
||||
}
|
||||
}
|
||||
function showHelp() {
|
||||
$("#helpModal").modal("show");
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
function updateFavoritesDropdown() {
|
||||
const favoritesList = JSON.parse(localStorage.getItem('favoritesList')) || [];
|
||||
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
var key = localStorage.key(i);
|
||||
var value = localStorage.getItem(key);
|
||||
|
||||
if (value === 'favorite') {
|
||||
const index = favoritesList.indexOf(key);
|
||||
if (index === -1) {
|
||||
favoritesList.push(key);
|
||||
localStorage.removeItem(key);
|
||||
console.log(`Added to favorites: ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dropdown = document.querySelector('#favoritesDropdown');
|
||||
|
||||
if (!dropdown) {
|
||||
console.error('Dropdown element with ID "favoritesDropdown" not found!');
|
||||
return;
|
||||
}
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
var hasFavorites = false;
|
||||
var addedFeatures = new Set();
|
||||
|
||||
for (var i = 0; i < favoritesList.length; i++) {
|
||||
var value = favoritesList[i];
|
||||
if (value) {
|
||||
var navbarEntry = document.querySelector(`a[data-bs-link='${value}']`);
|
||||
if (navbarEntry) {
|
||||
var featureName = navbarEntry.textContent.trim();
|
||||
|
||||
if (!addedFeatures.has(featureName)) {
|
||||
var dropdownItem = document.createElement('div');
|
||||
dropdownItem.className = 'dropdown-item d-flex justify-content-between align-items-center';
|
||||
|
||||
// Create a wrapper for the original content
|
||||
var contentWrapper = document.createElement('div');
|
||||
contentWrapper.className = 'd-flex align-items-center flex-grow-1';
|
||||
contentWrapper.style.textDecoration = 'none';
|
||||
contentWrapper.style.color = 'inherit';
|
||||
|
||||
// Clone the original content
|
||||
var originalContent = navbarEntry.querySelector('div').cloneNode(true);
|
||||
contentWrapper.appendChild(originalContent);
|
||||
|
||||
// Create the remove button
|
||||
var removeButton = document.createElement('button');
|
||||
removeButton.className = 'btn btn-sm btn-link p-0 ml-2';
|
||||
removeButton.innerHTML = '<i class="material-symbols-rounded close-icon" style="font-size: 18px;">close</i>';
|
||||
removeButton.onclick = function (itemKey, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
addToFavorites(itemKey);
|
||||
updateFavoritesDropdown();
|
||||
}.bind(null, value);
|
||||
|
||||
// Add click event to the content wrapper
|
||||
contentWrapper.onclick = function (itemHref, event) {
|
||||
event.preventDefault();
|
||||
window.location.href = itemHref;
|
||||
}.bind(null, navbarEntry.href);
|
||||
|
||||
dropdownItem.appendChild(contentWrapper);
|
||||
dropdownItem.appendChild(removeButton);
|
||||
dropdown.appendChild(dropdownItem);
|
||||
hasFavorites = true;
|
||||
addedFeatures.add(featureName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`Navbar entry not found for : ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasFavorites) {
|
||||
var defaultItem = document.createElement('a');
|
||||
defaultItem.className = 'dropdown-item';
|
||||
defaultItem.textContent = noFavourites || 'No favorites added';
|
||||
dropdown.appendChild(defaultItem);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFavoriteIcons() {
|
||||
const favoritesList = JSON.parse(localStorage.getItem('favoritesList')) || [];
|
||||
|
||||
// Select all favorite icons
|
||||
document.querySelectorAll('.favorite-icon').forEach((icon) => {
|
||||
const endpoint = icon.getAttribute('data-endpoint');
|
||||
const parent = icon.closest('.dropdown-item');
|
||||
|
||||
// Determine if the icon belongs to groupRecent or groupFavorites
|
||||
const isInGroupRecent = parent?.closest('#groupRecent') !== null;
|
||||
const isInGroupFavorites = parent?.closest('#groupFavorites') !== null;
|
||||
|
||||
if (isInGroupRecent) {
|
||||
icon.style.display = 'none';
|
||||
} else if (isInGroupFavorites) {
|
||||
icon.textContent = 'close_small';
|
||||
icon.style.color = 'palevioletred';
|
||||
} else {
|
||||
icon.textContent = favoritesList.includes(endpoint) ? 'close_small' : 'add';
|
||||
icon.className = favoritesList.includes(endpoint)
|
||||
? 'material-symbols-rounded favorite-icon close-icon'
|
||||
: 'material-symbols-rounded favorite-icon add-icon';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addToFavorites(entryId) {
|
||||
if (entryId) {
|
||||
const favoritesList = JSON.parse(localStorage.getItem('favoritesList')) || [];
|
||||
const index = favoritesList.indexOf(entryId);
|
||||
|
||||
if (index === -1) {
|
||||
favoritesList.push(entryId);
|
||||
console.log(`Added to favorites: ${entryId}`);
|
||||
} else {
|
||||
favoritesList.splice(index, 1);
|
||||
console.log(`Removed from favorites: ${entryId}`);
|
||||
}
|
||||
|
||||
localStorage.setItem('favoritesList', JSON.stringify(favoritesList));
|
||||
updateFavoritesDropdown();
|
||||
updateFavoriteIcons();
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath.includes('home-legacy')) {
|
||||
syncFavoritesLegacy();
|
||||
} else {
|
||||
initializeCards();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
window.fetchWithCsrf = async function(url, options = {}) {
|
||||
function getCsrfToken() {
|
||||
const cookieValue = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (cookieValue) {
|
||||
return cookieValue;
|
||||
}
|
||||
const csrfElement = document.querySelector('input[name="_csrf"]');
|
||||
return csrfElement ? csrfElement.value : null;
|
||||
}
|
||||
|
||||
// Create a new options object to avoid modifying the passed object
|
||||
const fetchOptions = { ...options };
|
||||
|
||||
// Ensure headers object exists
|
||||
fetchOptions.headers = { ...options.headers };
|
||||
|
||||
// Add CSRF token if available
|
||||
const csrfToken = getCsrfToken();
|
||||
if (csrfToken) {
|
||||
fetchOptions.headers['X-XSRF-TOKEN'] = csrfToken;
|
||||
}
|
||||
|
||||
return fetch(url, fetchOptions);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
class FileIconFactory {
|
||||
static createFileIcon(fileExtension) {
|
||||
let ext = fileExtension.toLowerCase();
|
||||
switch (ext) {
|
||||
case "pdf":
|
||||
return this.createPDFIcon();
|
||||
case "csv":
|
||||
return this.createCSVIcon();
|
||||
case "jpe":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "gif":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "ico":
|
||||
case "svg":
|
||||
case "svgz":
|
||||
case "tif":
|
||||
case "tiff":
|
||||
case "ai":
|
||||
case "drw":
|
||||
case "pct":
|
||||
case "psp":
|
||||
case "xcf":
|
||||
case "psd":
|
||||
case "raw":
|
||||
case "webp":
|
||||
case "heic":
|
||||
return this.createImageIcon();
|
||||
default:
|
||||
return this.createUnknownFileIcon();
|
||||
}
|
||||
}
|
||||
|
||||
static createPDFIcon() {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-filetype-pdf" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM1.6 11.85H0v3.999h.791v-1.342h.803q.43 0 .732-.173.305-.175.463-.474a1.4 1.4 0 0 0 .161-.677q0-.375-.158-.677a1.2 1.2 0 0 0-.46-.477q-.3-.18-.732-.179m.545 1.333a.8.8 0 0 1-.085.38.57.57 0 0 1-.238.241.8.8 0 0 1-.375.082H.788V12.48h.66q.327 0 .512.181.185.183.185.522m1.217-1.333v3.999h1.46q.602 0 .998-.237a1.45 1.45 0 0 0 .595-.689q.196-.45.196-1.084 0-.63-.196-1.075a1.43 1.43 0 0 0-.589-.68q-.396-.234-1.005-.234zm.791.645h.563q.371 0 .609.152a.9.9 0 0 1 .354.454q.118.302.118.753a2.3 2.3 0 0 1-.068.592 1.1 1.1 0 0 1-.196.422.8.8 0 0 1-.334.252 1.3 1.3 0 0 1-.483.082h-.563zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638z"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static createImageIcon() {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M216-144q-30 0-51-21.5T144-216v-528q0-29 21-50.5t51-21.5h528q30 0 51 21.5t21 50.5v528q0 29-21 50.5T744-144H216Zm48-144h432L552-480 444-336l-72-96-108 144Z"/></svg>`;
|
||||
}
|
||||
|
||||
static createUnknownFileIcon() {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M263.72-96Q234-96 213-117.15T192-168v-624q0-29.7 21.15-50.85Q234.3-864 264-864h312l192 192v504q0 29.7-21.16 50.85Q725.68-96 695.96-96H263.72ZM528-624h168L528-792v168Z"/></svg>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileIconFactory;
|
||||
@@ -0,0 +1,31 @@
|
||||
class FileUtils {
|
||||
static extractFileExtension(filename) {
|
||||
if (!filename || filename.trim().length <= 0) return "";
|
||||
let trimmedName = filename.trim();
|
||||
return trimmedName.substring(trimmedName.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
static transformFileSize(size) {
|
||||
if (!size) return `0Bs`;
|
||||
let oneKB = 1024;
|
||||
let oneMB = oneKB * 1024;
|
||||
let oneGB = oneMB * 1024;
|
||||
let oneTB = oneGB * 1024;
|
||||
|
||||
if (size < oneKB) return `${this._toFixed(size)}Bs`;
|
||||
else if (oneKB <= size && size < oneMB) return `${this._toFixed(size / oneKB)}KBs`;
|
||||
else if (oneMB <= size && size < oneGB) return `${this._toFixed(size / oneMB)}MBs`;
|
||||
else if (oneGB <= size && size < oneTB) return `${this._toFixed(size / oneGB)}GBs`;
|
||||
else return `${this._toFixed(size / oneTB)}TBs`;
|
||||
}
|
||||
|
||||
static _toFixed(val, digits = 1) {
|
||||
// Return value without ending 0s after decimal point
|
||||
// Example: if res == 145.0 then return 145, else if 145.x (where x != 0) return 145.x
|
||||
let res = val.toFixed(digits);
|
||||
let resRounded = (res|0);
|
||||
return res == resRounded ? resRounded : res;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileUtils;
|
||||
@@ -0,0 +1,549 @@
|
||||
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 => ((typeof(file.type) != undefined) && 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();
|
||||
|
||||
const uploadLimit = window.stirlingPDF?.uploadLimit ?? 0;
|
||||
if (uploadLimit > 0) {
|
||||
const oversizedFiles = allFiles.filter(f => f.size > uploadLimit);
|
||||
if (oversizedFiles.length > 0) {
|
||||
const names = oversizedFiles.map(f => `"${f.name}"`).join(', ');
|
||||
if (names.length === 1) {
|
||||
alert(`${names} ${window.stirlingPDF.uploadLimitExceededSingular} ${window.stirlingPDF.uploadLimitReadable}.`);
|
||||
} else {
|
||||
alert(`${names} ${window.stirlingPDF.uploadLimitExceededPlural} ${window.stirlingPDF.uploadLimitReadable}.`);
|
||||
}
|
||||
allFiles = allFiles.filter(f => f.size <= uploadLimit);
|
||||
const dataTransfer = new DataTransfer();
|
||||
allFiles.forEach(f => dataTransfer.items.add(f));
|
||||
input.files = dataTransfer.files;
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
inputContainer.querySelector('#fileInputText').innerHTML = originalText;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
function initializeGame() {
|
||||
const gameContainer = document.getElementById("game-container");
|
||||
const player = document.getElementById("player");
|
||||
|
||||
let playerSize = gameContainer.clientWidth * 0.0625; // 5% of container width
|
||||
player.style.width = playerSize + "px";
|
||||
player.style.height = playerSize + "px";
|
||||
|
||||
let playerX = gameContainer.clientWidth / 2 - playerSize / 2;
|
||||
let playerY = gameContainer.clientHeight * 0.1;
|
||||
const scoreElement = document.getElementById("score");
|
||||
const levelElement = document.getElementById("level");
|
||||
const livesElement = document.getElementById("lives");
|
||||
const highScoreElement = document.getElementById("high-score");
|
||||
|
||||
let pdfSize = gameContainer.clientWidth * 0.0625; // 5% of container width
|
||||
let projectileWidth = gameContainer.clientWidth * 0.00625; // 0.00625; // 0.5% of container width
|
||||
let projectileHeight = gameContainer.clientHeight * 0.01667; // 1% of container height
|
||||
|
||||
let paused = false;
|
||||
|
||||
const fireRate = 200; // Time between shots in milliseconds
|
||||
let lastProjectileTime = 0;
|
||||
let lives = 3;
|
||||
|
||||
let highScore = localStorage.getItem("highScore") ? parseInt(localStorage.getItem("highScore")) : 0;
|
||||
updateHighScore();
|
||||
|
||||
const PLAYER_MOVE_SPEED = 5;
|
||||
const BASE_PDF_SPEED = 1;
|
||||
const LEVEL_INCREASE_PDF_SPEED = 0.2;
|
||||
const BASE_SPAWN_INTERVAL_MS = 1250; // milliseconds before a new enemy spawns
|
||||
const LEVEL_INCREASE_FACTOR_MS = 25; // milliseconds to decrease the spawn interval per level
|
||||
const MAX_SPAWN_RATE_REDUCTION_MS = 800; // Max milliseconds from the base spawn interval
|
||||
|
||||
|
||||
let keysPressed = {};
|
||||
const pdfs = [];
|
||||
const projectiles = [];
|
||||
let score = 0;
|
||||
let level = 1;
|
||||
let pdfSpeed = BASE_PDF_SPEED;
|
||||
let gameOver = false;
|
||||
|
||||
|
||||
function handleKeys() {
|
||||
if (keysPressed["ArrowLeft"]) {
|
||||
playerX -= PLAYER_MOVE_SPEED;
|
||||
playerX = Math.max(0, playerX)
|
||||
}
|
||||
if (keysPressed["ArrowRight"]) {
|
||||
playerX += PLAYER_MOVE_SPEED;
|
||||
playerX = Math.min(gameContainer.clientWidth - playerSize, playerX);
|
||||
}
|
||||
if (keysPressed[" "] && !gameOver) {
|
||||
const currentTime = new Date().getTime();
|
||||
if (currentTime - lastProjectileTime >= fireRate) {
|
||||
shootProjectile();
|
||||
lastProjectileTime = currentTime;
|
||||
}
|
||||
}
|
||||
updatePlayerPosition();
|
||||
}
|
||||
|
||||
function onKeydown(event) {
|
||||
if (event.key === " ") {
|
||||
event.preventDefault();
|
||||
}
|
||||
keysPressed[event.key] = true;
|
||||
handleKeys();
|
||||
}
|
||||
function onKeyUp(event) {
|
||||
keysPressed[event.key] = false;
|
||||
}
|
||||
|
||||
document.removeEventListener("keydown", onKeydown);
|
||||
document.removeEventListener("keyup", onKeyUp);
|
||||
document.addEventListener("keydown", onKeydown);
|
||||
document.addEventListener("keyup", onKeyUp);
|
||||
|
||||
function updatePlayerPosition() {
|
||||
player.style.left = playerX + "px";
|
||||
player.style.bottom = playerY + "px";
|
||||
}
|
||||
|
||||
function updateLives() {
|
||||
livesElement.textContent = "Lives: " + lives;
|
||||
}
|
||||
|
||||
function updateHighScore() {
|
||||
highScoreElement.textContent = "High Score: " + highScore;
|
||||
}
|
||||
|
||||
function shootProjectile() {
|
||||
const projectile = document.createElement("div");
|
||||
projectile.classList.add("projectile");
|
||||
projectile.style.backgroundColor = "black";
|
||||
projectile.style.width = projectileWidth + "px";
|
||||
projectile.style.height = projectileHeight + "px";
|
||||
projectile.style.left = playerX + playerSize / 2 - projectileWidth / 2 + "px";
|
||||
projectile.style.top = gameContainer.clientHeight - playerY - playerSize + "px";
|
||||
gameContainer.appendChild(projectile);
|
||||
projectiles.push(projectile);
|
||||
}
|
||||
|
||||
function spawnPdf() {
|
||||
const pdf = document.createElement("img");
|
||||
pdf.src = "images/file-earmark-pdf.svg";
|
||||
pdf.classList.add("pdf");
|
||||
pdf.style.width = pdfSize + "px";
|
||||
pdf.style.height = pdfSize + "px";
|
||||
pdf.style.left = Math.floor(Math.random() * (gameContainer.clientWidth - (2*pdfSize))) + pdfSize + "px";
|
||||
pdf.style.top = "0px";
|
||||
gameContainer.appendChild(pdf);
|
||||
pdfs.push(pdf);
|
||||
}
|
||||
|
||||
function resetEnemies() {
|
||||
pdfs.forEach((pdf) => gameContainer.removeChild(pdf));
|
||||
pdfs.length = 0;
|
||||
}
|
||||
|
||||
function updateGame() {
|
||||
if (gameOver || paused) return;
|
||||
|
||||
handleKeys();
|
||||
for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) {
|
||||
const pdf = pdfs[pdfIndex];
|
||||
const pdfY = parseFloat(pdf.style.top) + pdfSpeed;
|
||||
if (pdfY + 50 > gameContainer.clientHeight) {
|
||||
gameContainer.removeChild(pdf);
|
||||
pdfs.splice(pdfIndex, 1);
|
||||
|
||||
// Deduct 2 points when a PDF gets past the player
|
||||
score -= 0;
|
||||
updateScore();
|
||||
|
||||
// Decrease lives and check if game over
|
||||
lives--;
|
||||
updateLives();
|
||||
if (lives <= 0) {
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
pdf.style.top = pdfY + "px";
|
||||
|
||||
// Check for collision with player
|
||||
if (collisionDetected(player, pdf)) {
|
||||
lives--;
|
||||
updateLives();
|
||||
resetEnemies();
|
||||
if (lives <= 0) {
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
projectiles.forEach((projectile, projectileIndex) => {
|
||||
const projectileY = parseInt(projectile.style.top) - 10;
|
||||
if (projectileY < 0) {
|
||||
gameContainer.removeChild(projectile);
|
||||
projectiles.splice(projectileIndex, 1);
|
||||
} else {
|
||||
projectile.style.top = projectileY + "px";
|
||||
}
|
||||
|
||||
for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) {
|
||||
const pdf = pdfs[pdfIndex];
|
||||
if (collisionDetected(projectile, pdf)) {
|
||||
gameContainer.removeChild(pdf);
|
||||
gameContainer.removeChild(projectile);
|
||||
pdfs.splice(pdfIndex, 1);
|
||||
projectiles.splice(projectileIndex, 1);
|
||||
score = score + 10;
|
||||
updateScore();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(updateGame, 1000 / 60);
|
||||
}
|
||||
|
||||
function resetGame() {
|
||||
playerX = gameContainer.clientWidth / 2;
|
||||
playerY = 50;
|
||||
updatePlayerPosition();
|
||||
|
||||
pdfs.forEach((pdf) => gameContainer.removeChild(pdf));
|
||||
projectiles.forEach((projectile) => gameContainer.removeChild(projectile));
|
||||
|
||||
pdfs.length = 0;
|
||||
projectiles.length = 0;
|
||||
|
||||
score = 0;
|
||||
level = 1;
|
||||
lives = 3;
|
||||
|
||||
gameOver = false;
|
||||
|
||||
updateScore();
|
||||
updateLives();
|
||||
levelElement.textContent = "Level: " + level;
|
||||
pdfSpeed = BASE_PDF_SPEED;
|
||||
clearTimeout(spawnPdfTimeout); // Clear the existing spawnPdfTimeout
|
||||
setTimeout(updateGame, 1000 / 60);
|
||||
spawnPdfInterval();
|
||||
}
|
||||
|
||||
function updateScore() {
|
||||
scoreElement.textContent = "Score: " + score;
|
||||
checkLevelUp();
|
||||
}
|
||||
|
||||
function checkLevelUp() {
|
||||
const newLevel = Math.floor(score / 100) + 1;
|
||||
if (newLevel > level) {
|
||||
level = newLevel;
|
||||
levelElement.textContent = "Level: " + level;
|
||||
pdfSpeed += LEVEL_INCREASE_PDF_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
function collisionDetected(a, b) {
|
||||
const rectA = a.getBoundingClientRect();
|
||||
const rectB = b.getBoundingClientRect();
|
||||
return rectA.left < rectB.right && rectA.right > rectB.left && rectA.top < rectB.bottom && rectA.bottom > rectB.top;
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
gameOver = true;
|
||||
if (score > highScore) {
|
||||
highScore = score;
|
||||
localStorage.setItem("highScore", highScore);
|
||||
updateHighScore();
|
||||
}
|
||||
alert("Game Over! Your final score is: " + score);
|
||||
document.getElementById("game-container-wrapper").close();
|
||||
}
|
||||
|
||||
let spawnPdfTimeout;
|
||||
|
||||
|
||||
|
||||
function spawnPdfInterval() {
|
||||
if (gameOver || paused) {
|
||||
clearTimeout(spawnPdfTimeout);
|
||||
return;
|
||||
}
|
||||
spawnPdf();
|
||||
let spawnRateReduction = Math.min(level * LEVEL_INCREASE_FACTOR_MS, MAX_SPAWN_RATE_REDUCTION_MS);
|
||||
let spawnRate = BASE_SPAWN_INTERVAL_MS - spawnRateReduction;
|
||||
spawnPdfTimeout = setTimeout(spawnPdfInterval, spawnRate);
|
||||
}
|
||||
|
||||
updatePlayerPosition();
|
||||
updateGame();
|
||||
spawnPdfInterval();
|
||||
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (document.hidden) {
|
||||
paused = true;
|
||||
} else {
|
||||
paused = false;
|
||||
updateGame();
|
||||
spawnPdfInterval();
|
||||
}
|
||||
});
|
||||
|
||||
window.resetGame = resetGame;
|
||||
}
|
||||
|
||||
window.initializeGame = initializeGame;
|
||||
@@ -0,0 +1,104 @@
|
||||
function compareVersions(version1, version2) {
|
||||
const v1 = version1.split(".");
|
||||
const v2 = version2.split(".");
|
||||
|
||||
for (let i = 0; i < v1.length || i < v2.length; i++) {
|
||||
const n1 = parseInt(v1[i]) || 0;
|
||||
const n2 = parseInt(v2[i]) || 0;
|
||||
|
||||
if (n1 > n2) {
|
||||
return 1;
|
||||
} else if (n1 < n2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function getLatestReleaseVersion() {
|
||||
const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
return data.tag_name ? data.tag_name.substring(1) : "";
|
||||
} else {
|
||||
// If the status is not 200, try to get the version from build.gradle
|
||||
return await getCurrentVersionFromBypass();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch latest version from GitHub:", error);
|
||||
// If an error occurs, try to get the version from build.gradle
|
||||
return await getCurrentVersionFromBypass();
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentVersionFromBypass() {
|
||||
const url = "https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/master/build.gradle";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.status === 200) {
|
||||
const text = await response.text();
|
||||
const versionRegex = /version\s*=\s*['"](\d+\.\d+\.\d+)['"]/;
|
||||
const match = versionRegex.exec(text);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
throw new Error("Version number not found");
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch latest version from build.gradle:", error);
|
||||
return ""; // Return an empty string if the fetch fails
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdate() {
|
||||
// Initialize the update button as hidden
|
||||
var updateBtn = document.getElementById("update-btn") || null;
|
||||
var updateLink = document.getElementById("update-link") || null;
|
||||
var updateLinkLegacy = document.getElementById("update-link-legacy") || null;
|
||||
if (updateBtn !== null) {
|
||||
updateBtn.style.display = "none";
|
||||
}
|
||||
if (updateLink !== null) {
|
||||
updateLink.style.display = "none";
|
||||
}
|
||||
if (updateLinkLegacy !== null) {
|
||||
console.log("hidden!");
|
||||
if (!updateLinkLegacy.classList.contains("visually-hidden")) {
|
||||
updateLinkLegacy.classList.add("visually-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
const latestVersion = await getLatestReleaseVersion();
|
||||
console.log("latestVersion=" + latestVersion);
|
||||
console.log("currentVersion=" + currentVersion);
|
||||
console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion));
|
||||
if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
|
||||
if (updateBtn != null) {
|
||||
document.getElementById("update-btn").style.display = "block";
|
||||
}
|
||||
if (updateLink !== null) {
|
||||
document.getElementById("update-link").style.display = "flex";
|
||||
}
|
||||
if (updateLinkLegacy !== null) {
|
||||
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>');
|
||||
if (updateLinkLegacy.classList.contains("visually-hidden")) {
|
||||
updateLinkLegacy.classList.remove("visually-hidden");
|
||||
}
|
||||
}
|
||||
console.log("visible");
|
||||
} else {
|
||||
if (updateLinkLegacy !== null) {
|
||||
if (!updateLinkLegacy.classList.contains("visually-hidden")) {
|
||||
updateLinkLegacy.classList.add("visually-hidden");
|
||||
}
|
||||
}
|
||||
console.log("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
checkForUpdate();
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
const SCOPES = "https://www.googleapis.com/auth/drive.readonly";
|
||||
const SESSION_STORAGE_ID = "googleDrivePickerAccessToken";
|
||||
|
||||
let tokenClient;
|
||||
let accessToken = sessionStorage.getItem(SESSION_STORAGE_ID);
|
||||
|
||||
let isScriptExecuted = false;
|
||||
if (!isScriptExecuted) {
|
||||
isScriptExecuted = true;
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll(".google-drive-button").forEach(setupGoogleDrivePicker);
|
||||
});
|
||||
}
|
||||
|
||||
function gisLoaded() {
|
||||
tokenClient = google.accounts.oauth2.initTokenClient({
|
||||
client_id: window.stirlingPDF.GoogleDriveClientId,
|
||||
scope: SCOPES,
|
||||
callback: "", // defined later
|
||||
});
|
||||
}
|
||||
|
||||
// add more as needed.
|
||||
// Google picker is limited on what mimeTypes are supported
|
||||
// Wild card are not supported
|
||||
const expandableMimeTypes = {
|
||||
"image/*" : ["image/jpeg", "image/png","image/svg+xml" ]
|
||||
}
|
||||
|
||||
function fileInputToGooglePickerMimeTypes(accept) {
|
||||
|
||||
if(accept == null || accept == "" || accept.includes("*/*")){
|
||||
|
||||
// Setting null will accept all supported mimetypes
|
||||
return null;
|
||||
}
|
||||
|
||||
let mimeTypes = [];
|
||||
accept.split(',').forEach(part => {
|
||||
if(!(part in expandableMimeTypes)){
|
||||
mimeTypes.push(part);
|
||||
return;
|
||||
}
|
||||
|
||||
expandableMimeTypes[part].forEach(mimeType => {
|
||||
mimeTypes.push(mimeType);
|
||||
});
|
||||
});
|
||||
|
||||
const mimeString = mimeTypes.join(",").replace(/\s+/g, '');
|
||||
console.log([accept, "became", mimeString]);
|
||||
return mimeString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback after api.js is loaded.
|
||||
*/
|
||||
function gapiLoaded() {
|
||||
gapi.load("client:picker", initializePicker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback after the API client is loaded. Loads the
|
||||
* discovery doc to initialize the API.
|
||||
*/
|
||||
async function initializePicker() {
|
||||
await gapi.client.load("https://www.googleapis.com/discovery/v1/apis/drive/v3/rest");
|
||||
}
|
||||
|
||||
function setupGoogleDrivePicker(picker) {
|
||||
|
||||
const name = picker.getAttribute('data-name');
|
||||
const accept = picker.getAttribute('data-accept');
|
||||
const multiple = picker.getAttribute('data-multiple') === "true";
|
||||
const mimeTypes = fileInputToGooglePickerMimeTypes(accept);
|
||||
|
||||
picker.addEventListener("click", onGoogleDriveButtonClick);
|
||||
|
||||
function onGoogleDriveButtonClick(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
tokenClient.callback = (response) => {
|
||||
if (response.error !== undefined) {
|
||||
throw response;
|
||||
}
|
||||
accessToken = response.access_token;
|
||||
sessionStorage.setItem(SESSION_STORAGE_ID, accessToken);
|
||||
createGooglePicker();
|
||||
};
|
||||
|
||||
tokenClient.requestAccessToken({ prompt: accessToken === null ? "consent" : "" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out the user upon button click.
|
||||
*/
|
||||
function signOut() {
|
||||
if (accessToken) {
|
||||
sessionStorage.removeItem(SESSION_STORAGE_ID);
|
||||
google.accounts.oauth2.revoke(accessToken);
|
||||
accessToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createGooglePicker() {
|
||||
let builder = new google.picker.PickerBuilder()
|
||||
.setDeveloperKey(window.stirlingPDF.GoogleDriveApiKey)
|
||||
.setAppId(window.stirlingPDF.GoogleDriveAppId)
|
||||
.setOAuthToken(accessToken)
|
||||
.addView(
|
||||
new google.picker.DocsView()
|
||||
.setIncludeFolders(true)
|
||||
.setMimeTypes(mimeTypes)
|
||||
)
|
||||
.addView(
|
||||
new google.picker.DocsView()
|
||||
.setIncludeFolders(true)
|
||||
.setEnableDrives(true)
|
||||
.setMimeTypes(mimeTypes)
|
||||
)
|
||||
.setCallback(pickerCallback);
|
||||
|
||||
if(multiple) {
|
||||
builder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
|
||||
}
|
||||
const picker = builder.build();
|
||||
|
||||
picker.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the file details of the user's selection.
|
||||
* @param {object} data - Containers the user selection from the picker
|
||||
*/
|
||||
async function pickerCallback(data) {
|
||||
if (data.action === google.picker.Action.PICKED) {
|
||||
const files = await Promise.all(
|
||||
data[google.picker.Response.DOCUMENTS].map(async (pickedFile) => {
|
||||
const fileId = pickedFile[google.picker.Document.ID];
|
||||
console.log(fileId);
|
||||
const res = await gapi.client.drive.files.get({
|
||||
fileId: fileId,
|
||||
alt: "media",
|
||||
});
|
||||
|
||||
let file = new File([new Uint8Array(res.body.length).map((_, i) => res.body.charCodeAt(i))], pickedFile.name, {
|
||||
type: pickedFile.mimeType,
|
||||
lastModified: pickedFile.lastModified,
|
||||
endings: pickedFile.endings,
|
||||
});
|
||||
return file;
|
||||
})
|
||||
);
|
||||
|
||||
document.body.dispatchEvent(new CustomEvent(name+"GoogleDriveDrivePicked", { detail: files }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
function filterCardsLegacy() {
|
||||
var input = document.getElementById('searchBar');
|
||||
var filter = input.value.toUpperCase();
|
||||
|
||||
let featureGroups = document.querySelectorAll('.feature-group-legacy');
|
||||
const collapsedGroups = getCollapsedGroups();
|
||||
|
||||
for (const featureGroup of featureGroups) {
|
||||
var cards = featureGroup.querySelectorAll('.feature-card');
|
||||
|
||||
let groupMatchesFilter = false;
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
var card = cards[i];
|
||||
var title = card.querySelector('h5.card-title').innerText;
|
||||
var text = card.querySelector('p.card-text').innerText;
|
||||
|
||||
// Get the navbar tags associated with the card
|
||||
var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`);
|
||||
var navbarTags = navbarItem ? navbarItem.getAttribute('data-bs-tags') : '';
|
||||
|
||||
var content = title + ' ' + text + ' ' + navbarTags;
|
||||
|
||||
if (content.toUpperCase().indexOf(filter) > -1) {
|
||||
card.style.display = '';
|
||||
groupMatchesFilter = true;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (!groupMatchesFilter) {
|
||||
featureGroup.style.display = 'none';
|
||||
} else {
|
||||
featureGroup.style.display = '';
|
||||
resetOrTemporarilyExpandGroup(featureGroup, filter, collapsedGroups);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCollapsedGroups() {
|
||||
return localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
|
||||
}
|
||||
|
||||
function resetOrTemporarilyExpandGroup(featureGroup, filterKeywords = '', collapsedGroups = []) {
|
||||
const shouldResetCollapse = filterKeywords.trim() === '';
|
||||
if (shouldResetCollapse) {
|
||||
// Resetting the group's expand/collapse to its original state (as in collapsed groups)
|
||||
const isCollapsed = collapsedGroups.indexOf(featureGroup.id) != -1;
|
||||
expandCollapseToggle(featureGroup, !isCollapsed);
|
||||
} else {
|
||||
// Temporarily expands feature group without affecting the actual/stored collapsed groups
|
||||
featureGroup.classList.remove('collapsed');
|
||||
featureGroup.querySelector('.header-expand-button').classList.remove('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFavoritesSectionLegacy() {
|
||||
const favoritesContainer = document.getElementById('groupFavorites').querySelector('.feature-group-container');
|
||||
favoritesContainer.innerHTML = '';
|
||||
const cards = Array.from(document.querySelectorAll('.feature-card:not(.duplicate)'));
|
||||
const addedCardIds = new Set();
|
||||
let favoritesAmount = 0;
|
||||
|
||||
cards.forEach((card) => {
|
||||
const favouritesList = JSON.parse(localStorage.getItem('favoritesList') || '[]');
|
||||
|
||||
if (favouritesList.includes(card.id) && !addedCardIds.has(card.id)) {
|
||||
const duplicate = card.cloneNode(true);
|
||||
duplicate.classList.add('duplicate');
|
||||
favoritesContainer.appendChild(duplicate);
|
||||
addedCardIds.add(card.id);
|
||||
favoritesAmount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (favoritesAmount === 0) {
|
||||
document.getElementById('groupFavorites').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('groupFavorites').style.display = 'flex';
|
||||
}
|
||||
reorderCards(favoritesContainer);
|
||||
}
|
||||
|
||||
function syncFavoritesLegacy() {
|
||||
const cards = Array.from(document.querySelectorAll('.feature-card'));
|
||||
cards.forEach((card) => {
|
||||
const isFavorite = localStorage.getItem(card.id) === 'favorite';
|
||||
const starIcon = card.querySelector('.favorite-icon span.material-symbols-rounded');
|
||||
if (starIcon) {
|
||||
if (isFavorite) {
|
||||
starIcon.classList.remove('no-fill');
|
||||
starIcon.classList.add('fill');
|
||||
card.classList.add('favorite');
|
||||
} else {
|
||||
starIcon.classList.remove('fill');
|
||||
starIcon.classList.add('no-fill');
|
||||
card.classList.remove('favorite');
|
||||
}
|
||||
}
|
||||
});
|
||||
updateFavoritesSectionLegacy();
|
||||
updateFavoritesDropdown();
|
||||
filterCardsLegacy();
|
||||
}
|
||||
|
||||
function reorderCards(container) {
|
||||
var cards = Array.from(container.querySelectorAll('.feature-card'));
|
||||
cards.forEach(function (card) {
|
||||
container.removeChild(card);
|
||||
});
|
||||
cards.sort(function (a, b) {
|
||||
var aIsFavorite = localStorage.getItem(a.id) === 'favorite';
|
||||
var bIsFavorite = localStorage.getItem(b.id) === 'favorite';
|
||||
if (a.id === 'update-link') {
|
||||
return -1;
|
||||
}
|
||||
if (b.id === 'update-link') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aIsFavorite && !bIsFavorite) {
|
||||
return -1;
|
||||
} else if (!aIsFavorite && bIsFavorite) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.id > b.id;
|
||||
}
|
||||
});
|
||||
cards.forEach(function (card) {
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function reorderAllCards() {
|
||||
const containers = Array.from(document.querySelectorAll('.feature-group-container'));
|
||||
containers.forEach(function (container) {
|
||||
reorderCards(container);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeCardsLegacy() {
|
||||
reorderAllCards();
|
||||
updateFavoritesSectionLegacy();
|
||||
updateFavoritesDropdown();
|
||||
filterCardsLegacy();
|
||||
}
|
||||
|
||||
function showFavoritesOnly() {
|
||||
const groups = Array.from(document.querySelectorAll('.feature-group-legacy'));
|
||||
if (localStorage.getItem('favoritesOnly') === 'true') {
|
||||
groups.forEach((group) => {
|
||||
if (group.id !== 'groupFavorites') {
|
||||
group.style.display = 'none';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
groups.forEach((group) => {
|
||||
if (group.id !== 'groupFavorites') {
|
||||
group.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFavoritesOnly() {
|
||||
if (localStorage.getItem('favoritesOnly') === 'true') {
|
||||
localStorage.setItem('favoritesOnly', 'false');
|
||||
} else {
|
||||
localStorage.setItem('favoritesOnly', 'true');
|
||||
}
|
||||
showFavoritesOnly();
|
||||
}
|
||||
|
||||
// Expands a feature group on true, collapses it on false and toggles state on null.
|
||||
function expandCollapseToggle(group, expand = null) {
|
||||
if (expand === null) {
|
||||
group.classList.toggle('collapsed');
|
||||
group.querySelector('.header-expand-button').classList.toggle('collapsed');
|
||||
} else if (expand) {
|
||||
group.classList.remove('collapsed');
|
||||
group.querySelector('.header-expand-button').classList.remove('collapsed');
|
||||
} else {
|
||||
group.classList.add('collapsed');
|
||||
group.querySelector('.header-expand-button').classList.add('collapsed');
|
||||
}
|
||||
|
||||
const collapsed = localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
|
||||
const groupIndex = collapsed.indexOf(group.id);
|
||||
|
||||
if (group.classList.contains('collapsed')) {
|
||||
if (groupIndex === -1) {
|
||||
collapsed.push(group.id);
|
||||
}
|
||||
} else {
|
||||
if (groupIndex !== -1) {
|
||||
collapsed.splice(groupIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('collapsedGroups', JSON.stringify(collapsed));
|
||||
}
|
||||
|
||||
function expandCollapseAll(expandAll) {
|
||||
const groups = Array.from(document.querySelectorAll('.feature-group-legacy'));
|
||||
groups.forEach((group) => {
|
||||
expandCollapseToggle(group, expandAll);
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
initializeCardsLegacy();
|
||||
syncFavoritesLegacy(); // Ensure everything is in sync on page load
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const materialIcons = new FontFaceObserver('Material Symbols Rounded');
|
||||
|
||||
materialIcons
|
||||
.load()
|
||||
.then(() => {
|
||||
document.querySelectorAll('.feature-card.hidden').forEach((el) => {
|
||||
el.classList.remove('hidden');
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('Material Symbols Rounded font failed to load.');
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll('.feature-group-header-legacy')).forEach((header) => {
|
||||
const parent = header.parentNode;
|
||||
const container = header.parentNode.querySelector('.feature-group-container');
|
||||
if (parent.id !== 'groupFavorites') {
|
||||
// container.style.maxHeight = container.scrollHeight + 'px';
|
||||
}
|
||||
header.onclick = () => {
|
||||
expandCollapseToggle(parent);
|
||||
};
|
||||
});
|
||||
|
||||
const collapsed = localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
|
||||
const groupsArray = Array.from(document.querySelectorAll('.feature-group-legacy'));
|
||||
|
||||
groupsArray.forEach((group) => {
|
||||
if (collapsed.indexOf(group.id) !== -1) {
|
||||
expandCollapseToggle(group, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Necessary in order to not fire the transition animation on page load, which looks wrong.
|
||||
// The timeout isn't doing anything visible to the user, so it's not making the page load look slower.
|
||||
setTimeout(() => {
|
||||
groupsArray.forEach((group) => {
|
||||
const container = group.querySelector('.feature-group-container');
|
||||
container.classList.add('animated-group');
|
||||
});
|
||||
}, 500);
|
||||
|
||||
Array.from(document.querySelectorAll('.feature-group-header')).forEach((header) => {
|
||||
const parent = header.parentNode;
|
||||
header.onclick = () => {
|
||||
expandCollapseToggle(parent);
|
||||
};
|
||||
});
|
||||
|
||||
showFavoritesOnly();
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
function filterCards() {
|
||||
var input = document.getElementById('searchBar');
|
||||
var filter = input.value.toUpperCase().trim();
|
||||
|
||||
// Split the input filter into individual words for multi-word matching
|
||||
var filterWords = filter.split(/[\s,;.\-]+/);
|
||||
|
||||
let featureGroups = document.querySelectorAll('.feature-group');
|
||||
for (const featureGroup of featureGroups) {
|
||||
var cards = featureGroup.querySelectorAll('.dropdown-item');
|
||||
|
||||
let groupMatchesFilter = false;
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
var card = cards[i];
|
||||
var title = card.getAttribute('title') || '';
|
||||
|
||||
// Get the navbar tags associated with the card
|
||||
var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`);
|
||||
var navbarTags = navbarItem ? navbarItem.getAttribute('data-bs-tags') : '';
|
||||
navbarTags = navbarItem ? navbarTags + ',' + navbarItem.getAttribute('data-bs-title') + ',' + navbarItem.children[0].getAttribute('data-title') : navbarTags;
|
||||
|
||||
var content = (title + ' ' + navbarTags).toUpperCase();
|
||||
|
||||
// Check if all words in the filter match the content
|
||||
var matches = filterWords.every((word) => content.includes(word));
|
||||
|
||||
if (matches) {
|
||||
card.style.display = '';
|
||||
groupMatchesFilter = true;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (!groupMatchesFilter) {
|
||||
featureGroup.style.display = 'none';
|
||||
} else {
|
||||
featureGroup.style.display = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFavoritesSection() {
|
||||
const favoritesContainer = document.getElementById('groupFavorites').querySelector('.nav-group-container');
|
||||
favoritesContainer.innerHTML = '';
|
||||
let favoritesAmount = 0;
|
||||
const favouritesList = JSON.parse(localStorage.getItem('favoritesList') || '[]');
|
||||
const isFavoritesView = JSON.parse(localStorage.getItem('favoritesView') || 'false');
|
||||
|
||||
favouritesList.forEach((value) => {
|
||||
var navbarEntry = document.querySelector(`a[data-bs-link='${value}']`);
|
||||
if (navbarEntry) {
|
||||
const duplicate = navbarEntry.cloneNode(true);
|
||||
favoritesContainer.appendChild(duplicate);
|
||||
}
|
||||
favoritesAmount++;
|
||||
});
|
||||
|
||||
if (favoritesAmount === 0 || !isFavoritesView) {
|
||||
document.getElementById('groupFavorites').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('groupFavorites').style.display = 'flex';
|
||||
}
|
||||
reorderCards(favoritesContainer);
|
||||
//favoritesContainer.style.maxHeight = favoritesContainer.scrollHeight + 'px';
|
||||
}
|
||||
|
||||
function reorderCards(container) {
|
||||
var cards = Array.from(container.querySelectorAll('.dropdown-item'));
|
||||
cards.forEach(function (card) {
|
||||
container.removeChild(card);
|
||||
});
|
||||
cards.sort(function (a, b) {
|
||||
var aIsFavorite = localStorage.getItem(a.id) === 'favorite';
|
||||
var bIsFavorite = localStorage.getItem(b.id) === 'favorite';
|
||||
|
||||
if (aIsFavorite && !bIsFavorite) {
|
||||
return -1;
|
||||
} else if (!aIsFavorite && bIsFavorite) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.id > b.id;
|
||||
}
|
||||
});
|
||||
cards.forEach(function (card) {
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeCards() {
|
||||
updateFavoritesSection();
|
||||
updateFavoritesView();
|
||||
updateFavoritesDropdown();
|
||||
filterCards();
|
||||
}
|
||||
|
||||
function updateFavoritesView() {
|
||||
const isFavoritesView = JSON.parse(localStorage.getItem('favoritesView') || 'false');
|
||||
const iconElement = document.getElementById('toggle-favourites-icon');
|
||||
const favoritesGroup = document.querySelector('#groupFavorites');
|
||||
const favoritesList = JSON.parse(localStorage.getItem('favoritesList')) || [];
|
||||
|
||||
if (isFavoritesView && favoritesList.length > 0) {
|
||||
document.getElementById('favouritesVisibility').style.display = 'flex';
|
||||
favoritesGroup.style.display = 'flex';
|
||||
} else {
|
||||
if (favoritesList.length > 0) {
|
||||
document.getElementById('favouritesVisibility').style.display = 'flex';
|
||||
favoritesGroup.style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('favouritesVisibility').style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFavoritesMode() {
|
||||
const favoritesMode = !document.querySelector('.toggle-favourites').classList.contains('active');
|
||||
document.querySelector('.toggle-favourites').classList.toggle('active', favoritesMode);
|
||||
|
||||
document.querySelectorAll('.favorite-icon').forEach((icon) => {
|
||||
const endpoint = icon.getAttribute('data-endpoint');
|
||||
const parent = icon.closest('.dropdown-item');
|
||||
const isInGroupRecent = parent.closest('#groupRecent') !== null;
|
||||
const isInGroupFavorites = parent.closest('#groupFavorites') !== null;
|
||||
|
||||
if (isInGroupRecent) {
|
||||
icon.style.display = 'none';
|
||||
} else if (isInGroupFavorites) {
|
||||
icon.style.display = favoritesMode ? 'inline-block' : 'none';
|
||||
icon.textContent = 'close_small';
|
||||
} else {
|
||||
icon.style.display = favoritesMode ? 'inline-block' : 'none';
|
||||
|
||||
const favoritesList = JSON.parse(localStorage.getItem('favoritesList')) || [];
|
||||
icon.textContent = favoritesList.includes(endpoint) ? 'close_small' : 'add';
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.dropdown-item').forEach((link) => {
|
||||
if (favoritesMode) {
|
||||
link.dataset.originalHref = link.getAttribute('href');
|
||||
link.setAttribute('href', '#');
|
||||
link.classList.add('no-hover');
|
||||
} else {
|
||||
link.setAttribute('href', link.dataset.originalHref || '#');
|
||||
link.classList.remove('no-hover');
|
||||
}
|
||||
});
|
||||
|
||||
const isFavoritesView = JSON.parse(localStorage.getItem('favoritesView') || 'false');
|
||||
if (favoritesMode && !isFavoritesView) {
|
||||
toggleFavoritesView();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFavoritesView() {
|
||||
const isFavoritesView = JSON.parse(localStorage.getItem('favoritesView') || 'false');
|
||||
localStorage.setItem('favoritesView', !isFavoritesView);
|
||||
updateFavoritesView();
|
||||
}
|
||||
window.onload = function () {
|
||||
initializeCards();
|
||||
};
|
||||
|
||||
function sortNavElements(criteria) {
|
||||
document.querySelectorAll('.nav-group-container').forEach((container) => {
|
||||
const items = Array.from(container.children);
|
||||
|
||||
items.sort((a, b) => {
|
||||
if (criteria === 'alphabetical') {
|
||||
const titleA = a.querySelector('.icon-text')?.textContent.trim().toLowerCase() || '';
|
||||
const titleB = b.querySelector('.icon-text')?.textContent.trim().toLowerCase() || '';
|
||||
return titleA.localeCompare(titleB);
|
||||
} else if (criteria === 'global') {
|
||||
const popularityA = parseInt(a.dataset.popularity, 10) || 1000;
|
||||
const popularityB = parseInt(b.dataset.popularity, 10) || 1000;
|
||||
return popularityA - popularityB;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
container.innerHTML = '';
|
||||
items.forEach((item) => container.appendChild(item));
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchPopularityData(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
function applyPopularityData(popularityData) {
|
||||
document.querySelectorAll('.dropdown-item').forEach((item) => {
|
||||
const endpoint = item.getAttribute('data-bs-link');
|
||||
const popularity = popularityData['/' + endpoint];
|
||||
if (endpoint && popularity !== undefined) {
|
||||
item.setAttribute('data-popularity', popularity);
|
||||
}
|
||||
});
|
||||
const currentSort = localStorage.getItem('homepageSort') || 'alphabetical';
|
||||
const sortDropdown = document.getElementById('sort-options');
|
||||
if (sortDropdown) {
|
||||
sortDropdown.value = currentSort;
|
||||
``;
|
||||
}
|
||||
sortNavElements(currentSort);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
const sortDropdown = document.getElementById('sort-options');
|
||||
if (sortDropdown) {
|
||||
sortDropdown.addEventListener('change', (event) => {
|
||||
const selectedOption = event.target.value;
|
||||
localStorage.setItem('homepageSort', selectedOption);
|
||||
|
||||
sortNavElements(selectedOption);
|
||||
});
|
||||
}
|
||||
try {
|
||||
const response = await fetch('files/popularity.txt');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const popularityData = await response.json();
|
||||
applyPopularityData(popularityData);
|
||||
} catch (error) {
|
||||
console.error('Error loading popularity data:', error);
|
||||
}
|
||||
|
||||
const materialIcons = new FontFaceObserver('Material Symbols Rounded');
|
||||
|
||||
materialIcons
|
||||
.load()
|
||||
.then(() => {
|
||||
document.querySelectorAll('.dropdown-item.hidden').forEach((el) => {
|
||||
el.classList.remove('hidden');
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('Material Symbols Rounded font failed to load.');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
function getStoredOrDefaultLocale() {
|
||||
const storedLocale = localStorage.getItem('languageCode');
|
||||
return storedLocale || getDetailedLanguageCode();
|
||||
}
|
||||
|
||||
function setLanguageForDropdown(dropdownClass) {
|
||||
const storedLocale = getStoredOrDefaultLocale();
|
||||
const dropdownItems = document.querySelectorAll(dropdownClass);
|
||||
|
||||
dropdownItems.forEach((item) => {
|
||||
item.classList.toggle('active', item.dataset.bsLanguageCode === storedLocale);
|
||||
item.removeEventListener('click', handleDropdownItemClick);
|
||||
item.addEventListener('click', handleDropdownItemClick);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUrlWithLanguage(languageCode) {
|
||||
const currentURL = new URL(window.location.href);
|
||||
currentURL.searchParams.set('lang', languageCode);
|
||||
window.location.href = currentURL.toString();
|
||||
}
|
||||
|
||||
function handleDropdownItemClick(event) {
|
||||
event.preventDefault();
|
||||
const languageCode = event.currentTarget.dataset.bsLanguageCode;
|
||||
if (languageCode) {
|
||||
localStorage.setItem('languageCode', languageCode);
|
||||
updateUrlWithLanguage(languageCode);
|
||||
} else {
|
||||
console.error('Language code is not set for this item.');
|
||||
}
|
||||
}
|
||||
|
||||
function checkUserLanguage(defaultLocale) {
|
||||
if (
|
||||
!localStorage.getItem('languageCode') ||
|
||||
document.documentElement.getAttribute('data-language') != defaultLocale
|
||||
) {
|
||||
localStorage.setItem('languageCode', defaultLocale);
|
||||
updateUrlWithLanguage(defaultLocale);
|
||||
}
|
||||
}
|
||||
|
||||
function initLanguageSettings() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
setLanguageForDropdown('.lang_dropdown-item');
|
||||
|
||||
const defaultLocale = getStoredOrDefaultLocale();
|
||||
checkUserLanguage(defaultLocale);
|
||||
|
||||
const dropdownItems = document.querySelectorAll('.lang_dropdown-item');
|
||||
dropdownItems.forEach((item) => {
|
||||
item.classList.toggle('active', item.dataset.bsLanguageCode === defaultLocale);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sortLanguageDropdown() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const dropdownMenu = document.getElementById('languageSelection');
|
||||
if (dropdownMenu) {
|
||||
const items = Array.from(dropdownMenu.children).filter((child) => child.querySelector('a'));
|
||||
items
|
||||
.sort((wrapperA, wrapperB) => {
|
||||
const a = wrapperA.querySelector('a');
|
||||
const b = wrapperB.querySelector('a');
|
||||
return a.dataset.bsLanguageCode.localeCompare(b.dataset.bsLanguageCode);
|
||||
})
|
||||
.forEach((node) => dropdownMenu.appendChild(node));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sortLanguageDropdown();
|
||||
@@ -0,0 +1,47 @@
|
||||
async function downloadFilesWithCallback(processFileCallback) {
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const files = fileInput.files;
|
||||
|
||||
const zipThreshold = 4;
|
||||
const zipFiles = files.length > zipThreshold;
|
||||
|
||||
let jszip = null;
|
||||
if (zipFiles) {
|
||||
jszip = new JSZip();
|
||||
}
|
||||
|
||||
const promises = Array.from(files).map(async (file) => {
|
||||
const { processedData, fileName } = await processFileCallback(file);
|
||||
|
||||
if (zipFiles) {
|
||||
jszip.file(fileName, processedData);
|
||||
} else {
|
||||
const url = URL.createObjectURL(processedData);
|
||||
const downloadOption = localStorage.getItem("downloadOption");
|
||||
|
||||
if (downloadOption === "sameWindow") {
|
||||
window.location.href = url;
|
||||
} else if (downloadOption === "newWindow") {
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = fileName;
|
||||
downloadLink.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (zipFiles) {
|
||||
const content = await jszip.generateAsync({ type: "blob" });
|
||||
const url = URL.createObjectURL(content);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "files.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
let currentSort = {
|
||||
field: null,
|
||||
descending: false,
|
||||
};
|
||||
|
||||
document.getElementById("fileInput-input").addEventListener("file-input-change", function () {
|
||||
var files = this.files;
|
||||
displayFiles(files);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {FileList} files
|
||||
*/
|
||||
async function displayFiles(files) {
|
||||
const list = document.getElementById("selectedFiles");
|
||||
|
||||
while (list.firstChild) {
|
||||
list.removeChild(list.firstChild);
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const pageCount = await getPDFPageCount(files[i]);
|
||||
const pageLabel = pageCount === 1 ? pageTranslation : pagesTranslation;
|
||||
|
||||
// Create list item
|
||||
const item = document.createElement("li");
|
||||
item.className = "list-group-item";
|
||||
|
||||
// Create filename div and set textContent to sanitize
|
||||
const fileNameDiv = document.createElement("div");
|
||||
fileNameDiv.className = "filename";
|
||||
fileNameDiv.setAttribute("data-file-id", files[i].uniqueId);
|
||||
fileNameDiv.textContent = files[i].name;
|
||||
|
||||
// Create page info div and set textContent to sanitize
|
||||
const pageInfoDiv = document.createElement("div");
|
||||
pageInfoDiv.className = "page-info";
|
||||
const pageCountSpan = document.createElement("span");
|
||||
pageCountSpan.className = "page-count";
|
||||
pageCountSpan.textContent = `${pageCount} ${pageLabel}`;
|
||||
pageInfoDiv.appendChild(pageCountSpan);
|
||||
|
||||
// Create arrows div with buttons
|
||||
const arrowsDiv = document.createElement("div");
|
||||
arrowsDiv.className = "arrows d-flex";
|
||||
|
||||
const moveUpButton = document.createElement("button");
|
||||
moveUpButton.className = "btn btn-secondary move-up";
|
||||
moveUpButton.innerHTML = "<span>↑</span>";
|
||||
|
||||
const moveDownButton = document.createElement("button");
|
||||
moveDownButton.className = "btn btn-secondary move-down";
|
||||
moveDownButton.innerHTML = "<span>↓</span>";
|
||||
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.className = "btn btn-danger remove-file";
|
||||
removeButton.innerHTML = "<span>×</span>";
|
||||
|
||||
arrowsDiv.append(moveUpButton, moveDownButton, removeButton);
|
||||
|
||||
// Append elements to item and then to list
|
||||
const itemContainer = document.createElement("div");
|
||||
itemContainer.className = "d-flex justify-content-between align-items-center w-100";
|
||||
itemContainer.append(fileNameDiv, pageInfoDiv, arrowsDiv);
|
||||
|
||||
item.appendChild(itemContainer);
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
attachMoveButtons();
|
||||
}
|
||||
|
||||
|
||||
async function getPDFPageCount(file) {
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
const pdf = await pdfjsLib.getDocument(blobUrl).promise;
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
return pdf.numPages;
|
||||
}
|
||||
|
||||
function attachMoveButtons() {
|
||||
var moveUpButtons = document.querySelectorAll(".move-up");
|
||||
for (var i = 0; i < moveUpButtons.length; i++) {
|
||||
moveUpButtons[i].addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
var parent = this.closest(".list-group-item");
|
||||
var grandParent = parent.parentNode;
|
||||
if (parent.previousElementSibling) {
|
||||
grandParent.insertBefore(parent, parent.previousElementSibling);
|
||||
updateFiles();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var moveDownButtons = document.querySelectorAll(".move-down");
|
||||
for (var i = 0; i < moveDownButtons.length; i++) {
|
||||
moveDownButtons[i].addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
var parent = this.closest(".list-group-item");
|
||||
var grandParent = parent.parentNode;
|
||||
if (parent.nextElementSibling) {
|
||||
grandParent.insertBefore(parent.nextElementSibling, parent);
|
||||
updateFiles();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var removeButtons = document.querySelectorAll(".remove-file");
|
||||
for (var i = 0; i < removeButtons.length; i++) {
|
||||
removeButtons[i].addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
var parent = this.closest(".list-group-item");
|
||||
//Get name of removed file
|
||||
let filenameNode = parent.querySelector(".filename");
|
||||
var fileName = filenameNode.innerText;
|
||||
const fileId = filenameNode.getAttribute("data-file-id");
|
||||
parent.remove();
|
||||
updateFiles();
|
||||
//Dispatch a custom event with the name of the removed file
|
||||
var event = new CustomEvent("fileRemoved", { detail: fileId });
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("sortByNameBtn").addEventListener("click", function () {
|
||||
if (currentSort.field === "name" && !currentSort.descending) {
|
||||
currentSort.descending = true;
|
||||
sortFiles((a, b) => b.name.localeCompare(a.name));
|
||||
} else {
|
||||
currentSort.field = "name";
|
||||
currentSort.descending = false;
|
||||
sortFiles((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("sortByDateBtn").addEventListener("click", function () {
|
||||
if (currentSort.field === "lastModified" && !currentSort.descending) {
|
||||
currentSort.descending = true;
|
||||
sortFiles((a, b) => b.lastModified - a.lastModified);
|
||||
} else {
|
||||
currentSort.field = "lastModified";
|
||||
currentSort.descending = false;
|
||||
sortFiles((a, b) => a.lastModified - b.lastModified);
|
||||
}
|
||||
});
|
||||
|
||||
function sortFiles(comparator) {
|
||||
// Convert FileList to array and sort
|
||||
const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator);
|
||||
|
||||
// Refresh displayed list
|
||||
displayFiles(sortedFilesArray);
|
||||
|
||||
// Update the files property
|
||||
const dataTransfer = new DataTransfer();
|
||||
sortedFilesArray.forEach((file) => dataTransfer.items.add(file));
|
||||
document.getElementById("fileInput-input").files = dataTransfer.files;
|
||||
}
|
||||
|
||||
function updateFiles() {
|
||||
var dataTransfer = new DataTransfer();
|
||||
var liElements = document.querySelectorAll("#selectedFiles li");
|
||||
const files = document.getElementById("fileInput-input").files;
|
||||
|
||||
for (var i = 0; i < liElements.length; i++) {
|
||||
var fileNameFromList = liElements[i].querySelector(".filename").innerText;
|
||||
var fileFromFiles;
|
||||
for (var j = 0; j < files.length; j++) {
|
||||
var file = files[j];
|
||||
if (file.name === fileNameFromList) {
|
||||
dataTransfer.items.add(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
document.getElementById("fileInput-input").files = dataTransfer.files;
|
||||
}
|
||||
|
||||
document.querySelector("#resetFileInputBtn").addEventListener("click", ()=>{
|
||||
let formElement = document.querySelector("#fileInput-input");
|
||||
formElement.value = '';
|
||||
clearLiElements();
|
||||
updateFiles();
|
||||
|
||||
});
|
||||
|
||||
function clearLiElements(){
|
||||
let listGroupItemNodeList = document.querySelectorAll(".list-group-item");
|
||||
for (let i = 0; i < listGroupItemNodeList.length; i++) {
|
||||
listGroupItemNodeList[i].remove();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
class DragDropManager {
|
||||
constructor(id, wrapperId) {
|
||||
this.dragContainer = document.getElementById(id);
|
||||
this.pageDirection = document.documentElement.getAttribute('dir');
|
||||
this.wrapper = document.getElementById(wrapperId);
|
||||
this.pageDragging = false;
|
||||
this.hoveredEl = undefined;
|
||||
this.draggedImageEl = undefined;
|
||||
this.draggedEl = undefined;
|
||||
this.selectedPageElements = []; // Store selected pages for multi-page mode
|
||||
this.elementTimeouts = new Map();
|
||||
|
||||
// Add CSS dynamically
|
||||
const styleElement = document.createElement('link');
|
||||
styleElement.rel = 'stylesheet';
|
||||
styleElement.href = 'css/dragdrop.css';
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
// Create the endpoint element
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('page-container');
|
||||
div.classList.add('drag-manager_endpoint');
|
||||
div.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
|
||||
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293V6.5z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
|
||||
</svg>`;
|
||||
this.endInsertionElement = div;
|
||||
|
||||
// Bind methods
|
||||
this.startDraggingPage = this.startDraggingPage.bind(this);
|
||||
this.onDragEl = this.onDragEl.bind(this);
|
||||
this.stopDraggingPage = this.stopDraggingPage.bind(this);
|
||||
|
||||
this.adapt(div);
|
||||
}
|
||||
|
||||
startDraggingPage(div) {
|
||||
if (window.selectPage) {
|
||||
// Multi-page drag logic
|
||||
this.selectedPageElements = window.selectedPages
|
||||
.map((index) => {
|
||||
const pageEl = Array.from(this.wrapper.childNodes)[index];
|
||||
if (pageEl) {
|
||||
pageEl.initialTransform = pageEl.style.transform || 'translate(0px, 0px)';
|
||||
pageEl.classList.add('drag-manager_dragging');
|
||||
}
|
||||
return pageEl;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (this.selectedPageElements.length === 0) return;
|
||||
|
||||
this.pageDragging = true;
|
||||
this.draggedImageEl = document.createElement('div');
|
||||
this.draggedImageEl.classList.add('multidrag');
|
||||
this.draggedImageEl.textContent = `${this.selectedPageElements.length} ${window.translations.dragDropMessage}`;
|
||||
this.draggedImageEl.style.visibility = 'hidden';
|
||||
|
||||
this.dragContainer.appendChild(this.draggedImageEl);
|
||||
} else {
|
||||
// Single-page drag logic
|
||||
this.pageDragging = true;
|
||||
this.draggedEl = div;
|
||||
const img = div.querySelector('img');
|
||||
div.classList.add('drag-manager_dragging');
|
||||
div.classList.remove('moved-element', 'remove');
|
||||
const imgEl = document.createElement('img');
|
||||
imgEl.classList.add('dragged-img');
|
||||
imgEl.src = img.src;
|
||||
imgEl.style.visibility = 'hidden';
|
||||
imgEl.style.transform = `rotate(${img.style.rotate === '' ? '0deg' : img.style.rotate}) translate(-50%, -50%)`;
|
||||
this.draggedImageEl = imgEl;
|
||||
this.dragContainer.appendChild(imgEl);
|
||||
}
|
||||
|
||||
// Common setup for both modes
|
||||
window.addEventListener('mouseup', this.stopDraggingPage);
|
||||
window.addEventListener('mousemove', this.onDragEl);
|
||||
this.wrapper.classList.add('drag-manager_dragging-container');
|
||||
this.wrapper.appendChild(this.endInsertionElement);
|
||||
}
|
||||
|
||||
onDragEl(mouseEvent) {
|
||||
const { clientX, clientY } = mouseEvent;
|
||||
if (this.draggedImageEl) {
|
||||
this.draggedImageEl.style.visibility = 'visible';
|
||||
this.draggedImageEl.style.left = `${clientX}px`;
|
||||
this.draggedImageEl.style.top = `${clientY}px`;
|
||||
}
|
||||
}
|
||||
|
||||
stopDraggingPage() {
|
||||
window.removeEventListener('mousemove', this.onDragEl);
|
||||
this.wrapper.classList.remove('drag-manager_dragging-container');
|
||||
this.wrapper.removeChild(this.endInsertionElement);
|
||||
window.removeEventListener('mouseup', this.stopDraggingPage);
|
||||
|
||||
if (this.draggedImageEl) {
|
||||
this.dragContainer.removeChild(this.draggedImageEl);
|
||||
this.draggedImageEl = undefined;
|
||||
}
|
||||
|
||||
if (window.selectPage) {
|
||||
// Multi-page drop logic
|
||||
if (
|
||||
!this.hoveredEl ||
|
||||
!this.hoveredEl.classList.contains('page-container') ||
|
||||
this.selectedPageElements.includes(this.hoveredEl)
|
||||
) {
|
||||
this.selectedPageElements.forEach((pageEl) => {
|
||||
pageEl.style.transform = pageEl.initialTransform || 'translate(0px, 0px)';
|
||||
pageEl.classList.remove('drag-manager_dragging');
|
||||
});
|
||||
} else {
|
||||
this.selectedPageElements.forEach((pageEl) => {
|
||||
pageEl.classList.remove('drag-manager_dragging');
|
||||
});
|
||||
|
||||
this.movePageTo(
|
||||
this.selectedPageElements,
|
||||
this.hoveredEl === this.endInsertionElement
|
||||
? null
|
||||
: this.hoveredEl);
|
||||
|
||||
this.selectedPageElements.forEach((pageEl) => {
|
||||
// Handle timeout for the current element
|
||||
this.handleTimeoutForElement(pageEl);
|
||||
});
|
||||
}
|
||||
this.selectedPageElements = [];
|
||||
window.resetPages();
|
||||
} else {
|
||||
// Single-page drop logic
|
||||
if (
|
||||
!this.hoveredEl ||
|
||||
!this.hoveredEl.classList.contains('page-container') ||
|
||||
this.hoveredEl === this.draggedEl
|
||||
) {
|
||||
this.draggedEl.style.transform = this.draggedEl.initialTransform || 'translate(0px, 0px)';
|
||||
this.draggedEl.classList.remove('drag-manager_dragging');
|
||||
return;
|
||||
}
|
||||
|
||||
this.draggedEl.classList.remove('drag-manager_dragging');
|
||||
|
||||
if (this.hoveredEl === this.endInsertionElement) {
|
||||
this.movePageTo(this.draggedEl);
|
||||
} else {
|
||||
this.movePageTo(this.draggedEl, this.hoveredEl);
|
||||
}
|
||||
|
||||
// Handle timeout for the current element
|
||||
this.handleTimeoutForElement(this.draggedEl);
|
||||
}
|
||||
|
||||
this.pageDragging = false;
|
||||
}
|
||||
|
||||
// Helper function to manage independent timeouts
|
||||
handleTimeoutForElement(element) {
|
||||
// Clear existing timeout if present
|
||||
if (this.elementTimeouts.has(element)) {
|
||||
clearTimeout(this.elementTimeouts.get(element));
|
||||
}
|
||||
|
||||
// Add the moved-element class and set a new timeout
|
||||
element.classList.remove('remove');
|
||||
element.classList.add('moved-element');
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
element.classList.add('remove');
|
||||
this.elementTimeouts.delete(element); // Cleanup the timeout map
|
||||
}, 2000);
|
||||
|
||||
// Store the timeout ID for this element
|
||||
this.elementTimeouts.set(element, timeoutId);
|
||||
}
|
||||
|
||||
setActions({ movePageTo }) {
|
||||
this.movePageTo = movePageTo;
|
||||
}
|
||||
|
||||
adapt(div) {
|
||||
const onDragStart = (e) => {
|
||||
e.preventDefault();
|
||||
this.startDraggingPage(div);
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (this.pageDragging) {
|
||||
this.hoveredEl = div;
|
||||
div.classList.add('drag-manager_draghover');
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
this.hoveredEl = undefined;
|
||||
div.classList.remove('drag-manager_draghover');
|
||||
};
|
||||
|
||||
div.addEventListener('dragstart', onDragStart);
|
||||
div.addEventListener('mouseenter', onMouseEnter);
|
||||
div.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
||||
export default DragDropManager;
|
||||
@@ -0,0 +1,66 @@
|
||||
class ImageHighlighter {
|
||||
imageHighlighter;
|
||||
constructor(id) {
|
||||
this.imageHighlighter = document.getElementById(id);
|
||||
this.imageHighlightCallback = this.imageHighlightCallback.bind(this);
|
||||
|
||||
var styleElement = document.createElement("link");
|
||||
styleElement.rel = "stylesheet";
|
||||
styleElement.href = "css/imageHighlighter.css";
|
||||
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
this.imageHighlighter.onclick = () => {
|
||||
this.imageHighlighter.childNodes.forEach((child) => {
|
||||
child.classList.add("remove");
|
||||
setTimeout(() => {
|
||||
this.imageHighlighter.removeChild(child);
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
imageHighlightCallback(highlightEvent) {
|
||||
var bigImg = document.createElement("img");
|
||||
bigImg.onclick = (imageClickEvent) => {
|
||||
// This prevents the highlighter's onClick from closing the image when clicking
|
||||
// on the image instead of next to it.
|
||||
imageClickEvent.preventDefault();
|
||||
imageClickEvent.stopPropagation();
|
||||
};
|
||||
bigImg.src = highlightEvent.target.src;
|
||||
bigImg.style.rotate = highlightEvent.target.style.rotate;
|
||||
this.imageHighlighter.appendChild(bigImg);
|
||||
}
|
||||
|
||||
setActions() {
|
||||
// not needed in this case
|
||||
}
|
||||
|
||||
adapt(div) {
|
||||
const img = div.querySelector(".page-image");
|
||||
img.addEventListener("click", this.imageHighlightCallback);
|
||||
return div;
|
||||
}
|
||||
|
||||
async addImageFile(file, nextSiblingElement) {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("page-container");
|
||||
|
||||
var img = document.createElement("img");
|
||||
img.classList.add("page-image");
|
||||
img.src = URL.createObjectURL(file);
|
||||
div.appendChild(img);
|
||||
|
||||
this.pdfAdapters.forEach((adapter) => {
|
||||
adapter.adapt?.(div);
|
||||
});
|
||||
if (nextSiblingElement) {
|
||||
this.pagesContainer.insertBefore(div, nextSiblingElement);
|
||||
} else {
|
||||
this.pagesContainer.appendChild(div);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageHighlighter;
|
||||
@@ -0,0 +1,272 @@
|
||||
import { DeletePageCommand } from "./commands/delete-page.js";
|
||||
import { SelectPageCommand } from "./commands/select.js";
|
||||
import { SplitFileCommand } from "./commands/split.js";
|
||||
import { UndoManager } from "./UndoManager.js";
|
||||
|
||||
class PdfActionsManager {
|
||||
pageDirection;
|
||||
pagesContainer;
|
||||
static selectedPages = []; // Static property shared across all instances
|
||||
undoManager;
|
||||
|
||||
constructor(id, undoManager) {
|
||||
this.pagesContainer = document.getElementById(id);
|
||||
this.pageDirection = document.documentElement.getAttribute("dir");
|
||||
|
||||
this.undoManager = undoManager || new UndoManager();
|
||||
|
||||
var styleElement = document.createElement("link");
|
||||
styleElement.rel = "stylesheet";
|
||||
styleElement.href = "css/pdfActions.css";
|
||||
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
getPageContainer(element) {
|
||||
var container = element;
|
||||
while (!container.classList.contains("page-container")) {
|
||||
container = container.parentNode;
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
moveUpButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
|
||||
const sibling = imgContainer.previousSibling;
|
||||
if (sibling) {
|
||||
this.movePageTo(imgContainer, sibling, true);
|
||||
}
|
||||
}
|
||||
|
||||
moveDownButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
const sibling = imgContainer.nextSibling;
|
||||
if (sibling) {
|
||||
this.movePageTo(
|
||||
imgContainer,
|
||||
sibling.nextSibling,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
rotateCCWButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
const img = imgContainer.querySelector("img");
|
||||
|
||||
let rotateCommand = this.rotateElement(img, -90);
|
||||
this._pushUndoClearRedo(rotateCommand);
|
||||
}
|
||||
|
||||
rotateCWButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
const img = imgContainer.querySelector("img");
|
||||
|
||||
let rotateCommand = this.rotateElement(img, 90);
|
||||
this._pushUndoClearRedo(rotateCommand);
|
||||
}
|
||||
|
||||
deletePageButtonCallback(e) {
|
||||
let imgContainer = this.getPageContainer(e.target);
|
||||
let deletePageCommand = new DeletePageCommand(
|
||||
imgContainer,
|
||||
this.pagesContainer
|
||||
);
|
||||
deletePageCommand.execute();
|
||||
|
||||
this._pushUndoClearRedo(deletePageCommand);
|
||||
}
|
||||
|
||||
insertFileButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
this.addFiles(imgContainer);
|
||||
}
|
||||
|
||||
insertFileBlankButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
this.addFiles(imgContainer, true);
|
||||
}
|
||||
|
||||
splitFileButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
|
||||
let splitFileCommand = new SplitFileCommand(imgContainer, "split-before");
|
||||
splitFileCommand.execute();
|
||||
|
||||
this._pushUndoClearRedo(splitFileCommand);
|
||||
}
|
||||
|
||||
_pushUndoClearRedo(command) {
|
||||
this.undoManager.pushUndoClearRedo(command);
|
||||
}
|
||||
|
||||
setActions({ movePageTo, addFiles, rotateElement }) {
|
||||
this.movePageTo = movePageTo;
|
||||
this.addFiles = addFiles;
|
||||
this.rotateElement = rotateElement;
|
||||
|
||||
this.moveUpButtonCallback = this.moveUpButtonCallback.bind(this);
|
||||
this.moveDownButtonCallback = this.moveDownButtonCallback.bind(this);
|
||||
this.rotateCCWButtonCallback = this.rotateCCWButtonCallback.bind(this);
|
||||
this.rotateCWButtonCallback = this.rotateCWButtonCallback.bind(this);
|
||||
this.deletePageButtonCallback = this.deletePageButtonCallback.bind(this);
|
||||
this.insertFileButtonCallback = this.insertFileButtonCallback.bind(this);
|
||||
this.insertFileBlankButtonCallback = this.insertFileBlankButtonCallback.bind(this);
|
||||
this.splitFileButtonCallback = this.splitFileButtonCallback.bind(this);
|
||||
}
|
||||
|
||||
|
||||
adapt(div) {
|
||||
div.classList.add("pdf-actions_container");
|
||||
const leftDirection = this.pageDirection === "rtl" ? "right" : "left";
|
||||
const rightDirection = this.pageDirection === "rtl" ? "left" : "right";
|
||||
const buttonContainer = document.createElement("div");
|
||||
|
||||
buttonContainer.classList.add("btn-group", "pdf-actions_button-container", "hide-on-drag");
|
||||
|
||||
const moveUp = document.createElement("button");
|
||||
moveUp.classList.add("pdf-actions_move-left-button", "btn", "btn-secondary");
|
||||
moveUp.setAttribute('title', window.translations.moveLeft);
|
||||
moveUp.innerHTML = `<span class="material-symbols-rounded">arrow_${leftDirection}_alt</span>`;
|
||||
moveUp.onclick = this.moveUpButtonCallback;
|
||||
buttonContainer.appendChild(moveUp);
|
||||
|
||||
const moveDown = document.createElement("button");
|
||||
moveDown.classList.add("pdf-actions_move-right-button", "btn", "btn-secondary");
|
||||
moveDown.setAttribute('title', window.translations.moveRight);
|
||||
moveDown.innerHTML = `<span class="material-symbols-rounded">arrow_${rightDirection}_alt</span>`;
|
||||
moveDown.onclick = this.moveDownButtonCallback;
|
||||
buttonContainer.appendChild(moveDown);
|
||||
|
||||
|
||||
const rotateCCW = document.createElement("button");
|
||||
rotateCCW.classList.add("btn", "btn-secondary");
|
||||
rotateCCW.setAttribute('title', window.translations.rotateLeft);
|
||||
rotateCCW.innerHTML = `<span class="material-symbols-rounded">rotate_left</span>`;
|
||||
rotateCCW.onclick = this.rotateCCWButtonCallback;
|
||||
buttonContainer.appendChild(rotateCCW);
|
||||
|
||||
const rotateCW = document.createElement("button");
|
||||
rotateCW.classList.add("btn", "btn-secondary");
|
||||
rotateCW.setAttribute('title', window.translations.rotateRight);
|
||||
rotateCW.innerHTML = `<span class="material-symbols-rounded">rotate_right</span>`;
|
||||
rotateCW.onclick = this.rotateCWButtonCallback;
|
||||
buttonContainer.appendChild(rotateCW);
|
||||
|
||||
const deletePage = document.createElement("button");
|
||||
deletePage.classList.add("btn", "btn-danger");
|
||||
deletePage.setAttribute('title', window.translations.delete);
|
||||
deletePage.innerHTML = `<span class="material-symbols-rounded">delete</span>`;
|
||||
deletePage.onclick = this.deletePageButtonCallback;
|
||||
buttonContainer.appendChild(deletePage);
|
||||
|
||||
div.appendChild(buttonContainer);
|
||||
|
||||
//enerate checkbox to select individual pages
|
||||
const selectCheckbox = document.createElement("input");
|
||||
selectCheckbox.type = "checkbox";
|
||||
selectCheckbox.classList.add("pdf-actions_checkbox", "form-check-input");
|
||||
selectCheckbox.id = `selectPageCheckbox`;
|
||||
selectCheckbox.checked = window.selectAll;
|
||||
|
||||
div.appendChild(selectCheckbox);
|
||||
|
||||
//only show whenpage select mode is active
|
||||
if (!window.selectPage) {
|
||||
selectCheckbox.classList.add("hidden");
|
||||
} else {
|
||||
selectCheckbox.classList.remove("hidden");
|
||||
}
|
||||
|
||||
selectCheckbox.onchange = () => {
|
||||
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
|
||||
let selectPageCommand = new SelectPageCommand(pageNumber, selectCheckbox);
|
||||
selectPageCommand.execute();
|
||||
};
|
||||
|
||||
const insertFileButtonContainer = document.createElement("div");
|
||||
|
||||
insertFileButtonContainer.classList.add(
|
||||
"pdf-actions_insert-file-button-container",
|
||||
leftDirection,
|
||||
`align-center-${leftDirection}`,
|
||||
);
|
||||
|
||||
const insertFileButton = document.createElement("button");
|
||||
insertFileButton.classList.add("btn", "btn-primary");
|
||||
moveUp.setAttribute('title', window.translations.addFile);
|
||||
insertFileButton.innerHTML = `<span class="material-symbols-rounded">add</span>`;
|
||||
insertFileButton.onclick = this.insertFileButtonCallback;
|
||||
insertFileButtonContainer.appendChild(insertFileButton);
|
||||
|
||||
const splitFileButton = document.createElement("button");
|
||||
splitFileButton.classList.add("btn", "btn-primary");
|
||||
splitFileButton.setAttribute('title', window.translations.split);
|
||||
splitFileButton.innerHTML = `<span class="material-symbols-rounded">cut</span>`;
|
||||
splitFileButton.onclick = this.splitFileButtonCallback;
|
||||
insertFileButtonContainer.appendChild(splitFileButton);
|
||||
|
||||
const insertFileBlankButton = document.createElement("button");
|
||||
insertFileBlankButton.classList.add("btn", "btn-primary");
|
||||
insertFileBlankButton.setAttribute('title', window.translations.insertPageBreak);
|
||||
insertFileBlankButton.innerHTML = `<span class="material-symbols-rounded">insert_page_break</span>`;
|
||||
insertFileBlankButton.onclick = this.insertFileBlankButtonCallback;
|
||||
insertFileButtonContainer.appendChild(insertFileBlankButton);
|
||||
|
||||
div.appendChild(insertFileButtonContainer);
|
||||
|
||||
// add this button to every element, but only show it on the last one :D
|
||||
const insertFileButtonRightContainer = document.createElement("div");
|
||||
insertFileButtonRightContainer.classList.add(
|
||||
"pdf-actions_insert-file-button-container",
|
||||
rightDirection,
|
||||
`align-center-${rightDirection}`,
|
||||
);
|
||||
|
||||
const insertFileButtonRight = document.createElement("button");
|
||||
insertFileButtonRight.classList.add("btn", "btn-primary");
|
||||
insertFileButtonRight.innerHTML = `<span class="material-symbols-rounded">add</span>`;
|
||||
insertFileButtonRight.onclick = () => addFiles();
|
||||
insertFileButtonRightContainer.appendChild(insertFileButtonRight);
|
||||
|
||||
div.appendChild(insertFileButtonRightContainer);
|
||||
|
||||
const adaptPageNumber = (pageNumber, div) => {
|
||||
const pageNumberElement = document.createElement("span");
|
||||
pageNumberElement.classList.add("page-number");
|
||||
pageNumberElement.textContent = pageNumber;
|
||||
|
||||
div.insertBefore(pageNumberElement, div.firstChild);
|
||||
};
|
||||
|
||||
div.addEventListener("mouseenter", () => {
|
||||
window.updatePageNumbersAndCheckboxes();
|
||||
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
|
||||
adaptPageNumber(pageNumber, div);
|
||||
const checkbox = document.getElementById(`selectPageCheckbox-${pageNumber}`);
|
||||
if (checkbox && !window.selectPage) {
|
||||
checkbox.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
div.addEventListener("mouseleave", () => {
|
||||
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
|
||||
const pageNumberElement = div.querySelector(".page-number");
|
||||
if (pageNumberElement) {
|
||||
div.removeChild(pageNumberElement);
|
||||
}
|
||||
const checkbox = document.getElementById(`selectPageCheckbox-${pageNumber}`);
|
||||
if (checkbox && !window.selectPage) {
|
||||
checkbox.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("selectedPagesUpdated", () => {
|
||||
window.updateSelectedPagesDisplay();
|
||||
});
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
||||
export default PdfActionsManager;
|
||||
@@ -0,0 +1,966 @@
|
||||
import { MovePageCommand } from './commands/move-page.js';
|
||||
import { RemoveSelectedCommand } from './commands/remove.js';
|
||||
import { RotateAllCommand, RotateElementCommand } from './commands/rotate.js';
|
||||
import { SplitAllCommand } from './commands/split.js';
|
||||
import { UndoManager } from './UndoManager.js';
|
||||
import { PageBreakCommand } from './commands/page-break.js';
|
||||
import { AddFilesCommand } from './commands/add-page.js';
|
||||
import { DecryptFile } from '../DecryptFiles.js';
|
||||
import { CommandSequence } from './commands/commands-sequence.js';
|
||||
|
||||
class PdfContainer {
|
||||
fileName;
|
||||
pagesContainer;
|
||||
pagesContainerWrapper;
|
||||
pdfAdapters;
|
||||
downloadLink;
|
||||
undoManager;
|
||||
|
||||
constructor(id, wrapperId, pdfAdapters, undoManager) {
|
||||
this.pagesContainer = document.getElementById(id);
|
||||
this.pagesContainerWrapper = document.getElementById(wrapperId);
|
||||
this.downloadLink = null;
|
||||
this.movePageTo = this.movePageTo.bind(this);
|
||||
this.addFiles = this.addFiles.bind(this);
|
||||
this.addFilesFromFiles = this.addFilesFromFiles.bind(this);
|
||||
this.rotateElement = this.rotateElement.bind(this);
|
||||
this.rotateAll = this.rotateAll.bind(this);
|
||||
this.exportPdf = this.exportPdf.bind(this);
|
||||
this.updateFilename = this.updateFilename.bind(this);
|
||||
this.setDownloadAttribute = this.setDownloadAttribute.bind(this);
|
||||
this.preventIllegalChars = this.preventIllegalChars.bind(this);
|
||||
this.addImageFile = this.addImageFile.bind(this);
|
||||
this.nameAndArchiveFiles = this.nameAndArchiveFiles.bind(this);
|
||||
this.splitPDF = this.splitPDF.bind(this);
|
||||
this.splitAll = this.splitAll.bind(this);
|
||||
this.deleteSelected = this.deleteSelected.bind(this);
|
||||
this.selectAll = this.selectAll.bind(this);
|
||||
this.deselectAll = this.deselectAll.bind(this);
|
||||
this.updateSelectedPagesDisplay = this.updateSelectedPagesDisplay.bind(this);
|
||||
this.toggleSelectPageVisibility = this.toggleSelectPageVisibility.bind(this);
|
||||
this.updatePagesFromCSV = this.updatePagesFromCSV.bind(this);
|
||||
this.addFilesBlankAll = this.addFilesBlankAll.bind(this);
|
||||
this.removeAllElements = this.removeAllElements.bind(this);
|
||||
this.resetPages = this.resetPages.bind(this);
|
||||
|
||||
this.decryptFile = new DecryptFile();
|
||||
|
||||
this.undoManager = undoManager || new UndoManager();
|
||||
|
||||
this.pdfAdapters = pdfAdapters;
|
||||
|
||||
this.pdfAdapters.forEach((adapter) => {
|
||||
adapter.setActions({
|
||||
movePageTo: this.movePageTo,
|
||||
addFiles: this.addFiles,
|
||||
rotateElement: this.rotateElement,
|
||||
updateFilename: this.updateFilename,
|
||||
deleteSelected: this.deleteSelected,
|
||||
});
|
||||
});
|
||||
|
||||
window.addFiles = this.addFiles;
|
||||
window.exportPdf = this.exportPdf;
|
||||
window.rotateAll = this.rotateAll;
|
||||
window.splitAll = this.splitAll;
|
||||
window.deleteSelected = this.deleteSelected;
|
||||
window.selectAll = this.selectAll;
|
||||
window.deselectAll = this.deselectAll;
|
||||
window.updateSelectedPagesDisplay = this.updateSelectedPagesDisplay;
|
||||
window.toggleSelectPageVisibility = this.toggleSelectPageVisibility;
|
||||
window.updatePagesFromCSV = this.updatePagesFromCSV;
|
||||
window.updateSelectedPagesDisplay = this.updateSelectedPagesDisplay;
|
||||
window.updatePageNumbersAndCheckboxes = this.updatePageNumbersAndCheckboxes;
|
||||
window.addFilesBlankAll = this.addFilesBlankAll;
|
||||
window.removeAllElements = this.removeAllElements;
|
||||
window.resetPages = this.resetPages;
|
||||
|
||||
let undoBtn = document.getElementById('undo-btn');
|
||||
let redoBtn = document.getElementById('redo-btn');
|
||||
|
||||
document.addEventListener('undo-manager-update', (e) => {
|
||||
let canUndo = e.detail.canUndo;
|
||||
let canRedo = e.detail.canRedo;
|
||||
|
||||
undoBtn.disabled = !canUndo;
|
||||
redoBtn.disabled = !canRedo;
|
||||
});
|
||||
|
||||
window.undo = () => {
|
||||
if (undoManager.canUndo()) undoManager.undo();
|
||||
else {
|
||||
undoBtn.disabled = !undoManager.canUndo();
|
||||
redoBtn.disabled = !undoManager.canRedo();
|
||||
}
|
||||
};
|
||||
|
||||
window.redo = () => {
|
||||
if (undoManager.canRedo()) undoManager.redo();
|
||||
else {
|
||||
undoBtn.disabled = !undoManager.canUndo();
|
||||
redoBtn.disabled = !undoManager.canRedo();
|
||||
}
|
||||
};
|
||||
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
|
||||
filenameInput.onkeyup = this.updateFilename;
|
||||
filenameInput.onkeydown = this.preventIllegalChars;
|
||||
filenameInput.disabled = false;
|
||||
filenameInput.innerText = '';
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
movePagesTo(startElements, endElement, scrollTo = false) {
|
||||
let commands = [];
|
||||
startElements.forEach((page) => {
|
||||
let command = new MovePageCommand(
|
||||
page,
|
||||
endElement,
|
||||
this.pagesContainer,
|
||||
this.pagesContainerWrapper,
|
||||
scrollTo
|
||||
)
|
||||
command.execute();
|
||||
commands.push(command);
|
||||
})
|
||||
|
||||
let commandSequence = new CommandSequence(commands);
|
||||
this.undoManager.pushUndoClearRedo(commandSequence);
|
||||
return commandSequence;
|
||||
}
|
||||
|
||||
showButton(button, show) {
|
||||
button.classList.toggle('hidden', !show);
|
||||
}
|
||||
|
||||
movePageTo(startElements, endElement, scrollTo = false) {
|
||||
|
||||
if (Array.isArray(startElements)){
|
||||
return this.movePagesTo(startElements, endElement, scrollTo = false);
|
||||
}
|
||||
|
||||
let movePageCommand = new MovePageCommand(
|
||||
startElements,
|
||||
endElement,
|
||||
this.pagesContainer,
|
||||
this.pagesContainerWrapper,
|
||||
scrollTo
|
||||
);
|
||||
|
||||
movePageCommand.execute();
|
||||
this.undoManager.pushUndoClearRedo(movePageCommand);
|
||||
return movePageCommand;
|
||||
}
|
||||
|
||||
async addFiles(element) {
|
||||
let addFilesCommand = new AddFilesCommand(
|
||||
element,
|
||||
window.selectedPages,
|
||||
this.addFilesAction.bind(this),
|
||||
this.pagesContainer
|
||||
);
|
||||
|
||||
await addFilesCommand.execute();
|
||||
|
||||
this.undoManager.pushUndoClearRedo(addFilesCommand);
|
||||
window.tooltipSetup();
|
||||
|
||||
}
|
||||
|
||||
async addFilesAction(nextSiblingElement) {
|
||||
let pages = [];
|
||||
return new Promise((resolve) => {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.setAttribute('accept', 'application/pdf,image/*');
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const files = e.target.files;
|
||||
if (files.length > 0) {
|
||||
pages = await this.addFilesFromFiles(files, nextSiblingElement, pages);
|
||||
this.updateFilename(files[0].name);
|
||||
|
||||
if(window.selectPage){
|
||||
this.showButton(document.getElementById('select-pages-container'), true);
|
||||
}
|
||||
}
|
||||
resolve(pages);
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
async handleDroppedFiles(files, nextSiblingElement = null) {
|
||||
if (files.length > 0) {
|
||||
const pages = await this.addFilesFromFiles(files, nextSiblingElement, []);
|
||||
this.updateFilename(files[0]?.name || 'untitled');
|
||||
|
||||
if(window.selectPage) {
|
||||
this.showButton(document.getElementById('select-pages-container'), true);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
}
|
||||
|
||||
async addFilesFromFiles(files, nextSiblingElement, pages) {
|
||||
this.fileName = files[0].name;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
const startTime = Date.now();
|
||||
let processingTime,
|
||||
errorMessage = null,
|
||||
pageCount = 0;
|
||||
|
||||
try {
|
||||
let decryptedFile = files[i];
|
||||
let isEncrypted = false;
|
||||
let requiresPassword = false;
|
||||
await this.decryptFile
|
||||
.checkFileEncrypted(decryptedFile)
|
||||
.then((result) => {
|
||||
isEncrypted = result.isEncrypted;
|
||||
requiresPassword = result.requiresPassword;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
if (decryptedFile.type === 'application/pdf' && isEncrypted) {
|
||||
decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
|
||||
if (!decryptedFile) {
|
||||
throw new Error('File decryption failed.');
|
||||
}
|
||||
}
|
||||
|
||||
if (decryptedFile.type === 'application/pdf') {
|
||||
const { renderer, pdfDocument } = await this.loadFile(decryptedFile);
|
||||
pageCount = renderer.pageCount || 0;
|
||||
pages = await this.addPdfFile(renderer, pdfDocument, nextSiblingElement, pages);
|
||||
} else if (decryptedFile.type.startsWith('image/')) {
|
||||
pages = await this.addImageFile(decryptedFile, nextSiblingElement, pages);
|
||||
}
|
||||
|
||||
processingTime = Date.now() - startTime;
|
||||
this.captureFileProcessingEvent(true, decryptedFile, processingTime, null, pageCount);
|
||||
} catch (error) {
|
||||
processingTime = Date.now() - startTime;
|
||||
errorMessage = error.message || 'Unknown error';
|
||||
this.captureFileProcessingEvent(false, files[i], processingTime, errorMessage, pageCount);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.enable-on-file').forEach((element) => {
|
||||
element.disabled = false;
|
||||
});
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
captureFileProcessingEvent(success, file, processingTime, errorMessage, pageCount) {
|
||||
try {
|
||||
if (analyticsEnabled) {
|
||||
posthog.capture('file_processing', {
|
||||
success,
|
||||
file_type: file?.type || 'unknown',
|
||||
file_size: file?.size || 0,
|
||||
processing_time: processingTime,
|
||||
error_message: errorMessage,
|
||||
pdf_pages: pageCount,
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async addFilesBlank(nextSiblingElement, pages) {
|
||||
let doc = await PDFLib.PDFDocument.create();
|
||||
let docBytes = await doc.save();
|
||||
|
||||
const url = URL.createObjectURL(new Blob([docBytes], { type: 'application/pdf' }));
|
||||
|
||||
const renderer = await this.toRenderer(url);
|
||||
pages = await this.addPdfFile(renderer, doc, nextSiblingElement, pages);
|
||||
return pages;
|
||||
}
|
||||
|
||||
rotateElement(element, deg) {
|
||||
let rotateCommand = new RotateElementCommand(element, deg);
|
||||
rotateCommand.execute();
|
||||
|
||||
return rotateCommand;
|
||||
}
|
||||
|
||||
async addPdfFile(renderer, pdfDocument, nextSiblingElement, pages) {
|
||||
for (var i = 0; i < renderer.pageCount; i++) {
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.classList.add('page-container');
|
||||
div.id = 'page-container-' + (i + 1);
|
||||
var img = document.createElement('img');
|
||||
img.classList.add('page-image');
|
||||
const imageSrc = await renderer.renderPage(i);
|
||||
img.src = imageSrc;
|
||||
img.pageIdx = i;
|
||||
img.rend = renderer;
|
||||
img.doc = pdfDocument;
|
||||
div.appendChild(img);
|
||||
|
||||
this.pdfAdapters.forEach((adapter) => {
|
||||
adapter.adapt?.(div);
|
||||
});
|
||||
|
||||
if (nextSiblingElement) {
|
||||
this.pagesContainer.insertBefore(div, nextSiblingElement);
|
||||
} else {
|
||||
this.pagesContainer.appendChild(div);
|
||||
}
|
||||
|
||||
pages.push(div);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
async addImageFile(file, nextSiblingElement, pages) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('page-container');
|
||||
|
||||
var img = document.createElement('img');
|
||||
img.classList.add('page-image');
|
||||
img.src = URL.createObjectURL(file);
|
||||
div.appendChild(img);
|
||||
|
||||
this.pdfAdapters.forEach((adapter) => {
|
||||
adapter.adapt?.(div);
|
||||
});
|
||||
if (nextSiblingElement) {
|
||||
this.pagesContainer.insertBefore(div, nextSiblingElement);
|
||||
} else {
|
||||
this.pagesContainer.appendChild(div);
|
||||
}
|
||||
pages.push(div);
|
||||
return pages;
|
||||
}
|
||||
|
||||
async loadFile(file) {
|
||||
var objectUrl = URL.createObjectURL(file);
|
||||
var pdfDocument = await this.toPdfLib(objectUrl);
|
||||
var renderer = await this.toRenderer(objectUrl);
|
||||
return { renderer, pdfDocument };
|
||||
}
|
||||
|
||||
async toRenderer(objectUrl) {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
const pdf = await pdfjsLib.getDocument(objectUrl).promise;
|
||||
return {
|
||||
document: pdf,
|
||||
pageCount: pdf.numPages,
|
||||
renderPage: async function (pageIdx) {
|
||||
const page = await this.document.getPage(pageIdx + 1);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
// set the canvas size to the size of the page
|
||||
if (page.rotate == 90 || page.rotate == 270) {
|
||||
canvas.width = page.view[3];
|
||||
canvas.height = page.view[2];
|
||||
} else {
|
||||
canvas.width = page.view[2];
|
||||
canvas.height = page.view[3];
|
||||
}
|
||||
|
||||
// render the page onto the canvas
|
||||
var renderContext = {
|
||||
canvasContext: canvas.getContext('2d'),
|
||||
viewport: page.getViewport({ scale: 1 }),
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
return canvas.toDataURL();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async toPdfLib(objectUrl) {
|
||||
const existingPdfBytes = await fetch(objectUrl).then((res) => res.arrayBuffer());
|
||||
const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
return pdfDoc;
|
||||
}
|
||||
|
||||
rotateAll(deg) {
|
||||
let elementsToRotate = [];
|
||||
for (let i = 0; i < this.pagesContainer.childNodes.length; i++) {
|
||||
const child = this.pagesContainer.children[i];
|
||||
if (!child) continue;
|
||||
|
||||
const pageIndex = i + 1;
|
||||
//if in page select mode is active rotate only selected pages
|
||||
if (window.selectPage && !window.selectedPages.includes(pageIndex)) continue;
|
||||
|
||||
const img = child.querySelector('img');
|
||||
if (!img) continue;
|
||||
|
||||
elementsToRotate.push(img);
|
||||
}
|
||||
|
||||
let rotateAllCommand = new RotateAllCommand(elementsToRotate, deg);
|
||||
rotateAllCommand.execute();
|
||||
|
||||
this.undoManager.pushUndoClearRedo(rotateAllCommand);
|
||||
}
|
||||
|
||||
removeAllElements() {
|
||||
let pageContainerNodeList = document.querySelectorAll('.page-container');
|
||||
for (var i = 0; i < pageContainerNodeList.length; i++) {
|
||||
pageContainerNodeList[i].remove();
|
||||
}
|
||||
document.querySelectorAll('.enable-on-file').forEach((element) => {
|
||||
element.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
deleteSelected() {
|
||||
window.selectedPages.sort((a, b) => a - b);
|
||||
let removeSelectedCommand = new RemoveSelectedCommand(
|
||||
this.pagesContainer,
|
||||
window.selectedPages,
|
||||
this.updatePageNumbersAndCheckboxes
|
||||
);
|
||||
removeSelectedCommand.execute();
|
||||
this.undoManager.pushUndoClearRedo(removeSelectedCommand);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const checkboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const selectIcon = document.getElementById('select-All-Container');
|
||||
const deselectIcon = document.getElementById('deselect-All-Container');
|
||||
|
||||
this.showButton(selectIcon, false);
|
||||
this.showButton(deselectIcon, true);
|
||||
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = true;
|
||||
|
||||
const pageNumber = Array.from(checkbox.parentNode.parentNode.children).indexOf(checkbox.parentNode) + 1;
|
||||
|
||||
if (!window.selectedPages.includes(pageNumber)) {
|
||||
window.selectedPages.push(pageNumber);
|
||||
}
|
||||
});
|
||||
|
||||
this.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
const checkboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const selectIcon = document.getElementById('select-All-Container');
|
||||
const deselectIcon = document.getElementById('deselect-All-Container');
|
||||
|
||||
this.showButton(selectIcon, true);
|
||||
this.showButton(deselectIcon, false);
|
||||
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
|
||||
const pageNumber = Array.from(checkbox.parentNode.parentNode.children).indexOf(checkbox.parentNode) + 1;
|
||||
|
||||
const index = window.selectedPages.indexOf(pageNumber);
|
||||
if (index !== -1) {
|
||||
window.selectedPages.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
this.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
parseCSVInput(csvInput, maxPageIndex) {
|
||||
const pages = new Set();
|
||||
|
||||
csvInput.split(',').forEach((item) => {
|
||||
const range = item.split('-').map((p) => parseInt(p.trim()));
|
||||
if (range.length === 2) {
|
||||
const [start, end] = range;
|
||||
for (let i = start; i <= end && i <= maxPageIndex; i++) {
|
||||
if (i > 0) {
|
||||
// Ensure the page number is greater than 0
|
||||
pages.add(i);
|
||||
}
|
||||
}
|
||||
} else if (range.length === 1 && Number.isInteger(range[0])) {
|
||||
const page = range[0];
|
||||
if (page > 0 && page <= maxPageIndex) {
|
||||
// Ensure page is within valid range
|
||||
pages.add(page);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(pages).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
updatePagesFromCSV() {
|
||||
const csvInput = document.getElementById('csv-input').value;
|
||||
|
||||
const allPages = this.pagesContainer.querySelectorAll('.page-container');
|
||||
const maxPageIndex = allPages.length;
|
||||
|
||||
window.selectedPages = this.parseCSVInput(csvInput, maxPageIndex);
|
||||
|
||||
this.updateSelectedPagesDisplay();
|
||||
|
||||
const allCheckboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
allCheckboxes.forEach((checkbox) => {
|
||||
const page = parseInt(checkbox.getAttribute('data-page-number'));
|
||||
checkbox.checked = window.selectedPages.includes(page);
|
||||
});
|
||||
}
|
||||
|
||||
formatSelectedPages(pages) {
|
||||
if (pages.length === 0) return '';
|
||||
|
||||
pages.sort((a, b) => a - b); // Sort the page numbers in ascending order
|
||||
const ranges = [];
|
||||
let start = pages[0];
|
||||
let end = start;
|
||||
|
||||
for (let i = 1; i < pages.length; i++) {
|
||||
if (pages[i] === end + 1) {
|
||||
// Consecutive page, update end
|
||||
end = pages[i];
|
||||
} else {
|
||||
// Non-consecutive page, finalize current range
|
||||
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
||||
start = pages[i];
|
||||
end = start;
|
||||
}
|
||||
}
|
||||
// Add the last range
|
||||
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
||||
|
||||
return ranges.join(', ');
|
||||
}
|
||||
|
||||
updateSelectedPagesDisplay() {
|
||||
const selectedPagesList = document.getElementById('selected-pages-list');
|
||||
const selectedPagesInput = document.getElementById('csv-input');
|
||||
selectedPagesList.innerHTML = ''; // Clear the list
|
||||
window.selectedPages.sort((a, b) => a - b);
|
||||
window.selectedPages.forEach((page) => {
|
||||
const pageItem = document.createElement('div');
|
||||
pageItem.className = 'page-item';
|
||||
|
||||
const pageNumber = document.createElement('span');
|
||||
const pagelabel = /*[[#{multiTool.page}]]*/ 'Page';
|
||||
pageNumber.className = 'selected-page-number';
|
||||
pageNumber.innerText = `${pagelabel} ${page}`;
|
||||
pageItem.appendChild(pageNumber);
|
||||
|
||||
const removeBtn = document.createElement('span');
|
||||
removeBtn.className = 'remove-btn';
|
||||
removeBtn.innerHTML = '✕';
|
||||
|
||||
// Remove page from selected pages list and update display and checkbox
|
||||
removeBtn.onclick = () => {
|
||||
window.selectedPages = window.selectedPages.filter((p) => p !== page);
|
||||
this.updateSelectedPagesDisplay();
|
||||
|
||||
const checkbox = document.getElementById(`selectPageCheckbox-${page}`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
};
|
||||
|
||||
pageItem.appendChild(removeBtn);
|
||||
selectedPagesList.appendChild(pageItem);
|
||||
});
|
||||
|
||||
// Update the input field with the formatted page list
|
||||
selectedPagesInput.value = this.formatSelectedPages(window.selectedPages);
|
||||
|
||||
const selectIcon = document.getElementById('select-All-Container');
|
||||
const deselectIcon = document.getElementById('deselect-All-Container');
|
||||
|
||||
if (window.selectPage) { // Check if selectPage mode is active
|
||||
console.log("Page Select on. Showing buttons");
|
||||
//Check if no pages are selected
|
||||
if (window.selectedPages.length === 0) {
|
||||
this.showButton(selectIcon, true);
|
||||
this.showButton(deselectIcon, false);
|
||||
} else {
|
||||
this.showButton(deselectIcon, true);
|
||||
}
|
||||
|
||||
//Check if all pages are selected
|
||||
const allCheckboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const allSelected = Array.from(allCheckboxes).every((checkbox) => checkbox.checked);
|
||||
if (allSelected) {
|
||||
this.showButton(selectIcon, false);
|
||||
this.showButton(deselectIcon, true);
|
||||
} else {
|
||||
this.showButton(selectIcon, true);
|
||||
}
|
||||
} else {
|
||||
console.log("Page Select off. Hidding buttons");
|
||||
this.showButton(selectIcon, false);
|
||||
this.showButton(deselectIcon, false);
|
||||
}
|
||||
}
|
||||
|
||||
parsePageRanges(ranges) {
|
||||
const pages = new Set();
|
||||
|
||||
ranges.split(',').forEach((range) => {
|
||||
const [start, end] = range.split('-').map(Number);
|
||||
if (end) {
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.add(i);
|
||||
}
|
||||
} else {
|
||||
pages.add(start);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(pages).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
async addFilesBlankAll() {
|
||||
const allPages = this.pagesContainer.querySelectorAll('.page-container');
|
||||
|
||||
let pageBreakCommand = new PageBreakCommand(
|
||||
allPages,
|
||||
window.selectPage,
|
||||
window.selectedPages,
|
||||
this.addFilesBlank.bind(this),
|
||||
this.pagesContainer
|
||||
);
|
||||
|
||||
await pageBreakCommand.execute();
|
||||
|
||||
this.undoManager.pushUndoClearRedo(pageBreakCommand);
|
||||
}
|
||||
|
||||
splitAll() {
|
||||
const allPages = this.pagesContainer.querySelectorAll('.page-container');
|
||||
let splitAllCommand = new SplitAllCommand(allPages, window.selectPage, window.selectedPages, 'split-before');
|
||||
splitAllCommand.execute();
|
||||
|
||||
this.undoManager.pushUndoClearRedo(splitAllCommand);
|
||||
}
|
||||
|
||||
async splitPDF(baseDocBytes, splitters) {
|
||||
const baseDocument = await PDFLib.PDFDocument.load(baseDocBytes);
|
||||
const pageNum = baseDocument.getPages().length;
|
||||
|
||||
splitters.sort((a, b) => a - b); // We'll sort the separator indexes just in case querySelectorAll does something funny.
|
||||
splitters.push(pageNum); // We'll also add a faux separator at the end in order to get the pages after the last separator.
|
||||
|
||||
const splitDocuments = [];
|
||||
for (const splitterPosition of splitters) {
|
||||
const subDocument = await PDFLib.PDFDocument.create();
|
||||
|
||||
const splitterIndex = splitters.indexOf(splitterPosition);
|
||||
|
||||
let firstPage = splitterIndex === 0 ? 0 : splitters[splitterIndex - 1];
|
||||
|
||||
const pageIndices = Array.from({ length: splitterPosition - firstPage }, (value, key) => firstPage + key);
|
||||
|
||||
const copiedPages = await subDocument.copyPages(baseDocument, pageIndices);
|
||||
|
||||
copiedPages.forEach((copiedPage) => {
|
||||
subDocument.addPage(copiedPage);
|
||||
});
|
||||
|
||||
const subDocumentBytes = await subDocument.save();
|
||||
|
||||
splitDocuments.push(subDocumentBytes);
|
||||
}
|
||||
|
||||
return splitDocuments;
|
||||
}
|
||||
|
||||
async nameAndArchiveFiles(pdfBytesArray, baseNameString) {
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < pdfBytesArray.length; i++) {
|
||||
const documentBlob = new Blob([pdfBytesArray[i]], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
zip.file(baseNameString + '-' + (i + 1) + '.pdf', documentBlob);
|
||||
}
|
||||
|
||||
return zip;
|
||||
}
|
||||
|
||||
async exportPdf(selected) {
|
||||
const pdfDoc = await PDFLib.PDFDocument.create();
|
||||
const pageContainers = this.pagesContainer.querySelectorAll('.page-container'); // Select all .page-container elements
|
||||
for (var i = 0; i < pageContainers.length; i++) {
|
||||
if (!selected || window.selectedPages.includes(i + 1)) {
|
||||
const img = pageContainers[i].querySelector('img'); // Find the img element within each .page-container
|
||||
if (!img) continue;
|
||||
let page;
|
||||
if (img.doc) {
|
||||
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx]);
|
||||
page = pages[0];
|
||||
pdfDoc.addPage(page);
|
||||
} else {
|
||||
page = pdfDoc.addPage([img.naturalWidth, img.naturalHeight]);
|
||||
const imageBytes = await fetch(img.src).then((res) => res.arrayBuffer());
|
||||
const uint8Array = new Uint8Array(imageBytes);
|
||||
const imageType = detectImageType(uint8Array);
|
||||
|
||||
let image;
|
||||
switch (imageType) {
|
||||
case 'PNG':
|
||||
image = await pdfDoc.embedPng(imageBytes);
|
||||
break;
|
||||
case 'JPEG':
|
||||
image = await pdfDoc.embedJpg(imageBytes);
|
||||
break;
|
||||
case 'TIFF':
|
||||
image = await pdfDoc.embedTiff(imageBytes);
|
||||
break;
|
||||
case 'GIF':
|
||||
console.warn(`Unsupported image type: ${imageType}`);
|
||||
continue; // Skip this image
|
||||
default:
|
||||
console.warn(`Unsupported image type: ${imageType}`);
|
||||
continue; // Skip this image
|
||||
}
|
||||
page.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
const rotation = img.style.rotate;
|
||||
if (rotation) {
|
||||
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ''));
|
||||
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle));
|
||||
}
|
||||
}
|
||||
}
|
||||
pdfDoc.setCreator(stirlingPDFLabel);
|
||||
pdfDoc.setProducer(stirlingPDFLabel);
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
|
||||
let inputArr = filenameInput.value.split('.');
|
||||
|
||||
if (inputArr !== null && inputArr !== undefined && inputArr.length > 0) {
|
||||
inputArr = inputArr.filter((n) => n); // remove all empty strings, nulls or undefined
|
||||
|
||||
if (inputArr.length > 1) {
|
||||
inputArr.pop(); // remove right part after last dot
|
||||
}
|
||||
|
||||
filenameInput.value = inputArr.join('');
|
||||
this.fileName = filenameInput.value;
|
||||
}
|
||||
|
||||
const separators = this.pagesContainer.querySelectorAll('.split-before');
|
||||
if (separators.length !== 0) {
|
||||
// Split the pdf if there are separators.
|
||||
const baseName = this.fileName ? this.fileName : 'managed';
|
||||
|
||||
const pagesArray = Array.from(this.pagesContainer.children);
|
||||
const splitters = [];
|
||||
separators.forEach((page) => {
|
||||
const pageIndex = pagesArray.indexOf(page);
|
||||
if (pageIndex !== 0) {
|
||||
splitters.push(pageIndex);
|
||||
}
|
||||
});
|
||||
|
||||
const splitDocuments = await this.splitPDF(pdfBytes, splitters);
|
||||
const archivedDocuments = await this.nameAndArchiveFiles(splitDocuments, baseName);
|
||||
|
||||
const self = this;
|
||||
archivedDocuments.generateAsync({ type: 'base64' }).then(function (base64) {
|
||||
const url = 'data:application/zip;base64,' + base64;
|
||||
self.downloadLink = document.createElement('a');
|
||||
self.downloadLink.href = url;
|
||||
self.downloadLink.setAttribute('download', baseName + '.zip');
|
||||
self.downloadLink.setAttribute('target', '_blank');
|
||||
self.downloadLink.click();
|
||||
});
|
||||
} else {
|
||||
// Continue normally if there are no separators
|
||||
|
||||
const url = URL.createObjectURL(pdfBlob);
|
||||
const downloadOption = localStorage.getItem('downloadOption');
|
||||
|
||||
if (!filenameInput.value.includes('.pdf')) {
|
||||
filenameInput.value = filenameInput.value + '.pdf';
|
||||
this.fileName = filenameInput.value;
|
||||
}
|
||||
|
||||
if (downloadOption === 'sameWindow') {
|
||||
// Open the file in the same window
|
||||
window.location.href = url;
|
||||
} else if (downloadOption === 'newWindow') {
|
||||
// Open the file in a new window
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
// Download the file
|
||||
this.downloadLink = document.createElement('a');
|
||||
this.downloadLink.id = 'download-link';
|
||||
this.downloadLink.href = url;
|
||||
// downloadLink.download = this.fileName ? this.fileName : 'managed.pdf';
|
||||
// downloadLink.download = this.fileName;
|
||||
this.downloadLink.setAttribute('download', this.fileName ? this.fileName : 'managed.pdf');
|
||||
this.downloadLink.setAttribute('target', '_blank');
|
||||
this.downloadLink.onclick = this.setDownloadAttribute;
|
||||
this.downloadLink.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetPages() {
|
||||
const pageContainers = this.pagesContainer.querySelectorAll('.page-container');
|
||||
|
||||
pageContainers.forEach((container, index) => {
|
||||
container.id = 'page-container-' + (index + 1);
|
||||
});
|
||||
|
||||
const checkboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const selectIcon = document.getElementById('select-All-Container');
|
||||
const deselectIcon = document.getElementById('deselect-All-Container');
|
||||
|
||||
checkboxes.forEach((checkbox) => {
|
||||
const pageNumber = Array.from(checkbox.parentNode.parentNode.children).indexOf(checkbox.parentNode) + 1;
|
||||
|
||||
const index = window.selectedPages.indexOf(pageNumber);
|
||||
if (index !== -1) {
|
||||
window.selectedPages.splice(index, 1);
|
||||
}
|
||||
});
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
|
||||
setDownloadAttribute() {
|
||||
this.downloadLink.setAttribute('download', this.fileName ? this.fileName : 'managed.pdf');
|
||||
}
|
||||
|
||||
updateFilename(fileName = '') {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const pagesContainer = document.getElementById('pages-container');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
|
||||
downloadBtn.disabled = pagesContainer.childElementCount === 0;
|
||||
|
||||
if (!this.fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
if (!filenameInput.value) {
|
||||
filenameInput.value = this.fileName;
|
||||
}
|
||||
}
|
||||
|
||||
preventIllegalChars(e) {
|
||||
// const filenameInput = document.getElementById('filename-input');
|
||||
//
|
||||
// filenameInput.value = filenameInput.value.replace('.pdf', '');
|
||||
//
|
||||
// // prevent .
|
||||
// if (filenameInput.value.includes('.')) {
|
||||
// filenameInput.value.replace('.','');
|
||||
// }
|
||||
}
|
||||
|
||||
toggleSelectPageVisibility() {
|
||||
window.selectPage = !window.selectPage;
|
||||
const checkboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.classList.toggle('hidden', !window.selectPage);
|
||||
});
|
||||
const deleteButton = document.getElementById('delete-button');
|
||||
deleteButton.classList.toggle('hidden', !window.selectPage);
|
||||
const selectedPages = document.getElementById('selected-pages-display');
|
||||
selectedPages.classList.toggle('hidden', !window.selectPage);
|
||||
|
||||
if(!window.selectPage)
|
||||
{
|
||||
this.showButton(document.getElementById('deselect-All-Container'), false);
|
||||
this.showButton(document.getElementById('select-All-Container'), false);
|
||||
|
||||
// Uncheck all checkboxes and clear selected pages
|
||||
const allCheckboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
allCheckboxes.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
window.selectedPages = [];
|
||||
this.updateSelectedPagesDisplay();
|
||||
}
|
||||
else{
|
||||
const allCheckboxes = document.querySelectorAll('.pdf-actions_checkbox');
|
||||
const allSelected = Array.from(allCheckboxes).every((checkbox) => checkbox.checked);
|
||||
if (!allSelected) {
|
||||
this.showButton(document.getElementById('select-All-Container'), true);
|
||||
}
|
||||
|
||||
if (window.selectedPages.length > 0) {
|
||||
this.showButton(document.getElementById('deselect-All-Container'), true);
|
||||
}
|
||||
}
|
||||
|
||||
const exportSelected = document.getElementById('export-selected-button');
|
||||
exportSelected.classList.toggle('hidden', !window.selectPage);
|
||||
const selectPagesButton = document.getElementById('select-pages-button');
|
||||
selectPagesButton.style.opacity = window.selectPage ? '1' : '0.5';
|
||||
|
||||
if (window.selectPage) {
|
||||
this.updatePageNumbersAndCheckboxes();
|
||||
}
|
||||
}
|
||||
|
||||
updatePageNumbersAndCheckboxes() {
|
||||
const pageDivs = document.querySelectorAll('.pdf-actions_container');
|
||||
|
||||
pageDivs.forEach((div, index) => {
|
||||
const pageNumber = index + 1;
|
||||
const checkbox = div.querySelector('.pdf-actions_checkbox');
|
||||
checkbox.id = `selectPageCheckbox-${pageNumber}`;
|
||||
checkbox.setAttribute('data-page-number', pageNumber);
|
||||
checkbox.checked = window.selectedPages.includes(pageNumber);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function detectImageType(uint8Array) {
|
||||
// Check for PNG signature
|
||||
if (uint8Array[0] === 137 && uint8Array[1] === 80 && uint8Array[2] === 78 && uint8Array[3] === 71) {
|
||||
return 'PNG';
|
||||
}
|
||||
|
||||
// Check for JPEG signature
|
||||
if (uint8Array[0] === 255 && uint8Array[1] === 216 && uint8Array[2] === 255) {
|
||||
return 'JPEG';
|
||||
}
|
||||
|
||||
// Check for TIFF signature (little-endian and big-endian)
|
||||
if (
|
||||
(uint8Array[0] === 73 && uint8Array[1] === 73 && uint8Array[2] === 42 && uint8Array[3] === 0) ||
|
||||
(uint8Array[0] === 77 && uint8Array[1] === 77 && uint8Array[2] === 0 && uint8Array[3] === 42)
|
||||
) {
|
||||
return 'TIFF';
|
||||
}
|
||||
|
||||
// Check for GIF signature
|
||||
if (uint8Array[0] === 71 && uint8Array[1] === 73 && uint8Array[2] === 70) {
|
||||
return 'GIF';
|
||||
}
|
||||
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
export default PdfContainer;
|
||||
@@ -0,0 +1,65 @@
|
||||
export class UndoManager {
|
||||
_undoStack;
|
||||
_redoStack;
|
||||
|
||||
constructor() {
|
||||
this._undoStack = [];
|
||||
this._redoStack = [];
|
||||
}
|
||||
|
||||
pushUndo(command) {
|
||||
this._undoStack.push(command);
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
pushRedo(command) {
|
||||
this._redoStack.push(command);
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
pushUndoClearRedo(command) {
|
||||
this._undoStack.push(command);
|
||||
this._redoStack = [];
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.canUndo()) return;
|
||||
|
||||
let cmd = this._undoStack.pop();
|
||||
cmd.undo();
|
||||
|
||||
this._redoStack.push(cmd);
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
canUndo() {
|
||||
return this._undoStack && this._undoStack.length > 0;
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!this.canRedo()) return;
|
||||
|
||||
let cmd = this._redoStack.pop();
|
||||
cmd.redo();
|
||||
|
||||
this._undoStack.push(cmd);
|
||||
this._dispatchStateChange();
|
||||
}
|
||||
|
||||
canRedo() {
|
||||
return this._redoStack && this._redoStack.length > 0;
|
||||
}
|
||||
|
||||
_dispatchStateChange() {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("undo-manager-update", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
canUndo: this.canUndo(),
|
||||
canRedo: this.canRedo(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {Command} from './command.js';
|
||||
|
||||
export class AddFilesCommand extends Command {
|
||||
constructor(element, selectedPages, addFilesAction, pagesContainer) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.selectedPages = selectedPages;
|
||||
this.addFilesAction = addFilesAction;
|
||||
this.pagesContainer = pagesContainer;
|
||||
this.addedElements = [];
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
undoBtn.disabled = true;
|
||||
if (this.element) {
|
||||
const newElement = await this.addFilesAction(this.element);
|
||||
if (newElement) {
|
||||
this.addedElements = newElement;
|
||||
}
|
||||
} else {
|
||||
const newElement = await this.addFilesAction(false);
|
||||
if (newElement) {
|
||||
this.addedElements = newElement;
|
||||
}
|
||||
}
|
||||
undoBtn.disabled = false;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.addedElements.forEach((element) => {
|
||||
const nextSibling = element.nextSibling;
|
||||
this.pagesContainer.removeChild(element);
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = '';
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
element._nextSibling = nextSibling;
|
||||
});
|
||||
this.addedElements = [];
|
||||
}
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class Command {
|
||||
execute() {}
|
||||
undo() {}
|
||||
redo() {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Command} from './command.js';
|
||||
|
||||
export class CommandSequence extends Command {
|
||||
constructor(commands) {
|
||||
super();
|
||||
this.commands = commands;
|
||||
|
||||
}
|
||||
execute() {
|
||||
this.commands.forEach((command) => command.execute())
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.commands.slice().reverse().forEach((command) => command.undo())
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class DeletePageCommand extends Command {
|
||||
constructor(element, pagesContainer) {
|
||||
super();
|
||||
|
||||
this.element = element;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
this.filenameInputValue = document.getElementById("filename-input").value;
|
||||
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
this.filenameParagraphText = filenameParagraph
|
||||
? filenameParagraph.innerText
|
||||
: "";
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.nextSibling = this.element.nextSibling;
|
||||
|
||||
this.pagesContainer.removeChild(this.element);
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
let node = this.nextSibling;
|
||||
if (node) this.pagesContainer.insertBefore(this.element, node);
|
||||
else this.pagesContainer.appendChild(this.element);
|
||||
|
||||
const pageNumberElement = this.element.querySelector(".page-number");
|
||||
if (pageNumberElement) {
|
||||
this.element.removeChild(pageNumberElement);
|
||||
}
|
||||
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = false;
|
||||
filenameInput.value = this.filenameInputValue;
|
||||
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
redo() {
|
||||
const pageNumberElement = this.element.querySelector(".page-number");
|
||||
if (pageNumberElement) {
|
||||
this.element.removeChild(pageNumberElement);
|
||||
}
|
||||
|
||||
this.pagesContainer.removeChild(this.element);
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {Command} from './command.js';
|
||||
|
||||
export class MovePageCommand extends Command {
|
||||
constructor(startElement, endElement, pagesContainer, pagesContainerWrapper, scrollTo = false) {
|
||||
super();
|
||||
|
||||
this.pagesContainer = pagesContainer;
|
||||
const childArray = Array.from(this.pagesContainer.childNodes);
|
||||
|
||||
this.startIndex = childArray.indexOf(startElement);
|
||||
this.endIndex = childArray.indexOf(endElement);
|
||||
|
||||
this.startElement = startElement;
|
||||
this.endElement = endElement;
|
||||
|
||||
this.scrollTo = scrollTo;
|
||||
this.pagesContainerWrapper = pagesContainerWrapper;
|
||||
}
|
||||
execute() {
|
||||
// Check & remove page number elements here too if they exist because Firefox doesn't fire the relevant event on page move.
|
||||
const pageNumberElement = this.startElement.querySelector('.page-number');
|
||||
if (pageNumberElement) {
|
||||
this.startElement.removeChild(pageNumberElement);
|
||||
}
|
||||
|
||||
this.pagesContainer.removeChild(this.startElement);
|
||||
if (!this.endElement) {
|
||||
this.pagesContainer.append(this.startElement);
|
||||
} else {
|
||||
this.pagesContainer.insertBefore(this.startElement, this.endElement);
|
||||
}
|
||||
|
||||
if (this.scrollTo) {
|
||||
const {width} = this.startElement.getBoundingClientRect();
|
||||
const vector = this.endIndex !== -1 && this.startIndex > this.endIndex ? 0 - width : width;
|
||||
|
||||
this.pagesContainerWrapper.scroll({
|
||||
left: this.pagesContainerWrapper.scrollLeft + vector,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.startElement) {
|
||||
this.pagesContainer.removeChild(this.startElement);
|
||||
let previousNeighbour = Array.from(this.pagesContainer.childNodes)[this.startIndex];
|
||||
previousNeighbour?.insertAdjacentElement('beforebegin', this.startElement)
|
||||
?? this.pagesContainer.append(this.startElement);
|
||||
}
|
||||
|
||||
if (this.scrollTo) {
|
||||
const {width} = this.startElement.getBoundingClientRect();
|
||||
const vector = this.endIndex === -1 || this.startIndex <= this.endIndex ? 0 - width : width;
|
||||
|
||||
this.pagesContainerWrapper.scroll({
|
||||
left: this.pagesContainerWrapper.scrollLeft - vector,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {Command} from './command.js';
|
||||
|
||||
export class PageBreakCommand extends Command {
|
||||
constructor(elements, isSelectedInWindow, selectedPages, pageBreakCallback, pagesContainer) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.isSelectedInWindow = isSelectedInWindow;
|
||||
this.selectedPages = selectedPages;
|
||||
this.pageBreakCallback = pageBreakCallback;
|
||||
this.pagesContainer = pagesContainer;
|
||||
this.addedElements = [];
|
||||
this.originalStates = Array.from(elements, (element) => ({
|
||||
element,
|
||||
hasContent: element.innerHTML.trim() !== '',
|
||||
}));
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
undoBtn.disabled = true;
|
||||
for (const [index, element] of this.elements.entries()) {
|
||||
if (!this.isSelectedInWindow || this.selectedPages.includes(index)) {
|
||||
if (index !== 0) {
|
||||
const newElement = await this.pageBreakCallback(element, this.addedElements);
|
||||
|
||||
if (newElement) {
|
||||
this.addedElements = newElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
undoBtn.disabled = false;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.addedElements.forEach((element) => {
|
||||
const nextSibling = element.nextSibling;
|
||||
|
||||
this.pagesContainer.removeChild(element);
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const filenameParagraph = document.getElementById('filename');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = '';
|
||||
filenameParagraph.innerText = '';
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
element._nextSibling = nextSibling;
|
||||
});
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class RemoveSelectedCommand extends Command {
|
||||
constructor(pagesContainer, selectedPages, updatePageNumbersAndCheckboxes) {
|
||||
super();
|
||||
this.pagesContainer = pagesContainer;
|
||||
this.selectedPages = selectedPages;
|
||||
|
||||
this.deletedChildren = [];
|
||||
|
||||
if (updatePageNumbersAndCheckboxes) {
|
||||
this.updatePageNumbersAndCheckboxes = updatePageNumbersAndCheckboxes;
|
||||
} else {
|
||||
const pageDivs = document.querySelectorAll(".pdf-actions_container");
|
||||
|
||||
pageDivs.forEach((div, index) => {
|
||||
const pageNumber = index + 1;
|
||||
const checkbox = div.querySelector(".pdf-actions_checkbox");
|
||||
checkbox.id = `selectPageCheckbox-${pageNumber}`;
|
||||
checkbox.setAttribute("data-page-number", pageNumber);
|
||||
checkbox.checked = window.selectedPages.includes(pageNumber);
|
||||
});
|
||||
}
|
||||
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
|
||||
this.originalFilenameInputValue = filenameInput ? filenameInput.value : "";
|
||||
if (filenameParagraph)
|
||||
this.originalFilenameParagraphText = filenameParagraph.innerText;
|
||||
}
|
||||
|
||||
execute() {
|
||||
let deletions = 0;
|
||||
|
||||
this.selectedPages.forEach((pageIndex) => {
|
||||
const adjustedIndex = pageIndex - 1 - deletions;
|
||||
const child = this.pagesContainer.children[adjustedIndex];
|
||||
if (child) {
|
||||
this.pagesContainer.removeChild(child);
|
||||
deletions++;
|
||||
|
||||
this.deletedChildren.push({
|
||||
idx: adjustedIndex,
|
||||
childNode: child,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
if (filenameInput) filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
if (filenameParagraph) filenameParagraph.innerText = "";
|
||||
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
window.selectedPages = [];
|
||||
this.updatePageNumbersAndCheckboxes();
|
||||
document.dispatchEvent(new Event("selectedPagesUpdated"));
|
||||
}
|
||||
|
||||
undo() {
|
||||
while (this.deletedChildren.length > 0) {
|
||||
let deletedChild = this.deletedChildren.pop();
|
||||
if (this.pagesContainer.children.length <= deletedChild.idx)
|
||||
this.pagesContainer.appendChild(deletedChild.childNode);
|
||||
else {
|
||||
this.pagesContainer.insertBefore(
|
||||
deletedChild.childNode,
|
||||
this.pagesContainer.children[deletedChild.idx]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pagesContainer.childElementCount > 0) {
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
const downloadBtn = document.getElementById("export-button");
|
||||
|
||||
if (filenameInput) filenameInput.disabled = false;
|
||||
filenameInput.value = this.originalFilenameInputValue;
|
||||
if (filenameParagraph)
|
||||
filenameParagraph.innerText = this.originalFilenameParagraphText;
|
||||
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
window.selectedPages = this.selectedPages;
|
||||
this.updatePageNumbersAndCheckboxes();
|
||||
document.dispatchEvent(new Event("selectedPagesUpdated"));
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class RotateElementCommand extends Command {
|
||||
constructor(element, degree) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
execute() {
|
||||
let lastTransform = this.element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const newAngle = lastAngle + parseInt(this.degree);
|
||||
|
||||
this.element.style.rotate = newAngle + "deg";
|
||||
}
|
||||
|
||||
undo() {
|
||||
let lastTransform = this.element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
|
||||
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const undoAngle = currentAngle + -parseInt(this.degree);
|
||||
|
||||
this.element.style.rotate = undoAngle + "deg";
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
export class RotateAllCommand extends Command {
|
||||
constructor(elements, degree) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
execute() {
|
||||
for (let element of this.elements) {
|
||||
let lastTransform = element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const newAngle = lastAngle + this.degree;
|
||||
|
||||
element.style.rotate = newAngle + "deg";
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
for (let element of this.elements) {
|
||||
let lastTransform = element.style.rotate;
|
||||
if (!lastTransform) {
|
||||
lastTransform = "0";
|
||||
}
|
||||
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const undoAngle = currentAngle + -this.degree;
|
||||
|
||||
element.style.rotate = undoAngle + "deg";
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class SelectPageCommand extends Command {
|
||||
constructor(pageNumber, checkbox) {
|
||||
super();
|
||||
this.pageNumber = pageNumber;
|
||||
this.selectCheckbox = checkbox;
|
||||
}
|
||||
|
||||
execute() {
|
||||
if (this.selectCheckbox.checked) {
|
||||
//adds to array of selected pages
|
||||
window.selectedPages.push(this.pageNumber);
|
||||
} else {
|
||||
//remove page from selected pages array
|
||||
const index = window.selectedPages.indexOf(this.pageNumber);
|
||||
if (index !== -1) {
|
||||
window.selectedPages.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.selectedPages.length > 0 && !window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
if (window.selectedPages.length == 0 && window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
|
||||
window.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.selectCheckbox.checked = !this.selectCheckbox.checked;
|
||||
if (this.selectCheckbox.checked) {
|
||||
//adds to array of selected pages
|
||||
window.selectedPages.push(this.pageNumber);
|
||||
} else {
|
||||
//remove page from selected pages array
|
||||
const index = window.selectedPages.indexOf(this.pageNumber);
|
||||
if (index !== -1) {
|
||||
window.selectedPages.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.selectedPages.length > 0 && !window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
if (window.selectedPages.length == 0 && window.selectPage) {
|
||||
window.toggleSelectPageVisibility();
|
||||
}
|
||||
|
||||
window.updateSelectedPagesDisplay();
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.selectCheckbox.checked = !this.selectCheckbox.checked;
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
export class SplitFileCommand extends Command {
|
||||
constructor(element, splitClass) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.splitClass = splitClass;
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.element.classList.toggle(this.splitClass);
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.element.classList.toggle(this.splitClass);
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
export class SplitAllCommand extends Command {
|
||||
constructor(elements, isSelectedInWindow, selectedPages, splitClass) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.isSelectedInWindow = isSelectedInWindow;
|
||||
this.selectedPages = selectedPages;
|
||||
this.splitClass = splitClass;
|
||||
}
|
||||
|
||||
execute() {
|
||||
if (!this.isSelectedInWindow) {
|
||||
const hasSplit = this._hasSplit(this.elements, this.splitClass);
|
||||
if (hasSplit) {
|
||||
this.elements.forEach((page) => {
|
||||
page.classList.remove(this.splitClass);
|
||||
});
|
||||
} else {
|
||||
this.elements.forEach((page) => {
|
||||
page.classList.add(this.splitClass);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements.forEach((page, index) => {
|
||||
const pageIndex = index;
|
||||
if (this.isSelectedInWindow && !this.selectedPages.includes(pageIndex))
|
||||
return;
|
||||
|
||||
if (page.classList.contains(this.splitClass)) {
|
||||
page.classList.remove(this.splitClass);
|
||||
} else {
|
||||
page.classList.add(this.splitClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_hasSplit() {
|
||||
if (!this.elements || this.elements.length == 0) return false;
|
||||
|
||||
for (const node of this.elements) {
|
||||
if (node.classList.contains(this.splitClass)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.isSelectedInWindow) {
|
||||
const hasSplit = this._hasSplit(this.elements, this.splitClass);
|
||||
if (hasSplit) {
|
||||
this.elements.forEach((page) => {
|
||||
page.classList.remove(this.splitClass);
|
||||
});
|
||||
} else {
|
||||
this.elements.forEach((page) => {
|
||||
page.classList.add(this.splitClass);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements.forEach((page, index) => {
|
||||
const pageIndex = index;
|
||||
if (this.isSelectedInWindow && !this.selectedPages.includes(pageIndex))
|
||||
return;
|
||||
|
||||
if (page.classList.contains(this.splitClass)) {
|
||||
page.classList.remove(this.splitClass);
|
||||
} else {
|
||||
page.classList.add(this.splitClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
function toolsManager() {
|
||||
const convertToPDF = document.querySelector('#groupConvertTo');
|
||||
const convertFromPDF = document.querySelector('#groupConvertFrom');
|
||||
|
||||
if (convertToPDF && convertFromPDF) {
|
||||
const itemsTo = Array.from(convertToPDF.querySelectorAll('.dropdown-item')).filter(
|
||||
(item) => !item.querySelector('hr.dropdown-divider')
|
||||
);
|
||||
const itemsFrom = Array.from(convertFromPDF.querySelectorAll('.dropdown-item')).filter(
|
||||
(item) => !item.querySelector('hr.dropdown-divider')
|
||||
);
|
||||
|
||||
const totalItems = itemsTo.length + itemsFrom.length;
|
||||
|
||||
if (totalItems > 12) {
|
||||
document.querySelectorAll('#convertGroup').forEach((element) => (element.style.display = 'none'));
|
||||
document.querySelectorAll('#groupConvertTo').forEach((element) => (element.style.display = 'flex'));
|
||||
document.querySelectorAll('#groupConvertFrom').forEach((element) => (element.style.display = 'flex'));
|
||||
} else {
|
||||
document.querySelectorAll('#convertGroup').forEach((element) => (element.style.display = 'flex'));
|
||||
document.querySelectorAll('#groupConvertTo').forEach((element) => (element.style.display = 'none'));
|
||||
document.querySelectorAll('#groupConvertFrom').forEach((element) => (element.style.display = 'none'));
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.navbar-item').forEach((element) => {
|
||||
if (!element.closest('#stacked')) {
|
||||
const dropdownItems = element.querySelectorAll('.dropdown-item');
|
||||
const items = Array.from(dropdownItems).filter((item) => !item.querySelector('hr.dropdown-divider'));
|
||||
|
||||
if (items.length === 0) {
|
||||
if (
|
||||
element.previousElementSibling &&
|
||||
element.previousElementSibling.classList.contains('navbar-item') &&
|
||||
element.previousElementSibling.classList.contains('nav-item-separator')
|
||||
) {
|
||||
element.previousElementSibling.remove();
|
||||
}
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.tooltipSetup = () => {
|
||||
const tooltipElements = document.querySelectorAll('[title]');
|
||||
|
||||
tooltipElements.forEach((element) => {
|
||||
const tooltipText = element.getAttribute('title');
|
||||
element.removeAttribute('title');
|
||||
element.setAttribute('data-title', tooltipText);
|
||||
const customTooltip = document.createElement('div');
|
||||
customTooltip.className = 'btn-tooltip';
|
||||
customTooltip.textContent = tooltipText;
|
||||
|
||||
document.body.appendChild(customTooltip);
|
||||
|
||||
element.addEventListener('mouseenter', (event) => {
|
||||
customTooltip.style.display = 'block';
|
||||
customTooltip.style.left = `${event.pageX + 10}px`; // Position tooltip slightly away from the cursor
|
||||
customTooltip.style.top = `${event.pageY + 10}px`;
|
||||
});
|
||||
|
||||
// Update the position of the tooltip as the user moves the mouse
|
||||
element.addEventListener('mousemove', (event) => {
|
||||
customTooltip.style.left = `${event.pageX + 10}px`;
|
||||
customTooltip.style.top = `${event.pageY + 10}px`;
|
||||
});
|
||||
|
||||
// Hide the tooltip when the mouse leaves
|
||||
element.addEventListener('mouseleave', () => {
|
||||
customTooltip.style.display = 'none';
|
||||
});
|
||||
});
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
tooltipSetup();
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
window.goToFirstOrLastPage = goToFirstOrLastPage;
|
||||
|
||||
document.getElementById('download-pdf').addEventListener('click', async () => {
|
||||
const downloadButton = document.getElementById('download-pdf');
|
||||
const originalContent = downloadButton.innerHTML;
|
||||
|
||||
downloadButton.disabled = true;
|
||||
downloadButton.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
`;
|
||||
|
||||
try {
|
||||
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
|
||||
const modifiedPdfBytes = await modifiedPdf.save();
|
||||
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = originalFileName + '_addedImage.pdf';
|
||||
link.click();
|
||||
} finally {
|
||||
downloadButton.disabled = false;
|
||||
downloadButton.innerHTML = originalContent;
|
||||
}
|
||||
});
|
||||
let originalFileName = '';
|
||||
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
|
||||
const fileInput = event.target;
|
||||
fileInput.addEventListener('file-input-change', async (e) => {
|
||||
const { allFiles } = e.detail;
|
||||
if (allFiles && allFiles.length > 0) {
|
||||
const file = allFiles[0];
|
||||
originalFileName = file.name.replace(/\.[^/.]+$/, '');
|
||||
const pdfData = await file.arrayBuffer();
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
await DraggableUtils.renderPage(pdfDoc, 0);
|
||||
|
||||
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
|
||||
el.style.cssText = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
|
||||
el.style.cssText = 'display:none !important';
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Delete') {
|
||||
DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const imageUpload = document.querySelector('input[name=image-upload]');
|
||||
imageUpload.addEventListener('change', (e) => {
|
||||
if (!e.target.files) {
|
||||
return;
|
||||
}
|
||||
for (const imageFile of e.target.files) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsDataURL(imageFile);
|
||||
reader.onloadend = function (e) {
|
||||
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function goToFirstOrLastPage(page) {
|
||||
if (page) {
|
||||
const lastPage = DraggableUtils.pdfDoc.numPages;
|
||||
await DraggableUtils.goToPage(lastPage - 1);
|
||||
} else {
|
||||
await DraggableUtils.goToPage(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
var canvas = document.getElementById('contrast-pdf-canvas');
|
||||
var context = canvas.getContext('2d');
|
||||
var originalImageData = null;
|
||||
var allPages = [];
|
||||
var pdfDoc = null;
|
||||
var pdf = null; // This is the current PDF document
|
||||
|
||||
async function renderPDFAndSaveOriginalImageData(file) {
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onload = async function () {
|
||||
var data = new Uint8Array(this.result);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
pdf = await pdfjsLib.getDocument({data: data}).promise;
|
||||
|
||||
// Get the number of pages in the PDF
|
||||
var numPages = pdf.numPages;
|
||||
allPages = Array.from({length: numPages}, (_, i) => i + 1);
|
||||
|
||||
// Create a new PDF document
|
||||
pdfDoc = await PDFLib.PDFDocument.create();
|
||||
// Render the first page in the viewer
|
||||
await renderPageAndAdjustImageProperties(1);
|
||||
document.getElementById('sliders-container').style.display = 'block';
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
// This function is now async and returns a promise
|
||||
function renderPageAndAdjustImageProperties(pageNum) {
|
||||
return new Promise(async function (resolve, reject) {
|
||||
var page = await pdf.getPage(pageNum);
|
||||
var scale = 1.5;
|
||||
var viewport = page.getViewport({scale: scale});
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
var renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
};
|
||||
|
||||
var renderTask = page.render(renderContext);
|
||||
renderTask.promise.then(function () {
|
||||
originalImageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
adjustImageProperties();
|
||||
resolve();
|
||||
});
|
||||
canvas.classList.add('fixed-shadow-canvas');
|
||||
});
|
||||
}
|
||||
|
||||
function adjustImageProperties() {
|
||||
var contrast = parseFloat(document.getElementById('contrast-slider').value);
|
||||
var brightness = parseFloat(document.getElementById('brightness-slider').value);
|
||||
var saturation = parseFloat(document.getElementById('saturation-slider').value);
|
||||
|
||||
contrast /= 100; // normalize to range [0, 2]
|
||||
brightness /= 100; // normalize to range [0, 2]
|
||||
saturation /= 100; // normalize to range [0, 2]
|
||||
|
||||
if (originalImageData) {
|
||||
var newImageData = context.createImageData(originalImageData.width, originalImageData.height);
|
||||
newImageData.data.set(originalImageData.data);
|
||||
|
||||
for (var i = 0; i < newImageData.data.length; i += 4) {
|
||||
var r = newImageData.data[i];
|
||||
var g = newImageData.data[i + 1];
|
||||
var b = newImageData.data[i + 2];
|
||||
// Adjust contrast
|
||||
r = adjustContrastForPixel(r, contrast);
|
||||
g = adjustContrastForPixel(g, contrast);
|
||||
b = adjustContrastForPixel(b, contrast);
|
||||
// Adjust brightness
|
||||
r = adjustBrightnessForPixel(r, brightness);
|
||||
g = adjustBrightnessForPixel(g, brightness);
|
||||
b = adjustBrightnessForPixel(b, brightness);
|
||||
// Adjust saturation
|
||||
var rgb = adjustSaturationForPixel(r, g, b, saturation);
|
||||
newImageData.data[i] = rgb[0];
|
||||
newImageData.data[i + 1] = rgb[1];
|
||||
newImageData.data[i + 2] = rgb[2];
|
||||
}
|
||||
context.putImageData(newImageData, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
(r /= 255), (g /= 255), (b /= 255);
|
||||
|
||||
var max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b);
|
||||
var h,
|
||||
s,
|
||||
l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
var d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [h, s, l];
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) {
|
||||
var r, g, b;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l; // achromatic
|
||||
} else {
|
||||
var hue2rgb = function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
var p = 2 * l - q;
|
||||
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
return [r * 255, g * 255, b * 255];
|
||||
}
|
||||
|
||||
function adjustContrastForPixel(pixel, contrast) {
|
||||
// Normalize to range [-0.5, 0.5]
|
||||
var normalized = pixel / 255 - 0.5;
|
||||
|
||||
// Apply contrast
|
||||
normalized *= contrast;
|
||||
|
||||
// Denormalize back to [0, 255]
|
||||
return (normalized + 0.5) * 255;
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function adjustSaturationForPixel(r, g, b, saturation) {
|
||||
var hsl = rgbToHsl(r, g, b);
|
||||
|
||||
// Adjust saturation
|
||||
hsl[1] = clamp(hsl[1] * saturation, 0, 1);
|
||||
|
||||
// Convert back to RGB
|
||||
var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
|
||||
|
||||
// Return adjusted RGB values
|
||||
return rgb;
|
||||
}
|
||||
|
||||
function adjustBrightnessForPixel(pixel, brightness) {
|
||||
return Math.max(0, Math.min(255, pixel * brightness));
|
||||
}
|
||||
let inputFileName = '';
|
||||
async function downloadPDF() {
|
||||
for (var i = 0; i < allPages.length; i++) {
|
||||
await renderPageAndAdjustImageProperties(allPages[i]);
|
||||
const pngImageBytes = canvas.toDataURL('image/png');
|
||||
const pngImage = await pdfDoc.embedPng(pngImageBytes);
|
||||
const pngDims = pngImage.scale(1);
|
||||
|
||||
// Create a blank page matching the dimensions of the image
|
||||
const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
|
||||
|
||||
// Draw the PNG image
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngDims.width,
|
||||
height: pngDims.height,
|
||||
});
|
||||
}
|
||||
|
||||
// Serialize the PDFDocument to bytes (a Uint8Array)
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
|
||||
// Create a Blob
|
||||
const blob = new Blob([pdfBytes.buffer], {type: 'application/pdf'});
|
||||
|
||||
// Create download link
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = URL.createObjectURL(blob);
|
||||
let newFileName = inputFileName ? inputFileName.replace('.pdf', '') : 'download';
|
||||
newFileName += '_adjusted_color.pdf';
|
||||
|
||||
downloadLink.download = newFileName;
|
||||
downloadLink.click();
|
||||
|
||||
// After download, reset the viewer and clear stored data
|
||||
allPages = []; // Clear the pages
|
||||
originalImageData = null; // Clear the image data
|
||||
|
||||
// Go back to page 1 and render it in the viewer
|
||||
if (pdf !== null) {
|
||||
renderPageAndAdjustImageProperties(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('fileInput-input').addEventListener('change', function (e) {
|
||||
const fileInput = e.target;
|
||||
fileInput.addEventListener('file-input-change', async (e) => {
|
||||
const {allFiles} = e.detail;
|
||||
if (allFiles && allFiles.length > 0) {
|
||||
const file = allFiles[0];
|
||||
inputFileName = file.name;
|
||||
renderPDFAndSaveOriginalImageData(file);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('contrast-slider').addEventListener('input', function () {
|
||||
document.getElementById('contrast-val').textContent = this.value;
|
||||
adjustImageProperties();
|
||||
});
|
||||
|
||||
document.getElementById('brightness-slider').addEventListener('input', function () {
|
||||
document.getElementById('brightness-val').textContent = this.value;
|
||||
adjustImageProperties();
|
||||
});
|
||||
|
||||
document.getElementById('saturation-slider').addEventListener('input', function () {
|
||||
document.getElementById('saturation-val').textContent = this.value;
|
||||
adjustImageProperties();
|
||||
});
|
||||
|
||||
document.getElementById('download-button').addEventListener('click', function () {
|
||||
downloadPDF();
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
const deleteAllCheckbox = document.querySelector('#deleteAll');
|
||||
let inputs = document.querySelectorAll('input');
|
||||
const customMetadataDiv = document.getElementById('customMetadata');
|
||||
const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
|
||||
|
||||
deleteAllCheckbox.addEventListener('change', function (event) {
|
||||
inputs.forEach((input) => {
|
||||
// If it's the deleteAllCheckbox or any file input, skip
|
||||
if (input === deleteAllCheckbox || input.type === 'file') {
|
||||
return;
|
||||
}
|
||||
// Disable or enable based on the checkbox state
|
||||
input.disabled = deleteAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
|
||||
const customModeCheckbox = document.getElementById('customModeCheckbox');
|
||||
const addMetadataBtn = document.getElementById('addMetadataBtn');
|
||||
const customMetadataFormContainer = document.getElementById('customMetadataEntries');
|
||||
var count = 1;
|
||||
const fileInput = document.querySelector('#fileInput-input');
|
||||
const authorInput = document.querySelector('#author');
|
||||
const creationDateInput = document.querySelector('#creationDate');
|
||||
const creatorInput = document.querySelector('#creator');
|
||||
const keywordsInput = document.querySelector('#keywords');
|
||||
const modificationDateInput = document.querySelector('#modificationDate');
|
||||
const producerInput = document.querySelector('#producer');
|
||||
const subjectInput = document.querySelector('#subject');
|
||||
const titleInput = document.querySelector('#title');
|
||||
const trappedInput = document.querySelector('#trapped');
|
||||
var lastPDFFileMeta = null;
|
||||
var lastPDFFile = null;
|
||||
|
||||
fileInput.addEventListener('change', async function () {
|
||||
fileInput.addEventListener('file-input-change', async (e) => {
|
||||
const {allFiles} = e.detail;
|
||||
if (allFiles && allFiles.length > 0) {
|
||||
const file = allFiles[0];
|
||||
while (otherMetadataEntriesDiv.firstChild) {
|
||||
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
|
||||
}
|
||||
while (customMetadataFormContainer.firstChild) {
|
||||
customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
|
||||
}
|
||||
var url = URL.createObjectURL(file);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
const pdf = await pdfjsLib.getDocument(url).promise;
|
||||
const pdfMetadata = await pdf.getMetadata();
|
||||
lastPDFFile = pdfMetadata?.info;
|
||||
console.log(pdfMetadata);
|
||||
if (!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
|
||||
customModeCheckbox.disabled = true;
|
||||
customModeCheckbox.checked = false;
|
||||
} else {
|
||||
customModeCheckbox.disabled = false;
|
||||
}
|
||||
authorInput.value = pdfMetadata?.info?.Author;
|
||||
creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
|
||||
creatorInput.value = pdfMetadata?.info?.Creator;
|
||||
keywordsInput.value = pdfMetadata?.info?.Keywords;
|
||||
modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
|
||||
producerInput.value = pdfMetadata?.info?.Producer;
|
||||
subjectInput.value = pdfMetadata?.info?.Subject;
|
||||
titleInput.value = pdfMetadata?.info?.Title;
|
||||
console.log(pdfMetadata?.info);
|
||||
const trappedValue = pdfMetadata?.info?.Trapped;
|
||||
// Get all options in the select element
|
||||
const options = trappedInput.options;
|
||||
// Loop through all options to find the one with a matching value
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (options[i].value === trappedValue) {
|
||||
options[i].selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
addExtra();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
addMetadataBtn.addEventListener('click', () => {
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.placeholder = 'Key';
|
||||
keyInput.className = 'form-control';
|
||||
keyInput.name = `allRequestParams[customKey${count}]`;
|
||||
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.placeholder = 'Value';
|
||||
valueInput.className = 'form-control';
|
||||
valueInput.name = `allRequestParams[customValue${count}]`;
|
||||
count = count + 1;
|
||||
|
||||
const formGroup = document.createElement('div');
|
||||
formGroup.className = 'mb-3';
|
||||
formGroup.appendChild(keyInput);
|
||||
formGroup.appendChild(valueInput);
|
||||
|
||||
customMetadataFormContainer.appendChild(formGroup);
|
||||
});
|
||||
function convertDateFormat(dateTimeString) {
|
||||
if (!dateTimeString || dateTimeString.length < 17) {
|
||||
return dateTimeString;
|
||||
}
|
||||
|
||||
const year = dateTimeString.substring(2, 6);
|
||||
const month = dateTimeString.substring(6, 8);
|
||||
const day = dateTimeString.substring(8, 10);
|
||||
const hour = dateTimeString.substring(10, 12);
|
||||
const minute = dateTimeString.substring(12, 14);
|
||||
const second = dateTimeString.substring(14, 16);
|
||||
|
||||
return year + '/' + month + '/' + day + ' ' + hour + ':' + minute + ':' + second;
|
||||
}
|
||||
|
||||
function addExtra() {
|
||||
const event = document.getElementById('customModeCheckbox');
|
||||
if (event.checked && lastPDFFile.Custom != null) {
|
||||
customMetadataDiv.style.display = 'block';
|
||||
for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
|
||||
if (
|
||||
key === 'Author' ||
|
||||
key === 'CreationDate' ||
|
||||
key === 'Creator' ||
|
||||
key === 'Keywords' ||
|
||||
key === 'ModDate' ||
|
||||
key === 'Producer' ||
|
||||
key === 'Subject' ||
|
||||
key === 'Title' ||
|
||||
key === 'Trapped'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const entryDiv = document.createElement('div');
|
||||
entryDiv.className = 'mb-3';
|
||||
entryDiv.innerHTML = `<div class="mb-3"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
|
||||
otherMetadataEntriesDiv.appendChild(entryDiv);
|
||||
}
|
||||
} else {
|
||||
customMetadataDiv.style.display = 'none';
|
||||
while (otherMetadataEntriesDiv.firstChild) {
|
||||
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customModeCheckbox.addEventListener('change', (event) => {
|
||||
addExtra();
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
let pdfCanvas = document.getElementById('cropPdfCanvas');
|
||||
let overlayCanvas = document.getElementById('overlayCanvas');
|
||||
let canvasesContainer = document.getElementById('canvasesContainer');
|
||||
canvasesContainer.style.display = 'none';
|
||||
let containerRect = canvasesContainer.getBoundingClientRect();
|
||||
|
||||
let context = pdfCanvas.getContext('2d');
|
||||
let overlayContext = overlayCanvas.getContext('2d');
|
||||
|
||||
overlayCanvas.width = pdfCanvas.width;
|
||||
overlayCanvas.height = pdfCanvas.height;
|
||||
|
||||
let isDrawing = false; // New flag to check if drawing is ongoing
|
||||
|
||||
let cropForm = document.getElementById('cropForm');
|
||||
let fileInput = document.getElementById('fileInput-input');
|
||||
let xInput = document.getElementById('x');
|
||||
let yInput = document.getElementById('y');
|
||||
let widthInput = document.getElementById('width');
|
||||
let heightInput = document.getElementById('height');
|
||||
|
||||
let pdfDoc = null;
|
||||
let currentPage = 1;
|
||||
let totalPages = 0;
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let rectWidth = 0;
|
||||
let rectHeight = 0;
|
||||
|
||||
let pageScale = 1; // The scale which the pdf page renders
|
||||
let timeId = null; // timeout id for resizing canvases event
|
||||
|
||||
function renderPageFromFile(file) {
|
||||
if (file.type === 'application/pdf') {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
let typedArray = new Uint8Array(reader.result);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
|
||||
pdfDoc = pdf;
|
||||
totalPages = pdf.numPages;
|
||||
renderPage(currentPage);
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
clearTimeout(timeId);
|
||||
|
||||
timeId = setTimeout(function () {
|
||||
if (fileInput.files.length == 0) return;
|
||||
let canvasesContainer = document.getElementById('canvasesContainer');
|
||||
let containerRect = canvasesContainer.getBoundingClientRect();
|
||||
|
||||
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
|
||||
|
||||
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
|
||||
pdfCanvas.width = containerRect.width;
|
||||
pdfCanvas.height = containerRect.height;
|
||||
|
||||
overlayCanvas.width = containerRect.width;
|
||||
overlayCanvas.height = containerRect.height;
|
||||
|
||||
let file = fileInput.files[0];
|
||||
renderPageFromFile(file);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
fileInput.addEventListener('file-input-change', async (e) => {
|
||||
const {allFiles} = e.detail;
|
||||
if (allFiles && allFiles.length > 0) {
|
||||
canvasesContainer.style.display = 'block'; // set for visual purposes
|
||||
let file = allFiles[0];
|
||||
renderPageFromFile(file);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
cropForm.addEventListener('submit', function (e) {
|
||||
if (xInput.value == '' && yInput.value == '' && widthInput.value == '' && heightInput.value == '') {
|
||||
// Ορίστε συντεταγμένες για ολόκληρη την επιφάνεια του PDF
|
||||
xInput.value = 0;
|
||||
yInput.value = 0;
|
||||
widthInput.value = containerRect.width;
|
||||
heightInput.value = containerRect.height;
|
||||
}
|
||||
});
|
||||
|
||||
overlayCanvas.addEventListener('mousedown', function (e) {
|
||||
// Clear previously drawn rectangle on the main canvas
|
||||
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
|
||||
renderPage(currentPage); // Re-render the PDF
|
||||
|
||||
// Clear the overlay canvas to ensure old drawings are removed
|
||||
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
|
||||
startX = e.offsetX;
|
||||
startY = e.offsetY;
|
||||
isDrawing = true;
|
||||
});
|
||||
|
||||
overlayCanvas.addEventListener('mousemove', function (e) {
|
||||
if (!isDrawing) return;
|
||||
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
|
||||
|
||||
rectWidth = e.offsetX - startX;
|
||||
rectHeight = e.offsetY - startY;
|
||||
overlayContext.strokeStyle = 'red';
|
||||
overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
|
||||
});
|
||||
|
||||
overlayCanvas.addEventListener('mouseup', function (e) {
|
||||
isDrawing = false;
|
||||
|
||||
rectWidth = e.offsetX - startX;
|
||||
rectHeight = e.offsetY - startY;
|
||||
|
||||
let flippedY = pdfCanvas.height - e.offsetY;
|
||||
|
||||
xInput.value = startX / pageScale;
|
||||
yInput.value = flippedY / pageScale;
|
||||
widthInput.value = rectWidth / pageScale;
|
||||
heightInput.value = rectHeight / pageScale;
|
||||
|
||||
// Draw the final rectangle on the main canvas
|
||||
context.strokeStyle = 'red';
|
||||
context.strokeRect(startX, startY, rectWidth, rectHeight);
|
||||
|
||||
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
|
||||
});
|
||||
|
||||
function renderPage(pageNumber) {
|
||||
pdfDoc.getPage(pageNumber).then(function (page) {
|
||||
let canvasesContainer = document.getElementById('canvasesContainer');
|
||||
let containerRect = canvasesContainer.getBoundingClientRect();
|
||||
|
||||
pageScale = containerRect.width / page.getViewport({scale: 1}).width; // The new scale
|
||||
|
||||
let viewport = page.getViewport({scale: containerRect.width / page.getViewport({scale: 1}).width});
|
||||
|
||||
canvasesContainer.width = viewport.width;
|
||||
canvasesContainer.height = viewport.height;
|
||||
|
||||
pdfCanvas.width = viewport.width;
|
||||
pdfCanvas.height = viewport.height;
|
||||
|
||||
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
|
||||
overlayCanvas.height = viewport.height;
|
||||
|
||||
let renderContext = {canvasContext: context, viewport: viewport};
|
||||
page.render(renderContext);
|
||||
pdfCanvas.classList.add('shadow-canvas');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*<![CDATA[*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (window.analyticsPromptBoolean) {
|
||||
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
|
||||
analyticsModal.show();
|
||||
|
||||
let retryCount = 0;
|
||||
function hideCookieBanner() {
|
||||
const cookieBanner = document.querySelector('#cc-main');
|
||||
if (cookieBanner && cookieBanner.offsetHeight > 0) {
|
||||
cookieBanner.style.display = "none";
|
||||
} else if (retryCount < 20) {
|
||||
retryCount++;
|
||||
setTimeout(hideCookieBanner, 100);
|
||||
}
|
||||
}
|
||||
hideCookieBanner();
|
||||
}
|
||||
});
|
||||
/*]]>*/function setAnalytics(enabled) {
|
||||
fetchWithCsrf('api/v1/settings/update-enable-analytics', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(enabled),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log('Analytics setting updated successfully');
|
||||
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
|
||||
|
||||
if (typeof CookieConsent !== "undefined") {
|
||||
if (enabled) {
|
||||
CookieConsent.acceptCategory(['analytics']);
|
||||
} else {
|
||||
CookieConsent.acceptCategory([]);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (response.status === 208) {
|
||||
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
|
||||
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
|
||||
} else {
|
||||
throw new Error('Unexpected response status: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error updating analytics setting:', error);
|
||||
alert('An error occurred while updating the analytics setting. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
updateFavoriteIcons();
|
||||
const contentPath = /*[[${@contextPath}]]*/ '';
|
||||
|
||||
const defaultView = localStorage.getItem('defaultView') || 'home'; // Default to "home"
|
||||
if (defaultView === 'home-legacy') {
|
||||
window.location.href = contentPath + 'home-legacy'; // Redirect to legacy view
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const surveyVersion = '3.0';
|
||||
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
||||
const dontShowAgain = document.getElementById('dontShowAgain');
|
||||
const takeSurveyButton = document.getElementById('takeSurvey');
|
||||
|
||||
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
|
||||
|
||||
// Check if survey version changed and reset page views if it did
|
||||
const storedVersion = localStorage.getItem('surveyVersion');
|
||||
if (storedVersion && storedVersion !== surveyVersion) {
|
||||
localStorage.setItem('pageViews', '0');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
}
|
||||
|
||||
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
||||
|
||||
pageViews++;
|
||||
localStorage.setItem('pageViews', pageViews.toString());
|
||||
|
||||
function shouldShowSurvey() {
|
||||
if(!window.showSurvey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('dontShowSurvey') === 'true' || localStorage.getItem('surveyTaken') === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If survey version changed and we hit a threshold, show the survey
|
||||
if (localStorage.getItem('surveyVersion') !== surveyVersion && viewThresholds.includes(pageViews)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return viewThresholds.includes(pageViews);
|
||||
}
|
||||
|
||||
if (shouldShowSurvey()) {
|
||||
modal.show();
|
||||
}
|
||||
|
||||
dontShowAgain.addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
localStorage.setItem('dontShowSurvey', 'true');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
} else {
|
||||
localStorage.removeItem('dontShowSurvey');
|
||||
localStorage.removeItem('surveyVersion');
|
||||
}
|
||||
});
|
||||
if (takeSurveyButton) {
|
||||
takeSurveyButton.addEventListener('click', function () {
|
||||
localStorage.setItem('surveyTaken', 'true');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
modal.hide();
|
||||
});
|
||||
}
|
||||
if (localStorage.getItem('dontShowSurvey')) {
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
if (window.location.pathname === '/') {
|
||||
const navItem = document.getElementById('navItemToHide');
|
||||
if (navItem) {
|
||||
navItem.style.display = 'none';
|
||||
}
|
||||
}
|
||||
updateFavoritesDropdown();
|
||||
});
|
||||
function setAsDefault(value) {
|
||||
localStorage.setItem('defaultView', value);
|
||||
console.log(`Default view set to: ${value}`);
|
||||
}
|
||||
|
||||
function adjustVisibleElements() {
|
||||
const container = document.querySelector('.recent-features');
|
||||
if(!container) return;
|
||||
const subElements = Array.from(container.children);
|
||||
|
||||
let totalWidth = 0;
|
||||
|
||||
subElements.forEach((element) => {
|
||||
totalWidth += 12 * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
|
||||
if (totalWidth > window.innerWidth) {
|
||||
element.style.display = 'none';
|
||||
} else {
|
||||
element.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function adjustContainerAlignment() {
|
||||
document.querySelectorAll('.features-container').forEach((parent) => {
|
||||
parent.querySelectorAll('.feature-rows').forEach((container) => {
|
||||
const containerWidth = parent.offsetWidth;
|
||||
if (containerWidth < 32 * parseFloat(getComputedStyle(document.documentElement).fontSize)) {
|
||||
container.classList.add('single-column');
|
||||
} else {
|
||||
container.classList.remove('single-column');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function toolsManager() {
|
||||
const convertToPDF = document.querySelector('#groupConvertTo');
|
||||
const convertFromPDF = document.querySelector('#groupConvertFrom');
|
||||
|
||||
if (convertToPDF && convertFromPDF) {
|
||||
const itemsTo = Array.from(convertToPDF.querySelectorAll('.dropdown-item')).filter(
|
||||
(item) => !item.querySelector('hr.dropdown-divider')
|
||||
);
|
||||
const itemsFrom = Array.from(convertFromPDF.querySelectorAll('.dropdown-item')).filter(
|
||||
(item) => !item.querySelector('hr.dropdown-divider')
|
||||
);
|
||||
|
||||
const totalItems = itemsTo.length + itemsFrom.length;
|
||||
|
||||
if (totalItems > 12) {
|
||||
document.querySelectorAll('#convertGroup').forEach((element) => element.remove());
|
||||
document.querySelectorAll('#groupConvertTo').forEach((element) => (element.style.display = 'flex'));
|
||||
document.querySelectorAll('#groupConvertFrom').forEach((element) => (element.style.display = 'flex'));
|
||||
} else {
|
||||
document.querySelectorAll('#convertGroup').forEach((element) => (element.style.display = 'flex'));
|
||||
document.querySelectorAll('#groupConvertTo').forEach((element) => element.remove());
|
||||
document.querySelectorAll('#groupConvertFrom').forEach((element) => element.remove());
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
toolsManager();
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
adjustContainerAlignment();
|
||||
adjustVisibleElements();
|
||||
});
|
||||
window.addEventListener('resize', () => {
|
||||
adjustContainerAlignment();
|
||||
adjustVisibleElements();
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
let pdfCanvas = document.getElementById('cropPdfCanvas');
|
||||
let overlayCanvas = document.getElementById('overlayCanvas');
|
||||
let canvasesContainer = document.getElementById('canvasesContainer');
|
||||
canvasesContainer.style.display = 'none';
|
||||
// let paginationBtnContainer = ;
|
||||
|
||||
let context = pdfCanvas.getContext('2d');
|
||||
let overlayContext = overlayCanvas.getContext('2d');
|
||||
|
||||
let btn1Object = document.getElementById('previous-page-btn');
|
||||
let btn2Object = document.getElementById('next-page-btn');
|
||||
overlayCanvas.width = pdfCanvas.width;
|
||||
overlayCanvas.height = pdfCanvas.height;
|
||||
|
||||
let fileInput = document.getElementById('fileInput-input');
|
||||
|
||||
let file;
|
||||
|
||||
let pdfDoc = null;
|
||||
let pageNumbers = document.getElementById('pageNumbers');
|
||||
let currentPage = 1;
|
||||
let totalPages = 0;
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let rectWidth = 0;
|
||||
let rectHeight = 0;
|
||||
|
||||
let timeId = null; // timeout id for resizing canvases event
|
||||
|
||||
btn1Object.addEventListener('click', function (e) {
|
||||
if (currentPage !== 1) {
|
||||
currentPage = currentPage - 1;
|
||||
pageNumbers.value = currentPage;
|
||||
|
||||
if (file.type === 'application/pdf') {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
let typedArray = new Uint8Array(reader.result);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
|
||||
pdfDoc = pdf;
|
||||
totalPages = pdf.numPages;
|
||||
renderPage(currentPage);
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
btn2Object.addEventListener('click', function (e) {
|
||||
if (currentPage !== totalPages) {
|
||||
currentPage = currentPage + 1;
|
||||
pageNumbers.value = currentPage;
|
||||
|
||||
if (file.type === 'application/pdf') {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
let typedArray = new Uint8Array(reader.result);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
|
||||
pdfDoc = pdf;
|
||||
totalPages = pdf.numPages;
|
||||
renderPage(currentPage);
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function renderPageFromFile(file) {
|
||||
if (file.type === 'application/pdf') {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
let typedArray = new Uint8Array(reader.result);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
|
||||
pdfDoc = pdf;
|
||||
totalPages = pdf.numPages;
|
||||
renderPage(currentPage);
|
||||
});
|
||||
pageNumbers.value = currentPage;
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
document.getElementById('pagination-button-container').style.display = 'flex';
|
||||
document.getElementById('instruction-text').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
clearTimeout(timeId);
|
||||
timeId = setTimeout(function () {
|
||||
if (fileInput.files.length == 0) return;
|
||||
let canvasesContainer = document.getElementById('canvasesContainer');
|
||||
let containerRect = canvasesContainer.getBoundingClientRect();
|
||||
|
||||
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
|
||||
|
||||
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
|
||||
pdfCanvas.width = containerRect.width;
|
||||
pdfCanvas.height = containerRect.height;
|
||||
|
||||
overlayCanvas.width = containerRect.width;
|
||||
overlayCanvas.height = containerRect.height;
|
||||
|
||||
let file = fileInput.files[0];
|
||||
renderPageFromFile(file);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
fileInput.addEventListener('file-input-change', async (e) => {
|
||||
const {allFiles} = e.detail;
|
||||
if (allFiles && allFiles.length > 0) {
|
||||
canvasesContainer.style.display = 'block'; // set for visual purposes
|
||||
file = e.target.files[0];
|
||||
renderPageFromFile(file);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function renderPage(pageNumber) {
|
||||
pdfDoc.getPage(pageNumber).then(function (page) {
|
||||
let viewport = page.getViewport({scale: 1.0});
|
||||
pdfCanvas.width = viewport.width;
|
||||
pdfCanvas.height = viewport.height;
|
||||
|
||||
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
|
||||
overlayCanvas.height = viewport.height;
|
||||
|
||||
let renderContext = {canvasContext: context, viewport: viewport};
|
||||
page.render(renderContext);
|
||||
pdfCanvas.classList.add('shadow-canvas');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
window.toggleSignatureView = toggleSignatureView;
|
||||
window.previewSignature = previewSignature;
|
||||
window.addSignatureFromPreview = addSignatureFromPreview;
|
||||
window.addDraggableFromPad = addDraggableFromPad;
|
||||
window.addDraggableFromText = addDraggableFromText;
|
||||
window.goToFirstOrLastPage = goToFirstOrLastPage;
|
||||
|
||||
let currentPreviewSrc = null;
|
||||
|
||||
function toggleSignatureView() {
|
||||
const gridView = document.getElementById("gridView");
|
||||
const listView = document.getElementById("listView");
|
||||
const gridText = document.querySelector(".grid-view-text");
|
||||
const listText = document.querySelector(".list-view-text");
|
||||
|
||||
if (gridView.style.display !== "none") {
|
||||
gridView.style.display = "none";
|
||||
listView.style.display = "block";
|
||||
gridText.style.display = "none";
|
||||
listText.style.display = "inline";
|
||||
} else {
|
||||
gridView.style.display = "block";
|
||||
listView.style.display = "none";
|
||||
gridText.style.display = "inline";
|
||||
listText.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function previewSignature(element) {
|
||||
const src = element.dataset.src;
|
||||
currentPreviewSrc = src;
|
||||
|
||||
const filename = element.querySelector(".signature-list-name").textContent;
|
||||
|
||||
const previewImage = document.getElementById("previewImage");
|
||||
const previewFileName = document.getElementById("previewFileName");
|
||||
|
||||
previewImage.src = src;
|
||||
previewFileName.textContent = filename;
|
||||
|
||||
const modal = new bootstrap.Modal(
|
||||
document.getElementById("signaturePreview")
|
||||
);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function addSignatureFromPreview() {
|
||||
if (currentPreviewSrc) {
|
||||
DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
|
||||
bootstrap.Modal.getInstance(
|
||||
document.getElementById("signaturePreview")
|
||||
).hide();
|
||||
}
|
||||
}
|
||||
|
||||
let originalFileName = "";
|
||||
document
|
||||
.querySelector("input[name=pdf-upload]")
|
||||
.addEventListener("change", async (event) => {
|
||||
const fileInput = event.target;
|
||||
fileInput.addEventListener("file-input-change", async (e) => {
|
||||
const { allFiles } = e.detail;
|
||||
if (allFiles && allFiles.length > 0) {
|
||||
const file = allFiles[0];
|
||||
originalFileName = file.name.replace(/\.[^/.]+$/, "");
|
||||
const pdfData = await file.arrayBuffer();
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc =
|
||||
"./pdfjs-legacy/pdf.worker.mjs";
|
||||
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
await DraggableUtils.renderPage(pdfDoc, 0);
|
||||
|
||||
document.querySelectorAll(".show-on-file-selected").forEach((el) => {
|
||||
el.style.cssText = "";
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll(".show-on-file-selected").forEach((el) => {
|
||||
el.style.cssText = "display:none !important";
|
||||
});
|
||||
document
|
||||
.querySelectorAll(".small-file-container-saved img ")
|
||||
.forEach((img) => {
|
||||
img.addEventListener("dragstart", (e) => {
|
||||
e.dataTransfer.setData("fileUrl", img.src);
|
||||
});
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Delete") {
|
||||
DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted());
|
||||
}
|
||||
});
|
||||
|
||||
addCustomSelect();
|
||||
});
|
||||
|
||||
function addCustomSelect() {
|
||||
let customSelectElementContainer =
|
||||
document.getElementById("signFontSelection");
|
||||
let originalSelectElement =
|
||||
customSelectElementContainer.querySelector("select");
|
||||
|
||||
let optionsCount = originalSelectElement.length;
|
||||
|
||||
let selectedItem = createAndStyleSelectedItem();
|
||||
|
||||
customSelectElementContainer.appendChild(selectedItem);
|
||||
|
||||
let customSelectionsOptionsContainer = createCustomOptionsContainer();
|
||||
createAndAddCustomOptions();
|
||||
customSelectElementContainer.appendChild(customSelectionsOptionsContainer);
|
||||
|
||||
selectedItem.addEventListener("click", function (e) {
|
||||
/* When the select box is clicked, close any other select boxes,
|
||||
and open/close the current select box: */
|
||||
e.stopPropagation();
|
||||
closeAllSelect(this);
|
||||
this.nextSibling.classList.toggle("select-hide");
|
||||
this.classList.toggle("select-arrow-active");
|
||||
});
|
||||
|
||||
function createAndAddCustomOptions() {
|
||||
for (let j = 0; j < optionsCount; j++) {
|
||||
/* For each option in the original select element,
|
||||
create a new DIV that will act as an option item: */
|
||||
let customOptionItem = createAndStyleCustomOption(j);
|
||||
|
||||
customOptionItem.addEventListener("click", onCustomOptionClick);
|
||||
customSelectionsOptionsContainer.appendChild(customOptionItem);
|
||||
}
|
||||
}
|
||||
|
||||
function createCustomOptionsContainer() {
|
||||
let customSelectionsOptionsContainer = document.createElement("DIV");
|
||||
customSelectionsOptionsContainer.setAttribute(
|
||||
"class",
|
||||
"select-items select-hide"
|
||||
);
|
||||
return customSelectionsOptionsContainer;
|
||||
}
|
||||
|
||||
function createAndStyleSelectedItem() {
|
||||
let selectedItem = document.createElement("DIV");
|
||||
selectedItem.setAttribute("class", "select-selected");
|
||||
selectedItem.innerHTML =
|
||||
originalSelectElement.options[
|
||||
originalSelectElement.selectedIndex
|
||||
].innerHTML;
|
||||
|
||||
selectedItem.style.fontFamily = window.getComputedStyle(
|
||||
originalSelectElement.options[originalSelectElement.selectedIndex]
|
||||
).fontFamily;
|
||||
return selectedItem;
|
||||
}
|
||||
|
||||
function onCustomOptionClick(e) {
|
||||
/* When an item is clicked, update the original select box,
|
||||
and the selected item: */
|
||||
let selectElement =
|
||||
this.parentNode.parentNode.getElementsByTagName("select")[0];
|
||||
let optionsCount = selectElement.length;
|
||||
let currentlySelectedCustomOption = this.parentNode.previousSibling;
|
||||
for (let i = 0; i < optionsCount; i++) {
|
||||
if (selectElement.options[i].innerHTML == this.innerHTML) {
|
||||
selectElement.selectedIndex = i;
|
||||
currentlySelectedCustomOption.innerHTML = this.innerHTML;
|
||||
currentlySelectedCustomOption.style.fontFamily = this.style.fontFamily;
|
||||
|
||||
let previouslySelectedOption =
|
||||
this.parentNode.getElementsByClassName("same-as-selected");
|
||||
|
||||
if (previouslySelectedOption && previouslySelectedOption.length > 0)
|
||||
previouslySelectedOption[0].classList.remove("same-as-selected");
|
||||
|
||||
this.classList.add("same-as-selected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentlySelectedCustomOption.click();
|
||||
}
|
||||
|
||||
function createAndStyleCustomOption(j) {
|
||||
let customOptionItem = document.createElement("DIV");
|
||||
customOptionItem.innerHTML = originalSelectElement.options[j].innerHTML;
|
||||
customOptionItem.classList.add(originalSelectElement.options[j].className);
|
||||
customOptionItem.style.fontFamily = window.getComputedStyle(
|
||||
originalSelectElement.options[j]
|
||||
).fontFamily;
|
||||
|
||||
if (j == originalSelectElement.selectedIndex)
|
||||
customOptionItem.classList.add("same-as-selected");
|
||||
return customOptionItem;
|
||||
}
|
||||
|
||||
function closeAllSelect(element) {
|
||||
/* A function that will close all select boxes in the document,
|
||||
except the current select box: */
|
||||
let allSelectedOptions = document.getElementsByClassName("select-selected");
|
||||
let allSelectedOptionsCount = allSelectedOptions.length;
|
||||
let indicesOfContainersToHide = [];
|
||||
for (let i = 0; i < allSelectedOptionsCount; i++) {
|
||||
if (element == allSelectedOptions[i]) {
|
||||
indicesOfContainersToHide.push(i);
|
||||
} else {
|
||||
allSelectedOptions[i].classList.remove("select-arrow-active");
|
||||
}
|
||||
}
|
||||
|
||||
hideOptionsContainers(indicesOfContainersToHide);
|
||||
}
|
||||
|
||||
/* If the user clicks anywhere outside the select box,
|
||||
then close all select boxes: */
|
||||
document.addEventListener("click", closeAllSelect);
|
||||
|
||||
function hideOptionsContainers(containersIndices) {
|
||||
let allOptionsContainers = document.getElementsByClassName("select-items");
|
||||
let allSelectionListsContainerCount = allOptionsContainers.length;
|
||||
for (let i = 0; i < allSelectionListsContainerCount; i++) {
|
||||
if (containersIndices.indexOf(i)) {
|
||||
allOptionsContainers[i].classList.add("select-hide");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const imageUpload = document.querySelector("input[name=image-upload]");
|
||||
imageUpload.addEventListener("change", (e) => {
|
||||
if (!e.target.files) return;
|
||||
for (const imageFile of e.target.files) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsDataURL(imageFile);
|
||||
reader.onloadend = function (e) {
|
||||
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const signaturePadCanvas = document.getElementById("drawing-pad-canvas");
|
||||
const signaturePad = new SignaturePad(signaturePadCanvas, {
|
||||
minWidth: 1,
|
||||
maxWidth: 2,
|
||||
penColor: "black",
|
||||
});
|
||||
|
||||
function addDraggableFromPad() {
|
||||
if (signaturePad.isEmpty()) return;
|
||||
const startTime = Date.now();
|
||||
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
|
||||
console.log(Date.now() - startTime);
|
||||
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
|
||||
}
|
||||
|
||||
function getCroppedCanvasDataUrl(canvas) {
|
||||
let originalCtx = canvas.getContext("2d");
|
||||
let originalWidth = canvas.width;
|
||||
let originalHeight = canvas.height;
|
||||
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
|
||||
|
||||
let minX = originalWidth + 1,
|
||||
maxX = -1,
|
||||
minY = originalHeight + 1,
|
||||
maxY = -1,
|
||||
x = 0,
|
||||
y = 0,
|
||||
currentPixelColorValueIndex;
|
||||
|
||||
for (y = 0; y < originalHeight; y++) {
|
||||
for (x = 0; x < originalWidth; x++) {
|
||||
currentPixelColorValueIndex = (y * originalWidth + x) * 4;
|
||||
let currentPixelAlphaValue =
|
||||
imageData.data[currentPixelColorValueIndex + 3];
|
||||
if (currentPixelAlphaValue > 0) {
|
||||
if (minX > x) minX = x;
|
||||
if (maxX < x) maxX = x;
|
||||
if (minY > y) minY = y;
|
||||
if (maxY < y) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let croppedWidth = maxX - minX;
|
||||
let croppedHeight = maxY - minY;
|
||||
if (croppedWidth < 0 || croppedHeight < 0) return null;
|
||||
let cuttedImageData = originalCtx.getImageData(
|
||||
minX,
|
||||
minY,
|
||||
croppedWidth,
|
||||
croppedHeight
|
||||
);
|
||||
|
||||
let croppedCanvas = document.createElement("canvas"),
|
||||
croppedCtx = croppedCanvas.getContext("2d");
|
||||
|
||||
croppedCanvas.width = croppedWidth;
|
||||
croppedCanvas.height = croppedHeight;
|
||||
croppedCtx.putImageData(cuttedImageData, 0, 0);
|
||||
|
||||
return croppedCanvas.toDataURL();
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
var ratio = Math.max(window.devicePixelRatio || 1, 1);
|
||||
var additionalFactor = 10;
|
||||
|
||||
signaturePadCanvas.width =
|
||||
signaturePadCanvas.offsetWidth * ratio * additionalFactor;
|
||||
signaturePadCanvas.height =
|
||||
signaturePadCanvas.offsetHeight * ratio * additionalFactor;
|
||||
signaturePadCanvas
|
||||
.getContext("2d")
|
||||
.scale(ratio * additionalFactor, ratio * additionalFactor);
|
||||
|
||||
signaturePad.clear();
|
||||
}
|
||||
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
if (entries.some((entry) => entry.intersectionRatio > 0)) {
|
||||
resizeCanvas();
|
||||
}
|
||||
}).observe(signaturePadCanvas);
|
||||
|
||||
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
|
||||
|
||||
function addDraggableFromText() {
|
||||
const sigText = document.getElementById("sigText").value;
|
||||
const font = document.querySelector("select[name=font]").value;
|
||||
const fontSize = 100;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.font = `${fontSize}px ${font}`;
|
||||
const textWidth = ctx.measureText(sigText).width;
|
||||
const textHeight = fontSize;
|
||||
|
||||
let paragraphs = sigText.split(/\r?\n/);
|
||||
|
||||
canvas.width = textWidth;
|
||||
canvas.height = paragraphs.length * textHeight * 1.35; // for tails
|
||||
ctx.font = `${fontSize}px ${font}`;
|
||||
|
||||
ctx.textBaseline = "top";
|
||||
|
||||
let y = 0;
|
||||
|
||||
paragraphs.forEach((paragraph) => {
|
||||
ctx.fillText(paragraph, 0, y);
|
||||
y += fontSize;
|
||||
});
|
||||
|
||||
const dataURL = canvas.toDataURL();
|
||||
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
|
||||
}
|
||||
|
||||
async function goToFirstOrLastPage(page) {
|
||||
if (page) {
|
||||
const lastPage = DraggableUtils.pdfDoc.numPages;
|
||||
await DraggableUtils.goToPage(lastPage - 1);
|
||||
} else {
|
||||
await DraggableUtils.goToPage(0);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("download-pdf").addEventListener("click", async () => {
|
||||
const downloadButton = document.getElementById("download-pdf");
|
||||
const originalContent = downloadButton.innerHTML;
|
||||
|
||||
downloadButton.disabled = true;
|
||||
downloadButton.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
`;
|
||||
|
||||
try {
|
||||
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
|
||||
const modifiedPdfBytes = await modifiedPdf.save();
|
||||
const blob = new Blob([modifiedPdfBytes], { type: "application/pdf" });
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = originalFileName + "_signed.pdf";
|
||||
link.click();
|
||||
} catch (error) {
|
||||
console.error("Error downloading PDF:", error);
|
||||
} finally {
|
||||
downloadButton.disabled = false;
|
||||
downloadButton.innerHTML = originalContent;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,768 @@
|
||||
document.getElementById("validateButton").addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
validatePipeline();
|
||||
});
|
||||
function validatePipeline() {
|
||||
let pipelineListItems = document.getElementById("pipelineList").children;
|
||||
let isValid = true;
|
||||
let containsAddPassword = false;
|
||||
for (let i = 0; i < pipelineListItems.length - 1; i++) {
|
||||
let currentOperation = pipelineListItems[i].querySelector(".operationName").textContent;
|
||||
let nextOperation = pipelineListItems[i + 1].querySelector(".operationName").textContent;
|
||||
if (currentOperation === "/add-password") {
|
||||
containsAddPassword = true;
|
||||
}
|
||||
|
||||
let currentOperationDescription = apiDocs[currentOperation]?.post?.description || "";
|
||||
let nextOperationDescription = apiDocs[nextOperation]?.post?.description || "";
|
||||
|
||||
// Strip off 'ZIP-' prefix
|
||||
currentOperationDescription = currentOperationDescription.replace("ZIP-", "");
|
||||
nextOperationDescription = nextOperationDescription.replace("ZIP-", "");
|
||||
|
||||
let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || "";
|
||||
let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || "";
|
||||
|
||||
// Splitting in case of multiple possible output/input
|
||||
let currentOperationOutputArr = currentOperationOutput.split("/");
|
||||
let nextOperationInputArr = nextOperationInput.split("/");
|
||||
|
||||
if (currentOperationOutput !== "ANY" && nextOperationInput !== "ANY") {
|
||||
let intersection = currentOperationOutputArr.filter((value) => nextOperationInputArr.includes(value));
|
||||
console.log(`Intersection: ${intersection}`);
|
||||
|
||||
if (intersection.length === 0) {
|
||||
updateValidateButton(false);
|
||||
isValid = false;
|
||||
console.log(
|
||||
`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`,
|
||||
);
|
||||
alert(
|
||||
`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
containsAddPassword &&
|
||||
pipelineListItems[pipelineListItems.length - 1].querySelector(".operationName").textContent !== "/add-password"
|
||||
) {
|
||||
updateValidateButton(false);
|
||||
alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.');
|
||||
return false;
|
||||
}
|
||||
if (isValid) {
|
||||
console.log("Pipeline is valid");
|
||||
// Continue with the pipeline operation
|
||||
} else {
|
||||
console.error("Pipeline is not valid");
|
||||
// Stop operation, maybe display an error to the user
|
||||
}
|
||||
updateValidateButton(isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function updateValidateButton(isValid) {
|
||||
var validateButton = document.getElementById("validateButton");
|
||||
if (isValid) {
|
||||
validateButton.classList.remove("btn-danger");
|
||||
validateButton.classList.add("btn-success");
|
||||
} else {
|
||||
validateButton.classList.remove("btn-success");
|
||||
validateButton.classList.add("btn-danger");
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("submitConfigBtn").addEventListener("click", function () {
|
||||
if (validatePipeline() === false) {
|
||||
return;
|
||||
}
|
||||
let selectedOperation = document.getElementById("operationsDropdown").value;
|
||||
|
||||
var pipelineName = document.getElementById("pipelineName").value;
|
||||
|
||||
|
||||
let pipelineList = document.getElementById("pipelineList").children;
|
||||
let pipelineConfig = {
|
||||
name: pipelineName,
|
||||
pipeline: [],
|
||||
_examples: {
|
||||
outputDir: "{outputFolder}/{folderName}",
|
||||
outputFileName: "{filename}-{pipelineName}-{date}-{time}",
|
||||
},
|
||||
outputDir: "httpWebRequest",
|
||||
outputFileName: "{filename}",
|
||||
};
|
||||
|
||||
for (let i = 0; i < pipelineList.length; i++) {
|
||||
let operationName = pipelineList[i].querySelector(".operationName").textContent;
|
||||
let parameters = operationSettings[operationName] || {};
|
||||
pipelineConfig.pipeline.push({
|
||||
operation: operationName,
|
||||
parameters: parameters,
|
||||
});
|
||||
}
|
||||
|
||||
let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2);
|
||||
let formData = new FormData();
|
||||
|
||||
let fileInput = document.getElementById("fileInput-input");
|
||||
let files = fileInput.files;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
console.log("files[i]", files[i].name);
|
||||
formData.append("fileInput", files[i], files[i].name);
|
||||
}
|
||||
|
||||
console.log("pipelineConfigJson", pipelineConfigJson);
|
||||
formData.append("json", pipelineConfigJson);
|
||||
console.log("formData", formData);
|
||||
|
||||
fetchWithCsrf("api/v1/pipeline/handleData", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => {
|
||||
// Save the response to use it later
|
||||
const responseToUseLater = response;
|
||||
|
||||
return response.blob().then((blob) => {
|
||||
let url = window.URL.createObjectURL(blob);
|
||||
let a = document.createElement("a");
|
||||
a.href = url;
|
||||
|
||||
// Use responseToUseLater instead of response
|
||||
const contentDisposition = responseToUseLater.headers.get("Content-Disposition");
|
||||
let filename = "download";
|
||||
if (contentDisposition && contentDisposition.indexOf("attachment") !== -1) {
|
||||
filename = decodeURIComponent(contentDisposition.split("filename=")[1].replace(/"/g, "")).trim();
|
||||
}
|
||||
a.download = filename;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
});
|
||||
|
||||
let apiDocs = {};
|
||||
let apiSchemas = {};
|
||||
let operationSettings = {};
|
||||
let operationStatus = {};
|
||||
|
||||
fetchWithCsrf("v1/api-docs")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
apiDocs = data.paths;
|
||||
apiSchemas = data.components.schemas;
|
||||
return fetchWithCsrf("api/v1/settings/get-endpoints-status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
operationStatus = data;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
const ignoreOperations = ["/api/v1/pipeline/handleData", "/api/v1/pipeline/operationToIgnore"]; // Add the operations you want to ignore here
|
||||
let operationsDropdown = document.getElementById("operationsDropdown");
|
||||
operationsDropdown.innerHTML = "";
|
||||
|
||||
let operationsByTag = {};
|
||||
|
||||
// Group operations by tags
|
||||
Object.keys(apiDocs).forEach((operationPath) => {
|
||||
let operation = apiDocs[operationPath].post;
|
||||
if (!operation || !operation.description) {
|
||||
console.log(operationPath);
|
||||
}
|
||||
//!operation.description.includes("Type:MISO")
|
||||
if (operation && !ignoreOperations.includes(operationPath)) {
|
||||
let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag
|
||||
if (!operationsByTag[operationTag]) {
|
||||
operationsByTag[operationTag] = [];
|
||||
}
|
||||
operationsByTag[operationTag].push(operationPath);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort operations within each tag alphabetically
|
||||
Object.keys(operationsByTag).forEach((tag) => {
|
||||
operationsByTag[tag].sort();
|
||||
});
|
||||
|
||||
// Specify the order of tags
|
||||
let tagOrder = ["General", "Security", "Convert", "Misc", "Filter"];
|
||||
|
||||
// Create dropdown options
|
||||
tagOrder.forEach((tag) => {
|
||||
if (operationsByTag[tag]) {
|
||||
let group = document.createElement("optgroup");
|
||||
group.label = tag;
|
||||
|
||||
operationsByTag[tag].forEach((operationPath) => {
|
||||
let option = document.createElement("option");
|
||||
|
||||
let operationPathDisplay = operationPath;
|
||||
operationPathDisplay = operationPath.replace(new RegExp("api/v1/" + tag.toLowerCase() + "/", "i"), "");
|
||||
|
||||
if (operationPath.includes("/convert")) {
|
||||
operationPathDisplay = operationPathDisplay.replace(/^\//, "").replaceAll("/", " to ");
|
||||
} else {
|
||||
operationPathDisplay = operationPathDisplay.replace(/\//g, ""); // Remove slashes
|
||||
}
|
||||
operationPathDisplay = operationPathDisplay.replaceAll(" ", "-");
|
||||
option.textContent = operationPathDisplay;
|
||||
|
||||
if (!(operationPathDisplay in operationStatus)) {
|
||||
option.value = operationPath; // Keep the value with slashes for querying
|
||||
group.appendChild(option);
|
||||
}
|
||||
});
|
||||
|
||||
operationsDropdown.appendChild(group);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
|
||||
document.getElementById('deletePipelineBtn').addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
let pipelineName = document.getElementById('pipelineName').value;
|
||||
|
||||
if (confirm(deletePipelineText + pipelineName)) {
|
||||
removePipelineFromUI(pipelineName);
|
||||
let key = "#Pipeline-" + pipelineName;
|
||||
if (localStorage.getItem(key)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
let pipelineSelect = document.getElementById("pipelineSelect");
|
||||
let modal = document.getElementById('pipelineSettingsModal');
|
||||
if (modal.style.display !== 'none') {
|
||||
$('#pipelineSettingsModal').modal('hide');
|
||||
}
|
||||
|
||||
if (pipelineSelect.options.length > 0) {
|
||||
pipelineSelect.selectedIndex = 0;
|
||||
pipelineSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function removePipelineFromUI(pipelineName) {
|
||||
let pipelineSelect = document.getElementById("pipelineSelect");
|
||||
for (let i = 0; i < pipelineSelect.options.length; i++) {
|
||||
console.log(pipelineSelect.options[i])
|
||||
console.log("list " + pipelineSelect.options[i].innerText + " vs " + pipelineName)
|
||||
if (pipelineSelect.options[i].innerText === pipelineName) {
|
||||
pipelineSelect.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.getElementById("addOperationBtn").addEventListener("click", function () {
|
||||
let selectedOperation = document.getElementById("operationsDropdown").value;
|
||||
let pipelineList = document.getElementById("pipelineList");
|
||||
|
||||
let listItem = document.createElement("li");
|
||||
listItem.className = "list-group-item";
|
||||
let hasSettings = false;
|
||||
if (apiDocs[selectedOperation] && apiDocs[selectedOperation].post) {
|
||||
const postMethod = apiDocs[selectedOperation].post;
|
||||
|
||||
// Check if parameters exist
|
||||
if (postMethod.parameters && postMethod.parameters.length > 0) {
|
||||
hasSettings = true;
|
||||
} else if (postMethod.requestBody && postMethod.requestBody.content["multipart/form-data"]) {
|
||||
// Extract the reference key
|
||||
const refKey = postMethod.requestBody.content["multipart/form-data"].schema["$ref"].split("/").pop();
|
||||
// Check if the referenced schema exists and has properties more than just its input file
|
||||
if (apiSchemas[refKey]) {
|
||||
const properties = apiSchemas[refKey].properties;
|
||||
const propertyKeys = Object.keys(properties);
|
||||
|
||||
// Check if there's more than one property or if there's exactly one property and its format is not 'binary'
|
||||
if (propertyKeys.length > 1 || (propertyKeys.length === 1 && properties[propertyKeys[0]].format !== "binary")) {
|
||||
hasSettings = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let containerDiv = document.createElement("div");
|
||||
containerDiv.className = "d-flex justify-content-between align-items-center w-100";
|
||||
|
||||
let operationNameDiv = document.createElement("div");
|
||||
operationNameDiv.className = "operationName";
|
||||
operationNameDiv.textContent = selectedOperation;
|
||||
containerDiv.appendChild(operationNameDiv);
|
||||
|
||||
let arrowsDiv = document.createElement("div");
|
||||
arrowsDiv.className = "arrows d-flex";
|
||||
|
||||
let moveUpButton = document.createElement("button");
|
||||
moveUpButton.className = "btn btn-secondary move-up ms-1";
|
||||
moveUpButton.innerHTML = '<span class="material-symbols-rounded">arrow_upward</span>';
|
||||
arrowsDiv.appendChild(moveUpButton);
|
||||
|
||||
let moveDownButton = document.createElement("button");
|
||||
moveDownButton.className = "btn btn-secondary move-down ms-1";
|
||||
moveDownButton.innerHTML = '<span class="material-symbols-rounded">arrow_downward</span>';
|
||||
arrowsDiv.appendChild(moveDownButton);
|
||||
|
||||
let settingsButton = document.createElement("button");
|
||||
settingsButton.className = `btn ${hasSettings ? "btn-warning" : "btn-secondary"} pipelineSettings ms-1`;
|
||||
if (!hasSettings) {
|
||||
settingsButton.disabled = true;
|
||||
}
|
||||
settingsButton.innerHTML = '<span class="material-symbols-rounded">settings</span>';
|
||||
arrowsDiv.appendChild(settingsButton);
|
||||
|
||||
let removeButton = document.createElement("button");
|
||||
removeButton.className = "btn btn-danger remove ms-1";
|
||||
removeButton.innerHTML = '<span class="material-symbols-rounded">close</span>';
|
||||
arrowsDiv.appendChild(removeButton);
|
||||
|
||||
containerDiv.appendChild(arrowsDiv);
|
||||
listItem.appendChild(containerDiv);
|
||||
|
||||
pipelineList.appendChild(listItem);
|
||||
|
||||
listItem.querySelector(".move-up").addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
if (listItem.previousElementSibling) {
|
||||
pipelineList.insertBefore(listItem, listItem.previousElementSibling);
|
||||
updateConfigInDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
listItem.querySelector(".move-down").addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
if (listItem.nextElementSibling) {
|
||||
pipelineList.insertBefore(listItem.nextElementSibling, listItem);
|
||||
updateConfigInDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
listItem.querySelector(".remove").addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
pipelineList.removeChild(listItem);
|
||||
hideOrShowPipelineHeader();
|
||||
updateConfigInDropdown();
|
||||
});
|
||||
|
||||
listItem.querySelector(".pipelineSettings").addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
showpipelineSettingsModal(selectedOperation);
|
||||
hideOrShowPipelineHeader();
|
||||
});
|
||||
|
||||
function showpipelineSettingsModal(operation) {
|
||||
let pipelineSettingsModal = document.getElementById("pipelineSettingsModal");
|
||||
let pipelineSettingsContent = document.getElementById("pipelineSettingsContent");
|
||||
let operationData = apiDocs[operation].post.parameters || [];
|
||||
|
||||
// Resolve the $ref reference to get actual schema properties
|
||||
let refKey = apiDocs[operation].post.requestBody.content["multipart/form-data"].schema["$ref"].split("/").pop();
|
||||
let requestBodyData = apiSchemas[refKey].properties || {};
|
||||
|
||||
// Combine operationData and requestBodyData into a single array
|
||||
operationData = operationData.concat(
|
||||
Object.keys(requestBodyData).map((key) => ({
|
||||
name: key,
|
||||
schema: requestBodyData[key],
|
||||
})),
|
||||
);
|
||||
|
||||
pipelineSettingsContent.innerHTML = "";
|
||||
|
||||
operationData.forEach((parameter) => {
|
||||
// If the parameter name is 'fileInput', return early to skip the rest of this iteration
|
||||
if (parameter.name === "fileInput") return;
|
||||
|
||||
let parameterDiv = document.createElement("div");
|
||||
parameterDiv.className = "mb-3";
|
||||
|
||||
let parameterLabel = document.createElement("label");
|
||||
parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `;
|
||||
parameterLabel.title = parameter.schema.description;
|
||||
parameterLabel.setAttribute("for", parameter.name);
|
||||
parameterDiv.appendChild(parameterLabel);
|
||||
|
||||
let defaultValue = parameter.schema.example;
|
||||
if (defaultValue === undefined) defaultValue = parameter.schema.default;
|
||||
|
||||
let parameterInput;
|
||||
|
||||
// check if enum exists in schema
|
||||
if (parameter.schema.enum) {
|
||||
// if enum exists, create a select element
|
||||
parameterInput = document.createElement("select");
|
||||
parameterInput.className = "form-control";
|
||||
|
||||
// iterate over each enum value and create an option for it
|
||||
parameter.schema.enum.forEach((value) => {
|
||||
let option = document.createElement("option");
|
||||
option.value = value;
|
||||
option.text = value;
|
||||
parameterInput.appendChild(option);
|
||||
});
|
||||
} else {
|
||||
// switch-case statement for handling non-enum types
|
||||
switch (parameter.schema.type) {
|
||||
case "string":
|
||||
if (parameter.schema.format === "binary") {
|
||||
// This is a file input
|
||||
|
||||
//parameterInput = document.createElement('input');
|
||||
//parameterInput.type = 'file';
|
||||
//parameterInput.className = "form-control";
|
||||
|
||||
parameterInput = document.createElement("input");
|
||||
parameterInput.type = "text";
|
||||
parameterInput.className = "form-control";
|
||||
parameterInput.value = "FileInputPathToBeInputtedManuallyForOffline";
|
||||
} else {
|
||||
parameterInput = document.createElement("input");
|
||||
parameterInput.type = "text";
|
||||
parameterInput.className = "form-control";
|
||||
if (defaultValue !== undefined) parameterInput.value = defaultValue;
|
||||
}
|
||||
break;
|
||||
case "number":
|
||||
case "integer":
|
||||
parameterInput = document.createElement("input");
|
||||
parameterInput.type = "number";
|
||||
parameterInput.className = "form-control";
|
||||
if (defaultValue !== undefined) parameterInput.value = defaultValue;
|
||||
break;
|
||||
case "boolean":
|
||||
parameterInput = document.createElement("input");
|
||||
parameterInput.type = "checkbox";
|
||||
if (defaultValue === true) parameterInput.checked = true;
|
||||
break;
|
||||
case "array":
|
||||
// If parameter.schema.format === 'binary' is to be checked, it should be checked here
|
||||
parameterInput = document.createElement("textarea");
|
||||
parameterInput.placeholder = 'Enter a JSON formatted array, e.g., ["item1", "item2", "item3"]';
|
||||
parameterInput.className = "form-control";
|
||||
break;
|
||||
case "object":
|
||||
parameterInput = document.createElement("textarea");
|
||||
parameterInput.placeholder = 'Enter a JSON formatted object, e.g., {"key": "value"} If this is a fileInput, it is not currently supported';
|
||||
parameterInput.className = "form-control";
|
||||
break;
|
||||
default:
|
||||
parameterInput = document.createElement("input");
|
||||
parameterInput.type = "text";
|
||||
parameterInput.className = "form-control";
|
||||
if (defaultValue !== undefined) parameterInput.value = defaultValue;
|
||||
}
|
||||
}
|
||||
parameterInput.id = parameter.name;
|
||||
|
||||
console.log("defaultValue", defaultValue);
|
||||
console.log("parameterInput", parameterInput);
|
||||
if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) {
|
||||
let savedValue = operationSettings[operation][parameter.name];
|
||||
|
||||
switch (parameter.schema.type) {
|
||||
case "number":
|
||||
case "integer":
|
||||
parameterInput.value = savedValue.toString();
|
||||
break;
|
||||
case "boolean":
|
||||
parameterInput.checked = savedValue;
|
||||
break;
|
||||
case "array":
|
||||
case "object":
|
||||
parameterInput.value = JSON.stringify(savedValue);
|
||||
break;
|
||||
default:
|
||||
parameterInput.value = savedValue;
|
||||
}
|
||||
}
|
||||
console.log("parameterInput2", parameterInput);
|
||||
parameterDiv.appendChild(parameterInput);
|
||||
|
||||
pipelineSettingsContent.appendChild(parameterDiv);
|
||||
});
|
||||
|
||||
if (hasSettings) {
|
||||
let saveButton = document.createElement("button");
|
||||
saveButton.textContent = saveSettings;
|
||||
saveButton.className = "btn btn-primary";
|
||||
saveButton.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
let settings = {};
|
||||
operationData.forEach((parameter) => {
|
||||
if (parameter.name !== "fileInput") {
|
||||
let value = document.getElementById(parameter.name).value;
|
||||
switch (parameter.schema.type) {
|
||||
case "number":
|
||||
case "integer":
|
||||
settings[parameter.name] = Number(value);
|
||||
break;
|
||||
case "boolean":
|
||||
settings[parameter.name] = document.getElementById(parameter.name).checked;
|
||||
break;
|
||||
case "array":
|
||||
case "object":
|
||||
if (value === null || value === "") {
|
||||
settings[parameter.name] = "";
|
||||
} else {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
if (Array.isArray(parsedValue)) {
|
||||
settings[parameter.name] = parsedValue;
|
||||
} else {
|
||||
settings[parameter.name] = value;
|
||||
}
|
||||
} catch (e) {
|
||||
settings[parameter.name] = value;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
settings[parameter.name] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
operationSettings[operation] = settings;
|
||||
//pipelineSettingsModal.style.display = "none";
|
||||
});
|
||||
pipelineSettingsContent.appendChild(saveButton);
|
||||
saveButton.click();
|
||||
}
|
||||
//pipelineSettingsModal.style.display = "block";
|
||||
|
||||
//pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() {
|
||||
// pipelineSettingsModal.style.display = "none";
|
||||
//}
|
||||
|
||||
//window.onclick = function(event) {
|
||||
// if (event.target == pipelineSettingsModal) {
|
||||
// pipelineSettingsModal.style.display = "none";
|
||||
// }
|
||||
//}
|
||||
}
|
||||
showpipelineSettingsModal(selectedOperation);
|
||||
updateConfigInDropdown();
|
||||
hideOrShowPipelineHeader();
|
||||
});
|
||||
|
||||
function loadBrowserPipelinesIntoDropdown() {
|
||||
let pipelineSelect = document.getElementById("pipelineSelect");
|
||||
|
||||
// Retrieve the current set of option values for comparison
|
||||
let existingOptions = new Set();
|
||||
for (let option of pipelineSelect.options) {
|
||||
existingOptions.add(option.value);
|
||||
}
|
||||
|
||||
// Iterate over all items in localStorage
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
let key = localStorage.key(i);
|
||||
if (key.startsWith("#Pipeline-")) {
|
||||
let pipelineData = localStorage.getItem(key);
|
||||
// Check if the data is already in the dropdown
|
||||
if (!existingOptions.has(pipelineData)) {
|
||||
let pipelineName = key.replace("#Pipeline-", ""); // Extract pipeline name from the key
|
||||
let option = new Option(pipelineName, pipelineData); // Use pipeline data as the option value
|
||||
pipelineSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loadBrowserPipelinesIntoDropdown();
|
||||
|
||||
function updateConfigInDropdown() {
|
||||
let pipelineSelect = document.getElementById("pipelineSelect");
|
||||
let selectedOption = pipelineSelect.options[pipelineSelect.selectedIndex];
|
||||
|
||||
// Get the current configuration as JSON
|
||||
let pipelineConfigJson = configToJson();
|
||||
console.log("pipelineConfigJson", pipelineConfigJson);
|
||||
if (!pipelineConfigJson) {
|
||||
console.error("Failed to update configuration: Invalid configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the value of the selected option with the new configuration
|
||||
selectedOption.value = pipelineConfigJson;
|
||||
}
|
||||
|
||||
var saveBtn = document.getElementById("savePipelineBtn");
|
||||
var saveBrowserBtn = document.getElementById("saveBrowserPipelineBtn");
|
||||
// Remove any existing event listeners
|
||||
saveBtn.removeEventListener("click", savePipeline);
|
||||
saveBrowserBtn.removeEventListener("click", savePipelineToBrowser);
|
||||
// Add the event listener
|
||||
saveBtn.addEventListener("click", savePipeline);
|
||||
saveBrowserBtn.addEventListener("click", savePipelineToBrowser);
|
||||
|
||||
function configToJson() {
|
||||
if (!validatePipeline()) {
|
||||
return null; // Return null if validation fails
|
||||
}
|
||||
|
||||
var pipelineName = document.getElementById("pipelineName").value;
|
||||
let pipelineList = document.getElementById("pipelineList").children;
|
||||
let pipelineConfig = {
|
||||
name: pipelineName,
|
||||
pipeline: [],
|
||||
_examples: {
|
||||
outputDir: "{outputFolder}/{folderName}",
|
||||
outputFileName: "{filename}-{pipelineName}-{date}-{time}",
|
||||
},
|
||||
outputDir: "{outputFolder}",
|
||||
outputFileName: "{filename}",
|
||||
};
|
||||
|
||||
for (let i = 0; i < pipelineList.length; i++) {
|
||||
let operationName = pipelineList[i].querySelector(".operationName").textContent;
|
||||
let parameters = operationSettings[operationName] || {};
|
||||
|
||||
parameters["fileInput"] = "automated";
|
||||
|
||||
pipelineConfig.pipeline.push({
|
||||
operation: operationName,
|
||||
parameters: parameters,
|
||||
});
|
||||
}
|
||||
return JSON.stringify(pipelineConfig, null, 2);
|
||||
}
|
||||
|
||||
function savePipelineToBrowser() {
|
||||
let pipelineConfigJson = configToJson();
|
||||
if (!pipelineConfigJson) {
|
||||
console.error("Failed to save pipeline: Invalid configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
let pipelineName = document.getElementById("pipelineName").value;
|
||||
if (!pipelineName) {
|
||||
console.error("Failed to save pipeline: Pipeline name is required");
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("#Pipeline-" +pipelineName, pipelineConfigJson);
|
||||
console.log("Pipeline configuration saved to localStorage");
|
||||
}
|
||||
function savePipeline() {
|
||||
let pipelineConfigJson = configToJson();
|
||||
if (!pipelineConfigJson) {
|
||||
console.error("Failed to save pipeline: Invalid configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
let pipelineName = document.getElementById("pipelineName").value;
|
||||
console.log("Downloading...");
|
||||
let a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(new Blob([pipelineConfigJson], { type: "application/json" }));
|
||||
a.download = pipelineName + ".json";
|
||||
a.style.display = "none";
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
async function processPipelineConfig(configString) {
|
||||
console.log("configString", configString);
|
||||
let pipelineConfig = JSON.parse(configString);
|
||||
let pipelineList = document.getElementById("pipelineList");
|
||||
|
||||
while (pipelineList.firstChild) {
|
||||
pipelineList.removeChild(pipelineList.firstChild);
|
||||
}
|
||||
document.getElementById("pipelineName").value = pipelineConfig.name;
|
||||
for (const operationConfig of pipelineConfig.pipeline) {
|
||||
let operationsDropdown = document.getElementById("operationsDropdown");
|
||||
operationsDropdown.value = operationConfig.operation;
|
||||
operationSettings[operationConfig.operation] = operationConfig.parameters;
|
||||
|
||||
// assuming addOperation is async
|
||||
await new Promise((resolve) => {
|
||||
document.getElementById("addOperationBtn").addEventListener("click", resolve, { once: true });
|
||||
document.getElementById("addOperationBtn").click();
|
||||
});
|
||||
|
||||
let lastOperation = pipelineList.lastChild;
|
||||
|
||||
Object.keys(operationConfig.parameters).forEach((parameterName) => {
|
||||
let input = document.getElementById(parameterName);
|
||||
if (input) {
|
||||
switch (input.type) {
|
||||
case "checkbox":
|
||||
input.checked = operationConfig.parameters[parameterName];
|
||||
break;
|
||||
case "number":
|
||||
input.value = operationConfig.parameters[parameterName].toString();
|
||||
break;
|
||||
case "file":
|
||||
if (parameterName !== "fileInput") {
|
||||
// Create a new file input element
|
||||
let newInput = document.createElement("input");
|
||||
newInput.type = "file";
|
||||
newInput.id = parameterName;
|
||||
|
||||
// Add the new file input to the main page (change the selector according to your needs)
|
||||
document.querySelector("#main").appendChild(newInput);
|
||||
}
|
||||
break;
|
||||
case "text":
|
||||
case "textarea":
|
||||
default:
|
||||
var value = operationConfig.parameters[parameterName]
|
||||
if (typeof value !== 'string') {
|
||||
input.value = JSON.stringify(value) ;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("uploadPipelineBtn").addEventListener("click", function () {
|
||||
document.getElementById("uploadPipelineInput").click();
|
||||
});
|
||||
|
||||
document.getElementById("uploadPipelineInput").addEventListener("change", function (e) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
processPipelineConfig(event.target.result);
|
||||
};
|
||||
reader.readAsText(e.target.files[0]);
|
||||
hideOrShowPipelineHeader();
|
||||
});
|
||||
|
||||
document.getElementById("pipelineSelect").addEventListener("change", function (e) {
|
||||
let selectedPipelineJson = e.target.value; // assuming the selected value is the JSON string of the pipeline config
|
||||
processPipelineConfig(selectedPipelineJson);
|
||||
});
|
||||
|
||||
function hideOrShowPipelineHeader() {
|
||||
var pipelineHeader = document.getElementById("pipelineHeader");
|
||||
var pipelineList = document.getElementById("pipelineList");
|
||||
|
||||
if (pipelineList.children.length === 0) {
|
||||
// Hide the pipeline header if there are no items in the pipeline list
|
||||
pipelineHeader.style.display = "none";
|
||||
} else {
|
||||
// Show the pipeline header if there are items in the pipeline list
|
||||
pipelineHeader.style.display = "block";
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
|
||||
window.onload = function () {
|
||||
var items = document.querySelectorAll(".dropdown-item, .nav-link");
|
||||
var dummyContainer = document.createElement("div");
|
||||
dummyContainer.style.position = "absolute";
|
||||
dummyContainer.style.visibility = "hidden";
|
||||
dummyContainer.style.whiteSpace = "nowrap"; // Ensure we measure full width
|
||||
document.body.appendChild(dummyContainer);
|
||||
|
||||
var maxWidth = 0;
|
||||
|
||||
items.forEach(function (item) {
|
||||
var clone = item.cloneNode(true);
|
||||
dummyContainer.appendChild(clone);
|
||||
var width = clone.offsetWidth;
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
}
|
||||
dummyContainer.removeChild(clone);
|
||||
});
|
||||
|
||||
document.body.removeChild(dummyContainer);
|
||||
|
||||
// Store max width for later use
|
||||
window.navItemMaxWidth = maxWidth;
|
||||
};
|
||||
|
||||
document.querySelector("#navbarSearchInput").addEventListener("input", function (e) {
|
||||
var searchText = e.target.value.trim().toLowerCase();
|
||||
var items = document.querySelectorAll("a.dropdown-item[data-bs-tags]");
|
||||
var resultsBox = document.querySelector("#searchResults");
|
||||
|
||||
resultsBox.innerHTML = "";
|
||||
|
||||
if (searchText !== "") {
|
||||
var addedResults = new Set();
|
||||
|
||||
items.forEach(function (item) {
|
||||
var titleElement = item.querySelector(".icon-text");
|
||||
var iconElement = item.querySelector(".material-symbols-rounded, .icon");
|
||||
var itemHref = item.getAttribute("href");
|
||||
var tags = item.getAttribute("data-bs-tags") || "";
|
||||
|
||||
if (titleElement && iconElement && itemHref !== "#") {
|
||||
var title = titleElement.innerText.trim();
|
||||
|
||||
if (
|
||||
(title.toLowerCase().includes(searchText) || tags.toLowerCase().includes(searchText)) &&
|
||||
!addedResults.has(itemHref)
|
||||
) {
|
||||
var dropdownItem = document.createElement("div");
|
||||
dropdownItem.className = "dropdown-item d-flex justify-content-between align-items-center";
|
||||
|
||||
var contentWrapper = document.createElement("div");
|
||||
contentWrapper.className = "d-flex align-items-center flex-grow-1";
|
||||
contentWrapper.style.textDecoration = "none";
|
||||
contentWrapper.style.color = "inherit";
|
||||
|
||||
var originalContent = item.querySelector("div").cloneNode(true);
|
||||
contentWrapper.appendChild(originalContent);
|
||||
|
||||
contentWrapper.onclick = function () {
|
||||
window.location.href = itemHref;
|
||||
};
|
||||
|
||||
dropdownItem.appendChild(contentWrapper);
|
||||
resultsBox.appendChild(dropdownItem);
|
||||
addedResults.add(itemHref);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resultsBox.style.width = window.navItemMaxWidth + "px";
|
||||
});
|
||||
|
||||
|
||||
const searchDropdown = document.getElementById('searchDropdown');
|
||||
const searchInput = document.getElementById('navbarSearchInput');
|
||||
const dropdownMenu = searchDropdown.querySelector('.dropdown-menu');
|
||||
|
||||
// Handle dropdown shown event
|
||||
searchDropdown.addEventListener('shown.bs.dropdown', function () {
|
||||
searchInput.focus();
|
||||
});
|
||||
|
||||
// Handle hover opening
|
||||
searchDropdown.addEventListener('mouseenter', function () {
|
||||
const dropdownInstance = new bootstrap.Dropdown(searchDropdown);
|
||||
dropdownInstance.show();
|
||||
|
||||
setTimeout(() => {
|
||||
searchInput.focus();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Handle mouse leave
|
||||
searchDropdown.addEventListener('mouseleave', function () {
|
||||
// Check if current value is empty (including if user typed and then deleted)
|
||||
if (searchInput.value.trim().length === 0) {
|
||||
searchInput.blur();
|
||||
const dropdownInstance = new bootstrap.Dropdown(searchDropdown);
|
||||
dropdownInstance.hide();
|
||||
}
|
||||
});
|
||||
|
||||
searchDropdown.addEventListener('hidden.bs.dropdown', function () {
|
||||
if (searchInput.value.trim().length === 0) {
|
||||
searchInput.blur();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
// Get the download option from local storage, or set it to 'sameWindow' if it doesn't exist
|
||||
var downloadOption = localStorage.getItem("downloadOption") || "sameWindow";
|
||||
|
||||
// Set the selected option in the dropdown
|
||||
document.getElementById("downloadOption").value = downloadOption;
|
||||
|
||||
// Save the selected option to local storage when the dropdown value changes
|
||||
document.getElementById("downloadOption").addEventListener("change", function () {
|
||||
downloadOption = this.value;
|
||||
localStorage.setItem("downloadOption", downloadOption);
|
||||
});
|
||||
|
||||
// Get the zipThreshold value from local storage, or set it to 0 if it doesn't exist
|
||||
var zipThreshold = parseInt(localStorage.getItem("zipThreshold"), 10) || 4;
|
||||
|
||||
// Set the value of the slider and the display span
|
||||
document.getElementById("zipThreshold").value = zipThreshold;
|
||||
document.getElementById("zipThresholdValue").textContent = zipThreshold;
|
||||
|
||||
// Save the selected value to local storage when the slider value changes
|
||||
document.getElementById("zipThreshold").addEventListener("input", function () {
|
||||
zipThreshold = this.value;
|
||||
document.getElementById("zipThresholdValue").textContent = zipThreshold;
|
||||
localStorage.setItem("zipThreshold", zipThreshold);
|
||||
});
|
||||
|
||||
var boredWaiting = localStorage.getItem("boredWaiting") || "disabled";
|
||||
document.getElementById("boredWaiting").checked = boredWaiting === "enabled";
|
||||
|
||||
document.getElementById("boredWaiting").addEventListener("change", function () {
|
||||
boredWaiting = this.checked ? "enabled" : "disabled";
|
||||
localStorage.setItem("boredWaiting", boredWaiting);
|
||||
});
|
||||
|
||||
var cacheInputs = localStorage.getItem("cacheInputs") || "disabled";
|
||||
document.getElementById("cacheInputs").checked = cacheInputs === "enabled";
|
||||
|
||||
document.getElementById("cacheInputs").addEventListener("change", function () {
|
||||
cacheInputs = this.checked ? "enabled" : "disabled";
|
||||
localStorage.setItem("cacheInputs", cacheInputs);
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
|
||||
const undoButton = document.getElementById("signature-undo-button");
|
||||
const redoButton = document.getElementById("signature-redo-button");
|
||||
const signaturePad = new SignaturePad(signaturePadCanvas, {
|
||||
minWidth: 1,
|
||||
maxWidth: 2,
|
||||
penColor: 'black',
|
||||
});
|
||||
|
||||
let undoData = [];
|
||||
|
||||
signaturePad.addEventListener("endStroke", () => {
|
||||
undoData = [];
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
switch (true) {
|
||||
case event.key === "z" && event.ctrlKey:
|
||||
undoButton.click();
|
||||
break;
|
||||
case event.key === "y" && event.ctrlKey:
|
||||
redoButton.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function undoDraw() {
|
||||
const data = signaturePad.toData();
|
||||
if (data && data.length > 0) {
|
||||
const removed = data.pop();
|
||||
undoData.push(removed);
|
||||
signaturePad.fromData(data);
|
||||
}
|
||||
}
|
||||
|
||||
function redoDraw() {
|
||||
if (undoData.length > 0) {
|
||||
const data = signaturePad.toData();
|
||||
data.push(undoData.pop());
|
||||
signaturePad.fromData(data);
|
||||
}
|
||||
}
|
||||
|
||||
function addDraggableFromPad() {
|
||||
if (signaturePad.isEmpty()) return;
|
||||
const startTime = Date.now();
|
||||
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
|
||||
console.log(Date.now() - startTime);
|
||||
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
|
||||
}
|
||||
|
||||
function getCroppedCanvasDataUrl(canvas) {
|
||||
let originalCtx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
let originalWidth = canvas.width;
|
||||
let originalHeight = canvas.height;
|
||||
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
|
||||
|
||||
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1;
|
||||
|
||||
for (let y = 0; y < originalHeight; y++) {
|
||||
for (let x = 0; x < originalWidth; x++) {
|
||||
let idx = (y * originalWidth + x) * 4;
|
||||
let alpha = imageData.data[idx + 3];
|
||||
if (alpha > 0) {
|
||||
if (minX > x) minX = x;
|
||||
if (maxX < x) maxX = x;
|
||||
if (minY > y) minY = y;
|
||||
if (maxY < y) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let croppedWidth = maxX - minX;
|
||||
let croppedHeight = maxY - minY;
|
||||
if (croppedWidth < 0 || croppedHeight < 0) return null;
|
||||
let cutImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
|
||||
|
||||
let croppedCanvas = document.createElement('canvas');
|
||||
let croppedCtx = croppedCanvas.getContext('2d');
|
||||
|
||||
croppedCanvas.width = croppedWidth;
|
||||
croppedCanvas.height = croppedHeight;
|
||||
croppedCtx.putImageData(cutImageData, 0, 0);
|
||||
|
||||
return croppedCanvas.toDataURL();
|
||||
}
|
||||
|
||||
function isMobile() {
|
||||
const userAgentCheck = /Mobi|Android|iPhone|iPad|iPod|Windows Phone|Opera Mini/i.test(navigator.userAgent);
|
||||
const viewportCheck = window.matchMedia('(max-width: 768px)').matches;
|
||||
return userAgentCheck || viewportCheck;
|
||||
}
|
||||
|
||||
function getDeviceScalingFactor() {
|
||||
return isMobile() ? 3 : 10;
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
const ratio = Math.max(window.devicePixelRatio || 1, 1);
|
||||
const additionalFactor = getDeviceScalingFactor();
|
||||
|
||||
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor;
|
||||
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
|
||||
signaturePadCanvas.getContext('2d').scale(ratio * additionalFactor, ratio * additionalFactor);
|
||||
|
||||
signaturePad.clear();
|
||||
}
|
||||
|
||||
const debounce = (fn, delay = 100) => {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const debouncedResize = debounce(resizeCanvas, 200);
|
||||
|
||||
new IntersectionObserver((entries) => {
|
||||
if (entries.some((entry) => entry.intersectionRatio > 0)) {
|
||||
debouncedResize();
|
||||
}
|
||||
}).observe(signaturePadCanvas);
|
||||
|
||||
new ResizeObserver(debouncedResize).observe(signaturePadCanvas);
|
||||
@@ -0,0 +1,37 @@
|
||||
TabContainer = {
|
||||
initTabGroups() {
|
||||
const groups = document.querySelectorAll('.tab-group');
|
||||
const unloadedGroups = [...groups].filter((g) => !g.initialised);
|
||||
unloadedGroups.forEach((group) => {
|
||||
const containers = group.querySelectorAll('.tab-container');
|
||||
const tabTitles = [...containers].map((c) => c.getAttribute('data-title'));
|
||||
const tabList = document.createElement('div');
|
||||
tabList.classList.add('tab-buttons');
|
||||
tabTitles.forEach((title) => {
|
||||
const tabButton = document.createElement('button');
|
||||
tabButton.textContent = title;
|
||||
tabButton.onclick = (e) => {
|
||||
this.setActiveTab(e.target);
|
||||
};
|
||||
tabList.appendChild(tabButton);
|
||||
});
|
||||
group.prepend(tabList);
|
||||
|
||||
this.setActiveTab(tabList.firstChild);
|
||||
|
||||
group.initialised = true;
|
||||
});
|
||||
},
|
||||
setActiveTab(tabButton) {
|
||||
const group = tabButton.closest('.tab-group');
|
||||
|
||||
group.querySelectorAll('.active').forEach((el) => el.classList.remove('active'));
|
||||
|
||||
tabButton.classList.add('active');
|
||||
group.querySelector(`[data-title="${tabButton.innerHTML}"]`).classList.add('active');
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
TabContainer.initTabGroups();
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,65 @@
|
||||
import './cookieconsent.umd.js';
|
||||
|
||||
// Enable dark mode
|
||||
document.documentElement.classList.add('cc--darkmode');
|
||||
|
||||
CookieConsent.run({
|
||||
guiOptions: {
|
||||
consentModal: {
|
||||
layout: "bar",
|
||||
position: "bottom",
|
||||
equalWeightButtons: true,
|
||||
flipButtons: true
|
||||
},
|
||||
preferencesModal: {
|
||||
layout: "box",
|
||||
position: "right",
|
||||
equalWeightButtons: true,
|
||||
flipButtons: true
|
||||
}
|
||||
},
|
||||
categories: {
|
||||
necessary: {
|
||||
readOnly: true
|
||||
},
|
||||
analytics: {}
|
||||
},
|
||||
language: {
|
||||
default: "en",
|
||||
translations: {
|
||||
en: {
|
||||
consentModal: {
|
||||
title: cookieBannerPopUpTitle,
|
||||
description: cookieBannerPopUpDescription1 + "<br>" + cookieBannerPopUpDescription2,
|
||||
acceptAllBtn: cookieBannerPopUpAcceptAllBtn,
|
||||
acceptNecessaryBtn: cookieBannerPopUpAcceptNecessaryBtn,
|
||||
showPreferencesBtn: cookieBannerPopUpShowPreferencesBtn,
|
||||
},
|
||||
preferencesModal: {
|
||||
title: cookieBannerPreferencesModalTitle,
|
||||
acceptAllBtn: cookieBannerPreferencesModalAcceptAllBtn,
|
||||
acceptNecessaryBtn: cookieBannerPreferencesModalAcceptNecessaryBtn,
|
||||
savePreferencesBtn: cookieBannerPreferencesModalSavePreferencesBtn,
|
||||
closeIconLabel: cookieBannerPreferencesModalCloseIconLabel,
|
||||
serviceCounterLabel: cookieBannerPreferencesModalServiceCounterLabel,
|
||||
sections: [
|
||||
{
|
||||
title: cookieBannerPreferencesModalSubtitle,
|
||||
description: cookieBannerPreferencesModalDescription1 + "<br><br>" + cookieBannerPreferencesModalDescription2 + "<b> " + cookieBannerPreferencesModalDescription3 + "</b>"
|
||||
},
|
||||
{
|
||||
title:cookieBannerPreferencesModalNecessaryTitle1 + "<span class=\"pm__badge\">" + cookieBannerPreferencesModalNecessaryTitle2 + "</span>",
|
||||
description: cookieBannerPreferencesModalNecessaryDescription,
|
||||
linkedCategory: "necessary"
|
||||
},
|
||||
{
|
||||
title: cookieBannerPreferencesModalAnalyticsTitle,
|
||||
description: cookieBannerPreferencesModalAnalyticsDescription,
|
||||
linkedCategory: "analytics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
+8
@@ -0,0 +1,8 @@
|
||||
/* Font Face Observer v2.1.0 - © Bram Stein. License: BSD-3-Clause */(function(){function l(a,b){document.addEventListener?a.addEventListener("scroll",b,!1):a.attachEvent("scroll",b)}function m(a){document.body?a():document.addEventListener?document.addEventListener("DOMContentLoaded",function c(){document.removeEventListener("DOMContentLoaded",c);a()}):document.attachEvent("onreadystatechange",function k(){if("interactive"==document.readyState||"complete"==document.readyState)document.detachEvent("onreadystatechange",k),a()})};function t(a){this.a=document.createElement("div");this.a.setAttribute("aria-hidden","true");this.a.appendChild(document.createTextNode(a));this.b=document.createElement("span");this.c=document.createElement("span");this.h=document.createElement("span");this.f=document.createElement("span");this.g=-1;this.b.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.c.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";
|
||||
this.f.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.h.style.cssText="display:inline-block;width:200%;height:200%;font-size:16px;max-width:none;";this.b.appendChild(this.h);this.c.appendChild(this.f);this.a.appendChild(this.b);this.a.appendChild(this.c)}
|
||||
function u(a,b){a.a.style.cssText="max-width:none;min-width:20px;min-height:20px;display:inline-block;overflow:hidden;position:absolute;width:auto;margin:0;padding:0;top:-999px;white-space:nowrap;font-synthesis:none;font:"+b+";"}function z(a){var b=a.a.offsetWidth,c=b+100;a.f.style.width=c+"px";a.c.scrollLeft=c;a.b.scrollLeft=a.b.scrollWidth+100;return a.g!==b?(a.g=b,!0):!1}function A(a,b){function c(){var a=k;z(a)&&a.a.parentNode&&b(a.g)}var k=a;l(a.b,c);l(a.c,c);z(a)};function B(a,b){var c=b||{};this.family=a;this.style=c.style||"normal";this.weight=c.weight||"normal";this.stretch=c.stretch||"normal"}var C=null,D=null,E=null,F=null;function G(){if(null===D)if(J()&&/Apple/.test(window.navigator.vendor)){var a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))(?:\.([0-9]+))/.exec(window.navigator.userAgent);D=!!a&&603>parseInt(a[1],10)}else D=!1;return D}function J(){null===F&&(F=!!document.fonts);return F}
|
||||
function K(){if(null===E){var a=document.createElement("div");try{a.style.font="condensed 100px sans-serif"}catch(b){}E=""!==a.style.font}return E}function L(a,b){return[a.style,a.weight,K()?a.stretch:"","100px",b].join(" ")}
|
||||
B.prototype.load=function(a,b){var c=this,k=a||"BESbswy",r=0,n=b||3E3,H=(new Date).getTime();return new Promise(function(a,b){if(J()&&!G()){var M=new Promise(function(a,b){function e(){(new Date).getTime()-H>=n?b(Error(""+n+"ms timeout exceeded")):document.fonts.load(L(c,'"'+c.family+'"'),k).then(function(c){1<=c.length?a():setTimeout(e,25)},b)}e()}),N=new Promise(function(a,c){r=setTimeout(function(){c(Error(""+n+"ms timeout exceeded"))},n)});Promise.race([N,M]).then(function(){clearTimeout(r);a(c)},
|
||||
b)}else m(function(){function v(){var b;if(b=-1!=f&&-1!=g||-1!=f&&-1!=h||-1!=g&&-1!=h)(b=f!=g&&f!=h&&g!=h)||(null===C&&(b=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent),C=!!b&&(536>parseInt(b[1],10)||536===parseInt(b[1],10)&&11>=parseInt(b[2],10))),b=C&&(f==w&&g==w&&h==w||f==x&&g==x&&h==x||f==y&&g==y&&h==y)),b=!b;b&&(d.parentNode&&d.parentNode.removeChild(d),clearTimeout(r),a(c))}function I(){if((new Date).getTime()-H>=n)d.parentNode&&d.parentNode.removeChild(d),b(Error(""+
|
||||
n+"ms timeout exceeded"));else{var a=document.hidden;if(!0===a||void 0===a)f=e.a.offsetWidth,g=p.a.offsetWidth,h=q.a.offsetWidth,v();r=setTimeout(I,50)}}var e=new t(k),p=new t(k),q=new t(k),f=-1,g=-1,h=-1,w=-1,x=-1,y=-1,d=document.createElement("div");d.dir="ltr";u(e,L(c,"sans-serif"));u(p,L(c,"serif"));u(q,L(c,"monospace"));d.appendChild(e.a);d.appendChild(p.a);d.appendChild(q.a);document.body.appendChild(d);w=e.a.offsetWidth;x=p.a.offsetWidth;y=q.a.offsetWidth;I();A(e,function(a){f=a;v()});u(e,
|
||||
L(c,'"'+c.family+'",sans-serif'));A(p,function(a){g=a;v()});u(p,L(c,'"'+c.family+'",serif'));A(q,function(a){h=a;v()});u(q,L(c,'"'+c.family+'",monospace'))})})};"object"===typeof module?module.exports=B:(window.FontFaceObserver=B,window.FontFaceObserver.prototype.load=B.prototype.load);}());
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,363 @@
|
||||
// We'll fetch data from the API instead of hardcoding it
|
||||
let allEndpointData = [];
|
||||
let filteredData = [];
|
||||
|
||||
// We'll store these as global variables that get updated when we fetch data
|
||||
let sortedData = [];
|
||||
let totalEndpoints = 0;
|
||||
let totalVisits = 0;
|
||||
|
||||
// Chart instance
|
||||
let myChart;
|
||||
|
||||
|
||||
// Function to get chart colors based on current theme
|
||||
function getChartColors() {
|
||||
var style = window.getComputedStyle(document.body)
|
||||
|
||||
const colours = {
|
||||
textColor: style.getPropertyValue('--md-sys-color-on-surface') ,
|
||||
primaryColor: style.getPropertyValue('--md-sys-color-primary'),
|
||||
backgroundColor: style.getPropertyValue('--md-sys-color-background'),
|
||||
gridColor: style.getPropertyValue('--md-sys-color-on-surface'),
|
||||
tooltipBgColor: style.getPropertyValue('--md-sys-color-inverse-on-surface'),
|
||||
tooltipTextColor: style.getPropertyValue('--md-sys-color-inverse-surface')
|
||||
}
|
||||
return colours;
|
||||
}
|
||||
|
||||
// Watch for theme changes and update chart if needed
|
||||
function setupThemeChangeListener() {
|
||||
|
||||
// Start observing theme changes
|
||||
document.addEventListener("modeChanged", (event) => {
|
||||
setTimeout(function() {
|
||||
if (myChart) {
|
||||
const currentLimit = document.getElementById('currentlyShowing').textContent;
|
||||
const limit = (currentLimit === endpointStatsTranslations.all)
|
||||
? filteredData.length
|
||||
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
|
||||
updateChart(limit);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Also watch for system preference changes
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', () => {
|
||||
if (myChart) {
|
||||
const currentLimit = document.getElementById('currentlyShowing').textContent;
|
||||
const limit = (currentLimit === endpointStatsTranslations.all)
|
||||
? filteredData.length
|
||||
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
|
||||
updateChart(limit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to filter data based on checkbox settings
|
||||
function filterData() {
|
||||
const includeHome = document.getElementById('hideHomeCheckbox').checked;
|
||||
const includeLogin = document.getElementById('hideLoginCheckbox').checked;
|
||||
|
||||
filteredData = allEndpointData.filter(item => {
|
||||
if (!includeHome && item.endpoint === '/') return false;
|
||||
if (!includeLogin && item.endpoint === '/login') return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort and calculate
|
||||
sortedData = [...filteredData].sort((a, b) => b.count - a.count);
|
||||
totalEndpoints = filteredData.length;
|
||||
totalVisits = filteredData.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
// Update stats
|
||||
document.getElementById('totalEndpoints').textContent = totalEndpoints.toLocaleString();
|
||||
document.getElementById('totalVisits').textContent = totalVisits.toLocaleString();
|
||||
|
||||
// Update the chart with current limit
|
||||
const currentLimit = document.getElementById('currentlyShowing').textContent;
|
||||
const limit = (currentLimit === endpointStatsTranslations.all)
|
||||
? filteredData.length
|
||||
: (currentLimit === endpointStatsTranslations.top20 ? 20 : 10);
|
||||
updateChart(limit);
|
||||
}
|
||||
|
||||
// Function to fetch data from the API
|
||||
async function fetchEndpointData() {
|
||||
try {
|
||||
// Show loading state
|
||||
const chartContainer = document.querySelector('.chart-container');
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.className = 'loading';
|
||||
loadingDiv.innerHTML = `
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">${endpointStatsTranslations.loading}</span>
|
||||
</div>`;
|
||||
chartContainer.appendChild(loadingDiv);
|
||||
|
||||
// Also add animation to refresh button
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
refreshBtn.classList.add('refreshing');
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
const response = await fetch('/api/v1/info/load/all');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
allEndpointData = data;
|
||||
|
||||
// Apply filters
|
||||
filterData();
|
||||
|
||||
// Remove loading state
|
||||
chartContainer.removeChild(loadingDiv);
|
||||
refreshBtn.classList.remove('refreshing');
|
||||
refreshBtn.disabled = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching endpoint data:', error);
|
||||
// Show error message to user
|
||||
showError(endpointStatsTranslations.failedToLoad);
|
||||
|
||||
// Reset refresh button
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
refreshBtn.classList.remove('refreshing');
|
||||
refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to format endpoint names
|
||||
function formatEndpointName(endpoint) {
|
||||
if (endpoint === '/') return endpointStatsTranslations.home;
|
||||
if (endpoint === '/login') return endpointStatsTranslations.login;
|
||||
return endpoint.replace('/', '').replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
// Function to update the table
|
||||
function updateTable(data) {
|
||||
const tableBody = document.getElementById('endpointTableBody');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const percentage = ((item.count / totalVisits) * 100).toFixed(2);
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Format endpoint for better readability
|
||||
let displayEndpoint = item.endpoint;
|
||||
if (displayEndpoint.length > 40) {
|
||||
displayEndpoint = displayEndpoint.substring(0, 37) + '...';
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${index + 1}</td>
|
||||
<td title="${item.endpoint}">${displayEndpoint}</td>
|
||||
<td>${item.count.toLocaleString()}</td>
|
||||
<td>${percentage}%</td>
|
||||
`;
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update the chart
|
||||
function updateChart(dataLimit) {
|
||||
const chartData = sortedData.slice(0, dataLimit);
|
||||
|
||||
// Calculate displayed statistics
|
||||
const displayedVisits = chartData.reduce((sum, item) => sum + item.count, 0);
|
||||
const displayedPercentage = totalVisits > 0
|
||||
? ((displayedVisits / totalVisits) * 100).toFixed(2)
|
||||
: '0';
|
||||
|
||||
document.getElementById('displayedVisits').textContent = displayedVisits.toLocaleString();
|
||||
document.getElementById('displayedPercentage').textContent = displayedPercentage;
|
||||
|
||||
// If the limit equals the total filtered items, show "All"; otherwise "Top X"
|
||||
document.getElementById('currentlyShowing').textContent =
|
||||
(dataLimit === filteredData.length)
|
||||
? endpointStatsTranslations.all
|
||||
: endpointStatsTranslations.top + dataLimit;
|
||||
|
||||
// Update the table with new data
|
||||
updateTable(chartData);
|
||||
|
||||
// Prepare labels and datasets
|
||||
const labels = chartData.map(item => formatEndpointName(item.endpoint));
|
||||
const data = chartData.map(item => item.count);
|
||||
|
||||
// Get theme-specific colors
|
||||
const colors = getChartColors();
|
||||
|
||||
// Destroy previous chart if it exists
|
||||
if (myChart) {
|
||||
myChart.destroy();
|
||||
}
|
||||
|
||||
// Create chart context
|
||||
const ctx = document.getElementById('endpointChart').getContext('2d');
|
||||
|
||||
// Create new chart with theme-appropriate colors
|
||||
myChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: endpointStatsTranslations.numberOfVisits,
|
||||
data: data,
|
||||
backgroundColor: colors.primaryColor.replace('rgb', 'rgba').replace(')', ', 0.6)'),
|
||||
borderColor: colors.primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: dataLimit > 20 ? 'x' : 'y',
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: colors.primaryColor,
|
||||
font: {
|
||||
weight: 'bold'
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: colors.tooltipBgColor,
|
||||
titleColor: colors.tooltipTextColor,
|
||||
bodyColor: colors.tooltipTextColor,
|
||||
borderColor: colors.tooltipBgColor,
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
titleFont: {
|
||||
size: 14,
|
||||
weight: 'bold'
|
||||
},
|
||||
bodyFont: {
|
||||
size: 13
|
||||
},
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const value = context.raw;
|
||||
const percentage = totalVisits > 0
|
||||
? ((value / totalVisits) * 100).toFixed(2)
|
||||
: '0';
|
||||
// Insert your i18n text in the final string:
|
||||
// e.g. "Visits: 12 (34% of total)"
|
||||
// If your translation includes placeholders, you'd parse them here:
|
||||
return endpointStatsTranslations.visitsTooltip
|
||||
.replace('{0}', value.toLocaleString())
|
||||
.replace('{1}', percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
border: {
|
||||
color: colors.gridColor
|
||||
},
|
||||
ticks: {
|
||||
color: colors.gridColor,
|
||||
font: {
|
||||
size: 12
|
||||
},
|
||||
callback: function(value, index, values) {
|
||||
let label = this.getLabelForValue(value);
|
||||
return label.length > 15 ? label.substr(0, 15) + '...' : label;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: `${colors.gridColor}`
|
||||
}
|
||||
},
|
||||
y: {
|
||||
border: {
|
||||
color: colors.gridColor
|
||||
},
|
||||
min: 0,
|
||||
ticks: {
|
||||
color: colors.gridColor,
|
||||
font: {
|
||||
size: 12
|
||||
},
|
||||
precision: 0
|
||||
},
|
||||
grid: {
|
||||
color: `${colors.gridColor}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize with fetch and top 10
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set up theme change listener
|
||||
setupThemeChangeListener();
|
||||
|
||||
// Initial data fetch
|
||||
fetchEndpointData();
|
||||
|
||||
// Set up button event listeners
|
||||
document.getElementById('top10Btn').addEventListener('click', function() {
|
||||
updateChart(10);
|
||||
setActiveButton(this);
|
||||
});
|
||||
|
||||
document.getElementById('top20Btn').addEventListener('click', function() {
|
||||
updateChart(20);
|
||||
setActiveButton(this);
|
||||
});
|
||||
|
||||
document.getElementById('allBtn').addEventListener('click', function() {
|
||||
updateChart(filteredData.length);
|
||||
setActiveButton(this);
|
||||
});
|
||||
|
||||
document.getElementById('refreshBtn').addEventListener('click', function() {
|
||||
fetchEndpointData();
|
||||
});
|
||||
|
||||
// Set up filter checkbox listeners
|
||||
document.getElementById('hideHomeCheckbox').addEventListener('change', filterData);
|
||||
document.getElementById('hideLoginCheckbox').addEventListener('change', filterData);
|
||||
});
|
||||
|
||||
function setActiveButton(activeButton) {
|
||||
// Remove active class from all buttons
|
||||
document.querySelectorAll('.chart-controls button').forEach(button => {
|
||||
button.classList.remove('active');
|
||||
});
|
||||
// Add active class to clicked button
|
||||
activeButton.classList.add('active');
|
||||
}
|
||||
|
||||
// Function to handle errors in a user-friendly way
|
||||
function showError(message) {
|
||||
const chartContainer = document.querySelector('.chart-container');
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'alert alert-danger';
|
||||
errorDiv.innerHTML = `
|
||||
<span class="material-symbols-rounded" style="vertical-align: bottom; margin-right: 5px;">error</span>
|
||||
${message}
|
||||
<button id="errorRetryBtn" class="btn btn-outline-danger btn-sm" style="margin-left: 10px;">
|
||||
<span class="material-symbols-rounded" style="font-size: 1rem; vertical-align: bottom;">refresh</span>
|
||||
${endpointStatsTranslations.retry}
|
||||
</button>
|
||||
`;
|
||||
|
||||
chartContainer.innerHTML = '';
|
||||
chartContainer.appendChild(errorDiv);
|
||||
|
||||
// Add retry button functionality
|
||||
document.getElementById('errorRetryBtn').addEventListener('click', fetchEndpointData);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
class UUID {
|
||||
static uuidv4() {
|
||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
|
||||
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UUID;
|
||||
Reference in New Issue
Block a user