Skip to content

Commit b7cd75c

Browse files
authored
feat: add continuous local dev and coloured highlight layer for multi colour highlight support (#55)
1 parent 08275c5 commit b7cd75c

File tree

14 files changed

+408
-41
lines changed

14 files changed

+408
-41
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,39 @@ export default function PDFViewer() {
5050
}
5151
```
5252

53+
## Local Development using PNPM and Yalc
54+
55+
When you are using "pnpm link", you are bound to use pnpm on your consumer project when you are developing locally.
56+
With yalc, we are decoupling the need for pnpm and now the package can be tested with any package managers. Any
57+
changes should be automatically published to yalc on save, forcing a rebuilt and updating the consumer project.
58+
59+
Install yalc globally:
60+
61+
```
62+
pnpm i yalc -g
63+
```
64+
65+
From lector:
66+
67+
```bash
68+
# navigate to lector package folder and install dependencies
69+
pnpm i
70+
# when you first start development, make sure you publish the package locally
71+
yalc publish
72+
# and run the project in development mode to start a watcher that rebuilds the project and pushes the changes locally on save
73+
pnpm dev
74+
```
75+
76+
From consumer project:
77+
(It doesn't really matter what package manager you are using)
78+
79+
```bash
80+
# add local package to your package.json of the consumer project using yalc
81+
yalc add @anaralabs/lector
82+
# or if you don't want to add the yalc package in your package.json
83+
yalc link @anaralabs/lector
84+
```
85+
5386
## Features
5487

5588
- 📱 Responsive and mobile-friendly

packages/lector/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"access": "public"
2323
},
2424
"scripts": {
25-
"dev": "tsup --watch",
25+
"dev": "tsup --watch --onSuccess 'yalc push'",
2626
"prebuild": "rm -rf dist",
2727
"build": "tsup",
2828
"lint": "eslint .",
@@ -74,6 +74,7 @@
7474
"@tanstack/react-virtual": "^3.10.9",
7575
"react-dom": "18.3.1",
7676
"use-debounce": "^10.0.4",
77+
"uuid": "^11.1.0",
7778
"zustand": "^5.0.2"
7879
},
7980
"optionalDependencies": {

packages/lector/scripts/prepack.sh

100644100755
File mode changed.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { ColoredHighlight } from "../../../internal";
2+
import { SelectionTooltip } from "../../selection-tooltip";
3+
4+
type ColorSelectionToolProps = {
5+
highlighterColors?: colorItem[];
6+
onColorSelection: (colorItem: colorItem) => void;
7+
};
8+
9+
type colorItem = {
10+
color: string;
11+
localization: {
12+
id: string;
13+
defaultMessage: string;
14+
};
15+
};
16+
17+
const defaultColors: colorItem[] = [
18+
{
19+
color: "#e3b127",
20+
localization: {
21+
id: "yellow",
22+
defaultMessage: "Yellow",
23+
},
24+
},
25+
{
26+
color: "#419931",
27+
localization: {
28+
id: "green",
29+
defaultMessage: "Green",
30+
},
31+
},
32+
{
33+
color: "#4286c9",
34+
localization: {
35+
id: "blue",
36+
defaultMessage: "Blue",
37+
},
38+
},
39+
{
40+
color: "#f246b6",
41+
localization: {
42+
id: "pink",
43+
defaultMessage: "Pink",
44+
},
45+
},
46+
{
47+
color: "#a53dd1",
48+
localization: {
49+
id: "purple",
50+
defaultMessage: "Purple",
51+
},
52+
},
53+
{
54+
color: "#f09037",
55+
localization: {
56+
id: "orange",
57+
defaultMessage: "Orange",
58+
},
59+
},
60+
{
61+
color: "#37f0d4",
62+
localization: {
63+
id: "teal",
64+
defaultMessage: "Teal",
65+
},
66+
},
67+
{
68+
color: "#3d0ff5",
69+
localization: {
70+
id: "purple",
71+
defaultMessage: "Purple",
72+
},
73+
},
74+
{
75+
color: "#f50f26",
76+
localization: {
77+
id: "red",
78+
defaultMessage: "Red",
79+
},
80+
},
81+
];
82+
83+
export const ColorSelectionTool = ({
84+
highlighterColors = defaultColors,
85+
onColorSelection,
86+
}: ColorSelectionToolProps) => {
87+
return (
88+
<SelectionTooltip>
89+
<div
90+
style={{
91+
display: "flex",
92+
gap: "0.5rem",
93+
padding: "0.5rem",
94+
backgroundColor: "#363636",
95+
borderRadius: "0.5rem",
96+
}}
97+
>
98+
{highlighterColors.map((colorItem, index) => (
99+
<button
100+
key={index}
101+
onClick={() => onColorSelection(colorItem)}
102+
title={colorItem.localization.defaultMessage}
103+
aria-label={colorItem.localization.defaultMessage}
104+
style={{
105+
width: "1.25rem",
106+
height: "1.25rem",
107+
borderRadius: "0.25rem",
108+
cursor: "pointer",
109+
backgroundColor: colorItem.color,
110+
}}
111+
/>
112+
))}
113+
</div>
114+
</SelectionTooltip>
115+
);
116+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useCallback } from "react";
2+
import { usePDFPageNumber } from "../../../hooks/usePdfPageNumber";
3+
import { useSelectionDimensions } from "../../../hooks/useSelectionDimensions";
4+
import { usePdf, type ColoredHighlight } from "../../../internal";
5+
import { v4 as uuidv4 } from "uuid";
6+
7+
import { ColoredHighlightComponent } from "./colored-highlight";
8+
import { ColorSelectionTool } from "./color-selection-tool";
9+
10+
type ColoredHighlightLayerProps = {
11+
onHighlight?: (highlight: ColoredHighlight) => void;
12+
};
13+
14+
export const ColoredHighlightLayer = ({
15+
onHighlight,
16+
}: ColoredHighlightLayerProps) => {
17+
const pageNumber = usePDFPageNumber();
18+
const { getSelection } = useSelectionDimensions();
19+
20+
const highlights: ColoredHighlight[] = usePdf(
21+
(state) => state.coloredHighlights,
22+
);
23+
const addColoredHighlight = usePdf((state) => state.addColoredHighlight);
24+
25+
const handleHighlighting = useCallback((color: string) => {
26+
const { highlights, text } = getSelection();
27+
28+
if (highlights[0]) {
29+
const highlight: ColoredHighlight = {
30+
uuid: uuidv4(),
31+
pageNumber: highlights[0].pageNumber, // usePDFPageNumber() doesn't return the correct page number, so i'm getting the number directly from the first highlight
32+
color,
33+
rectangles: highlights,
34+
text,
35+
};
36+
addColoredHighlight(highlight);
37+
if (onHighlight) onHighlight(highlight);
38+
}
39+
}, []);
40+
41+
return (
42+
<div className="colored-highlights-layer">
43+
{highlights
44+
.filter((selection) => selection.pageNumber === pageNumber)
45+
.map((selection) => (
46+
<ColoredHighlightComponent
47+
key={selection.uuid}
48+
selection={selection}
49+
/>
50+
))}
51+
<ColorSelectionTool
52+
onColorSelection={(colorItem) => handleHighlighting(colorItem.color)}
53+
/>
54+
</div>
55+
);
56+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { usePdf, type ColoredHighlight } from "../../../internal";
2+
import {
3+
getEndOfHighlight,
4+
getMidHeightOfHighlightLine,
5+
} from "../../../utils/selectionUtils";
6+
import { useState } from "react";
7+
8+
type ColoredHighlightComponentProps = {
9+
selection: ColoredHighlight;
10+
};
11+
12+
export const ColoredHighlightComponent = ({
13+
selection,
14+
}: ColoredHighlightComponentProps) => {
15+
const deleteColoredHighlight = usePdf(
16+
(state) => state.deleteColoredHighlight,
17+
);
18+
const [showButton, setShowButton] = useState(false);
19+
20+
return (
21+
<div className="colored-highlight">
22+
{selection.rectangles.map((rect, index) => (
23+
<span
24+
key={`${selection.uuid}-${index}`}
25+
onClick={() => setShowButton(!showButton)}
26+
style={{
27+
position: "absolute",
28+
top: rect.top,
29+
left: rect.left,
30+
height: rect.height,
31+
width: rect.width,
32+
cursor: "pointer",
33+
zIndex: 30,
34+
backgroundColor: selection.color,
35+
// mixBlendMode: "lighten", // changes the color of the text
36+
mixBlendMode: "darken", // best results
37+
// mixBlendMode: "multiply", // works but coloring has some inconsistencies
38+
borderRadius: "0.2rem",
39+
}}
40+
/>
41+
))}
42+
{showButton && (
43+
<button
44+
key={`${selection.uuid}-delete-button`}
45+
style={{
46+
backgroundColor: "white",
47+
color: "white",
48+
borderRadius: "5px",
49+
padding: "5px",
50+
cursor: "pointer",
51+
boxShadow: "2px 2px 5px black",
52+
position: "absolute",
53+
top: getMidHeightOfHighlightLine(selection),
54+
left: getEndOfHighlight(selection),
55+
zIndex: 30,
56+
transform: "translateY(-50%)",
57+
}}
58+
onClick={() => deleteColoredHighlight(selection.uuid)}
59+
>
60+
<svg
61+
fill="#000000"
62+
version="1.1"
63+
id="Capa_1"
64+
xmlns="http://www.w3.org/2000/svg"
65+
width="15px"
66+
height="15px"
67+
viewBox="0 0 485 485"
68+
>
69+
<g>
70+
<g>
71+
<rect x="67.224" width="350.535" height="71.81" />
72+
<path
73+
d="M417.776,92.829H67.237V485h350.537V92.829H417.776z M165.402,431.447h-28.362V146.383h28.362V431.447z M256.689,431.447
74+
h-28.363V146.383h28.363V431.447z M347.97,431.447h-28.361V146.383h28.361V431.447z"
75+
/>
76+
</g>
77+
</g>
78+
</svg>
79+
</button>
80+
)}
81+
</div>
82+
);
83+
};

packages/lector/src/components/pages.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ export const Pages = ({
160160
: 0;
161161

162162
const maxOffset = scrollSize - size;
163-
console.log(scrollSize, maxOffset, toOffset);
164163

165164
return Math.max(toOffset, 0);
166165
};

packages/lector/src/components/selection/custom-selection-trigger.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,23 @@ export const CustomSelectionTrigger = () => {
2727
}, [getDimension, setCustomSelectionRects]);
2828

2929
useEffect(() => {
30+
const controller = new AbortController();
31+
const { signal } = controller;
3032
// Handle selection changes
31-
document.addEventListener("selectionchange", handleSelection);
33+
document.addEventListener("selectionchange", handleSelection, { signal });
3234

3335
// Handle blur events on the document
34-
document.addEventListener("blur", handleSelection, true);
36+
document.addEventListener("blur", handleSelection, {
37+
signal,
38+
capture: true,
39+
});
3540

3641
// Handle mouseup for cases where selectionchange might not fire
37-
document.addEventListener("mouseup", handleSelection);
42+
document.addEventListener("mouseup", handleSelection, { signal });
3843

3944
// Clean up all listeners
4045
return () => {
41-
document.removeEventListener("selectionchange", handleSelection);
42-
document.removeEventListener("blur", handleSelection, true);
43-
document.removeEventListener("mouseup", handleSelection);
46+
controller.abort();
4447
};
4548
}, [handleSelection]);
4649

0 commit comments

Comments
 (0)