Resolve conflicts using files from Stirling-2.0

This commit is contained in:
Reece
2025-06-24 12:51:39 +01:00
parent 16e56e3208
commit a51960b199
691 changed files with 168291 additions and 0 deletions
@@ -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>&uarr;</span>";
const moveDownButton = document.createElement("button");
moveDownButton.className = "btn btn-secondary move-down";
moveDownButton.innerHTML = "<span>&darr;</span>";
const removeButton = document.createElement("button");
removeButton.className = "btn btn-danger remove-file";
removeButton.innerHTML = "<span>&times;</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,653 @@
document.addEventListener('DOMContentLoaded', function() {
const bookmarksContainer = document.getElementById('bookmarks-container');
const addBookmarkBtn = document.getElementById('addBookmarkBtn');
const bookmarkDataInput = document.getElementById('bookmarkData');
let bookmarks = [];
let counter = 0; // Used for generating unique IDs
// Add event listener to the file input to extract existing bookmarks
document.getElementById('fileInput-input').addEventListener('change', async function(e) {
if (!e.target.files || e.target.files.length === 0) {
return;
}
// Reset bookmarks initially
bookmarks = [];
updateBookmarksUI();
const formData = new FormData();
formData.append('file', e.target.files[0]);
// Show loading indicator
showLoadingIndicator();
try {
// Call the API to extract bookmarks using fetchWithCsrf for CSRF protection
const response = await fetchWithCsrf('/api/v1/general/extract-bookmarks', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Failed to extract bookmarks: ${response.status} ${response.statusText}`);
}
const extractedBookmarks = await response.json();
// Convert extracted bookmarks to our format with IDs
if (extractedBookmarks && extractedBookmarks.length > 0) {
bookmarks = extractedBookmarks.map(convertExtractedBookmark);
} else {
showEmptyState();
}
} catch (error) {
// Show error message
showErrorMessage('Failed to extract bookmarks. You can still create new ones.');
// Add a default bookmark if no bookmarks and error
if (bookmarks.length === 0) {
showEmptyState();
}
} finally {
// Remove loading indicator
removeLoadingIndicator();
// Update the UI
updateBookmarksUI();
}
});
function showLoadingIndicator() {
const loadingEl = document.createElement('div');
loadingEl.className = 'alert alert-info';
loadingEl.textContent = 'Loading bookmarks from PDF...';
loadingEl.id = 'loading-bookmarks';
bookmarksContainer.innerHTML = '';
bookmarksContainer.appendChild(loadingEl);
}
function removeLoadingIndicator() {
const loadingEl = document.getElementById('loading-bookmarks');
if (loadingEl) {
loadingEl.remove();
}
}
function showErrorMessage(message) {
const errorEl = document.createElement('div');
errorEl.className = 'alert alert-danger';
errorEl.textContent = message;
bookmarksContainer.appendChild(errorEl);
}
function showEmptyState() {
const emptyStateEl = document.createElement('div');
emptyStateEl.className = 'empty-bookmarks mb-3';
emptyStateEl.innerHTML = `
<span class="material-symbols-rounded mb-2" style="font-size: 48px;">bookmark_add</span>
<h5>No bookmarks found</h5>
<p class="mb-3">This PDF doesn't have any bookmarks yet. Add your first bookmark to get started.</p>
<button type="button" class="btn btn-primary btn-add-first-bookmark">
<span class="material-symbols-rounded">add</span> Add First Bookmark
</button>
`;
// Add event listener to the "Add First Bookmark" button
emptyStateEl.querySelector('.btn-add-first-bookmark').addEventListener('click', function() {
addBookmark(null, 'New Bookmark', 1);
emptyStateEl.remove();
});
bookmarksContainer.appendChild(emptyStateEl);
}
// Function to convert extracted bookmarks to our format with IDs
function convertExtractedBookmark(bookmark) {
counter++;
const result = {
id: Date.now() + counter, // Generate a unique ID
title: bookmark.title || 'Untitled Bookmark',
pageNumber: bookmark.pageNumber || 1,
children: [],
expanded: false // All bookmarks start collapsed for better visibility
};
// Convert children recursively
if (bookmark.children && bookmark.children.length > 0) {
result.children = bookmark.children.map(child => {
return convertExtractedBookmark(child);
});
}
return result;
}
// Add bookmark button click handler
addBookmarkBtn.addEventListener('click', function(e) {
e.preventDefault();
addBookmark();
});
// Add form submit handler to update JSON data
document.getElementById('editTocForm').addEventListener('submit', function() {
updateBookmarkData();
});
function addBookmark(parent = null, title = '', pageNumber = 1) {
counter++;
const newBookmark = {
id: Date.now() + counter,
title: title || 'New Bookmark',
pageNumber: pageNumber || 1,
children: [],
expanded: false // New bookmarks start collapsed
};
if (parent === null) {
bookmarks.push(newBookmark);
} else {
const parentBookmark = findBookmark(bookmarks, parent);
if (parentBookmark) {
parentBookmark.children.push(newBookmark);
parentBookmark.expanded = true; // Auto-expand the parent when adding a child
} else {
// Add to root level if parent not found
bookmarks.push(newBookmark);
}
}
updateBookmarksUI();
// After updating UI, find and focus the new bookmark's title field
setTimeout(() => {
const newElement = document.querySelector(`[data-id="${newBookmark.id}"]`);
if (newElement) {
const titleInput = newElement.querySelector('.bookmark-title');
if (titleInput) {
titleInput.focus();
titleInput.select();
}
// Scroll to the new element
newElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 50);
}
function findBookmark(bookmarkArray, id) {
for (const bookmark of bookmarkArray) {
if (bookmark.id === id) {
return bookmark;
}
if (bookmark.children.length > 0) {
const found = findBookmark(bookmark.children, id);
if (found) return found;
}
}
return null;
}
// Find the parent bookmark of a given bookmark by ID
function findParentBookmark(bookmarkArray, id, parent = null) {
for (const bookmark of bookmarkArray) {
if (bookmark.id === id) {
return parent; // Return the parent ID (or null if top-level)
}
if (bookmark.children.length > 0) {
const found = findParentBookmark(bookmark.children, id, bookmark.id);
if (found !== undefined) return found;
}
}
return undefined; // Not found at this level
}
function removeBookmark(id) {
// Remove from top level
const index = bookmarks.findIndex(b => b.id === id);
if (index !== -1) {
bookmarks.splice(index, 1);
updateBookmarksUI();
return;
}
// Remove from children
function removeFromChildren(bookmarkArray, id) {
for (const bookmark of bookmarkArray) {
const childIndex = bookmark.children.findIndex(b => b.id === id);
if (childIndex !== -1) {
bookmark.children.splice(childIndex, 1);
return true;
}
if (removeFromChildren(bookmark.children, id)) {
return true;
}
}
return false;
}
if (removeFromChildren(bookmarks, id)) {
updateBookmarksUI();
}
// If no bookmarks left, show empty state
if (bookmarks.length === 0) {
showEmptyState();
}
}
function toggleBookmarkExpanded(id) {
const bookmark = findBookmark(bookmarks, id);
if (bookmark) {
bookmark.expanded = !bookmark.expanded;
updateBookmarksUI();
}
}
function updateBookmarkData() {
// Create a clean version without the IDs for submission
const cleanBookmarks = bookmarks.map(cleanBookmark);
bookmarkDataInput.value = JSON.stringify(cleanBookmarks);
}
function cleanBookmark(bookmark) {
return {
title: bookmark.title,
pageNumber: bookmark.pageNumber,
children: bookmark.children.map(cleanBookmark)
};
}
function updateBookmarksUI() {
if (!bookmarksContainer) {
return;
}
// Only clear the container if there are no error messages or loading indicators
if (!document.querySelector('#bookmarks-container .alert')) {
bookmarksContainer.innerHTML = '';
}
// Check if there are bookmarks to display
if (bookmarks.length === 0 && !document.querySelector('.empty-bookmarks')) {
showEmptyState();
} else {
// Remove empty state if it exists and there are bookmarks
const emptyState = document.querySelector('.empty-bookmarks');
if (emptyState && bookmarks.length > 0) {
emptyState.remove();
}
// Create bookmark elements
bookmarks.forEach(bookmark => {
const bookmarkElement = createBookmarkElement(bookmark);
bookmarksContainer.appendChild(bookmarkElement);
});
}
updateBookmarkData();
// Initialize tooltips for dynamically added elements
if (typeof $ !== 'undefined') {
$('[data-bs-toggle="tooltip"]').tooltip();
}
}
// Create the main bookmark element with collapsible interface
function createBookmarkElement(bookmark, level = 0) {
const bookmarkEl = document.createElement('div');
bookmarkEl.className = 'bookmark-item';
bookmarkEl.dataset.id = bookmark.id;
bookmarkEl.dataset.level = level;
// Create the header (always visible part)
const header = createBookmarkHeader(bookmark, level);
bookmarkEl.appendChild(header);
// Create the content (collapsible part)
const content = document.createElement('div');
content.className = 'bookmark-content';
if (!bookmark.expanded) {
content.style.display = 'none';
}
// Main input row
const inputRow = createInputRow(bookmark);
content.appendChild(inputRow);
bookmarkEl.appendChild(content);
// Add children container if has children and expanded
if (bookmark.children && bookmark.children.length > 0) {
const childrenContainer = createChildrenContainer(bookmark, level);
if (bookmark.expanded) {
bookmarkEl.appendChild(childrenContainer);
}
}
return bookmarkEl;
}
// Create the header that's always visible
function createBookmarkHeader(bookmark, level) {
const header = document.createElement('div');
header.className = 'bookmark-header';
if (!bookmark.expanded) {
header.classList.add('collapsed');
}
// Left side of header with expand/collapse and info
const headerLeft = document.createElement('div');
headerLeft.className = 'd-flex align-items-center';
// Toggle expand/collapse icon with child count
const toggleContainer = document.createElement('div');
toggleContainer.className = 'd-flex align-items-center';
toggleContainer.style.marginRight = '8px';
// Only show toggle if has children
if (bookmark.children && bookmark.children.length > 0) {
// Create toggle icon
const toggleIcon = document.createElement('span');
toggleIcon.className = 'material-symbols-rounded toggle-icon me-1';
toggleIcon.textContent = 'expand_more';
toggleIcon.style.cursor = 'pointer';
toggleContainer.appendChild(toggleIcon);
// Add child count indicator
const childCount = document.createElement('span');
childCount.className = 'badge rounded-pill';
// Use theme-appropriate badge color
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
childCount.classList.add(isDarkMode ? 'bg-info' : 'bg-secondary');
childCount.style.fontSize = '0.7rem';
childCount.style.padding = '0.2em 0.5em';
childCount.textContent = bookmark.children.length;
childCount.setAttribute('data-bs-toggle', 'tooltip');
childCount.setAttribute('data-bs-placement', 'top');
childCount.title = `${bookmark.children.length} child bookmark${bookmark.children.length > 1 ? 's' : ''}`;
toggleContainer.appendChild(childCount);
} else {
// Add spacer if no children
const spacer = document.createElement('span');
spacer.style.width = '24px';
spacer.style.display = 'inline-block';
toggleContainer.appendChild(spacer);
}
headerLeft.appendChild(toggleContainer);
// Level indicator for nested items
if (level > 0) {
// Add relationship indicator visual line
const relationshipIndicator = document.createElement('div');
relationshipIndicator.className = 'bookmark-relationship-indicator';
const line = document.createElement('div');
line.className = 'relationship-line';
relationshipIndicator.appendChild(line);
const arrow = document.createElement('div');
arrow.className = 'relationship-arrow';
relationshipIndicator.appendChild(arrow);
header.appendChild(relationshipIndicator);
// Text indicator
const levelIndicator = document.createElement('span');
levelIndicator.className = 'bookmark-level-indicator';
levelIndicator.textContent = `Child`;
headerLeft.appendChild(levelIndicator);
}
// Title preview
const titlePreview = document.createElement('span');
titlePreview.className = 'bookmark-title-preview';
titlePreview.textContent = bookmark.title;
headerLeft.appendChild(titlePreview);
// Page number preview
const pagePreview = document.createElement('span');
pagePreview.className = 'bookmark-page-preview';
pagePreview.textContent = `Page ${bookmark.pageNumber}`;
headerLeft.appendChild(pagePreview);
// Right side of header with action buttons
const headerRight = document.createElement('div');
headerRight.className = 'bookmark-actions-header';
// Quick add buttons with clear visual distinction - using Stirling-PDF's tooltip system
const quickAddChildButton = createButton('subdirectory_arrow_right', 'btn-add-child', 'Add child bookmark', function(e) {
e.preventDefault();
e.stopPropagation();
addBookmark(bookmark.id);
});
const quickAddSiblingButton = createButton('add', 'btn-add-sibling', 'Add sibling bookmark', function(e) {
e.preventDefault();
e.stopPropagation();
// Find parent of current bookmark
const parentId = findParentBookmark(bookmarks, bookmark.id);
addBookmark(parentId, '', bookmark.pageNumber); // Same level as current bookmark
});
// Quick remove button
const quickRemoveButton = createButton('delete', 'btn-outline-danger', 'Remove bookmark', function(e) {
e.preventDefault();
e.stopPropagation();
if (confirm('Are you sure you want to remove this bookmark' +
(bookmark.children.length > 0 ? ' and all its children?' : '?'))) {
removeBookmark(bookmark.id);
}
});
headerRight.appendChild(quickAddChildButton);
headerRight.appendChild(quickAddSiblingButton);
headerRight.appendChild(quickRemoveButton);
// Assemble header
header.appendChild(headerLeft);
header.appendChild(headerRight);
// Add click handler for expansion toggle
header.addEventListener('click', function(e) {
// Only toggle if not clicking on buttons
if (!e.target.closest('button')) {
toggleBookmarkExpanded(bookmark.id);
}
});
return header;
}
function createInputRow(bookmark) {
const row = document.createElement('div');
row.className = 'row';
// Title input
row.appendChild(createTitleInputElement(bookmark));
// Page input
row.appendChild(createPageInputElement(bookmark));
return row;
}
function createTitleInputElement(bookmark) {
const titleCol = document.createElement('div');
titleCol.className = 'col-md-8';
const titleGroup = document.createElement('div');
titleGroup.className = 'mb-3';
const titleLabel = document.createElement('label');
titleLabel.textContent = 'Title';
titleLabel.className = 'form-label';
const titleInput = document.createElement('input');
titleInput.type = 'text';
titleInput.className = 'form-control bookmark-title';
titleInput.value = bookmark.title;
titleInput.addEventListener('input', function() {
bookmark.title = this.value;
updateBookmarkData();
// Also update the preview in the header
const header = titleInput.closest('.bookmark-item').querySelector('.bookmark-title-preview');
if (header) {
header.textContent = this.value;
}
});
titleGroup.appendChild(titleLabel);
titleGroup.appendChild(titleInput);
titleCol.appendChild(titleGroup);
return titleCol;
}
function createPageInputElement(bookmark) {
const pageCol = document.createElement('div');
pageCol.className = 'col-md-4';
const pageGroup = document.createElement('div');
pageGroup.className = 'mb-3';
const pageLabel = document.createElement('label');
pageLabel.textContent = 'Page';
pageLabel.className = 'form-label';
const pageInput = document.createElement('input');
pageInput.type = 'number';
pageInput.className = 'form-control bookmark-page';
pageInput.value = bookmark.pageNumber;
pageInput.min = 1;
pageInput.addEventListener('input', function() {
bookmark.pageNumber = parseInt(this.value) || 1;
updateBookmarkData();
// Also update the preview in the header
const header = pageInput.closest('.bookmark-item').querySelector('.bookmark-page-preview');
if (header) {
header.textContent = `Page ${bookmark.pageNumber}`;
}
});
pageGroup.appendChild(pageLabel);
pageGroup.appendChild(pageInput);
pageCol.appendChild(pageGroup);
return pageCol;
}
function createButton(icon, className, title, clickHandler) {
const button = document.createElement('button');
button.type = 'button';
button.className = `btn ${className} btn-bookmark-action`;
button.innerHTML = `<span class="material-symbols-rounded">${icon}</span>`;
// Use Bootstrap tooltips
button.setAttribute('data-bs-toggle', 'tooltip');
button.setAttribute('data-bs-placement', 'top');
button.title = title;
button.addEventListener('click', clickHandler);
return button;
}
function createChildrenContainer(bookmark, level) {
const childrenContainer = document.createElement('div');
childrenContainer.className = 'bookmark-children';
bookmark.children.forEach(child => {
childrenContainer.appendChild(createBookmarkElement(child, level + 1));
});
return childrenContainer;
}
// Update the add bookmark button appearance with clear visual cue
addBookmarkBtn.innerHTML = '<span class="material-symbols-rounded">add</span> Add Top-level Bookmark';
addBookmarkBtn.className = 'btn btn-primary btn-add-bookmark top-level';
// Use Bootstrap tooltips
addBookmarkBtn.setAttribute('data-bs-toggle', 'tooltip');
addBookmarkBtn.setAttribute('data-bs-placement', 'top');
addBookmarkBtn.title = 'Add a new top-level bookmark';
// Add icon to empty state button as well
const updateEmptyStateButton = function() {
const emptyStateBtn = document.querySelector('.btn-add-first-bookmark');
if (emptyStateBtn) {
emptyStateBtn.innerHTML = '<span class="material-symbols-rounded">add</span> Add First Bookmark';
emptyStateBtn.setAttribute('data-bs-toggle', 'tooltip');
emptyStateBtn.setAttribute('data-bs-placement', 'top');
emptyStateBtn.title = 'Add first bookmark';
// Initialize tooltips for the empty state button
if (typeof $ !== 'undefined') {
$('[data-bs-toggle="tooltip"]').tooltip();
}
}
};
// Initialize with an empty state if no bookmarks
if (bookmarks.length === 0) {
showEmptyState();
updateEmptyStateButton();
}
// Listen for theme changes to update badge colors
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'data-bs-theme') {
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
document.querySelectorAll('.badge').forEach(badge => {
badge.classList.remove('bg-secondary', 'bg-info');
badge.classList.add(isDarkMode ? 'bg-info' : 'bg-secondary');
});
}
});
});
observer.observe(document.documentElement, { attributes: true });
// Add visual enhancement to clearly show the top-level/child relationship
document.addEventListener('mouseover', function(e) {
// When hovering over add buttons, highlight their relationship targets
const button = e.target.closest('.btn-add-child, .btn-add-sibling');
if (button) {
if (button.classList.contains('btn-add-child')) {
// Highlight parent-child relationship
const bookmarkItem = button.closest('.bookmark-item');
if (bookmarkItem) {
bookmarkItem.style.boxShadow = '0 0 0 2px var(--btn-add-child-border, #198754)';
}
} else if (button.classList.contains('btn-add-sibling')) {
// Highlight sibling relationship
const bookmarkItem = button.closest('.bookmark-item');
if (bookmarkItem) {
// Find siblings
const parent = bookmarkItem.parentElement;
const siblings = parent.querySelectorAll(':scope > .bookmark-item');
siblings.forEach(sibling => {
if (sibling !== bookmarkItem) {
sibling.style.boxShadow = '0 0 0 2px var(--btn-add-sibling-border, #0d6efd)';
}
});
}
}
}
});
document.addEventListener('mouseout', function(e) {
// Remove highlights when not hovering
const button = e.target.closest('.btn-add-child, .btn-add-sibling');
if (button) {
// Remove all highlights
document.querySelectorAll('.bookmark-item').forEach(item => {
item.style.boxShadow = '';
});
}
});
});
@@ -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
@@ -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
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;