Fix displayed identical fonts in sign PDF (#2751)

# Description of Changes

### Changes:
- Add a new custom select to display fonts correctly and to allow more
styling flexibility in Sign PDF feature by hiding the original
`<select>` element (`display: none;`) and wrap it with a `<div>` and
then create a custom selection menu using `<div>`s and CSS to achieve
the required results due to the limitations of `<select>` and `<option>`
while still preserving the hidden `<select>` for form submission.

### Why was the change made?
1. A bug that caused font families to not be displayed in Firefox.
2. Select/Option element are not flexible when it comes to styling
(compared to DIVs for example) but bullet point `1.` is of higher
priority.

### UI Changes:
- Dark Mode:
   - Before:

![image](https://github.com/user-attachments/assets/37f79c81-8155-4430-9e36-2b4cc2a442e6)

   - After:

![image](https://github.com/user-attachments/assets/e6a2b209-0d8f-4ff2-94ea-54827706d0cd)

- Light Mode:
   - Before:

![image](https://github.com/user-attachments/assets/c5356899-6be9-497b-8ad9-d50e6bd077d5)

   - After: 

![image](https://github.com/user-attachments/assets/29373b1a-cfa1-48a2-9040-3ed2fd5f7fd3)

Note:
- Changes in `sign.js` are between the lines 95-228, as it seems the
file was auto-formatted affecting whitespaces and
single_quotes/double_quotes.


#### Useful quotes from MDN:

> [Styling with
CSS](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option#styling_with_css)
Styling the <option> element is highly limited. Options don't inherit
the font set on the parent. In Firefox, only
[color](https://developer.mozilla.org/en-US/docs/Web/CSS/color) and
[background-color](https://developer.mozilla.org/en-US/docs/Web/CSS/background-color)
can be set, however in Chrome and Safari it's not possible to set any
properties. You can find more details about styling in [our guide to
advanced form
styling](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Advanced_form_styling).

#### Useful references:
- [Option
Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option#styling_with_css)
- [Select
Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)
- [Advanced Form
Styling](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Advanced_form_styling)

Closes #1575

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [x] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Omar Ahmed Hassan 2025-01-20 14:11:31 +02:00 committed by GitHub
parent 8353c399d2
commit b4451da2f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 302 additions and 76 deletions

View File

@ -32,7 +32,7 @@ select#font-select option {
margin-left: -2.2rem; margin-left: -2.2rem;
} }
.draggable-buttons-box>button { .draggable-buttons-box > button {
z-index: 4; z-index: 4;
background-color: rgba(13, 110, 253, 0.1); background-color: rgba(13, 110, 253, 0.1);
flex: 1 1 auto; flex: 1 1 auto;
@ -40,7 +40,6 @@ select#font-select option {
max-width: 4rem; max-width: 4rem;
} }
.rotation-handle { .rotation-handle {
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -163,3 +162,76 @@ select#font-select option {
.small-file-container-saved:hover .drag-icon { .small-file-container-saved:hover .drag-icon {
display: flex; display: flex;
} }
/* The container must be positioned relative: */
.custom-select {
position: relative;
font-family: inherit;
}
.custom-select select {
display: none; /*hide original SELECT element: */
}
.select-selected {
background-color: inherit;
line-height: 30px;
font-size: 30px;
border-radius: 3rem !important;
}
/* Style the arrow inside the select element: */
.select-selected:after {
position: absolute;
content: "";
top: 50%;
right: 10px;
translate: 0 -50%;
width: 0;
height: 0;
border: 6px solid transparent;
border-color: #fff transparent transparent transparent;
}
/* Point the arrow upwards when the select box is open (active): */
.select-selected.select-arrow-active:after {
border-color: transparent transparent #fff transparent;
translate: 0 -75%;
}
/* style the items (options), including the selected item: */
.select-items div,
.select-selected {
color: inherit;
padding: 8px 16px;
cursor: pointer;
}
.select-items div {
border: 1px solid transparent;
border-color: transparent transparent transparent transparent;
line-height: 30px;
font-size: 30px;
}
/* Style items (options): */
.select-items {
position: absolute;
background-color: inherit;
top: 100%;
left: 0;
right: 0;
z-index: 101;
border: inherit;
}
/* Hide the items when the select box is closed: */
.select-hide {
display: none;
}
.select-items div:hover,
.same-as-selected {
background-color: rgba(54, 54, 54, 0.1);
}

View File

@ -8,21 +8,21 @@ window.goToFirstOrLastPage = goToFirstOrLastPage;
let currentPreviewSrc = null; let currentPreviewSrc = null;
function toggleSignatureView() { function toggleSignatureView() {
const gridView = document.getElementById('gridView'); const gridView = document.getElementById("gridView");
const listView = document.getElementById('listView'); const listView = document.getElementById("listView");
const gridText = document.querySelector('.grid-view-text'); const gridText = document.querySelector(".grid-view-text");
const listText = document.querySelector('.list-view-text'); const listText = document.querySelector(".list-view-text");
if (gridView.style.display !== 'none') { if (gridView.style.display !== "none") {
gridView.style.display = 'none'; gridView.style.display = "none";
listView.style.display = 'block'; listView.style.display = "block";
gridText.style.display = 'none'; gridText.style.display = "none";
listText.style.display = 'inline'; listText.style.display = "inline";
} else { } else {
gridView.style.display = 'block'; gridView.style.display = "block";
listView.style.display = 'none'; listView.style.display = "none";
gridText.style.display = 'inline'; gridText.style.display = "inline";
listText.style.display = 'none'; listText.style.display = "none";
} }
} }
@ -30,63 +30,204 @@ function previewSignature(element) {
const src = element.dataset.src; const src = element.dataset.src;
currentPreviewSrc = src; currentPreviewSrc = src;
const filename = element.querySelector('.signature-list-name').textContent; const filename = element.querySelector(".signature-list-name").textContent;
const previewImage = document.getElementById('previewImage'); const previewImage = document.getElementById("previewImage");
const previewFileName = document.getElementById('previewFileName'); const previewFileName = document.getElementById("previewFileName");
previewImage.src = src; previewImage.src = src;
previewFileName.textContent = filename; previewFileName.textContent = filename;
const modal = new bootstrap.Modal(document.getElementById('signaturePreview')); const modal = new bootstrap.Modal(
document.getElementById("signaturePreview")
);
modal.show(); modal.show();
} }
function addSignatureFromPreview() { function addSignatureFromPreview() {
if (currentPreviewSrc) { if (currentPreviewSrc) {
DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc); DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide(); bootstrap.Modal.getInstance(
document.getElementById("signaturePreview")
).hide();
} }
} }
let originalFileName = ''; let originalFileName = "";
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => { document
.querySelector("input[name=pdf-upload]")
.addEventListener("change", async (event) => {
const fileInput = event.target; const fileInput = event.target;
fileInput.addEventListener('file-input-change', async (e) => { fileInput.addEventListener("file-input-change", async (e) => {
const {allFiles} = e.detail; const { allFiles } = e.detail;
if (allFiles && allFiles.length > 0) { if (allFiles && allFiles.length > 0) {
const file = allFiles[0]; const file = allFiles[0];
originalFileName = file.name.replace(/\.[^/.]+$/, ''); originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer(); const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; pdfjsLib.GlobalWorkerOptions.workerSrc =
const pdfDoc = await pdfjsLib.getDocument({data: pdfData}).promise; "./pdfjs-legacy/pdf.worker.mjs";
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0); await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll('.show-on-file-selected').forEach((el) => { document.querySelectorAll(".show-on-file-selected").forEach((el) => {
el.style.cssText = ''; el.style.cssText = "";
}); });
} }
}); });
}); });
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('.show-on-file-selected').forEach((el) => { document.querySelectorAll(".show-on-file-selected").forEach((el) => {
el.style.cssText = 'display:none !important'; el.style.cssText = "display:none !important";
}); });
document.querySelectorAll('.small-file-container-saved img ').forEach((img) => { document
img.addEventListener('dragstart', (e) => { .querySelectorAll(".small-file-container-saved img ")
e.dataTransfer.setData('fileUrl', img.src); .forEach((img) => {
img.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("fileUrl", img.src);
}); });
}); });
document.addEventListener('keydown', (e) => { document.addEventListener("keydown", (e) => {
if (e.key === 'Delete') { if (e.key === "Delete") {
DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted()); DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted());
} }
}); });
addCustomSelect();
}); });
const imageUpload = document.querySelector('input[name=image-upload]'); function addCustomSelect() {
imageUpload.addEventListener('change', (e) => { 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; if (!e.target.files) return;
for (const imageFile of e.target.files) { for (const imageFile of e.target.files) {
var reader = new FileReader(); var reader = new FileReader();
@ -97,11 +238,11 @@ imageUpload.addEventListener('change', (e) => {
} }
}); });
const signaturePadCanvas = document.getElementById('drawing-pad-canvas'); const signaturePadCanvas = document.getElementById("drawing-pad-canvas");
const signaturePad = new SignaturePad(signaturePadCanvas, { const signaturePad = new SignaturePad(signaturePadCanvas, {
minWidth: 1, minWidth: 1,
maxWidth: 2, maxWidth: 2,
penColor: 'black', penColor: "black",
}); });
function addDraggableFromPad() { function addDraggableFromPad() {
@ -113,7 +254,7 @@ function addDraggableFromPad() {
} }
function getCroppedCanvasDataUrl(canvas) { function getCroppedCanvasDataUrl(canvas) {
let originalCtx = canvas.getContext('2d'); let originalCtx = canvas.getContext("2d");
let originalWidth = canvas.width; let originalWidth = canvas.width;
let originalHeight = canvas.height; let originalHeight = canvas.height;
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
@ -129,7 +270,8 @@ function getCroppedCanvasDataUrl(canvas) {
for (y = 0; y < originalHeight; y++) { for (y = 0; y < originalHeight; y++) {
for (x = 0; x < originalWidth; x++) { for (x = 0; x < originalWidth; x++) {
currentPixelColorValueIndex = (y * originalWidth + x) * 4; currentPixelColorValueIndex = (y * originalWidth + x) * 4;
let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3]; let currentPixelAlphaValue =
imageData.data[currentPixelColorValueIndex + 3];
if (currentPixelAlphaValue > 0) { if (currentPixelAlphaValue > 0) {
if (minX > x) minX = x; if (minX > x) minX = x;
if (maxX < x) maxX = x; if (maxX < x) maxX = x;
@ -142,10 +284,15 @@ function getCroppedCanvasDataUrl(canvas) {
let croppedWidth = maxX - minX; let croppedWidth = maxX - minX;
let croppedHeight = maxY - minY; let croppedHeight = maxY - minY;
if (croppedWidth < 0 || croppedHeight < 0) return null; if (croppedWidth < 0 || croppedHeight < 0) return null;
let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight); let cuttedImageData = originalCtx.getImageData(
minX,
minY,
croppedWidth,
croppedHeight
);
let croppedCanvas = document.createElement('canvas'), let croppedCanvas = document.createElement("canvas"),
croppedCtx = croppedCanvas.getContext('2d'); croppedCtx = croppedCanvas.getContext("2d");
croppedCanvas.width = croppedWidth; croppedCanvas.width = croppedWidth;
croppedCanvas.height = croppedHeight; croppedCanvas.height = croppedHeight;
@ -158,9 +305,13 @@ function resizeCanvas() {
var ratio = Math.max(window.devicePixelRatio || 1, 1); var ratio = Math.max(window.devicePixelRatio || 1, 1);
var additionalFactor = 10; var additionalFactor = 10;
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor; signaturePadCanvas.width =
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor; signaturePadCanvas.offsetWidth * ratio * additionalFactor;
signaturePadCanvas.getContext('2d').scale(ratio * additionalFactor, ratio * additionalFactor); signaturePadCanvas.height =
signaturePadCanvas.offsetHeight * ratio * additionalFactor;
signaturePadCanvas
.getContext("2d")
.scale(ratio * additionalFactor, ratio * additionalFactor);
signaturePad.clear(); signaturePad.clear();
} }
@ -174,12 +325,12 @@ new IntersectionObserver((entries, observer) => {
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas); new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
function addDraggableFromText() { function addDraggableFromText() {
const sigText = document.getElementById('sigText').value; const sigText = document.getElementById("sigText").value;
const font = document.querySelector('select[name=font]').value; const font = document.querySelector("select[name=font]").value;
const fontSize = 100; const fontSize = 100;
const canvas = document.createElement('canvas'); const canvas = document.createElement("canvas");
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
ctx.font = `${fontSize}px ${font}`; ctx.font = `${fontSize}px ${font}`;
const textWidth = ctx.measureText(sigText).width; const textWidth = ctx.measureText(sigText).width;
const textHeight = fontSize; const textHeight = fontSize;
@ -190,7 +341,7 @@ function addDraggableFromText() {
canvas.height = paragraphs.length * textHeight * 1.35; // for tails canvas.height = paragraphs.length * textHeight * 1.35; // for tails
ctx.font = `${fontSize}px ${font}`; ctx.font = `${fontSize}px ${font}`;
ctx.textBaseline = 'top'; ctx.textBaseline = "top";
let y = 0; let y = 0;
@ -212,8 +363,8 @@ async function goToFirstOrLastPage(page) {
} }
} }
document.getElementById('download-pdf').addEventListener('click', async () => { document.getElementById("download-pdf").addEventListener("click", async () => {
const downloadButton = document.getElementById('download-pdf'); const downloadButton = document.getElementById("download-pdf");
const originalContent = downloadButton.innerHTML; const originalContent = downloadButton.innerHTML;
downloadButton.disabled = true; downloadButton.disabled = true;
@ -224,13 +375,13 @@ document.getElementById('download-pdf').addEventListener('click', async () => {
try { try {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument(); const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save(); const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'}); const blob = new Blob([modifiedPdfBytes], { type: "application/pdf" });
const link = document.createElement('a'); const link = document.createElement("a");
link.href = URL.createObjectURL(blob); link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_signed.pdf'; link.download = originalFileName + "_signed.pdf";
link.click(); link.click();
} catch (error) { } catch (error) {
console.error('Error downloading PDF:', error); console.error("Error downloading PDF:", error);
} finally { } finally {
downloadButton.disabled = false; downloadButton.disabled = false;
downloadButton.innerHTML = originalContent; downloadButton.innerHTML = originalContent;

View File

@ -15,7 +15,8 @@
#font-select option[value="[[${font.name}]]"] { #font-select option[value="[[${font.name}]]"] {
font-family: "[[${font.name}]]", font-family: "[[${font.name}]]",
cursive; cursive
!important;
} }
</style> </style>
</th:block> </th:block>
@ -133,10 +134,12 @@
<label class="form-check-label" for="sigText" th:text="#{text}"></label> <label class="form-check-label" for="sigText" th:text="#{text}"></label>
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea> <textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
<label th:text="#{font}"></label> <label th:text="#{font}"></label>
<div id="signFontSelection" class="custom-select form-control">
<select class="form-control" name="font" id="font-select"> <select class="form-control" name="font" id="font-select">
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}" <option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}"
th:class="${font.name.toLowerCase()+'-font'}"></option> th:class="${font.name.toLowerCase()+'-font'}"></option>
</select> </select>
</div>
<div class="margin-auto-parent"> <div class="margin-auto-parent">
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" <button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center"
onclick="addDraggableFromText()" th:text="#{sign.add}"></button> onclick="addDraggableFromText()" th:text="#{sign.add}"></button>