Resolve conflicts using files from Stirling-2.0

This commit is contained in:
Reece
2025-06-24 12:51:39 +01:00
parent 16e56e3208
commit a51960b199
691 changed files with 168291 additions and 0 deletions
@@ -0,0 +1,76 @@
window.goToFirstOrLastPage = goToFirstOrLastPage;
document.getElementById('download-pdf').addEventListener('click', async () => {
const downloadButton = document.getElementById('download-pdf');
const originalContent = downloadButton.innerHTML;
downloadButton.disabled = true;
downloadButton.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
`;
try {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_addedImage.pdf';
link.click();
} finally {
downloadButton.disabled = false;
downloadButton.innerHTML = originalContent;
}
});
let originalFileName = '';
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const fileInput = event.target;
fileInput.addEventListener('file-input-change', async (e) => {
const { allFiles } = e.detail;
if (allFiles && allFiles.length > 0) {
const file = allFiles[0];
originalFileName = file.name.replace(/\.[^/.]+$/, '');
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
el.style.cssText = '';
});
}
});
});
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
el.style.cssText = 'display:none !important';
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete') {
DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted());
}
});
});
const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', (e) => {
if (!e.target.files) {
return;
}
for (const imageFile of e.target.files) {
var reader = new FileReader();
reader.readAsDataURL(imageFile);
reader.onloadend = function (e) {
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
};
}
});
async function goToFirstOrLastPage(page) {
if (page) {
const lastPage = DraggableUtils.pdfDoc.numPages;
await DraggableUtils.goToPage(lastPage - 1);
} else {
await DraggableUtils.goToPage(0);
}
}
@@ -0,0 +1,253 @@
var canvas = document.getElementById('contrast-pdf-canvas');
var context = canvas.getContext('2d');
var originalImageData = null;
var allPages = [];
var pdfDoc = null;
var pdf = null; // This is the current PDF document
async function renderPDFAndSaveOriginalImageData(file) {
var fileReader = new FileReader();
fileReader.onload = async function () {
var data = new Uint8Array(this.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdf = await pdfjsLib.getDocument({data: data}).promise;
// Get the number of pages in the PDF
var numPages = pdf.numPages;
allPages = Array.from({length: numPages}, (_, i) => i + 1);
// Create a new PDF document
pdfDoc = await PDFLib.PDFDocument.create();
// Render the first page in the viewer
await renderPageAndAdjustImageProperties(1);
document.getElementById('sliders-container').style.display = 'block';
};
fileReader.readAsArrayBuffer(file);
}
// This function is now async and returns a promise
function renderPageAndAdjustImageProperties(pageNum) {
return new Promise(async function (resolve, reject) {
var page = await pdf.getPage(pageNum);
var scale = 1.5;
var viewport = page.getViewport({scale: scale});
canvas.height = viewport.height;
canvas.width = viewport.width;
var renderContext = {
canvasContext: context,
viewport: viewport,
};
var renderTask = page.render(renderContext);
renderTask.promise.then(function () {
originalImageData = context.getImageData(0, 0, canvas.width, canvas.height);
adjustImageProperties();
resolve();
});
canvas.classList.add('fixed-shadow-canvas');
});
}
function adjustImageProperties() {
var contrast = parseFloat(document.getElementById('contrast-slider').value);
var brightness = parseFloat(document.getElementById('brightness-slider').value);
var saturation = parseFloat(document.getElementById('saturation-slider').value);
contrast /= 100; // normalize to range [0, 2]
brightness /= 100; // normalize to range [0, 2]
saturation /= 100; // normalize to range [0, 2]
if (originalImageData) {
var newImageData = context.createImageData(originalImageData.width, originalImageData.height);
newImageData.data.set(originalImageData.data);
for (var i = 0; i < newImageData.data.length; i += 4) {
var r = newImageData.data[i];
var g = newImageData.data[i + 1];
var b = newImageData.data[i + 2];
// Adjust contrast
r = adjustContrastForPixel(r, contrast);
g = adjustContrastForPixel(g, contrast);
b = adjustContrastForPixel(b, contrast);
// Adjust brightness
r = adjustBrightnessForPixel(r, brightness);
g = adjustBrightnessForPixel(g, brightness);
b = adjustBrightnessForPixel(b, brightness);
// Adjust saturation
var rgb = adjustSaturationForPixel(r, g, b, saturation);
newImageData.data[i] = rgb[0];
newImageData.data[i + 1] = rgb[1];
newImageData.data[i + 2] = rgb[2];
}
context.putImageData(newImageData, 0, 0);
}
}
function rgbToHsl(r, g, b) {
(r /= 255), (g /= 255), (b /= 255);
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h,
s,
l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h, s, l];
}
function hslToRgb(h, s, l) {
var r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [r * 255, g * 255, b * 255];
}
function adjustContrastForPixel(pixel, contrast) {
// Normalize to range [-0.5, 0.5]
var normalized = pixel / 255 - 0.5;
// Apply contrast
normalized *= contrast;
// Denormalize back to [0, 255]
return (normalized + 0.5) * 255;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function adjustSaturationForPixel(r, g, b, saturation) {
var hsl = rgbToHsl(r, g, b);
// Adjust saturation
hsl[1] = clamp(hsl[1] * saturation, 0, 1);
// Convert back to RGB
var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
// Return adjusted RGB values
return rgb;
}
function adjustBrightnessForPixel(pixel, brightness) {
return Math.max(0, Math.min(255, pixel * brightness));
}
let inputFileName = '';
async function downloadPDF() {
for (var i = 0; i < allPages.length; i++) {
await renderPageAndAdjustImageProperties(allPages[i]);
const pngImageBytes = canvas.toDataURL('image/png');
const pngImage = await pdfDoc.embedPng(pngImageBytes);
const pngDims = pngImage.scale(1);
// Create a blank page matching the dimensions of the image
const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
// Draw the PNG image
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngDims.width,
height: pngDims.height,
});
}
// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();
// Create a Blob
const blob = new Blob([pdfBytes.buffer], {type: 'application/pdf'});
// Create download link
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
let newFileName = inputFileName ? inputFileName.replace('.pdf', '') : 'download';
newFileName += '_adjusted_color.pdf';
downloadLink.download = newFileName;
downloadLink.click();
// After download, reset the viewer and clear stored data
allPages = []; // Clear the pages
originalImageData = null; // Clear the image data
// Go back to page 1 and render it in the viewer
if (pdf !== null) {
renderPageAndAdjustImageProperties(1);
}
}
// Event listeners
document.getElementById('fileInput-input').addEventListener('change', function (e) {
const fileInput = e.target;
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
const file = allFiles[0];
inputFileName = file.name;
renderPDFAndSaveOriginalImageData(file);
}
});
});
document.getElementById('contrast-slider').addEventListener('input', function () {
document.getElementById('contrast-val').textContent = this.value;
adjustImageProperties();
});
document.getElementById('brightness-slider').addEventListener('input', function () {
document.getElementById('brightness-val').textContent = this.value;
adjustImageProperties();
});
document.getElementById('saturation-slider').addEventListener('input', function () {
document.getElementById('saturation-val').textContent = this.value;
adjustImageProperties();
});
document.getElementById('download-button').addEventListener('click', function () {
downloadPDF();
});
@@ -0,0 +1,150 @@
const deleteAllCheckbox = document.querySelector('#deleteAll');
let inputs = document.querySelectorAll('input');
const customMetadataDiv = document.getElementById('customMetadata');
const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
deleteAllCheckbox.addEventListener('change', function (event) {
inputs.forEach((input) => {
// If it's the deleteAllCheckbox or any file input, skip
if (input === deleteAllCheckbox || input.type === 'file') {
return;
}
// Disable or enable based on the checkbox state
input.disabled = deleteAllCheckbox.checked;
});
});
const customModeCheckbox = document.getElementById('customModeCheckbox');
const addMetadataBtn = document.getElementById('addMetadataBtn');
const customMetadataFormContainer = document.getElementById('customMetadataEntries');
var count = 1;
const fileInput = document.querySelector('#fileInput-input');
const authorInput = document.querySelector('#author');
const creationDateInput = document.querySelector('#creationDate');
const creatorInput = document.querySelector('#creator');
const keywordsInput = document.querySelector('#keywords');
const modificationDateInput = document.querySelector('#modificationDate');
const producerInput = document.querySelector('#producer');
const subjectInput = document.querySelector('#subject');
const titleInput = document.querySelector('#title');
const trappedInput = document.querySelector('#trapped');
var lastPDFFileMeta = null;
var lastPDFFile = null;
fileInput.addEventListener('change', async function () {
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
const file = allFiles[0];
while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
}
while (customMetadataFormContainer.firstChild) {
customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
}
var url = URL.createObjectURL(file);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument(url).promise;
const pdfMetadata = await pdf.getMetadata();
lastPDFFile = pdfMetadata?.info;
console.log(pdfMetadata);
if (!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
customModeCheckbox.disabled = true;
customModeCheckbox.checked = false;
} else {
customModeCheckbox.disabled = false;
}
authorInput.value = pdfMetadata?.info?.Author;
creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
creatorInput.value = pdfMetadata?.info?.Creator;
keywordsInput.value = pdfMetadata?.info?.Keywords;
modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
producerInput.value = pdfMetadata?.info?.Producer;
subjectInput.value = pdfMetadata?.info?.Subject;
titleInput.value = pdfMetadata?.info?.Title;
console.log(pdfMetadata?.info);
const trappedValue = pdfMetadata?.info?.Trapped;
// Get all options in the select element
const options = trappedInput.options;
// Loop through all options to find the one with a matching value
for (let i = 0; i < options.length; i++) {
if (options[i].value === trappedValue) {
options[i].selected = true;
break;
}
}
addExtra();
}
});
});
addMetadataBtn.addEventListener('click', () => {
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Key';
keyInput.className = 'form-control';
keyInput.name = `allRequestParams[customKey${count}]`;
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value';
valueInput.className = 'form-control';
valueInput.name = `allRequestParams[customValue${count}]`;
count = count + 1;
const formGroup = document.createElement('div');
formGroup.className = 'mb-3';
formGroup.appendChild(keyInput);
formGroup.appendChild(valueInput);
customMetadataFormContainer.appendChild(formGroup);
});
function convertDateFormat(dateTimeString) {
if (!dateTimeString || dateTimeString.length < 17) {
return dateTimeString;
}
const year = dateTimeString.substring(2, 6);
const month = dateTimeString.substring(6, 8);
const day = dateTimeString.substring(8, 10);
const hour = dateTimeString.substring(10, 12);
const minute = dateTimeString.substring(12, 14);
const second = dateTimeString.substring(14, 16);
return year + '/' + month + '/' + day + ' ' + hour + ':' + minute + ':' + second;
}
function addExtra() {
const event = document.getElementById('customModeCheckbox');
if (event.checked && lastPDFFile.Custom != null) {
customMetadataDiv.style.display = 'block';
for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
if (
key === 'Author' ||
key === 'CreationDate' ||
key === 'Creator' ||
key === 'Keywords' ||
key === 'ModDate' ||
key === 'Producer' ||
key === 'Subject' ||
key === 'Title' ||
key === 'Trapped'
) {
continue;
}
const entryDiv = document.createElement('div');
entryDiv.className = 'mb-3';
entryDiv.innerHTML = `<div class="mb-3"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
otherMetadataEntriesDiv.appendChild(entryDiv);
}
} else {
customMetadataDiv.style.display = 'none';
while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
}
}
}
customModeCheckbox.addEventListener('change', (event) => {
addExtra();
});
@@ -0,0 +1,159 @@
let pdfCanvas = document.getElementById('cropPdfCanvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
canvasesContainer.style.display = 'none';
let containerRect = canvasesContainer.getBoundingClientRect();
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let isDrawing = false; // New flag to check if drawing is ongoing
let cropForm = document.getElementById('cropForm');
let fileInput = document.getElementById('fileInput-input');
let xInput = document.getElementById('x');
let yInput = document.getElementById('y');
let widthInput = document.getElementById('width');
let heightInput = document.getElementById('height');
let pdfDoc = null;
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
let pageScale = 1; // The scale which the pdf page renders
let timeId = null; // timeout id for resizing canvases event
function renderPageFromFile(file) {
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
window.addEventListener('resize', function () {
clearTimeout(timeId);
timeId = setTimeout(function () {
if (fileInput.files.length == 0) return;
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
pdfCanvas.width = containerRect.width;
pdfCanvas.height = containerRect.height;
overlayCanvas.width = containerRect.width;
overlayCanvas.height = containerRect.height;
let file = fileInput.files[0];
renderPageFromFile(file);
}, 1000);
});
fileInput.addEventListener('change', function (e) {
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
canvasesContainer.style.display = 'block'; // set for visual purposes
let file = allFiles[0];
renderPageFromFile(file);
}
});
});
cropForm.addEventListener('submit', function (e) {
if (xInput.value == '' && yInput.value == '' && widthInput.value == '' && heightInput.value == '') {
// Ορίστε συντεταγμένες για ολόκληρη την επιφάνεια του PDF
xInput.value = 0;
yInput.value = 0;
widthInput.value = containerRect.width;
heightInput.value = containerRect.height;
}
});
overlayCanvas.addEventListener('mousedown', function (e) {
// Clear previously drawn rectangle on the main canvas
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
renderPage(currentPage); // Re-render the PDF
// Clear the overlay canvas to ensure old drawings are removed
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
startX = e.offsetX;
startY = e.offsetY;
isDrawing = true;
});
overlayCanvas.addEventListener('mousemove', function (e) {
if (!isDrawing) return;
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
overlayContext.strokeStyle = 'red';
overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
});
overlayCanvas.addEventListener('mouseup', function (e) {
isDrawing = false;
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
let flippedY = pdfCanvas.height - e.offsetY;
xInput.value = startX / pageScale;
yInput.value = flippedY / pageScale;
widthInput.value = rectWidth / pageScale;
heightInput.value = rectHeight / pageScale;
// Draw the final rectangle on the main canvas
context.strokeStyle = 'red';
context.strokeRect(startX, startY, rectWidth, rectHeight);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function (page) {
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
pageScale = containerRect.width / page.getViewport({scale: 1}).width; // The new scale
let viewport = page.getViewport({scale: containerRect.width / page.getViewport({scale: 1}).width});
canvasesContainer.width = viewport.width;
canvasesContainer.height = viewport.height;
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = {canvasContext: context, viewport: viewport};
page.render(renderContext);
pdfCanvas.classList.add('shadow-canvas');
});
}
@@ -0,0 +1,653 @@
document.addEventListener('DOMContentLoaded', function() {
const bookmarksContainer = document.getElementById('bookmarks-container');
const addBookmarkBtn = document.getElementById('addBookmarkBtn');
const bookmarkDataInput = document.getElementById('bookmarkData');
let bookmarks = [];
let counter = 0; // Used for generating unique IDs
// Add event listener to the file input to extract existing bookmarks
document.getElementById('fileInput-input').addEventListener('change', async function(e) {
if (!e.target.files || e.target.files.length === 0) {
return;
}
// Reset bookmarks initially
bookmarks = [];
updateBookmarksUI();
const formData = new FormData();
formData.append('file', e.target.files[0]);
// Show loading indicator
showLoadingIndicator();
try {
// Call the API to extract bookmarks using fetchWithCsrf for CSRF protection
const response = await fetchWithCsrf('/api/v1/general/extract-bookmarks', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Failed to extract bookmarks: ${response.status} ${response.statusText}`);
}
const extractedBookmarks = await response.json();
// Convert extracted bookmarks to our format with IDs
if (extractedBookmarks && extractedBookmarks.length > 0) {
bookmarks = extractedBookmarks.map(convertExtractedBookmark);
} else {
showEmptyState();
}
} catch (error) {
// Show error message
showErrorMessage('Failed to extract bookmarks. You can still create new ones.');
// Add a default bookmark if no bookmarks and error
if (bookmarks.length === 0) {
showEmptyState();
}
} finally {
// Remove loading indicator
removeLoadingIndicator();
// Update the UI
updateBookmarksUI();
}
});
function showLoadingIndicator() {
const loadingEl = document.createElement('div');
loadingEl.className = 'alert alert-info';
loadingEl.textContent = 'Loading bookmarks from PDF...';
loadingEl.id = 'loading-bookmarks';
bookmarksContainer.innerHTML = '';
bookmarksContainer.appendChild(loadingEl);
}
function removeLoadingIndicator() {
const loadingEl = document.getElementById('loading-bookmarks');
if (loadingEl) {
loadingEl.remove();
}
}
function showErrorMessage(message) {
const errorEl = document.createElement('div');
errorEl.className = 'alert alert-danger';
errorEl.textContent = message;
bookmarksContainer.appendChild(errorEl);
}
function showEmptyState() {
const emptyStateEl = document.createElement('div');
emptyStateEl.className = 'empty-bookmarks mb-3';
emptyStateEl.innerHTML = `
<span class="material-symbols-rounded mb-2" style="font-size: 48px;">bookmark_add</span>
<h5>No bookmarks found</h5>
<p class="mb-3">This PDF doesn't have any bookmarks yet. Add your first bookmark to get started.</p>
<button type="button" class="btn btn-primary btn-add-first-bookmark">
<span class="material-symbols-rounded">add</span> Add First Bookmark
</button>
`;
// Add event listener to the "Add First Bookmark" button
emptyStateEl.querySelector('.btn-add-first-bookmark').addEventListener('click', function() {
addBookmark(null, 'New Bookmark', 1);
emptyStateEl.remove();
});
bookmarksContainer.appendChild(emptyStateEl);
}
// Function to convert extracted bookmarks to our format with IDs
function convertExtractedBookmark(bookmark) {
counter++;
const result = {
id: Date.now() + counter, // Generate a unique ID
title: bookmark.title || 'Untitled Bookmark',
pageNumber: bookmark.pageNumber || 1,
children: [],
expanded: false // All bookmarks start collapsed for better visibility
};
// Convert children recursively
if (bookmark.children && bookmark.children.length > 0) {
result.children = bookmark.children.map(child => {
return convertExtractedBookmark(child);
});
}
return result;
}
// Add bookmark button click handler
addBookmarkBtn.addEventListener('click', function(e) {
e.preventDefault();
addBookmark();
});
// Add form submit handler to update JSON data
document.getElementById('editTocForm').addEventListener('submit', function() {
updateBookmarkData();
});
function addBookmark(parent = null, title = '', pageNumber = 1) {
counter++;
const newBookmark = {
id: Date.now() + counter,
title: title || 'New Bookmark',
pageNumber: pageNumber || 1,
children: [],
expanded: false // New bookmarks start collapsed
};
if (parent === null) {
bookmarks.push(newBookmark);
} else {
const parentBookmark = findBookmark(bookmarks, parent);
if (parentBookmark) {
parentBookmark.children.push(newBookmark);
parentBookmark.expanded = true; // Auto-expand the parent when adding a child
} else {
// Add to root level if parent not found
bookmarks.push(newBookmark);
}
}
updateBookmarksUI();
// After updating UI, find and focus the new bookmark's title field
setTimeout(() => {
const newElement = document.querySelector(`[data-id="${newBookmark.id}"]`);
if (newElement) {
const titleInput = newElement.querySelector('.bookmark-title');
if (titleInput) {
titleInput.focus();
titleInput.select();
}
// Scroll to the new element
newElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 50);
}
function findBookmark(bookmarkArray, id) {
for (const bookmark of bookmarkArray) {
if (bookmark.id === id) {
return bookmark;
}
if (bookmark.children.length > 0) {
const found = findBookmark(bookmark.children, id);
if (found) return found;
}
}
return null;
}
// Find the parent bookmark of a given bookmark by ID
function findParentBookmark(bookmarkArray, id, parent = null) {
for (const bookmark of bookmarkArray) {
if (bookmark.id === id) {
return parent; // Return the parent ID (or null if top-level)
}
if (bookmark.children.length > 0) {
const found = findParentBookmark(bookmark.children, id, bookmark.id);
if (found !== undefined) return found;
}
}
return undefined; // Not found at this level
}
function removeBookmark(id) {
// Remove from top level
const index = bookmarks.findIndex(b => b.id === id);
if (index !== -1) {
bookmarks.splice(index, 1);
updateBookmarksUI();
return;
}
// Remove from children
function removeFromChildren(bookmarkArray, id) {
for (const bookmark of bookmarkArray) {
const childIndex = bookmark.children.findIndex(b => b.id === id);
if (childIndex !== -1) {
bookmark.children.splice(childIndex, 1);
return true;
}
if (removeFromChildren(bookmark.children, id)) {
return true;
}
}
return false;
}
if (removeFromChildren(bookmarks, id)) {
updateBookmarksUI();
}
// If no bookmarks left, show empty state
if (bookmarks.length === 0) {
showEmptyState();
}
}
function toggleBookmarkExpanded(id) {
const bookmark = findBookmark(bookmarks, id);
if (bookmark) {
bookmark.expanded = !bookmark.expanded;
updateBookmarksUI();
}
}
function updateBookmarkData() {
// Create a clean version without the IDs for submission
const cleanBookmarks = bookmarks.map(cleanBookmark);
bookmarkDataInput.value = JSON.stringify(cleanBookmarks);
}
function cleanBookmark(bookmark) {
return {
title: bookmark.title,
pageNumber: bookmark.pageNumber,
children: bookmark.children.map(cleanBookmark)
};
}
function updateBookmarksUI() {
if (!bookmarksContainer) {
return;
}
// Only clear the container if there are no error messages or loading indicators
if (!document.querySelector('#bookmarks-container .alert')) {
bookmarksContainer.innerHTML = '';
}
// Check if there are bookmarks to display
if (bookmarks.length === 0 && !document.querySelector('.empty-bookmarks')) {
showEmptyState();
} else {
// Remove empty state if it exists and there are bookmarks
const emptyState = document.querySelector('.empty-bookmarks');
if (emptyState && bookmarks.length > 0) {
emptyState.remove();
}
// Create bookmark elements
bookmarks.forEach(bookmark => {
const bookmarkElement = createBookmarkElement(bookmark);
bookmarksContainer.appendChild(bookmarkElement);
});
}
updateBookmarkData();
// Initialize tooltips for dynamically added elements
if (typeof $ !== 'undefined') {
$('[data-bs-toggle="tooltip"]').tooltip();
}
}
// Create the main bookmark element with collapsible interface
function createBookmarkElement(bookmark, level = 0) {
const bookmarkEl = document.createElement('div');
bookmarkEl.className = 'bookmark-item';
bookmarkEl.dataset.id = bookmark.id;
bookmarkEl.dataset.level = level;
// Create the header (always visible part)
const header = createBookmarkHeader(bookmark, level);
bookmarkEl.appendChild(header);
// Create the content (collapsible part)
const content = document.createElement('div');
content.className = 'bookmark-content';
if (!bookmark.expanded) {
content.style.display = 'none';
}
// Main input row
const inputRow = createInputRow(bookmark);
content.appendChild(inputRow);
bookmarkEl.appendChild(content);
// Add children container if has children and expanded
if (bookmark.children && bookmark.children.length > 0) {
const childrenContainer = createChildrenContainer(bookmark, level);
if (bookmark.expanded) {
bookmarkEl.appendChild(childrenContainer);
}
}
return bookmarkEl;
}
// Create the header that's always visible
function createBookmarkHeader(bookmark, level) {
const header = document.createElement('div');
header.className = 'bookmark-header';
if (!bookmark.expanded) {
header.classList.add('collapsed');
}
// Left side of header with expand/collapse and info
const headerLeft = document.createElement('div');
headerLeft.className = 'd-flex align-items-center';
// Toggle expand/collapse icon with child count
const toggleContainer = document.createElement('div');
toggleContainer.className = 'd-flex align-items-center';
toggleContainer.style.marginRight = '8px';
// Only show toggle if has children
if (bookmark.children && bookmark.children.length > 0) {
// Create toggle icon
const toggleIcon = document.createElement('span');
toggleIcon.className = 'material-symbols-rounded toggle-icon me-1';
toggleIcon.textContent = 'expand_more';
toggleIcon.style.cursor = 'pointer';
toggleContainer.appendChild(toggleIcon);
// Add child count indicator
const childCount = document.createElement('span');
childCount.className = 'badge rounded-pill';
// Use theme-appropriate badge color
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
childCount.classList.add(isDarkMode ? 'bg-info' : 'bg-secondary');
childCount.style.fontSize = '0.7rem';
childCount.style.padding = '0.2em 0.5em';
childCount.textContent = bookmark.children.length;
childCount.setAttribute('data-bs-toggle', 'tooltip');
childCount.setAttribute('data-bs-placement', 'top');
childCount.title = `${bookmark.children.length} child bookmark${bookmark.children.length > 1 ? 's' : ''}`;
toggleContainer.appendChild(childCount);
} else {
// Add spacer if no children
const spacer = document.createElement('span');
spacer.style.width = '24px';
spacer.style.display = 'inline-block';
toggleContainer.appendChild(spacer);
}
headerLeft.appendChild(toggleContainer);
// Level indicator for nested items
if (level > 0) {
// Add relationship indicator visual line
const relationshipIndicator = document.createElement('div');
relationshipIndicator.className = 'bookmark-relationship-indicator';
const line = document.createElement('div');
line.className = 'relationship-line';
relationshipIndicator.appendChild(line);
const arrow = document.createElement('div');
arrow.className = 'relationship-arrow';
relationshipIndicator.appendChild(arrow);
header.appendChild(relationshipIndicator);
// Text indicator
const levelIndicator = document.createElement('span');
levelIndicator.className = 'bookmark-level-indicator';
levelIndicator.textContent = `Child`;
headerLeft.appendChild(levelIndicator);
}
// Title preview
const titlePreview = document.createElement('span');
titlePreview.className = 'bookmark-title-preview';
titlePreview.textContent = bookmark.title;
headerLeft.appendChild(titlePreview);
// Page number preview
const pagePreview = document.createElement('span');
pagePreview.className = 'bookmark-page-preview';
pagePreview.textContent = `Page ${bookmark.pageNumber}`;
headerLeft.appendChild(pagePreview);
// Right side of header with action buttons
const headerRight = document.createElement('div');
headerRight.className = 'bookmark-actions-header';
// Quick add buttons with clear visual distinction - using Stirling-PDF's tooltip system
const quickAddChildButton = createButton('subdirectory_arrow_right', 'btn-add-child', 'Add child bookmark', function(e) {
e.preventDefault();
e.stopPropagation();
addBookmark(bookmark.id);
});
const quickAddSiblingButton = createButton('add', 'btn-add-sibling', 'Add sibling bookmark', function(e) {
e.preventDefault();
e.stopPropagation();
// Find parent of current bookmark
const parentId = findParentBookmark(bookmarks, bookmark.id);
addBookmark(parentId, '', bookmark.pageNumber); // Same level as current bookmark
});
// Quick remove button
const quickRemoveButton = createButton('delete', 'btn-outline-danger', 'Remove bookmark', function(e) {
e.preventDefault();
e.stopPropagation();
if (confirm('Are you sure you want to remove this bookmark' +
(bookmark.children.length > 0 ? ' and all its children?' : '?'))) {
removeBookmark(bookmark.id);
}
});
headerRight.appendChild(quickAddChildButton);
headerRight.appendChild(quickAddSiblingButton);
headerRight.appendChild(quickRemoveButton);
// Assemble header
header.appendChild(headerLeft);
header.appendChild(headerRight);
// Add click handler for expansion toggle
header.addEventListener('click', function(e) {
// Only toggle if not clicking on buttons
if (!e.target.closest('button')) {
toggleBookmarkExpanded(bookmark.id);
}
});
return header;
}
function createInputRow(bookmark) {
const row = document.createElement('div');
row.className = 'row';
// Title input
row.appendChild(createTitleInputElement(bookmark));
// Page input
row.appendChild(createPageInputElement(bookmark));
return row;
}
function createTitleInputElement(bookmark) {
const titleCol = document.createElement('div');
titleCol.className = 'col-md-8';
const titleGroup = document.createElement('div');
titleGroup.className = 'mb-3';
const titleLabel = document.createElement('label');
titleLabel.textContent = 'Title';
titleLabel.className = 'form-label';
const titleInput = document.createElement('input');
titleInput.type = 'text';
titleInput.className = 'form-control bookmark-title';
titleInput.value = bookmark.title;
titleInput.addEventListener('input', function() {
bookmark.title = this.value;
updateBookmarkData();
// Also update the preview in the header
const header = titleInput.closest('.bookmark-item').querySelector('.bookmark-title-preview');
if (header) {
header.textContent = this.value;
}
});
titleGroup.appendChild(titleLabel);
titleGroup.appendChild(titleInput);
titleCol.appendChild(titleGroup);
return titleCol;
}
function createPageInputElement(bookmark) {
const pageCol = document.createElement('div');
pageCol.className = 'col-md-4';
const pageGroup = document.createElement('div');
pageGroup.className = 'mb-3';
const pageLabel = document.createElement('label');
pageLabel.textContent = 'Page';
pageLabel.className = 'form-label';
const pageInput = document.createElement('input');
pageInput.type = 'number';
pageInput.className = 'form-control bookmark-page';
pageInput.value = bookmark.pageNumber;
pageInput.min = 1;
pageInput.addEventListener('input', function() {
bookmark.pageNumber = parseInt(this.value) || 1;
updateBookmarkData();
// Also update the preview in the header
const header = pageInput.closest('.bookmark-item').querySelector('.bookmark-page-preview');
if (header) {
header.textContent = `Page ${bookmark.pageNumber}`;
}
});
pageGroup.appendChild(pageLabel);
pageGroup.appendChild(pageInput);
pageCol.appendChild(pageGroup);
return pageCol;
}
function createButton(icon, className, title, clickHandler) {
const button = document.createElement('button');
button.type = 'button';
button.className = `btn ${className} btn-bookmark-action`;
button.innerHTML = `<span class="material-symbols-rounded">${icon}</span>`;
// Use Bootstrap tooltips
button.setAttribute('data-bs-toggle', 'tooltip');
button.setAttribute('data-bs-placement', 'top');
button.title = title;
button.addEventListener('click', clickHandler);
return button;
}
function createChildrenContainer(bookmark, level) {
const childrenContainer = document.createElement('div');
childrenContainer.className = 'bookmark-children';
bookmark.children.forEach(child => {
childrenContainer.appendChild(createBookmarkElement(child, level + 1));
});
return childrenContainer;
}
// Update the add bookmark button appearance with clear visual cue
addBookmarkBtn.innerHTML = '<span class="material-symbols-rounded">add</span> Add Top-level Bookmark';
addBookmarkBtn.className = 'btn btn-primary btn-add-bookmark top-level';
// Use Bootstrap tooltips
addBookmarkBtn.setAttribute('data-bs-toggle', 'tooltip');
addBookmarkBtn.setAttribute('data-bs-placement', 'top');
addBookmarkBtn.title = 'Add a new top-level bookmark';
// Add icon to empty state button as well
const updateEmptyStateButton = function() {
const emptyStateBtn = document.querySelector('.btn-add-first-bookmark');
if (emptyStateBtn) {
emptyStateBtn.innerHTML = '<span class="material-symbols-rounded">add</span> Add First Bookmark';
emptyStateBtn.setAttribute('data-bs-toggle', 'tooltip');
emptyStateBtn.setAttribute('data-bs-placement', 'top');
emptyStateBtn.title = 'Add first bookmark';
// Initialize tooltips for the empty state button
if (typeof $ !== 'undefined') {
$('[data-bs-toggle="tooltip"]').tooltip();
}
}
};
// Initialize with an empty state if no bookmarks
if (bookmarks.length === 0) {
showEmptyState();
updateEmptyStateButton();
}
// Listen for theme changes to update badge colors
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'data-bs-theme') {
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
document.querySelectorAll('.badge').forEach(badge => {
badge.classList.remove('bg-secondary', 'bg-info');
badge.classList.add(isDarkMode ? 'bg-info' : 'bg-secondary');
});
}
});
});
observer.observe(document.documentElement, { attributes: true });
// Add visual enhancement to clearly show the top-level/child relationship
document.addEventListener('mouseover', function(e) {
// When hovering over add buttons, highlight their relationship targets
const button = e.target.closest('.btn-add-child, .btn-add-sibling');
if (button) {
if (button.classList.contains('btn-add-child')) {
// Highlight parent-child relationship
const bookmarkItem = button.closest('.bookmark-item');
if (bookmarkItem) {
bookmarkItem.style.boxShadow = '0 0 0 2px var(--btn-add-child-border, #198754)';
}
} else if (button.classList.contains('btn-add-sibling')) {
// Highlight sibling relationship
const bookmarkItem = button.closest('.bookmark-item');
if (bookmarkItem) {
// Find siblings
const parent = bookmarkItem.parentElement;
const siblings = parent.querySelectorAll(':scope > .bookmark-item');
siblings.forEach(sibling => {
if (sibling !== bookmarkItem) {
sibling.style.boxShadow = '0 0 0 2px var(--btn-add-sibling-border, #0d6efd)';
}
});
}
}
}
});
document.addEventListener('mouseout', function(e) {
// Remove highlights when not hovering
const button = e.target.closest('.btn-add-child, .btn-add-sibling');
if (button) {
// Remove all highlights
document.querySelectorAll('.bookmark-item').forEach(item => {
item.style.boxShadow = '';
});
}
});
});
@@ -0,0 +1,203 @@
/*<![CDATA[*/
document.addEventListener('DOMContentLoaded', function () {
if (window.analyticsPromptBoolean) {
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
analyticsModal.show();
let retryCount = 0;
function hideCookieBanner() {
const cookieBanner = document.querySelector('#cc-main');
if (cookieBanner && cookieBanner.offsetHeight > 0) {
cookieBanner.style.display = "none";
} else if (retryCount < 20) {
retryCount++;
setTimeout(hideCookieBanner, 100);
}
}
hideCookieBanner();
}
});
/*]]>*/function setAnalytics(enabled) {
fetchWithCsrf('api/v1/settings/update-enable-analytics', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(enabled),
})
.then((response) => {
if (response.status === 200) {
console.log('Analytics setting updated successfully');
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
if (typeof CookieConsent !== "undefined") {
if (enabled) {
CookieConsent.acceptCategory(['analytics']);
} else {
CookieConsent.acceptCategory([]);
}
}
} else if (response.status === 208) {
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
} else {
throw new Error('Unexpected response status: ' + response.status);
}
})
.catch((error) => {
console.error('Error updating analytics setting:', error);
alert('An error occurred while updating the analytics setting. Please try again.');
});
}
updateFavoriteIcons();
const contentPath = /*[[${@contextPath}]]*/ '';
const defaultView = localStorage.getItem('defaultView') || 'home'; // Default to "home"
if (defaultView === 'home-legacy') {
window.location.href = contentPath + 'home-legacy'; // Redirect to legacy view
}
document.addEventListener('DOMContentLoaded', function () {
const surveyVersion = '3.0';
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
const dontShowAgain = document.getElementById('dontShowAgain');
const takeSurveyButton = document.getElementById('takeSurvey');
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
// Check if survey version changed and reset page views if it did
const storedVersion = localStorage.getItem('surveyVersion');
if (storedVersion && storedVersion !== surveyVersion) {
localStorage.setItem('pageViews', '0');
localStorage.setItem('surveyVersion', surveyVersion);
}
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
pageViews++;
localStorage.setItem('pageViews', pageViews.toString());
function shouldShowSurvey() {
if(!window.showSurvey) {
return false;
}
if (localStorage.getItem('dontShowSurvey') === 'true' || localStorage.getItem('surveyTaken') === 'true') {
return false;
}
// If survey version changed and we hit a threshold, show the survey
if (localStorage.getItem('surveyVersion') !== surveyVersion && viewThresholds.includes(pageViews)) {
return true;
}
return viewThresholds.includes(pageViews);
}
if (shouldShowSurvey()) {
modal.show();
}
dontShowAgain.addEventListener('change', function () {
if (this.checked) {
localStorage.setItem('dontShowSurvey', 'true');
localStorage.setItem('surveyVersion', surveyVersion);
} else {
localStorage.removeItem('dontShowSurvey');
localStorage.removeItem('surveyVersion');
}
});
if (takeSurveyButton) {
takeSurveyButton.addEventListener('click', function () {
localStorage.setItem('surveyTaken', 'true');
localStorage.setItem('surveyVersion', surveyVersion);
modal.hide();
});
}
if (localStorage.getItem('dontShowSurvey')) {
modal.hide();
}
if (window.location.pathname === '/') {
const navItem = document.getElementById('navItemToHide');
if (navItem) {
navItem.style.display = 'none';
}
}
updateFavoritesDropdown();
});
function setAsDefault(value) {
localStorage.setItem('defaultView', value);
console.log(`Default view set to: ${value}`);
}
function adjustVisibleElements() {
const container = document.querySelector('.recent-features');
if(!container) return;
const subElements = Array.from(container.children);
let totalWidth = 0;
subElements.forEach((element) => {
totalWidth += 12 * parseFloat(getComputedStyle(document.documentElement).fontSize);
if (totalWidth > window.innerWidth) {
element.style.display = 'none';
} else {
element.style.display = 'block';
}
});
}
function adjustContainerAlignment() {
document.querySelectorAll('.features-container').forEach((parent) => {
parent.querySelectorAll('.feature-rows').forEach((container) => {
const containerWidth = parent.offsetWidth;
if (containerWidth < 32 * parseFloat(getComputedStyle(document.documentElement).fontSize)) {
container.classList.add('single-column');
} else {
container.classList.remove('single-column');
}
});
});
}
function toolsManager() {
const convertToPDF = document.querySelector('#groupConvertTo');
const convertFromPDF = document.querySelector('#groupConvertFrom');
if (convertToPDF && convertFromPDF) {
const itemsTo = Array.from(convertToPDF.querySelectorAll('.dropdown-item')).filter(
(item) => !item.querySelector('hr.dropdown-divider')
);
const itemsFrom = Array.from(convertFromPDF.querySelectorAll('.dropdown-item')).filter(
(item) => !item.querySelector('hr.dropdown-divider')
);
const totalItems = itemsTo.length + itemsFrom.length;
if (totalItems > 12) {
document.querySelectorAll('#convertGroup').forEach((element) => element.remove());
document.querySelectorAll('#groupConvertTo').forEach((element) => (element.style.display = 'flex'));
document.querySelectorAll('#groupConvertFrom').forEach((element) => (element.style.display = 'flex'));
} else {
document.querySelectorAll('#convertGroup').forEach((element) => (element.style.display = 'flex'));
document.querySelectorAll('#groupConvertTo').forEach((element) => element.remove());
document.querySelectorAll('#groupConvertFrom').forEach((element) => element.remove());
}
}
}
document.addEventListener('DOMContentLoaded', function () {
toolsManager();
});
window.addEventListener('load', () => {
adjustContainerAlignment();
adjustVisibleElements();
});
window.addEventListener('resize', () => {
adjustContainerAlignment();
adjustVisibleElements();
});
@@ -0,0 +1,138 @@
let pdfCanvas = document.getElementById('cropPdfCanvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
canvasesContainer.style.display = 'none';
// let paginationBtnContainer = ;
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
let btn1Object = document.getElementById('previous-page-btn');
let btn2Object = document.getElementById('next-page-btn');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let fileInput = document.getElementById('fileInput-input');
let file;
let pdfDoc = null;
let pageNumbers = document.getElementById('pageNumbers');
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
let timeId = null; // timeout id for resizing canvases event
btn1Object.addEventListener('click', function (e) {
if (currentPage !== 1) {
currentPage = currentPage - 1;
pageNumbers.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
btn2Object.addEventListener('click', function (e) {
if (currentPage !== totalPages) {
currentPage = currentPage + 1;
pageNumbers.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
function renderPageFromFile(file) {
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pageNumbers.value = currentPage;
};
reader.readAsArrayBuffer(file);
document.getElementById('pagination-button-container').style.display = 'flex';
document.getElementById('instruction-text').style.display = 'block';
}
}
window.addEventListener('resize', function () {
clearTimeout(timeId);
timeId = setTimeout(function () {
if (fileInput.files.length == 0) return;
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
pdfCanvas.width = containerRect.width;
pdfCanvas.height = containerRect.height;
overlayCanvas.width = containerRect.width;
overlayCanvas.height = containerRect.height;
let file = fileInput.files[0];
renderPageFromFile(file);
}, 1000);
});
fileInput.addEventListener('change', function (e) {
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
canvasesContainer.style.display = 'block'; // set for visual purposes
file = e.target.files[0];
renderPageFromFile(file);
}
});
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function (page) {
let viewport = page.getViewport({scale: 1.0});
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = {canvasContext: context, viewport: viewport};
page.render(renderContext);
pdfCanvas.classList.add('shadow-canvas');
});
}
@@ -0,0 +1,389 @@
window.toggleSignatureView = toggleSignatureView;
window.previewSignature = previewSignature;
window.addSignatureFromPreview = addSignatureFromPreview;
window.addDraggableFromPad = addDraggableFromPad;
window.addDraggableFromText = addDraggableFromText;
window.goToFirstOrLastPage = goToFirstOrLastPage;
let currentPreviewSrc = null;
function toggleSignatureView() {
const gridView = document.getElementById("gridView");
const listView = document.getElementById("listView");
const gridText = document.querySelector(".grid-view-text");
const listText = document.querySelector(".list-view-text");
if (gridView.style.display !== "none") {
gridView.style.display = "none";
listView.style.display = "block";
gridText.style.display = "none";
listText.style.display = "inline";
} else {
gridView.style.display = "block";
listView.style.display = "none";
gridText.style.display = "inline";
listText.style.display = "none";
}
}
function previewSignature(element) {
const src = element.dataset.src;
currentPreviewSrc = src;
const filename = element.querySelector(".signature-list-name").textContent;
const previewImage = document.getElementById("previewImage");
const previewFileName = document.getElementById("previewFileName");
previewImage.src = src;
previewFileName.textContent = filename;
const modal = new bootstrap.Modal(
document.getElementById("signaturePreview")
);
modal.show();
}
function addSignatureFromPreview() {
if (currentPreviewSrc) {
DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
bootstrap.Modal.getInstance(
document.getElementById("signaturePreview")
).hide();
}
}
let originalFileName = "";
document
.querySelector("input[name=pdf-upload]")
.addEventListener("change", async (event) => {
const fileInput = event.target;
fileInput.addEventListener("file-input-change", async (e) => {
const { allFiles } = e.detail;
if (allFiles && allFiles.length > 0) {
const file = allFiles[0];
originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc =
"./pdfjs-legacy/pdf.worker.mjs";
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll(".show-on-file-selected").forEach((el) => {
el.style.cssText = "";
});
}
});
});
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".show-on-file-selected").forEach((el) => {
el.style.cssText = "display:none !important";
});
document
.querySelectorAll(".small-file-container-saved img ")
.forEach((img) => {
img.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("fileUrl", img.src);
});
});
document.addEventListener("keydown", (e) => {
if (e.key === "Delete") {
DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted());
}
});
addCustomSelect();
});
function addCustomSelect() {
let customSelectElementContainer =
document.getElementById("signFontSelection");
let originalSelectElement =
customSelectElementContainer.querySelector("select");
let optionsCount = originalSelectElement.length;
let selectedItem = createAndStyleSelectedItem();
customSelectElementContainer.appendChild(selectedItem);
let customSelectionsOptionsContainer = createCustomOptionsContainer();
createAndAddCustomOptions();
customSelectElementContainer.appendChild(customSelectionsOptionsContainer);
selectedItem.addEventListener("click", function (e) {
/* When the select box is clicked, close any other select boxes,
and open/close the current select box: */
e.stopPropagation();
closeAllSelect(this);
this.nextSibling.classList.toggle("select-hide");
this.classList.toggle("select-arrow-active");
});
function createAndAddCustomOptions() {
for (let j = 0; j < optionsCount; j++) {
/* For each option in the original select element,
create a new DIV that will act as an option item: */
let customOptionItem = createAndStyleCustomOption(j);
customOptionItem.addEventListener("click", onCustomOptionClick);
customSelectionsOptionsContainer.appendChild(customOptionItem);
}
}
function createCustomOptionsContainer() {
let customSelectionsOptionsContainer = document.createElement("DIV");
customSelectionsOptionsContainer.setAttribute(
"class",
"select-items select-hide"
);
return customSelectionsOptionsContainer;
}
function createAndStyleSelectedItem() {
let selectedItem = document.createElement("DIV");
selectedItem.setAttribute("class", "select-selected");
selectedItem.innerHTML =
originalSelectElement.options[
originalSelectElement.selectedIndex
].innerHTML;
selectedItem.style.fontFamily = window.getComputedStyle(
originalSelectElement.options[originalSelectElement.selectedIndex]
).fontFamily;
return selectedItem;
}
function onCustomOptionClick(e) {
/* When an item is clicked, update the original select box,
and the selected item: */
let selectElement =
this.parentNode.parentNode.getElementsByTagName("select")[0];
let optionsCount = selectElement.length;
let currentlySelectedCustomOption = this.parentNode.previousSibling;
for (let i = 0; i < optionsCount; i++) {
if (selectElement.options[i].innerHTML == this.innerHTML) {
selectElement.selectedIndex = i;
currentlySelectedCustomOption.innerHTML = this.innerHTML;
currentlySelectedCustomOption.style.fontFamily = this.style.fontFamily;
let previouslySelectedOption =
this.parentNode.getElementsByClassName("same-as-selected");
if (previouslySelectedOption && previouslySelectedOption.length > 0)
previouslySelectedOption[0].classList.remove("same-as-selected");
this.classList.add("same-as-selected");
break;
}
}
currentlySelectedCustomOption.click();
}
function createAndStyleCustomOption(j) {
let customOptionItem = document.createElement("DIV");
customOptionItem.innerHTML = originalSelectElement.options[j].innerHTML;
customOptionItem.classList.add(originalSelectElement.options[j].className);
customOptionItem.style.fontFamily = window.getComputedStyle(
originalSelectElement.options[j]
).fontFamily;
if (j == originalSelectElement.selectedIndex)
customOptionItem.classList.add("same-as-selected");
return customOptionItem;
}
function closeAllSelect(element) {
/* A function that will close all select boxes in the document,
except the current select box: */
let allSelectedOptions = document.getElementsByClassName("select-selected");
let allSelectedOptionsCount = allSelectedOptions.length;
let indicesOfContainersToHide = [];
for (let i = 0; i < allSelectedOptionsCount; i++) {
if (element == allSelectedOptions[i]) {
indicesOfContainersToHide.push(i);
} else {
allSelectedOptions[i].classList.remove("select-arrow-active");
}
}
hideOptionsContainers(indicesOfContainersToHide);
}
/* If the user clicks anywhere outside the select box,
then close all select boxes: */
document.addEventListener("click", closeAllSelect);
function hideOptionsContainers(containersIndices) {
let allOptionsContainers = document.getElementsByClassName("select-items");
let allSelectionListsContainerCount = allOptionsContainers.length;
for (let i = 0; i < allSelectionListsContainerCount; i++) {
if (containersIndices.indexOf(i)) {
allOptionsContainers[i].classList.add("select-hide");
}
}
}
}
const imageUpload = document.querySelector("input[name=image-upload]");
imageUpload.addEventListener("change", (e) => {
if (!e.target.files) return;
for (const imageFile of e.target.files) {
var reader = new FileReader();
reader.readAsDataURL(imageFile);
reader.onloadend = function (e) {
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
};
}
});
const signaturePadCanvas = document.getElementById("drawing-pad-canvas");
const signaturePad = new SignaturePad(signaturePadCanvas, {
minWidth: 1,
maxWidth: 2,
penColor: "black",
});
function addDraggableFromPad() {
if (signaturePad.isEmpty()) return;
const startTime = Date.now();
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
console.log(Date.now() - startTime);
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
}
function getCroppedCanvasDataUrl(canvas) {
let originalCtx = canvas.getContext("2d");
let originalWidth = canvas.width;
let originalHeight = canvas.height;
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
let minX = originalWidth + 1,
maxX = -1,
minY = originalHeight + 1,
maxY = -1,
x = 0,
y = 0,
currentPixelColorValueIndex;
for (y = 0; y < originalHeight; y++) {
for (x = 0; x < originalWidth; x++) {
currentPixelColorValueIndex = (y * originalWidth + x) * 4;
let currentPixelAlphaValue =
imageData.data[currentPixelColorValueIndex + 3];
if (currentPixelAlphaValue > 0) {
if (minX > x) minX = x;
if (maxX < x) maxX = x;
if (minY > y) minY = y;
if (maxY < y) maxY = y;
}
}
}
let croppedWidth = maxX - minX;
let croppedHeight = maxY - minY;
if (croppedWidth < 0 || croppedHeight < 0) return null;
let cuttedImageData = originalCtx.getImageData(
minX,
minY,
croppedWidth,
croppedHeight
);
let croppedCanvas = document.createElement("canvas"),
croppedCtx = croppedCanvas.getContext("2d");
croppedCanvas.width = croppedWidth;
croppedCanvas.height = croppedHeight;
croppedCtx.putImageData(cuttedImageData, 0, 0);
return croppedCanvas.toDataURL();
}
function resizeCanvas() {
var ratio = Math.max(window.devicePixelRatio || 1, 1);
var additionalFactor = 10;
signaturePadCanvas.width =
signaturePadCanvas.offsetWidth * ratio * additionalFactor;
signaturePadCanvas.height =
signaturePadCanvas.offsetHeight * ratio * additionalFactor;
signaturePadCanvas
.getContext("2d")
.scale(ratio * additionalFactor, ratio * additionalFactor);
signaturePad.clear();
}
new IntersectionObserver((entries, observer) => {
if (entries.some((entry) => entry.intersectionRatio > 0)) {
resizeCanvas();
}
}).observe(signaturePadCanvas);
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
function addDraggableFromText() {
const sigText = document.getElementById("sigText").value;
const font = document.querySelector("select[name=font]").value;
const fontSize = 100;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.font = `${fontSize}px ${font}`;
const textWidth = ctx.measureText(sigText).width;
const textHeight = fontSize;
let paragraphs = sigText.split(/\r?\n/);
canvas.width = textWidth;
canvas.height = paragraphs.length * textHeight * 1.35; // for tails
ctx.font = `${fontSize}px ${font}`;
ctx.textBaseline = "top";
let y = 0;
paragraphs.forEach((paragraph) => {
ctx.fillText(paragraph, 0, y);
y += fontSize;
});
const dataURL = canvas.toDataURL();
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
}
async function goToFirstOrLastPage(page) {
if (page) {
const lastPage = DraggableUtils.pdfDoc.numPages;
await DraggableUtils.goToPage(lastPage - 1);
} else {
await DraggableUtils.goToPage(0);
}
}
document.getElementById("download-pdf").addEventListener("click", async () => {
const downloadButton = document.getElementById("download-pdf");
const originalContent = downloadButton.innerHTML;
downloadButton.disabled = true;
downloadButton.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
`;
try {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: "application/pdf" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = originalFileName + "_signed.pdf";
link.click();
} catch (error) {
console.error("Error downloading PDF:", error);
} finally {
downloadButton.disabled = false;
downloadButton.innerHTML = originalContent;
}
});