Skip to content

Commit 02455b3

Browse files
authored
Refactor Annotations example from awesome-nutrient/playground/web-annotations.md (#163)
1 parent b5ef404 commit 02455b3

File tree

13 files changed

+317
-2
lines changed

13 files changed

+317
-2
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Annotation } from "@nutrient-sdk/viewer";
2+
import { baseOptions } from "../../shared/base-options";
3+
4+
const username = "Omar";
5+
6+
window.NutrientViewer.load({
7+
...baseOptions,
8+
theme: window.NutrientViewer.Theme.DARK,
9+
toolbarItems: [{ type: "comment" }],
10+
}).then((instance) => {
11+
const annotation =
12+
new window.NutrientViewer.Annotations.CommentMarkerAnnotation({
13+
id: "test",
14+
pageIndex: 0,
15+
boundingBox: new window.NutrientViewer.Geometry.Rect({
16+
top: 100,
17+
left: 100,
18+
width: 10,
19+
height: 10,
20+
}),
21+
customData: { circleId: "my-circle" },
22+
});
23+
24+
const comment = new window.NutrientViewer.Comment({
25+
id: "commentId",
26+
rootId: "test",
27+
pageIndex: 0,
28+
text: { format: "plain", value: "Hello" },
29+
});
30+
31+
instance.setAnnotationCreatorName(username);
32+
instance.create([annotation, comment]);
33+
34+
instance.addEventListener("annotations.create", (annotations) => {
35+
const firstAnnotation = annotations.first() as Annotation | undefined;
36+
if (firstAnnotation) {
37+
console.log(firstAnnotation.toJS());
38+
}
39+
});
40+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: annotations
3+
title: Create Comment Thread Programmatically
4+
description: Demonstrates how to programmatically create a comment marker annotation with an associated comment thread, setting the creator name and listening for annotation creation events.
5+
keywords: [comment, annotation, programmatic, CommentMarkerAnnotation, thread]
6+
---
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { CustomUI, Instance } from "@nutrient-sdk/viewer";
2+
import { baseOptions } from "../../shared/base-options";
3+
4+
// Declare instance variable
5+
let instance: Instance | null = null;
6+
7+
// Custom UI Configuration for the sidebar
8+
const customUIConfiguration: CustomUI = {
9+
[window.NutrientViewer.UIElement.Sidebar]: {
10+
[window.NutrientViewer.SidebarMode.ANNOTATIONS]: ({ containerNode }) => ({
11+
node: containerNode,
12+
onRenderItem: ({ item: annotation, itemContainerNode }) => {
13+
// If the annotation is a highlight, fetch the text within the highlight
14+
if (
15+
annotation instanceof
16+
window.NutrientViewer.Annotations.HighlightAnnotation
17+
) {
18+
instance
19+
?.getTextFromRects(annotation.pageIndex, annotation.rects)
20+
.then((text) => {
21+
const labelElement = (
22+
itemContainerNode as HTMLElement
23+
).querySelector(".BaselineUI-Text");
24+
if (labelElement) {
25+
labelElement.textContent = text;
26+
}
27+
});
28+
}
29+
},
30+
}),
31+
},
32+
};
33+
34+
// Load NutrientViewer with custom configurations
35+
window.NutrientViewer.load({
36+
...baseOptions,
37+
theme: window.NutrientViewer.Theme.DARK,
38+
customUI: customUIConfiguration,
39+
}).then((_instance) => {
40+
instance = _instance;
41+
console.log("NutrientViewer for Web successfully loaded!!", instance);
42+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: annotations
3+
title: Display Highlighted Text in Sidebar
4+
description: Customizes the annotations sidebar to show the actual highlighted text content for each highlight annotation instead of the default label.
5+
keywords: [highlight, sidebar, customUI, text, getTextFromRects]
6+
---
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { baseOptions } from "../../shared/base-options";
2+
3+
window.NutrientViewer.load({
4+
...baseOptions,
5+
theme: window.NutrientViewer.Theme.DARK,
6+
initialViewState: new window.NutrientViewer.ViewState({
7+
currentPageIndex: 1,
8+
}),
9+
}).then(async (instance) => {
10+
const linkAnnotations = await instance.getAnnotations(1);
11+
instance.setSelectedAnnotations(linkAnnotations);
12+
console.log("NutrientViewer loaded!");
13+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: annotations
3+
title: Hide Link Annotation Popover
4+
description: Hides the link editor popover that appears when selecting link annotations using CSS, while still allowing programmatic selection of links.
5+
keywords: [link, annotation, popover, hide, CSS, setSelectedAnnotations]
6+
---
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/* Add your CSS here */
2+
.PSPDFKit-Link-Editor-Popover {
3+
display: none;
4+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { baseOptions } from "../../shared/base-options";
2+
3+
window.NutrientViewer.load({
4+
...baseOptions,
5+
theme: window.NutrientViewer.Theme.DARK,
6+
}).then(async (instance) => {
7+
const request = await fetch("https://picsum.photos/id/237/300/300");
8+
const blob = await request.blob();
9+
const imageAttachmentId = await instance.createAttachment(blob);
10+
const annotation = new window.NutrientViewer.Annotations.ImageAnnotation({
11+
pageIndex: 0,
12+
contentType: "image/png",
13+
imageAttachmentId,
14+
description: "Example Image Annotation",
15+
boundingBox: new window.NutrientViewer.Geometry.Rect({
16+
left: 10,
17+
top: 20,
18+
width: 150,
19+
height: 150,
20+
}),
21+
action: new window.NutrientViewer.Actions.URIAction({
22+
uri: "https://picsum.photos/id/237/300/300",
23+
}),
24+
});
25+
await instance.create(annotation);
26+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: annotations
3+
title: Image Annotation from URL with Link Action
4+
description: Creates an image annotation by fetching an image from a URL and attaching a clickable URI action that opens the original image source.
5+
keywords: [image, annotation, URL, fetch, attachment, URIAction, link]
6+
---
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import type {
2+
CustomUI,
3+
Instance,
4+
ToolbarItem,
5+
ViewState,
6+
} from "@nutrient-sdk/viewer";
7+
import { baseOptions } from "../../shared/base-options";
8+
9+
let printHistory: string[] = [];
10+
let instance: Instance | null = null;
11+
12+
function getCurrentDateTime(): string {
13+
const now = new Date();
14+
const options: Intl.DateTimeFormatOptions = {
15+
year: "numeric",
16+
month: "long",
17+
day: "numeric",
18+
hour: "2-digit",
19+
minute: "2-digit",
20+
second: "2-digit",
21+
hour12: true,
22+
};
23+
return now.toLocaleDateString("en-US", options);
24+
}
25+
26+
function logPrintActivity(): void {
27+
const printTime = getCurrentDateTime();
28+
const printEntry = `Printed on ${printTime}`;
29+
30+
printHistory.unshift(printEntry);
31+
32+
if (printHistory.length > 10) {
33+
printHistory = printHistory.slice(0, 10);
34+
}
35+
updateCustomUI();
36+
}
37+
38+
function createCustomUI(): CustomUI {
39+
return {
40+
[window.NutrientViewer.UIElement.Sidebar]: {
41+
[window.NutrientViewer.SidebarMode.CUSTOM]: ({ containerNode }) => {
42+
const container = containerNode as HTMLElement;
43+
container.innerHTML = "";
44+
45+
const mainContainer = document.createElement("div");
46+
mainContainer.style.padding = "20px";
47+
mainContainer.style.height = "100%";
48+
mainContainer.style.overflowY = "auto";
49+
50+
const printTitle = document.createElement("h4");
51+
printTitle.innerText = "Print Activity Log";
52+
printTitle.style.margin = "0 0 15px 0";
53+
printTitle.style.color = "#fff";
54+
55+
const historyList = document.createElement("div");
56+
historyList.style.maxHeight = "400px";
57+
historyList.style.overflowY = "auto";
58+
59+
if (printHistory.length > 0) {
60+
printHistory.forEach((entry, index) => {
61+
const historyItem = document.createElement("div");
62+
historyItem.style.background = "#f8f9fa";
63+
historyItem.style.border = "1px solid #dee2e6";
64+
historyItem.style.borderRadius = "4px";
65+
historyItem.style.padding = "10px";
66+
historyItem.style.marginBottom = "8px";
67+
historyItem.style.fontSize = "14px";
68+
historyItem.style.color = "#333";
69+
const printNumber = printHistory.length - index;
70+
historyItem.innerHTML = `<strong>Print #${printNumber}</strong><br>${entry}`;
71+
historyList.appendChild(historyItem);
72+
});
73+
} else {
74+
const noHistory = document.createElement("div");
75+
noHistory.innerText = "No print activity yet";
76+
noHistory.style.color = "#6c757d";
77+
noHistory.style.fontStyle = "italic";
78+
noHistory.style.textAlign = "center";
79+
noHistory.style.padding = "20px";
80+
historyList.appendChild(noHistory);
81+
}
82+
83+
mainContainer.appendChild(printTitle);
84+
mainContainer.appendChild(historyList);
85+
container.appendChild(mainContainer);
86+
87+
return { node: containerNode };
88+
},
89+
},
90+
};
91+
}
92+
93+
function updateCustomUI(): void {
94+
if (!instance) return;
95+
instance.setCustomUIConfiguration(createCustomUI());
96+
}
97+
98+
function setupPrintEventListeners(): void {
99+
window.addEventListener("beforeprint", () => {
100+
logPrintActivity();
101+
});
102+
103+
document.addEventListener("keydown", (event) => {
104+
if ((event.ctrlKey || event.metaKey) && event.key === "p") {
105+
setTimeout(() => {
106+
logPrintActivity();
107+
}, 100);
108+
}
109+
});
110+
}
111+
112+
window.NutrientViewer.load({
113+
...baseOptions,
114+
theme: window.NutrientViewer.Theme.DARK,
115+
enableHistory: true,
116+
enableClipboardActions: true,
117+
}).then((instanceRef) => {
118+
instance = instanceRef;
119+
120+
instance.setCustomUIConfiguration(createCustomUI());
121+
122+
const custom: ToolbarItem = {
123+
type: "custom",
124+
id: "Print Activity Log",
125+
title: "Print Activity Log",
126+
selected: false,
127+
dropdownGroup: "sidebar",
128+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
129+
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
130+
</svg>`,
131+
onPress: () => {
132+
instance?.setViewState((viewState: ViewState) =>
133+
viewState.set("sidebarMode", "CUSTOM"),
134+
);
135+
},
136+
};
137+
138+
const toolbarItems = window.NutrientViewer.defaultToolbarItems.reduce<
139+
ToolbarItem[]
140+
>(
141+
(acc, item) =>
142+
"sidebar-thumbnails" === item.type
143+
? acc.concat([item, custom])
144+
: acc.concat([item]),
145+
[],
146+
);
147+
148+
instance.setToolbarItems([...toolbarItems]);
149+
150+
instance.setViewState(instance.viewState.set("sidebarMode", "CUSTOM"));
151+
152+
setupPrintEventListeners();
153+
});

0 commit comments

Comments
 (0)