mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
co-authored by
EthanHealy01
parent
c9dafc85fd
commit
2bacb4dc81
@@ -1,364 +0,0 @@
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
function formatProblemDetailsJson(input) {
|
||||
try {
|
||||
const obj = typeof input === 'string' ? JSON.parse(input) : input;
|
||||
const preferredOrder = [
|
||||
'errorCode',
|
||||
'title',
|
||||
'status',
|
||||
'type',
|
||||
'detail',
|
||||
'instance',
|
||||
'path',
|
||||
'timestamp',
|
||||
'hints',
|
||||
'actionRequired'
|
||||
];
|
||||
|
||||
const ordered = {};
|
||||
preferredOrder.forEach((key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
ordered[key] = obj[key];
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(ordered, key)) {
|
||||
ordered[key] = obj[key];
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.stringify(ordered, null, 2);
|
||||
} catch (err) {
|
||||
if (typeof input === 'string') return input;
|
||||
try {
|
||||
return JSON.stringify(input, null, 2);
|
||||
} catch (jsonErr) {
|
||||
return String(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatUserFriendlyError(json) {
|
||||
if (!json || typeof json !== 'object') {
|
||||
return typeof json === 'string' ? json : '';
|
||||
}
|
||||
|
||||
const lines = [];
|
||||
const title = json.title || json.error || '';
|
||||
const detail = json.detail || json.message || '';
|
||||
|
||||
const primaryLine = title && detail ? `${title}: ${detail}` : title || detail;
|
||||
|
||||
if (primaryLine) {
|
||||
lines.push(primaryLine);
|
||||
}
|
||||
|
||||
if (json.errorCode) {
|
||||
lines.push('');
|
||||
lines.push(`Error Code: ${json.errorCode}`);
|
||||
}
|
||||
|
||||
const detailAlreadyIncluded = detail && primaryLine && primaryLine.includes(detail);
|
||||
if (detail && !detailAlreadyIncluded) {
|
||||
lines.push('');
|
||||
lines.push(detail);
|
||||
}
|
||||
|
||||
if (json.hints && Array.isArray(json.hints) && json.hints.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('How to fix:');
|
||||
json.hints.forEach((hint, index) => {
|
||||
lines.push(` ${index + 1}. ${hint}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (json.actionRequired) {
|
||||
lines.push('');
|
||||
lines.push(json.actionRequired);
|
||||
}
|
||||
|
||||
if (json.supportId) {
|
||||
lines.push('');
|
||||
lines.push(`Support ID: ${json.supportId}`);
|
||||
}
|
||||
|
||||
return lines
|
||||
.filter((line, index, arr) => {
|
||||
if (line !== '') return true;
|
||||
if (index === 0 || index === arr.length - 1) return false;
|
||||
return arr[index - 1] !== '';
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function buildPdfPasswordProblemDetail(fileName) {
|
||||
const stirling = window.stirlingPDF || {};
|
||||
const detailTemplate = stirling.pdfPasswordDetail || 'The PDF Document is passworded and either the password was not provided or was incorrect';
|
||||
const title = stirling.pdfPasswordTitle || 'PDF Password Required';
|
||||
const hints = [
|
||||
stirling.pdfPasswordHint1,
|
||||
stirling.pdfPasswordHint2,
|
||||
stirling.pdfPasswordHint3,
|
||||
stirling.pdfPasswordHint4,
|
||||
stirling.pdfPasswordHint5,
|
||||
stirling.pdfPasswordHint6
|
||||
].filter((hint) => typeof hint === 'string' && hint.trim().length > 0);
|
||||
const actionRequired = stirling.pdfPasswordAction || 'Provide the owner/permissions password, not just the document open password.';
|
||||
|
||||
return {
|
||||
errorCode: 'E004',
|
||||
title,
|
||||
detail: detailTemplate.replace('{0}', fileName),
|
||||
type: '/errors/pdf-password',
|
||||
path: '/api/v1/security/remove-password',
|
||||
hints,
|
||||
actionRequired
|
||||
};
|
||||
}
|
||||
|
||||
function buildCorruptedPdfProblemDetail(fileName) {
|
||||
const stirling = window.stirlingPDF || {};
|
||||
const detailTemplate = stirling.pdfCorruptedMessage || 'The PDF file "{0}" appears to be corrupted or has an invalid structure.';
|
||||
const hints = [
|
||||
stirling.pdfCorruptedHint1,
|
||||
stirling.pdfCorruptedHint2,
|
||||
stirling.pdfCorruptedHint3
|
||||
].filter((hint) => typeof hint === 'string' && hint.trim().length > 0);
|
||||
const actionRequired = stirling.pdfCorruptedAction || stirling.tryRepairMessage || '';
|
||||
|
||||
return {
|
||||
errorCode: 'E001',
|
||||
title: stirling.pdfCorruptedTitle || 'PDF File Corrupted',
|
||||
detail: detailTemplate.replace('{0}', fileName),
|
||||
type: '/errors/pdf-corrupted',
|
||||
hints,
|
||||
actionRequired
|
||||
};
|
||||
}
|
||||
|
||||
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}`);
|
||||
const problemDetail = buildPdfPasswordProblemDetail(file.name);
|
||||
this.showProblemDetail(problemDetail);
|
||||
return null; // No file to return
|
||||
}
|
||||
|
||||
formData.append('password', password);
|
||||
}
|
||||
// Send decryption request
|
||||
const response = await fetchWithCsrf('/api/v1/security/remove-password', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json') || contentType.includes('application/problem+json')) {
|
||||
const errorJson = await response.json();
|
||||
this.showProblemDetail(errorJson);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error(`${window.decrypt.invalidPassword} ${errorText}`);
|
||||
const fallbackProblem = buildPdfPasswordProblemDetail(file.name);
|
||||
if (errorText && errorText.trim().length > 0) {
|
||||
fallbackProblem.detail = errorText.trim();
|
||||
}
|
||||
this.showProblemDetail(fallbackProblem);
|
||||
}
|
||||
return null; // No file to return
|
||||
}
|
||||
|
||||
this.removeErrorBanner();
|
||||
const decryptedBlob = await response.blob();
|
||||
return new File([decryptedBlob], file.name, {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle network or unexpected errors
|
||||
console.error(`Failed to decrypt PDF: ${file.name}`, error);
|
||||
const fallbackDetail =
|
||||
(error && error.message) ||
|
||||
window.decrypt.unexpectedError ||
|
||||
'There was an error processing the file. Please try again.';
|
||||
|
||||
const unexpectedProblem = {
|
||||
title: (window.stirlingPDF && window.stirlingPDF.errorUnexpectedTitle) || 'Unexpected Error',
|
||||
detail: fallbackDetail,
|
||||
};
|
||||
|
||||
if (window.decrypt.serverError) {
|
||||
unexpectedProblem.hints = [
|
||||
window.decrypt.serverError.replace('{0}', file.name),
|
||||
];
|
||||
}
|
||||
|
||||
this.showProblemDetail(unexpectedProblem);
|
||||
return null; // No file to return
|
||||
}
|
||||
}
|
||||
|
||||
async checkFileEncrypted(file) {
|
||||
try {
|
||||
if (file.type !== 'application/pdf') {
|
||||
return {isEncrypted: false, requiresPassword: false};
|
||||
}
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const arrayBufferForPdfLib = arrayBuffer.slice(0);
|
||||
var loadingTask;
|
||||
|
||||
if(this.decryptWorker == null){
|
||||
loadingTask = pdfjsLib.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
data: arrayBuffer,
|
||||
});
|
||||
this.decryptWorker = loadingTask._worker
|
||||
|
||||
}else {
|
||||
loadingTask = pdfjsLib.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
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};
|
||||
}
|
||||
} else if (error.name === 'InvalidPDFException' ||
|
||||
(error.message && error.message.includes('Invalid PDF structure'))) {
|
||||
// Handle corrupted PDF files
|
||||
console.error('Corrupted PDF detected:', error);
|
||||
if (window.stirlingPDF.currentPage !== 'repair') {
|
||||
const corruptedProblem = buildCorruptedPdfProblemDetail(file.name);
|
||||
this.showProblemDetail(corruptedProblem);
|
||||
} else {
|
||||
console.log('Suppressing corrupted PDF warning banner on repair page');
|
||||
}
|
||||
throw new Error('PDF file is corrupted.');
|
||||
}
|
||||
|
||||
console.error('Error checking encryption:', error);
|
||||
throw new Error('Failed to determine if the file is encrypted.');
|
||||
}
|
||||
}
|
||||
|
||||
showProblemDetail(problemDetail) {
|
||||
const errorContainer = document.getElementById('errorContainer');
|
||||
if (!errorContainer) {
|
||||
console.error('Error container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
errorContainer.style.display = 'block';
|
||||
|
||||
const heading = errorContainer.querySelector('.alert-heading');
|
||||
const messageEl = errorContainer.querySelector('p');
|
||||
const traceEl = document.querySelector('#traceContent');
|
||||
|
||||
const fallbackHeading = (window.stirlingPDF && window.stirlingPDF.error) || 'Error';
|
||||
|
||||
if (heading) {
|
||||
heading.textContent =
|
||||
(problemDetail && typeof problemDetail === 'object' && problemDetail.title) ||
|
||||
fallbackHeading;
|
||||
}
|
||||
|
||||
if (messageEl) {
|
||||
messageEl.style.whiteSpace = 'pre-wrap';
|
||||
messageEl.textContent =
|
||||
typeof problemDetail === 'object'
|
||||
? formatUserFriendlyError(problemDetail)
|
||||
: String(problemDetail || '');
|
||||
}
|
||||
|
||||
if (traceEl) {
|
||||
traceEl.textContent =
|
||||
typeof problemDetail === 'object' ? formatProblemDetailsJson(problemDetail) : '';
|
||||
}
|
||||
}
|
||||
|
||||
removeErrorBanner() {
|
||||
const errorContainer = document.getElementById('errorContainer');
|
||||
if (!errorContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
errorContainer.style.display = 'none';
|
||||
|
||||
const heading = errorContainer.querySelector('.alert-heading');
|
||||
if (heading) {
|
||||
heading.textContent = (window.stirlingPDF && window.stirlingPDF.error) || 'Error';
|
||||
}
|
||||
|
||||
const messageEl = errorContainer.querySelector('p');
|
||||
if (messageEl) {
|
||||
messageEl.textContent = '';
|
||||
}
|
||||
|
||||
const traceEl = document.querySelector('#traceContent');
|
||||
if (traceEl) {
|
||||
traceEl.textContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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
@@ -1,213 +0,0 @@
|
||||
importScripts('./diff.js');
|
||||
|
||||
let complexMessage = 'One or both of the provided documents are large files, accuracy of comparison may be reduced';
|
||||
let largeFilesMessage = 'One or Both of the provided documents are too large to process';
|
||||
|
||||
// Early: Listener for SET messages (before onmessage)
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'SET_COMPLEX_MESSAGE') {
|
||||
complexMessage = event.data.message;
|
||||
} else if (event.data.type === 'SET_TOO_LARGE_MESSAGE') {
|
||||
largeFilesMessage = event.data.message;
|
||||
}
|
||||
});
|
||||
|
||||
self.onmessage = async function (e) {
|
||||
const data = e.data;
|
||||
if (data.type !== 'COMPARE') {
|
||||
console.log('Worker ignored non-COMPARE message');
|
||||
return;
|
||||
}
|
||||
|
||||
const { text1, text2, color1, color2 } = data;
|
||||
console.log('Received text for comparison:', { lengths: { text1: text1.length, text2: text2.length } }); // Safe Log
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Safe Trim
|
||||
if (!text1 || !text2 || text1.trim() === "" || text2.trim() === "") {
|
||||
self.postMessage({ status: 'error', message: 'One or both of the texts are empty.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Robust Word-Split (handles spaces/punctuation better)
|
||||
const words1 = text1.trim().split(/\s+/).filter(w => w.length > 0);
|
||||
const words2 = text2.trim().split(/\s+/).filter(w => w.length > 0);
|
||||
|
||||
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;
|
||||
|
||||
if (isTooLarge) {
|
||||
self.postMessage({ status: 'error', message: largeFilesMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComplex) {
|
||||
self.postMessage({ status: 'warning', message: complexMessage });
|
||||
}
|
||||
|
||||
// Diff based on size
|
||||
let differences;
|
||||
if (isComplex) {
|
||||
differences = await staggeredBatchDiff(words1, words2, color1 || '#ff0000', color2 || '#008000', BATCH_SIZE, OVERLAP_SIZE);
|
||||
} else {
|
||||
differences = diff(words1, words2, color1 || '#ff0000', color2 || '#008000');
|
||||
}
|
||||
|
||||
console.log(`Diff took ${performance.now() - startTime} ms for ${words1.length + words2.length} words`);
|
||||
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
|
||||
|
||||
// Track processed indices to dedupe overlaps
|
||||
const processed1 = new Set();
|
||||
const processed2 = new Set();
|
||||
|
||||
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);
|
||||
|
||||
// Adaptive: If many diffs, smaller batch (max 3x downscale)
|
||||
const recentDiffs = differences.slice(-100).filter(([c]) => c !== 'black').length;
|
||||
// If difference is too high decrease batch size for more granular check
|
||||
const dynamicBatchSize = Math.max(batchSize / Math.min(8, 1 + recentDiffs / 50), batchSize / 8);
|
||||
|
||||
const extendedEnd1 = Math.min(end1 + dynamicBatchSize, totalWords1);
|
||||
const extendedEnd2 = Math.min(end2 + dynamicBatchSize, totalWords2);
|
||||
|
||||
const batchWords1 = words1.slice(start1, extendedEnd1);
|
||||
const batchWords2 = words2.slice(start2, extendedEnd2);
|
||||
|
||||
// Include overlap from the previous chunk
|
||||
const overlapStart1 = Math.max(0, previousEnd1 - overlapSize);
|
||||
const overlapStart2 = Math.max(0, previousEnd2 - overlapSize);
|
||||
const overlapWords1 = previousEnd1 > 0 ? words1.slice(overlapStart1, previousEnd1) : [];
|
||||
const overlapWords2 = previousEnd2 > 0 ? words2.slice(overlapStart2, previousEnd2) : [];
|
||||
|
||||
|
||||
// Combine overlaps and current batches for comparison
|
||||
const combinedWords1 = [...overlapWords1, ...batchWords1];
|
||||
const combinedWords2 = [...overlapWords2, ...batchWords2];
|
||||
|
||||
// Perform the diff on the combined words
|
||||
const batchDifferences = diff(combinedWords1, combinedWords2, color1, color2);
|
||||
|
||||
const combinedIndices1 = [];
|
||||
for (let i = overlapStart1; i < previousEnd1; i++) {
|
||||
combinedIndices1.push(i);
|
||||
}
|
||||
for (let i = start1; i < extendedEnd1; i++) {
|
||||
combinedIndices1.push(i);
|
||||
}
|
||||
|
||||
const combinedIndices2 = [];
|
||||
for (let i = overlapStart2; i < previousEnd2; i++) {
|
||||
combinedIndices2.push(i);
|
||||
}
|
||||
for (let i = start2; i < extendedEnd2; i++) {
|
||||
combinedIndices2.push(i);
|
||||
}
|
||||
|
||||
let pointer1 = 0;
|
||||
let pointer2 = 0;
|
||||
|
||||
const filteredBatch = [];
|
||||
batchDifferences.forEach(([color, word]) => {
|
||||
if (color === color1) {
|
||||
const globalIndex1 = combinedIndices1[pointer1];
|
||||
if (globalIndex1 === undefined || !processed1.has(globalIndex1)) {
|
||||
filteredBatch.push([color, word]);
|
||||
}
|
||||
if (globalIndex1 !== undefined) {
|
||||
processed1.add(globalIndex1);
|
||||
}
|
||||
pointer1++;
|
||||
} else if (color === color2) {
|
||||
const globalIndex2 = combinedIndices2[pointer2];
|
||||
if (globalIndex2 === undefined || !processed2.has(globalIndex2)) {
|
||||
filteredBatch.push([color, word]);
|
||||
}
|
||||
if (globalIndex2 !== undefined) {
|
||||
processed2.add(globalIndex2);
|
||||
}
|
||||
pointer2++;
|
||||
} else {
|
||||
const globalIndex1 = combinedIndices1[pointer1];
|
||||
const globalIndex2 = combinedIndices2[pointer2];
|
||||
const alreadyProcessed = (globalIndex1 !== undefined && processed1.has(globalIndex1)) && (globalIndex2 !== undefined && processed2.has(globalIndex2));
|
||||
if (!alreadyProcessed) {
|
||||
filteredBatch.push([color, word]);
|
||||
}
|
||||
if (globalIndex1 !== undefined) {
|
||||
processed1.add(globalIndex1);
|
||||
}
|
||||
if (globalIndex2 !== undefined) {
|
||||
processed2.add(globalIndex2);
|
||||
}
|
||||
pointer1++;
|
||||
pointer2++;
|
||||
}
|
||||
});
|
||||
|
||||
differences.push(...filteredBatch);
|
||||
|
||||
// Mark as processed
|
||||
for (let k = start1; k < end1; k++) processed1.add(k);
|
||||
for (let k = start2; k < end2; k++) processed2.add(k);
|
||||
|
||||
previousEnd1 = end1;
|
||||
previousEnd2 = end2;
|
||||
|
||||
// Yield for async (avoids blocking)
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
return differences;
|
||||
}
|
||||
|
||||
// Standard diff function for small text comparisons
|
||||
function diff(words1, words2, color1, color2) {
|
||||
console.log(`Diff: ${words1.length} vs ${words2.length} words`);
|
||||
const oldStr = words1.join(' '); // As string for diff.js
|
||||
const newStr = words2.join(' ');
|
||||
// Static method: No 'new' needed, avoids constructor error
|
||||
const changes = Diff.diffWords(oldStr, newStr, { ignoreWhitespace: true });
|
||||
|
||||
// Map changes to [color, word] format (change.value and added/removed)
|
||||
const differences = [];
|
||||
changes.forEach(change => {
|
||||
const value = change.value;
|
||||
const op = change.added ? 1 : change.removed ? -1 : 0;
|
||||
|
||||
// Split value into words and process
|
||||
const words = value.split(/\s+/).filter(w => w.length > 0);
|
||||
words.forEach(word => {
|
||||
if (op === 0) { // Equal
|
||||
differences.push(['black', word]);
|
||||
} else if (op === 1) { // Insert
|
||||
differences.push([color2, word]);
|
||||
} else if (op === -1) { // Delete
|
||||
differences.push([color1, word]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return differences;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,791 +0,0 @@
|
||||
(function () {
|
||||
if (window.isDownloadScriptInitialized) return; // Prevent re-execution
|
||||
window.isDownloadScriptInitialized = true;
|
||||
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
// Global PDF processing count tracking for survey system
|
||||
window.incrementPdfProcessingCount = function() {
|
||||
let pdfProcessingCount = parseInt(localStorage.getItem('pdfProcessingCount') || '0');
|
||||
pdfProcessingCount++;
|
||||
localStorage.setItem('pdfProcessingCount', pdfProcessingCount.toString());
|
||||
};
|
||||
|
||||
const {
|
||||
pdfPasswordPrompt,
|
||||
multipleInputsForSingleRequest,
|
||||
disableMultipleFiles,
|
||||
remoteCall,
|
||||
sessionExpired,
|
||||
refreshPage,
|
||||
error,
|
||||
} = window.stirlingPDF;
|
||||
|
||||
// Format Problem Details JSON with consistent key order and pretty-printing
|
||||
function formatProblemDetailsJson(input) {
|
||||
try {
|
||||
const obj = typeof input === 'string' ? JSON.parse(input) : input;
|
||||
const preferredOrder = [
|
||||
'errorCode',
|
||||
'title',
|
||||
'status',
|
||||
'type',
|
||||
'detail',
|
||||
'instance',
|
||||
'path',
|
||||
'timestamp',
|
||||
'hints',
|
||||
'actionRequired'
|
||||
];
|
||||
|
||||
const out = {};
|
||||
// Place preferred keys first if present
|
||||
preferredOrder.forEach((k) => {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, k)) {
|
||||
out[k] = obj[k];
|
||||
}
|
||||
});
|
||||
// Append remaining keys preserving their original order
|
||||
Object.keys(obj).forEach((k) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(out, k)) {
|
||||
out[k] = obj[k];
|
||||
}
|
||||
});
|
||||
return JSON.stringify(out, null, 2);
|
||||
} catch (e) {
|
||||
// Fallback: if it's already a string, return as-is; otherwise pretty-print best effort
|
||||
if (typeof input === 'string') return input;
|
||||
try {
|
||||
return JSON.stringify(input, null, 2);
|
||||
} catch {
|
||||
return String(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showErrorBanner(message, stackTrace) {
|
||||
const errorContainer = document.getElementById('errorContainer');
|
||||
if (!errorContainer) {
|
||||
console.error('Error container not found');
|
||||
return;
|
||||
}
|
||||
errorContainer.style.display = 'block'; // Display the banner
|
||||
const heading = errorContainer.querySelector('.alert-heading');
|
||||
const messageEl = errorContainer.querySelector('p');
|
||||
const traceEl = document.querySelector('#traceContent');
|
||||
|
||||
if (heading) heading.textContent = error;
|
||||
if (messageEl) {
|
||||
messageEl.style.whiteSpace = 'pre-wrap';
|
||||
messageEl.textContent = message;
|
||||
}
|
||||
|
||||
// Format stack trace: if it looks like JSON, pretty-print with consistent key order; otherwise clean it up
|
||||
if (traceEl) {
|
||||
if (stackTrace) {
|
||||
// Check if stackTrace is already JSON formatted
|
||||
if (stackTrace.trim().startsWith('{') || stackTrace.trim().startsWith('[')) {
|
||||
traceEl.textContent = formatProblemDetailsJson(stackTrace);
|
||||
} else {
|
||||
// Filter out unhelpful stack traces (internal browser/library paths)
|
||||
// Only show if it contains meaningful error info
|
||||
const lines = stackTrace.split('\n');
|
||||
const meaningfulLines = lines.filter(line =>
|
||||
!line.includes('pdfjs-legacy') &&
|
||||
!line.includes('pdf.worker') &&
|
||||
!line.includes('pdf.mjs') &&
|
||||
line.trim().length > 0
|
||||
);
|
||||
traceEl.textContent = meaningfulLines.length > 0 ? meaningfulLines.join('\n') : 'No additional trace information available';
|
||||
}
|
||||
} else {
|
||||
traceEl.textContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
// Log fileOrder for debugging
|
||||
const fileOrderValue = formData.get('fileOrder');
|
||||
if (fileOrderValue) {
|
||||
console.log('FormData fileOrder:', fileOrderValue);
|
||||
}
|
||||
|
||||
// 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 = pdfjsPath + 'pdf.worker.mjs';
|
||||
const pdf = await pdfjsLib
|
||||
.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
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 = pdfjsPath + '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({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
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.decrypt.passwordPrompt}`);
|
||||
|
||||
if (!password) {
|
||||
console.error(`No password provided for encrypted PDF: ${file.name}`);
|
||||
|
||||
// Create a Problem Detail object matching the server's E004 response using localized strings
|
||||
const passwordDetailTemplate =
|
||||
window.stirlingPDF?.pdfPasswordDetail ||
|
||||
`The PDF file "${file.name}" requires a password to proceed.`;
|
||||
const hints = [
|
||||
window.stirlingPDF?.pdfPasswordHint1,
|
||||
window.stirlingPDF?.pdfPasswordHint2,
|
||||
window.stirlingPDF?.pdfPasswordHint3,
|
||||
window.stirlingPDF?.pdfPasswordHint4,
|
||||
window.stirlingPDF?.pdfPasswordHint5,
|
||||
window.stirlingPDF?.pdfPasswordHint6
|
||||
].filter(Boolean);
|
||||
const noProblemDetail = {
|
||||
errorCode: 'E004',
|
||||
title: window.stirlingPDF?.pdfPasswordTitle || 'PDF Password Required',
|
||||
detail: passwordDetailTemplate.includes('{0}')
|
||||
? passwordDetailTemplate.replace('{0}', file.name)
|
||||
: passwordDetailTemplate,
|
||||
hints,
|
||||
actionRequired:
|
||||
window.stirlingPDF?.pdfPasswordAction ||
|
||||
'Provide the owner/permissions password, not just the document open password.'
|
||||
};
|
||||
|
||||
const bannerMessage = formatUserFriendlyError(noProblemDetail);
|
||||
const debugInfo = formatProblemDetailsJson(noProblemDetail);
|
||||
showErrorBanner(bannerMessage, debugInfo);
|
||||
|
||||
const err = new Error(noProblemDetail.detail);
|
||||
err.alreadyHandled = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
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 fetchWithCsrf(removePasswordUrl, {method: 'POST', body: formData});
|
||||
|
||||
// Check if we got an error response (RFC 7807 Problem Details)
|
||||
if (!decryptionResult.ok) {
|
||||
const contentType = decryptionResult.headers.get('content-type');
|
||||
if (contentType && (contentType.includes('application/json') || contentType.includes('application/problem+json'))) {
|
||||
// Parse the RFC 7807 error response
|
||||
const errorJson = await decryptionResult.json();
|
||||
const formattedError = formatUserFriendlyError(errorJson);
|
||||
const debugInfo = formatProblemDetailsJson(errorJson);
|
||||
const title = errorJson.title || 'Decryption Failed';
|
||||
const detail = errorJson.detail || 'Failed to decrypt PDF';
|
||||
const bannerMessage = formattedError || `${title}: ${detail}`;
|
||||
showErrorBanner(bannerMessage, debugInfo);
|
||||
const err = new Error(detail);
|
||||
err.alreadyHandled = true; // Mark error as already handled
|
||||
throw err;
|
||||
} else {
|
||||
throw new Error('Decryption failed: Invalid server response');
|
||||
}
|
||||
}
|
||||
|
||||
if (decryptionResult && decryptionResult.blob) {
|
||||
const decryptedBlob = await decryptionResult.blob();
|
||||
const decryptedFile = new File([decryptedBlob], file.name, {type: 'application/pdf'});
|
||||
|
||||
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);
|
||||
// Error banner already shown above with formatted hints/actions
|
||||
throw decryptError;
|
||||
}
|
||||
} else if (error.name === 'InvalidPDFException' ||
|
||||
(error.message && error.message.includes('Invalid PDF structure'))) {
|
||||
// Handle corrupted PDF files
|
||||
console.log(`Corrupted PDF detected: ${file.name}`, error);
|
||||
if (window.stirlingPDF.currentPage !== 'repair') {
|
||||
// Create a formatted error message using properties from language files
|
||||
const errorMessage = window.stirlingPDF.pdfCorruptedMessage.replace('{0}', file.name);
|
||||
const hints = [
|
||||
window.stirlingPDF.pdfCorruptedHint1,
|
||||
window.stirlingPDF.pdfCorruptedHint2,
|
||||
window.stirlingPDF.pdfCorruptedHint3
|
||||
].filter((hint) => typeof hint === 'string' && hint.trim().length > 0);
|
||||
const action = window.stirlingPDF.pdfCorruptedAction || window.stirlingPDF.tryRepairMessage;
|
||||
|
||||
const problemDetails = {
|
||||
title: window.stirlingPDF.pdfCorruptedTitle || window.stirlingPDF.error || 'Error',
|
||||
detail: errorMessage
|
||||
};
|
||||
|
||||
if (hints.length > 0) {
|
||||
problemDetails.hints = hints;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
problemDetails.actionRequired = action;
|
||||
}
|
||||
|
||||
const bannerMessage = formatUserFriendlyError(problemDetails);
|
||||
const debugInfo = formatProblemDetailsJson(problemDetails);
|
||||
|
||||
showErrorBanner(bannerMessage, debugInfo);
|
||||
// Mark error as already handled to prevent double display
|
||||
error.alreadyHandled = true;
|
||||
} else {
|
||||
// On repair page, suppress banner; user already knows and is repairing
|
||||
console.log('Suppressing corrupted PDF banner on repair page');
|
||||
}
|
||||
throw error;
|
||||
} 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;
|
||||
// Check for JSON error responses first (including RFC 7807 Problem Details)
|
||||
if (contentType && (contentType.includes('application/json') || contentType.includes('application/problem+json'))) {
|
||||
console.error('Throwing error banner, response was not okay');
|
||||
await handleJsonResponse(response);
|
||||
// Return early - error banner already shown by handleJsonResponse
|
||||
// Don't throw to avoid double error display
|
||||
return null;
|
||||
}
|
||||
// Only show session expired for 401 without JSON body (actual auth failure)
|
||||
if (response.status === 401) {
|
||||
showSessionExpiredPrompt();
|
||||
return;
|
||||
}
|
||||
// For non-JSON errors, try to extract error message from response body
|
||||
try {
|
||||
const errorText = await response.text();
|
||||
if (errorText && errorText.trim().length > 0) {
|
||||
showErrorBanner(`HTTP ${response.status}`, errorText);
|
||||
// Return early - error already shown
|
||||
return null;
|
||||
}
|
||||
} catch (textError) {
|
||||
// If we can't read the response body, show generic error
|
||||
const errorMsg = `HTTP ${response.status} - ${response.statusText || 'Request failed'}`;
|
||||
showErrorBanner('Error', errorMsg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// Increment PDF processing count for survey tracking
|
||||
if (success && typeof window.incrementPdfProcessingCount === 'function') {
|
||||
window.incrementPdfProcessingCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format error details in a user-friendly way
|
||||
* Extracts key information and presents hints/actions prominently
|
||||
*/
|
||||
function formatUserFriendlyError(json) {
|
||||
if (!json || typeof json !== 'object') {
|
||||
return typeof json === 'string' ? json : '';
|
||||
}
|
||||
|
||||
const lines = [];
|
||||
const title = json.title || json.error || '';
|
||||
const detail = json.detail || json.message || '';
|
||||
|
||||
const primaryLine = title && detail
|
||||
? `${title}: ${detail}`
|
||||
: title || detail;
|
||||
|
||||
if (primaryLine) {
|
||||
lines.push(primaryLine);
|
||||
}
|
||||
|
||||
if (json.errorCode) {
|
||||
lines.push('');
|
||||
lines.push(`Error Code: ${json.errorCode}`);
|
||||
}
|
||||
|
||||
const detailAlreadyIncluded = detail && primaryLine && primaryLine.includes(detail);
|
||||
|
||||
if (detail && !detailAlreadyIncluded) {
|
||||
lines.push('');
|
||||
lines.push(detail);
|
||||
}
|
||||
|
||||
if (json.hints && Array.isArray(json.hints) && json.hints.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('How to fix:');
|
||||
json.hints.forEach((hint, index) => {
|
||||
lines.push(` ${index + 1}. ${hint}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (json.actionRequired) {
|
||||
lines.push('');
|
||||
lines.push(json.actionRequired);
|
||||
}
|
||||
|
||||
if (json.supportId) {
|
||||
lines.push('');
|
||||
lines.push(`Support ID: ${json.supportId}`);
|
||||
}
|
||||
|
||||
return lines
|
||||
.filter((line, index, arr) => {
|
||||
if (line !== '') return true;
|
||||
if (index === 0 || index === arr.length - 1) return false;
|
||||
return arr[index - 1] !== '';
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
async function handleJsonResponse(response) {
|
||||
const json = await response.json();
|
||||
|
||||
// Format the full JSON response for display in stack trace with errorCode first
|
||||
const formattedJson = formatProblemDetailsJson(json);
|
||||
|
||||
// Check for PDF password errors using RFC 7807 fields
|
||||
const isPdfPasswordError =
|
||||
json.type === '/errors/pdf-password' ||
|
||||
json.errorCode === 'E004' ||
|
||||
(json.detail && (
|
||||
json.detail.toLowerCase().includes('pdf document is passworded') ||
|
||||
json.detail.toLowerCase().includes('password is incorrect') ||
|
||||
json.detail.toLowerCase().includes('password was not provided') ||
|
||||
json.detail.toLowerCase().includes('pdf contains an encryption dictionary')
|
||||
));
|
||||
|
||||
const fallbackTitle = json.title || json.error || 'Error';
|
||||
const fallbackDetail = json.detail || json.message || '';
|
||||
const fallbackMessage = fallbackDetail ? `${fallbackTitle}: ${fallbackDetail}` : fallbackTitle;
|
||||
const bannerMessage = formatUserFriendlyError(json) || fallbackMessage;
|
||||
|
||||
if (isPdfPasswordError) {
|
||||
showErrorBanner(bannerMessage, formattedJson);
|
||||
|
||||
// Show alert only once for user attention
|
||||
if (!firstErrorOccurred) {
|
||||
firstErrorOccurred = true;
|
||||
const detail = json.detail || 'The PDF document requires a password to open.';
|
||||
alert(pdfPasswordPrompt + '\n\n' + detail);
|
||||
}
|
||||
} else {
|
||||
// Show user-friendly error, fallback to full JSON for debugging
|
||||
showErrorBanner(bannerMessage, formattedJson);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Skip if error was already handled and displayed
|
||||
if (error.alreadyHandled) {
|
||||
return;
|
||||
}
|
||||
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 downloadDetails is null, error was already shown, skip processing
|
||||
if (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);
|
||||
setTimeout(() => {
|
||||
progressBar.closest('.progressBarContainer').hide();
|
||||
progressBar.css('width', '0%');
|
||||
progressBar.attr('aria-valuenow', 0);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
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'");
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -1,631 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
var traceVisible = false;
|
||||
|
||||
function toggletrace() {
|
||||
var traceDiv = document.getElementById("trace");
|
||||
if (!traceVisible) {
|
||||
traceDiv.style.maxHeight = "100vh";
|
||||
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");
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
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 divElement = navbarEntry.querySelector('div');
|
||||
if (divElement) {
|
||||
var originalContent = divElement.cloneNode(true);
|
||||
contentWrapper.appendChild(originalContent);
|
||||
} else {
|
||||
// Fallback: create content manually if div is not found
|
||||
var fallbackContent = document.createElement('div');
|
||||
fallbackContent.innerHTML = navbarEntry.innerHTML;
|
||||
contentWrapper.appendChild(fallbackContent);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Authentication utility for cookie-based JWT
|
||||
window.JWTManager = {
|
||||
|
||||
// Logout - clear cookies and redirect to login
|
||||
logout: function() {
|
||||
|
||||
// Clear JWT cookie manually (fallback)
|
||||
document.cookie = 'stirling_jwt=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=None; Secure';
|
||||
|
||||
// Perform logout request to clear server-side session
|
||||
fetch('/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
}).then(response => {
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
} else {
|
||||
window.location.href = '/login?logout=true';
|
||||
}
|
||||
}).catch(() => {
|
||||
// If logout fails, let server handle it
|
||||
window.location.href = '/logout';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Always include credentials to send JWT cookies
|
||||
fetchOptions.credentials = 'include';
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
// Handle 401 responses (unauthorized)
|
||||
if (response.status === 401) {
|
||||
console.warn('Authentication failed, redirecting to login');
|
||||
window.JWTManager.logout();
|
||||
return response;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
class FileIconFactory {
|
||||
static createFileIcon(fileExtension) {
|
||||
let ext = fileExtension.toLowerCase();
|
||||
switch (ext) {
|
||||
case "pdf":
|
||||
return this.createPDFIcon();
|
||||
case "csv":
|
||||
return this.createCSVIcon();
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
return this.createXLSXIcon();
|
||||
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 createCSVIcon() {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-filetype-csv" 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.5zM3.517 14.841a1.13 1.13 0 0 0 .401.823q.195.162.478.252.284.091.665.091.507 0 .859-.158.354-.158.539-.54.185-.382.185-.816 0-.335-.123-.628a1.4 1.4 0 0 0-.366-.486 1.8 1.8 0 0 0-.614-.314 2.8 2.8 0 0 0-.865-.118 2.1 2.1 0 0 0-.614.094 1.4 1.4 0 0 0-.471.264 1.1 1.1 0 0 0-.298.429.9.9 0 0 0-.103.539h.606a.4.4 0 0 1 .096-.258.5.5 0 0 1 .213-.164.6.6 0 0 1 .33-.082.7.7 0 0 1 .458.132.4.4 0 0 1 .153.372.4.4 0 0 1-.085.235.7.7 0 0 1-.25.192 1.4 1.4 0 0 1-.407.115c-.127.023-.266.05-.416.081a1.8 1.8 0 0 0-.534.187 1.2 1.2 0 0 0-.382.346 1 1 0 0 0-.138.537q0 .295.101.517M8.717 14.841a1.13 1.13 0 0 0 .401.823q.195.162.478.252.284.091.665.091.507 0 .859-.158.354-.158.539-.54.185-.382.185-.816 0-.335-.123-.628a1.4 1.4 0 0 0-.366-.486 1.8 1.8 0 0 0-.614-.314 2.8 2.8 0 0 0-.865-.118 2.1 2.1 0 0 0-.614.094 1.4 1.4 0 0 0-.471.264 1.1 1.1 0 0 0-.298.429.9.9 0 0 0-.103.539h.606a.4.4 0 0 1 .096-.258.5.5 0 0 1 .213-.164.6.6 0 0 1 .33-.082.7.7 0 0 1 .458.132.4.4 0 0 1 .153.372.4.4 0 0 1-.085.235.7.7 0 0 1-.25.192 1.4 1.4 0 0 1-.407.115c-.127.023-.266.05-.416.081a1.8 1.8 0 0 0-.534.187 1.2 1.2 0 0 0-.382.346 1 1 0 0 0-.138.537q0 .295.101.517M14.229 13.12v.506H11.85v-.506h1.063v-1.277H11.85v-.506h2.379v.506h-1.063z"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static createXLSXIcon() {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark-excel" viewBox="0 0 16 16">
|
||||
<path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.116 2.54a.5.5 0 0 0 .768-.641L8.651 10l2.233-2.68a.5.5 0 0 0-.768-.64L8 9.219l-2.116-2.54z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.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.5z"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static createUnknownFileIcon() {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark" viewBox="0 0 16 16">
|
||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileIconFactory;
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
@@ -1,625 +0,0 @@
|
||||
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",
|
||||
};
|
||||
|
||||
const isMultiToolPage = () => window.location.pathname?.includes('multi-tool');
|
||||
|
||||
const isSvgFile = (file) => {
|
||||
if (!file) return false;
|
||||
const type = (file.type || '').toLowerCase();
|
||||
if (type === 'image/svg+xml') {
|
||||
return true;
|
||||
}
|
||||
const name = (file.name || '').toLowerCase();
|
||||
return name.endsWith('.svg');
|
||||
};
|
||||
|
||||
function filterSvgFiles(files) {
|
||||
if (!Array.isArray(files) || !isMultiToolPage()) {
|
||||
return { allowed: files ?? [], rejected: [] };
|
||||
}
|
||||
|
||||
const allowed = [];
|
||||
const rejected = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
if (isSvgFile(file)) {
|
||||
rejected.push(file);
|
||||
} else {
|
||||
allowed.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
return { allowed, rejected };
|
||||
}
|
||||
|
||||
function showSvgWarning(rejectedFiles = []) {
|
||||
if (!rejectedFiles.length) return;
|
||||
|
||||
const message = window.multiTool?.svgNotSupported ||
|
||||
'SVG files are not supported in Multi Tool and were ignored.';
|
||||
const rejectedNames = rejectedFiles
|
||||
.map((file) => file?.name)
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
if (rejectedNames) {
|
||||
alert(`${message}\n${rejectedNames}`);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
} else if (inputContainer.id === 'attachments-input-container') {
|
||||
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.addAttachments;
|
||||
}
|
||||
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 { allowed: nonSvgFiles, rejected: rejectedSvgFiles } = filterSvgFiles(allFiles);
|
||||
if (rejectedSvgFiles.length > 0) {
|
||||
showSvgWarning(rejectedSvgFiles);
|
||||
allFiles = nonSvgFiles;
|
||||
|
||||
const updatedTransfer = toDataTransfer(allFiles);
|
||||
element.files = updatedTransfer.files;
|
||||
if (allFiles.length === 0) {
|
||||
element.value = '';
|
||||
}
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
inputContainer.querySelector('#fileInputText').innerHTML = originalText;
|
||||
showOrHideSelectedFilesContainer(allFiles);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 &&
|
||||
!window.location.pathname.includes('remove-password')) {
|
||||
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);
|
||||
|
||||
// Check if this is a PDF corruption error
|
||||
if (error.message && error.message.includes('PDF file is corrupted')) {
|
||||
// The error banner is already shown by DecryptFiles.js, just continue with the file
|
||||
console.warn(`Continuing with corrupted PDF file: ${file.name}`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
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;
|
||||
@@ -1,390 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
function getDownloadUrl() {
|
||||
// Only show download for non-Docker installations
|
||||
if (machineType === 'Docker' || machineType === 'Kubernetes') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseUrl = 'https://files.stirlingpdf.com/';
|
||||
|
||||
// Determine file based on machine type and security
|
||||
if (machineType === 'Server-jar') {
|
||||
return baseUrl + (activeSecurity ? 'Stirling-PDF-with-login.jar' : 'Stirling-PDF.jar');
|
||||
}
|
||||
|
||||
// Client installations
|
||||
if (machineType.startsWith('Client-')) {
|
||||
const os = machineType.replace('Client-', ''); // win, mac, unix
|
||||
const type = activeSecurity ? '-server-security' : '-server';
|
||||
|
||||
if (os === 'unix') {
|
||||
return baseUrl + os + type + '.jar';
|
||||
} else if (os === 'win') {
|
||||
return baseUrl + os + '-installer.exe';
|
||||
} else if (os === 'mac') {
|
||||
return baseUrl + os + '-installer.dmg';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function to get translated priority text
|
||||
function getTranslatedPriority(priority) {
|
||||
switch(priority?.toLowerCase()) {
|
||||
case 'urgent': return updatePriorityUrgent;
|
||||
case 'normal': return updatePriorityNormal;
|
||||
case 'minor': return updatePriorityMinor;
|
||||
case 'low': return updatePriorityLow;
|
||||
default: return priority?.toUpperCase() || 'NORMAL';
|
||||
}
|
||||
}
|
||||
|
||||
async function getUpdateSummary() {
|
||||
// Map Java License enum to API types
|
||||
let type = 'normal';
|
||||
if (licenseType === 'PRO') {
|
||||
type = 'pro';
|
||||
} else if (licenseType === 'ENTERPRISE') {
|
||||
type = 'enterprise';
|
||||
}
|
||||
const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=true`;
|
||||
console.log("Fetching update summary from:", url);
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
console.log("Response status:", response.status);
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} else {
|
||||
console.error("Failed to fetch update summary from Supabase:", response.status);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch update summary from Supabase:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getFullUpdateInfo() {
|
||||
// Map Java License enum to API types
|
||||
let type = 'normal';
|
||||
if (licenseType === 'PRO') {
|
||||
type = 'pro';
|
||||
} else if (licenseType === 'ENTERPRISE') {
|
||||
type = 'enterprise';
|
||||
}
|
||||
const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=false`;
|
||||
console.log("Fetching full update info from:", url);
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
console.log("Full update response status:", response.status);
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} else {
|
||||
console.error("Failed to fetch full update info from Supabase:", response.status);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch full update info from Supabase:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
updateBtn.classList.remove("btn-danger", "btn-warning", "btn-outline-primary");
|
||||
}
|
||||
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 updateSummary = await getUpdateSummary();
|
||||
if (!updateSummary) {
|
||||
console.log("No update summary available");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("updateSummary=", updateSummary);
|
||||
console.log("currentVersion=" + currentVersion);
|
||||
console.log("latestVersion=" + updateSummary.latest_version);
|
||||
|
||||
if (updateSummary.latest_version && compareVersions(updateSummary.latest_version, currentVersion) > 0) {
|
||||
const priority = updateSummary.max_priority || 'normal';
|
||||
|
||||
if (updateBtn != null) {
|
||||
// Style button based on priority
|
||||
if (priority === 'urgent') {
|
||||
updateBtn.classList.add("btn-danger");
|
||||
updateBtn.innerHTML = urgentUpdateAvailable;
|
||||
} else if (priority === 'normal') {
|
||||
updateBtn.classList.add("btn-warning");
|
||||
updateBtn.innerHTML = updateAvailableText;
|
||||
} else {
|
||||
updateBtn.classList.add("btn-outline-primary");
|
||||
updateBtn.innerHTML = updateAvailableText;
|
||||
}
|
||||
|
||||
// Store summary for initial display
|
||||
updateBtn.setAttribute('data-update-summary', JSON.stringify(updateSummary));
|
||||
updateBtn.style.display = "block";
|
||||
|
||||
// Add click handler for update details modal
|
||||
updateBtn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
showUpdateModal();
|
||||
};
|
||||
}
|
||||
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>' + updateSummary.latest_version + '</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");
|
||||
}
|
||||
}
|
||||
|
||||
async function showUpdateModal() {
|
||||
// Close settings modal if open
|
||||
const settingsModal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
|
||||
if (settingsModal) {
|
||||
settingsModal.hide();
|
||||
}
|
||||
|
||||
// Get summary data from button
|
||||
const updateBtn = document.getElementById("update-btn");
|
||||
const summaryData = JSON.parse(updateBtn.getAttribute('data-update-summary'));
|
||||
|
||||
// Utility function to escape HTML special characters
|
||||
function escapeHtml(str) {
|
||||
if (typeof str !== 'string') return str;
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/');
|
||||
}
|
||||
|
||||
// Create initial modal with loading state
|
||||
const initialModalHtml = `
|
||||
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" role="document" style="max-height: 80vh;">
|
||||
<div class="modal-content" style="max-height: 80vh;">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="updateModalLabel">${updateModalTitle}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="updateModalBody" style="max-height: 60vh; overflow-y: auto;">
|
||||
<div class="update-summary mb-4">
|
||||
<div class="row mb-3">
|
||||
<div class="${summaryData.latest_stable_version ? 'col-4' : 'col-6'} text-center">
|
||||
<small class="text-muted">${updateCurrent}</small><br>
|
||||
<strong>${escapeHtml(currentVersion)}</strong>
|
||||
</div>
|
||||
<div class="${summaryData.latest_stable_version ? 'col-4' : 'col-6'} text-center">
|
||||
<small class="text-muted">${updateLatest}</small><br>
|
||||
<strong class="text-primary">${escapeHtml(summaryData.latest_version)}</strong>
|
||||
</div>
|
||||
${summaryData.latest_stable_version ? `
|
||||
<div class="col-4 text-center">
|
||||
<small class="text-muted">${updateLatestStable}</small><br>
|
||||
<strong class="text-success">${escapeHtml(summaryData.latest_stable_version)}</strong>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="alert ${summaryData.max_priority === 'urgent' ? 'alert-danger' : 'alert-warning'}" role="alert">
|
||||
<strong>${updatePriority}:</strong> ${getTranslatedPriority(summaryData.max_priority)}
|
||||
${summaryData.recommended_action ? `<br><strong>${updateRecommendedAction}:</strong> ${escapeHtml(summaryData.recommended_action)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${summaryData.any_breaking ? `
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6><strong>${updateBreakingChangesDetected}</strong></h6>
|
||||
<p>${updateBreakingChangesMessage}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${summaryData.migration_guides && summaryData.migration_guides.length > 0 ? `
|
||||
<div class="migration-guides mb-4">
|
||||
<h6>${updateMigrationGuides}</h6>
|
||||
<ul class="list-group">
|
||||
${summaryData.migration_guides.map(guide => `
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>${updateVersion} ${escapeHtml(guide.version)}:</strong> ${escapeHtml(guide.notes)}
|
||||
</div>
|
||||
<a href="${escapeHtml(guide.url)}" target="_blank" class="btn btn-sm btn-outline-primary">${updateViewGuide}</a>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status" id="loadingSpinner">
|
||||
<span class="visually-hidden">${updateLoadingDetailedInfo}</span>
|
||||
</div>
|
||||
<p class="mt-2">${updateLoadingDetailedInfo}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${updateClose}</button>
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank" class="btn btn-outline-primary">${updateViewAllReleases}</a>
|
||||
${getDownloadUrl() ? `<a href="${escapeHtml(getDownloadUrl())}" class="btn btn-success" target="_blank">${updateDownloadLatest}</a>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing modal if present
|
||||
const existingModal = document.getElementById('updateModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// Add modal to body
|
||||
document.body.insertAdjacentHTML('beforeend', initialModalHtml);
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('updateModal'));
|
||||
modal.show();
|
||||
|
||||
// Fetch full update info
|
||||
const fullUpdateInfo = await getFullUpdateInfo();
|
||||
|
||||
// Update modal with full information
|
||||
const modalBody = document.getElementById('updateModalBody');
|
||||
if (fullUpdateInfo && fullUpdateInfo.new_versions) {
|
||||
const storedMode = localStorage.getItem("dark-mode");
|
||||
const isDarkMode = storedMode === "on" ||
|
||||
(storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
const darkClasses = isDarkMode ? {
|
||||
accordionItem: 'bg-dark border-secondary text-light',
|
||||
accordionButton: 'bg-dark text-light border-secondary',
|
||||
accordionBody: 'bg-dark text-light'
|
||||
} : {
|
||||
accordionItem: '',
|
||||
accordionButton: '',
|
||||
accordionBody: ''
|
||||
};
|
||||
|
||||
const detailedVersionsHtml = `
|
||||
<div class="detailed-versions mt-4">
|
||||
<h6>${updateAvailableUpdates}</h6>
|
||||
<div class="accordion" id="versionsAccordion">
|
||||
${fullUpdateInfo.new_versions.map((version, index) => `
|
||||
<div class="accordion-item" style="border-color: var(--md-sys-color-outline);">
|
||||
<h2 class="accordion-header" id="heading${index}">
|
||||
<button class="accordion-button ${index === 0 ? '' : 'collapsed'}" style="color: var(--md-sys-color-on-surface); background-color:
|
||||
var(--md-sys-color-surface);" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse${index}" aria-expanded="${index === 0 ? 'true' : 'false'}" aria-controls="collapse${index}">
|
||||
<div class="d-flex justify-content-between w-100 me-3">
|
||||
<span><strong>${updateVersion} ${version.version}</strong></span>
|
||||
<span class="badge ${version.priority === 'urgent' ? 'bg-danger' : version.priority === 'normal' ? 'bg-warning' : 'bg-secondary'}">${getTranslatedPriority(version.priority)}</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse${index}" class="accordion-collapse collapse ${index === 0 ? 'show' : ''}"
|
||||
aria-labelledby="heading${index}" data-bs-parent="#versionsAccordion">
|
||||
<div class="accordion-body" style="color: var(--md-sys-color-on-surface); background-color:
|
||||
var(--md-sys-color-surface-bright);">
|
||||
<h6>${version.announcement.title}</h6>
|
||||
<p>${version.announcement.message}</p>
|
||||
${version.compatibility.breaking_changes ? `
|
||||
<div class="alert alert-warning alert-sm" role="alert">
|
||||
<small><strong>⚠️ ${updateBreakingChanges}</strong> ${version.compatibility.breaking_description || updateBreakingChangesDefault}</small>
|
||||
${version.compatibility.migration_guide_url ? `<br><a href="${version.compatibility.migration_guide_url}" target="_blank" class="btn btn-sm btn-outline-warning mt-1">${updateMigrationGuide}</a>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove loading spinner and add detailed info
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
if (spinner) {
|
||||
spinner.parentElement.remove();
|
||||
}
|
||||
modalBody.insertAdjacentHTML('beforeend', detailedVersionsHtml);
|
||||
|
||||
} else {
|
||||
// Remove loading spinner if failed to load
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
if (spinner) {
|
||||
spinner.parentElement.innerHTML = `<p class="text-muted">${updateUnableToLoadDetails}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
checkForUpdate();
|
||||
});
|
||||
@@ -1,158 +0,0 @@
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,249 +0,0 @@
|
||||
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) {
|
||||
const errorText = await response.text().catch(() => '');
|
||||
const errorMsg = errorText || response.statusText || 'Request failed';
|
||||
throw new Error(`HTTP ${response.status}: ${errorMsg}`);
|
||||
}
|
||||
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) {
|
||||
const errorText = await response.text().catch(() => '');
|
||||
const errorMsg = errorText || response.statusText || 'Request failed';
|
||||
throw new Error(`HTTP ${response.status}: ${errorMsg}`);
|
||||
}
|
||||
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.');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
// JWT Authentication Management Script
|
||||
// This script handles cookie-based JWT authentication and page access control
|
||||
|
||||
(function() {
|
||||
// Clean up JWT token from URL parameters after OAuth/Login flows
|
||||
function cleanupTokenFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const hasToken = urlParams.get('jwt') || urlParams.get('token');
|
||||
if (hasToken) {
|
||||
// Clean up URL by removing token parameter
|
||||
// Token should now be set as cookie by server
|
||||
urlParams.delete('jwt');
|
||||
urlParams.delete('token');
|
||||
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize JWT handling when page loads
|
||||
function initializeJWT() {
|
||||
// Clean up any JWT tokens from URL (OAuth flow)
|
||||
cleanupTokenFromUrl();
|
||||
|
||||
// Authentication is handled server-side
|
||||
// If user is not authenticated, server will redirect to login
|
||||
console.log('JWT initialization complete - authentication handled server-side');
|
||||
}
|
||||
|
||||
// No form enhancement needed for cookie-based JWT
|
||||
// Cookies are automatically sent with form submissions
|
||||
function enhanceFormSubmissions() {
|
||||
// Cookie-based JWT is automatically included in form submissions
|
||||
// No additional processing needed
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeJWT();
|
||||
});
|
||||
} else {
|
||||
initializeJWT();
|
||||
}
|
||||
})();
|
||||
@@ -1,74 +0,0 @@
|
||||
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();
|
||||
@@ -1,47 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
let currentSort = {
|
||||
field: null,
|
||||
descending: false,
|
||||
};
|
||||
|
||||
document.getElementById("fileInput-input").addEventListener("file-input-change", function () {
|
||||
var files = this.files;
|
||||
displayFiles(files);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {FileList} files
|
||||
*/
|
||||
async function displayFiles(files) {
|
||||
const list = document.getElementById("selectedFiles");
|
||||
|
||||
while (list.firstChild) {
|
||||
list.removeChild(list.firstChild);
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const pageCount = await getPDFPageCount(files[i]);
|
||||
const pageLabel = pageCount === 1 ? pageTranslation : pagesTranslation;
|
||||
|
||||
// Create list item
|
||||
const item = document.createElement("li");
|
||||
item.className = "list-group-item";
|
||||
|
||||
// Create filename div and set textContent to sanitize
|
||||
const fileNameDiv = document.createElement("div");
|
||||
fileNameDiv.className = "filename";
|
||||
fileNameDiv.setAttribute("data-file-id", files[i].uniqueId);
|
||||
fileNameDiv.textContent = files[i].name;
|
||||
|
||||
// Create page info div and set textContent to sanitize
|
||||
const pageInfoDiv = document.createElement("div");
|
||||
pageInfoDiv.className = "page-info";
|
||||
const pageCountSpan = document.createElement("span");
|
||||
pageCountSpan.className = "page-count";
|
||||
pageCountSpan.textContent = `${pageCount} ${pageLabel}`;
|
||||
pageInfoDiv.appendChild(pageCountSpan);
|
||||
|
||||
// Create arrows div with buttons
|
||||
const arrowsDiv = document.createElement("div");
|
||||
arrowsDiv.className = "arrows d-flex";
|
||||
|
||||
const moveUpButton = document.createElement("button");
|
||||
moveUpButton.className = "btn btn-secondary move-up";
|
||||
moveUpButton.innerHTML = "<span>↑</span>";
|
||||
|
||||
const moveDownButton = document.createElement("button");
|
||||
moveDownButton.className = "btn btn-secondary move-down";
|
||||
moveDownButton.innerHTML = "<span>↓</span>";
|
||||
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.className = "btn btn-danger remove-file";
|
||||
removeButton.innerHTML = "<span>×</span>";
|
||||
|
||||
arrowsDiv.append(moveUpButton, moveDownButton, removeButton);
|
||||
|
||||
// Append elements to item and then to list
|
||||
const itemContainer = document.createElement("div");
|
||||
itemContainer.className = "d-flex justify-content-between align-items-center w-100";
|
||||
itemContainer.append(fileNameDiv, pageInfoDiv, arrowsDiv);
|
||||
|
||||
item.appendChild(itemContainer);
|
||||
list.appendChild(item);
|
||||
}
|
||||
|
||||
attachMoveButtons();
|
||||
}
|
||||
|
||||
|
||||
async function getPDFPageCount(file) {
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
|
||||
const pdf = await pdfjsLib
|
||||
.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
url: 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", async function () {
|
||||
if (currentSort.field === "name" && !currentSort.descending) {
|
||||
currentSort.descending = true;
|
||||
await sortFiles((a, b) => b.name.localeCompare(a.name));
|
||||
} else {
|
||||
currentSort.field = "name";
|
||||
currentSort.descending = false;
|
||||
await sortFiles((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("sortByDateBtn").addEventListener("click", async function () {
|
||||
if (currentSort.field === "lastModified" && !currentSort.descending) {
|
||||
currentSort.descending = true;
|
||||
await sortFiles((a, b) => b.lastModified - a.lastModified);
|
||||
} else {
|
||||
currentSort.field = "lastModified";
|
||||
currentSort.descending = false;
|
||||
await sortFiles((a, b) => a.lastModified - b.lastModified);
|
||||
}
|
||||
});
|
||||
|
||||
async function sortFiles(comparator) {
|
||||
// Convert FileList to array and sort
|
||||
const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator);
|
||||
|
||||
// Refresh displayed list (wait for it to complete since it's async)
|
||||
await displayFiles(sortedFilesArray);
|
||||
|
||||
// Update the file input and fileOrder based on the current display order
|
||||
// This ensures consistency between display and file input
|
||||
updateFiles();
|
||||
}
|
||||
|
||||
function updateFiles() {
|
||||
var dataTransfer = new DataTransfer();
|
||||
var liElements = document.querySelectorAll("#selectedFiles li");
|
||||
const files = document.getElementById("fileInput-input").files;
|
||||
|
||||
console.log("updateFiles: found", liElements.length, "LI elements and", files.length, "files");
|
||||
|
||||
for (var i = 0; i < liElements.length; i++) {
|
||||
var fileNameFromList = liElements[i].querySelector(".filename").innerText;
|
||||
var found = false;
|
||||
for (var j = 0; j < files.length; j++) {
|
||||
var file = files[j];
|
||||
if (file.name === fileNameFromList) {
|
||||
dataTransfer.items.add(file);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
console.warn("updateFiles: Could not find file:", fileNameFromList);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("fileInput-input").files = dataTransfer.files;
|
||||
console.log("updateFiles: Updated file input with", dataTransfer.files.length, "files");
|
||||
|
||||
// Also populate hidden fileOrder to preserve visible order
|
||||
const order = Array.from(liElements)
|
||||
.map((li) => li.querySelector(".filename").innerText)
|
||||
.join("\n");
|
||||
const orderInput = document.getElementById("fileOrder");
|
||||
if (orderInput) {
|
||||
orderInput.value = order;
|
||||
console.log("Updated fileOrder:", order);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
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;
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageHighlighter;
|
||||
@@ -1,294 +0,0 @@
|
||||
import { DeletePageCommand } from "./commands/delete-page.js";
|
||||
import { DuplicatePageCommand } from "./commands/duplicate-page.js";
|
||||
import { SelectPageCommand } from "./commands/select.js";
|
||||
import { SplitFileCommand } from "./commands/split.js";
|
||||
import { UndoManager } from "./UndoManager.js";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
duplicatePageButtonCallback(e) {
|
||||
let imgContainer = this.getPageContainer(e.target);
|
||||
let duplicatePageCommand = new DuplicatePageCommand(
|
||||
imgContainer,
|
||||
this.duplicatePage,
|
||||
this.pagesContainer
|
||||
);
|
||||
duplicatePageCommand.execute();
|
||||
|
||||
this._pushUndoClearRedo(duplicatePageCommand);
|
||||
}
|
||||
|
||||
insertFileButtonCallback(e) {
|
||||
var imgContainer = this.getPageContainer(e.target);
|
||||
this.addFiles(imgContainer);
|
||||
}
|
||||
|
||||
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, duplicatePage }) {
|
||||
this.movePageTo = movePageTo;
|
||||
this.addFiles = addFiles;
|
||||
this.rotateElement = rotateElement;
|
||||
this.duplicatePage = duplicatePage;
|
||||
|
||||
this.moveUpButtonCallback = this.moveUpButtonCallback.bind(this);
|
||||
this.moveDownButtonCallback = this.moveDownButtonCallback.bind(this);
|
||||
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);
|
||||
this.duplicatePageButtonCallback = this.duplicatePageButtonCallback.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 duplicatePage = document.createElement("button");
|
||||
duplicatePage.classList.add("btn", "btn-secondary");
|
||||
duplicatePage.setAttribute('title', window.translations.duplicate);
|
||||
duplicatePage.innerHTML = `<span class="material-symbols-rounded">control_point_duplicate</span>`;
|
||||
duplicatePage.onclick = this.duplicatePageButtonCallback;
|
||||
buttonContainer.appendChild(duplicatePage);
|
||||
|
||||
const deletePage = document.createElement("button");
|
||||
deletePage.classList.add("btn", "btn-danger");
|
||||
deletePage.setAttribute('title', window.translations.delete);
|
||||
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");
|
||||
insertFileButton.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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
||||
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(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { CommandWithAnchors } from './command.js';
|
||||
|
||||
export class AddFilesCommand extends CommandWithAnchors {
|
||||
/**
|
||||
* @param {HTMLElement|null} element - Anchor element (optional, forwarded to addFilesAction)
|
||||
* @param {number[]} selectedPages
|
||||
* @param {Function} addFilesAction - async (nextSiblingElement|false) => HTMLElement[]|HTMLElement|null
|
||||
* @param {HTMLElement} pagesContainer
|
||||
*/
|
||||
constructor(element, selectedPages, addFilesAction, pagesContainer) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.selectedPages = selectedPages;
|
||||
this.addFilesAction = addFilesAction;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
/** @type {HTMLElement[]} */
|
||||
this.addedElements = [];
|
||||
|
||||
/**
|
||||
* Anchors captured on undo to support redo reinsertion.
|
||||
* @type {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }[]}
|
||||
*/
|
||||
this._anchors = [];
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
if (undoBtn) undoBtn.disabled = true;
|
||||
|
||||
const result = await this.addFilesAction(this.element || false);
|
||||
if (Array.isArray(result)) {
|
||||
this.addedElements = result;
|
||||
} else if (result) {
|
||||
this.addedElements = [result];
|
||||
} else {
|
||||
this.addedElements = [];
|
||||
}
|
||||
|
||||
// Capture anchors right after insertion so redo does not depend on undo.
|
||||
this._anchors = this.addedElements.map((el) => this.captureAnchor(el, this.pagesContainer));
|
||||
|
||||
if (undoBtn) undoBtn.disabled = false;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this._anchors = [];
|
||||
|
||||
for (const el of this.addedElements) {
|
||||
this._anchors.push(this.captureAnchor(el, this.pagesContainer));
|
||||
this.pagesContainer.removeChild(el);
|
||||
}
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = '';
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!this.addedElements.length) return;
|
||||
// If the elements are already in the DOM (no prior undo), do nothing.
|
||||
const alreadyInDom =
|
||||
this.addedElements[0].parentNode === this.pagesContainer;
|
||||
if (alreadyInDom) return;
|
||||
|
||||
// Use pre-captured anchors (from execute) or fall back to capturing now.
|
||||
const anchors = (this._anchors && this._anchors.length)
|
||||
? this._anchors
|
||||
: this.addedElements.map((el) =>
|
||||
this.captureAnchor(el, this.pagesContainer));
|
||||
|
||||
for (const anchor of anchors) {
|
||||
this.insertWithAnchor(this.pagesContainer, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
export class Command {
|
||||
execute() {}
|
||||
undo() {}
|
||||
redo() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class that provides anchor capture and reinsertion helpers
|
||||
* to avoid storing custom state on DOM nodes.
|
||||
*/
|
||||
export class CommandWithAnchors extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }[]} */
|
||||
this._anchors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child index of an element in a container.
|
||||
* @param {HTMLElement} container
|
||||
* @param {HTMLElement} el
|
||||
* @returns {number}
|
||||
*/
|
||||
_indexOf(container, el) {
|
||||
return Array.prototype.indexOf.call(container.children, el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures an anchor for later reinsertion.
|
||||
* @param {HTMLElement} el
|
||||
* @param {HTMLElement} container
|
||||
* @returns {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }}
|
||||
*/
|
||||
captureAnchor(el, container) {
|
||||
return {
|
||||
el,
|
||||
nextSibling: el.nextSibling,
|
||||
index: this._indexOf(container, el),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinserts an element using a previously captured anchor.
|
||||
* Prefers stored nextSibling when still valid; otherwise falls back to index.
|
||||
* @param {HTMLElement} container
|
||||
* @param {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }} anchor
|
||||
*/
|
||||
insertWithAnchor(container, anchor) {
|
||||
const { el, nextSibling, index } = anchor;
|
||||
const nextValid = nextSibling && nextSibling.parentNode === container;
|
||||
|
||||
let ref = null;
|
||||
if (nextValid) {
|
||||
ref = nextSibling;
|
||||
} else if (
|
||||
Number.isInteger(index) &&
|
||||
index >= 0 &&
|
||||
index < container.children.length
|
||||
) {
|
||||
ref = container.children[index] || null;
|
||||
}
|
||||
|
||||
container.insertBefore(el, ref || null);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Command } from './command.js';
|
||||
|
||||
/**
|
||||
* Composes multiple commands into a single atomic operation.
|
||||
* Executes in order; undo in reverse order.
|
||||
*/
|
||||
export class CommandSequence extends Command {
|
||||
/** @param {Command[]} commands - Commands to be executed/undone/redone as a group. */
|
||||
constructor(commands) {
|
||||
super();
|
||||
this.commands = commands;
|
||||
}
|
||||
|
||||
/** Execute: run each command in order. */
|
||||
execute() {
|
||||
this.commands.forEach((command) => command.execute());
|
||||
}
|
||||
|
||||
/** Undo: undo in reverse order. */
|
||||
undo() {
|
||||
this.commands.slice().reverse().forEach((command) => command.undo());
|
||||
}
|
||||
|
||||
/** Redo: simply execute again. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Removes a page from the container and restores it on undo.
|
||||
*/
|
||||
export class DeletePageCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} element - Page container to delete.
|
||||
* @param {HTMLElement} pagesContainer - Parent container holding all pages.
|
||||
*/
|
||||
constructor(element, pagesContainer) {
|
||||
super();
|
||||
|
||||
this.element = element;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
/** @type {ChildNode|null} */
|
||||
this.nextSibling = null;
|
||||
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
/** @type {string} */
|
||||
this.filenameInputValue = filenameInput ? filenameInput.value : "";
|
||||
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
/** @type {string} */
|
||||
this.filenameParagraphText = filenameParagraph ? filenameParagraph.innerText : "";
|
||||
}
|
||||
|
||||
/** Execute: remove the page and update empty-state UI if needed. */
|
||||
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");
|
||||
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Undo: reinsert the page at its original position. */
|
||||
undo() {
|
||||
const node = /** @type {ChildNode|null} */ (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");
|
||||
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = false;
|
||||
filenameInput.value = this.filenameInputValue;
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Redo: remove again and maintain empty-state UI. */
|
||||
redo() {
|
||||
const pageNumberElement = this.element.querySelector(".page-number");
|
||||
if (pageNumberElement) {
|
||||
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");
|
||||
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = "";
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { CommandWithAnchors } from './command.js';
|
||||
|
||||
export class DuplicatePageCommand extends CommandWithAnchors {
|
||||
/**
|
||||
* @param {HTMLElement} element - The page element to duplicate.
|
||||
* @param {Function} duplicatePageAction - (element) => HTMLElement (new clone already inserted)
|
||||
* @param {HTMLElement} pagesContainer
|
||||
*/
|
||||
constructor(element, duplicatePageAction, pagesContainer) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.duplicatePageAction = duplicatePageAction;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
/** @type {HTMLElement|null} */
|
||||
this.newElement = null;
|
||||
|
||||
/** @type {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }|null} */
|
||||
this._anchor = null;
|
||||
}
|
||||
|
||||
execute() {
|
||||
// Create and insert a duplicate next to the original
|
||||
this.newElement = this.duplicatePageAction(this.element);
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.newElement) return;
|
||||
|
||||
// Capture anchor before removal so redo can reinsert at the same position
|
||||
this._anchor = this.captureAnchor(this.newElement, this.pagesContainer);
|
||||
|
||||
this.pagesContainer.removeChild(this.newElement);
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = '';
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.updatePageNumbersAndCheckboxes?.();
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!this.newElement) {
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
if (this._anchor) {
|
||||
this.insertWithAnchor(this.pagesContainer, this._anchor);
|
||||
} else {
|
||||
// Fallback: insert relative to the original element
|
||||
this.pagesContainer.insertBefore(this.newElement, this.element.nextSibling || null);
|
||||
}
|
||||
window.updatePageNumbersAndCheckboxes?.();
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Command } from './command.js';
|
||||
|
||||
/**
|
||||
* Moves a page (or multiple pages, via PdfContainer wrapper) inside the container.
|
||||
*/
|
||||
export class MovePageCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} startElement - Dragged page container.
|
||||
* @param {HTMLElement|null} endElement - Destination reference; insert before this node. Null = append.
|
||||
* @param {HTMLElement} pagesContainer - Parent container with all pages.
|
||||
* @param {HTMLElement} pagesContainerWrapper - Scrollable wrapper element.
|
||||
* @param {boolean} [scrollTo=false] - Whether to apply a subtle scroll after move.
|
||||
*/
|
||||
constructor(startElement, endElement, pagesContainer, pagesContainerWrapper, scrollTo = false) {
|
||||
super();
|
||||
|
||||
this.pagesContainer = pagesContainer;
|
||||
const childArray = Array.from(this.pagesContainer.childNodes);
|
||||
|
||||
/** @type {number} */
|
||||
this.startIndex = childArray.indexOf(startElement);
|
||||
/** @type {number} */
|
||||
this.endIndex = childArray.indexOf(endElement);
|
||||
|
||||
this.startElement = startElement;
|
||||
this.endElement = endElement;
|
||||
|
||||
this.scrollTo = scrollTo;
|
||||
this.pagesContainerWrapper = pagesContainerWrapper;
|
||||
}
|
||||
|
||||
/** Execute: perform DOM move and optional scroll. */
|
||||
execute() {
|
||||
// Remove stale page number badge if present (Firefox sometimes misses the event)
|
||||
const pageNumberElement = this.startElement.querySelector('.page-number');
|
||||
if (pageNumberElement) {
|
||||
this.startElement.removeChild(pageNumberElement);
|
||||
}
|
||||
|
||||
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: restore original order and optional scroll back. */
|
||||
undo() {
|
||||
if (this.startElement) {
|
||||
this.pagesContainer.removeChild(this.startElement);
|
||||
const previousNeighbour = Array.from(this.pagesContainer.childNodes)[this.startIndex];
|
||||
previousNeighbour?.insertAdjacentElement('beforebegin', this.startElement)
|
||||
?? this.pagesContainer.append(this.startElement);
|
||||
}
|
||||
|
||||
if (this.scrollTo) {
|
||||
const { width } = this.startElement.getBoundingClientRect();
|
||||
const vector = this.endIndex === -1 || this.startIndex <= this.endIndex ? 0 - width : width;
|
||||
|
||||
this.pagesContainerWrapper.scroll({
|
||||
left: this.pagesContainerWrapper.scrollLeft - vector,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Redo: same as execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { CommandWithAnchors } from './command.js';
|
||||
|
||||
export class PageBreakCommand extends CommandWithAnchors {
|
||||
/**
|
||||
* @param {HTMLElement[]} elements
|
||||
* @param {boolean} isSelectedInWindow
|
||||
* @param {number[]} selectedPages - 0-based indices of selected pages
|
||||
* @param {Function} pageBreakCallback - async (element, addedSoFar) => HTMLElement[]|HTMLElement|null
|
||||
* @param {HTMLElement} pagesContainer
|
||||
*/
|
||||
constructor(elements, isSelectedInWindow, selectedPages, pageBreakCallback, pagesContainer) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.isSelectedInWindow = isSelectedInWindow;
|
||||
this.selectedPages = selectedPages;
|
||||
this.pageBreakCallback = pageBreakCallback;
|
||||
this.pagesContainer = pagesContainer;
|
||||
|
||||
/** @type {HTMLElement[]} */
|
||||
this.addedElements = [];
|
||||
|
||||
/**
|
||||
* Anchors captured on undo to support redo reinsertion.
|
||||
* @type {{ el: HTMLElement, nextSibling: ChildNode|null, index: number }[]}
|
||||
*/
|
||||
this._anchors = [];
|
||||
|
||||
// Keep content snapshot if needed for future enhancements
|
||||
this.originalStates = Array.from(elements, (element) => ({
|
||||
element,
|
||||
hasContent: element.innerHTML.trim() !== '',
|
||||
}));
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
if (undoBtn) undoBtn.disabled = true;
|
||||
|
||||
for (const [index, element] of this.elements.entries()) {
|
||||
const withinSelection = !this.isSelectedInWindow || this.selectedPages.includes(index);
|
||||
if (!withinSelection) continue;
|
||||
|
||||
if (index !== 0) {
|
||||
const result = await this.pageBreakCallback(element, this.addedElements);
|
||||
if (!Array.isArray(this.addedElements)) {
|
||||
this.addedElements = [];
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
this.addedElements.push(...result);
|
||||
} else if (result) {
|
||||
this.addedElements.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture anchors right after insertion so redo does not depend on undo.
|
||||
this._anchors = this.addedElements.map((el) => this.captureAnchor(el, this.pagesContainer));
|
||||
|
||||
if (undoBtn) undoBtn.disabled = false;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this._anchors = [];
|
||||
|
||||
for (const el of this.addedElements) {
|
||||
this._anchors.push(this.captureAnchor(el, this.pagesContainer));
|
||||
this.pagesContainer.removeChild(el);
|
||||
}
|
||||
|
||||
if (this.pagesContainer.childElementCount === 0) {
|
||||
const filenameInput = document.getElementById('filename-input');
|
||||
const filenameParagraph = document.getElementById('filename');
|
||||
const downloadBtn = document.getElementById('export-button');
|
||||
|
||||
if (filenameInput) {
|
||||
filenameInput.disabled = true;
|
||||
filenameInput.value = '';
|
||||
}
|
||||
if (filenameParagraph) {
|
||||
filenameParagraph.innerText = '';
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
// If elements are already present (no prior undo), do nothing.
|
||||
if (!this.addedElements.length) return;
|
||||
const alreadyInDom =
|
||||
this.addedElements[0].parentNode === this.pagesContainer;
|
||||
if (alreadyInDom) return;
|
||||
|
||||
// Use pre-captured anchors (from execute) or fall back to current ones.
|
||||
const anchors = (this._anchors && this._anchors.length)
|
||||
? this._anchors
|
||||
: this.addedElements.map((el) => this.captureAnchor(el, this.pagesContainer));
|
||||
|
||||
for (const anchor of anchors) {
|
||||
this.insertWithAnchor(this.pagesContainer, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Deletes a set of selected pages and restores them on undo.
|
||||
*/
|
||||
export class RemoveSelectedCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} pagesContainer - Parent container.
|
||||
* @param {number[]} selectedPages - 1-based page numbers to remove.
|
||||
* @param {Function} updatePageNumbersAndCheckboxes - Callback to refresh UI state.
|
||||
*/
|
||||
constructor(pagesContainer, selectedPages, updatePageNumbersAndCheckboxes) {
|
||||
super();
|
||||
this.pagesContainer = pagesContainer;
|
||||
this.selectedPages = selectedPages;
|
||||
|
||||
/** @type {{idx:number, childNode:HTMLElement}[]} */
|
||||
this.deletedChildren = [];
|
||||
|
||||
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");
|
||||
if (checkbox) {
|
||||
checkbox.id = `selectPageCheckbox-${pageNumber}`;
|
||||
checkbox.setAttribute("data-page-number", pageNumber);
|
||||
checkbox.checked = window.selectedPages.includes(pageNumber);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const filenameInput = document.getElementById("filename-input");
|
||||
const filenameParagraph = document.getElementById("filename");
|
||||
|
||||
/** @type {string} */
|
||||
this.originalFilenameInputValue = filenameInput ? filenameInput.value : "";
|
||||
/** @type {string|undefined} */
|
||||
this.originalFilenameParagraphText = filenameParagraph?.innerText;
|
||||
}
|
||||
|
||||
/** Execute: remove selected pages and update empty state. */
|
||||
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;
|
||||
if (filenameInput) filenameInput.value = "";
|
||||
if (filenameParagraph) filenameParagraph.innerText = "";
|
||||
if (downloadBtn) downloadBtn.disabled = true;
|
||||
}
|
||||
|
||||
window.selectedPages = [];
|
||||
this.updatePageNumbersAndCheckboxes();
|
||||
document.dispatchEvent(new Event("selectedPagesUpdated"));
|
||||
}
|
||||
|
||||
/** Undo: restore all removed nodes at their original indices. */
|
||||
undo() {
|
||||
while (this.deletedChildren.length > 0) {
|
||||
const 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;
|
||||
if (filenameInput) filenameInput.value = this.originalFilenameInputValue;
|
||||
if (filenameParagraph && this.originalFilenameParagraphText !== undefined) {
|
||||
filenameParagraph.innerText = this.originalFilenameParagraphText;
|
||||
}
|
||||
if (downloadBtn) downloadBtn.disabled = false;
|
||||
}
|
||||
|
||||
window.selectedPages = this.selectedPages;
|
||||
this.updatePageNumbersAndCheckboxes();
|
||||
document.dispatchEvent(new Event("selectedPagesUpdated"));
|
||||
}
|
||||
|
||||
/** Redo mirrors execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Rotates a single image element by a relative degree.
|
||||
*/
|
||||
export class RotateElementCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} element - The <img> element to rotate.
|
||||
* @param {number|string} degree - Relative degrees to add (e.g., 90 or "-90").
|
||||
*/
|
||||
constructor(element, degree) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
/** Execute: apply rotation. */
|
||||
execute() {
|
||||
let lastTransform = this.element.style.rotate || "0";
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const newAngle = lastAngle + parseInt(this.degree);
|
||||
|
||||
this.element.style.rotate = newAngle + "deg";
|
||||
}
|
||||
|
||||
/** Undo: revert by subtracting the same delta. */
|
||||
undo() {
|
||||
let lastTransform = this.element.style.rotate || "0";
|
||||
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const undoAngle = currentAngle + -parseInt(this.degree);
|
||||
|
||||
this.element.style.rotate = undoAngle + "deg";
|
||||
}
|
||||
|
||||
/** Redo mirrors execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a set of image elements by a relative degree.
|
||||
*/
|
||||
export class RotateAllCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement[]} elements - Image elements to rotate.
|
||||
* @param {number} degree - Relative degrees to add for all.
|
||||
*/
|
||||
constructor(elements, degree) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
/** Execute: apply rotation to all. */
|
||||
execute() {
|
||||
for (const element of this.elements) {
|
||||
let lastTransform = element.style.rotate || "0";
|
||||
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const newAngle = lastAngle + this.degree;
|
||||
|
||||
element.style.rotate = newAngle + "deg";
|
||||
}
|
||||
}
|
||||
|
||||
/** Undo: revert rotation for all. */
|
||||
undo() {
|
||||
for (const element of this.elements) {
|
||||
let lastTransform = element.style.rotate || "0";
|
||||
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
|
||||
const undoAngle = currentAngle + -this.degree;
|
||||
|
||||
element.style.rotate = undoAngle + "deg";
|
||||
}
|
||||
}
|
||||
|
||||
/** Redo mirrors execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Toggles selection state of a single page via its checkbox.
|
||||
*/
|
||||
export class SelectPageCommand extends Command {
|
||||
/**
|
||||
* @param {number} pageNumber - 1-based page number.
|
||||
* @param {HTMLInputElement} checkbox - Checkbox linked to the page.
|
||||
*/
|
||||
constructor(pageNumber, checkbox) {
|
||||
super();
|
||||
this.pageNumber = pageNumber;
|
||||
this.selectCheckbox = checkbox;
|
||||
}
|
||||
|
||||
/** Execute: apply current checkbox state to global selection. */
|
||||
execute() {
|
||||
if (this.selectCheckbox.checked) {
|
||||
window.selectedPages.push(this.pageNumber);
|
||||
} else {
|
||||
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: invert checkbox and apply same logic as execute. */
|
||||
undo() {
|
||||
this.selectCheckbox.checked = !this.selectCheckbox.checked;
|
||||
this.execute();
|
||||
}
|
||||
|
||||
/** Redo: invert again then execute. */
|
||||
redo() {
|
||||
this.selectCheckbox.checked = !this.selectCheckbox.checked;
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Command } from "./command.js";
|
||||
|
||||
/**
|
||||
* Toggles a split class on a single page element.
|
||||
*/
|
||||
export class SplitFileCommand extends Command {
|
||||
/**
|
||||
* @param {HTMLElement} element - Target page container.
|
||||
* @param {string} splitClass - CSS class to toggle for split markers.
|
||||
*/
|
||||
constructor(element, splitClass) {
|
||||
super();
|
||||
this.element = element;
|
||||
this.splitClass = splitClass;
|
||||
}
|
||||
|
||||
/** Execute: toggle split class. */
|
||||
execute() {
|
||||
this.element.classList.toggle(this.splitClass);
|
||||
}
|
||||
|
||||
/** Undo: toggle split class back. */
|
||||
undo() {
|
||||
this.element.classList.toggle(this.splitClass);
|
||||
}
|
||||
|
||||
/** Redo: same as execute. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles split class across a set of elements, optionally limited by selection.
|
||||
*/
|
||||
export class SplitAllCommand extends Command {
|
||||
/**
|
||||
* @param {NodeListOf<HTMLElement>|HTMLElement[]} elements - All page containers.
|
||||
* @param {boolean} isSelectedInWindow - Whether multi-select mode is active.
|
||||
* @param {number[]} selectedPages - 0-based indices of selected pages (when active).
|
||||
* @param {string} splitClass - CSS class used as split marker.
|
||||
*/
|
||||
constructor(elements, isSelectedInWindow, selectedPages, splitClass) {
|
||||
super();
|
||||
this.elements = elements;
|
||||
this.isSelectedInWindow = isSelectedInWindow;
|
||||
this.selectedPages = selectedPages;
|
||||
this.splitClass = splitClass;
|
||||
}
|
||||
|
||||
/** Execute: toggle split for all or selected pages. */
|
||||
execute() {
|
||||
if (!this.isSelectedInWindow) {
|
||||
const hasSplit = this._hasSplit();
|
||||
(this.elements || []).forEach((page) => {
|
||||
if (hasSplit) {
|
||||
page.classList.remove(this.splitClass);
|
||||
} else {
|
||||
page.classList.add(this.splitClass);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements.forEach((page, index) => {
|
||||
if (!this.selectedPages.includes(index)) return;
|
||||
page.classList.toggle(this.splitClass);
|
||||
});
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if any element currently has the split class. */
|
||||
_hasSplit() {
|
||||
if (!this.elements || this.elements.length === 0) return false;
|
||||
for (const node of this.elements) {
|
||||
if (node.classList.contains(this.splitClass)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Undo mirrors execute logic. */
|
||||
undo() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
/** Redo mirrors execute logic. */
|
||||
redo() {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupDropdowns() {
|
||||
const dropdowns = document.querySelectorAll('.navbar-nav > .nav-item.dropdown');
|
||||
|
||||
dropdowns.forEach((dropdown) => {
|
||||
const toggle = dropdown.querySelector('[data-bs-toggle="dropdown"]');
|
||||
if (!toggle) return;
|
||||
|
||||
// Skip search dropdown, it has its own logic
|
||||
if (toggle.id === 'searchDropdown') {
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.addEventListener('show.bs.dropdown', () => {
|
||||
// Find all other open dropdowns and hide them
|
||||
const openDropdowns = document.querySelectorAll('.navbar-nav .dropdown-menu.show');
|
||||
openDropdowns.forEach((menu) => {
|
||||
const parentDropdown = menu.closest('.dropdown');
|
||||
if (parentDropdown && parentDropdown !== dropdown) {
|
||||
const parentToggle = parentDropdown.querySelector('[data-bs-toggle="dropdown"]');
|
||||
if (parentToggle) {
|
||||
// Get or create Bootstrap dropdown instance
|
||||
let instance = bootstrap.Dropdown.getInstance(parentToggle);
|
||||
if (!instance) {
|
||||
instance = new bootstrap.Dropdown(parentToggle);
|
||||
}
|
||||
instance.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tooltipSetup() {
|
||||
// initialize global tooltip element or get reference
|
||||
let customTooltip = document.getElementById("customTooltip");
|
||||
if (!customTooltip) {
|
||||
customTooltip = document.createElement("div");
|
||||
customTooltip.id = "customTooltip";
|
||||
customTooltip.className = "btn-tooltip";
|
||||
document.body.appendChild(customTooltip);
|
||||
}
|
||||
|
||||
function updateTooltipPosition(event, text) {
|
||||
if (window.innerWidth >= 1200) {
|
||||
customTooltip.textContent = text;
|
||||
customTooltip.style.display = "block";
|
||||
customTooltip.style.left = `${event.pageX + 10}px`;
|
||||
customTooltip.style.top = `${event.pageY + 10}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
customTooltip.style.display = "none";
|
||||
}
|
||||
|
||||
// find uninitialized tooltips and set up event listeners
|
||||
const tooltipElements = document.querySelectorAll("[title]");
|
||||
|
||||
tooltipElements.forEach((element) => {
|
||||
const tooltipText = element.getAttribute("title");
|
||||
element.removeAttribute("title");
|
||||
element.setAttribute("data-title", tooltipText); // no UI effect, just for reference
|
||||
|
||||
element.addEventListener("mouseenter", (event) => updateTooltipPosition(event, tooltipText));
|
||||
element.addEventListener("mousemove", (event) => updateTooltipPosition(event, tooltipText));
|
||||
element.addEventListener("mouseleave", hideTooltip);
|
||||
|
||||
// in case UI moves and mouseleave is not triggered, the tooltip is re-added when the mouse is moved over the element
|
||||
element.addEventListener("click", hideTooltip);
|
||||
});
|
||||
};
|
||||
window.tooltipSetup = tooltipSetup;
|
||||
|
||||
// Override the bootstrap dropdown styles for mobile
|
||||
function fixNavbarDropdownStyles() {
|
||||
if (window.innerWidth < 1200) {
|
||||
document.querySelectorAll('.navbar .dropdown-menu').forEach(function(menu) {
|
||||
menu.style.transform = 'none';
|
||||
menu.style.transformOrigin = 'none';
|
||||
menu.style.left = '0';
|
||||
menu.style.right = '0';
|
||||
menu.style.maxWidth = '95vw';
|
||||
menu.style.width = '100vw';
|
||||
menu.style.marginBottom = '0';
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll('.navbar .dropdown-menu').forEach(function(menu) {
|
||||
menu.style.transform = '';
|
||||
menu.style.transformOrigin = '';
|
||||
menu.style.left = '';
|
||||
menu.style.right = '';
|
||||
menu.style.maxWidth = '';
|
||||
menu.style.width = '';
|
||||
menu.style.marginBottom = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
tooltipSetup();
|
||||
setupDropdowns();
|
||||
fixNavbarDropdownStyles();
|
||||
// Setup logout button functionality
|
||||
const logoutButton = document.querySelector('a[href="/logout"]');
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
if (window.JWTManager) {
|
||||
window.JWTManager.logout();
|
||||
} else {
|
||||
// Fallback if JWTManager is not available
|
||||
window.location.href = '/logout';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
window.addEventListener('resize', fixNavbarDropdownStyles);
|
||||
@@ -1,85 +0,0 @@
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
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 = pdfjsPath + 'pdf.worker.mjs';
|
||||
const pdfDoc = await pdfjsLib.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
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 = pdfjsPath + 'pdf.worker.mjs';
|
||||
pdf = await pdfjsLib.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
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();
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
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 = pdfjsPath + 'pdf.worker.mjs';
|
||||
const pdf = await pdfjsLib
|
||||
.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
url: 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();
|
||||
});
|
||||
@@ -1,195 +0,0 @@
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
let pdfCanvas = document.getElementById('cropPdfCanvas');
|
||||
let overlayCanvas = document.getElementById('overlayCanvas');
|
||||
let canvasesContainer = document.getElementById('canvasesContainer');
|
||||
canvasesContainer.style.display = 'none';
|
||||
|
||||
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
|
||||
let currentRenderTask = null; // Track current PDF render task to cancel if needed
|
||||
|
||||
function renderPageFromFile(file) {
|
||||
if (file.type === 'application/pdf') {
|
||||
// Cancel any ongoing render task when loading a new file
|
||||
if (currentRenderTask) {
|
||||
currentRenderTask.cancel();
|
||||
currentRenderTask = null;
|
||||
}
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
let typedArray = new Uint8Array(reader.result);
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
|
||||
pdfjsLib
|
||||
.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
data: 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 (!pdfDoc) return; // Only resize if we have a PDF loaded
|
||||
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);
|
||||
|
||||
// Re-render with new container size
|
||||
renderPage(currentPage);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('file-input-change', async (e) => {
|
||||
if (!e.detail) return; // Guard against null detail
|
||||
const {allFiles} = e.detail;
|
||||
if (allFiles && allFiles.length > 0) {
|
||||
canvasesContainer.style.display = 'block'; // set for visual purposes
|
||||
|
||||
// Wait for the layout to be updated before rendering
|
||||
setTimeout(() => {
|
||||
let file = allFiles[0];
|
||||
renderPageFromFile(file);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
cropForm.addEventListener('submit', function (e) {
|
||||
if (xInput.value == '' && yInput.value == '' && widthInput.value == '' && heightInput.value == '') {
|
||||
// Set coordinates for the entire PDF surface
|
||||
let currentContainerRect = canvasesContainer.getBoundingClientRect();
|
||||
xInput.value = 0;
|
||||
yInput.value = 0;
|
||||
widthInput.value = currentContainerRect.width;
|
||||
heightInput.value = currentContainerRect.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) {
|
||||
// Cancel any ongoing render task
|
||||
if (currentRenderTask) {
|
||||
currentRenderTask.cancel();
|
||||
currentRenderTask = null;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Normalize rotation to 0, 90, 180, or 270 degrees
|
||||
let normalizedRotation = ((page.rotate % 360) + 360) % 360;
|
||||
let viewport = page.getViewport({scale: pageScale, rotation: normalizedRotation});
|
||||
|
||||
// Don't set container width, let CSS handle it
|
||||
canvasesContainer.style.height = viewport.height + 'px';
|
||||
|
||||
pdfCanvas.width = viewport.width;
|
||||
pdfCanvas.height = viewport.height;
|
||||
|
||||
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
|
||||
overlayCanvas.height = viewport.height;
|
||||
|
||||
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
|
||||
|
||||
context.fillStyle = 'white';
|
||||
context.fillRect(0, 0, pdfCanvas.width, pdfCanvas.height);
|
||||
|
||||
let renderContext = {canvasContext: context, viewport: viewport};
|
||||
currentRenderTask = page.render(renderContext);
|
||||
currentRenderTask.promise.then(function() {
|
||||
currentRenderTask = null;
|
||||
pdfCanvas.classList.add('shadow-canvas');
|
||||
}).catch(function(error) {
|
||||
if (error.name !== 'RenderingCancelledException') {
|
||||
console.error('PDF rendering error:', error);
|
||||
}
|
||||
currentRenderTask = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,802 +0,0 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const bookmarksContainer = document.getElementById("bookmarks-container");
|
||||
const errorMessageContainer = document.getElementById("error-message-container");
|
||||
const addBookmarkBtn = document.getElementById("addBookmarkBtn");
|
||||
const bookmarkDataInput = document.getElementById("bookmarkData");
|
||||
let bookmarks = [];
|
||||
let counter = 0; // Used for generating unique IDs
|
||||
|
||||
// callback function on file input change to extract bookmarks from PDF
|
||||
async function getBookmarkDataFromPdf(event) {
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", event.target.files[0]);
|
||||
|
||||
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 fetch API: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const extractedBookmarks = await response.json();
|
||||
return extractedBookmarks;
|
||||
} catch (error) {
|
||||
throw new Error("Error extracting bookmark data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// callback function on file input change to extract bookmarks from JSON
|
||||
async function getBookmarkDataFromJson(event) {
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = event.target.files[0];
|
||||
|
||||
try {
|
||||
const fileText = await file.text();
|
||||
const jsonData = JSON.parse(fileText);
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
throw new Error(`Error extracting bookmark data: error while reading or parsing JSON file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// display new bookmark data given by a callback function that loads or fetches the data
|
||||
async function loadBookmarks(getBookmarkDataCallback) {
|
||||
// reset bookmarks
|
||||
bookmarks = [];
|
||||
updateBookmarksUI();
|
||||
showLoadingIndicator();
|
||||
|
||||
try {
|
||||
// Get new bookmarks from the callback
|
||||
const newBookmarks = await getBookmarkDataCallback();
|
||||
|
||||
// Convert extracted bookmarks to our format with IDs
|
||||
if (newBookmarks && newBookmarks.length > 0) {
|
||||
bookmarks = newBookmarks.map(convertExtractedBookmark);
|
||||
}
|
||||
} catch (error) {
|
||||
bookmarks = [];
|
||||
throw new Error(`Error loading bookmarks: ${error}`);
|
||||
} finally {
|
||||
removeLoadingIndicator();
|
||||
updateBookmarksUI();
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener to the file input to extract existing bookmarks
|
||||
document.getElementById("fileInput-input").addEventListener("change", async function (event) {
|
||||
try {
|
||||
await loadBookmarks(async function () {
|
||||
return getBookmarkDataFromPdf(event);
|
||||
});
|
||||
} catch {
|
||||
showErrorMessage("Failed to extract bookmarks. You can still create new ones.");
|
||||
}
|
||||
});
|
||||
|
||||
function showLoadingIndicator() {
|
||||
const loadingEl = document.createElement("div");
|
||||
loadingEl.className = "alert alert-info";
|
||||
loadingEl.textContent = "Loading bookmarks from PDF...";
|
||||
loadingEl.id = "loading-bookmarks";
|
||||
errorMessageContainer.innerHTML = "";
|
||||
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;
|
||||
errorMessageContainer.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
|
||||
window.tooltipSetup();
|
||||
}
|
||||
|
||||
// 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.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>`;
|
||||
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";
|
||||
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.title = "Add first bookmark";
|
||||
|
||||
// Initialize tooltips for the empty state button
|
||||
window.tooltipSetup();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize with an empty state if no bookmarks
|
||||
if (bookmarks.length === 0) {
|
||||
showEmptyState();
|
||||
updateEmptyStateButton();
|
||||
}
|
||||
|
||||
// Add bookmarks Import/Export functionality
|
||||
|
||||
// Import/Export button references
|
||||
const importDefaultBtn = document.getElementById("importDefaultBtn");
|
||||
const exportDefaultBtn = document.getElementById("exportDefaultBtn");
|
||||
const importUploadJsonFileInput = document.getElementById("importUploadJsonFileInput");
|
||||
const importPasteFromClipboardBtn = document.getElementById("importPasteFromClipboardBtn");
|
||||
const exportDownloadJsonFileBtn = document.getElementById("exportDownloadJsonFileBtn");
|
||||
const exportCopyToClipboardBtn = document.getElementById("exportCopyToClipboardBtn");
|
||||
|
||||
// display import/export from/to clipboard buttons if supported
|
||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||
importPasteFromClipboardBtn.parentElement.classList.remove("d-none");
|
||||
}
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
exportCopyToClipboardBtn.parentElement.classList.remove("d-none");
|
||||
}
|
||||
|
||||
function flashButtonSuccess(button) {
|
||||
const originalClass = button.className;
|
||||
|
||||
button.classList.remove("btn-outline-primary");
|
||||
button.classList.add("btn-success", "success-flash");
|
||||
|
||||
setTimeout(() => {
|
||||
button.className = originalClass;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Import handlers
|
||||
async function handleJsonFileInputChange(event) {
|
||||
try {
|
||||
await loadBookmarks(async function () {
|
||||
return getBookmarkDataFromJson(event);
|
||||
});
|
||||
flashButtonSuccess(importDefaultBtn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to import bookmarks from JSON file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function importBookmarksFromClipboard() {
|
||||
console.log("Importing bookmarks from clipboard...");
|
||||
|
||||
try {
|
||||
await loadBookmarks(async function () {
|
||||
const clipboardText = await navigator.clipboard.readText();
|
||||
if (!clipboardText) return [];
|
||||
|
||||
return JSON.parse(clipboardText);
|
||||
});
|
||||
flashButtonSuccess(importDefaultBtn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to import bookmarks from clipboard: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBookmarksPasteFromClipboard(event) {
|
||||
// do not override normal paste behavior on input fields
|
||||
if (event.target.tagName.toLowerCase() === "input") return;
|
||||
|
||||
try {
|
||||
await loadBookmarks(async function () {
|
||||
const clipboardText = event.clipboardData?.getData("text/plain");
|
||||
if (!clipboardText) return [];
|
||||
|
||||
return JSON.parse(clipboardText);
|
||||
});
|
||||
flashButtonSuccess(importDefaultBtn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to import bookmarks from clipboard (ctrl-v): ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Export handlers
|
||||
async function exportBookmarksToJson() {
|
||||
console.log("Exporting bookmarks to JSON...");
|
||||
|
||||
try {
|
||||
const bookmarkData = bookmarkDataInput.value;
|
||||
const blob = new Blob([bookmarkData], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "bookmarks.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
flashButtonSuccess(exportDefaultBtn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to export bookmarks to JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportBookmarksToClipboard() {
|
||||
const bookmarkData = bookmarkDataInput.value;
|
||||
try {
|
||||
await navigator.clipboard.writeText(bookmarkData);
|
||||
flashButtonSuccess(exportDefaultBtn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to export bookmarks to clipboard: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBookmarksCopyToClipboard(event) {
|
||||
// do not override normal copy behavior on input fields
|
||||
if (event.target.tagName.toLowerCase() === "input") return;
|
||||
|
||||
const bookmarkData = bookmarkDataInput.value;
|
||||
|
||||
try {
|
||||
event.clipboardData.setData("text/plain", bookmarkData);
|
||||
event.preventDefault();
|
||||
flashButtonSuccess(exportDefaultBtn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to export bookmarks to clipboard (ctrl-c): ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// register event listeners for import/export functions
|
||||
importUploadJsonFileInput.addEventListener("change", handleJsonFileInputChange);
|
||||
importPasteFromClipboardBtn.addEventListener("click", importBookmarksFromClipboard);
|
||||
exportDownloadJsonFileBtn.addEventListener("click", exportBookmarksToJson);
|
||||
exportCopyToClipboardBtn.addEventListener("click", exportBookmarksToClipboard);
|
||||
document.body.addEventListener("copy", handleBookmarksCopyToClipboard);
|
||||
document.body.addEventListener("paste", handleBookmarksPasteFromClipboard);
|
||||
// set default actions
|
||||
// importDefaultBtn is already handled by being a label for the file input
|
||||
exportDefaultBtn.addEventListener("click", exportBookmarksToJson);
|
||||
|
||||
// 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 = "";
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,200 +0,0 @@
|
||||
/*<![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 pdfProcessingThresholds = [8, 15, 22, 35, 50, 75, 100, 150];
|
||||
|
||||
// Check if survey version changed and reset PDF processing count if it did
|
||||
const storedVersion = localStorage.getItem('surveyVersion');
|
||||
if (storedVersion && storedVersion !== surveyVersion) {
|
||||
localStorage.setItem('pdfProcessingCount', '0');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
}
|
||||
|
||||
let pdfProcessingCount = parseInt(localStorage.getItem('pdfProcessingCount') || '0');
|
||||
|
||||
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 && pdfProcessingThresholds.includes(pdfProcessingCount)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pdfProcessingThresholds.includes(pdfProcessingCount);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
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 = pdfjsPath + 'pdf.worker.mjs';
|
||||
pdfjsLib
|
||||
.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
data: 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 = pdfjsPath + 'pdf.worker.mjs';
|
||||
pdfjsLib
|
||||
.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
data: 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 = pdfjsPath + 'pdf.worker.mjs';
|
||||
pdfjsLib
|
||||
.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
data: 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');
|
||||
});
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
const PDFJS_DEFAULT_OPTIONS = {
|
||||
cMapUrl: pdfjsPath + 'cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
|
||||
};
|
||||
|
||||
window.toggleSignatureView = toggleSignatureView;
|
||||
window.previewSignature = previewSignature;
|
||||
window.addSignatureFromPreview = addSignatureFromPreview;
|
||||
window.addDraggableFromPad = addDraggableFromPad;
|
||||
window.addDraggableFromText = addDraggableFromText;
|
||||
window.goToFirstOrLastPage = goToFirstOrLastPage;
|
||||
|
||||
let currentPreviewSrc = null;
|
||||
|
||||
function getSelectedSignatureColor() {
|
||||
const textPicker = document.getElementById('signature-color-text');
|
||||
const drawPicker = document.getElementById('signature-color');
|
||||
return (textPicker && textPicker.value) || (drawPicker && drawPicker.value) || '#000000';
|
||||
}
|
||||
|
||||
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 = pdfjsPath + 'pdf.worker.mjs';
|
||||
const pdfDoc = await pdfjsLib.getDocument({
|
||||
...PDFJS_DEFAULT_OPTIONS,
|
||||
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: "#000000",
|
||||
});
|
||||
|
||||
// Keep pad color in sync if draw picker exists
|
||||
(function initPadColorSync() {
|
||||
const drawPicker = document.getElementById('signature-color');
|
||||
if (!drawPicker) return;
|
||||
if (drawPicker.value) signaturePad.penColor = drawPicker.value;
|
||||
drawPicker.addEventListener('input', () => {
|
||||
signaturePad.penColor = drawPicker.value || '#000000';
|
||||
});
|
||||
})();
|
||||
|
||||
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 color = getSelectedSignatureColor();
|
||||
|
||||
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.fillStyle = color;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -1,768 +0,0 @@
|
||||
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
@@ -1,143 +0,0 @@
|
||||
|
||||
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 divElement = item.querySelector("div");
|
||||
if (divElement) {
|
||||
var originalContent = divElement.cloneNode(true);
|
||||
contentWrapper.appendChild(originalContent);
|
||||
} else {
|
||||
// Fallback: create content manually if div is not found
|
||||
var fallbackContent = document.createElement("div");
|
||||
fallbackContent.innerHTML = item.innerHTML;
|
||||
contentWrapper.appendChild(fallbackContent);
|
||||
}
|
||||
|
||||
contentWrapper.onclick = function () {
|
||||
window.location.href = itemHref;
|
||||
};
|
||||
|
||||
dropdownItem.appendChild(contentWrapper);
|
||||
resultsBox.appendChild(dropdownItem);
|
||||
addedResults.add(itemHref);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resultsBox.style.width = window.navItemMaxWidth + "px";
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const searchDropdown = document.getElementById('searchDropdown');
|
||||
const searchInput = document.getElementById('navbarSearchInput');
|
||||
|
||||
// Check if elements are missing and skip initialization if necessary
|
||||
if (!searchDropdown || !searchInput) {
|
||||
console.warn('Search dropdown or input not found. Skipping initialization.');
|
||||
return;
|
||||
}
|
||||
const dropdownMenu = searchDropdown.querySelector('.dropdown-menu');
|
||||
if (!dropdownMenu) {
|
||||
console.warn('Dropdown menu not found within the search dropdown. Skipping initialization.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a single dropdown instance
|
||||
const dropdownInstance = new bootstrap.Dropdown(searchDropdown);
|
||||
|
||||
// Handle click for mobile
|
||||
searchDropdown.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const isOpen = dropdownMenu.classList.contains('show');
|
||||
// Close all other open dropdowns
|
||||
document.querySelectorAll('.navbar-nav .dropdown-menu.show').forEach((menu) => {
|
||||
if (menu !== dropdownMenu) {
|
||||
const parentDropdown = menu.closest('.dropdown');
|
||||
if (parentDropdown) {
|
||||
const parentToggle = parentDropdown.querySelector('[data-bs-toggle="dropdown"]');
|
||||
if (parentToggle) {
|
||||
let instance = bootstrap.Dropdown.getInstance(parentToggle);
|
||||
if (!instance) {
|
||||
instance = new bootstrap.Dropdown(parentToggle);
|
||||
}
|
||||
instance.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!isOpen) {
|
||||
dropdownInstance.show();
|
||||
setTimeout(() => searchInput.focus(), 150);
|
||||
} else {
|
||||
dropdownInstance.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Hide dropdown if it's open and user clicks outside
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!searchDropdown.contains(e.target) && dropdownMenu.classList.contains('show')) {
|
||||
dropdownInstance.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Keep dropdown open if search input is clicked
|
||||
searchInput.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
// 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);
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
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: '#000000', // default color
|
||||
});
|
||||
|
||||
(function initSignatureColor() {
|
||||
const colorInput = document.getElementById('signature-color');
|
||||
if (!colorInput) return;
|
||||
|
||||
if (colorInput.value) {
|
||||
signaturePad.penColor = colorInput.value;
|
||||
}
|
||||
colorInput.addEventListener('input', () => {
|
||||
signaturePad.penColor = colorInput.value || '#000000';
|
||||
});
|
||||
})();
|
||||
|
||||
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);
|
||||
@@ -1,37 +0,0 @@
|
||||
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
@@ -1,80 +0,0 @@
|
||||
import './cookieconsent.umd.js';
|
||||
|
||||
// Enable dark mode
|
||||
document.documentElement.classList.add('cc--darkmode');
|
||||
|
||||
// Build analytics services dynamically based on backend config
|
||||
const analyticsServices = {};
|
||||
if (typeof posthogEnabled !== 'undefined' && posthogEnabled) {
|
||||
analyticsServices.posthog = {
|
||||
label: cookieBannerPreferencesModalPosthogLabel
|
||||
};
|
||||
}
|
||||
if (typeof scarfEnabled !== 'undefined' && scarfEnabled) {
|
||||
analyticsServices.scarf = {
|
||||
label: cookieBannerPreferencesModalScarfLabel
|
||||
};
|
||||
}
|
||||
|
||||
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: {
|
||||
services: analyticsServices
|
||||
}
|
||||
},
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
/* 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
@@ -1,363 +0,0 @@
|
||||
// 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 fetchWithCsrf('/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);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
class UUID {
|
||||
static uuidv4() {
|
||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
|
||||
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UUID;
|
||||
Reference in New Issue
Block a user