1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-18 14:18:23 +02:00
pdf.js/web/signature_manager.js

1067 lines
28 KiB
JavaScript

/* Copyright 2025 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AnnotationEditorParamsType,
DOMSVGFactory,
noContextMenu,
SignatureExtractor,
stopEvent,
SupportedImageMimeTypes,
} from "pdfjs-lib";
// Default height of the added signature in page coordinates.
const DEFAULT_HEIGHT_IN_PAGE = 40;
class SignatureManager {
#addButton;
#tabsToAltText = null;
#clearButton;
#clearDescription;
#currentEditor;
#description;
#dialog;
#drawCurves = null;
#drawPlaceholder;
#drawPath = null;
#drawPathString = "";
#drawPoints = null;
#drawSVG;
#drawThickness;
#errorBar;
#extractedSignatureData = null;
#imagePath = null;
#imagePicker;
#imagePickerLink;
#imagePlaceholder;
#imageSVG;
#saveCheckbox;
#saveContainer;
#tabButtons;
#addSignatureToolbarButton;
#loadSignaturesPromise = null;
#typeInput;
#currentTab = null;
#currentTabAC = null;
#hasDescriptionChanged = false;
#eventBus;
#l10n;
#overlayManager;
#editDescriptionDialog;
#signatureStorage;
#uiManager = null;
static #l10nDescription = null;
constructor(
{
dialog,
panels,
typeButton,
typeInput,
drawButton,
drawPlaceholder,
drawSVG,
drawThickness,
imageButton,
imageSVG,
imagePlaceholder,
imagePicker,
imagePickerLink,
description,
clearButton,
cancelButton,
addButton,
errorCloseButton,
errorBar,
saveCheckbox,
saveContainer,
},
editSignatureElements,
addSignatureToolbarButton,
overlayManager,
l10n,
signatureStorage,
eventBus
) {
this.#addButton = addButton;
this.#clearButton = clearButton;
this.#clearDescription = description.lastElementChild;
this.#description = description.firstElementChild;
this.#dialog = dialog;
this.#drawSVG = drawSVG;
this.#drawPlaceholder = drawPlaceholder;
this.#drawThickness = drawThickness;
this.#errorBar = errorBar;
this.#imageSVG = imageSVG;
this.#imagePlaceholder = imagePlaceholder;
this.#imagePicker = imagePicker;
this.#imagePickerLink = imagePickerLink;
this.#overlayManager = overlayManager;
this.#saveCheckbox = saveCheckbox;
this.#saveContainer = saveContainer;
this.#addSignatureToolbarButton = addSignatureToolbarButton;
this.#typeInput = typeInput;
this.#l10n = l10n;
this.#signatureStorage = signatureStorage;
this.#eventBus = eventBus;
this.#editDescriptionDialog = new EditDescriptionDialog(
editSignatureElements,
overlayManager
);
SignatureManager.#l10nDescription ||= Object.freeze({
signature: "pdfjs-editor-add-signature-description-default-when-drawing",
});
dialog.addEventListener("close", this.#close.bind(this));
dialog.addEventListener("contextmenu", e => {
const { target } = e;
if (target !== this.#typeInput && target !== this.#description) {
e.preventDefault();
}
});
dialog.addEventListener("drop", e => {
stopEvent(e);
});
cancelButton.addEventListener("click", this.#cancel.bind(this));
addButton.addEventListener("click", this.#add.bind(this));
clearButton.addEventListener(
"click",
() => {
this.#reportTelemetry({
type: "signature",
action: "pdfjs.signature.clear",
data: {
type: this.#currentTab,
},
});
this.#initTab(null);
},
{ passive: true }
);
this.#description.addEventListener(
"input",
() => {
this.#clearDescription.disabled = this.#description.value === "";
},
{ passive: true }
);
this.#clearDescription.addEventListener(
"click",
() => {
this.#description.value = "";
this.#clearDescription.disabled = true;
},
{ passive: true }
);
errorCloseButton.addEventListener(
"click",
() => {
errorBar.hidden = true;
},
{ passive: true }
);
this.#initTabButtons(typeButton, drawButton, imageButton, panels);
imagePicker.accept = SupportedImageMimeTypes.join(",");
eventBus._on("storedsignatureschanged", this.#signaturesChanged.bind(this));
overlayManager.register(dialog);
}
#initTabButtons(typeButton, drawButton, imageButton, panels) {
const buttons = (this.#tabButtons = new Map([
["type", typeButton],
["draw", drawButton],
["image", imageButton],
]));
const tabCallback = e => {
for (const [name, button] of buttons) {
if (button === e.target) {
button.setAttribute("aria-selected", true);
button.setAttribute("tabindex", 0);
panels.setAttribute("data-selected", name);
this.#initTab(name);
} else {
button.setAttribute("aria-selected", false);
// Only the active tab is focusable: the others can be
// reached by keyboard navigation (left/right arrows).
button.setAttribute("tabindex", -1);
}
}
};
const buttonsArray = Array.from(buttons.values());
for (let i = 0, ii = buttonsArray.length; i < ii; i++) {
const button = buttonsArray[i];
button.addEventListener("click", tabCallback, { passive: true });
button.addEventListener(
"keydown",
({ key }) => {
if (key !== "ArrowLeft" && key !== "ArrowRight") {
return;
}
buttonsArray[i + (key === "ArrowLeft" ? -1 : 1)]?.focus();
},
{ passive: true }
);
}
}
#resetCommon() {
this.#hasDescriptionChanged = false;
this.#description.value = "";
if (this.#currentTab) {
this.#tabsToAltText.get(this.#currentTab).value = "";
}
}
#resetTab(name) {
switch (name) {
case "type":
this.#typeInput.value = "";
break;
case "draw":
this.#drawCurves = null;
this.#drawPoints = null;
this.#drawPathString = "";
this.#drawPath?.remove();
this.#drawPath = null;
this.#drawPlaceholder.hidden = false;
this.#drawThickness.value = 1;
break;
case "image":
this.#imagePlaceholder.hidden = false;
this.#imagePath?.remove();
this.#imagePath = null;
break;
}
}
#initTab(name) {
if (name && this.#currentTab === name) {
return;
}
if (this.#currentTab) {
this.#tabsToAltText.get(this.#currentTab).value = this.#description.value;
}
if (name) {
this.#currentTab = name;
}
this.#errorBar.hidden = true;
const reset = !name;
if (reset) {
this.#resetCommon();
} else {
this.#description.value = this.#tabsToAltText.get(this.#currentTab).value;
}
this.#clearDescription.disabled = this.#description.value === "";
this.#currentTabAC?.abort();
this.#currentTabAC = new AbortController();
switch (this.#currentTab) {
case "type":
this.#initTypeTab(reset);
break;
case "draw":
this.#initDrawTab(reset);
break;
case "image":
this.#initImageTab(reset);
break;
}
}
#disableButtons(value) {
this.#saveCheckbox.disabled =
this.#clearButton.disabled =
this.#addButton.disabled =
this.#description.disabled =
!value;
}
#initTypeTab(reset) {
if (reset) {
this.#resetTab("type");
}
this.#disableButtons(this.#typeInput.value);
const { signal } = this.#currentTabAC;
const options = { passive: true, signal };
this.#typeInput.addEventListener(
"input",
() => {
const { value } = this.#typeInput;
if (!this.#hasDescriptionChanged) {
this.#tabsToAltText.get("type").default = this.#description.value =
value;
this.#clearDescription.disabled = value === "";
}
this.#disableButtons(value);
},
options
);
this.#description.addEventListener(
"input",
() => {
this.#hasDescriptionChanged =
this.#typeInput.value !== this.#description.value;
},
options
);
}
#initDrawTab(reset) {
if (reset) {
this.#resetTab("draw");
}
this.#disableButtons(this.#drawPath);
const { signal } = this.#currentTabAC;
const options = { signal };
let currentPointerId = NaN;
const drawCallback = e => {
const { pointerId } = e;
if (!isNaN(currentPointerId) && currentPointerId !== pointerId) {
return;
}
currentPointerId = pointerId;
e.preventDefault();
this.#drawSVG.setPointerCapture(pointerId);
const { width: drawWidth, height: drawHeight } =
this.#drawSVG.getBoundingClientRect();
let { offsetX, offsetY } = e;
offsetX = Math.round(offsetX);
offsetY = Math.round(offsetY);
if (e.target === this.#drawPlaceholder) {
this.#drawPlaceholder.hidden = true;
}
if (!this.#drawCurves) {
this.#drawCurves = {
width: drawWidth,
height: drawHeight,
thickness: parseInt(this.#drawThickness.value),
curves: [],
};
this.#disableButtons(true);
const svgFactory = new DOMSVGFactory();
const path = (this.#drawPath = svgFactory.createElement("path"));
path.setAttribute("stroke-width", this.#drawThickness.value);
this.#drawSVG.append(path);
this.#drawSVG.addEventListener("pointerdown", drawCallback, options);
this.#drawPlaceholder.removeEventListener("pointerdown", drawCallback);
if (this.#description.value === "") {
this.#l10n
.get(SignatureManager.#l10nDescription.signature)
.then(description => {
this.#tabsToAltText.get("draw").default = description;
this.#description.value ||= description;
this.#clearDescription.disabled = this.#description.value === "";
});
}
}
this.#drawPoints = [offsetX, offsetY];
this.#drawCurves.curves.push({ points: this.#drawPoints });
this.#drawPathString += `M ${offsetX} ${offsetY}`;
this.#drawPath.setAttribute("d", this.#drawPathString);
const finishDrawAC = new AbortController();
const listenerDrawOptions = {
signal: AbortSignal.any([signal, finishDrawAC.signal]),
};
this.#drawSVG.addEventListener(
"contextmenu",
noContextMenu,
listenerDrawOptions
);
this.#drawSVG.addEventListener(
"pointermove",
evt => {
evt.preventDefault();
let { offsetX: x, offsetY: y } = evt;
x = Math.round(x);
y = Math.round(y);
const drawPoints = this.#drawPoints;
if (
x < 0 ||
y < 0 ||
x > drawWidth ||
y > drawHeight ||
(x === drawPoints.at(-2) && y === drawPoints.at(-1))
) {
return;
}
if (drawPoints.length >= 4) {
const [x1, y1, x2, y2] = drawPoints.slice(-4);
this.#drawPathString += `C${(x1 + 5 * x2) / 6} ${(y1 + 5 * y2) / 6} ${(5 * x2 + x) / 6} ${(5 * y2 + y) / 6} ${(x2 + x) / 2} ${(y2 + y) / 2}`;
} else {
this.#drawPathString += `L${x} ${y}`;
}
drawPoints.push(x, y);
this.#drawPath.setAttribute("d", this.#drawPathString);
},
listenerDrawOptions
);
this.#drawSVG.addEventListener(
"pointerup",
evt => {
const { pointerId: pId } = evt;
if (!isNaN(currentPointerId) && currentPointerId !== pId) {
return;
}
currentPointerId = NaN;
evt.preventDefault();
this.#drawSVG.releasePointerCapture(pId);
finishDrawAC.abort();
if (this.#drawPoints.length === 2) {
this.#drawPathString += `L${this.#drawPoints[0]} ${this.#drawPoints[1]}`;
this.#drawPath.setAttribute("d", this.#drawPathString);
}
},
listenerDrawOptions
);
};
if (this.#drawCurves) {
this.#drawSVG.addEventListener("pointerdown", drawCallback, options);
} else {
this.#drawPlaceholder.addEventListener(
"pointerdown",
drawCallback,
options
);
}
this.#drawThickness.addEventListener(
"input",
() => {
const { value: thickness } = this.#drawThickness;
this.#drawThickness.setAttribute(
"data-l10n-args",
JSON.stringify({ thickness })
);
if (!this.#drawCurves) {
return;
}
this.#drawPath.setAttribute("stroke-width", thickness);
this.#drawCurves.thickness = thickness;
},
options
);
}
#initImageTab(reset) {
if (reset) {
this.#resetTab("image");
}
this.#disableButtons(this.#imagePath);
const { signal } = this.#currentTabAC;
const options = { signal };
const passiveOptions = { passive: true, signal };
this.#imagePickerLink.addEventListener(
"keydown",
e => {
const { key } = e;
if (key === "Enter" || key === " ") {
stopEvent(e);
this.#imagePicker.click();
}
},
options
);
this.#imagePicker.addEventListener(
"click",
() => {
this.#dialog.classList.toggle("waiting", true);
},
passiveOptions
);
this.#imagePicker.addEventListener(
"change",
async () => {
const file = this.#imagePicker.files?.[0];
if (!file || !SupportedImageMimeTypes.includes(file.type)) {
this.#errorBar.hidden = false;
this.#dialog.classList.toggle("waiting", false);
return;
}
await this.#extractSignature(file);
},
passiveOptions
);
this.#imagePicker.addEventListener(
"cancel",
() => {
this.#dialog.classList.toggle("waiting", false);
},
passiveOptions
);
this.#imagePlaceholder.addEventListener(
"dragover",
e => {
const { dataTransfer } = e;
for (const { type } of dataTransfer.items) {
if (!SupportedImageMimeTypes.includes(type)) {
continue;
}
dataTransfer.dropEffect =
dataTransfer.effectAllowed === "copy" ? "copy" : "move";
stopEvent(e);
return;
}
dataTransfer.dropEffect = "none";
},
options
);
this.#imagePlaceholder.addEventListener(
"drop",
e => {
const {
dataTransfer: { files },
} = e;
if (!files?.length) {
return;
}
for (const file of files) {
if (SupportedImageMimeTypes.includes(file.type)) {
this.#extractSignature(file);
break;
}
}
stopEvent(e);
this.#dialog.classList.toggle("waiting", true);
},
options
);
}
async #extractSignature(file) {
let data;
try {
data = await this.#uiManager.imageManager.getFromFile(file);
} catch (e) {
console.error("SignatureManager.#extractSignature.", e);
}
if (!data) {
this.#errorBar.hidden = false;
this.#dialog.classList.toggle("waiting", false);
return;
}
const { outline } = (this.#extractedSignatureData =
this.#currentEditor.getFromImage(data.bitmap));
if (!outline) {
this.#dialog.classList.toggle("waiting", false);
return;
}
this.#imagePlaceholder.hidden = true;
this.#disableButtons(true);
const svgFactory = new DOMSVGFactory();
const path = (this.#imagePath = svgFactory.createElement("path"));
this.#imageSVG.setAttribute("viewBox", outline.viewBox);
this.#imageSVG.setAttribute("preserveAspectRatio", "xMidYMid meet");
this.#imageSVG.append(path);
path.setAttribute("d", outline.toSVGPath());
this.#tabsToAltText.get("image").default = file.name;
if (this.#description.value === "") {
this.#description.value = file.name || "";
this.#clearDescription.disabled = this.#description.value === "";
}
this.#dialog.classList.toggle("waiting", false);
}
#getOutlineForType() {
return this.#currentEditor.getFromText(
this.#typeInput.value,
window.getComputedStyle(this.#typeInput)
);
}
#getOutlineForDraw() {
const { width, height } = this.#drawSVG.getBoundingClientRect();
return this.#currentEditor.getDrawnSignature(
this.#drawCurves,
width,
height
);
}
#reportTelemetry(data) {
this.#eventBus.dispatch("reporttelemetry", {
source: this,
details: {
type: "editing",
data,
},
});
}
#addToolbarButton(signatureData, uuid, description) {
const { curves, areContours, thickness, width, height } = signatureData;
const maxDim = Math.max(width, height);
const outlineData = SignatureExtractor.processDrawnLines({
lines: {
curves,
thickness,
width,
height,
},
pageWidth: maxDim,
pageHeight: maxDim,
rotation: 0,
innerMargin: 0,
mustSmooth: false,
areContours,
});
if (!outlineData) {
return;
}
const { outline } = outlineData;
const svgFactory = new DOMSVGFactory();
const div = document.createElement("div");
const button = document.createElement("button");
button.addEventListener("click", () => {
this.#eventBus.dispatch("switchannotationeditorparams", {
source: this,
type: AnnotationEditorParamsType.CREATE,
value: {
signatureData: {
lines: {
curves,
thickness,
width,
height,
},
mustSmooth: false,
areContours,
description,
uuid,
heightInPage: DEFAULT_HEIGHT_IN_PAGE,
},
},
});
});
div.append(button);
div.classList.add("toolbarAddSignatureButtonContainer");
const svg = svgFactory.create(1, 1, true);
button.append(svg);
const span = document.createElement("span");
span.ariaHidden = true;
button.append(span);
button.classList.add("toolbarAddSignatureButton");
button.type = "button";
span.textContent = description;
button.setAttribute(
"data-l10n-id",
"pdfjs-editor-add-saved-signature-button"
);
button.setAttribute("data-l10n-args", JSON.stringify({ description }));
button.tabIndex = 0;
const path = svgFactory.createElement("path");
svg.append(path);
svg.setAttribute("viewBox", outline.viewBox);
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
if (areContours) {
path.classList.add("contours");
}
path.setAttribute("d", outline.toSVGPath());
const deleteButton = document.createElement("button");
div.append(deleteButton);
deleteButton.classList.add("toolbarButton", "deleteButton");
deleteButton.setAttribute(
"data-l10n-id",
"pdfjs-editor-delete-signature-button1"
);
deleteButton.type = "button";
deleteButton.tabIndex = 0;
deleteButton.addEventListener("click", async () => {
if (await this.#signatureStorage.delete(uuid)) {
div.remove();
this.#reportTelemetry({
type: "signature",
action: "pdfjs.signature.delete_saved",
data: {
savedCount: await this.#signatureStorage.size(),
},
});
}
});
const deleteSpan = document.createElement("span");
deleteButton.append(deleteSpan);
deleteSpan.setAttribute(
"data-l10n-id",
"pdfjs-editor-delete-signature-button-label1"
);
this.#addSignatureToolbarButton.before(div);
}
async #signaturesChanged() {
const parent = this.#addSignatureToolbarButton.parentElement;
while (parent.firstElementChild !== this.#addSignatureToolbarButton) {
parent.firstElementChild.remove();
}
this.#loadSignaturesPromise = null;
await this.loadSignatures(/* reload = */ true);
}
getSignature(params) {
return this.open(params);
}
async loadSignatures(reload = false) {
if (
!this.#addSignatureToolbarButton ||
(!reload && this.#addSignatureToolbarButton.previousElementSibling) ||
!this.#signatureStorage
) {
return;
}
if (!this.#loadSignaturesPromise) {
// The first call of loadSignatures() starts loading the signatures.
// The second one will wait until the signatures are loaded in the DOM.
this.#loadSignaturesPromise = this.#signatureStorage
.getAll()
.then(async signatures => [
signatures,
await Promise.all(
Array.from(signatures.values(), ({ signatureData }) =>
SignatureExtractor.decompressSignature(signatureData)
)
),
]);
if (!reload) {
return;
}
}
const [signatures, signaturesData] = await this.#loadSignaturesPromise;
this.#loadSignaturesPromise = null;
let i = 0;
for (const [uuid, { description }] of signatures) {
const data = signaturesData[i++];
if (!data) {
continue;
}
data.curves = data.outlines.map(points => ({ points }));
delete data.outlines;
this.#addToolbarButton(data, uuid, description);
}
}
async renderEditButton(editor) {
const button = document.createElement("button");
button.classList.add("altText", "editDescription");
button.tabIndex = 0;
button.title = editor.description;
const span = document.createElement("span");
button.append(span);
span.setAttribute(
"data-l10n-id",
"pdfjs-editor-add-signature-edit-button-label"
);
button.addEventListener(
"click",
() => {
this.#editDescriptionDialog.open(editor);
},
{ passive: true }
);
return button;
}
async open({ uiManager, editor }) {
this.#tabsToAltText ||= new Map(
this.#tabButtons.keys().map(name => [name, { value: "", default: "" }])
);
this.#uiManager = uiManager;
this.#currentEditor = editor;
this.#uiManager.removeEditListeners();
const isStorageFull = await this.#signatureStorage.isFull();
this.#saveContainer.classList.toggle("fullStorage", isStorageFull);
this.#saveCheckbox.checked = !isStorageFull;
await this.#overlayManager.open(this.#dialog);
const tabType = this.#tabButtons.get("type");
tabType.focus();
tabType.click();
}
#cancel() {
this.#finish();
}
#finish() {
this.#overlayManager.closeIfActive(this.#dialog);
}
#close() {
if (this.#currentEditor._drawId === null) {
this.#currentEditor.remove();
}
this.#uiManager?.addEditListeners();
this.#currentTabAC?.abort();
this.#currentTabAC = null;
this.#uiManager = null;
this.#currentEditor = null;
this.#resetCommon();
for (const [name] of this.#tabButtons) {
this.#resetTab(name);
}
this.#disableButtons(false);
this.#currentTab = null;
this.#tabsToAltText = null;
}
async #add() {
let data;
const type = this.#currentTab;
switch (type) {
case "type":
data = this.#getOutlineForType();
break;
case "draw":
data = this.#getOutlineForDraw();
break;
case "image":
data = this.#extractedSignatureData;
break;
}
let uuid = null;
const description = this.#description.value;
if (this.#saveCheckbox.checked) {
const { newCurves, areContours, thickness, width, height } = data;
const signatureData = await SignatureExtractor.compressSignature({
outlines: newCurves,
areContours,
thickness,
width,
height,
});
uuid = await this.#signatureStorage.create({
description,
signatureData,
});
if (uuid) {
this.#addToolbarButton(
{
curves: newCurves.map(points => ({ points })),
areContours,
thickness,
width,
height,
},
uuid,
description
);
} else {
console.warn("SignatureManager.add: cannot save the signature.");
}
}
const altText = this.#tabsToAltText.get(type);
this.#reportTelemetry({
type: "signature",
action: "pdfjs.signature.created",
data: {
type,
saved: !!uuid,
savedCount: await this.#signatureStorage.size(),
descriptionChanged: description !== altText.default,
},
});
this.#currentEditor.addSignature(
data,
DEFAULT_HEIGHT_IN_PAGE,
this.#description.value,
uuid
);
this.#finish();
}
destroy() {
this.#uiManager = null;
this.#finish();
}
}
class EditDescriptionDialog {
#currentEditor;
#previousDescription;
#description;
#dialog;
#overlayManager;
#signatureSVG;
#uiManager;
constructor(
{ dialog, description, cancelButton, updateButton, editSignatureView },
overlayManager
) {
const descriptionInput = (this.#description =
description.firstElementChild);
this.#signatureSVG = editSignatureView;
this.#dialog = dialog;
this.#overlayManager = overlayManager;
dialog.addEventListener("close", this.#close.bind(this));
dialog.addEventListener("contextmenu", e => {
if (e.target !== this.#description) {
e.preventDefault();
}
});
cancelButton.addEventListener("click", this.#cancel.bind(this));
updateButton.addEventListener("click", this.#update.bind(this));
const clearDescription = description.lastElementChild;
clearDescription.addEventListener("click", () => {
descriptionInput.value = "";
clearDescription.disabled = true;
updateButton.disabled = this.#previousDescription === "";
});
descriptionInput.addEventListener(
"input",
() => {
const { value } = descriptionInput;
clearDescription.disabled = value === "";
updateButton.disabled = value === this.#previousDescription;
editSignatureView.setAttribute("aria-label", value);
},
{ passive: true }
);
overlayManager.register(dialog);
}
async open(editor) {
this.#uiManager = editor._uiManager;
this.#currentEditor = editor;
this.#previousDescription = this.#description.value = editor.description;
this.#description.dispatchEvent(new Event("input"));
this.#uiManager.removeEditListeners();
const { areContours, outline } = editor.getSignaturePreview();
const svgFactory = new DOMSVGFactory();
const path = svgFactory.createElement("path");
this.#signatureSVG.append(path);
this.#signatureSVG.setAttribute("viewBox", outline.viewBox);
path.setAttribute("d", outline.toSVGPath());
if (areContours) {
path.classList.add("contours");
}
await this.#overlayManager.open(this.#dialog);
}
async #update() {
// The description has been changed because the button isn't disabled.
this.#currentEditor._reportTelemetry({
action: "pdfjs.signature.edit_description",
data: {
hasBeenChanged: true,
},
});
this.#currentEditor.description = this.#description.value;
this.#finish();
}
#cancel() {
this.#currentEditor._reportTelemetry({
action: "pdfjs.signature.edit_description",
data: {
hasBeenChanged: false,
},
});
this.#finish();
}
#finish() {
this.#overlayManager.closeIfActive(this.#dialog);
}
#close() {
this.#uiManager?.addEditListeners();
this.#uiManager = null;
this.#currentEditor = null;
this.#signatureSVG.firstElementChild.remove();
}
}
export { SignatureManager };