mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-01-10 00:06:51 +01:00
Feature/improved signature element (#2489)
# Description Please provide a summary of the changes, including relevant motivation and context. Closes #(issue_number) ## Checklist - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have performed a self-review of my own code - [ ] I have attached images of the change if it is UI based - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] If my code has heavily changed functionality I have updated relevant docs on [Stirling-PDFs doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) - [ ] My changes generate no new warnings - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Co-authored-by: Reece Browne <reece@stirling.pdf>
This commit is contained in:
parent
715efca25d
commit
49fb634690
@ -831,7 +831,7 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
#repair
|
||||
repair.title=Repair
|
||||
repair.header=Repair PDFs
|
||||
@ -1285,6 +1285,8 @@ splitByChapters.submit=Split PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
@ -2,22 +2,32 @@
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#pdf-canvas {
|
||||
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.draggable-buttons-box {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
width: calc(100% + 4.4rem);
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
z-index: 5;
|
||||
margin-left: -2.2rem;
|
||||
}
|
||||
.draggable-buttons-box > button {
|
||||
z-index: 10;
|
||||
|
||||
.draggable-buttons-box>button {
|
||||
z-index: 4;
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
flex: 1 1 auto;
|
||||
min-width: 2.5rem;
|
||||
max-width: 4rem;
|
||||
}
|
||||
|
||||
|
||||
.draggable-canvas {
|
||||
border: 1px solid red;
|
||||
position: absolute;
|
||||
@ -26,3 +36,20 @@
|
||||
top: 0px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-with-icon .icon {
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
pointer-events: none;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.input-with-icon input {
|
||||
padding-left: 2.2rem;
|
||||
}
|
||||
|
@ -118,6 +118,7 @@
|
||||
row-gap: 1px;
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
top:4px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
@ -165,8 +166,8 @@
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
|
||||
right: 10px;
|
||||
top: -5px;
|
||||
right: 0px;
|
||||
top: -17px;
|
||||
}
|
||||
|
||||
.remove-selected-file * {
|
||||
@ -219,3 +220,54 @@
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgb(105, 116, 134, 0.5);
|
||||
}
|
||||
|
||||
.draggable-image-overlay{
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
z-index: 10;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
left:0;
|
||||
top:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.small-file-container:hover .drag-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#imagePreviewModal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
@ -22,18 +22,38 @@ select#font-select option {
|
||||
}
|
||||
|
||||
.draggable-buttons-box {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
width: calc(100% + 4.4rem);
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
z-index: 5;
|
||||
margin-left: -2.2rem;
|
||||
}
|
||||
|
||||
.draggable-buttons-box>button {
|
||||
z-index: 4;
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
flex: 1 1 auto;
|
||||
min-width: 2.5rem;
|
||||
max-width: 4rem;
|
||||
}
|
||||
|
||||
|
||||
.rotation-handle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #3498db;
|
||||
background-color: rgba(52, 152, 219, 0.1);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
position: absolute;
|
||||
cursor: grab;
|
||||
top: -30px;
|
||||
left: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.draggable-canvas {
|
||||
@ -113,3 +133,33 @@ select#font-select option {
|
||||
text-align: right;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-with-icon .icon {
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
pointer-events: none;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.input-with-icon input {
|
||||
padding-left: 2.2rem;
|
||||
}
|
||||
|
||||
.small-file-container-saved {
|
||||
padding-top: 1px;
|
||||
position: relative;
|
||||
row-gap: 1px;
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.small-file-container-saved:hover .drag-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -7,75 +7,137 @@ const DraggableUtils = {
|
||||
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;
|
||||
const x = (parseFloat(target.getAttribute('data-bs-x')) || 0) + event.dx;
|
||||
const y = (parseFloat(target.getAttribute('data-bs-y')) || 0) + event.dy;
|
||||
|
||||
// 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);
|
||||
|
||||
this.onInteraction(target);
|
||||
//update the last interacted element
|
||||
this.lastInteracted = event.target;
|
||||
// Set the last interacted element
|
||||
this.lastInteracted = target;
|
||||
},
|
||||
},
|
||||
})
|
||||
.resizable({
|
||||
edges: {left: true, right: true, bottom: true, top: true},
|
||||
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) => {
|
||||
var target = event.target;
|
||||
var x = parseFloat(target.getAttribute('data-bs-x')) || 0;
|
||||
var y = parseFloat(target.getAttribute('data-bs-y')) || 0;
|
||||
const target = event.target;
|
||||
|
||||
// check if control key is pressed
|
||||
if (event.ctrlKey) {
|
||||
const aspectRatio = target.offsetWidth / target.offsetHeight;
|
||||
// preserve aspect ratio
|
||||
let width = event.rect.width;
|
||||
let height = event.rect.height;
|
||||
const MAX_CHANGE = 60;
|
||||
|
||||
if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) {
|
||||
height = width / aspectRatio;
|
||||
} else {
|
||||
width = height * aspectRatio;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
event.rect.width = width;
|
||||
event.rect.height = height;
|
||||
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;
|
||||
}
|
||||
|
||||
target.style.width = event.rect.width + 'px';
|
||||
target.style.height = event.rect.height + 'px';
|
||||
|
||||
// translate when resizing from top or left edges
|
||||
x += event.deltaRect.left;
|
||||
y += event.deltaRect.top;
|
||||
|
||||
target.style.transform = 'translate(' + x + 'px,' + y + 'px)';
|
||||
|
||||
target.setAttribute('data-bs-x', x);
|
||||
target.setAttribute('data-bs-y', y);
|
||||
target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height);
|
||||
|
||||
this.onInteraction(target);
|
||||
},
|
||||
},
|
||||
|
||||
modifiers: [
|
||||
interact.modifiers.restrictSize({
|
||||
min: {width: 5, height: 5},
|
||||
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) => {
|
||||
@ -117,7 +179,8 @@ const DraggableUtils = {
|
||||
}
|
||||
|
||||
// Update position
|
||||
target.style.transform = `translate(${x}px, ${y}px)`;
|
||||
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);
|
||||
|
||||
@ -125,72 +188,97 @@ const DraggableUtils = {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onInteraction(target) {
|
||||
this.boxDragContainer.appendChild(target);
|
||||
},
|
||||
|
||||
createDraggableCanvas() {
|
||||
const createdCanvas = document.createElement('canvas');
|
||||
createdCanvas.id = `draggable-canvas-${this.nextId++}`;
|
||||
createdCanvas.classList.add('draggable-canvas');
|
||||
|
||||
const x = 0;
|
||||
const y = 20;
|
||||
createdCanvas.style.transform = `translate(${x}px, ${y}px)`;
|
||||
createdCanvas.setAttribute('data-bs-x', x);
|
||||
createdCanvas.setAttribute('data-bs-y', y);
|
||||
|
||||
//Click element in order to enable arrow keys
|
||||
createdCanvas.addEventListener('click', () => {
|
||||
this.lastInteracted = createdCanvas;
|
||||
});
|
||||
|
||||
createdCanvas.onclick = (e) => this.onInteraction(e.target);
|
||||
|
||||
this.boxDragContainer.appendChild(createdCanvas);
|
||||
|
||||
//Enable Arrow keys directly after the element is created
|
||||
this.lastInteracted = createdCanvas;
|
||||
|
||||
return createdCanvas;
|
||||
this.lastInteracted = target;
|
||||
// this.boxDragContainer.appendChild(target);
|
||||
// target.appendChild(target.querySelector(".display-canvas"));
|
||||
},
|
||||
createDraggableCanvasFromUrl(dataUrl) {
|
||||
return new Promise((resolve) => {
|
||||
var myImage = new Image();
|
||||
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 = () => {
|
||||
var createdCanvas = this.createDraggableCanvas();
|
||||
const context = createdCanvas.getContext('2d');
|
||||
|
||||
createdCanvas.width = myImage.width;
|
||||
createdCanvas.height = myImage.height;
|
||||
|
||||
const imgAspect = myImage.width / myImage.height;
|
||||
const pdfAspect = this.boxDragContainer.offsetWidth / this.boxDragContainer.offsetHeight;
|
||||
const containerWidth = this.boxDragContainer.offsetWidth;
|
||||
const containerHeight = this.boxDragContainer.offsetHeight;
|
||||
|
||||
var scaleMultiplier;
|
||||
if (imgAspect > pdfAspect) {
|
||||
scaleMultiplier = this.boxDragContainer.offsetWidth / myImage.width;
|
||||
} else {
|
||||
scaleMultiplier = this.boxDragContainer.offsetHeight / myImage.height;
|
||||
}
|
||||
let scaleMultiplier = Math.min(containerWidth / myImage.width, containerHeight / myImage.height);
|
||||
const scaleFactor = 0.5;
|
||||
|
||||
var newWidth = createdCanvas.width;
|
||||
var newHeight = createdCanvas.height;
|
||||
if (scaleMultiplier < 1) {
|
||||
newWidth = newWidth * scaleMultiplier;
|
||||
newHeight = newHeight * scaleMultiplier;
|
||||
}
|
||||
const newWidth = myImage.width * scaleMultiplier * scaleFactor;
|
||||
const newHeight = myImage.height * scaleMultiplier * scaleFactor;
|
||||
|
||||
createdCanvas.style.width = newWidth + 'px';
|
||||
createdCanvas.style.height = newHeight + 'px';
|
||||
// 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;
|
||||
|
||||
var myContext = createdCanvas.getContext('2d');
|
||||
myContext.drawImage(myImage, 0, 0);
|
||||
resolve(createdCanvas);
|
||||
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());
|
||||
},
|
||||
@ -266,9 +354,61 @@ const DraggableUtils = {
|
||||
}
|
||||
},
|
||||
getLastInteracted() {
|
||||
return this.boxDragContainer.querySelector('.draggable-canvas:last-of-type');
|
||||
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) {
|
||||
@ -325,7 +465,7 @@ const DraggableUtils = {
|
||||
// render the page onto the canvas
|
||||
var renderContext = {
|
||||
canvasContext: this.pdfCanvas.getContext('2d'),
|
||||
viewport: page.getViewport({scale: 1}),
|
||||
viewport: page.getViewport({ scale: 1 }),
|
||||
};
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
@ -352,8 +492,6 @@ const DraggableUtils = {
|
||||
this.loadPageContents();
|
||||
}
|
||||
},
|
||||
|
||||
parseTransform(element) {},
|
||||
async getOverlayedPdfDocument() {
|
||||
const pdfBytes = await this.pdfDoc.getData();
|
||||
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, {
|
||||
@ -367,7 +505,6 @@ const DraggableUtils = {
|
||||
if (pageIdx.includes('offset')) {
|
||||
continue;
|
||||
}
|
||||
console.log(typeof pageIdx);
|
||||
|
||||
const page = pdfDocModified.getPage(parseInt(pageIdx));
|
||||
let draggablesData = pagesMap[pageIdx];
|
||||
@ -376,45 +513,61 @@ const DraggableUtils = {
|
||||
const offsetHeight = pagesMap[pageIdx + '-offsetHeight'];
|
||||
|
||||
for (const draggableData of draggablesData) {
|
||||
// embed the draggable canvas
|
||||
const draggableElement = draggableData.element;
|
||||
// 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);
|
||||
|
||||
// calculate the position in the pdf document
|
||||
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
|
||||
const transformComponents = tansform.split(',');
|
||||
// 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: parseFloat(transformComponents[0]),
|
||||
y: parseFloat(transformComponents[1]),
|
||||
width: draggableData.offsetWidth,
|
||||
height: draggableData.offsetHeight,
|
||||
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
|
||||
};
|
||||
|
||||
//Auxiliary variables
|
||||
// Auxiliary variables
|
||||
let widthAdjusted = page.getWidth();
|
||||
let heightAdjusted = page.getHeight();
|
||||
const rotation = page.getRotation();
|
||||
|
||||
//Normalizing angle
|
||||
// Normalize page rotation angle
|
||||
let normalizedAngle = rotation.angle % 360;
|
||||
if (normalizedAngle < 0) {
|
||||
normalizedAngle += 360;
|
||||
}
|
||||
|
||||
//Changing the page dimension if the angle is 90 or 270
|
||||
// Adjust page dimensions for rotated pages
|
||||
if (normalizedAngle === 90 || normalizedAngle === 270) {
|
||||
let widthTemp = widthAdjusted;
|
||||
widthAdjusted = heightAdjusted;
|
||||
heightAdjusted = widthTemp;
|
||||
[widthAdjusted, heightAdjusted] = [heightAdjusted, widthAdjusted];
|
||||
}
|
||||
|
||||
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 * widthAdjusted,
|
||||
y: draggablePositionRelative.y * heightAdjusted,
|
||||
@ -422,11 +575,13 @@ const DraggableUtils = {
|
||||
height: draggablePositionRelative.height * heightAdjusted,
|
||||
};
|
||||
|
||||
//Defining the image if the page has a 0-degree angle
|
||||
// Calculate position based on normalized page rotation
|
||||
let x = draggablePositionPdf.x;
|
||||
let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height;
|
||||
|
||||
//Defining the image position if it is at other angles
|
||||
let originx = x + draggablePositionPdf.width / 2;
|
||||
let originy = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height / 2;
|
||||
|
||||
if (normalizedAngle === 90) {
|
||||
x = draggablePositionPdf.y + draggablePositionPdf.height;
|
||||
y = draggablePositionPdf.x;
|
||||
@ -437,17 +592,32 @@ const DraggableUtils = {
|
||||
x = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height;
|
||||
y = widthAdjusted - draggablePositionPdf.x;
|
||||
}
|
||||
|
||||
// draw the image
|
||||
// let angle = draggablePositionPixels.angle % 360;
|
||||
// if (angle < 0) angle += 360; // Normalize to positive angle
|
||||
const radians = -draggablePositionPixels.angle; // Convert angle to radians
|
||||
page.pushOperators(
|
||||
PDFLib.pushGraphicsState(),
|
||||
PDFLib.concatTransformationMatrix(1, 0, 0, 1, originx, originy),
|
||||
PDFLib.concatTransformationMatrix(
|
||||
Math.cos(radians),
|
||||
Math.sin(radians),
|
||||
-Math.sin(radians),
|
||||
Math.cos(radians),
|
||||
0,
|
||||
0
|
||||
),
|
||||
PDFLib.concatTransformationMatrix(1, 0, 0, 1, -1 * originx, -1 * originy)
|
||||
);
|
||||
page.drawImage(pdfImageObject, {
|
||||
x: x,
|
||||
y: y,
|
||||
width: draggablePositionPdf.width,
|
||||
height: draggablePositionPdf.height,
|
||||
rotate: rotation,
|
||||
});
|
||||
page.pushOperators(PDFLib.popGraphicsState());
|
||||
}
|
||||
}
|
||||
|
||||
this.loadPageContents();
|
||||
return pdfDocModified;
|
||||
},
|
||||
|
@ -9,6 +9,7 @@ if (!isScriptExecuted) {
|
||||
document.querySelectorAll('.custom-file-chooser').forEach(setupFileInput);
|
||||
});
|
||||
}
|
||||
let hasDroppedImage = false;
|
||||
|
||||
function setupFileInput(chooser) {
|
||||
const elementId = chooser.getAttribute('data-bs-element-id');
|
||||
@ -18,6 +19,11 @@ function setupFileInput(chooser) {
|
||||
|
||||
let inputContainer = document.getElementById(inputContainerId);
|
||||
|
||||
if (inputContainer.id === 'pdf-upload-input-container') {
|
||||
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropPDF;
|
||||
} else if (inputContainer.id === 'image-upload-input-container') {
|
||||
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropImage;
|
||||
}
|
||||
let allFiles = [];
|
||||
let overlay;
|
||||
let dragCounter = 0;
|
||||
@ -141,12 +147,17 @@ function setupFileInput(chooser) {
|
||||
files.forEach((file) => dataTransfer.items.add(file));
|
||||
return dataTransfer;
|
||||
}
|
||||
|
||||
function handleFileInputChange(inputElement) {
|
||||
const files = allFiles;
|
||||
showOrHideSelectedFilesContainer(files);
|
||||
|
||||
const filesInfo = files.map((f) => ({name: f.name, size: f.size, uniqueId: f.uniqueId}));
|
||||
const filesInfo = files.map((f) => ({
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
uniqueId: f.uniqueId,
|
||||
type: f.type,
|
||||
url: URL.createObjectURL(f),
|
||||
}));
|
||||
|
||||
const selectedFilesContainer = $(inputContainer).siblings('.selected-files');
|
||||
selectedFilesContainer.empty();
|
||||
@ -157,30 +168,111 @@ function setupFileInput(chooser) {
|
||||
$(fileContainer).addClass(fileContainerClasses);
|
||||
$(fileContainer).attr('id', info.uniqueId);
|
||||
|
||||
let fileIconContainer = createFileIconContainer(info);
|
||||
let fileIconContainer = document.createElement('div');
|
||||
const isDragAndDropEnabled =
|
||||
window.location.pathname.includes('add-image') || window.location.pathname.includes('sign');
|
||||
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);
|
||||
|
||||
let removeBtn = document.createElement('div');
|
||||
removeBtn.classList.add('remove-selected-file');
|
||||
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(fileIconContainer);
|
||||
$(fileContainer).append(fileInfoContainer);
|
||||
$(fileContainer).append(removeBtn);
|
||||
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();
|
||||
});
|
||||
|
||||
showOrHideSelectedFilesContainer(filesInfo);
|
||||
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 (files && files.length > 0) chooser.style.setProperty('--selected-files-display', 'flex');
|
||||
else chooser.style.setProperty('--selected-files-display', 'none');
|
||||
if (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) {
|
||||
@ -235,4 +327,52 @@ function setupFileInput(chooser) {
|
||||
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,23 +1,38 @@
|
||||
window.goToFirstOrLastPage = goToFirstOrLastPage;
|
||||
|
||||
document.getElementById('download-pdf').addEventListener('click', async () => {
|
||||
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();
|
||||
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;
|
||||
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;
|
||||
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
await DraggableUtils.renderPage(pdfDoc, 0);
|
||||
|
||||
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
|
||||
@ -30,6 +45,11 @@ 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]');
|
||||
@ -45,3 +65,12 @@ imageUpload.addEventListener('change', (e) => {
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function goToFirstOrLastPage(page) {
|
||||
if (page) {
|
||||
const lastPage = DraggableUtils.pdfDoc.numPages;
|
||||
await DraggableUtils.goToPage(lastPage - 1);
|
||||
} else {
|
||||
await DraggableUtils.goToPage(0);
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,16 @@ 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());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const imageUpload = document.querySelector('input[name=image-upload]');
|
||||
@ -203,11 +213,26 @@ async function goToFirstOrLastPage(page) {
|
||||
}
|
||||
|
||||
document.getElementById('download-pdf').addEventListener('click', async () => {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
85
src/main/resources/static/js/sign/signature-canvas.js
Normal file
85
src/main/resources/static/js/sign/signature-canvas.js
Normal file
@ -0,0 +1,85 @@
|
||||
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 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();
|
||||
}
|
||||
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
if (entries.some((entry) => entry.intersectionRatio > 0)) {
|
||||
resizeCanvas();
|
||||
}
|
||||
}).observe(signaturePadCanvas);
|
||||
|
||||
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
|
@ -213,7 +213,10 @@
|
||||
unexpectedError: '[[#{decrypt.unexpectedError}]]',
|
||||
serverError: '[[#{decrypt.serverError}]]',
|
||||
success: '[[#{decrypt.success}]]',
|
||||
};</script>
|
||||
};
|
||||
window.fileInput = {
|
||||
dragAndDropPDF : '[[#{fileChooser.dragAndDropPDF}]]',
|
||||
dragAndDropImage : '[[#{fileChooser.dragAndDropImage}]]'};</script>
|
||||
<div class="custom-file-chooser mb-3" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
|
||||
<div class="mb-3 d-flex flex-row justify-content-center align-items-center flex-wrap input-container" th:name="${name}+'-input'" th:id="${name}+'-input-container'" th:data-text="#{fileChooser.hoveredDragAndDrop}">
|
||||
<label class="file-input-btn d-none">
|
||||
@ -222,7 +225,7 @@
|
||||
</label>
|
||||
<div th:text="#{fileChooser.click}"></div>
|
||||
<div th:text="#{fileChooser.or}"></div>
|
||||
<div th:text="#{fileChooser.dragAndDrop}"></div>
|
||||
<div th:text="#{fileChooser.dragAndDrop}" id="dragAndDrop"></div>
|
||||
</div>
|
||||
<div class="selected-files flex-wrap"></div>
|
||||
</div>
|
||||
|
@ -1,65 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{addImage.title}, header=#{addImage.header})}"></th:block>
|
||||
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
||||
<link rel="stylesheet" th:href="@{'/css/add-image.css'}">
|
||||
</head>
|
||||
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
||||
<link rel="stylesheet" th:href="@{'/css/add-image.css'}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">add_photo_alternate</span>
|
||||
<span class="tool-header-text" th:text="#{addImage.header}"></span>
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">add_photo_alternate</span>
|
||||
<span class="tool-header-text" th:text="#{addImage.header}"></span>
|
||||
</div>
|
||||
|
||||
<!-- pdf selector -->
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/add-image.js'}"></script>
|
||||
<div class="tab-group show-on-file-selected">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- pdf selector -->
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/add-image.js'}"></script>
|
||||
<div class="tab-group show-on-file-selected">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
|
||||
</div>
|
||||
|
||||
<!-- draggables box -->
|
||||
<div id="box-drag-container" class="show-on-file-selected">
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
||||
<div class="draggable-buttons-box ignore-rtl">
|
||||
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
|
||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="draggable-buttons-box ignore-rtl">
|
||||
<button class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())"
|
||||
style="color: #C02223; border-color: #C02223; background-color: rgba(255, 0, 0, 0.1);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
|
||||
viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
|
||||
<path
|
||||
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.delete}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.addAllPagesDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<span class="material-symbols-rounded">
|
||||
content_copy
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.addToAll}"></span>
|
||||
</button>
|
||||
<div id="rotation-controls" class="align-items-center" style="display: none;">
|
||||
<div class="input-with-icon">
|
||||
<span class="material-symbols-rounded icon" style="margin-right: 3px;">
|
||||
screen_rotation
|
||||
</span>
|
||||
<input type="number" id="rotation-input" class="form-control form-control-sm me-2" value="0" step="10"
|
||||
style="width: 6rem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- download button -->
|
||||
<div class="margin-auto-parent">
|
||||
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center" th:text="#{downloadPdf}"></button>
|
||||
</div>
|
||||
<button id="ratioToggleBtn" class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.toggleMaintainRatio()">
|
||||
<span class="material-symbols-rounded">
|
||||
Aspect_Ratio
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.maintainRatio}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="goToFirstOrLastPage(false)" style="margin-left:auto">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_left
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.first}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="incrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.previous}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="decrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.next}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="goToFirstOrLastPage(true)">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_right
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.last}"></span>
|
||||
</button>
|
||||
<button id="download-pdf" class="btn btn-outline-secondary"
|
||||
style="color: green; border-color: green; background: rgba(0, 128, 0, 0.2)">
|
||||
<span class="material-symbols-rounded">
|
||||
download
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{downloadPdf}"></span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- draggables box -->
|
||||
<div id="box-drag-container" class="show-on-file-selected">
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -21,8 +21,6 @@
|
||||
</th:block>
|
||||
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
|
||||
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/sign.js'}"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -32,7 +30,7 @@
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="col-md-7 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon sign">signature</span>
|
||||
<span class="tool-header-text" th:text="#{sign.header}"></span>
|
||||
@ -46,7 +44,7 @@
|
||||
<div class="tab-group show-on-file-selected">
|
||||
<div class="tab-container" th:title="#{sign.upload}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
|
||||
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -61,13 +59,6 @@
|
||||
|
||||
<div class="tab-container" th:title="#{sign.saved}">
|
||||
<div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}">
|
||||
<!-- View Toggle Button -->
|
||||
<div class="view-toggle mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="toggleSignatureView()">
|
||||
<span class="material-symbols-rounded grid-view-text">view_list</span>
|
||||
<span class="material-symbols-rounded list-view-text" style="display: none;">grid_view</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<div class="modal fade" id="signaturePreview" tabindex="-1">
|
||||
@ -89,66 +80,49 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
<div id="gridView">
|
||||
<!-- Personal Signatures -->
|
||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
||||
<h5 th:text="#{sign.personalSigs}"></h5>
|
||||
<div class="signature-grid">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}" class="signature-item">
|
||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
|
||||
th:data-filename="${sig.fileName}" style="max-width: 200px; cursor: pointer;"
|
||||
onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)" />
|
||||
<!-- Personal Signatures -->
|
||||
<div th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
||||
<h5 th:text="#{sign.personalSigs}"></h5>
|
||||
<div class="selected-files flex-wrap" style="position: relative; display:flex">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}"
|
||||
class="small-file-container-saved d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="drag-icon"><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">
|
||||
</path>
|
||||
</svg></div>
|
||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
|
||||
th:data-filename="${sig.fileName}" style="width: 50px; height: 50px; object-fit: cover;" />
|
||||
<div class="file-info d-flex flex-column align-items-center justify-content-center">
|
||||
<div class="signature-name" th:text="${sig.fileName}"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared Signatures -->
|
||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
||||
<h5 th:text="#{sign.sharedSigs}"></h5>
|
||||
<div class="signature-grid">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}" class="signature-item">
|
||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
|
||||
th:data-filename="${sig.fileName}" style="max-width: 200px; cursor: pointer;"
|
||||
onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)" />
|
||||
<!-- Shared Signatures -->
|
||||
<div th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
||||
<h5 th:text="#{sign.sharedSigs}"></h5>
|
||||
<div class="selected-files flex-wrap" style="position: relative; display: flex;">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}"
|
||||
class="small-file-container-saved d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="drag-icon"><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">
|
||||
</path>
|
||||
</svg></div>
|
||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
|
||||
th:data-filename="${sig.fileName}" style="width: 50px; height: 50px; object-fit: cover;" />
|
||||
<div class="file-info d-flex flex-column align-items-center justify-content-center">
|
||||
<div class="signature-name" th:text="${sig.fileName}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View (Initially Hidden) -->
|
||||
<div id="listView" style="display: none;">
|
||||
<!-- Personal Signatures -->
|
||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
||||
<h5 th:text="#{sign.personalSigs}"></h5>
|
||||
<div class="signature-list">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}"
|
||||
class="signature-list-item" th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
|
||||
onclick="previewSignature(this)">
|
||||
<div class="signature-list-info">
|
||||
<span th:text="${sig.fileName}" class="signature-list-name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared Signatures -->
|
||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
||||
<h5 th:text="#{sign.sharedSigs}"></h5>
|
||||
<div class="signature-list">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}"
|
||||
class="signature-list-item" th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
|
||||
onclick="previewSignature(this)">
|
||||
<div class="signature-list-info">
|
||||
<span th:text="${sig.fileName}" class="signature-list-name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${#lists.isEmpty(signatures)}" class="text-center p-3">
|
||||
<p th:text="#{sign.noSavedSigs}">No saved signatures found</p>
|
||||
@ -169,72 +143,99 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/sign/signature-canvas.js'}"></script>
|
||||
<!-- draggables box -->
|
||||
<div id="box-drag-container" class="show-on-file-selected">
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
||||
<div class="draggable-buttons-box ignore-rtl">
|
||||
<button class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
|
||||
viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
|
||||
<path
|
||||
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.delete}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.addAllPagesDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<span class="material-symbols-rounded">
|
||||
content_copy
|
||||
<div class="draggable-buttons-box ignore-rtl">
|
||||
<button class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())"
|
||||
style="border-color: #C02223; color: #C02223; background: rgba(255, 0, 0, 0.1)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
|
||||
viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
|
||||
<path
|
||||
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.delete}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.addAllPagesDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<span class="material-symbols-rounded">
|
||||
content_copy
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.addToAll}"></span>
|
||||
</button>
|
||||
<div id="rotation-controls" class="align-items-center" style="display: none;">
|
||||
<div class="input-with-icon">
|
||||
<span class="material-symbols-rounded icon" style="margin-right: 3px;">
|
||||
screen_rotation
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.addToAll}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="goToFirstOrLastPage(false)" style="margin-left:auto">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_left
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.first}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="incrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.previous}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="decrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.next}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="goToFirstOrLastPage(true)">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_right
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.last}"></span>
|
||||
</button>
|
||||
<input type="number" id="rotation-input" class="form-control form-control-sm me-2" value="0" step="10"
|
||||
style="width: 6rem" />
|
||||
</div>
|
||||
</div>
|
||||
<button id="ratioToggleBtn" class="btn btn-outline-secondary"
|
||||
onclick="DraggableUtils.toggleMaintainRatio()">
|
||||
<span class="material-symbols-rounded">
|
||||
Aspect_Ratio
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.maintainRatio}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="goToFirstOrLastPage(false)" style="margin-left:auto">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_left
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.first}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="incrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.previous}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="decrementPage"
|
||||
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<span class="btn-tooltip" th:text="#{sign.next}"></span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="goToFirstOrLastPage(true)">
|
||||
<span class="material-symbols-rounded">
|
||||
keyboard_double_arrow_right
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{sign.last}"></span>
|
||||
</button>
|
||||
<button id="download-pdf" class="btn btn-outline-secondary"
|
||||
style="color: green;border-color: green; background: rgba(0, 128, 0, 0.2);">
|
||||
<span class="material-symbols-rounded">
|
||||
download
|
||||
</span>
|
||||
<span class="btn-tooltip" th:text="#{downloadPdf}"></span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- download button -->
|
||||
<div class="margin-auto-parent">
|
||||
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center"
|
||||
th:text="#{downloadPdf}"></button>
|
||||
<div id="box-drag-container" class="show-on-file-selected">
|
||||
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fabric@latest/dist/index.min.js"></script>
|
||||
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
||||
<script type="module" th:src="@{'/js/pages/sign.js'}"></script>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user