@thangph2146/lexical-editor 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/editor-x/editor.cjs +33121 -0
  2. package/dist/editor-x/editor.cjs.map +1 -0
  3. package/dist/editor-x/editor.css +2854 -0
  4. package/dist/editor-x/editor.css.map +1 -0
  5. package/dist/editor-x/editor.d.cts +12 -0
  6. package/dist/editor-x/editor.d.ts +12 -0
  7. package/dist/editor-x/editor.js +33095 -0
  8. package/dist/editor-x/editor.js.map +1 -0
  9. package/dist/index.cjs +33210 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +2854 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.cts +15 -0
  14. package/dist/index.d.ts +15 -0
  15. package/dist/index.js +33183 -0
  16. package/dist/index.js.map +1 -0
  17. package/package.json +84 -0
  18. package/src/components/lexical-editor.tsx +123 -0
  19. package/src/context/editor-container-context.tsx +29 -0
  20. package/src/context/priority-image-context.tsx +7 -0
  21. package/src/context/toolbar-context.tsx +60 -0
  22. package/src/context/uploads-context.tsx +53 -0
  23. package/src/editor-hooks/use-debounce.ts +80 -0
  24. package/src/editor-hooks/use-modal.tsx +64 -0
  25. package/src/editor-hooks/use-report.ts +57 -0
  26. package/src/editor-hooks/use-update-toolbar.ts +41 -0
  27. package/src/editor-ui/broken-image.tsx +18 -0
  28. package/src/editor-ui/caption-composer.tsx +45 -0
  29. package/src/editor-ui/code-button.tsx +75 -0
  30. package/src/editor-ui/color-picker.tsx +2010 -0
  31. package/src/editor-ui/content-editable.tsx +37 -0
  32. package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
  33. package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
  34. package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
  35. package/src/editor-ui/image-component.tsx +321 -0
  36. package/src/editor-ui/image-placeholder.tsx +57 -0
  37. package/src/editor-ui/image-resizer.tsx +499 -0
  38. package/src/editor-ui/image-sizing.ts +120 -0
  39. package/src/editor-ui/lazy-image.tsx +136 -0
  40. package/src/editor-x/editor.tsx +117 -0
  41. package/src/editor-x/nodes.ts +79 -0
  42. package/src/editor-x/plugins.tsx +380 -0
  43. package/src/hooks/use-click-outside.ts +27 -0
  44. package/src/hooks/use-element-size.ts +54 -0
  45. package/src/hooks/use-header-height.ts +95 -0
  46. package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
  47. package/src/index.ts +4 -0
  48. package/src/lib/logger.ts +6 -0
  49. package/src/lib/utils.ts +19 -0
  50. package/src/nodes/autocomplete-node.tsx +94 -0
  51. package/src/nodes/embeds/tweet-node.tsx +224 -0
  52. package/src/nodes/embeds/youtube-node.tsx +519 -0
  53. package/src/nodes/emoji-node.tsx +83 -0
  54. package/src/nodes/image-node.tsx +328 -0
  55. package/src/nodes/keyword-node.tsx +58 -0
  56. package/src/nodes/layout-container-node.tsx +128 -0
  57. package/src/nodes/layout-item-node.tsx +118 -0
  58. package/src/nodes/list-with-color-node.tsx +160 -0
  59. package/src/nodes/mention-node.ts +122 -0
  60. package/src/plugins/actions/actions-plugin.tsx +3 -0
  61. package/src/plugins/actions/character-limit-plugin.tsx +27 -0
  62. package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
  63. package/src/plugins/actions/counter-character-plugin.tsx +80 -0
  64. package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
  65. package/src/plugins/actions/import-export-plugin.tsx +61 -0
  66. package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
  67. package/src/plugins/actions/max-length-plugin.tsx +59 -0
  68. package/src/plugins/actions/share-content-plugin.tsx +72 -0
  69. package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
  70. package/src/plugins/actions/tree-view-plugin.tsx +63 -0
  71. package/src/plugins/align-plugin.tsx +86 -0
  72. package/src/plugins/auto-link-plugin.tsx +34 -0
  73. package/src/plugins/autocomplete-plugin.tsx +2574 -0
  74. package/src/plugins/code-action-menu-plugin.tsx +240 -0
  75. package/src/plugins/code-highlight-plugin.tsx +22 -0
  76. package/src/plugins/component-picker-menu-plugin.tsx +427 -0
  77. package/src/plugins/context-menu-plugin.tsx +311 -0
  78. package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
  79. package/src/plugins/draggable-block-plugin.tsx +50 -0
  80. package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
  81. package/src/plugins/embeds/twitter-plugin.tsx +45 -0
  82. package/src/plugins/embeds/youtube-plugin.tsx +84 -0
  83. package/src/plugins/emoji-picker-plugin.tsx +206 -0
  84. package/src/plugins/emojis-plugin.tsx +84 -0
  85. package/src/plugins/floating-link-editor-plugin.tsx +791 -0
  86. package/src/plugins/floating-text-format-plugin.tsx +710 -0
  87. package/src/plugins/images-plugin.tsx +671 -0
  88. package/src/plugins/keywords-plugin.tsx +59 -0
  89. package/src/plugins/layout-plugin.tsx +658 -0
  90. package/src/plugins/link-plugin.tsx +18 -0
  91. package/src/plugins/list-color-plugin.tsx +178 -0
  92. package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
  93. package/src/plugins/mentions-plugin.tsx +714 -0
  94. package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
  95. package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
  96. package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
  97. package/src/plugins/picker/code-picker-plugin.tsx +30 -0
  98. package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
  99. package/src/plugins/picker/component-picker-option.tsx +47 -0
  100. package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
  101. package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
  102. package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
  103. package/src/plugins/picker/image-picker-plugin.tsx +16 -0
  104. package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
  105. package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
  106. package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
  107. package/src/plugins/picker/table-picker-plugin.tsx +56 -0
  108. package/src/plugins/tab-focus-plugin.tsx +66 -0
  109. package/src/plugins/table-column-resizer-plugin.tsx +309 -0
  110. package/src/plugins/table-plugin.tsx +299 -0
  111. package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
  112. package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
  113. package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
  114. package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
  115. package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
  116. package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
  117. package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
  118. package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
  119. package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
  120. package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
  121. package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
  122. package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
  123. package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
  124. package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
  125. package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
  126. package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
  127. package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
  128. package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
  129. package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
  130. package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
  131. package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
  132. package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
  133. package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
  134. package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
  135. package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
  136. package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
  137. package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
  138. package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
  139. package/src/plugins/typing-pref-plugin.tsx +118 -0
  140. package/src/shared/can-use-dom.ts +4 -0
  141. package/src/shared/environment.ts +47 -0
  142. package/src/shared/invariant.ts +16 -0
  143. package/src/shared/use-layout-effect.ts +12 -0
  144. package/src/themes/_mixins.scss +107 -0
  145. package/src/themes/_variables.scss +33 -0
  146. package/src/themes/editor-theme.scss +622 -0
  147. package/src/themes/editor-theme.ts +118 -0
  148. package/src/themes/plugins.scss +1180 -0
  149. package/src/themes/ui-components.scss +936 -0
  150. package/src/transformers/markdown-emoji-transformer.ts +20 -0
  151. package/src/transformers/markdown-hr-transformer.ts +28 -0
  152. package/src/transformers/markdown-image-transformer.ts +31 -0
  153. package/src/transformers/markdown-list-transformer.ts +51 -0
  154. package/src/transformers/markdown-table-transformer.ts +200 -0
  155. package/src/transformers/markdown-tweet-transformer.ts +26 -0
  156. package/src/ui/button-group.tsx +10 -0
  157. package/src/ui/button.tsx +29 -0
  158. package/src/ui/collapsible.tsx +67 -0
  159. package/src/ui/command.tsx +48 -0
  160. package/src/ui/dialog.tsx +146 -0
  161. package/src/ui/flex.tsx +38 -0
  162. package/src/ui/input.tsx +20 -0
  163. package/src/ui/label.tsx +20 -0
  164. package/src/ui/popover.tsx +128 -0
  165. package/src/ui/scroll-area.tsx +17 -0
  166. package/src/ui/select.tsx +171 -0
  167. package/src/ui/separator.tsx +20 -0
  168. package/src/ui/slider.tsx +14 -0
  169. package/src/ui/slot.tsx +3 -0
  170. package/src/ui/tabs.tsx +87 -0
  171. package/src/ui/toggle-group.tsx +109 -0
  172. package/src/ui/toggle.tsx +28 -0
  173. package/src/ui/tooltip.tsx +28 -0
  174. package/src/ui/typography.tsx +44 -0
  175. package/src/utils/doc-serialization.ts +68 -0
  176. package/src/utils/emoji-list.ts +16604 -0
  177. package/src/utils/get-dom-range-rect.ts +20 -0
  178. package/src/utils/get-selected-node.ts +20 -0
  179. package/src/utils/is-mobile-width.ts +0 -0
  180. package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
  181. package/src/utils/set-floating-elem-position.ts +44 -0
  182. package/src/utils/swipe.ts +119 -0
  183. package/src/utils/url.ts +32 -0
@@ -0,0 +1,59 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ */
10
+ import { useEffect } from "react"
11
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
12
+ import { $trimTextContentFromAnchor } from "@lexical/selection"
13
+ import { $restoreEditorState } from "@lexical/utils"
14
+ import {
15
+ $getSelection,
16
+ $isRangeSelection,
17
+ EditorState,
18
+ RootNode,
19
+ } from "lexical"
20
+
21
+ export function MaxLengthPlugin({ maxLength }: { maxLength: number }): null {
22
+ const [editor] = useLexicalComposerContext()
23
+
24
+ useEffect(() => {
25
+ let lastRestoredEditorState: EditorState | null = null
26
+
27
+ return editor.registerNodeTransform(RootNode, (rootNode: RootNode) => {
28
+ const selection = $getSelection()
29
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
30
+ return
31
+ }
32
+ const prevEditorState = editor.getEditorState()
33
+ const prevTextContentSize = prevEditorState.read(() =>
34
+ rootNode.getTextContentSize()
35
+ )
36
+ const textContentSize = rootNode.getTextContentSize()
37
+ if (prevTextContentSize !== textContentSize) {
38
+ const delCount = textContentSize - maxLength
39
+ const anchor = selection.anchor
40
+
41
+ if (delCount > 0) {
42
+ // Restore the old editor state instead if the last
43
+ // text content was already at the limit.
44
+ if (
45
+ prevTextContentSize === maxLength &&
46
+ lastRestoredEditorState !== prevEditorState
47
+ ) {
48
+ lastRestoredEditorState = prevEditorState
49
+ $restoreEditorState(editor, prevEditorState)
50
+ } else {
51
+ $trimTextContentFromAnchor(editor, anchor, delCount)
52
+ }
53
+ }
54
+ }
55
+ })
56
+ }, [editor, maxLength])
57
+
58
+ return null
59
+ }
@@ -0,0 +1,72 @@
1
+ "use client"
2
+
3
+ import { useEffect } from "react"
4
+ import {
5
+ editorStateFromSerializedDocument,
6
+ SerializedDocument,
7
+ serializedDocumentFromEditorState,
8
+ } from "@lexical/file"
9
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
10
+ import { CLEAR_HISTORY_COMMAND } from "lexical"
11
+ import { SendIcon } from "lucide-react"
12
+ import { toast } from "sonner"
13
+
14
+ import {
15
+ docFromHash,
16
+ docToHash,
17
+ } from "../../utils/doc-serialization"
18
+ import { Button } from "../../ui/button"
19
+ import {
20
+ Tooltip,
21
+ TooltipContent,
22
+ TooltipTrigger,
23
+ } from "../../ui/tooltip"
24
+ import { IconSize } from "../../ui/typography"
25
+
26
+ export function ShareContentPlugin() {
27
+ const [editor] = useLexicalComposerContext()
28
+ async function shareDoc(doc: SerializedDocument): Promise<void> {
29
+ const url = new URL(window.location.toString())
30
+ url.hash = await docToHash(doc)
31
+ const newUrl = url.toString()
32
+ window.history.replaceState({}, "", newUrl)
33
+ await window.navigator.clipboard.writeText(newUrl)
34
+ }
35
+ useEffect(() => {
36
+ docFromHash(window.location.hash).then((doc) => {
37
+ if (doc && doc.source === "editor") {
38
+ editor.setEditorState(editorStateFromSerializedDocument(editor, doc))
39
+ editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined)
40
+ }
41
+ })
42
+ }, [editor])
43
+
44
+ return (
45
+ <Tooltip>
46
+ <TooltipTrigger asChild>
47
+ <Button
48
+ variant={"ghost"}
49
+ onClick={() =>
50
+ shareDoc(
51
+ serializedDocumentFromEditorState(editor.getEditorState(), {
52
+ source: "editor",
53
+ })
54
+ ).then(
55
+ () => toast.success("URL copied to clipboard"),
56
+ () => toast.error("URL could not be copied to clipboard")
57
+ )
58
+ }
59
+ title="Share"
60
+ aria-label="Share Playground link to current editor state"
61
+ size={"sm"}
62
+ className="editor-p-2"
63
+ >
64
+ <IconSize size="sm">
65
+ <SendIcon />
66
+ </IconSize>
67
+ </Button>
68
+ </TooltipTrigger>
69
+ <TooltipContent>Share Content</TooltipContent>
70
+ </Tooltip>
71
+ )
72
+ }
@@ -0,0 +1,159 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ */
10
+ import { useEffect, useRef, useState } from "react"
11
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
12
+ import type { LexicalCommand, LexicalEditor, RangeSelection } from "lexical"
13
+ import {
14
+ $getSelection,
15
+ $isRangeSelection,
16
+ COMMAND_PRIORITY_EDITOR,
17
+ createCommand,
18
+ REDO_COMMAND,
19
+ UNDO_COMMAND,
20
+ } from "lexical"
21
+ import { MicIcon } from "lucide-react"
22
+
23
+ import { useReport } from "../../editor-hooks/use-report"
24
+ import { CAN_USE_DOM } from "../../shared/can-use-dom"
25
+ import { Button } from "../../ui/button"
26
+ import {
27
+ Tooltip,
28
+ TooltipContent,
29
+ TooltipTrigger,
30
+ } from "../../ui/tooltip"
31
+ import { IconSize } from "../../ui/typography"
32
+
33
+ export const SPEECH_TO_TEXT_COMMAND: LexicalCommand<boolean> = createCommand(
34
+ "SPEECH_TO_TEXT_COMMAND"
35
+ )
36
+
37
+ const VOICE_COMMANDS: Readonly<
38
+ Record<
39
+ string,
40
+ (arg0: { editor: LexicalEditor; selection: RangeSelection }) => void
41
+ >
42
+ > = {
43
+ "\n": ({ selection }) => {
44
+ selection.insertParagraph()
45
+ },
46
+ redo: ({ editor }) => {
47
+ editor.dispatchCommand(REDO_COMMAND, undefined)
48
+ },
49
+ undo: ({ editor }) => {
50
+ editor.dispatchCommand(UNDO_COMMAND, undefined)
51
+ },
52
+ }
53
+
54
+ export const SUPPORT_SPEECH_RECOGNITION: boolean =
55
+ CAN_USE_DOM &&
56
+ ("SpeechRecognition" in window || "webkitSpeechRecognition" in window)
57
+
58
+ function SpeechToTextPluginImpl() {
59
+ const [editor] = useLexicalComposerContext()
60
+ const [isEnabled, setIsEnabled] = useState<boolean>(false)
61
+ const [isSpeechToText, setIsSpeechToText] = useState<boolean>(false)
62
+ const SpeechRecognition =
63
+ CAN_USE_DOM &&
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ ((window as any).SpeechRecognition || (window as any).webkitSpeechRecognition)
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ const recognition = useRef<any | null>(null)
68
+ const report = useReport()
69
+
70
+ useEffect(() => {
71
+ if (isEnabled && recognition.current === null) {
72
+ recognition.current = new SpeechRecognition()
73
+ recognition.current.continuous = true
74
+ recognition.current.interimResults = true
75
+ recognition.current.addEventListener(
76
+ "result",
77
+ (event: typeof SpeechRecognition) => {
78
+ const resultItem = event.results.item(event.resultIndex)
79
+ const { transcript } = resultItem.item(0)
80
+ report(transcript)
81
+
82
+ if (!resultItem.isFinal) {
83
+ return
84
+ }
85
+
86
+ editor.update(() => {
87
+ const selection = $getSelection()
88
+
89
+ if ($isRangeSelection(selection)) {
90
+ const command = VOICE_COMMANDS[transcript.toLowerCase().trim()]
91
+
92
+ if (command) {
93
+ command({
94
+ editor,
95
+ selection,
96
+ })
97
+ } else if (transcript.match(/\s*\n\s*/)) {
98
+ selection.insertParagraph()
99
+ } else {
100
+ selection.insertText(transcript)
101
+ }
102
+ }
103
+ })
104
+ }
105
+ )
106
+ }
107
+
108
+ if (recognition.current) {
109
+ if (isEnabled) {
110
+ recognition.current.start()
111
+ } else {
112
+ recognition.current.stop()
113
+ }
114
+ }
115
+
116
+ return () => {
117
+ if (recognition.current !== null) {
118
+ recognition.current.stop()
119
+ }
120
+ }
121
+ }, [SpeechRecognition, editor, isEnabled, report])
122
+ useEffect(() => {
123
+ return editor.registerCommand(
124
+ SPEECH_TO_TEXT_COMMAND,
125
+ (_isEnabled: boolean) => {
126
+ setIsEnabled(_isEnabled)
127
+ return true
128
+ },
129
+ COMMAND_PRIORITY_EDITOR
130
+ )
131
+ }, [editor])
132
+
133
+ return (
134
+ <Tooltip>
135
+ <TooltipTrigger asChild>
136
+ <Button
137
+ onClick={() => {
138
+ editor.dispatchCommand(SPEECH_TO_TEXT_COMMAND, !isSpeechToText)
139
+ setIsSpeechToText(!isSpeechToText)
140
+ }}
141
+ variant={isSpeechToText ? "secondary" : "ghost"}
142
+ title="Speech To Text"
143
+ aria-label={`${isSpeechToText ? "Enable" : "Disable"} speech to text`}
144
+ className="editor-p-2"
145
+ size={"sm"}
146
+ >
147
+ <IconSize size="sm">
148
+ <MicIcon />
149
+ </IconSize>
150
+ </Button>
151
+ </TooltipTrigger>
152
+ <TooltipContent>Speech To Text</TooltipContent>
153
+ </Tooltip>
154
+ )
155
+ }
156
+
157
+ export const SpeechToTextPlugin = SUPPORT_SPEECH_RECOGNITION
158
+ ? SpeechToTextPluginImpl
159
+ : () => null
@@ -0,0 +1,63 @@
1
+ "use client"
2
+
3
+ import { JSX } from "react"
4
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
5
+ import { TreeView } from "@lexical/react/LexicalTreeView"
6
+ import { NotebookPenIcon } from "lucide-react"
7
+
8
+ import { Button } from "../../ui/button"
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ DialogTrigger,
16
+ } from "../../ui/dialog"
17
+ import {
18
+ Tooltip,
19
+ TooltipContent,
20
+ TooltipTrigger,
21
+ } from "../../ui/tooltip"
22
+ import { ScrollArea, ScrollBar } from "../../ui/scroll-area"
23
+ import { IconSize } from "../../ui/typography"
24
+
25
+ export function TreeViewPlugin(): JSX.Element {
26
+ const [editor] = useLexicalComposerContext()
27
+ return (
28
+ <Dialog>
29
+ <Tooltip>
30
+ <TooltipTrigger asChild>
31
+ <DialogTrigger asChild>
32
+ <Button size={"sm"} variant={"ghost"} className="editor-p-2">
33
+ <IconSize size="sm">
34
+ <NotebookPenIcon />
35
+ </IconSize>
36
+ </Button>
37
+ </DialogTrigger>
38
+ </TooltipTrigger>
39
+ <TooltipContent>View Tree</TooltipContent>
40
+ </Tooltip>
41
+ <DialogContent disableOutsideClick={true}>
42
+ <DialogHeader>
43
+ <DialogTitle>Tree View</DialogTitle>
44
+ <DialogDescription>
45
+ Xem cấu trúc cây của nội dung editor
46
+ </DialogDescription>
47
+ </DialogHeader>
48
+ <ScrollArea className="editor-tree-view-scroll-area">
49
+ <TreeView
50
+ viewClassName="tree-view-output"
51
+ treeTypeButtonClassName="debug-treetype-button"
52
+ timeTravelPanelClassName="debug-timetravel-panel"
53
+ timeTravelButtonClassName="debug-timetravel-button"
54
+ timeTravelPanelSliderClassName="debug-timetravel-panel-slider"
55
+ timeTravelPanelButtonClassName="debug-timetravel-panel-button"
56
+ editor={editor}
57
+ />
58
+ <ScrollBar />
59
+ </ScrollArea>
60
+ </DialogContent>
61
+ </Dialog>
62
+ )
63
+ }
@@ -0,0 +1,86 @@
1
+ "use client"
2
+
3
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
4
+ import {
5
+ $getSelection,
6
+ $isElementNode,
7
+ $isRangeSelection,
8
+ $isNodeSelection,
9
+ COMMAND_PRIORITY_EDITOR,
10
+ ElementFormatType,
11
+ FORMAT_ELEMENT_COMMAND,
12
+ LexicalNode,
13
+ } from "lexical"
14
+ import { useEffect } from "react"
15
+
16
+ export function AlignPlugin(): null {
17
+ const [editor] = useLexicalComposerContext()
18
+
19
+ useEffect(() => {
20
+ // Ensure theme has textAlign configuration
21
+ // This handles cases where the consumer app's theme might be missing these definitions
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ const config = editor._config as any
24
+ if (config.theme && !config.theme.textAlign) {
25
+ // eslint-disable-next-line react-hooks/immutability
26
+ config.theme.textAlign = {
27
+ left: "editor-text-align-left",
28
+ center: "editor-text-align-center",
29
+ right: "editor-text-align-right",
30
+ justify: "editor-text-align-justify",
31
+ }
32
+ }
33
+
34
+ return editor.registerCommand<ElementFormatType>(
35
+ FORMAT_ELEMENT_COMMAND,
36
+ (formatType) => {
37
+ const selection = $getSelection()
38
+
39
+ if ($isRangeSelection(selection)) {
40
+ const nodes = selection.getNodes()
41
+ const processedBlocks = new Set<string>()
42
+
43
+ nodes.forEach((node) => {
44
+ let block: LexicalNode | null = node
45
+ // Navigate up to find the nearest block element
46
+ if (!$isElementNode(block) || block.isInline()) {
47
+ const parent = block.getParentOrThrow()
48
+ block = parent
49
+ }
50
+
51
+ // Continue navigating up if it's still inline (just in case)
52
+ while (block !== null && (!$isElementNode(block) || block.isInline())) {
53
+ block = block.getParent()
54
+ }
55
+
56
+ if (block && $isElementNode(block) && !processedBlocks.has(block.getKey())) {
57
+ processedBlocks.add(block.getKey())
58
+ block.setFormat(formatType)
59
+ }
60
+ })
61
+ } else if ($isNodeSelection(selection)) {
62
+ const nodes = selection.getNodes()
63
+ const processedBlocks = new Set<string>()
64
+
65
+ nodes.forEach((node) => {
66
+ // For NodeSelection (e.g. ImageNode), find the parent block
67
+ let block = node.getParent()
68
+ while (block !== null && (!$isElementNode(block) || block.isInline())) {
69
+ block = block.getParent()
70
+ }
71
+
72
+ if (block && $isElementNode(block) && !processedBlocks.has(block.getKey())) {
73
+ processedBlocks.add(block.getKey())
74
+ block.setFormat(formatType)
75
+ }
76
+ })
77
+ }
78
+
79
+ return true
80
+ },
81
+ COMMAND_PRIORITY_EDITOR
82
+ )
83
+ }, [editor])
84
+
85
+ return null
86
+ }
@@ -0,0 +1,34 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ */
10
+ import * as React from "react"
11
+ import { JSX } from "react"
12
+ import {
13
+ createLinkMatcherWithRegExp,
14
+ AutoLinkPlugin as LexicalAutoLinkPlugin,
15
+ } from "@lexical/react/LexicalAutoLinkPlugin"
16
+
17
+ const URL_REGEX =
18
+ /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)(?<![-.+():%])/
19
+
20
+ const EMAIL_REGEX =
21
+ /(([^<>()[\]\\.,;:\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,}))/
22
+
23
+ const MATCHERS = [
24
+ createLinkMatcherWithRegExp(URL_REGEX, (text) => {
25
+ return text.startsWith("http") ? text : `https://${text}`
26
+ }),
27
+ createLinkMatcherWithRegExp(EMAIL_REGEX, (text) => {
28
+ return `mailto:${text}`
29
+ }),
30
+ ]
31
+
32
+ export function AutoLinkPlugin(): JSX.Element {
33
+ return <LexicalAutoLinkPlugin matchers={MATCHERS} />
34
+ }