Skip to content

Commit 89edd87

Browse files
committed
feat: improve types and add safe guards for extension loader
1 parent 96f9290 commit 89edd87

File tree

4 files changed

+62
-18
lines changed

4 files changed

+62
-18
lines changed

packages/react-dsfr-tiptap/README.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ Ce dépôt contient :
1616

1717
## Installation
1818

19+
Note de compatibilité
20+
21+
- Cette librairie cible Tiptap v3 (React 18+). Assurez-vous d'installer les extensions en version 3.x.
22+
- `@tiptap/starter-kit` inclut désormais des extensions comme `Link` et `Underline`. Si vous fournissez vos propres versions (par ex. via chargement dynamique), désactivez celles du StarterKit pour éviter les doublons.
23+
- Si vous utilisez Jest, certains packages ESM (ex: `@tiptap/markdown``marked`) nécessitent d'être transformés. Dans `jest.config.js`, ajoutez par exemple `transformIgnorePatterns: ["/node_modules/(?!(@codegouvfr|@tiptap/markdown|marked)/)"]`.
24+
1925
### Texte Riche
2026

2127
Pour installer ce package dans votre projet React lancez la commande:
@@ -55,7 +61,7 @@ Importez le fichier:
5561
import "react-dsfr-tiptap/index.css";
5662
```
5763

58-
Et utilisez la classe `fr-tiptap sur le conteneur qui va afficher le code HTML:
64+
Et utilisez la classe `fr-tiptap` sur le conteneur qui va afficher le code HTML:
5965

6066
```tsx
6167
<div className="fr-tiptap" dangerouslySetInnerHTML={{ __html: htmlContent }}></div>
@@ -147,7 +153,7 @@ Puis configurez le composant `<RichTextEditor>` en lui ajoutant les extensions e
147153

148154
```tsx
149155
import { RichTextEditor } from "react-dsfr-tiptap";
150-
import StarterKit from "@tiptap/extension-kit";
156+
import StarterKit from "@tiptap/starter-kit";
151157

152158
import Color from "@tiptap/extension-color";
153159
import Highlight from "@tiptap/extension-highlight";
@@ -169,7 +175,20 @@ function MyComponent() {
169175
["AlignLeft", "AlignCenter", "AlignRight", "AlignJustify"],
170176
["Undo", "Redo"],
171177
]}
172-
extensions={[StarterKit, Color, Highlight, Subscript, Superscript, TextAlign, TextStyle, Underline]}
178+
extensions={[
179+
StarterKit.configure({
180+
// Désactivez Link/Underline si vous ajoutez vos versions personnalisées
181+
// link: false,
182+
// underline: false,
183+
}),
184+
Color,
185+
Highlight,
186+
Subscript,
187+
Superscript,
188+
TextAlign,
189+
TextStyle,
190+
Underline,
191+
]}
173192
onContentUpdate={setContent}
174193
/>
175194
);
@@ -200,7 +219,7 @@ et activez les boutons dans le menu:
200219
import { markdownEditorDefaultControls, RichTextEditor } from "react-dsfr-tiptap";
201220
import { ControlImage, ControlLink, ControlUnlink, ControlYoutube } from "react-dsfr-tiptap/dialog";
202221
import "react-dsfr-tiptap/index.css";
203-
import StarterKit from "@tiptap/extension-kit";
222+
import StarterKit from "@tiptap/starter-kit";
204223

205224
import Image from "@tiptap/extension-image";
206225
import Link from "@tiptap/extension-link";
@@ -213,7 +232,15 @@ function MyComponent() {
213232
<RichTextEditor
214233
content={content}
215234
controls={[...markdownEditorDefaultControls, [ControlLink, ControlUnlink], [ControlImage, ControlImage]]}
216-
extensions={[StarterKit, Image, Link, Youtube]}
235+
extensions={[
236+
StarterKit.configure({
237+
// Désactivez link si vous souhaitez gérer un Link personnalisé
238+
// link: false,
239+
}),
240+
Image,
241+
Link,
242+
Youtube,
243+
]}
217244
onContentUpdate={setContent}
218245
/>
219246
<div className="fr-tiptap" dangerouslySetInnerHTML={{ __html: content }}></div>
@@ -228,7 +255,7 @@ ou via la props `controlMap`:
228255
import { RichTextEditor } from "react-dsfr-tiptap";
229256
import { ControlImage, ControlLink, ControlUnlink, ControlYoutube } from "react-dsfr-tiptap/dialog";
230257
import "react-dsfr-tiptap/index.css";
231-
import StarterKit from "@tiptap/extension-kit";
258+
import StarterKit from "@tiptap/starter-kit";
232259

233260
import Image from "@tiptap/extension-image";
234261
import Link from "@tiptap/extension-link";

packages/react-dsfr-tiptap/src/components/Loader.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReactNode, useEffect, useMemo, useState } from "react";
2-
import { AnyExtension } from "@tiptap/react";
2+
import { AnyExtension } from "@tiptap/core";
33
import StarterKit from "@tiptap/starter-kit";
44

55
import { Control } from "../types/controls";
@@ -85,20 +85,37 @@ function Loader(props: ILoaderProps) {
8585
const [extensions, setExtensions] = useState<Partial<Record<Extension, AnyExtension>>>(() =>
8686
Object.fromEntries(props.extensions?.map((extension) => [extension.name, extension]) ?? [["starterKit", StarterKit]])
8787
);
88-
const extensionsToLoad = useMemo(() => {
89-
const neededExtensions = [
88+
const { extensionsToLoad, missingExtensions } = useMemo(() => {
89+
const neededFromControls = controls
90+
.flat()
91+
.filter((feature) => typeof feature === "string")
92+
.map((feature) => extensionMapping[feature]);
93+
94+
const neededExtensions = [...new Set(neededFromControls.filter((name) => extensionLoader[name]))];
95+
const loadedExtensions = Object.keys(extensions);
96+
const extensionsToLoad = neededExtensions.filter((name) => !loadedExtensions.includes(name));
97+
98+
// Detect controls that require an extension which is neither already loaded nor provided by the loader.
99+
const missingExtensions = [
90100
...new Set(
91-
controls
92-
.flat()
93-
.filter((feature) => typeof feature === "string")
94-
.map((feature) => extensionMapping[feature])
95-
.filter((name) => extensionLoader[name])
101+
neededFromControls.filter(
102+
(name) => name !== "starterKit" && !loadedExtensions.includes(name) && !extensionLoader[name as keyof typeof extensionLoader]
103+
)
96104
),
97105
];
98-
const loadedExtensions = Object.keys(extensions);
99-
return neededExtensions.filter((name) => !loadedExtensions.includes(name));
106+
107+
return { extensionsToLoad, missingExtensions };
100108
}, [controls, extensionLoader, extensions]);
101109

110+
// Dev-time hint to help consumers wire required extensions
111+
if (missingExtensions.length > 0) {
112+
console.warn(
113+
`[react-dsfr-tiptap] Missing extensions for controls: ${missingExtensions.join(", ")}\n` +
114+
`Provide them via ` +
115+
`extensionLoader={{ ${missingExtensions.map((n) => `${n}: () => import('@tiptap/extension-${n}').then(m => m.default)`).join(", ")} }}`
116+
);
117+
}
118+
102119
useEffect(() => {
103120
if (extensionsToLoad.length > 0) {
104121
Promise.all(extensionsToLoad.map((name) => extensionLoader[name]!())).then((loadedExtensions) => {

packages/react-dsfr-tiptap/src/constants/markdownEditor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Markdown } from "@tiptap/markdown";
2-
import { AnyExtension } from "@tiptap/react";
2+
import { AnyExtension } from "@tiptap/core";
33
import StarterKit from "@tiptap/starter-kit";
44

55
import { MarkdownControl } from "../types/controls";

packages/react-dsfr-tiptap/src/constants/richTextEditor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AnyExtension } from "@tiptap/react";
1+
import { AnyExtension } from "@tiptap/core";
22
import StarterKit from "@tiptap/starter-kit";
33

44
import { Control } from "../types/controls";

0 commit comments

Comments
 (0)