@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.
- package/dist/editor-x/editor.cjs +33121 -0
- package/dist/editor-x/editor.cjs.map +1 -0
- package/dist/editor-x/editor.css +2854 -0
- package/dist/editor-x/editor.css.map +1 -0
- package/dist/editor-x/editor.d.cts +12 -0
- package/dist/editor-x/editor.d.ts +12 -0
- package/dist/editor-x/editor.js +33095 -0
- package/dist/editor-x/editor.js.map +1 -0
- package/dist/index.cjs +33210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2854 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +33183 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
- package/src/components/lexical-editor.tsx +123 -0
- package/src/context/editor-container-context.tsx +29 -0
- package/src/context/priority-image-context.tsx +7 -0
- package/src/context/toolbar-context.tsx +60 -0
- package/src/context/uploads-context.tsx +53 -0
- package/src/editor-hooks/use-debounce.ts +80 -0
- package/src/editor-hooks/use-modal.tsx +64 -0
- package/src/editor-hooks/use-report.ts +57 -0
- package/src/editor-hooks/use-update-toolbar.ts +41 -0
- package/src/editor-ui/broken-image.tsx +18 -0
- package/src/editor-ui/caption-composer.tsx +45 -0
- package/src/editor-ui/code-button.tsx +75 -0
- package/src/editor-ui/color-picker.tsx +2010 -0
- package/src/editor-ui/content-editable.tsx +37 -0
- package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
- package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
- package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
- package/src/editor-ui/image-component.tsx +321 -0
- package/src/editor-ui/image-placeholder.tsx +57 -0
- package/src/editor-ui/image-resizer.tsx +499 -0
- package/src/editor-ui/image-sizing.ts +120 -0
- package/src/editor-ui/lazy-image.tsx +136 -0
- package/src/editor-x/editor.tsx +117 -0
- package/src/editor-x/nodes.ts +79 -0
- package/src/editor-x/plugins.tsx +380 -0
- package/src/hooks/use-click-outside.ts +27 -0
- package/src/hooks/use-element-size.ts +54 -0
- package/src/hooks/use-header-height.ts +95 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
- package/src/index.ts +4 -0
- package/src/lib/logger.ts +6 -0
- package/src/lib/utils.ts +19 -0
- package/src/nodes/autocomplete-node.tsx +94 -0
- package/src/nodes/embeds/tweet-node.tsx +224 -0
- package/src/nodes/embeds/youtube-node.tsx +519 -0
- package/src/nodes/emoji-node.tsx +83 -0
- package/src/nodes/image-node.tsx +328 -0
- package/src/nodes/keyword-node.tsx +58 -0
- package/src/nodes/layout-container-node.tsx +128 -0
- package/src/nodes/layout-item-node.tsx +118 -0
- package/src/nodes/list-with-color-node.tsx +160 -0
- package/src/nodes/mention-node.ts +122 -0
- package/src/plugins/actions/actions-plugin.tsx +3 -0
- package/src/plugins/actions/character-limit-plugin.tsx +27 -0
- package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
- package/src/plugins/actions/counter-character-plugin.tsx +80 -0
- package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
- package/src/plugins/actions/import-export-plugin.tsx +61 -0
- package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
- package/src/plugins/actions/max-length-plugin.tsx +59 -0
- package/src/plugins/actions/share-content-plugin.tsx +72 -0
- package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
- package/src/plugins/actions/tree-view-plugin.tsx +63 -0
- package/src/plugins/align-plugin.tsx +86 -0
- package/src/plugins/auto-link-plugin.tsx +34 -0
- package/src/plugins/autocomplete-plugin.tsx +2574 -0
- package/src/plugins/code-action-menu-plugin.tsx +240 -0
- package/src/plugins/code-highlight-plugin.tsx +22 -0
- package/src/plugins/component-picker-menu-plugin.tsx +427 -0
- package/src/plugins/context-menu-plugin.tsx +311 -0
- package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
- package/src/plugins/draggable-block-plugin.tsx +50 -0
- package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
- package/src/plugins/embeds/twitter-plugin.tsx +45 -0
- package/src/plugins/embeds/youtube-plugin.tsx +84 -0
- package/src/plugins/emoji-picker-plugin.tsx +206 -0
- package/src/plugins/emojis-plugin.tsx +84 -0
- package/src/plugins/floating-link-editor-plugin.tsx +791 -0
- package/src/plugins/floating-text-format-plugin.tsx +710 -0
- package/src/plugins/images-plugin.tsx +671 -0
- package/src/plugins/keywords-plugin.tsx +59 -0
- package/src/plugins/layout-plugin.tsx +658 -0
- package/src/plugins/link-plugin.tsx +18 -0
- package/src/plugins/list-color-plugin.tsx +178 -0
- package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
- package/src/plugins/mentions-plugin.tsx +714 -0
- package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
- package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/code-picker-plugin.tsx +30 -0
- package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
- package/src/plugins/picker/component-picker-option.tsx +47 -0
- package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
- package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
- package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
- package/src/plugins/picker/image-picker-plugin.tsx +16 -0
- package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
- package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
- package/src/plugins/picker/table-picker-plugin.tsx +56 -0
- package/src/plugins/tab-focus-plugin.tsx +66 -0
- package/src/plugins/table-column-resizer-plugin.tsx +309 -0
- package/src/plugins/table-plugin.tsx +299 -0
- package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
- package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
- package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
- package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
- package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
- package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
- package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
- package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
- package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
- package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
- package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
- package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
- package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
- package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
- package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
- package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
- package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
- package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
- package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
- package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
- package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
- package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
- package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
- package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
- package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
- package/src/plugins/typing-pref-plugin.tsx +118 -0
- package/src/shared/can-use-dom.ts +4 -0
- package/src/shared/environment.ts +47 -0
- package/src/shared/invariant.ts +16 -0
- package/src/shared/use-layout-effect.ts +12 -0
- package/src/themes/_mixins.scss +107 -0
- package/src/themes/_variables.scss +33 -0
- package/src/themes/editor-theme.scss +622 -0
- package/src/themes/editor-theme.ts +118 -0
- package/src/themes/plugins.scss +1180 -0
- package/src/themes/ui-components.scss +936 -0
- package/src/transformers/markdown-emoji-transformer.ts +20 -0
- package/src/transformers/markdown-hr-transformer.ts +28 -0
- package/src/transformers/markdown-image-transformer.ts +31 -0
- package/src/transformers/markdown-list-transformer.ts +51 -0
- package/src/transformers/markdown-table-transformer.ts +200 -0
- package/src/transformers/markdown-tweet-transformer.ts +26 -0
- package/src/ui/button-group.tsx +10 -0
- package/src/ui/button.tsx +29 -0
- package/src/ui/collapsible.tsx +67 -0
- package/src/ui/command.tsx +48 -0
- package/src/ui/dialog.tsx +146 -0
- package/src/ui/flex.tsx +38 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +20 -0
- package/src/ui/popover.tsx +128 -0
- package/src/ui/scroll-area.tsx +17 -0
- package/src/ui/select.tsx +171 -0
- package/src/ui/separator.tsx +20 -0
- package/src/ui/slider.tsx +14 -0
- package/src/ui/slot.tsx +3 -0
- package/src/ui/tabs.tsx +87 -0
- package/src/ui/toggle-group.tsx +109 -0
- package/src/ui/toggle.tsx +28 -0
- package/src/ui/tooltip.tsx +28 -0
- package/src/ui/typography.tsx +44 -0
- package/src/utils/doc-serialization.ts +68 -0
- package/src/utils/emoji-list.ts +16604 -0
- package/src/utils/get-dom-range-rect.ts +20 -0
- package/src/utils/get-selected-node.ts +20 -0
- package/src/utils/is-mobile-width.ts +0 -0
- package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
- package/src/utils/set-floating-elem-position.ts +44 -0
- package/src/utils/swipe.ts +119 -0
- 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
|
+
}
|