From ff2b9c85df863595b8b51a13e693fff73e55054a Mon Sep 17 00:00:00 2001 From: Tony CABAYE Date: Thu, 2 Jan 2025 14:31:45 +0100 Subject: [PATCH] docs: update README --- README.md | 331 ++++++++++++++++-- examples/src/Custom.tsx | 3 +- examples/src/Tiptap.tsx | 4 +- examples/src/TiptapCustomButtons.tsx | 147 ++++---- .../src/components/MarkdownEditor.tsx | 33 +- .../src/components/RichTextEditor.tsx | 6 +- tests/README.md | 1 - 7 files changed, 400 insertions(+), 125 deletions(-) delete mode 100644 tests/README.md diff --git a/README.md b/README.md index 36b7912..a296da4 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,328 @@ -# Modèle de ReadMe +# react-dsfr-tiptap -Ci-dessous une proposition de readme pour tout projet +Composant de texte riche React pour le [System de design du gouvernement français (alias DSFR) 🇫🇷](https://www.systeme-de-design.gouv.fr/). +## Description -## Description/Résumé du projet +Ce dépôt contient : -Dans cette section, on décrit la vision générale du projet ainsi que ses objectifs à destination des futurs utilisateurs et des développeurs. +- la documentation (ce fichier Readme) +- la librairie du composant de texte riche dans `packages/react-dsfr-tiptap` +- des examples d'intégration dans `examples` -Pour ce dépôt : +## Installation -Ce dépôt permet de répertorier les différents éléments essentiels dans un dépôt SIDC : -* ce ReadMe -* des modèles de tickets type pour des ajouts de fonctionnalité, réparation de bug, ajout de documentation, maintenance/montée de version... -* des milestones témoins (ex: backlog, sprint1...) -* des exemples de github Actions CI/CD (lancement de test, build d'images...) -* des exemples de label pour les futurs tickets +### Texte Riche +Pour installer ce package dans votre projet React lancez la commande: -## Installation +```bash +npm i react-dsfr-tiptap +``` + +(ou installez avec le package manager de votre choix) + +Vous devez également avoir installé les dépendances suivantes sur votre projet pour que ce package fonctionne: + +- `react` +- `react-dom` +- `@codegouvfr/react-dsfr` +- `tss-react` + +### Editeur Markdown + +Pour utiliser l'éditeur markdown, installez les dépendances supplémentaires avec la commande: + +```bash +npm i tiptap-markdown +``` + +(ou installez avec le package manager de votre choix) + +## Utilisation + +### Texte Riche + +Une fois installée vous pouvez utilisé le composant `RichTextEditor` de cette manière: + +```tsx +import { RichTextEditor } from "react-dsfr-tiptap"; + +function MyComponent() { + const [content, setContent] = useState(`

Content title

`); + return ( + <> + +
+ + ); +} +``` + +### Editeur Markdown + +Utilisez l'éditeur markdown en important le composant `MarkdownEditor` depuis `react-dsfr-tiptap/markdown` + +```tsx +import { MarkdownEditor } from "react-dsfr-tiptap/markdown"; + +function MyComponent() { + const [content, setContent] = useState(`## Markdown title`); + return ( + <> + +
{content}
+ + ); +} +``` + +### Utilisation avancée + +Vous pouvez également utiliser les composants de plus bas niveau pour construire votre composant de texte riche: + +```tsx +import StarterKit from "@tiptap/starter-kit"; +import { RichTextEditor } from "react-dsfr-tiptap"; + +function MyComponent() { + const [content, setContent] = useState(`## Markdown title`); + + return ( + editor.getHTML()}> + + + + + + + + + + + ); +} +``` + +Ils vous faudra fournir alors les extensions et configurer le menu par vous même. + +## Ajout d'extensions + +### Boutons classiques + +Les extensions tiptap suivantes sont d'office prise en charge avec le composant `RichTextEditor`. + +Il suffit de les installer pour que les boutons apparaissent dans le menu: + +- `@tiptap/extension-color` +- `@tiptap/extension-highlight` +- `@tiptap/extension-subscript` +- `@tiptap/extension-superscript` +- `@tiptap/extension-text-align` +- `@tiptap/extension-text-style` +- `@tiptap/extension-underline` + +Ces extensions ne fonctionnent qu'avec le composant `RichTextEditor` et pas le composant `MarkdownEditor`. + +### Boutons avec modale + +Les extensions suivantes sont également prise en charge mais nécessite une configuration supplémentaire: -La procédure d'installation du projet doit être décrite dans cette section ou dans un fichier complémentaire dont le lien est présent ici. +- `@tiptap/extension-image` +- `@tiptap/extension-link` +- `@tiptap/extension-youtube` +Pour utiliser ces extensions installez les packages supplémentaires suivants: -## Documentation développeurs +```bash +npm i react-hook-form @hookform/resolvers yup validator +``` -Lien vers la documentation pour les développeurs, à la fois pour maintenir le projet, le déployer et ajouter de nouvelles fonctionnalités. Schémas UML... +et activez les boutons dans le menu via la props `controlMap`: +```tsx +import { RichTextEditor } from "react-dsfr-tiptap"; +import { ControlImage, ControlLink, ControlUnlink, ControlYoutube } from "react-dsfr-tiptap/dialog"; + +function MyComponent() { + const [content, setContent] = useState(`

Content title

`); + return ( + <> + +
+ + ); +} +``` + +Cela fonctionne de la même manière pour le composant `MarkdownEditor` sauf qu'il ne supporte que les liens et les images (et pas les vidéos). + +## Configuration + +### Props + +Les 2 composants `RichTextEditor` et `MarkdownEditor` fonctionne de la même manière et ont les mêmes props: + +| Props | Type | Valeur par défaut | Description | +| ----------------- | ------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------- | +| `content` | `string` | `""` | Contenu de la zone de texte riche | +| `controlMap` | `Partial ReactNode) \| LazyExoticComponent<() => ReactNode>>>` | `{}` | Permet de configurer les composants des boutons préconfigurés | +| `controls` | `(Control \| (() => ReactNode) \| LazyExoticComponent<() => ReactNode>)[][]` | `defaultControls` | Permet de configurer les boutons du menu | +| `extensions` | `AnyExtension[]` | `defaultExtensions` | Permet d'ajouter des extensions | +| `menu` | `"top" \| "bottom"` | `"top"` | Position du menu | +| `onContentUpdate` | `(content: string) => void` | | Fonction appelé quand le contenu est mis à jour par l'utilisateur | + +Les autres props seront passé au hoos `useEditor` de [la librairie `@tiptap/react`](https://tiptap.dev/docs/editor/getting-started/install/react). + +Pour le composant `RichTextEditor`: + +- `defaultControls` est égal à: + ```ts + [ + ["Bold", "Italic", "Underline", "Strike", "Subscript", "Superscript", "Code", "Highlight", "Color", "ClearFormatting"], + ["H1", "H2", "H3", "H4", "H5", "H6", "Paragraph"], + ["BulletList", "OrderedList", "CodeBlock", "Blockquote", "HorizontalRule"], + ["AlignLeft", "AlignCenter", "AlignRight", "AlignJustify"], + ["Undo", "Redo"], + ["Link", "Unlink"], + ["Image", "Youtube"], + ]; + ``` +- `defaultExtensions` est égal à: `[require("@tiptap/starter-kit")]` + +Pour le composant `RichTextEditor`: + +- `defaultControls` est égal à: + ```ts + [ + ["Bold", "Italic", "Code", "ClearFormatting"], + ["H1", "H2", "H3", "H4", "H5", "H6", "Paragraph"], + ["BulletList", "OrderedList", "CodeBlock", "Blockquote", "HorizontalRule"], + ["Undo", "Redo"], + ["Link", "Unlink"], + ["Image"], + ]; + ``` +- `defaultExtensions` est égal à: `[require("@tiptap/starter-kit"), require("tiptap-markdown")]` + +### Ajout de boutons personnalisés + +Vous pouvez développer et ajouter vos propres boutons dans le composant de texte riche. + +Exemple de bouton: + +```tsx +import { Editor, useEditorState } from "@tiptap/react"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { useEditor } from "react-dsfr-tiptap"; + +export default function CustomControl() { + const editor = useEditor(); + const editorState = useEditorState({ + editor, + selector: ({ editor }: { editor: Editor }) => ({ + disabled: !editor?.can().chain().focus().insertContent("[custom]").run(), + }), + }); + + return ( + + ); +} +``` + +Dans ce cas vous pouvez ajoutez votre bouton personnalisé via la props `controls`: + +```tsx +import { RichTextEditor } from "react-dsfr-tiptap"; +import CustomControl from "./CustomControl"; + +function MyComponent() { + const [content, setContent] = useState(`

Content title

`); + return ( + <> + +
+ + ); +} +``` + +Dans ce cas il vous faudra fournir la liste complète des contrôles. + +### Utilitaire de création de boutons personalisés + +Vous pouvez aussi utilisez les utilitaires suivants pour faciliter la création de boutons personalisées: + +```tsx +import { createControl } from "react-dsfr-tiptap"; + +export default createControl({ + buttonProps: { children: "Insérer du contenu" }, + operation: { name: "insertContent", attributes: "[custom]" }, +}); +``` + +Cela créera exactement le même contrôle `CustomControl` que celui du chapitre précédent. + +De la même manière il suffira ensuite de l'ajouter à la props `controls` du composant `RichTextEditor`. + +Il y a également un utilitaire pour créer un bouton qui ouvre une modale: + +```tsx +import { createDialogControl } from "react-dsfr-tiptap"; + +export default createDialogControl({ + buttonProps: { children: "Insérer du contenu" }, + DialogContent: CustomDialog, // Le composant de modale + onClick: (editor, ref) => ref.current?.open(), +}); +``` + +Pour un example plus complet regardez le fichier `examples/src/TiptapCustomButtons.tsx`. ## L'arborescence du projet Exemple d'arborescence de projet : -* `.github/` : dossier contenant les modèles d'issues et github actions ; -* `.vscode/` : dossier contenant une configuration vscode pour le projet; -* `doc/` : dossier contenant des fichiers .md de documentation (ex: install.md) ; -* `tests/`: scripts et explications pour lancer les tests ; -* `README.md` : ce fichier +- `.github/` : dossier contenant les modèles d'issues et github actions. +- `.husky/` : dossier contenant des scripts git hooks. +- `.vscode/` : dossier contenant une configuration vscode pour le projet. +- `doc/` : dossier contenant des fichiers .md de documentation (ex: install.md). +- `examples/` : dossier contenant une application avec des examples d'utilisation. +- `packages/` : dossier contenant le code source de la librarie. +- `README.md` : ce fichier. ## Contacts du projets Ici on met la liste des personnes qui travaillent sur ce projet et le maintiennent à jour. - -|Nom|Prénom|mail|fonction| -|---|---|---|---| -| | | | | -| | | | | -| | | | | +| Nom | Prénom | mail | fonction | +| --- | ------ | ---- | -------- | +| | | | | +| | | | | +| | | | | diff --git a/examples/src/Custom.tsx b/examples/src/Custom.tsx index 634ccf4..6561ac6 100644 --- a/examples/src/Custom.tsx +++ b/examples/src/Custom.tsx @@ -4,7 +4,7 @@ import { ControlImage, ControlLink, ControlUnlink, ControlYoutube } from "react- import StarterKit from "@tiptap/starter-kit"; import Link from "@tiptap/extension-link"; -import { CustomControl1, CustomControl2 } from "./TiptapCustomButtons"; +import { CustomControl1, CustomControl2, CustomControl3 } from "./TiptapCustomButtons"; const Custom = () => { const [content, setContent] = useState(` @@ -23,6 +23,7 @@ this is a basic example of Tiptap. Sure, there are all kind of + diff --git a/examples/src/Tiptap.tsx b/examples/src/Tiptap.tsx index fe2083b..61040a8 100644 --- a/examples/src/Tiptap.tsx +++ b/examples/src/Tiptap.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { RichTextEditor } from "react-dsfr-tiptap"; import { ControlImage, ControlLink, ControlUnlink, ControlYoutube } from "react-dsfr-tiptap/dialog"; -import { CustomControl1, CustomControl2 } from "./TiptapCustomButtons"; +import { CustomControl1, CustomControl2, CustomControl3 } from "./TiptapCustomButtons"; const initialContent = `

@@ -51,7 +51,7 @@ const Tiptap = () => { ["Undo", "Redo"], ["Link", "Unlink"], ["Image", "Youtube"], - [CustomControl1, CustomControl2], + [CustomControl1, CustomControl2, CustomControl3], ]} onContentUpdate={setContent} /> diff --git a/examples/src/TiptapCustomButtons.tsx b/examples/src/TiptapCustomButtons.tsx index 347e5a6..52b34a5 100644 --- a/examples/src/TiptapCustomButtons.tsx +++ b/examples/src/TiptapCustomButtons.tsx @@ -1,13 +1,12 @@ +import { useEffect } from "react"; import { Editor, useEditorState } from "@tiptap/react"; import Button from "@codegouvfr/react-dsfr/Button"; -import { createControl, useEditor } from "react-dsfr-tiptap"; -// import { Select } from "@codegouvfr/react-dsfr/Select"; -// import { useForm } from "react-hook-form"; -// import { yupResolver } from "@hookform/resolvers/yup"; -// import * as yup from "yup"; - -// import { useDialog } from "./components/RichTextEditor/dialogs/Dialog"; -// import { useEffect } from "react"; +import { Select } from "@codegouvfr/react-dsfr/Select"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { createControl, createDialogControl, useEditor } from "react-dsfr-tiptap"; +import { useDialog } from "react-dsfr-tiptap/dialog"; // 1. Create the component yourself: use the hook `useRichTextEditor` to access the editor object export function CustomControl1() { @@ -38,75 +37,75 @@ export const CustomControl2 = createControl({ }); // 3. Use the `createDialogControl` helper to create a control that uses a dialog: use the hook `useDialog` to access the dialog data -// function CustomDialog() { -// const editor = useRichTextEditor(); -// const { isOpened, modal, onClose } = useDialog(); +function CustomDialog() { + const editor = useEditor(); + const { isOpened, modal, onClose } = useDialog(); -// const schema = yup.object({ -// text: yup.string().required(), -// }); + const schema = yup.object({ + text: yup.string().required(), + }); -// const { -// register, -// handleSubmit, -// getValues, -// formState: { errors }, -// reset, -// } = useForm<{ text: string }>({ -// mode: "onSubmit", -// resolver: yupResolver(schema), -// }); + const { + register, + handleSubmit, + getValues, + formState: { errors }, + reset, + } = useForm<{ text: string }>({ + mode: "onSubmit", + resolver: yupResolver(schema), + }); -// useEffect(() => { -// if (isOpened) { -// reset(); -// } -// }, [isOpened, reset]); + useEffect(() => { + if (isOpened) { + reset(); + } + }, [isOpened, reset]); -// const onSubmit = handleSubmit(() => { -// const { text } = getValues(); -// editor?.chain().focus().insertContent(text).run(); -// onClose(); -// }); + const onSubmit = handleSubmit(() => { + const { text } = getValues(); + editor?.chain().focus().insertContent(text).run(); + onClose(); + }); -// return ( -// -//
-// -// -//
-//
-// ); -// } + return ( + +
+ + +
+
+ ); +} -// export const CustomControl3 = createDialogControl({ -// buttonProps: { children: "Insérer du contenu" }, -// DialogContent: CustomDialog, -// onClick: (editor, ref) => ref.current?.open(), -// }); +export const CustomControl3 = createDialogControl({ + buttonProps: { children: "Insérer du contenu" }, + DialogContent: CustomDialog, + onClick: (editor, ref) => ref.current?.open(), +}); diff --git a/packages/react-dsfr-tiptap/src/components/MarkdownEditor.tsx b/packages/react-dsfr-tiptap/src/components/MarkdownEditor.tsx index cc36a77..581b803 100644 --- a/packages/react-dsfr-tiptap/src/components/MarkdownEditor.tsx +++ b/packages/react-dsfr-tiptap/src/components/MarkdownEditor.tsx @@ -1,5 +1,5 @@ import { LazyExoticComponent, ReactNode } from "react"; -import { EditorEvents } from "@tiptap/react"; +import { AnyExtension, EditorEvents } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { Markdown } from "tiptap-markdown"; @@ -13,7 +13,7 @@ import RichTextEditorContent from "./Content"; import RichTextEditorGroup from "./Group"; export interface IMarkdownEditorProps extends Omit { - controls?: MarkdownControl[][]; + controls?: (MarkdownControl | (() => ReactNode) | LazyExoticComponent<() => ReactNode>)[][]; onContentUpdate?: (content: string) => void; } @@ -26,6 +26,17 @@ const defaultControls: MarkdownControl[][] = [ ["Image"], ]; +const defaultExtensions: AnyExtension[] = [ + StarterKit, + Markdown.configure({ + html: false, + linkify: true, + breaks: true, + transformPastedText: true, + transformCopiedText: true, + }), +]; + type MarkdownControls = { [key in MarkdownControl]: (() => ReactNode) | LazyExoticComponent<() => ReactNode>; }; @@ -45,23 +56,7 @@ const MarkdownEditor = ((props: IMarkdownEditorProps) => { onContentUpdate?.(props.editor.storage.markdown.getMarkdown()); } - return ( - - ); + return ; }) as IMarkdownEditor; Object.entries(markdownControls).forEach(([key, component]) => { diff --git a/packages/react-dsfr-tiptap/src/components/RichTextEditor.tsx b/packages/react-dsfr-tiptap/src/components/RichTextEditor.tsx index c3ed90b..5183df5 100644 --- a/packages/react-dsfr-tiptap/src/components/RichTextEditor.tsx +++ b/packages/react-dsfr-tiptap/src/components/RichTextEditor.tsx @@ -1,5 +1,5 @@ import { LazyExoticComponent, ReactNode } from "react"; -import { EditorEvents } from "@tiptap/react"; +import { AnyExtension, EditorEvents } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { Control } from "../types/controls"; @@ -26,6 +26,8 @@ const defaultControls: Control[][] = [ ["Image", "Youtube"], ]; +const defaultExtensions: AnyExtension[] = [StarterKit]; + type RichTextEditorControls = { [key in Control]: (() => ReactNode) | LazyExoticComponent<() => ReactNode>; }; @@ -45,7 +47,7 @@ const RichTextEditor = ((props: IRichTextEditorProps) => { onContentUpdate?.(props.editor.getHTML()); } - return ; + return ; }) as IRichTextEditor; Object.entries(richTextEditorControls).forEach(([key, component]) => { diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index 1a10f43..0000000 --- a/tests/README.md +++ /dev/null @@ -1 +0,0 @@ -# PENSEZ A TESTER ! \ No newline at end of file