Skip to content

Commit c6ad5c0

Browse files
committed
feat: allows passing configuration of intersection parameters to control when a code block is highlighted
1 parent e912c06 commit c6ad5c0

File tree

4 files changed

+68
-43
lines changed

4 files changed

+68
-43
lines changed

README.md

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
Enhance your React-Notion-X projects with a versatile code block component.
32
This component offers out-of-the-box support for multiple programming languages and automatically adapts to light and dark themes,
43
powered by [Shiki](https://github.com/shikijs/shiki).
@@ -28,7 +27,7 @@ To use the component, import Code from the package and include it in your Notion
2827

2928
```tsx
3029
import { Code } from "react-notion-x-code-block";
31-
import {NotionRenderer} from "react-notion-x";
30+
import { NotionRenderer } from "react-notion-x";
3231

3332
<NotionRenderer
3433
// ...
@@ -76,7 +75,7 @@ And then import it to the page:
7675

7776
```tsx
7877
import { Code } from "react-notion-x-code-block";
79-
import {NotionRenderer} from "react-notion-x";
78+
import { NotionRenderer } from "react-notion-x";
8079

8180
import "./style.css";
8281

@@ -89,9 +88,11 @@ import "./style.css";
8988
```
9089

9190
### Personalization settings
92-
Since `NotionRenderer` will only accept react components as props, we need to wrapper `Code` component and set specific settings.
91+
92+
Since `NotionRenderer` will only accept react components as props, we need to wrapper `Code` component and set specific settings.
9393

9494
**Specific theme**
95+
9596
```tsx
9697
import { type CodeBlock, ExtendedRecordMap } from "notion-types";
9798
import { Code } from "react-notion-x-code-block";
@@ -116,26 +117,22 @@ import { type CodeBlock, ExtendedRecordMap } from "notion-types";
116117
import { Code } from "react-notion-x-code-block";
117118

118119
function PersonalizedCode({ block }: { block: CodeBlock }) {
119-
return (
120-
<Code
121-
block={block}
122-
showCopy={false}
123-
/>
124-
);
120+
return <Code block={block} showCopy={false} />;
125121
}
126122
```
127123

128124
## API
129125

130-
| Property | Description | Type | Default |
131-
|-----------------|----------------------------------------------------------------------------| --------- | -------------------------------------------- |
132-
| block | Receives render code content from `NotionRenderer` | CodeBlock | - |
133-
| className | Additional class for Code | string | - |
134-
| defaultLanguage | Default programming language if not specified in `block` | string | typescript |
135-
| themes | Themes for rendering code | object | {light: "catppuccin-latte", dark: "dracula"} |
136-
| showCopy | Whether to show the copy button on the top right corner | boolean | true |
137-
| showLangLabel | Whether to show the language type label on the top left corner | boolean | true |
138-
| lazyLoading | Whether to run highlighting rendering when a code block is within viewport | boolean | true |
126+
| Property | Description | Type | Default |
127+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------- | -------------------------------------------- |
128+
| block | Receives render code content from `NotionRenderer` | CodeBlock | - |
129+
| className | Additional class for Code | string | - |
130+
| defaultLanguage | Default programming language if not specified in `block` | string | typescript |
131+
| themes | Themes for rendering code | object | {light: "catppuccin-latte", dark: "dracula"} |
132+
| IntersectionObserverOptions | Manage the conditions under which the highlighting of a code block should be triggered (Need `lazyLoading` property to be true) | object | {rootMargin: "0px",threshold: 0.1} |
133+
| showCopy | Whether to show the copy button on the top right corner | boolean | true |
134+
| showLangLabel | Whether to show the language type label on the top left corner | boolean | true |
135+
| lazyLoading | Whether to run highlighting rendering when a code block is within viewport | boolean | true |
139136

140137
## Run the Example
141138

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-notion-x-code-block",
3-
"version": "0.3.1",
3+
"version": "0.4.0",
44
"description": "Enhance your React-Notion-X projects with a code block component. This component offers out-of-the-box support for multiple programming languages and automatically adapts to light and dark themes",
55
"homepage": "https://react-notion-x-code-block.vercel.app",
66
"type": "module",

src/code.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import { getBlockTitle } from "notion-utils";
22
import { IoMdCopy } from "react-icons/io";
3-
import React, { useCallback, useEffect, useRef, useState } from "react";
3+
import React, {
4+
memo,
5+
useCallback,
6+
useEffect,
7+
useMemo,
8+
useRef,
9+
useState
10+
} from "react";
411
import { cs, useNotionContext, Text } from "react-notion-x";
512
import { codeToHtml } from "shiki";
6-
import { observerManager } from "./manager";
13+
import { ObserverManager, type ObserverManagerProps } from "./manager";
714
import styles from "./code.module.css";
815

916
import type { CodeBlock } from "notion-types";
1017
import type { BundledTheme } from "shiki/themes";
1118

12-
export const Code: React.FC<{
19+
const _Code: React.FC<{
1320
block: CodeBlock;
1421
defaultLanguage?: string;
1522
className?: string;
1623
showCopy?: boolean;
1724
showLangLabel?: boolean;
1825
lazyRendering?: boolean;
26+
IntersectionObserverOptions?: ObserverManagerProps;
1927
themes?: {
2028
light: BundledTheme;
2129
dark: BundledTheme;
@@ -28,6 +36,10 @@ export const Code: React.FC<{
2836
light: "catppuccin-latte",
2937
dark: "dracula"
3038
},
39+
IntersectionObserverOptions = {
40+
rootMargin: "0px",
41+
threshold: 0.1
42+
},
3143
showCopy = true,
3244
showLangLabel = true,
3345
lazyRendering = true
@@ -38,6 +50,9 @@ export const Code: React.FC<{
3850
const [isCopied, setIsCopied] = useState(false);
3951
const timer = useRef<null | number>(null);
4052
const codeRef = useRef<HTMLDivElement | null>(null);
53+
const observerManager = useMemo(() => {
54+
return ObserverManager.getInstance(IntersectionObserverOptions);
55+
}, [IntersectionObserverOptions]);
4156

4257
const renderCodeToHtml = useCallback(async () => {
4358
const htmlCode = await codeToHtml(content, {
@@ -52,7 +67,13 @@ export const Code: React.FC<{
5267
if (codeRef.current) {
5368
observerManager.unobserve(codeRef.current);
5469
}
55-
}, [content, block.properties?.language, defaultLanguage, themes]);
70+
}, [
71+
content,
72+
block.properties?.language,
73+
defaultLanguage,
74+
themes,
75+
observerManager
76+
]);
5677

5778
useEffect(() => {
5879
// if code is not null (means the highlighted codes has been rendered), we don't want to observe it again
@@ -72,7 +93,7 @@ export const Code: React.FC<{
7293
return () => {
7394
unobservedElement?.();
7495
};
75-
}, [renderCodeToHtml, lazyRendering, code]);
96+
}, [renderCodeToHtml, lazyRendering, code, observerManager]);
7697

7798
const clickCopy = useCallback(() => {
7899
navigator.clipboard.writeText(content).then(() => {
@@ -114,3 +135,5 @@ export const Code: React.FC<{
114135
</figure>
115136
);
116137
};
138+
139+
export const Code = memo(_Code);

src/manager.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
11
type HandlerFunction = (element: Element) => void;
2+
export type ObserverManagerProps = Pick<
3+
IntersectionObserverInit,
4+
"rootMargin" | "threshold"
5+
>;
6+
7+
export class ObserverManager {
8+
private static instances: Map<string, ObserverManager> = new Map();
29

3-
class ObserverManager {
410
private observer?: IntersectionObserver;
511
private handlers: Map<Element, HandlerFunction>;
612

7-
constructor() {
13+
static getInstance(options: ObserverManagerProps): ObserverManager {
14+
const key = `${options.rootMargin}-${options.threshold}`;
15+
if (!ObserverManager.instances.has(key))
16+
ObserverManager.instances.set(key, new ObserverManager(options));
17+
return ObserverManager.instances.get(key)!;
18+
}
19+
20+
constructor(options: IntersectionObserverInit) {
821
this.handlers = new Map();
922

1023
// Skip when SSR
1124
if (typeof window === "undefined") {
1225
return;
1326
}
1427

15-
this.observer = new IntersectionObserver(
16-
(entries) => {
17-
entries.forEach((entry) => {
18-
const handler = this.handlers.get(entry.target);
19-
if (handler && entry.isIntersecting) {
20-
handler(entry.target);
21-
}
22-
});
23-
},
24-
{
25-
rootMargin: "0px",
26-
threshold: 0.1
27-
}
28-
);
28+
this.observer = new IntersectionObserver((entries) => {
29+
entries.forEach((entry) => {
30+
const handler = this.handlers.get(entry.target);
31+
if (handler && entry.isIntersecting) {
32+
handler(entry.target);
33+
}
34+
});
35+
}, options);
2936
}
3037

3138
// Return unobserve callback directly
@@ -41,5 +48,3 @@ class ObserverManager {
4148
this.observer?.unobserve(element);
4249
}
4350
}
44-
45-
export const observerManager = new ObserverManager();

0 commit comments

Comments
 (0)