Skip to content

Commit f0e3840

Browse files
authored
Refactor Signature Examples: (#173)
1 parent e5df003 commit f0e3840

File tree

10 files changed

+570
-0
lines changed

10 files changed

+570
-0
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import type {
2+
ImageAnnotation,
3+
InkAnnotation,
4+
Instance,
5+
Rect,
6+
WidgetAnnotation,
7+
} from "@nutrient-sdk/viewer";
8+
import { baseOptions } from "../../shared/base-options";
9+
10+
let globalInstance: Instance | null = null;
11+
12+
function scrollToNextSignatureField(index: number, rect: Rect) {
13+
if (!globalInstance) return;
14+
15+
const page = (
16+
globalInstance.contentDocument.getRootNode() as Document
17+
).querySelector(`[data-page-index="${index}"]`);
18+
19+
const arrow = document.createElement("span");
20+
arrow.classList.add("blob");
21+
arrow.style.cssText += `
22+
background: url(https://cdn-icons-png.flaticon.com/512/545/545682.png) 50% no-repeat;
23+
content: "";
24+
height: 1.2em;
25+
left: -2rem;
26+
position: absolute;
27+
top: 0;
28+
width: 1rem;
29+
`;
30+
31+
const item = new window.NutrientViewer.CustomOverlayItem({
32+
id: "arrow",
33+
node: arrow,
34+
pageIndex: index,
35+
position: new window.NutrientViewer.Geometry.Point({
36+
x: rect.left + 15,
37+
y: rect.top,
38+
}),
39+
});
40+
41+
globalInstance.setCustomOverlayItem(item);
42+
43+
window.requestAnimationFrame(() => {
44+
page?.scrollIntoView({
45+
block: "end",
46+
inline: "nearest",
47+
behavior: "smooth",
48+
});
49+
});
50+
}
51+
52+
localStorage.setItem("signature-field-counter", "0");
53+
54+
window.NutrientViewer.load({
55+
...baseOptions,
56+
}).then(async (instance: Instance) => {
57+
globalInstance = instance;
58+
59+
const formFields = await instance.getFormFields();
60+
61+
const signatureFormFields = formFields.filter(
62+
(field) =>
63+
field instanceof window.NutrientViewer.FormFields.SignatureFormField,
64+
);
65+
66+
const signatureNameIds = new Set(
67+
signatureFormFields.map((field) => field.name),
68+
);
69+
70+
const signatureFields = (
71+
await Promise.all(
72+
Array.from({ length: instance.totalPageCount }).map((_, pageIndex) =>
73+
instance.getAnnotations(pageIndex),
74+
),
75+
)
76+
).flatMap((annotations) =>
77+
annotations
78+
.filter((annotation) =>
79+
signatureNameIds.has(
80+
(annotation as WidgetAnnotation).formFieldName ?? "",
81+
),
82+
)
83+
.toJS(),
84+
);
85+
86+
setTimeout(() => {
87+
if (signatureFields.length > 0) {
88+
scrollToNextSignatureField(
89+
signatureFields[0].pageIndex,
90+
signatureFields[0].boundingBox,
91+
);
92+
}
93+
}, 1000);
94+
95+
instance.addEventListener("annotations.willChange", (event) => {
96+
const { annotations, reason } = event;
97+
const counter = Number.parseInt(
98+
localStorage.getItem("signature-field-counter") ?? "0",
99+
10,
100+
);
101+
102+
const firstAnnotation = annotations.first() as
103+
| InkAnnotation
104+
| ImageAnnotation
105+
| undefined;
106+
if (!firstAnnotation) return;
107+
108+
if (
109+
firstAnnotation.isSignature &&
110+
reason === window.NutrientViewer.AnnotationsWillChangeReason.SELECT_END
111+
) {
112+
instance.removeCustomOverlayItem("arrow");
113+
114+
const newCounter = counter + 1;
115+
localStorage.setItem("signature-field-counter", newCounter.toString());
116+
117+
if (newCounter < signatureFields.length) {
118+
const nextSignatureField = signatureFields[newCounter];
119+
const nextPageIndex = nextSignatureField.pageIndex;
120+
121+
const onViewStateChange = () => {
122+
setTimeout(() => {
123+
scrollToNextSignatureField(
124+
nextSignatureField.pageIndex,
125+
nextSignatureField.boundingBox,
126+
);
127+
}, 100);
128+
129+
instance.removeEventListener("viewState.change", onViewStateChange);
130+
};
131+
132+
instance.addEventListener("viewState.change", onViewStateChange);
133+
134+
instance.setViewState((viewState) =>
135+
viewState.set("currentPageIndex", nextPageIndex),
136+
);
137+
}
138+
}
139+
});
140+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: signatures
3+
title: Automatically Navigate to Next Signature Field
4+
description: Automatically scrolls to and highlights the next signature field after a user signs, guiding them through multiple signature fields in a document.
5+
keywords: [signature, navigation, scroll, form fields, custom overlay, electronic signatures, workflow]
6+
---
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type {
2+
DrawingPoint,
3+
ImageAnnotation,
4+
InkAnnotation,
5+
Instance,
6+
List,
7+
} from "@nutrient-sdk/viewer";
8+
import { baseOptions } from "../../shared/base-options";
9+
10+
const scalingFactorTypeSignature = 0.2;
11+
const scalingFactorDrawSignature = 0.2;
12+
13+
window.NutrientViewer.load({
14+
...baseOptions,
15+
theme: window.NutrientViewer.Theme.DARK,
16+
}).then((instance: Instance) => {
17+
instance.addEventListener(
18+
"annotations.create",
19+
async (createdAnnotations) => {
20+
const typeSignatures = createdAnnotations.filter(
21+
(annotation): annotation is ImageAnnotation =>
22+
annotation instanceof
23+
window.NutrientViewer.Annotations.ImageAnnotation,
24+
);
25+
26+
const firstTypeSignature = typeSignatures.first() as
27+
| ImageAnnotation
28+
| undefined;
29+
30+
if (firstTypeSignature?.isSignature) {
31+
const boundingBox = firstTypeSignature.boundingBox;
32+
const newWidth = scalingFactorTypeSignature * boundingBox.width;
33+
const newHeight = scalingFactorTypeSignature * boundingBox.height;
34+
const newLeft = boundingBox.left + (boundingBox.width - newWidth) / 2;
35+
const newTop = boundingBox.top + (boundingBox.height - newHeight) / 2;
36+
37+
const newBoundingBox = new window.NutrientViewer.Geometry.Rect({
38+
left: newLeft,
39+
top: newTop,
40+
width: newWidth,
41+
height: newHeight,
42+
});
43+
44+
const newAnnotation = firstTypeSignature.set(
45+
"boundingBox",
46+
newBoundingBox,
47+
);
48+
await instance.update(newAnnotation);
49+
return;
50+
}
51+
52+
const inkSignatures = createdAnnotations.filter(
53+
(annotation): annotation is InkAnnotation =>
54+
annotation instanceof
55+
window.NutrientViewer.Annotations.InkAnnotation &&
56+
annotation.isSignature,
57+
);
58+
59+
if (inkSignatures.size > 0) {
60+
const scaledAnnotations = inkSignatures.map((annotation) => {
61+
const boundingBox = annotation.boundingBox;
62+
if (!boundingBox) return annotation;
63+
64+
const scaledBoundingBox = boundingBox
65+
.scale(scalingFactorDrawSignature)
66+
.merge({
67+
left: boundingBox.left + boundingBox.width / 4,
68+
top: boundingBox.top + boundingBox.height / 4,
69+
});
70+
71+
const scaledLines = annotation.lines.map((line: List<DrawingPoint>) =>
72+
line.map((point: DrawingPoint) =>
73+
point
74+
.translate({
75+
x: -boundingBox.left,
76+
y: -boundingBox.top,
77+
})
78+
.scale(scalingFactorDrawSignature)
79+
.translate({
80+
x: boundingBox.left + boundingBox.width / 4,
81+
y: boundingBox.top + boundingBox.height / 4,
82+
}),
83+
),
84+
);
85+
86+
return annotation
87+
.set("boundingBox", scaledBoundingBox)
88+
.set("lines", scaledLines);
89+
});
90+
91+
await instance.update(scaledAnnotations);
92+
}
93+
},
94+
);
95+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: signatures
3+
title: Custom Signature Size
4+
description: Automatically scales down electronic signatures (both typed and drawn) after creation to a custom size while maintaining their position.
5+
keywords: [signature, resize, scale, ink annotation, image annotation, electronic signatures]
6+
---
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ImageAnnotation, InkAnnotation } from "@nutrient-sdk/viewer";
2+
import { baseOptions } from "../../shared/base-options";
3+
4+
window.NutrientViewer.load({
5+
...baseOptions,
6+
theme: window.NutrientViewer.Theme.DARK,
7+
isEditableAnnotation: (annotation) => {
8+
if ("isSignature" in annotation) {
9+
return !(annotation as InkAnnotation | ImageAnnotation).isSignature;
10+
}
11+
return true;
12+
},
13+
}).then(() => {});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: signatures
3+
title: Prevent Signature Edits
4+
description: Prevents users from editing or modifying electronic signatures after they have been placed on the document.
5+
keywords: [signature, read-only, prevent edit, isEditableAnnotation, electronic signatures]
6+
---
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type {
2+
FormField,
3+
InkAnnotation,
4+
WidgetAnnotation,
5+
} from "@nutrient-sdk/viewer";
6+
import { baseOptions } from "../../shared/base-options";
7+
8+
interface Size {
9+
width: number;
10+
height: number;
11+
}
12+
13+
window.NutrientViewer.load({
14+
...baseOptions,
15+
theme: window.NutrientViewer.Theme.DARK,
16+
}).then(async (instance) => {
17+
let storedSignature: InkAnnotation | null = null;
18+
19+
const formFields = await instance.getFormFields();
20+
21+
instance.addEventListener("annotations.create", (annotations) => {
22+
const annotation = annotations.first();
23+
if (!annotation) return;
24+
25+
if (
26+
annotation instanceof window.NutrientViewer.Annotations.InkAnnotation &&
27+
annotation.isSignature
28+
) {
29+
storedSignature = annotation;
30+
}
31+
});
32+
33+
instance.addEventListener("annotations.press", (event) => {
34+
const widget = event.annotation as WidgetAnnotation;
35+
const { id } = widget;
36+
37+
const isWidgetSignature = formFields.some((form: FormField) => {
38+
if (
39+
form instanceof window.NutrientViewer.FormFields.SignatureFormField &&
40+
form.annotationIds.includes(id)
41+
) {
42+
return true;
43+
}
44+
return false;
45+
});
46+
47+
if (!storedSignature || !isWidgetSignature) return;
48+
49+
event.preventDefault?.();
50+
instance.create(translateInkAnnotation(storedSignature, widget));
51+
});
52+
53+
function translateInkAnnotation(
54+
annotation: InkAnnotation,
55+
widget: WidgetAnnotation,
56+
) {
57+
const newSize = fitIn(
58+
{
59+
width: annotation.boundingBox.width,
60+
height: annotation.boundingBox.height,
61+
},
62+
{
63+
width: widget.boundingBox.width,
64+
height: widget.boundingBox.height,
65+
},
66+
);
67+
68+
const newLeft =
69+
widget.boundingBox.left +
70+
widget.boundingBox.width / 2 -
71+
newSize.width / 2;
72+
const newTop =
73+
widget.boundingBox.top +
74+
widget.boundingBox.height / 2 -
75+
newSize.height / 2;
76+
77+
const newBoundingBox = new window.NutrientViewer.Geometry.Rect({
78+
left: newLeft,
79+
top: newTop,
80+
width: newSize.width,
81+
height: newSize.height,
82+
});
83+
84+
return annotation
85+
.set("id", window.NutrientViewer.generateInstantId())
86+
.set("pageIndex", widget.pageIndex)
87+
.set("boundingBox", newBoundingBox);
88+
}
89+
90+
function fitIn(size: Size, containerSize: Size): Size {
91+
const { width, height } = size;
92+
93+
const widthRatio = containerSize.width / width;
94+
const heightRatio = containerSize.height / height;
95+
96+
const ratio = Math.min(widthRatio, heightRatio);
97+
98+
return {
99+
width: width * ratio,
100+
height: height * ratio,
101+
};
102+
}
103+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: signatures
3+
title: Reuse Signature in Form Fields
4+
description: Stores a drawn signature and allows placing it into signature form field widgets by clicking on them, automatically scaling to fit.
5+
keywords: [signature, form-field, ink-annotation, widget, reuse]
6+
---

0 commit comments

Comments
 (0)