From 0408b796eb627f671a302867e65786dcb9b0e1aa Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 6 Sep 2024 10:27:55 +0800 Subject: [PATCH] test: Lexical editor --- package.json | 1 + public/images/emoji/1F600.png | Bin 0 -> 1768 bytes public/images/emoji/1F641.png | Bin 0 -> 1417 bytes public/images/emoji/1F642.png | Bin 0 -> 1380 bytes public/images/emoji/2764.png | Bin 0 -> 1386 bytes public/images/emoji/LICENSE.md | 5 + public/images/icons/LICENSE.md | 5 + public/images/icons/arrow-clockwise.svg | 4 + .../images/icons/arrow-counterclockwise.svg | 4 + public/images/icons/chat-square-quote.svg | 4 + public/images/icons/chevron-down.svg | 3 + public/images/icons/code.svg | 3 + public/images/icons/journal-code.svg | 5 + public/images/icons/journal-text.svg | 5 + public/images/icons/justify.svg | 3 + public/images/icons/link.svg | 4 + public/images/icons/list-ol.svg | 4 + public/images/icons/list-ul.svg | 3 + public/images/icons/pencil-fill.svg | 3 + public/images/icons/text-center.svg | 3 + public/images/icons/text-left.svg | 3 + public/images/icons/text-paragraph.svg | 3 + public/images/icons/text-right.svg | 3 + public/images/icons/type-bold.svg | 3 + public/images/icons/type-h1.svg | 3 + public/images/icons/type-h2.svg | 3 + public/images/icons/type-h3.svg | 3 + public/images/icons/type-italic.svg | 3 + public/images/icons/type-strikethrough.svg | 3 + public/images/icons/type-underline.svg | 3 + src/components/LexicalEditor/Index.jsx | 79 ++ .../LexicalEditor/plugins/AutoLinkPlugin.jsx | 34 + .../plugins/CodeHighlightPlugin.jsx | 11 + .../FloatingTextFormatToolbarPlugin.tsx | 400 ++++++++++ .../plugins/ListMaxIndentLevelPlugin.jsx | 68 ++ .../LexicalEditor/plugins/ToolbarPlugin.jsx | 714 +++++++++++++++++ .../LexicalEditor/plugins/TreeViewPlugin.jsx | 16 + src/components/LexicalEditor/styles.css | 751 ++++++++++++++++++ .../LexicalEditor/themes/ExampleTheme.js | 69 ++ .../Online/Input/EmailComposer.jsx | 60 +- .../Online/Input/InputComposer.jsx | 10 +- vite.config.js | 3 +- 42 files changed, 2294 insertions(+), 10 deletions(-) create mode 100644 public/images/emoji/1F600.png create mode 100644 public/images/emoji/1F641.png create mode 100644 public/images/emoji/1F642.png create mode 100644 public/images/emoji/2764.png create mode 100644 public/images/emoji/LICENSE.md create mode 100644 public/images/icons/LICENSE.md create mode 100644 public/images/icons/arrow-clockwise.svg create mode 100644 public/images/icons/arrow-counterclockwise.svg create mode 100644 public/images/icons/chat-square-quote.svg create mode 100644 public/images/icons/chevron-down.svg create mode 100644 public/images/icons/code.svg create mode 100644 public/images/icons/journal-code.svg create mode 100644 public/images/icons/journal-text.svg create mode 100644 public/images/icons/justify.svg create mode 100644 public/images/icons/link.svg create mode 100644 public/images/icons/list-ol.svg create mode 100644 public/images/icons/list-ul.svg create mode 100644 public/images/icons/pencil-fill.svg create mode 100644 public/images/icons/text-center.svg create mode 100644 public/images/icons/text-left.svg create mode 100644 public/images/icons/text-paragraph.svg create mode 100644 public/images/icons/text-right.svg create mode 100644 public/images/icons/type-bold.svg create mode 100644 public/images/icons/type-h1.svg create mode 100644 public/images/icons/type-h2.svg create mode 100644 public/images/icons/type-h3.svg create mode 100644 public/images/icons/type-italic.svg create mode 100644 public/images/icons/type-strikethrough.svg create mode 100644 public/images/icons/type-underline.svg create mode 100644 src/components/LexicalEditor/Index.jsx create mode 100644 src/components/LexicalEditor/plugins/AutoLinkPlugin.jsx create mode 100644 src/components/LexicalEditor/plugins/CodeHighlightPlugin.jsx create mode 100644 src/components/LexicalEditor/plugins/FloatingTextFormatToolbarPlugin.tsx create mode 100644 src/components/LexicalEditor/plugins/ListMaxIndentLevelPlugin.jsx create mode 100644 src/components/LexicalEditor/plugins/ToolbarPlugin.jsx create mode 100644 src/components/LexicalEditor/plugins/TreeViewPlugin.jsx create mode 100644 src/components/LexicalEditor/styles.css create mode 100644 src/components/LexicalEditor/themes/ExampleTheme.js diff --git a/package.json b/package.json index cbfd1d6..a853e4b 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "tailwindcss": "^3.4.1", "vite": "^4.5.1", "vite-plugin-css-modules": "^0.0.1", + "vite-plugin-svgr": "^4.2.0", "vite-plugin-windicss": "^1.9.3", "windicss": "^3.5.6" } diff --git a/public/images/emoji/1F600.png b/public/images/emoji/1F600.png new file mode 100644 index 0000000000000000000000000000000000000000..36014c94df203cc0380c6402f285a3ae3b90309f GIT binary patch literal 1768 zcmdT_`#aMM9Nw5=ZKiy8bD7x%8*=F+t=u=Z%vwX29i5DFD3@?n%QcH0w~j@*M;Wo? zP$8!dM;+y!OC08K>WPqR#6iMw{*3c^p3nO}&*%N+ectDNo=-B5>xzJB!$2Sqg3V&^ zcbN6xswwZF?>=V{2&B}-<9IS%FLHJ+|I_(4ZL;HhTHw09y$uF|E(HVsjMKESkk9X^ zR_5x7(HNF9+|>!LuBPVVa9$79C(jT);7kzpt{11`CO zK7anasi|rAQ9E%c@Uq6>&lU<+T@|gax(`620Z2R7_Fk3_M-S85+IohphsWau0s))N zhC-oCCez&9oJb^&j*hOZtUM~gR~6$0o=C0T%C}Q-7JD?@ToA?-)xKj66-9clo)WXu zwBL0bD%vS~X=>?-*wv4;3>(w;J%(9yv&F?lN84TNleFjv6k8X~Wg+@sk)cXTKb+8v zyM%_SfE5Y_pa!jB9yn`=@8#JS6&1ba94;>}r)eUpocF_3AT%1y00C>LB8bJ}*x1-% zPp8DhM47#%mzNhy7v0<2E3q<4PfxFASP!;RJPF!CRD+cLG;5URen68>r|)FFxw&a; zYl}o8^>tNnI2@Hq{q*TmJB6_SiO=-_seHSzuJ!Zs))$Ple)f249j<%v6bk4H zzT86+&9BWVM*dnEx{jzDW|d-j-KM- zxxtT@ffHEh=jZudN4#|;HGm1g50LX>iq1WzDn_N(HAxh*qQtToHA10?!;C{jCq{O1 zo(uIcd@fBm=RY6*Ue|7RVQfSA=GTRo`KSd8bMvant7j789KQI|Pd}LeZ-uTuGFpC_ zf4ZmXkX>VEL{&|oddbPLCz(%7vmTBwj0ID#I_kF{mL*#|RNpt^+a_;TD#3fTC*rv` z;tPo3`)n|3GtKoe0ZzlL$cF}j77gt-4U6<}dkXGnc)&Y9Z@R;j8|?b7fM%QM5>zaa z=?5`una)cf6?&#gKE>SsaP9S4yeER0BY+j({Krxr?s->!q!58+>n;iTZ^Sy|n%ReI z{9)1G!K#FF!;n)EOdaU^rH+-x8 zQaRL0$vR^h5-I8!qA#>2G9HZOSjPNab0ZAtbV@=Pa6Ysh!GWRmP6GxOMU{-q}I>vQ-9QlIlume7}LTy!i-uXfchC6DNy(iYh{ ze8F1ZK5o75Rj5f-XQSZMmB<&0%!satAAR#~B?yy*?cWJo3B@AHP*~Hf|D@@|+w@!4 zj1es)-&;T?TN54a7vi(}3mJ%YXT%6Ui0edu!u(3^vmE( zcKA}7v+H2#lR!7z)xM&Zf&R4o)3CdyvLwbI!r9i%<&F+w@0xvFu5z}1@T&^eI2IPH z`dS`gLtAQ-e@*Z1d;6s<-KgxFP%X5jz5%T=j*{l@mc^GFb}Hv#wIF{rK^_){MJ>-J1ey-@w&^!4ADwN hypq+YZ#!lVUex#7)~I)jl$QPXtk_I0;}<6%$-mc$tm^;( literal 0 HcmV?d00001 diff --git a/public/images/emoji/1F641.png b/public/images/emoji/1F641.png new file mode 100644 index 0000000000000000000000000000000000000000..c618faf7ae7bab0080d7eb7de610c5c99af055dd GIT binary patch literal 1417 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 literal 0 HcmV?d00001 diff --git a/public/images/emoji/2764.png b/public/images/emoji/2764.png new file mode 100644 index 0000000000000000000000000000000000000000..8b9f48089353dd07043fe7750c6cf0dab9fc0dda GIT binary patch literal 1386 zcmV-w1(o`VP)K4)eW|G19k!347)i7LBBBNqKO2j`((UC6EPu1 zW4y$Om>6T!Xo4h$7)^#oLnH_>-m+j&6j9>`k?4x-@z0y{*s5#TSkLJ>?fWFZFWdT@ zr@!;=J?Fgda~5>p)w4j77%4#tl7gflDM$*Ef}|iRNFoIpMi3$gM8H2z1&2T%P-c-+0sZP{qJR=`K|OYl;RGR?zz1Bc z0_uwjXwbWub_6_hXN`|WmR3*@KeH-I{FgcouD?p&hzR|UwO>)p>hN(CVv;}NsF+DG5q+`$E3mG6c-#b+$aKWBY^T;9il z_#H-y_}f|L6*cS(c;iRZzElX}kwf+@3q9LX&jrqz7sElywpjSf>;+?-_h6*qn)5!r zrk;?4^X|}75YG)!oF$esFAju*e8&F*moai!&b)XAwW)cdAf%_Dqd@$Nppc}1Tzt0i zZFtAmBD0!;;&|VwG=d_Uf!jk1cGh_G`!SpIzdA|Hw)UPrJy!I zbuRNw(zUL978KD^kR4pKC<=Wsu+}VcM6IZquG9|fTHh$X4e0&`zi9;B7hKMx=a3$E z(%rm9!;b_;g&$$cbL=drQx8E3ukzjI_ZVK#sNchKj_LU{9|TiamY`SKR~c3gYU2gD zDX>EC1+#HLh1oXxxHoJxIie=i#-nFE6)TDgcok4pfs-bO)*C_$FT45+^H|QCKuOh_ zbRIYbXv30H`jM9^QpkDGTUo|(J_iM<-qrAep8<{4Elar|)WC9n1yx4b7wZ6*6cqM+ z>xz`gc`4Gslk#P-(NH_9{G{)+X3@kAExOD3u&0&sZPwTWsX@1%4*;sn%i&EvUHE!% z9r^NeSYdId(cZhrRvJ`rB!7tYz;B&X(Lpke!x- z7(_8RJ=+b)ZBFE66tvOV literal 0 HcmV?d00001 diff --git a/public/images/emoji/LICENSE.md b/public/images/emoji/LICENSE.md new file mode 100644 index 0000000..87b04e9 --- /dev/null +++ b/public/images/emoji/LICENSE.md @@ -0,0 +1,5 @@ +OpenMoji +https://openmoji.org + +Licensed under Attribution-ShareAlike 4.0 International +https://creativecommons.org/licenses/by-sa/4.0/ diff --git a/public/images/icons/LICENSE.md b/public/images/icons/LICENSE.md new file mode 100644 index 0000000..ce74f6a --- /dev/null +++ b/public/images/icons/LICENSE.md @@ -0,0 +1,5 @@ +Bootstrap Icons +https://icons.getbootstrap.com + +Licensed under MIT license +https://github.com/twbs/icons/blob/main/LICENSE.md diff --git a/public/images/icons/arrow-clockwise.svg b/public/images/icons/arrow-clockwise.svg new file mode 100644 index 0000000..b072eb0 --- /dev/null +++ b/public/images/icons/arrow-clockwise.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/images/icons/arrow-counterclockwise.svg b/public/images/icons/arrow-counterclockwise.svg new file mode 100644 index 0000000..b0b23b9 --- /dev/null +++ b/public/images/icons/arrow-counterclockwise.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/images/icons/chat-square-quote.svg b/public/images/icons/chat-square-quote.svg new file mode 100644 index 0000000..40893f4 --- /dev/null +++ b/public/images/icons/chat-square-quote.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/images/icons/chevron-down.svg b/public/images/icons/chevron-down.svg new file mode 100644 index 0000000..1f0b8bc --- /dev/null +++ b/public/images/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/code.svg b/public/images/icons/code.svg new file mode 100644 index 0000000..079f5c6 --- /dev/null +++ b/public/images/icons/code.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/journal-code.svg b/public/images/icons/journal-code.svg new file mode 100644 index 0000000..82098b9 --- /dev/null +++ b/public/images/icons/journal-code.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/images/icons/journal-text.svg b/public/images/icons/journal-text.svg new file mode 100644 index 0000000..9b66f43 --- /dev/null +++ b/public/images/icons/journal-text.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/images/icons/justify.svg b/public/images/icons/justify.svg new file mode 100644 index 0000000..009bd72 --- /dev/null +++ b/public/images/icons/justify.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/link.svg b/public/images/icons/link.svg new file mode 100644 index 0000000..df35bc8 --- /dev/null +++ b/public/images/icons/link.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/images/icons/list-ol.svg b/public/images/icons/list-ol.svg new file mode 100644 index 0000000..5782568 --- /dev/null +++ b/public/images/icons/list-ol.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/images/icons/list-ul.svg b/public/images/icons/list-ul.svg new file mode 100644 index 0000000..217d153 --- /dev/null +++ b/public/images/icons/list-ul.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/pencil-fill.svg b/public/images/icons/pencil-fill.svg new file mode 100644 index 0000000..59d2830 --- /dev/null +++ b/public/images/icons/pencil-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/text-center.svg b/public/images/icons/text-center.svg new file mode 100644 index 0000000..2887a99 --- /dev/null +++ b/public/images/icons/text-center.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/text-left.svg b/public/images/icons/text-left.svg new file mode 100644 index 0000000..0452611 --- /dev/null +++ b/public/images/icons/text-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/text-paragraph.svg b/public/images/icons/text-paragraph.svg new file mode 100644 index 0000000..9779bea --- /dev/null +++ b/public/images/icons/text-paragraph.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/text-right.svg b/public/images/icons/text-right.svg new file mode 100644 index 0000000..34686b0 --- /dev/null +++ b/public/images/icons/text-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/type-bold.svg b/public/images/icons/type-bold.svg new file mode 100644 index 0000000..276d133 --- /dev/null +++ b/public/images/icons/type-bold.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/type-h1.svg b/public/images/icons/type-h1.svg new file mode 100644 index 0000000..4c89181 --- /dev/null +++ b/public/images/icons/type-h1.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/type-h2.svg b/public/images/icons/type-h2.svg new file mode 100644 index 0000000..b6ab765 --- /dev/null +++ b/public/images/icons/type-h2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/type-h3.svg b/public/images/icons/type-h3.svg new file mode 100644 index 0000000..154c293 --- /dev/null +++ b/public/images/icons/type-h3.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/type-italic.svg b/public/images/icons/type-italic.svg new file mode 100644 index 0000000..3ac6b09 --- /dev/null +++ b/public/images/icons/type-italic.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/type-strikethrough.svg b/public/images/icons/type-strikethrough.svg new file mode 100644 index 0000000..1c940e4 --- /dev/null +++ b/public/images/icons/type-strikethrough.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/icons/type-underline.svg b/public/images/icons/type-underline.svg new file mode 100644 index 0000000..c299b8b --- /dev/null +++ b/public/images/icons/type-underline.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/LexicalEditor/Index.jsx b/src/components/LexicalEditor/Index.jsx new file mode 100644 index 0000000..d853665 --- /dev/null +++ b/src/components/LexicalEditor/Index.jsx @@ -0,0 +1,79 @@ +import ExampleTheme from "./themes/ExampleTheme"; +import { LexicalComposer } from "@lexical/react/LexicalComposer"; +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; +import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; +import {LexicalErrorBoundary} from "@lexical/react/LexicalErrorBoundary"; +import TreeViewPlugin from "./plugins/TreeViewPlugin"; +import ToolbarPlugin from "./plugins/ToolbarPlugin"; +import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { TableCellNode, TableNode, TableRowNode } from "@lexical/table"; +import { ListItemNode, ListNode } from "@lexical/list"; +import { CodeHighlightNode, CodeNode } from "@lexical/code"; +import { AutoLinkNode, LinkNode } from "@lexical/link"; +import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; +import { ListPlugin } from "@lexical/react/LexicalListPlugin"; +import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; +import { TRANSFORMERS } from "@lexical/markdown"; + +import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin"; +import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin"; +import AutoLinkPlugin from "./plugins/AutoLinkPlugin"; + +import './styles.css'; + +function Placeholder() { + return
Enter some rich text...
; +} + +const editorConfig = { + // The editor theme + // theme: {}, + theme: ExampleTheme, + // Handling of errors during update + onError(error) { + throw error; + }, + // Any custom nodes go here + nodes: [ + HeadingNode, + ListNode, + ListItemNode, + QuoteNode, + CodeNode, + CodeHighlightNode, + TableNode, + TableCellNode, + TableRowNode, + AutoLinkNode, + LinkNode + ] +}; + +export default function Editor() { + return ( + +
+ +
+ {/* */} + } + placeholder={} + ErrorBoundary={LexicalErrorBoundary} + /> + + {import.meta.env.DEV && } + + + + + + + +
+
+
+ ); +} diff --git a/src/components/LexicalEditor/plugins/AutoLinkPlugin.jsx b/src/components/LexicalEditor/plugins/AutoLinkPlugin.jsx new file mode 100644 index 0000000..3475c91 --- /dev/null +++ b/src/components/LexicalEditor/plugins/AutoLinkPlugin.jsx @@ -0,0 +1,34 @@ +import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin"; + +const URL_MATCHER = /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; + +const EMAIL_MATCHER = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/; + +const MATCHERS = [ + (text) => { + const match = URL_MATCHER.exec(text); + return ( + match && { + index: match.index, + length: match[0].length, + text: match[0], + url: match[0] + } + ); + }, + (text) => { + const match = EMAIL_MATCHER.exec(text); + return ( + match && { + index: match.index, + length: match[0].length, + text: match[0], + url: `mailto:${match[0]}` + } + ); + } +]; + +export default function PlaygroundAutoLinkPlugin() { + return ; +} diff --git a/src/components/LexicalEditor/plugins/CodeHighlightPlugin.jsx b/src/components/LexicalEditor/plugins/CodeHighlightPlugin.jsx new file mode 100644 index 0000000..f931805 --- /dev/null +++ b/src/components/LexicalEditor/plugins/CodeHighlightPlugin.jsx @@ -0,0 +1,11 @@ +import { registerCodeHighlighting } from "@lexical/code"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { useEffect } from "react"; + +export default function CodeHighlightPlugin() { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + return registerCodeHighlighting(editor); + }, [editor]); + return null; +} diff --git a/src/components/LexicalEditor/plugins/FloatingTextFormatToolbarPlugin.tsx b/src/components/LexicalEditor/plugins/FloatingTextFormatToolbarPlugin.tsx new file mode 100644 index 0000000..6283fa7 --- /dev/null +++ b/src/components/LexicalEditor/plugins/FloatingTextFormatToolbarPlugin.tsx @@ -0,0 +1,400 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import './index.css'; + +import {$isCodeHighlightNode} from '@lexical/code'; +import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link'; +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {mergeRegister} from '@lexical/utils'; +import { + $getSelection, + $isParagraphNode, + $isRangeSelection, + $isTextNode, + COMMAND_PRIORITY_LOW, + FORMAT_TEXT_COMMAND, + LexicalEditor, + SELECTION_CHANGE_COMMAND, +} from 'lexical'; +import {Dispatch, useCallback, useEffect, useRef, useState} from 'react'; +import * as React from 'react'; +import {createPortal} from 'react-dom'; + +import {getDOMRangeRect} from '../../utils/getDOMRangeRect'; +import {getSelectedNode} from '../../utils/getSelectedNode'; +import {setFloatingElemPosition} from '../../utils/setFloatingElemPosition'; +import {INSERT_INLINE_COMMAND} from '../CommentPlugin'; + +function TextFormatFloatingToolbar({ + editor, + anchorElem, + isLink, + isBold, + isItalic, + isUnderline, + isCode, + isStrikethrough, + isSubscript, + isSuperscript, + setIsLinkEditMode, +}: { + editor: LexicalEditor; + anchorElem: HTMLElement; + isBold: boolean; + isCode: boolean; + isItalic: boolean; + isLink: boolean; + isStrikethrough: boolean; + isSubscript: boolean; + isSuperscript: boolean; + isUnderline: boolean; + setIsLinkEditMode: Dispatch; +}): JSX.Element { + const popupCharStylesEditorRef = useRef(null); + + const insertLink = useCallback(() => { + if (!isLink) { + setIsLinkEditMode(true); + editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://'); + } else { + setIsLinkEditMode(false); + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [editor, isLink, setIsLinkEditMode]); + + const insertComment = () => { + editor.dispatchCommand(INSERT_INLINE_COMMAND, undefined); + }; + + function mouseMoveListener(e: MouseEvent) { + if ( + popupCharStylesEditorRef?.current && + (e.buttons === 1 || e.buttons === 3) + ) { + if (popupCharStylesEditorRef.current.style.pointerEvents !== 'none') { + const x = e.clientX; + const y = e.clientY; + const elementUnderMouse = document.elementFromPoint(x, y); + + if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) { + // Mouse is not over the target element => not a normal click, but probably a drag + popupCharStylesEditorRef.current.style.pointerEvents = 'none'; + } + } + } + } + function mouseUpListener(e: MouseEvent) { + if (popupCharStylesEditorRef?.current) { + if (popupCharStylesEditorRef.current.style.pointerEvents !== 'auto') { + popupCharStylesEditorRef.current.style.pointerEvents = 'auto'; + } + } + } + + useEffect(() => { + if (popupCharStylesEditorRef?.current) { + document.addEventListener('mousemove', mouseMoveListener); + document.addEventListener('mouseup', mouseUpListener); + + return () => { + document.removeEventListener('mousemove', mouseMoveListener); + document.removeEventListener('mouseup', mouseUpListener); + }; + } + }, [popupCharStylesEditorRef]); + + const $updateTextFormatFloatingToolbar = useCallback(() => { + const selection = $getSelection(); + + const popupCharStylesEditorElem = popupCharStylesEditorRef.current; + const nativeSelection = window.getSelection(); + + if (popupCharStylesEditorElem === null) { + return; + } + + const rootElement = editor.getRootElement(); + if ( + selection !== null && + nativeSelection !== null && + !nativeSelection.isCollapsed && + rootElement !== null && + rootElement.contains(nativeSelection.anchorNode) + ) { + const rangeRect = getDOMRangeRect(nativeSelection, rootElement); + + setFloatingElemPosition( + rangeRect, + popupCharStylesEditorElem, + anchorElem, + isLink, + ); + } + }, [editor, anchorElem, isLink]); + + useEffect(() => { + const scrollerElem = anchorElem.parentElement; + + const update = () => { + editor.getEditorState().read(() => { + $updateTextFormatFloatingToolbar(); + }); + }; + + window.addEventListener('resize', update); + if (scrollerElem) { + scrollerElem.addEventListener('scroll', update); + } + + return () => { + window.removeEventListener('resize', update); + if (scrollerElem) { + scrollerElem.removeEventListener('scroll', update); + } + }; + }, [editor, $updateTextFormatFloatingToolbar, anchorElem]); + + useEffect(() => { + editor.getEditorState().read(() => { + $updateTextFormatFloatingToolbar(); + }); + return mergeRegister( + editor.registerUpdateListener(({editorState}) => { + editorState.read(() => { + $updateTextFormatFloatingToolbar(); + }); + }), + + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + () => { + $updateTextFormatFloatingToolbar(); + return false; + }, + COMMAND_PRIORITY_LOW, + ), + ); + }, [editor, $updateTextFormatFloatingToolbar]); + + return ( +
+ {editor.isEditable() && ( + <> + + + + + + + + + + )} + +
+ ); +} + +function useFloatingTextFormatToolbar( + editor: LexicalEditor, + anchorElem: HTMLElement, + setIsLinkEditMode: Dispatch, +): JSX.Element | null { + const [isText, setIsText] = useState(false); + const [isLink, setIsLink] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + const [isUnderline, setIsUnderline] = useState(false); + const [isStrikethrough, setIsStrikethrough] = useState(false); + const [isSubscript, setIsSubscript] = useState(false); + const [isSuperscript, setIsSuperscript] = useState(false); + const [isCode, setIsCode] = useState(false); + + const updatePopup = useCallback(() => { + editor.getEditorState().read(() => { + // Should not to pop up the floating toolbar when using IME input + if (editor.isComposing()) { + return; + } + const selection = $getSelection(); + const nativeSelection = window.getSelection(); + const rootElement = editor.getRootElement(); + + if ( + nativeSelection !== null && + (!$isRangeSelection(selection) || + rootElement === null || + !rootElement.contains(nativeSelection.anchorNode)) + ) { + setIsText(false); + return; + } + + if (!$isRangeSelection(selection)) { + return; + } + + const node = getSelectedNode(selection); + + // Update text format + setIsBold(selection.hasFormat('bold')); + setIsItalic(selection.hasFormat('italic')); + setIsUnderline(selection.hasFormat('underline')); + setIsStrikethrough(selection.hasFormat('strikethrough')); + setIsSubscript(selection.hasFormat('subscript')); + setIsSuperscript(selection.hasFormat('superscript')); + setIsCode(selection.hasFormat('code')); + + // Update links + const parent = node.getParent(); + if ($isLinkNode(parent) || $isLinkNode(node)) { + setIsLink(true); + } else { + setIsLink(false); + } + + if ( + !$isCodeHighlightNode(selection.anchor.getNode()) && + selection.getTextContent() !== '' + ) { + setIsText($isTextNode(node) || $isParagraphNode(node)); + } else { + setIsText(false); + } + + const rawTextContent = selection.getTextContent().replace(/\n/g, ''); + if (!selection.isCollapsed() && rawTextContent === '') { + setIsText(false); + return; + } + }); + }, [editor]); + + useEffect(() => { + document.addEventListener('selectionchange', updatePopup); + return () => { + document.removeEventListener('selectionchange', updatePopup); + }; + }, [updatePopup]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(() => { + updatePopup(); + }), + editor.registerRootListener(() => { + if (editor.getRootElement() === null) { + setIsText(false); + } + }), + ); + }, [editor, updatePopup]); + + if (!isText) { + return null; + } + + return createPortal( + , + anchorElem, + ); +} + +export default function FloatingTextFormatToolbarPlugin({ + anchorElem = document.body, + setIsLinkEditMode, +}: { + anchorElem?: HTMLElement; + setIsLinkEditMode: Dispatch; +}): JSX.Element | null { + const [editor] = useLexicalComposerContext(); + return useFloatingTextFormatToolbar(editor, anchorElem, setIsLinkEditMode); +} diff --git a/src/components/LexicalEditor/plugins/ListMaxIndentLevelPlugin.jsx b/src/components/LexicalEditor/plugins/ListMaxIndentLevelPlugin.jsx new file mode 100644 index 0000000..657535e --- /dev/null +++ b/src/components/LexicalEditor/plugins/ListMaxIndentLevelPlugin.jsx @@ -0,0 +1,68 @@ +import { $getListDepth, $isListItemNode, $isListNode } from "@lexical/list"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { + $getSelection, + $isElementNode, + $isRangeSelection, + INDENT_CONTENT_COMMAND, + COMMAND_PRIORITY_HIGH +} from "lexical"; +import { useEffect } from "react"; + +function getElementNodesInSelection(selection) { + const nodesInSelection = selection.getNodes(); + + if (nodesInSelection.length === 0) { + return new Set([ + selection.anchor.getNode().getParentOrThrow(), + selection.focus.getNode().getParentOrThrow() + ]); + } + + return new Set( + nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow())) + ); +} + +function isIndentPermitted(maxDepth) { + const selection = $getSelection(); + + if (!$isRangeSelection(selection)) { + return false; + } + + const elementNodesInSelection = getElementNodesInSelection(selection); + + let totalDepth = 0; + + for (const elementNode of elementNodesInSelection) { + if ($isListNode(elementNode)) { + totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth); + } else if ($isListItemNode(elementNode)) { + const parent = elementNode.getParent(); + if (!$isListNode(parent)) { + throw new Error( + "ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent." + ); + } + + totalDepth = Math.max($getListDepth(parent) + 1, totalDepth); + } + } + + return totalDepth <= maxDepth; +} + +export default function ListMaxIndentLevelPlugin({ maxDepth }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerCommand( + INDENT_CONTENT_COMMAND, + () => !isIndentPermitted(maxDepth ?? 7), + COMMAND_PRIORITY_HIGH + ); + }, [editor, maxDepth]); + + return null; +} diff --git a/src/components/LexicalEditor/plugins/ToolbarPlugin.jsx b/src/components/LexicalEditor/plugins/ToolbarPlugin.jsx new file mode 100644 index 0000000..52aa8a0 --- /dev/null +++ b/src/components/LexicalEditor/plugins/ToolbarPlugin.jsx @@ -0,0 +1,714 @@ +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, + REDO_COMMAND, + UNDO_COMMAND, + SELECTION_CHANGE_COMMAND, + FORMAT_TEXT_COMMAND, + FORMAT_ELEMENT_COMMAND, + $getSelection, + $isRangeSelection, + $createParagraphNode, + $getNodeByKey +} from "lexical"; +import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; +import { + $isParentElementRTL, + $wrapNodes, + $isAtNodeEnd +} from "@lexical/selection"; +import { $getNearestNodeOfType, mergeRegister, } from "@lexical/utils"; +import { + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, + REMOVE_LIST_COMMAND, + $isListNode, + ListNode +} from "@lexical/list"; +import { createPortal } from "react-dom"; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode +} from "@lexical/rich-text"; +import { + $createCodeNode, + $isCodeNode, + getDefaultCodeLanguage, + getCodeLanguages +} from "@lexical/code"; + +const LowPriority = 1; + +const supportedBlockTypes = new Set([ + "paragraph", + "quote", + "code", + "h1", + "h2", + "ul", + "ol" +]); + +const blockTypeToBlockName = { + code: "Code Block", + h1: "Large Heading", + h2: "Small Heading", + h3: "Heading", + h4: "Heading", + h5: "Heading", + ol: "Numbered List", + paragraph: "Normal", + quote: "Quote", + ul: "Bulleted List" +}; + +function Divider() { + return
; +} + +function positionEditorElement(editor, rect) { + if (rect === null) { + editor.style.opacity = "0"; + editor.style.top = "-1000px"; + editor.style.left = "-1000px"; + } else { + editor.style.opacity = "1"; + editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`; + editor.style.left = `${ + rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2 + }px`; + } +} + +function FloatingLinkEditor({ editor }) { + const editorRef = useRef(null); + const inputRef = useRef(null); + const mouseDownRef = useRef(false); + const [linkUrl, setLinkUrl] = useState(""); + const [isEditMode, setEditMode] = useState(false); + const [lastSelection, setLastSelection] = useState(null); + + const updateLinkEditor = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent)) { + setLinkUrl(parent.getURL()); + } else if ($isLinkNode(node)) { + setLinkUrl(node.getURL()); + } else { + setLinkUrl(""); + } + } + const editorElem = editorRef.current; + const nativeSelection = window.getSelection(); + const activeElement = document.activeElement; + + if (editorElem === null) { + return; + } + + const rootElement = editor.getRootElement(); + if ( + selection !== null && + !nativeSelection.isCollapsed && + rootElement !== null && + rootElement.contains(nativeSelection.anchorNode) + ) { + const domRange = nativeSelection.getRangeAt(0); + let rect; + if (nativeSelection.anchorNode === rootElement) { + let inner = rootElement; + while (inner.firstElementChild != null) { + inner = inner.firstElementChild; + } + rect = inner.getBoundingClientRect(); + } else { + rect = domRange.getBoundingClientRect(); + } + + if (!mouseDownRef.current) { + positionEditorElement(editorElem, rect); + } + setLastSelection(selection); + } else if (!activeElement || activeElement.className !== "link-input") { + positionEditorElement(editorElem, null); + setLastSelection(null); + setEditMode(false); + setLinkUrl(""); + } + + return true; + }, [editor]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + updateLinkEditor(); + }); + }), + + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + () => { + updateLinkEditor(); + return true; + }, + LowPriority + ) + ); + }, [editor, updateLinkEditor]); + + useEffect(() => { + editor.getEditorState().read(() => { + updateLinkEditor(); + }); + }, [editor, updateLinkEditor]); + + useEffect(() => { + if (isEditMode && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditMode]); + + return ( +
+ {isEditMode ? ( + { + setLinkUrl(event.target.value); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (lastSelection !== null) { + if (linkUrl !== "") { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl); + } + setEditMode(false); + } + } else if (event.key === "Escape") { + event.preventDefault(); + setEditMode(false); + } + }} + /> + ) : ( + <> +
+ + {linkUrl} + +
event.preventDefault()} + onClick={() => { + setEditMode(true); + }} + /> +
+ + )} +
+ ); +} + +function Select({ onChange, className, options, value }) { + return ( + + ); +} + +function getSelectedNode(selection) { + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = selection.anchor.getNode(); + const focusNode = selection.focus.getNode(); + if (anchorNode === focusNode) { + return anchorNode; + } + const isBackward = selection.isBackward(); + if (isBackward) { + return $isAtNodeEnd(focus) ? anchorNode : focusNode; + } else { + return $isAtNodeEnd(anchor) ? focusNode : anchorNode; + } +} + +function getDomRangeRect(nativeSelection, rootElement) { + const domRange = nativeSelection.getRangeAt(0); + + let rect; + + if (nativeSelection.anchorNode === rootElement) { + let inner = rootElement; + while (inner.firstElementChild != null) { + inner = inner.firstElementChild; + } + rect = inner.getBoundingClientRect(); + } else { + rect = domRange.getBoundingClientRect(); + } + return rect; +} + +function BlockOptionsDropdownList({ + editor, + blockType, + toolbarRef, + setShowBlockOptionsDropDown +}) { + const dropDownRef = useRef(null); + + useEffect(() => { + const toolbar = toolbarRef.current; + const dropDown = dropDownRef.current; + + if (toolbar !== null && dropDown !== null) { + const { top, left } = toolbar.getBoundingClientRect(); + dropDown.style.top = `${top + 40}px`; + dropDown.style.left = `${left}px`; + } + }, [dropDownRef, toolbarRef]); + + useEffect(() => { + const dropDown = dropDownRef.current; + const toolbar = toolbarRef.current; + + if (dropDown !== null && toolbar !== null) { + const handle = (event) => { + const target = event.target; + + if (!dropDown.contains(target) && !toolbar.contains(target)) { + setShowBlockOptionsDropDown(false); + } + }; + document.addEventListener("click", handle); + + return () => { + document.removeEventListener("click", handle); + }; + } + }, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]); + + const formatParagraph = () => { + if (blockType !== "paragraph") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createParagraphNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatLargeHeading = () => { + if (blockType !== "h1") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h1")); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatSmallHeading = () => { + if (blockType !== "h2") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h2")); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatBulletList = () => { + if (blockType !== "ul") { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND); + } + setShowBlockOptionsDropDown(false); + }; + + const formatNumberedList = () => { + if (blockType !== "ol") { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND); + } + setShowBlockOptionsDropDown(false); + }; + + const formatQuote = () => { + if (blockType !== "quote") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createQuoteNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatCode = () => { + if (blockType !== "code") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createCodeNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + return ( +
+ + + + + + + +
+ ); +} + +export default function ToolbarPlugin() { + const [editor] = useLexicalComposerContext(); + const toolbarRef = useRef(null); + const [canUndo, setCanUndo] = useState(false); + const [canRedo, setCanRedo] = useState(false); + const [blockType, setBlockType] = useState("paragraph"); + const [selectedElementKey, setSelectedElementKey] = useState(null); + const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState( + false + ); + const [codeLanguage, setCodeLanguage] = useState(""); + const [isRTL, setIsRTL] = useState(false); + const [isLink, setIsLink] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + const [isUnderline, setIsUnderline] = useState(false); + const [isStrikethrough, setIsStrikethrough] = useState(false); + const [isCode, setIsCode] = useState(false); + + const updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + const element = + anchorNode.getKey() === "root" + ? anchorNode + : anchorNode.getTopLevelElementOrThrow(); + const elementKey = element.getKey(); + const elementDOM = editor.getElementByKey(elementKey); + if (elementDOM !== null) { + setSelectedElementKey(elementKey); + if ($isListNode(element)) { + const parentList = $getNearestNodeOfType(anchorNode, ListNode); + const type = parentList ? parentList.getTag() : element.getTag(); + setBlockType(type); + } else { + const type = $isHeadingNode(element) + ? element.getTag() + : element.getType(); + setBlockType(type); + if ($isCodeNode(element)) { + setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage()); + } + } + } + // Update text format + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + setIsUnderline(selection.hasFormat("underline")); + setIsStrikethrough(selection.hasFormat("strikethrough")); + setIsCode(selection.hasFormat("code")); + setIsRTL($isParentElementRTL(selection)); + + // Update links + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent) || $isLinkNode(node)) { + setIsLink(true); + } else { + setIsLink(false); + } + } + }, [editor]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, newEditor) => { + updateToolbar(); + return false; + }, + LowPriority + ), + editor.registerCommand( + CAN_UNDO_COMMAND, + (payload) => { + setCanUndo(payload); + return false; + }, + LowPriority + ), + editor.registerCommand( + CAN_REDO_COMMAND, + (payload) => { + setCanRedo(payload); + return false; + }, + LowPriority + ) + ); + }, [editor, updateToolbar]); + + const codeLanguges = useMemo(() => getCodeLanguages(), []); + const onCodeLanguageSelect = useCallback( + (e) => { + editor.update(() => { + if (selectedElementKey !== null) { + const node = $getNodeByKey(selectedElementKey); + if ($isCodeNode(node)) { + node.setLanguage(e.target.value); + } + } + }); + }, + [editor, selectedElementKey] + ); + + const insertLink = useCallback(() => { + if (!isLink) { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); + } else { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [editor, isLink]); + + return ( +
+ + + + {supportedBlockTypes.has(blockType) && ( + <> + + {showBlockOptionsDropDown && + createPortal( + , + document.body + )} + + + )} + {blockType === "code" ? ( + <> +